vasari_core/schema/
constraint.rs1use serde::{Deserialize, Serialize};
2use serde_json::json;
3
4#[cfg(test)]
5mod tests {
6 use super::*;
7 use crate::schema::NodeId;
8
9 fn dummy_id() -> NodeId {
10 NodeId("cafebabe".repeat(8))
11 }
12
13 #[test]
14 fn polarity_is_included_in_hash() {
15 let text = "You must validate all inputs before writing.";
16 let derived = dummy_id();
17 let mandatory = Constraint::new(
18 text.into(),
19 derived.clone(),
20 ConstraintPolarity::Mandatory,
21 vec![],
22 );
23 let prohibitive = Constraint::new(
24 text.into(),
25 derived.clone(),
26 ConstraintPolarity::Prohibitive,
27 vec![],
28 );
29 assert_ne!(
30 mandatory.id, prohibitive.id,
31 "different polarities must produce different IDs"
32 );
33 }
34
35 #[test]
36 fn serde_round_trip() {
37 let c = Constraint::new(
38 "Never store tokens in plaintext.".into(),
39 dummy_id(),
40 ConstraintPolarity::Prohibitive,
41 vec![],
42 );
43 let json = serde_json::to_string(&c).unwrap();
44 let decoded: Constraint = serde_json::from_str(&json).unwrap();
45 assert_eq!(decoded.id, c.id);
46 assert_eq!(decoded.polarity, ConstraintPolarity::Prohibitive);
47 assert_eq!(decoded.text, c.text);
48 }
49
50 #[test]
51 fn schema_version_defaults_to_one_on_deserialize() {
52 let json = r#"{"id":"abcd1234","text":"must do","derived_from":"ef567890","polarity":"mandatory","parent_ids":[]}"#;
53 let c: Constraint = serde_json::from_str(json).unwrap();
54 assert_eq!(c.schema_version, "1");
55 }
56
57 #[test]
58 fn schema_version_excluded_from_hash() {
59 let text = "You must always sanitize inputs.";
62 let derived = dummy_id();
63 let c1 = Constraint::new(
64 text.into(),
65 derived.clone(),
66 ConstraintPolarity::Mandatory,
67 vec![],
68 );
69 let mut c2 = c1.clone();
71 c2.schema_version = "99".into();
72 assert_eq!(
73 c1.id, c2.id,
74 "schema_version must not affect the content hash"
75 );
76 }
77}
78
79use super::NodeId;
80use crate::hash::node_id;
81
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
86#[serde(rename_all = "snake_case")]
87pub enum ConstraintPolarity {
88 #[default]
89 Mandatory,
90 Prohibitive,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct Constraint {
95 pub id: NodeId,
96 pub text: String,
97 pub derived_from: NodeId,
99 pub polarity: ConstraintPolarity,
100 pub parent_ids: Vec<NodeId>,
101 #[serde(default = "default_schema_version")]
104 pub schema_version: String,
105}
106
107fn default_schema_version() -> String {
108 "1".to_string()
109}
110
111impl Constraint {
112 pub fn new(
113 text: String,
114 derived_from: NodeId,
115 polarity: ConstraintPolarity,
116 parent_ids: Vec<NodeId>,
117 ) -> Self {
118 let id = NodeId(node_id(&Self::hash_input(
119 &text,
120 &derived_from,
121 &polarity,
122 &parent_ids,
123 )));
124 Self {
125 id,
126 text,
127 derived_from,
128 polarity,
129 parent_ids,
130 schema_version: default_schema_version(),
131 }
132 }
133
134 fn hash_input(
135 text: &str,
136 derived_from: &NodeId,
137 polarity: &ConstraintPolarity,
138 parent_ids: &[NodeId],
139 ) -> serde_json::Value {
140 let polarity_str = match polarity {
141 ConstraintPolarity::Mandatory => "mandatory",
142 ConstraintPolarity::Prohibitive => "prohibitive",
143 };
144 json!({
145 "type": "constraint",
146 "text": text,
147 "derived_from": derived_from.as_str(),
148 "polarity": polarity_str,
149 "parent_ids": parent_ids.iter().map(|id| id.as_str()).collect::<Vec<_>>(),
150 })
151 }
152}