tailwind_rs_core/
api_contracts.rs

1//! API Contracts and Contract Testing
2//!
3//! This module provides comprehensive API contracts and contract testing
4//! to ensure API stability, backward compatibility, and reliability.
5
6use 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
13/// API contract trait for ensuring API stability
14pub trait ApiContract {
15    type Input;
16    type Output;
17    type Error;
18    
19    /// Validate input according to contract
20    fn validate_input(&self, input: &Self::Input) -> Result<(), ContractError>;
21    
22    /// Process input according to contract
23    fn process(&self, input: Self::Input) -> Result<Self::Output, Self::Error>;
24    
25    /// Validate output according to contract
26    fn validate_output(&self, output: &Self::Output) -> Result<(), ContractError>;
27}
28
29/// Contract error type
30#[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/// ClassBuilder API contract
52#[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        // Validate class names
90        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        // Validate breakpoints
100        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        // Add base classes
115        for class in input.classes {
116            builder = builder.class(class);
117        }
118        
119        // Add responsive classes
120        for (breakpoint, class) in input.responsive {
121            builder = builder.responsive(breakpoint, class);
122        }
123        
124        // Add conditional classes
125        for (condition, class) in input.conditional {
126            builder = builder.conditional(condition, class);
127        }
128        
129        // Add custom properties
130        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        // Validate ClassSet structure
139        if output.len() == 0 && !output.is_empty() {
140            return Err(ContractError::InvalidOutput("Invalid ClassSet state".to_string()));
141        }
142        
143        // Validate CSS class string format
144        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/// Input for ClassBuilder contract
154#[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/// CssGenerator API contract
163#[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        // Validate CSS rules
196        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        // Validate media queries
206        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        // Add CSS rules
219        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        // Generate CSS based on format
228        match input.format {
229            CssFormat::Regular => Ok(generator.generate_css()),
230            CssFormat::Minified => Ok(generator.generate_minified_css()),
231            CssFormat::WithSourceMaps => {
232                // For now, just return regular CSS
233                // In a full implementation, this would generate source maps
234                Ok(generator.generate_css())
235            }
236        }
237    }
238    
239    fn validate_output(&self, output: &Self::Output) -> Result<(), ContractError> {
240        // Validate CSS syntax
241        if output.is_empty() {
242            return Err(ContractError::InvalidOutput("Empty CSS output".to_string()));
243        }
244        
245        // Check for basic CSS structure
246        if !output.contains("{") || !output.contains("}") {
247            return Err(ContractError::InvalidOutput("Invalid CSS structure".to_string()));
248        }
249        
250        Ok(())
251    }
252}
253
254/// Input for CssGenerator contract
255#[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/// Contract testing framework
276#[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        // This is a simplified implementation
319        // In a real implementation, this would run the actual contract tests
320        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/// Runtime contract validation
365#[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        // Simplified contract storage
381        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            // In a real implementation, this would validate the input
391            // For now, we'll just return Ok
392            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        // Test input validation
423        assert!(contract.validate_input(&input).is_ok());
424        
425        // Test processing
426        let output = contract.process(input).unwrap();
427        
428        // Test output validation
429        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        // Test input validation
450        assert!(contract.validate_input(&input).is_ok());
451        
452        // Test processing
453        let output = contract.process(input).unwrap();
454        
455        // Test output validation
456        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        // Test validation
481        let result = validator.validate_call("test_api", "test_input");
482        assert!(result.is_err()); // Should fail because no contract is registered
483        
484        // Test enabling/disabling validation
485        validator.disable_validation();
486        let result = validator.validate_call("test_api", "test_input");
487        assert!(result.is_ok()); // Should pass because validation is disabled
488    }
489}