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