Skip to main content

css_ast/
constraints.rs

1use crate::diagnostics::CssDiagnostic;
2use css_parse::{Cursor, Diagnostic, KindSet, Parse, Parser, Peek, Result, ToNormalisedValue, ToNumberValue};
3use csskit_derives::*;
4use csskit_proc_macro::node;
5
6/// A non-negative value wrapper.
7///
8/// This wrapper validates that literal values are >= 0 at parse time.
9#[node]
10#[derive(Peek, IntoCursor, ToCursors, ToSpan, SemanticEq, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(transparent))]
12#[cfg_attr(feature = "visitable", derive(Visitable), visit(children))]
13#[derive(NodeWithMetadata)]
14pub struct NonNegative<T>(pub T);
15
16impl<'a, T: Parse<'a> + ToNumberValue> Parse<'a> for NonNegative<T> {
17	fn parse<I>(p: &mut Parser<'a, I>) -> Result<Self>
18	where
19		I: Iterator<Item = Cursor> + Clone,
20	{
21		let cursor = p.peek_n(1);
22		let value = p.parse::<T>()?;
23		if let Some(num) = value.to_number_value()
24			&& num < 0.0
25		{
26			Err(Diagnostic::new(cursor, Diagnostic::non_negative))?;
27		}
28
29		Ok(Self(value))
30	}
31}
32
33impl<T> NonNegative<T> {
34	/// Returns a reference to the inner value.
35	pub fn inner(&self) -> &T {
36		&self.0
37	}
38
39	/// Consumes self and returns the inner value.
40	pub fn into_inner(self) -> T {
41		self.0
42	}
43}
44
45/// A positive value wrapper.
46///
47/// This wrapper validates that literal values are > 0 at parse time.
48#[node]
49#[derive(IntoCursor, Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
50#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(transparent))]
51#[cfg_attr(feature = "visitable", derive(Visitable), visit(children))]
52#[derive(NodeWithMetadata)]
53pub struct Positive<T>(pub T);
54
55impl<'a, T: Parse<'a> + ToNumberValue> Parse<'a> for Positive<T> {
56	fn parse<I>(p: &mut Parser<'a, I>) -> Result<Self>
57	where
58		I: Iterator<Item = Cursor> + Clone,
59	{
60		let cursor = p.peek_n(1);
61		let value = p.parse::<T>()?;
62
63		if let Some(num) = value.to_number_value()
64			&& num <= 0.0
65		{
66			Err(Diagnostic::new(cursor, Diagnostic::positive))?;
67		}
68
69		Ok(Self(value))
70	}
71}
72
73impl<T> Positive<T> {
74	/// Returns a reference to the inner value.
75	pub fn inner(&self) -> &T {
76		&self.0
77	}
78
79	/// Consumes self and returns the inner value.
80	pub fn into_inner(self) -> T {
81		self.0
82	}
83}
84
85/// A non-zero value wrapper.
86///
87/// This wrapper validates that literal values are != 0 at parse time.
88#[node]
89#[derive(IntoCursor, Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
90#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(transparent))]
91#[cfg_attr(feature = "visitable", derive(Visitable), visit(children))]
92#[derive(NodeWithMetadata)]
93pub struct NonZero<T>(pub T);
94
95impl<'a, T: Parse<'a> + ToNumberValue> Parse<'a> for NonZero<T> {
96	fn parse<I>(p: &mut Parser<'a, I>) -> Result<Self>
97	where
98		I: Iterator<Item = Cursor> + Clone,
99	{
100		let cursor = p.peek_n(1);
101		let value = p.parse::<T>()?;
102
103		if let Some(num) = value.to_number_value()
104			&& num == 0.0
105		{
106			Err(Diagnostic::new(cursor, <Diagnostic as CssDiagnostic>::unexpected_zero))?;
107		}
108
109		Ok(Self(value))
110	}
111}
112
113impl<T> NonZero<T> {
114	/// Returns a reference to the inner value.
115	pub fn inner(&self) -> &T {
116		&self.0
117	}
118
119	/// Consumes self and returns the inner value.
120	pub fn into_inner(self) -> T {
121		self.0
122	}
123}
124
125/// A range-constrained value wrapper using const generics.
126///
127/// This wrapper validates that literal values fall within [MIN, MAX] at parse time.
128#[node]
129#[derive(ToSpan, IntoCursor, ToCursors, SemanticEq, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
130#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(transparent))]
131#[cfg_attr(feature = "visitable", derive(Visitable), visit(children))]
132#[derive(NodeWithMetadata)]
133pub struct Ranged<T, const MIN: i32, const MAX: i32>(pub T);
134
135impl<'a, T: Peek<'a>, const MIN: i32, const MAX: i32> Peek<'a> for Ranged<T, MIN, MAX> {
136	const PEEK_KINDSET: KindSet = T::PEEK_KINDSET;
137
138	fn peek<I>(p: &Parser<'a, I>, c: Cursor) -> bool
139	where
140		I: Iterator<Item = Cursor> + Clone,
141	{
142		if !T::peek(p, c) {
143			return false;
144		}
145		let kind = c.token().kind();
146		if kind == css_lexer::Kind::Number || kind == css_lexer::Kind::Dimension {
147			let num = c.token().value();
148			num >= MIN as f32 && num <= MAX as f32
149		} else {
150			true
151		}
152	}
153}
154
155impl<'a, T: Parse<'a> + ToNormalisedValue, const MIN: i32, const MAX: i32> Parse<'a> for Ranged<T, MIN, MAX> {
156	fn parse<I>(p: &mut Parser<'a, I>) -> Result<Self>
157	where
158		I: Iterator<Item = Cursor> + Clone,
159	{
160		let cursor = p.peek_n(1);
161		let value = p.parse::<T>()?;
162
163		if let Some(num) = value.to_normalised_value()
164			&& (num < MIN as f32 || num > MAX as f32)
165		{
166			Err(Diagnostic::new(cursor, Diagnostic::number_out_of_bounds))?;
167		}
168
169		Ok(Self(value))
170	}
171}
172
173impl<T: ToNumberValue, const MIN: i32, const MAX: i32> ToNumberValue for Ranged<T, MIN, MAX> {
174	fn to_number_value(&self) -> Option<f32> {
175		self.0.to_number_value()
176	}
177}
178
179impl<T, const MIN: i32, const MAX: i32> Ranged<T, MIN, MAX> {
180	/// Returns a reference to the inner value.
181	pub fn inner(&self) -> &T {
182		&self.0
183	}
184
185	/// Consumes self and returns the inner value.
186	pub fn into_inner(self) -> T {
187		self.0
188	}
189}
190
191/// An exact value wrapper using const generics.
192///
193/// This wrapper validates that literal values are exactly equal to the specified VALUE at parse time.
194#[node]
195#[derive(IntoCursor, ToCursors, ToSpan, SemanticEq, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
196#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(transparent))]
197#[cfg_attr(feature = "visitable", derive(Visitable), visit(children))]
198#[derive(NodeWithMetadata)]
199pub struct Exact<T, const VALUE: i32>(pub T);
200
201impl<'a, T: Peek<'a>, const VALUE: i32> Peek<'a> for Exact<T, VALUE> {
202	const PEEK_KINDSET: KindSet = T::PEEK_KINDSET;
203
204	fn peek<I>(p: &Parser<'a, I>, c: Cursor) -> bool
205	where
206		I: Iterator<Item = Cursor> + Clone,
207	{
208		if !T::peek(p, c) {
209			return false;
210		}
211		let kind = c.token().kind();
212		if kind == css_lexer::Kind::Number || kind == css_lexer::Kind::Dimension {
213			c.token().value() == VALUE as f32
214		} else {
215			true
216		}
217	}
218}
219
220impl<'a, T: Parse<'a> + ToNumberValue, const VALUE: i32> Parse<'a> for Exact<T, VALUE> {
221	fn parse<I>(p: &mut Parser<'a, I>) -> Result<Self>
222	where
223		I: Iterator<Item = Cursor> + Clone,
224	{
225		let cursor = p.peek_n(1);
226		let value = p.parse::<T>()?;
227
228		if let Some(num) = value.to_number_value()
229			&& num != VALUE as f32
230		{
231			Err(Diagnostic::new(cursor, Diagnostic::number_out_of_bounds))?;
232		}
233
234		Ok(Self(value))
235	}
236}
237
238impl<T: ToNumberValue, const VALUE: i32> ToNumberValue for Exact<T, VALUE> {
239	fn to_number_value(&self) -> Option<f32> {
240		self.0.to_number_value()
241	}
242}
243
244impl<T, const VALUE: i32> Exact<T, VALUE> {
245	/// Returns a reference to the inner value.
246	pub fn inner(&self) -> &T {
247		&self.0
248	}
249
250	/// Consumes self and returns the inner value.
251	pub fn into_inner(self) -> T {
252		self.0
253	}
254}
255
256/// A non-empty collection wrapper.
257///
258/// Wraps any collection type that derefs to a slice (`Deref<Target = [_]>`)
259/// and validates at parse time that the collection contains at least one item.
260///
261/// Works with [`css_parse::Vec`] and any other slice-backed type.
262#[node]
263#[derive(Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
264#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(transparent))]
265#[cfg_attr(feature = "visitable", derive(Visitable), visit(children))]
266#[derive(NodeWithMetadata)]
267pub struct NonEmpty<T>(pub T);
268
269impl<'a, T, Item> Parse<'a> for NonEmpty<T>
270where
271	T: Peek<'a> + Parse<'a> + std::ops::Deref<Target = [Item]>,
272{
273	fn parse<I>(p: &mut Parser<'a, I>) -> Result<Self>
274	where
275		I: Iterator<Item = Cursor> + Clone,
276	{
277		let cursor = p.peek_n(1);
278		let value = p.parse::<T>()?;
279		if value.is_empty() {
280			Err(Diagnostic::new(cursor, <Diagnostic as CssDiagnostic>::empty_collection))?;
281		}
282		Ok(Self(value))
283	}
284}
285
286impl<T> NonEmpty<T> {
287	pub fn inner(&self) -> &T {
288		&self.0
289	}
290
291	pub fn into_inner(self) -> T {
292		self.0
293	}
294}
295
296#[cfg(test)]
297mod tests {
298	use super::*;
299	use crate::CssAtomSet;
300	use css_parse::Vec;
301	use css_parse::{T, assert_parse, assert_parse_error, assert_peek_false};
302
303	type ExactOne = Exact<T![Number], 1>;
304	type RangedZeroOne = Ranged<T![Number], 0, 1>;
305
306	#[test]
307	fn test_exact_accepts_correct_value() {
308		assert_parse!(CssAtomSet::ATOMS, ExactOne, "1");
309	}
310
311	#[test]
312	fn test_exact_rejects_wrong_value() {
313		assert_peek_false!(CssAtomSet::ATOMS, ExactOne, "2");
314	}
315
316	#[test]
317	fn test_ranged_accepts_within_range() {
318		assert_parse!(CssAtomSet::ATOMS, RangedZeroOne, "0.5");
319	}
320
321	#[test]
322	fn test_ranged_rejects_out_of_range() {
323		assert_peek_false!(CssAtomSet::ATOMS, RangedZeroOne, "1.5");
324	}
325
326	#[test]
327	fn test_non_negative_accepts_zero() {
328		assert_parse!(CssAtomSet::ATOMS, NonNegative<T![Number]>, "0");
329	}
330
331	#[test]
332	fn test_non_negative_rejects_negative() {
333		assert_parse_error!(CssAtomSet::ATOMS, NonNegative<T![Number]>, "-1");
334	}
335
336	#[test]
337	fn test_positive_accepts_positive() {
338		assert_parse!(CssAtomSet::ATOMS, Positive<T![Number]>, "1");
339	}
340
341	#[test]
342	fn test_positive_rejects_zero() {
343		assert_parse_error!(CssAtomSet::ATOMS, Positive<T![Number]>, "0");
344	}
345
346	#[test]
347	fn test_non_zero_accepts_positive() {
348		assert_parse!(CssAtomSet::ATOMS, NonZero<T![Number]>, "1");
349	}
350
351	#[test]
352	fn test_non_zero_accepts_negative() {
353		assert_parse!(CssAtomSet::ATOMS, NonZero<T![Number]>, "-1");
354	}
355
356	#[test]
357	fn test_non_zero_rejects_zero() {
358		assert_parse_error!(CssAtomSet::ATOMS, NonZero<T![Number]>, "0");
359	}
360
361	#[test]
362	fn test_non_empty_accepts_one() {
363		assert_parse!(CssAtomSet::ATOMS, NonEmpty<Vec<T![Ident]>>, "foo");
364	}
365
366	#[test]
367	fn test_non_empty_accepts_multiple() {
368		assert_parse!(CssAtomSet::ATOMS, NonEmpty<Vec<T![Ident]>>, "foo bar");
369	}
370
371	#[test]
372	fn test_non_empty_rejects_empty() {
373		assert_peek_false!(CssAtomSet::ATOMS, NonEmpty<Vec<T![Ident]>>, "");
374	}
375}