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, Presentation};
85
86 #[test]
87 fn closed_enum_rejects_unknown_accepts_known() {
88 let rule = FieldRule {
89 at: PathPat::each_item_of("audience"),
90 ty: Some(FieldType::Str),
91 constraint: Some(Constraint::Enum {
92 values: vec![Term::value("public"), Term::value("private")],
93 closed: true,
94 }),
95 present: Presentation::default(),
96 };
97 assert_eq!(rule.validate(&Value::Str("public".into())), Validation::Ok);
98 assert!(matches!(
99 rule.validate(&Value::Str("familly".into())),
100 Validation::Reject(_)
101 ));
102 }
103
104 #[test]
105 fn reference_constraint_is_not_checked_here() {
106 let rule = FieldRule {
107 at: PathPat::key("part_of"),
108 ty: Some(FieldType::Ref),
109 constraint: Some(Constraint::Reference {
110 relation: "part_of".into(),
111 cardinality: Cardinality::One,
112 spanning: true,
113 }),
114 present: Presentation::default(),
115 };
116 assert_eq!(
117 rule.validate(&Value::Str("anything".into())),
118 Validation::Ok
119 );
120 assert_eq!(rule.reference(), Some("part_of"));
121 }
122
123 #[test]
124 fn schema_rule_for_matches_by_path() {
125 let schema = Schema::new(vec![FieldRule {
126 at: PathPat::key("status"),
127 ty: None,
128 constraint: Some(Constraint::Enum {
129 values: vec![
130 Term::value("active"),
131 Term {
132 retired: true,
133 ..Term::value("archived")
134 },
135 ],
136 closed: true,
137 }),
138 present: Presentation::default(),
139 }]);
140 let rule = schema.rule_for(&[Seg::Key("status".into())]).unwrap();
141 assert_eq!(rule.validate(&Value::Str("active".into())), Validation::Ok);
142 assert_eq!(
145 rule.validate(&Value::Str("archived".into())),
146 Validation::Warn(Issue::retired("archived"))
147 );
148 let unknown = rule.validate(&Value::Str("activ".into()));
151 assert!(unknown.is_reject());
152 assert_eq!(
153 unknown.issue().and_then(|i| i.suggestion.as_deref()),
154 Some("active")
155 );
156 }
157}