1use alloc::{
4 boxed::Box,
5 string::{String, ToString},
6 vec::Vec,
7};
8use core::fmt;
9
10use crate::value::Value;
11
12#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum VarType {
15 Str,
17 Bool,
19 Int,
21 Float,
23 List(Vec<VarDecl>),
25 Struct(Vec<VarDecl>),
27 Enum(Vec<VariantDecl>),
29 Tmpl(Vec<VarDecl>),
31 Option(Box<VarType>),
34}
35
36fn fmt_fields(fields: &[VarDecl], f: &mut fmt::Formatter<'_>) -> fmt::Result {
38 for (i, decl) in fields.iter().enumerate() {
39 if i > 0 {
40 write!(f, ", ")?;
41 }
42 if decl.name.is_empty() {
43 write!(f, "{}", decl.var_type)?;
44 } else {
45 write!(f, "{} = {}", decl.name, decl.var_type)?;
46 }
47 }
48 Ok(())
49}
50
51impl fmt::Display for VarType {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 match self {
54 Self::Str => f.write_str(crate::consts::TYPE_STR),
55 Self::Bool => f.write_str(crate::consts::TYPE_BOOL),
56 Self::Int => f.write_str(crate::consts::TYPE_INT),
57 Self::Float => f.write_str(crate::consts::TYPE_FLOAT),
58 Self::List(fields) => {
59 f.write_str(crate::consts::TYPE_LIST_PREFIX)?;
60 fmt_fields(fields, f)?;
61 write!(f, ")")
62 }
63 Self::Struct(fields) => {
64 f.write_str(crate::consts::TYPE_STRUCT_PREFIX)?;
65 fmt_fields(fields, f)?;
66 write!(f, ")")
67 }
68 Self::Enum(variants) => {
69 if let Some(inner_ty) = Self::detect_option_inner(variants) {
71 write!(f, "{}{inner_ty})", crate::consts::TYPE_OPTION_PREFIX)
72 } else {
73 f.write_str(crate::consts::TYPE_ENUM_PREFIX)?;
74 for (i, var) in variants.iter().enumerate() {
75 if i > 0 {
76 write!(f, ", ")?;
77 }
78 write!(f, "{}", var.name)?;
79 if !var.fields.is_empty() {
80 write!(f, "(")?;
81 fmt_fields(&var.fields, f)?;
82 write!(f, ")")?;
83 }
84 }
85 write!(f, ")")
86 }
87 }
88 Self::Tmpl(fields) => {
89 f.write_str(crate::consts::TYPE_TMPL_PREFIX)?;
90 fmt_fields(fields, f)?;
91 write!(f, ")")
92 }
93 Self::Option(inner) => write!(f, "{}{inner})", crate::consts::TYPE_OPTION_PREFIX),
94 }
95 }
96}
97
98impl VarType {
99 #[must_use]
106 pub fn is_displayable(&self) -> bool {
107 matches!(self, Self::Str | Self::Int | Self::Float | Self::Bool)
108 }
109
110 #[must_use]
121 pub fn matches(&self, value: &Value) -> bool {
122 self.check(value).is_ok()
123 }
124
125 pub fn check(&self, value: &Value) -> Result<(), TypeCheckError> {
137 if self.check_fast(value) {
139 return Ok(());
140 }
141 self.check_inner(value, String::new())
143 }
144
145 #[inline]
152 fn check_fast(&self, value: &Value) -> bool {
153 match self {
154 Self::Str => matches!(value, Value::Str(_)),
155 Self::Bool => matches!(value, Value::Bool(_)),
156 Self::Int => matches!(value, Value::Int(_)),
157 Self::Float => matches!(value, Value::Float(_)),
158 Self::List(fields) => Self::check_fast_list(fields, value),
159 Self::Struct(fields) => Self::check_fast_struct(fields, value),
160 Self::Enum(variants) => Self::check_fast_enum(variants, value),
161 Self::Tmpl(expected) => Self::check_fast_tmpl(expected, value),
162 Self::Option(inner) => matches!(value, Value::None) || inner.check_fast(value),
163 }
164 }
165
166 fn check_fast_list(fields: &[VarDecl], value: &Value) -> bool {
168 let Value::List(items) = value else {
169 return false;
170 };
171 if fields.is_empty() {
172 return true;
173 }
174 for item in items.iter() {
175 if fields.len() == 1 && fields[0].name.is_empty() {
176 if !fields[0].var_type.check_fast(item) {
177 return false;
178 }
179 continue;
180 }
181 let Value::Struct(map) = item else {
182 return false;
183 };
184 if !Self::check_fast_struct_fields(fields, map) {
185 return false;
186 }
187 }
188 true
189 }
190
191 fn check_fast_struct(fields: &[VarDecl], value: &Value) -> bool {
193 let Value::Struct(map) = value else {
194 return false;
195 };
196 Self::check_fast_struct_fields(fields, map)
197 }
198
199 fn check_fast_struct_fields(
202 fields: &[VarDecl],
203 map: &crate::compat::HashMap<String, Value>,
204 ) -> bool {
205 for decl in fields {
206 match map.get(&decl.name) {
207 Some(v) => {
208 if !decl.var_type.check_fast(v) {
209 return false;
210 }
211 }
212 None => return false,
213 }
214 }
215 true
216 }
217
218 fn check_fast_enum(variants: &[VariantDecl], value: &Value) -> bool {
221 match value {
222 Value::Str(s) => variants.iter().any(|v| v.name == *s && v.fields.is_empty()),
223 Value::Struct(map) => {
224 let tag_key = crate::consts::ENUM_TAG_KEY;
225 let Some(Value::Str(tag)) = map.get(tag_key) else {
226 return false;
227 };
228 let Some(var) = variants.iter().find(|v| v.name == *tag) else {
229 return false;
230 };
231 for decl in &var.fields {
232 match map.get(&decl.name) {
233 Some(v) => {
234 if !decl.var_type.check_fast(v) {
235 return false;
236 }
237 }
238 None => return false,
239 }
240 }
241 true
242 }
243 _ => false,
244 }
245 }
246
247 fn check_fast_tmpl(expected: &[VarDecl], value: &Value) -> bool {
250 let Value::Tmpl(tmpl) = value else {
251 return false;
252 };
253 let actual_decls = tmpl.declarations();
254 for exp in expected {
255 match actual_decls.iter().find(|d| d.name == exp.name) {
256 Some(act) => {
257 if act.var_type != exp.var_type {
258 return false;
259 }
260 }
261 None => return false,
262 }
263 }
264 for act in actual_decls {
265 if act.default_value.is_none() && !expected.iter().any(|e| e.name == act.name) {
266 return false;
267 }
268 }
269 true
270 }
271
272 fn check_inner(&self, value: &Value, path: String) -> Result<(), TypeCheckError> {
273 match self {
274 Self::Str => {
275 if matches!(value, Value::Str(_)) {
276 Ok(())
277 } else {
278 Err(TypeCheckError::new(path, crate::consts::TYPE_STR, value))
279 }
280 }
281 Self::Bool => {
282 if matches!(value, Value::Bool(_)) {
283 Ok(())
284 } else {
285 Err(TypeCheckError::new(path, crate::consts::TYPE_BOOL, value))
286 }
287 }
288 Self::Int => {
289 if matches!(value, Value::Int(_)) {
290 Ok(())
291 } else {
292 Err(TypeCheckError::new(path, crate::consts::TYPE_INT, value))
293 }
294 }
295 Self::Float => {
296 if matches!(value, Value::Float(_)) {
297 Ok(())
298 } else {
299 Err(TypeCheckError::new(path, crate::consts::TYPE_FLOAT, value))
300 }
301 }
302 Self::List(fields) => Self::check_list(fields, value, path),
303 Self::Struct(fields) => Self::check_dict(fields, value, path),
304 Self::Enum(variants) => Self::check_enum(variants, value, path),
305 Self::Tmpl(params) => Self::check_tmpl(params, value, path),
306 Self::Option(inner) => {
307 if matches!(value, Value::None) {
308 Ok(())
309 } else {
310 inner.check_inner(value, path)
311 }
312 }
313 }
314 }
315
316 fn check_list(fields: &[VarDecl], value: &Value, path: String) -> Result<(), TypeCheckError> {
318 let Value::List(items) = value else {
319 return Err(TypeCheckError::new(path, crate::consts::TYPE_LIST, value));
320 };
321 if fields.is_empty() {
322 return Ok(());
323 }
324 for (i, item) in items.iter().enumerate() {
325 if fields.len() == 1 && fields[0].name.is_empty() {
326 fields[0]
328 .var_type
329 .check_inner(item, format!("{path}[{i}]"))?;
330 continue;
331 }
332 let Value::Struct(map) = item else {
333 return Err(TypeCheckError::new(
334 format!("{path}[{i}]"),
335 crate::consts::TYPE_STRUCT,
336 item,
337 ));
338 };
339 for decl in fields {
340 let field_path = if path.is_empty() {
341 format!("[{i}].{}", decl.name)
342 } else {
343 format!("{path}[{i}].{}", decl.name)
344 };
345 match map.get(&decl.name) {
346 Some(v) => decl.var_type.check_inner(v, field_path)?,
347 None => {
348 return Err(TypeCheckError {
349 path: field_path,
350 expected: decl.var_type.to_string(),
351 actual: "missing".into(),
352 actual_value: String::new(),
353 });
354 }
355 }
356 }
357 }
358 Ok(())
359 }
360
361 fn check_dict(fields: &[VarDecl], value: &Value, path: String) -> Result<(), TypeCheckError> {
363 let Value::Struct(map) = value else {
364 return Err(TypeCheckError::new(path, crate::consts::TYPE_STRUCT, value));
365 };
366 for decl in fields {
367 let field_path = if path.is_empty() {
368 decl.name.clone()
369 } else {
370 format!("{path}.{}", decl.name)
371 };
372 match map.get(&decl.name) {
373 Some(v) => decl.var_type.check_inner(v, field_path)?,
374 None => {
375 return Err(TypeCheckError {
376 path: field_path,
377 expected: decl.var_type.to_string(),
378 actual: "missing".into(),
379 actual_value: String::new(),
380 });
381 }
382 }
383 }
384 Ok(())
385 }
386
387 fn check_enum(
390 variants: &[VariantDecl],
391 value: &Value,
392 path: String,
393 ) -> Result<(), TypeCheckError> {
394 match value {
395 Value::Str(s) => {
396 if variants.iter().any(|v| v.name == *s && v.fields.is_empty()) {
397 Ok(())
398 } else {
399 let variant_names: Vec<&str> =
400 variants.iter().map(|v| v.name.as_str()).collect();
401 Err(TypeCheckError {
402 path,
403 expected: format!("enum({})", variant_names.join(", ")),
404 actual: format!("str({s})"),
405 actual_value: s.clone(),
406 })
407 }
408 }
409 Value::Struct(map) => {
410 let tag_key = crate::consts::ENUM_TAG_KEY;
411 let Some(Value::Str(tag)) = map.get(tag_key) else {
412 return Err(TypeCheckError {
413 path,
414 expected: format!("enum dict with '{tag_key}' field"),
415 actual: value.type_name().into(),
416 actual_value: value.to_string(),
417 });
418 };
419 let Some(var) = variants.iter().find(|v| v.name == *tag) else {
420 let variant_names: Vec<&str> =
421 variants.iter().map(|v| v.name.as_str()).collect();
422 return Err(TypeCheckError {
423 path: format!("{path}.{tag_key}"),
424 expected: format!("one of [{}]", variant_names.join(", ")),
425 actual: format!("'{tag}'"),
426 actual_value: tag.clone(),
427 });
428 };
429 for decl in &var.fields {
430 let field_path = if path.is_empty() {
431 decl.name.clone()
432 } else {
433 format!("{path}.{}", decl.name)
434 };
435 match map.get(&decl.name) {
436 Some(v) => decl.var_type.check_inner(v, field_path)?,
437 None => {
438 return Err(TypeCheckError {
439 path: field_path,
440 expected: decl.var_type.to_string(),
441 actual: "missing".into(),
442 actual_value: String::new(),
443 });
444 }
445 }
446 }
447 Ok(())
448 }
449 _ => Err(TypeCheckError::new(
450 path,
451 &VarType::Enum(variants.to_vec()).to_string(),
452 value,
453 )),
454 }
455 }
456
457 fn check_tmpl(expected: &[VarDecl], value: &Value, path: String) -> Result<(), TypeCheckError> {
460 let Value::Tmpl(tmpl) = value else {
461 return Err(TypeCheckError::new(path, crate::consts::TYPE_TMPL, value));
462 };
463
464 let actual_decls = tmpl.declarations();
469
470 for exp in expected {
471 let found = actual_decls.iter().find(|d| d.name == exp.name);
472 match found {
473 Some(act) => {
474 if act.var_type != exp.var_type {
475 return Err(TypeCheckError {
476 path: if path.is_empty() {
477 exp.name.clone()
478 } else {
479 format!("{path}.{}", exp.name)
480 },
481 expected: exp.var_type.to_string(),
482 actual: act.var_type.to_string(),
483 actual_value: String::new(),
484 });
485 }
486 }
487 None => {
488 return Err(TypeCheckError {
489 path: if path.is_empty() {
490 exp.name.clone()
491 } else {
492 format!("{path}.{}", exp.name)
493 },
494 expected: exp.var_type.to_string(),
495 actual: "missing".into(),
496 actual_value: String::new(),
497 });
498 }
499 }
500 }
501
502 for act in actual_decls {
504 if act.default_value.is_none() && !expected.iter().any(|e| e.name == act.name) {
505 return Err(TypeCheckError {
506 path: if path.is_empty() {
507 act.name.clone()
508 } else {
509 format!("{path}.{}", act.name)
510 },
511 expected: "in signature".into(),
512 actual: "missing".into(),
513 actual_value: String::new(),
514 });
515 }
516 }
517
518 Ok(())
519 }
520}
521
522#[derive(Debug, Clone)]
524pub struct TypeCheckError {
525 pub path: String,
527 pub expected: String,
529 pub actual: String,
531 pub actual_value: String,
533}
534
535const MAX_PREVIEW_LEN: usize = 60;
537
538impl TypeCheckError {
539 fn new(path: String, expected: &str, value: &Value) -> Self {
540 let preview = value.to_string();
541 let actual_value = if preview.len() > MAX_PREVIEW_LEN {
542 let truncate_at = preview
544 .char_indices()
545 .map(|(i, _)| i)
546 .take_while(|&i| i <= MAX_PREVIEW_LEN - 3)
547 .last()
548 .unwrap_or(0);
549 format!("{}…", &preview[..truncate_at])
550 } else {
551 preview
552 };
553 Self {
554 path,
555 expected: expected.into(),
556 actual: value.type_name().into(),
557 actual_value,
558 }
559 }
560}
561
562impl fmt::Display for TypeCheckError {
563 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
564 if self.path.is_empty() {
565 write!(f, "expected {}, got {}", self.expected, self.actual)?;
566 } else {
567 write!(
568 f,
569 "at '{}': expected {}, got {}",
570 self.path, self.expected, self.actual
571 )?;
572 }
573 if !self.actual_value.is_empty() {
574 write!(f, " ({})", self.actual_value)?;
575 }
576 Ok(())
577 }
578}
579
580#[derive(Debug, Clone, PartialEq, Eq)]
582pub struct VariantDecl {
583 pub name: String,
585 pub fields: Vec<VarDecl>,
587}
588
589#[derive(Debug, Clone, PartialEq, Eq)]
591pub struct VarDecl {
592 pub name: String,
594 pub var_type: VarType,
596 pub default_value: Option<crate::value::Value>,
598}
599
600impl VarDecl {
601 #[must_use]
603 pub fn default_value(&self) -> Option<&crate::value::Value> {
604 self.default_value.as_ref()
605 }
606}
607
608impl VarType {
609 #[must_use]
612 pub fn is_option(&self) -> bool {
613 match self {
614 VarType::Option(_) => true,
615 VarType::Enum(v) => Self::detect_option_inner(v).is_some(),
616 _ => false,
617 }
618 }
619
620 #[must_use]
622 pub fn option_inner_type(&self) -> Option<&VarType> {
623 match self {
624 VarType::Option(inner) => Some(inner),
625 VarType::Enum(variants) => Self::detect_option_inner(variants),
626 _ => None,
627 }
628 }
629
630 fn detect_option_inner(variants: &[VariantDecl]) -> Option<&VarType> {
634 use crate::consts::{OPTION_NONE, OPTION_SOME, OPTION_VAL_FIELD};
635 if variants.len() != 2 {
636 return None;
637 }
638 let (some, none) = if variants[0].name == OPTION_SOME && variants[1].name == OPTION_NONE {
639 (&variants[0], &variants[1])
640 } else {
641 return None;
642 };
643 if !none.fields.is_empty() {
644 return None;
645 }
646 if some.fields.len() != 1 || some.fields[0].name != OPTION_VAL_FIELD {
647 return None;
648 }
649 Some(&some.fields[0].var_type)
650 }
651}
652
653pub const BUILTIN_TYPE_NAMES: &[&str] = &[
659 crate::consts::TYPE_STR,
660 crate::consts::TYPE_BOOL,
661 crate::consts::TYPE_INT,
662 crate::consts::TYPE_FLOAT,
663 crate::consts::TYPE_LIST,
664 crate::consts::TYPE_STRUCT,
665 crate::consts::TYPE_ENUM,
666 crate::consts::TYPE_TMPL,
667 crate::consts::TYPE_OPTION,
668];
669
670#[must_use]
687pub fn to_pascal_case(s: &str) -> String {
688 s.split(['_', '-'])
689 .filter(|part| !part.is_empty())
690 .map(|part| {
691 let mut chars = part.chars();
692 match chars.next() {
693 Some(first) => {
694 let upper: String = first.to_uppercase().collect();
695 format!("{upper}{}", chars.as_str())
696 }
697 None => String::new(),
698 }
699 })
700 .collect()
701}
702
703#[cfg(all(test, feature = "std"))]
708mod tests {
709 use std::sync::Arc;
710
711 use super::*;
712 use crate::{compat::HashMap, consts::ENUM_TAG_KEY};
713
714 #[test]
717 fn display_scalar_types() {
718 assert_eq!(VarType::Str.to_string(), "str");
719 assert_eq!(VarType::Bool.to_string(), "bool");
720 assert_eq!(VarType::Int.to_string(), "int");
721 assert_eq!(VarType::Float.to_string(), "float");
722 }
723
724 #[test]
725 fn display_list_with_fields() {
726 let var_type = VarType::List(vec![
727 VarDecl {
728 name: "name".into(),
729 var_type: VarType::Str,
730 default_value: None,
731 },
732 VarDecl {
733 name: "score".into(),
734 var_type: VarType::Int,
735 default_value: None,
736 },
737 ]);
738 assert_eq!(var_type.to_string(), "list(name = str, score = int)");
739 }
740
741 #[test]
742 fn display_struct_with_fields() {
743 let var_type = VarType::Struct(vec![VarDecl {
744 name: "label".into(),
745 var_type: VarType::Str,
746 default_value: None,
747 }]);
748 assert_eq!(var_type.to_string(), "struct(label = str)");
749 }
750
751 #[test]
754 fn str_matches_str_only() {
755 assert!(VarType::Str.matches(&Value::Str("hello".into())));
756 assert!(!VarType::Str.matches(&Value::Bool(true)));
757 assert!(!VarType::Str.matches(&Value::Int(1)));
758 }
759
760 #[test]
761 fn bool_matches_bool_only() {
762 assert!(VarType::Bool.matches(&Value::Bool(false)));
763 assert!(!VarType::Bool.matches(&Value::Str("true".into())));
764 }
765
766 #[test]
767 fn int_matches_int_only() {
768 assert!(VarType::Int.matches(&Value::Int(42)));
769 assert!(!VarType::Int.matches(&Value::Float(42.0)));
770 }
771
772 #[test]
773 fn float_matches_float_only() {
774 assert!(VarType::Float.matches(&Value::Float(3.25)));
775 assert!(!VarType::Float.matches(&Value::Int(3)));
776 }
777
778 #[test]
779 fn list_no_fields_matches_any_list() {
780 assert!(VarType::List(vec![]).matches(&Value::List(Arc::new(vec![]))));
781 assert!(VarType::List(vec![]).matches(&Value::List(Arc::new(vec![Value::Int(1)]))));
782 assert!(!VarType::List(vec![]).matches(&Value::Str("x".into())));
783 }
784
785 #[test]
786 fn list_with_fields_validates_all_items() {
787 let var_type = VarType::List(vec![VarDecl {
788 name: "name".into(),
789 var_type: VarType::Str,
790 default_value: None,
791 }]);
792
793 assert!(var_type.matches(&Value::List(Arc::new(vec![]))));
795
796 let valid_item = Value::Struct(Arc::new(HashMap::from([(
798 "name".into(),
799 Value::Str("a".into()),
800 )])));
801 assert!(var_type.matches(&Value::List(Arc::new(vec![valid_item]))));
802
803 let invalid_item = Value::Struct(Arc::new(HashMap::from([("id".into(), Value::Int(1))])));
805 assert!(!var_type.matches(&Value::List(Arc::new(vec![invalid_item]))));
806
807 assert!(!var_type.matches(&Value::List(Arc::new(vec![Value::Int(1)]))));
809 }
810
811 #[test]
812 fn list_with_fields_rejects_wrong_value_type() {
813 let var_type = VarType::List(vec![VarDecl {
814 name: "name".into(),
815 var_type: VarType::Str,
816 default_value: None,
817 }]);
818
819 let wrong_type = Value::Struct(Arc::new(HashMap::from([("name".into(), Value::Int(42))])));
821 assert!(
822 !var_type.matches(&Value::List(Arc::new(vec![wrong_type]))),
823 "should reject list item where 'name' is int, not str"
824 );
825 }
826
827 #[test]
828 fn list_validates_all_items_not_just_first() {
829 let var_type = VarType::List(vec![VarDecl {
830 name: "name".into(),
831 var_type: VarType::Str,
832 default_value: None,
833 }]);
834
835 let good = Value::Struct(Arc::new(HashMap::from([(
836 "name".into(),
837 Value::Str("ok".into()),
838 )])));
839 let bad = Value::Struct(Arc::new(HashMap::from([("name".into(), Value::Int(99))])));
840
841 assert!(
843 !var_type.matches(&Value::List(Arc::new(vec![good.clone(), bad]))),
844 "should validate ALL items, not just the first"
845 );
846
847 assert!(var_type.matches(&Value::List(Arc::new(vec![good.clone(), good]))));
849 }
850
851 #[test]
852 fn struct_validates_required_keys_and_types() {
853 let var_type = VarType::Struct(vec![
854 VarDecl {
855 name: "title".into(),
856 var_type: VarType::Str,
857 default_value: None,
858 },
859 VarDecl {
860 name: "count".into(),
861 var_type: VarType::Int,
862 default_value: None,
863 },
864 ]);
865
866 let valid = Value::Struct(Arc::new(HashMap::from([
867 ("title".into(), Value::Str("task".into())),
868 ("count".into(), Value::Int(5)),
869 ])));
870 assert!(var_type.matches(&valid));
871
872 let missing_field = Value::Struct(Arc::new(HashMap::from([(
874 "title".into(),
875 Value::Str("task".into()),
876 )])));
877 assert!(!var_type.matches(&missing_field));
878
879 assert!(!var_type.matches(&Value::Str("oops".into())));
881 }
882
883 #[test]
884 fn struct_rejects_wrong_field_type() {
885 let var_type = VarType::Struct(vec![VarDecl {
886 name: "count".into(),
887 var_type: VarType::Int,
888 default_value: None,
889 }]);
890
891 let wrong = Value::Struct(Arc::new(HashMap::from([(
893 "count".into(),
894 Value::Str("five".into()),
895 )])));
896 assert!(
897 !var_type.matches(&wrong),
898 "should reject struct where 'count' is str, not int"
899 );
900 }
901
902 #[test]
903 fn struct_nested_type_checking() {
904 let var_type = VarType::Struct(vec![VarDecl {
906 name: "meta".into(),
907 var_type: VarType::Struct(vec![VarDecl {
908 name: "version".into(),
909 var_type: VarType::Int,
910 default_value: None,
911 }]),
912 default_value: None,
913 }]);
914
915 let valid = Value::Struct(Arc::new(HashMap::from([(
916 "meta".into(),
917 Value::Struct(Arc::new(HashMap::from([("version".into(), Value::Int(3))]))),
918 )])));
919 assert!(var_type.matches(&valid));
920
921 let wrong = Value::Struct(Arc::new(HashMap::from([(
923 "meta".into(),
924 Value::Struct(Arc::new(HashMap::from([(
925 "version".into(),
926 Value::Str("3".into()),
927 )]))),
928 )])));
929 assert!(
930 !var_type.matches(&wrong),
931 "should recursively check nested struct field types"
932 );
933 }
934
935 #[test]
936 fn struct_no_fields_matches_any_dict() {
937 assert!(VarType::Struct(vec![]).matches(&Value::Struct(Arc::new(HashMap::new()))));
938 assert!(!VarType::Struct(vec![]).matches(&Value::List(Arc::new(vec![]))));
939 }
940
941 #[test]
942 fn display_enum_with_fields() {
943 let var_type = VarType::Enum(vec![
944 VariantDecl {
945 name: "Confirmed".into(),
946 fields: vec![VarDecl {
947 name: "evidence".into(),
948 var_type: VarType::Str,
949 default_value: None,
950 }],
951 },
952 VariantDecl {
953 name: "Inconclusive".into(),
954 fields: vec![],
955 },
956 ]);
957 assert_eq!(
958 var_type.to_string(),
959 "enum(Confirmed(evidence = str), Inconclusive)"
960 );
961 }
962
963 #[test]
964 fn enum_matches_validation() {
965 let var_type = VarType::Enum(vec![
966 VariantDecl {
967 name: "Confirmed".into(),
968 fields: vec![VarDecl {
969 name: "evidence".into(),
970 var_type: VarType::Str,
971 default_value: None,
972 }],
973 },
974 VariantDecl {
975 name: "Inconclusive".into(),
976 fields: vec![],
977 },
978 ]);
979
980 assert!(var_type.matches(&Value::Str("Inconclusive".into())));
982 assert!(!var_type.matches(&Value::Str("Confirmed".into())));
983
984 let valid_dict = Value::Struct(Arc::new(HashMap::from([
986 (ENUM_TAG_KEY.into(), Value::Str("Confirmed".into())),
987 ("evidence".into(), Value::Str("some evidence".into())),
988 ])));
989 assert!(var_type.matches(&valid_dict));
990
991 let missing_field = Value::Struct(Arc::new(HashMap::from([(
993 ENUM_TAG_KEY.into(),
994 Value::Str("Confirmed".into()),
995 )])));
996 assert!(!var_type.matches(&missing_field));
997
998 let invalid_variant = Value::Struct(Arc::new(HashMap::from([(
1000 ENUM_TAG_KEY.into(),
1001 Value::Str("Unknown".into()),
1002 )])));
1003 assert!(!var_type.matches(&invalid_variant));
1004 }
1005
1006 #[test]
1007 fn enum_rejects_wrong_field_type() {
1008 let var_type = VarType::Enum(vec![VariantDecl {
1009 name: "Confirmed".into(),
1010 fields: vec![VarDecl {
1011 name: "evidence".into(),
1012 var_type: VarType::Str,
1013 default_value: None,
1014 }],
1015 }]);
1016
1017 let wrong = Value::Struct(Arc::new(HashMap::from([
1019 (ENUM_TAG_KEY.into(), Value::Str("Confirmed".into())),
1020 ("evidence".into(), Value::Int(42)),
1021 ])));
1022 assert!(
1023 !var_type.matches(&wrong),
1024 "should reject enum variant where 'evidence' is int, not str"
1025 );
1026 }
1027
1028 #[test]
1031 fn check_scalar_error_has_empty_path() {
1032 let err = VarType::Int.check(&Value::Str("oops".into())).unwrap_err();
1033 assert!(
1034 err.path.is_empty(),
1035 "scalar mismatch should have empty path"
1036 );
1037 assert_eq!(err.expected, "int");
1038 assert_eq!(err.actual, "str");
1039 }
1040
1041 #[test]
1042 fn check_list_item_field_path() {
1043 let var_type = VarType::List(vec![VarDecl {
1044 name: "score".into(),
1045 var_type: VarType::Int,
1046 default_value: None,
1047 }]);
1048 let items = Value::List(Arc::new(vec![
1050 Value::Struct(Arc::new(HashMap::from([("score".into(), Value::Int(10))]))),
1051 Value::Struct(Arc::new(HashMap::from([(
1052 "score".into(),
1053 Value::Str("bad".into()),
1054 )]))),
1055 ]));
1056 let err = var_type.check(&items).unwrap_err();
1057 assert_eq!(err.path, "[1].score", "should point to items[1].score");
1058 assert_eq!(err.expected, "int");
1059 }
1060
1061 #[test]
1062 fn check_dict_missing_field_path() {
1063 let var_type = VarType::Struct(vec![VarDecl {
1064 name: "title".into(),
1065 var_type: VarType::Str,
1066 default_value: None,
1067 }]);
1068 let value = Value::Struct(Arc::new(HashMap::new())); let err = var_type.check(&value).unwrap_err();
1070 assert_eq!(err.path, "title");
1071 assert_eq!(err.actual, "missing");
1072 }
1073
1074 #[test]
1075 fn check_nested_dict_path() {
1076 let var_type = VarType::Struct(vec![VarDecl {
1077 name: "meta".into(),
1078 var_type: VarType::Struct(vec![VarDecl {
1079 name: "version".into(),
1080 var_type: VarType::Int,
1081 default_value: None,
1082 }]),
1083 default_value: None,
1084 }]);
1085 let value = Value::Struct(Arc::new(HashMap::from([(
1086 "meta".into(),
1087 Value::Struct(Arc::new(HashMap::from([(
1088 "version".into(),
1089 Value::Str("3".into()),
1090 )]))),
1091 )])));
1092 let err = var_type.check(&value).unwrap_err();
1093 assert_eq!(err.path, "meta.version", "should show nested path");
1094 }
1095
1096 #[test]
1097 fn check_enum_invalid_tag_path() {
1098 let var_type = VarType::Enum(vec![VariantDecl {
1099 name: "Confirmed".into(),
1100 fields: vec![],
1101 }]);
1102 let value = Value::Struct(Arc::new(HashMap::from([(
1103 ENUM_TAG_KEY.into(),
1104 Value::Str("Unknown".into()),
1105 )])));
1106 let err = var_type.check(&value).unwrap_err();
1107 assert_eq!(err.path, format!(".{ENUM_TAG_KEY}"));
1108 }
1109
1110 #[test]
1111 fn check_display_with_path() {
1112 let err = TypeCheckError {
1113 path: "tasks[2].title".into(),
1114 expected: "str".into(),
1115 actual: "int".into(),
1116 actual_value: "42".into(),
1117 };
1118 assert_eq!(
1119 err.to_string(),
1120 "at 'tasks[2].title': expected str, got int (42)"
1121 );
1122 }
1123
1124 #[test]
1125 fn check_display_empty_path() {
1126 let err = TypeCheckError {
1127 path: String::new(),
1128 expected: "str".into(),
1129 actual: "int".into(),
1130 actual_value: "42".into(),
1131 };
1132 assert_eq!(err.to_string(), "expected str, got int (42)");
1133 }
1134
1135 #[test]
1138 fn pascal_case_snake_case() {
1139 assert_eq!(super::to_pascal_case("code_review"), "CodeReview");
1140 assert_eq!(super::to_pascal_case("simple_greeting"), "SimpleGreeting");
1141 }
1142
1143 #[test]
1144 fn pascal_case_kebab_case() {
1145 assert_eq!(super::to_pascal_case("task-report"), "TaskReport");
1146 }
1147
1148 #[test]
1149 fn pascal_case_single_word() {
1150 assert_eq!(super::to_pascal_case("single"), "Single");
1151 }
1152
1153 #[test]
1154 fn pascal_case_empty() {
1155 assert_eq!(super::to_pascal_case(""), "");
1156 }
1157
1158 #[test]
1159 fn pascal_case_mixed() {
1160 assert_eq!(
1161 super::to_pascal_case("already_PascalCase"),
1162 "AlreadyPascalCase"
1163 );
1164 }
1165
1166 #[test]
1167 fn pascal_case_leading_trailing_separators() {
1168 assert_eq!(super::to_pascal_case("_leading"), "Leading");
1169 assert_eq!(super::to_pascal_case("trailing_"), "Trailing");
1170 assert_eq!(super::to_pascal_case("__double__"), "Double");
1171 }
1172
1173 #[test]
1176 fn builtin_type_names_contains_all_expected() {
1177 for name in &[
1178 "str", "bool", "int", "float", "list", "struct", "enum", "option",
1179 ] {
1180 assert!(
1181 super::BUILTIN_TYPE_NAMES.contains(name),
1182 "BUILTIN_TYPE_NAMES should contain '{name}'"
1183 );
1184 }
1185 }
1186}