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::error::TailwindError;
9use crate::responsive::Breakpoint;
10use std::collections::HashMap;
11use std::result::Result;
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) => {
45                write!(f, "Backward compatibility violation: {}", msg)
46            }
47        }
48    }
49}
50
51impl std::error::Error for ContractError {}
52
53/// ClassBuilder API contract
54#[derive(Debug, Clone)]
55pub struct ClassBuilderContract {
56    version: ApiVersion,
57    supported_methods: Vec<String>,
58}
59
60#[derive(Debug, Clone, PartialEq)]
61pub enum ApiVersion {
62    V1_0_0,
63    V1_1_0,
64    V2_0_0,
65}
66
67impl ClassBuilderContract {
68    pub fn new(version: ApiVersion) -> Self {
69        Self {
70            version,
71            supported_methods: vec![
72                "new".to_string(),
73                "class".to_string(),
74                "classes".to_string(),
75                "responsive".to_string(),
76                "conditional".to_string(),
77                "custom".to_string(),
78                "build".to_string(),
79                "build_string".to_string(),
80            ],
81        }
82    }
83}
84
85impl ApiContract for ClassBuilderContract {
86    type Input = ClassBuilderInput;
87    type Output = ClassSet;
88    type Error = TailwindError;
89
90    fn validate_input(&self, input: &Self::Input) -> Result<(), ContractError> {
91        // Validate class names
92        for class in &input.classes {
93            if class.is_empty() {
94                return Err(ContractError::InvalidInput("Empty class name".to_string()));
95            }
96            if class.contains(" ") {
97                return Err(ContractError::InvalidInput(
98                    "Class name contains spaces".to_string(),
99                ));
100            }
101        }
102
103        // Validate breakpoints
104        for (breakpoint, _) in &input.responsive {
105            match breakpoint {
106                Breakpoint::Base
107                | Breakpoint::Sm
108                | Breakpoint::Md
109                | Breakpoint::Lg
110                | Breakpoint::Xl
111                | Breakpoint::Xl2 => {}
112            }
113        }
114
115        Ok(())
116    }
117
118    fn process(&self, input: Self::Input) -> Result<Self::Output, Self::Error> {
119        let mut builder = ClassBuilder::new();
120
121        // Add base classes
122        for class in input.classes {
123            builder = builder.class(class);
124        }
125
126        // Add responsive classes
127        for (breakpoint, class) in input.responsive {
128            builder = builder.responsive(breakpoint, class);
129        }
130
131        // Add conditional classes
132        for (condition, class) in input.conditional {
133            builder = builder.conditional(condition, class);
134        }
135
136        // Add custom properties
137        for (property, value) in input.custom {
138            builder = builder.custom(property, value);
139        }
140
141        Ok(builder.build())
142    }
143
144    fn validate_output(&self, output: &Self::Output) -> Result<(), ContractError> {
145        // Validate ClassSet structure
146        if output.is_empty() && !output.is_empty() {
147            return Err(ContractError::InvalidOutput(
148                "Invalid ClassSet state".to_string(),
149            ));
150        }
151
152        // Validate CSS class string format
153        let css_classes = output.to_css_classes();
154        if css_classes.contains("  ") {
155            return Err(ContractError::InvalidOutput(
156                "CSS classes contain double spaces".to_string(),
157            ));
158        }
159
160        Ok(())
161    }
162}
163
164/// Input for ClassBuilder contract
165#[derive(Debug, Clone)]
166pub struct ClassBuilderInput {
167    pub classes: Vec<String>,
168    pub responsive: Vec<(Breakpoint, String)>,
169    pub conditional: Vec<(String, String)>,
170    pub custom: Vec<(String, String)>,
171}
172
173/// CssGenerator API contract
174#[derive(Debug, Clone)]
175pub struct CssGeneratorContract {
176    version: ApiVersion,
177    supported_formats: Vec<CssFormat>,
178}
179
180#[derive(Debug, Clone, PartialEq)]
181pub enum CssFormat {
182    Regular,
183    Minified,
184    WithSourceMaps,
185}
186
187impl CssGeneratorContract {
188    pub fn new(version: ApiVersion) -> Self {
189        Self {
190            version,
191            supported_formats: vec![
192                CssFormat::Regular,
193                CssFormat::Minified,
194                CssFormat::WithSourceMaps,
195            ],
196        }
197    }
198}
199
200impl ApiContract for CssGeneratorContract {
201    type Input = CssGeneratorInput;
202    type Output = String;
203    type Error = TailwindError;
204
205    fn validate_input(&self, input: &Self::Input) -> Result<(), ContractError> {
206        // Validate CSS rules
207        for rule in &input.rules {
208            if rule.selector.is_empty() {
209                return Err(ContractError::InvalidInput(
210                    "Empty CSS selector".to_string(),
211                ));
212            }
213            if rule.properties.is_empty() {
214                return Err(ContractError::InvalidInput(
215                    "Empty CSS properties".to_string(),
216                ));
217            }
218        }
219
220        // Validate media queries
221        for media_query in &input.media_queries {
222            if !media_query.starts_with("@media") {
223                return Err(ContractError::InvalidInput(
224                    "Invalid media query format".to_string(),
225                ));
226            }
227        }
228
229        Ok(())
230    }
231
232    fn process(&self, input: Self::Input) -> Result<Self::Output, Self::Error> {
233        let mut generator = CssGenerator::new();
234
235        // Add CSS rules
236        for rule in input.rules {
237            let properties_str = rule
238                .properties
239                .iter()
240                .map(|p| format!("{}: {}", p.name, p.value))
241                .collect::<Vec<_>>()
242                .join("; ");
243            generator.add_css_selector(&rule.selector, &properties_str)?;
244        }
245
246        // Generate CSS based on format
247        match input.format {
248            CssFormat::Regular => Ok(generator.generate_css()),
249            CssFormat::Minified => Ok(generator.generate_minified_css()),
250            CssFormat::WithSourceMaps => {
251                // For now, just return regular CSS
252                // In a full implementation, this would generate source maps
253                Ok(generator.generate_css())
254            }
255        }
256    }
257
258    fn validate_output(&self, output: &Self::Output) -> Result<(), ContractError> {
259        // Validate CSS syntax
260        if output.is_empty() {
261            return Err(ContractError::InvalidOutput("Empty CSS output".to_string()));
262        }
263
264        // Check for basic CSS structure
265        if !output.contains("{") || !output.contains("}") {
266            return Err(ContractError::InvalidOutput(
267                "Invalid CSS structure".to_string(),
268            ));
269        }
270
271        Ok(())
272    }
273}
274
275/// Input for CssGenerator contract
276#[derive(Debug, Clone)]
277pub struct CssGeneratorInput {
278    pub rules: Vec<CssRuleInput>,
279    pub media_queries: Vec<String>,
280    pub format: CssFormat,
281}
282
283#[derive(Debug, Clone)]
284pub struct CssRuleInput {
285    pub selector: String,
286    pub properties: Vec<CssPropertyInput>,
287}
288
289#[derive(Debug, Clone)]
290pub struct CssPropertyInput {
291    pub name: String,
292    pub value: String,
293    pub important: bool,
294}
295
296/// Contract testing framework
297#[derive(Debug, Clone)]
298pub struct ContractTester {
299    contracts: Vec<String>,
300    test_cases: Vec<TestCase>,
301}
302
303#[derive(Debug, Clone)]
304pub struct TestCase {
305    pub name: String,
306    pub input: String,
307    pub expected_output: String,
308    pub should_fail: bool,
309}
310
311impl Default for ContractTester {
312    fn default() -> Self {
313        Self::new()
314    }
315}
316
317impl ContractTester {
318    pub fn new() -> Self {
319        Self {
320            contracts: Vec::new(),
321            test_cases: Vec::new(),
322        }
323    }
324
325    pub fn add_contract(&mut self, contract: String) {
326        self.contracts.push(contract);
327    }
328
329    pub fn add_test_case(&mut self, test_case: TestCase) {
330        self.test_cases.push(test_case);
331    }
332
333    pub fn run_tests(&self) -> Result<TestResults, ContractError> {
334        let mut results = TestResults::new();
335
336        for test_case in &self.test_cases {
337            let result = self.run_single_test(test_case);
338            results.add_result(test_case.name.clone(), result);
339        }
340
341        Ok(results)
342    }
343
344    fn run_single_test(&self, _test_case: &TestCase) -> TestResult {
345        // This is a simplified implementation
346        // In a real implementation, this would run the actual contract tests
347        TestResult {
348            passed: true,
349            error: None,
350            duration: std::time::Duration::from_millis(1),
351        }
352    }
353}
354
355#[derive(Debug, Clone)]
356pub struct TestResults {
357    pub results: HashMap<String, TestResult>,
358    pub total_tests: usize,
359    pub passed_tests: usize,
360    pub failed_tests: usize,
361}
362
363impl Default for TestResults {
364    fn default() -> Self {
365        Self::new()
366    }
367}
368
369impl TestResults {
370    pub fn new() -> Self {
371        Self {
372            results: HashMap::new(),
373            total_tests: 0,
374            passed_tests: 0,
375            failed_tests: 0,
376        }
377    }
378
379    pub fn add_result(&mut self, name: String, result: TestResult) {
380        self.results.insert(name, result.clone());
381        self.total_tests += 1;
382        if result.passed {
383            self.passed_tests += 1;
384        } else {
385            self.failed_tests += 1;
386        }
387    }
388}
389
390#[derive(Debug, Clone)]
391pub struct TestResult {
392    pub passed: bool,
393    pub error: Option<String>,
394    pub duration: std::time::Duration,
395}
396
397/// Runtime contract validation
398#[derive(Debug, Clone)]
399pub struct ContractValidator {
400    contracts: HashMap<String, String>,
401    validation_enabled: bool,
402}
403
404impl Default for ContractValidator {
405    fn default() -> Self {
406        Self::new()
407    }
408}
409
410impl ContractValidator {
411    pub fn new() -> Self {
412        Self {
413            contracts: HashMap::new(),
414            validation_enabled: true,
415        }
416    }
417
418    pub fn add_contract(&mut self, name: String, _contract: Box<dyn std::any::Any>) {
419        // Simplified contract storage
420        self.contracts.insert(name, "contract".to_string());
421    }
422
423    pub fn validate_call<T>(&self, api_name: &str, _input: T) -> Result<(), ContractError> {
424        if !self.validation_enabled {
425            return Ok(());
426        }
427
428        if let Some(_contract) = self.contracts.get(api_name) {
429            // In a real implementation, this would validate the input
430            // For now, we'll just return Ok
431            Ok(())
432        } else {
433            Err(ContractError::ContractViolation(format!(
434                "Unknown API: {}",
435                api_name
436            )))
437        }
438    }
439
440    pub fn enable_validation(&mut self) {
441        self.validation_enabled = true;
442    }
443
444    pub fn disable_validation(&mut self) {
445        self.validation_enabled = false;
446    }
447}
448
449#[cfg(test)]
450mod tests {
451    use super::*;
452
453    #[test]
454    fn test_class_builder_contract() {
455        let contract = ClassBuilderContract::new(ApiVersion::V2_0_0);
456
457        let input = ClassBuilderInput {
458            classes: vec!["p-4".to_string(), "m-2".to_string()],
459            responsive: vec![(Breakpoint::Md, "text-lg".to_string())],
460            conditional: vec![("hover".to_string(), "bg-blue-600".to_string())],
461            custom: vec![("primary-color".to_string(), "#3b82f6".to_string())],
462        };
463
464        // Test input validation
465        assert!(contract.validate_input(&input).is_ok());
466
467        // Test processing
468        let output = contract.process(input).unwrap();
469
470        // Test output validation
471        assert!(contract.validate_output(&output).is_ok());
472    }
473
474    #[test]
475    fn test_css_generator_contract() {
476        let contract = CssGeneratorContract::new(ApiVersion::V2_0_0);
477
478        let input = CssGeneratorInput {
479            rules: vec![CssRuleInput {
480                selector: ".test".to_string(),
481                properties: vec![CssPropertyInput {
482                    name: "padding".to_string(),
483                    value: "1rem".to_string(),
484                    important: false,
485                }],
486            }],
487            media_queries: vec!["@media (min-width: 768px)".to_string()],
488            format: CssFormat::Regular,
489        };
490
491        // Test input validation
492        assert!(contract.validate_input(&input).is_ok());
493
494        // Test processing
495        let output = contract.process(input).unwrap();
496
497        // Test output validation
498        assert!(contract.validate_output(&output).is_ok());
499    }
500
501    #[test]
502    fn test_contract_tester() {
503        let mut tester = ContractTester::new();
504
505        let test_case = TestCase {
506            name: "test_case_1".to_string(),
507            input: "test_input".to_string(),
508            expected_output: "test_output".to_string(),
509            should_fail: false,
510        };
511
512        tester.add_test_case(test_case);
513
514        let results = tester.run_tests().unwrap();
515        assert_eq!(results.total_tests, 1);
516    }
517
518    #[test]
519    fn test_contract_validator() {
520        let mut validator = ContractValidator::new();
521
522        // Test validation
523        let result = validator.validate_call("test_api", "test_input");
524        assert!(result.is_err()); // Should fail because no contract is registered
525
526        // Test enabling/disabling validation
527        validator.disable_validation();
528        let result = validator.validate_call("test_api", "test_input");
529        assert!(result.is_ok()); // Should pass because validation is disabled
530    }
531}