1use 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
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) => {
45 write!(f, "Backward compatibility violation: {}", msg)
46 }
47 }
48 }
49}
50
51impl std::error::Error for ContractError {}
52
53#[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 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 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 for class in input.classes {
123 builder = builder.class(class);
124 }
125
126 for (breakpoint, class) in input.responsive {
128 builder = builder.responsive(breakpoint, class);
129 }
130
131 for (condition, class) in input.conditional {
133 builder = builder.conditional(condition, class);
134 }
135
136 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 if output.is_empty() && !output.is_empty() {
147 return Err(ContractError::InvalidOutput(
148 "Invalid ClassSet state".to_string(),
149 ));
150 }
151
152 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#[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#[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 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 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 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 match input.format {
248 CssFormat::Regular => Ok(generator.generate_css()),
249 CssFormat::Minified => Ok(generator.generate_minified_css()),
250 CssFormat::WithSourceMaps => {
251 Ok(generator.generate_css())
254 }
255 }
256 }
257
258 fn validate_output(&self, output: &Self::Output) -> Result<(), ContractError> {
259 if output.is_empty() {
261 return Err(ContractError::InvalidOutput("Empty CSS output".to_string()));
262 }
263
264 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#[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#[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 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#[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 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 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 assert!(contract.validate_input(&input).is_ok());
466
467 let output = contract.process(input).unwrap();
469
470 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 assert!(contract.validate_input(&input).is_ok());
493
494 let output = contract.process(input).unwrap();
496
497 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 let result = validator.validate_call("test_api", "test_input");
524 assert!(result.is_err()); validator.disable_validation();
528 let result = validator.validate_call("test_api", "test_input");
529 assert!(result.is_ok()); }
531}