Skip to main content

css_ast/
constraints.rs

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