1use crate::classes::{ClassBuilder, ClassSet};
7use crate::css_generator::CssGenerator;
8use crate::responsive::Breakpoint;
9use crate::error::TailwindError;
10use std::result::Result;
11use std::collections::HashMap;
12
13pub trait ApiContract {
15 type Input;
16 type Output;
17 type Error;
18
19 fn validate_input(&self, input: &Self::Input) -> Result<(), ContractError>;
21
22 fn process(&self, input: Self::Input) -> Result<Self::Output, Self::Error>;
24
25 fn validate_output(&self, output: &Self::Output) -> Result<(), ContractError>;
27}
28
29#[derive(Debug, Clone, PartialEq)]
31pub enum ContractError {
32 InvalidInput(String),
33 InvalidOutput(String),
34 ContractViolation(String),
35 BackwardCompatibilityViolation(String),
36}
37
38impl std::fmt::Display for ContractError {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 match self {
41 ContractError::InvalidInput(msg) => write!(f, "Invalid input: {}", msg),
42 ContractError::InvalidOutput(msg) => write!(f, "Invalid output: {}", msg),
43 ContractError::ContractViolation(msg) => write!(f, "Contract violation: {}", msg),
44 ContractError::BackwardCompatibilityViolation(msg) => write!(f, "Backward compatibility violation: {}", msg),
45 }
46 }
47}
48
49impl std::error::Error for ContractError {}
50
51#[derive(Debug, Clone)]
53pub struct ClassBuilderContract {
54 version: ApiVersion,
55 supported_methods: Vec<String>,
56}
57
58#[derive(Debug, Clone, PartialEq)]
59pub enum ApiVersion {
60 V1_0_0,
61 V1_1_0,
62 V2_0_0,
63}
64
65impl ClassBuilderContract {
66 pub fn new(version: ApiVersion) -> Self {
67 Self {
68 version,
69 supported_methods: vec![
70 "new".to_string(),
71 "class".to_string(),
72 "classes".to_string(),
73 "responsive".to_string(),
74 "conditional".to_string(),
75 "custom".to_string(),
76 "build".to_string(),
77 "build_string".to_string(),
78 ],
79 }
80 }
81}
82
83impl ApiContract for ClassBuilderContract {
84 type Input = ClassBuilderInput;
85 type Output = ClassSet;
86 type Error = TailwindError;
87
88 fn validate_input(&self, input: &Self::Input) -> Result<(), ContractError> {
89 for class in &input.classes {
91 if class.is_empty() {
92 return Err(ContractError::InvalidInput("Empty class name".to_string()));
93 }
94 if class.contains(" ") {
95 return Err(ContractError::InvalidInput("Class name contains spaces".to_string()));
96 }
97 }
98
99 for (breakpoint, _) in &input.responsive {
101 match breakpoint {
102 Breakpoint::Base | Breakpoint::Sm | Breakpoint::Md |
103 Breakpoint::Lg | Breakpoint::Xl | Breakpoint::Xl2 => {},
104 _ => return Err(ContractError::InvalidInput("Invalid breakpoint".to_string())),
105 }
106 }
107
108 Ok(())
109 }
110
111 fn process(&self, input: Self::Input) -> Result<Self::Output, Self::Error> {
112 let mut builder = ClassBuilder::new();
113
114 for class in input.classes {
116 builder = builder.class(class);
117 }
118
119 for (breakpoint, class) in input.responsive {
121 builder = builder.responsive(breakpoint, class);
122 }
123
124 for (condition, class) in input.conditional {
126 builder = builder.conditional(condition, class);
127 }
128
129 for (property, value) in input.custom {
131 builder = builder.custom(property, value);
132 }
133
134 Ok(builder.build())
135 }
136
137 fn validate_output(&self, output: &Self::Output) -> Result<(), ContractError> {
138 if output.len() == 0 && !output.is_empty() {
140 return Err(ContractError::InvalidOutput("Invalid ClassSet state".to_string()));
141 }
142
143 let css_classes = output.to_css_classes();
145 if css_classes.contains(" ") {
146 return Err(ContractError::InvalidOutput("CSS classes contain double spaces".to_string()));
147 }
148
149 Ok(())
150 }
151}
152
153#[derive(Debug, Clone)]
155pub struct ClassBuilderInput {
156 pub classes: Vec<String>,
157 pub responsive: Vec<(Breakpoint, String)>,
158 pub conditional: Vec<(String, String)>,
159 pub custom: Vec<(String, String)>,
160}
161
162#[derive(Debug, Clone)]
164pub struct CssGeneratorContract {
165 version: ApiVersion,
166 supported_formats: Vec<CssFormat>,
167}
168
169#[derive(Debug, Clone, PartialEq)]
170pub enum CssFormat {
171 Regular,
172 Minified,
173 WithSourceMaps,
174}
175
176impl CssGeneratorContract {
177 pub fn new(version: ApiVersion) -> Self {
178 Self {
179 version,
180 supported_formats: vec![
181 CssFormat::Regular,
182 CssFormat::Minified,
183 CssFormat::WithSourceMaps,
184 ],
185 }
186 }
187}
188
189impl ApiContract for CssGeneratorContract {
190 type Input = CssGeneratorInput;
191 type Output = String;
192 type Error = TailwindError;
193
194 fn validate_input(&self, input: &Self::Input) -> Result<(), ContractError> {
195 for rule in &input.rules {
197 if rule.selector.is_empty() {
198 return Err(ContractError::InvalidInput("Empty CSS selector".to_string()));
199 }
200 if rule.properties.is_empty() {
201 return Err(ContractError::InvalidInput("Empty CSS properties".to_string()));
202 }
203 }
204
205 for media_query in &input.media_queries {
207 if !media_query.starts_with("@media") {
208 return Err(ContractError::InvalidInput("Invalid media query format".to_string()));
209 }
210 }
211
212 Ok(())
213 }
214
215 fn process(&self, input: Self::Input) -> Result<Self::Output, Self::Error> {
216 let mut generator = CssGenerator::new();
217
218 for rule in input.rules {
220 let properties_str = rule.properties.iter()
221 .map(|p| format!("{}: {}", p.name, p.value))
222 .collect::<Vec<_>>()
223 .join("; ");
224 generator.add_css_selector(&rule.selector, &properties_str)?;
225 }
226
227 match input.format {
229 CssFormat::Regular => Ok(generator.generate_css()),
230 CssFormat::Minified => Ok(generator.generate_minified_css()),
231 CssFormat::WithSourceMaps => {
232 Ok(generator.generate_css())
235 }
236 }
237 }
238
239 fn validate_output(&self, output: &Self::Output) -> Result<(), ContractError> {
240 if output.is_empty() {
242 return Err(ContractError::InvalidOutput("Empty CSS output".to_string()));
243 }
244
245 if !output.contains("{") || !output.contains("}") {
247 return Err(ContractError::InvalidOutput("Invalid CSS structure".to_string()));
248 }
249
250 Ok(())
251 }
252}
253
254#[derive(Debug, Clone)]
256pub struct CssGeneratorInput {
257 pub rules: Vec<CssRuleInput>,
258 pub media_queries: Vec<String>,
259 pub format: CssFormat,
260}
261
262#[derive(Debug, Clone)]
263pub struct CssRuleInput {
264 pub selector: String,
265 pub properties: Vec<CssPropertyInput>,
266}
267
268#[derive(Debug, Clone)]
269pub struct CssPropertyInput {
270 pub name: String,
271 pub value: String,
272 pub important: bool,
273}
274
275#[derive(Debug, Clone)]
277pub struct ContractTester {
278 contracts: Vec<String>,
279 test_cases: Vec<TestCase>,
280}
281
282#[derive(Debug, Clone)]
283pub struct TestCase {
284 pub name: String,
285 pub input: String,
286 pub expected_output: String,
287 pub should_fail: bool,
288}
289
290impl ContractTester {
291 pub fn new() -> Self {
292 Self {
293 contracts: Vec::new(),
294 test_cases: Vec::new(),
295 }
296 }
297
298 pub fn add_contract(&mut self, contract: String) {
299 self.contracts.push(contract);
300 }
301
302 pub fn add_test_case(&mut self, test_case: TestCase) {
303 self.test_cases.push(test_case);
304 }
305
306 pub fn run_tests(&self) -> Result<TestResults, ContractError> {
307 let mut results = TestResults::new();
308
309 for test_case in &self.test_cases {
310 let result = self.run_single_test(test_case);
311 results.add_result(test_case.name.clone(), result);
312 }
313
314 Ok(results)
315 }
316
317 fn run_single_test(&self, test_case: &TestCase) -> TestResult {
318 TestResult {
321 passed: true,
322 error: None,
323 duration: std::time::Duration::from_millis(1),
324 }
325 }
326}
327
328#[derive(Debug, Clone)]
329pub struct TestResults {
330 pub results: HashMap<String, TestResult>,
331 pub total_tests: usize,
332 pub passed_tests: usize,
333 pub failed_tests: usize,
334}
335
336impl TestResults {
337 pub fn new() -> Self {
338 Self {
339 results: HashMap::new(),
340 total_tests: 0,
341 passed_tests: 0,
342 failed_tests: 0,
343 }
344 }
345
346 pub fn add_result(&mut self, name: String, result: TestResult) {
347 self.results.insert(name, result.clone());
348 self.total_tests += 1;
349 if result.passed {
350 self.passed_tests += 1;
351 } else {
352 self.failed_tests += 1;
353 }
354 }
355}
356
357#[derive(Debug, Clone)]
358pub struct TestResult {
359 pub passed: bool,
360 pub error: Option<String>,
361 pub duration: std::time::Duration,
362}
363
364#[derive(Debug, Clone)]
366pub struct ContractValidator {
367 contracts: HashMap<String, String>,
368 validation_enabled: bool,
369}
370
371impl ContractValidator {
372 pub fn new() -> Self {
373 Self {
374 contracts: HashMap::new(),
375 validation_enabled: true,
376 }
377 }
378
379 pub fn add_contract(&mut self, name: String, _contract: Box<dyn std::any::Any>) {
380 self.contracts.insert(name, "contract".to_string());
382 }
383
384 pub fn validate_call<T>(&self, api_name: &str, input: T) -> Result<(), ContractError> {
385 if !self.validation_enabled {
386 return Ok(());
387 }
388
389 if let Some(contract) = self.contracts.get(api_name) {
390 Ok(())
393 } else {
394 Err(ContractError::ContractViolation(format!("Unknown API: {}", api_name)))
395 }
396 }
397
398 pub fn enable_validation(&mut self) {
399 self.validation_enabled = true;
400 }
401
402 pub fn disable_validation(&mut self) {
403 self.validation_enabled = false;
404 }
405}
406
407#[cfg(test)]
408mod tests {
409 use super::*;
410
411 #[test]
412 fn test_class_builder_contract() {
413 let contract = ClassBuilderContract::new(ApiVersion::V2_0_0);
414
415 let input = ClassBuilderInput {
416 classes: vec!["p-4".to_string(), "m-2".to_string()],
417 responsive: vec![(Breakpoint::Md, "text-lg".to_string())],
418 conditional: vec![("hover".to_string(), "bg-blue-600".to_string())],
419 custom: vec![("primary-color".to_string(), "#3b82f6".to_string())],
420 };
421
422 assert!(contract.validate_input(&input).is_ok());
424
425 let output = contract.process(input).unwrap();
427
428 assert!(contract.validate_output(&output).is_ok());
430 }
431
432 #[test]
433 fn test_css_generator_contract() {
434 let contract = CssGeneratorContract::new(ApiVersion::V2_0_0);
435
436 let input = CssGeneratorInput {
437 rules: vec![CssRuleInput {
438 selector: ".test".to_string(),
439 properties: vec![CssPropertyInput {
440 name: "padding".to_string(),
441 value: "1rem".to_string(),
442 important: false,
443 }],
444 }],
445 media_queries: vec!["@media (min-width: 768px)".to_string()],
446 format: CssFormat::Regular,
447 };
448
449 assert!(contract.validate_input(&input).is_ok());
451
452 let output = contract.process(input).unwrap();
454
455 assert!(contract.validate_output(&output).is_ok());
457 }
458
459 #[test]
460 fn test_contract_tester() {
461 let mut tester = ContractTester::new();
462
463 let test_case = TestCase {
464 name: "test_case_1".to_string(),
465 input: Box::new("test_input"),
466 expected_output: Box::new("test_output"),
467 should_fail: false,
468 };
469
470 tester.add_test_case(test_case);
471
472 let results = tester.run_tests().unwrap();
473 assert_eq!(results.total_tests, 1);
474 }
475
476 #[test]
477 fn test_contract_validator() {
478 let mut validator = ContractValidator::new();
479
480 let result = validator.validate_call("test_api", "test_input");
482 assert!(result.is_err()); validator.disable_validation();
486 let result = validator.validate_call("test_api", "test_input");
487 assert!(result.is_ok()); }
489}