1use fig::Value;
14use fig_schema::{Cardinality, Term, Validate, Validation, validate_enum};
15
16pub type FieldRule = fig_schema::FieldRule<Constraint>;
19pub type Schema = fig_schema::Schema<Constraint>;
20
21#[derive(Debug, Clone)]
23pub enum Constraint {
24 Enum {
26 values: Vec<Term>,
28 closed: bool,
30 },
31 Reference {
33 relation: String,
35 cardinality: Cardinality,
37 spanning: bool,
39 },
40}
41
42impl Validate for Constraint {
43 fn validate(&self, value: &Value) -> Validation {
47 match self {
48 Constraint::Enum { values, closed } => validate_enum(values, *closed, value),
49 Constraint::Reference { .. } => Validation::Ok,
50 }
51 }
52}
53
54pub trait FieldRuleExt {
58 fn enum_constraint(&self) -> Option<(&[Term], bool)>;
60 fn reference(&self) -> Option<&str>;
62}
63
64impl FieldRuleExt for FieldRule {
65 fn enum_constraint(&self) -> Option<(&[Term], bool)> {
66 match &self.constraint {
67 Some(Constraint::Enum { values, closed }) => Some((values, *closed)),
68 _ => None,
69 }
70 }
71
72 fn reference(&self) -> Option<&str> {
73 match &self.constraint {
74 Some(Constraint::Reference { relation, .. }) => Some(relation),
75 _ => None,
76 }
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83 use crate::tree::Seg;
84 use fig_schema::{FieldType, Issue, PathPat};
85
86 #[test]
87 fn closed_enum_rejects_unknown_accepts_known() {
88 let rule = FieldRule::new(PathPat::each_item_of("audience"))
89 .ty(FieldType::Str)
90 .constraint(Constraint::Enum {
91 values: vec![Term::value("public"), Term::value("private")],
92 closed: true,
93 });
94 assert_eq!(rule.validate(&Value::Str("public".into())), Validation::Ok);
95 assert!(matches!(
96 rule.validate(&Value::Str("familly".into())),
97 Validation::Reject(_)
98 ));
99 }
100
101 #[test]
102 fn reference_constraint_is_not_checked_here() {
103 let rule = FieldRule::new(PathPat::key("part_of"))
104 .ty(FieldType::Ref)
105 .constraint(Constraint::Reference {
106 relation: "part_of".into(),
107 cardinality: Cardinality::One,
108 spanning: true,
109 });
110 assert_eq!(
111 rule.validate(&Value::Str("anything".into())),
112 Validation::Ok
113 );
114 assert_eq!(rule.reference(), Some("part_of"));
115 }
116
117 #[test]
118 fn schema_rule_for_matches_by_path() {
119 let schema = Schema::new(vec![FieldRule::new(PathPat::key("status")).constraint(
120 Constraint::Enum {
121 values: vec![Term::value("active"), Term::value("archived").retired(true)],
122 closed: true,
123 },
124 )]);
125 let rule = schema.rule_for(&[Seg::Key("status".into())]).unwrap();
126 assert_eq!(rule.validate(&Value::Str("active".into())), Validation::Ok);
127 assert_eq!(
130 rule.validate(&Value::Str("archived".into())),
131 Validation::Warn(Issue::retired("archived"))
132 );
133 let unknown = rule.validate(&Value::Str("activ".into()));
136 assert!(unknown.is_reject());
137 assert_eq!(
138 unknown.issue().and_then(|i| i.suggestion.as_deref()),
139 Some("active")
140 );
141 }
142}