1use std::collections::{HashMap, HashSet};
7use thiserror::Error;
8use crate::custom_variant::CustomVariantManager;
9
10#[derive(Debug, Error)]
12pub enum ValidationError {
13 #[error("Invalid class name: {0}")]
14 InvalidClass(String),
15
16 #[error("Class conflict: {0} conflicts with {1}")]
17 ClassConflict(String, String),
18
19 #[error("Deprecated class: {0}")]
20 DeprecatedClass(String),
21
22 #[error("Unsupported class: {0}")]
23 UnsupportedClass(String),
24
25 #[error("Invalid custom variant: {0}")]
26 InvalidCustomVariant(String),
27
28 #[error("Custom variant validation failed: {0}")]
29 CustomVariantValidation(String),
30}
31
32#[derive(Debug, Clone)]
34pub struct ValidationRules {
35 pub allowed_patterns: Vec<regex::Regex>,
37 pub forbidden_patterns: Vec<regex::Regex>,
39 pub deprecated_classes: HashSet<String>,
41 pub class_conflicts: HashMap<String, HashSet<String>>,
43 pub required_classes: HashMap<String, HashSet<String>>,
45}
46
47impl Default for ValidationRules {
48 fn default() -> Self {
49 Self::new()
50 }
51}
52
53impl ValidationRules {
54 pub fn new() -> Self {
56 let mut rules = Self {
57 allowed_patterns: Vec::new(),
58 forbidden_patterns: Vec::new(),
59 deprecated_classes: HashSet::new(),
60 class_conflicts: HashMap::new(),
61 required_classes: HashMap::new(),
62 };
63
64 rules.add_allowed_patterns();
66 rules.add_class_conflicts();
67 rules.add_required_classes();
68
69 rules
70 }
71
72 fn add_allowed_patterns(&mut self) {
74 let patterns = vec![
75 r"^(block|inline-block|inline|flex|inline-flex|table|inline-table|table-caption|table-cell|table-column|table-column-group|table-footer-group|table-header-group|table-row-group|table-row|flow-root|grid|inline-grid|contents|list-item|hidden)$",
77 r"^(flex-row|flex-row-reverse|flex-col|flex-col-reverse|flex-wrap|flex-wrap-reverse|flex-nowrap|items-start|items-end|items-center|items-baseline|items-stretch|justify-start|justify-end|justify-center|justify-between|justify-around|justify-evenly|justify-stretch|content-start|content-end|content-center|content-between|content-around|content-evenly|content-stretch|self-auto|self-start|self-end|self-center|self-stretch|self-baseline)$",
79 r"^(grid-cols-\d+|grid-cols-none|grid-cols-subgrid|col-auto|col-span-\d+|col-start-\d+|col-end-\d+|col-start-auto|col-end-auto|grid-rows-\d+|grid-rows-none|grid-rows-subgrid|row-auto|row-span-\d+|row-start-\d+|row-end-\d+|row-start-auto|row-end-auto|auto-cols-auto|auto-cols-min|auto-cols-max|auto-cols-fr|auto-rows-auto|auto-rows-min|auto-rows-max|auto-rows-fr|gap-\d+|gap-x-\d+|gap-y-\d+|justify-items-start|justify-items-end|justify-items-center|justify-items-stretch|justify-self-auto|justify-self-start|justify-self-end|justify-self-center|justify-self-stretch|place-content-start|place-content-end|place-content-center|place-content-between|place-content-around|place-content-evenly|place-content-stretch|place-items-start|place-items-end|place-items-center|place-items-stretch|place-self-auto|place-self-start|place-self-end|place-self-center|place-self-stretch)$",
81 r"^(p-\d+|pt-\d+|pr-\d+|pb-\d+|pl-\d+|px-\d+|py-\d+|m-\d+|mt-\d+|mr-\d+|mb-\d+|ml-\d+|mx-\d+|my-\d+|space-x-\d+|space-y-\d+|space-x-reverse|space-y-reverse)$",
83 r"^(w-\d+|w-auto|w-px|w-0\.5|w-1\.5|w-2\.5|w-3\.5|w-1\/2|w-1\/3|w-2\/3|w-1\/4|w-2\/4|w-3\/4|w-1\/5|w-2\/5|w-3\/5|w-4\/5|w-1\/6|w-2\/6|w-3\/6|w-4\/6|w-5\/6|w-1\/12|w-2\/12|w-3\/12|w-4\/12|w-5\/12|w-6\/12|w-7\/12|w-8\/12|w-9\/12|w-10\/12|w-11\/12|w-full|w-screen|w-min|w-max|w-fit|h-\d+|h-auto|h-px|h-0\.5|h-1\.5|h-2\.5|h-3\.5|h-1\/2|h-1\/3|h-2\/3|h-1\/4|h-2\/4|h-3\/4|h-1\/5|h-2\/5|h-3\/5|h-4\/5|h-1\/6|h-2\/6|h-3\/6|h-4\/6|h-5\/6|h-1\/12|h-2\/12|h-3\/12|h-4\/12|h-5\/12|h-6\/12|h-7\/12|h-8\/12|h-9\/12|h-10\/12|h-11\/12|h-full|h-screen|h-min|h-max|h-fit|min-w-0|min-w-full|min-w-min|min-w-max|min-w-fit|max-w-0|max-w-none|max-w-xs|max-w-sm|max-w-md|max-w-lg|max-w-xl|max-w-2xl|max-w-3xl|max-w-4xl|max-w-5xl|max-w-6xl|max-w-7xl|max-w-full|max-w-min|max-w-max|max-w-fit|max-w-prose|max-w-screen-sm|max-w-screen-md|max-w-screen-lg|max-w-screen-xl|max-w-screen-2xl|min-h-0|min-h-full|min-h-screen|min-h-min|min-h-max|min-h-fit|max-h-0|max-h-px|max-h-0\.5|max-h-1|max-h-1\.5|max-h-2|max-h-2\.5|max-h-3|max-h-3\.5|max-h-4|max-h-5|max-h-6|max-h-7|max-h-8|max-h-9|max-h-10|max-h-11|max-h-12|max-h-14|max-h-16|max-h-20|max-h-24|max-h-28|max-h-32|max-h-36|max-h-40|max-h-44|max-h-48|max-h-52|max-h-56|max-h-60|max-h-64|max-h-72|max-h-80|max-h-96|max-h-px|max-h-0\.5|max-h-1|max-h-1\.5|max-h-2|max-h-2\.5|max-h-3|max-h-3\.5|max-h-4|max-h-5|max-h-6|max-h-7|max-h-8|max-h-9|max-h-10|max-h-11|max-h-12|max-h-14|max-h-16|max-h-20|max-h-24|max-h-28|max-h-32|max-h-36|max-h-40|max-h-44|max-h-48|max-h-52|max-h-56|max-h-60|max-h-64|max-h-72|max-h-80|max-h-96|max-h-full|max-h-screen)$",
85 r"^(text-xs|text-sm|text-base|text-lg|text-xl|text-2xl|text-3xl|text-4xl|text-5xl|text-6xl|text-7xl|text-8xl|text-9xl|font-thin|font-extralight|font-light|font-normal|font-medium|font-semibold|font-bold|font-extrabold|font-black|italic|not-italic|leading-3|leading-4|leading-5|leading-6|leading-7|leading-8|leading-9|leading-10|leading-none|leading-tight|leading-snug|leading-normal|leading-relaxed|leading-loose|tracking-tighter|tracking-tight|tracking-normal|tracking-wide|tracking-wider|tracking-widest|text-left|text-center|text-right|text-justify|text-start|text-end|text-inherit|text-current|text-transparent|text-black|text-white|text-slate-\d+|text-gray-\d+|text-zinc-\d+|text-neutral-\d+|text-stone-\d+|text-red-\d+|text-orange-\d+|text-amber-\d+|text-yellow-\d+|text-lime-\d+|text-green-\d+|text-emerald-\d+|text-teal-\d+|text-cyan-\d+|text-sky-\d+|text-blue-\d+|text-indigo-\d+|text-violet-\d+|text-purple-\d+|text-fuchsia-\d+|text-pink-\d+|text-rose-\d+)$",
87 r"^(bg-inherit|bg-current|bg-transparent|bg-black|bg-white|bg-slate-\d+|bg-gray-\d+|bg-zinc-\d+|bg-neutral-\d+|bg-stone-\d+|bg-red-\d+|bg-orange-\d+|bg-amber-\d+|bg-yellow-\d+|bg-lime-\d+|bg-green-\d+|bg-emerald-\d+|bg-teal-\d+|bg-cyan-\d+|bg-sky-\d+|bg-blue-\d+|bg-indigo-\d+|bg-violet-\d+|bg-purple-\d+|bg-fuchsia-\d+|bg-pink-\d+|bg-rose-\d+)$",
89 r"^(border-0|border-2|border-4|border-8|border|border-t-0|border-t-2|border-t-4|border-t-8|border-t|border-r-0|border-r-2|border-r-4|border-r-8|border-r|border-b-0|border-b-2|border-b-4|border-b-8|border-b|border-l-0|border-l-2|border-l-4|border-l-8|border-l|border-x-0|border-x-2|border-x-4|border-x-8|border-x|border-y-0|border-y-2|border-y-4|border-y-8|border-y|border-solid|border-dashed|border-dotted|border-double|border-none|border-inherit|border-current|border-transparent|border-black|border-white|border-slate-\d+|border-gray-\d+|border-zinc-\d+|border-neutral-\d+|border-stone-\d+|border-red-\d+|border-orange-\d+|border-amber-\d+|border-yellow-\d+|border-lime-\d+|border-green-\d+|border-emerald-\d+|border-teal-\d+|border-cyan-\d+|border-sky-\d+|border-blue-\d+|border-indigo-\d+|border-violet-\d+|border-purple-\d+|border-fuchsia-\d+|border-pink-\d+|border-rose-\d+|rounded-none|rounded-sm|rounded|rounded-md|rounded-lg|rounded-xl|rounded-2xl|rounded-3xl|rounded-full|rounded-t-none|rounded-t-sm|rounded-t|rounded-t-md|rounded-t-lg|rounded-t-xl|rounded-t-2xl|rounded-t-3xl|rounded-t-full|rounded-r-none|rounded-r-sm|rounded-r|rounded-r-md|rounded-r-lg|rounded-r-xl|rounded-r-2xl|rounded-r-3xl|rounded-r-full|rounded-b-none|rounded-b-sm|rounded-b|rounded-b-md|rounded-b-lg|rounded-b-xl|rounded-b-2xl|rounded-b-3xl|rounded-b-full|rounded-l-none|rounded-l-sm|rounded-l|rounded-l-md|rounded-l-lg|rounded-l-xl|rounded-l-2xl|rounded-l-3xl|rounded-l-full|rounded-tl-none|rounded-tl-sm|rounded-tl|rounded-tl-md|rounded-tl-lg|rounded-tl-xl|rounded-tl-2xl|rounded-tl-3xl|rounded-tl-full|rounded-tr-none|rounded-tr-sm|rounded-tr|rounded-tr-md|rounded-tr-lg|rounded-tr-xl|rounded-tr-2xl|rounded-tr-3xl|rounded-tr-full|rounded-br-none|rounded-br-sm|rounded-br|rounded-br-md|rounded-br-lg|rounded-br-xl|rounded-br-2xl|rounded-br-3xl|rounded-br-full|rounded-bl-none|rounded-bl-sm|rounded-bl|rounded-bl-md|rounded-bl-lg|rounded-bl-xl|rounded-bl-2xl|rounded-bl-3xl|rounded-bl-full)$",
91 r"^(shadow-sm|shadow|shadow-md|shadow-lg|shadow-xl|shadow-2xl|shadow-inner|shadow-none|shadow-inherit|shadow-current|shadow-transparent|shadow-black|shadow-white|shadow-slate-\d+|shadow-gray-\d+|shadow-zinc-\d+|shadow-neutral-\d+|shadow-stone-\d+|shadow-red-\d+|shadow-orange-\d+|shadow-amber-\d+|shadow-yellow-\d+|shadow-lime-\d+|shadow-green-\d+|shadow-emerald-\d+|shadow-teal-\d+|shadow-cyan-\d+|shadow-sky-\d+|shadow-blue-\d+|shadow-indigo-\d+|shadow-violet-\d+|shadow-purple-\d+|shadow-fuchsia-\d+|shadow-pink-\d+|shadow-rose-\d+|opacity-0|opacity-5|opacity-10|opacity-20|opacity-25|opacity-30|opacity-40|opacity-50|opacity-60|opacity-70|opacity-75|opacity-80|opacity-90|opacity-95|opacity-100)$",
93 r"^(transform|transform-gpu|transform-none|origin-center|origin-top|origin-top-right|origin-right|origin-bottom-right|origin-bottom|origin-bottom-left|origin-left|origin-top-left|scale-0|scale-50|scale-75|scale-90|scale-95|scale-100|scale-105|scale-110|scale-125|scale-150|scale-x-0|scale-x-50|scale-x-75|scale-x-90|scale-x-95|scale-x-100|scale-x-105|scale-x-110|scale-x-125|scale-x-150|scale-y-0|scale-y-50|scale-y-75|scale-y-90|scale-y-95|scale-y-100|scale-y-105|scale-y-110|scale-y-125|scale-y-150|rotate-0|rotate-1|rotate-2|rotate-3|rotate-6|rotate-12|rotate-45|rotate-90|rotate-180|-rotate-180|-rotate-90|-rotate-45|-rotate-12|-rotate-6|-rotate-3|-rotate-2|-rotate-1|translate-x-0|translate-x-px|translate-x-0\.5|translate-x-1|translate-x-1\.5|translate-x-2|translate-x-2\.5|translate-x-3|translate-x-3\.5|translate-x-4|translate-x-5|translate-x-6|translate-x-7|translate-x-8|translate-x-9|translate-x-10|translate-x-11|translate-x-12|translate-x-14|translate-x-16|translate-x-20|translate-x-24|translate-x-28|translate-x-32|translate-x-36|translate-x-40|translate-x-44|translate-x-48|translate-x-52|translate-x-56|translate-x-60|translate-x-64|translate-x-72|translate-x-80|translate-x-96|translate-x-1\/2|translate-x-1\/3|translate-x-2\/3|translate-x-1\/4|translate-x-2\/4|translate-x-3\/4|translate-x-full|-translate-x-0|-translate-x-px|-translate-x-0\.5|-translate-x-1|-translate-x-1\.5|-translate-x-2|-translate-x-2\.5|-translate-x-3|-translate-x-3\.5|-translate-x-4|-translate-x-5|-translate-x-6|-translate-x-7|-translate-x-8|-translate-x-9|-translate-x-10|-translate-x-11|-translate-x-12|-translate-x-14|-translate-x-16|-translate-x-20|-translate-x-24|-translate-x-28|-translate-x-32|-translate-x-36|-translate-x-40|-translate-x-44|-translate-x-48|-translate-x-52|-translate-x-56|-translate-x-60|-translate-x-64|-translate-x-72|-translate-x-80|-translate-x-96|-translate-x-1\/2|-translate-x-1\/3|-translate-x-2\/3|-translate-x-1\/4|-translate-x-2\/4|-translate-x-3\/4|-translate-x-full|translate-y-0|translate-y-px|translate-y-0\.5|translate-y-1|translate-y-1\.5|translate-y-2|translate-y-2\.5|translate-y-3|translate-y-3\.5|translate-y-4|translate-y-5|translate-y-6|translate-y-7|translate-y-8|translate-y-9|translate-y-10|translate-y-11|translate-y-12|translate-y-14|translate-y-16|translate-y-20|translate-y-24|translate-y-28|translate-y-32|translate-y-36|translate-y-40|translate-y-44|translate-y-48|translate-y-52|translate-y-56|translate-y-60|translate-y-64|translate-y-72|translate-y-80|translate-y-96|translate-y-1\/2|translate-y-1\/3|translate-y-2\/3|translate-y-1\/4|translate-y-2\/4|translate-y-3\/4|translate-y-full|-translate-y-0|-translate-y-px|-translate-y-0\.5|-translate-y-1|-translate-y-1\.5|-translate-y-2|-translate-y-2\.5|-translate-y-3|-translate-y-3\.5|-translate-y-4|-translate-y-5|-translate-y-6|-translate-y-7|-translate-y-8|-translate-y-9|-translate-y-10|-translate-y-11|-translate-y-12|-translate-y-14|-translate-y-16|-translate-y-20|-translate-y-24|-translate-y-28|-translate-y-32|-translate-y-36|-translate-y-40|-translate-y-44|-translate-y-48|-translate-y-52|-translate-y-56|-translate-y-60|-translate-y-64|-translate-y-72|-translate-y-80|-translate-y-96|-translate-y-1\/2|-translate-y-1\/3|-translate-y-2\/3|-translate-y-1\/4|-translate-y-2\/4|-translate-y-3\/4|-translate-y-full|skew-x-0|skew-x-1|skew-x-2|skew-x-3|skew-x-6|skew-x-12|-skew-x-12|-skew-x-6|-skew-x-3|-skew-x-2|-skew-x-1|skew-y-0|skew-y-1|skew-y-2|skew-y-3|skew-y-6|skew-y-12|-skew-y-12|-skew-y-6|-skew-y-3|-skew-y-2|-skew-y-1)$",
95 r"^(appearance-none|appearance-auto|cursor-auto|cursor-default|cursor-pointer|cursor-wait|cursor-text|cursor-move|cursor-help|cursor-not-allowed|pointer-events-none|pointer-events-auto|resize-none|resize-y|resize-x|resize|select-none|select-text|select-all|select-auto)$",
97 r"^(fill-current|stroke-current|stroke-0|stroke-1|stroke-2)$",
99 r"^(sr-only|not-sr-only|focus-within|focus-visible|focus|focus:outline-none|focus:outline|focus:outline-2|focus:outline-4|focus:outline-8|focus:outline-offset-0|focus:outline-offset-1|focus:outline-offset-2|focus:outline-offset-4|focus:outline-offset-8)$",
101 r"^(sm:|md:|lg:|xl:|2xl:).*$",
103 r"^(hover:|focus:|active:|visited:|disabled:|checked:|indeterminate:|required:|valid:|invalid:|in-range:|out-of-range:|read-only:|read-write:|optional:|placeholder-shown:|autofill:|default:|first-child:|last-child:|only-child:|first-of-type:|last-of-type:|only-of-type:|empty:|target:|root:|not:|where:|is:|has:|before:|after:|first-letter:|first-line:|selection:|marker:|placeholder:|file:|backdrop:|any-link:|link:|local-link:|scope:|current:|past:|future:|playing:|paused:|seeking:|buffering:|stalled:|muted:|volume-locked:|user-invalid:|user-valid:|modal:|picture-in-picture:|fullscreen:|resize:|scroll:|snap:|touch:|user-select:|will-change:|accent-color:|appearance:|cursor:|outline:).*$",
105 ];
106
107 for pattern in patterns {
108 if let Ok(regex) = regex::Regex::new(pattern) {
109 self.allowed_patterns.push(regex);
110 }
111 }
112 }
113
114 fn add_class_conflicts(&mut self) {
116 let mut display_conflicts = HashSet::new();
118 display_conflicts.insert("block".to_string());
119 display_conflicts.insert("inline-block".to_string());
120 display_conflicts.insert("inline".to_string());
121 display_conflicts.insert("flex".to_string());
122 display_conflicts.insert("inline-flex".to_string());
123 display_conflicts.insert("grid".to_string());
124 display_conflicts.insert("inline-grid".to_string());
125 display_conflicts.insert("hidden".to_string());
126 self.class_conflicts
127 .insert("display".to_string(), display_conflicts);
128
129 let mut position_conflicts = HashSet::new();
131 position_conflicts.insert("static".to_string());
132 position_conflicts.insert("fixed".to_string());
133 position_conflicts.insert("absolute".to_string());
134 position_conflicts.insert("relative".to_string());
135 position_conflicts.insert("sticky".to_string());
136 self.class_conflicts
137 .insert("position".to_string(), position_conflicts);
138 }
139
140 fn add_required_classes(&mut self) {
142 let mut grid_required = HashSet::new();
144 grid_required.insert("grid".to_string());
145 self.required_classes
146 .insert("grid-cols-".to_string(), grid_required.clone());
147 self.required_classes
148 .insert("grid-rows-".to_string(), grid_required.clone());
149 self.required_classes
150 .insert("gap-".to_string(), grid_required);
151
152 let mut flex_required = HashSet::new();
154 flex_required.insert("flex".to_string());
155 self.required_classes
156 .insert("flex-".to_string(), flex_required.clone());
157 self.required_classes
158 .insert("items-".to_string(), flex_required.clone());
159 self.required_classes
160 .insert("justify-".to_string(), flex_required);
161 }
162
163 pub fn add_allowed_pattern(&mut self, pattern: regex::Regex) {
165 self.allowed_patterns.push(pattern);
166 }
167
168 pub fn add_forbidden_pattern(&mut self, pattern: regex::Regex) {
170 self.forbidden_patterns.push(pattern);
171 }
172
173 pub fn add_deprecated_class(&mut self, class: String) {
175 self.deprecated_classes.insert(class);
176 }
177
178 pub fn add_class_conflict(&mut self, group: String, class: String) {
180 self.class_conflicts
181 .entry(group)
182 .or_default()
183 .insert(class);
184 }
185
186 pub fn add_required_class(&mut self, class: String, required: String) {
188 self.required_classes
189 .entry(class)
190 .or_default()
191 .insert(required);
192 }
193}
194
195pub struct ErrorReporter {
197 pub enabled: bool,
199 #[allow(clippy::type_complexity)]
201 pub callback: Option<Box<dyn Fn(&ValidationError) + Send + Sync>>,
202}
203
204impl Default for ErrorReporter {
205 fn default() -> Self {
206 Self::new()
207 }
208}
209
210impl ErrorReporter {
211 pub fn new() -> Self {
213 Self {
214 enabled: true,
215 callback: None,
216 }
217 }
218
219 pub fn set_callback<F>(&mut self, callback: F)
221 where
222 F: Fn(&ValidationError) + Send + Sync + 'static,
223 {
224 self.callback = Some(Box::new(callback));
225 }
226
227 pub fn report(&self, error: &ValidationError) {
229 if self.enabled && let Some(ref callback) = self.callback {
230 callback(error);
231 }
232 }
233}
234
235pub struct ClassValidator {
237 #[allow(dead_code)]
238 valid_classes: HashSet<String>,
239 validation_rules: ValidationRules,
240 error_reporter: ErrorReporter,
241 custom_variant_manager: CustomVariantManager,
242}
243
244impl Default for ClassValidator {
245 fn default() -> Self {
246 Self::new()
247 }
248}
249
250impl ClassValidator {
251 pub fn new() -> Self {
253 Self {
254 valid_classes: HashSet::new(),
255 validation_rules: ValidationRules::new(),
256 error_reporter: ErrorReporter::new(),
257 custom_variant_manager: CustomVariantManager::with_defaults(),
258 }
259 }
260
261 pub fn with_rules(validation_rules: ValidationRules) -> Self {
263 Self {
264 valid_classes: HashSet::new(),
265 validation_rules,
266 error_reporter: ErrorReporter::new(),
267 custom_variant_manager: CustomVariantManager::with_defaults(),
268 }
269 }
270
271 pub fn with_custom_variants(custom_variant_manager: CustomVariantManager) -> Self {
273 Self {
274 valid_classes: HashSet::new(),
275 validation_rules: ValidationRules::new(),
276 error_reporter: ErrorReporter::new(),
277 custom_variant_manager,
278 }
279 }
280
281 pub fn set_error_reporter(&mut self, error_reporter: ErrorReporter) {
283 self.error_reporter = error_reporter;
284 }
285
286 pub fn validate_class(&self, class_name: &str) -> std::result::Result<(), ValidationError> {
288 if self
290 .validation_rules
291 .deprecated_classes
292 .contains(class_name)
293 {
294 let error = ValidationError::DeprecatedClass(class_name.to_string());
295 self.error_reporter.report(&error);
296 return Err(error);
297 }
298
299 for pattern in &self.validation_rules.forbidden_patterns {
301 if pattern.is_match(class_name) {
302 let error = ValidationError::UnsupportedClass(class_name.to_string());
303 self.error_reporter.report(&error);
304 return Err(error);
305 }
306 }
307
308 let mut is_valid = false;
310 for pattern in &self.validation_rules.allowed_patterns {
311 if pattern.is_match(class_name) {
312 is_valid = true;
313 break;
314 }
315 }
316
317 if !is_valid {
318 let error = ValidationError::InvalidClass(class_name.to_string());
319 self.error_reporter.report(&error);
320 return Err(error);
321 }
322
323 Ok(())
324 }
325
326 pub fn validate_classes(&self, classes: &[String]) -> std::result::Result<(), ValidationError> {
328 for class in classes {
329 self.validate_class(class)?;
330 }
331
332 self.check_class_conflicts(classes)?;
334
335 self.check_required_classes(classes)?;
337
338 Ok(())
339 }
340
341 pub fn validate_custom_variant(&self, variant: &str) -> std::result::Result<(), ValidationError> {
343 self.custom_variant_manager.validate_variant(variant)
345 .map_err(|e| ValidationError::CustomVariantValidation(e.to_string()))
346 }
347
348 pub fn validate_variant_class(&self, variant: &str, class: &str) -> std::result::Result<(), ValidationError> {
350 self.validate_custom_variant(variant)?;
352
353 self.validate_class(class)?;
355
356 Ok(())
357 }
358
359 pub fn get_variant_suggestions(&self, partial: &str) -> Vec<String> {
361 self.custom_variant_manager.get_suggestions(partial)
362 }
363
364 pub fn register_custom_variant(&mut self, variant: crate::custom_variant::CustomVariant) -> std::result::Result<(), ValidationError> {
366 self.custom_variant_manager.register(variant)
367 .map_err(|e| ValidationError::CustomVariantValidation(e.to_string()))
368 }
369
370 pub fn custom_variant_manager(&self) -> &CustomVariantManager {
372 &self.custom_variant_manager
373 }
374
375 pub fn custom_variant_manager_mut(&mut self) -> &mut CustomVariantManager {
377 &mut self.custom_variant_manager
378 }
379
380 fn check_class_conflicts(
382 &self,
383 classes: &[String],
384 ) -> std::result::Result<(), ValidationError> {
385 for conflicting_classes in self.validation_rules.class_conflicts.values() {
386 let mut found_classes = Vec::new();
387
388 for class in classes {
389 if conflicting_classes.contains(class) {
390 found_classes.push(class.clone());
391 }
392 }
393
394 if found_classes.len() > 1 {
395 let error = ValidationError::ClassConflict(
396 found_classes[0].clone(),
397 found_classes[1].clone(),
398 );
399 self.error_reporter.report(&error);
400 return Err(error);
401 }
402 }
403
404 Ok(())
405 }
406
407 fn check_required_classes(
409 &self,
410 classes: &[String],
411 ) -> std::result::Result<(), ValidationError> {
412 for class in classes {
413 for (required_prefix, required_classes) in &self.validation_rules.required_classes {
414 if class.starts_with(required_prefix) {
415 let mut has_required = false;
416 for required_class in required_classes {
417 if classes.contains(required_class) {
418 has_required = true;
419 break;
420 }
421 }
422
423 if !has_required {
424 let error = ValidationError::InvalidClass(format!(
425 "Class '{}' requires one of: {}",
426 class,
427 required_classes
428 .iter()
429 .map(|s| s.as_str())
430 .collect::<Vec<_>>()
431 .join(", ")
432 ));
433 self.error_reporter.report(&error);
434 return Err(error);
435 }
436 }
437 }
438 }
439
440 Ok(())
441 }
442
443 pub fn rules(&self) -> &ValidationRules {
445 &self.validation_rules
446 }
447
448 pub fn rules_mut(&mut self) -> &mut ValidationRules {
450 &mut self.validation_rules
451 }
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457
458 #[test]
459 fn test_validation_rules_creation() {
460 let rules = ValidationRules::new();
461 assert!(!rules.allowed_patterns.is_empty());
462 assert!(!rules.class_conflicts.is_empty());
463 assert!(!rules.required_classes.is_empty());
464 }
465
466 #[test]
467 fn test_class_validator_creation() {
468 let validator = ClassValidator::new();
469 assert!(!validator.rules().allowed_patterns.is_empty());
470 }
471
472 #[test]
473 fn test_validate_valid_class() {
474 let validator = ClassValidator::new();
475 assert!(validator.validate_class("bg-blue-600").is_ok());
476 assert!(validator.validate_class("text-white").is_ok());
477 assert!(validator.validate_class("px-4").is_ok());
478 assert!(validator.validate_class("py-2").is_ok());
479 }
480
481 #[test]
482 fn test_validate_invalid_class() {
483 let validator = ClassValidator::new();
484 assert!(validator.validate_class("invalid-class").is_err());
485 assert!(validator.validate_class("bg-invalid-color").is_err());
486 }
487
488 #[test]
489 fn test_validate_deprecated_class() {
490 let mut validator = ClassValidator::new();
491 validator
492 .rules_mut()
493 .add_deprecated_class("deprecated-class".to_string());
494
495 assert!(validator.validate_class("deprecated-class").is_err());
496 }
497
498 #[test]
499 fn test_validate_class_conflicts() {
500 let validator = ClassValidator::new();
501 let classes = vec!["block".to_string(), "flex".to_string()];
502
503 assert!(validator.validate_classes(&classes).is_err());
504 }
505
506 #[test]
507 fn test_validate_required_classes() {
508 let validator = ClassValidator::new();
509 let classes = vec!["grid-cols-2".to_string()];
510
511 assert!(validator.validate_classes(&classes).is_err());
513 }
514
515 #[test]
516 fn test_validate_multiple_classes() {
517 let validator = ClassValidator::new();
518 let classes = vec![
519 "bg-blue-600".to_string(),
520 "text-white".to_string(),
521 "px-4".to_string(),
522 "py-2".to_string(),
523 ];
524
525 assert!(validator.validate_classes(&classes).is_ok());
526 }
527
528 #[test]
529 fn test_error_reporter() {
530 let mut reporter = ErrorReporter::new();
531 let error_count = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
532 let error_count_clone = error_count.clone();
533
534 reporter.set_callback(move |_error| {
535 error_count_clone.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
536 });
537
538 let error = ValidationError::InvalidClass("test".to_string());
539 reporter.report(&error);
540
541 assert_eq!(error_count.load(std::sync::atomic::Ordering::Relaxed), 1);
542 }
543
544 #[test]
545 fn test_validation_error_display() {
546 let error = ValidationError::InvalidClass("test".to_string());
547 assert_eq!(format!("{}", error), "Invalid class name: test");
548
549 let error = ValidationError::ClassConflict("class1".to_string(), "class2".to_string());
550 assert_eq!(
551 format!("{}", error),
552 "Class conflict: class1 conflicts with class2"
553 );
554
555 let error = ValidationError::DeprecatedClass("deprecated".to_string());
556 assert_eq!(format!("{}", error), "Deprecated class: deprecated");
557
558 let error = ValidationError::UnsupportedClass("unsupported".to_string());
559 assert_eq!(format!("{}", error), "Unsupported class: unsupported");
560 }
561}