Skip to main content

zen_engine/policy/
raw.rs

1use std::sync::Arc;
2
3use serde::{Deserialize, Deserializer, Serialize, Serializer};
4
5use crate::policy::blocks::{AssertionDoc, DecisionTableDoc, ExpressionDoc, MatchDoc};
6
7#[derive(Debug, Clone, Deserialize, Serialize)]
8#[serde(rename_all = "camelCase")]
9pub struct PolicyDocument {
10    #[serde(default)]
11    pub imports: Vec<Arc<str>>,
12    pub blocks: Vec<BlockDoc>,
13}
14
15#[derive(Debug, Clone)]
16pub enum BlockDoc {
17    Assertion {
18        id: Arc<str>,
19        data: AssertionDoc,
20    },
21    DecisionTable {
22        id: Arc<str>,
23        data: DecisionTableDoc,
24    },
25    Expression {
26        id: Arc<str>,
27        data: ExpressionDoc,
28    },
29    Match {
30        id: Arc<str>,
31        data: MatchDoc,
32    },
33    DataModel {
34        id: Arc<str>,
35        data: DataModelDoc,
36    },
37    Dictionary {
38        id: Arc<str>,
39        data: DictionaryDoc,
40    },
41    Ignored(serde_json::Value),
42}
43
44impl BlockDoc {
45    pub fn id(&self) -> Option<&str> {
46        match self {
47            Self::Assertion { id, .. }
48            | Self::DecisionTable { id, .. }
49            | Self::Expression { id, .. }
50            | Self::Match { id, .. }
51            | Self::DataModel { id, .. }
52            | Self::Dictionary { id, .. } => Some(id),
53            Self::Ignored(value) => value.get("id").and_then(serde_json::Value::as_str),
54        }
55    }
56
57    fn decode_known(tag: BlockTag, value: serde_json::Value) -> Result<Self, serde_json::Error> {
58        use serde::de::Error;
59
60        let BlockEnvelope { id, props } = serde_json::from_value(value)?;
61        let data = props
62            .data
63            .ok_or_else(|| serde_json::Error::missing_field("data"))?;
64
65        match tag {
66            BlockTag::Assertion => Ok(Self::Assertion {
67                id,
68                data: serde_json::from_value(data)?,
69            }),
70            BlockTag::DecisionTable => Ok(Self::DecisionTable {
71                id,
72                data: DecisionTableDoc::decode_wire(data).map_err(serde_json::Error::custom)?,
73            }),
74            BlockTag::Expression => Ok(Self::Expression {
75                id,
76                data: serde_json::from_value(data)?,
77            }),
78            BlockTag::Match => Ok(Self::Match {
79                id,
80                data: serde_json::from_value(data)?,
81            }),
82            BlockTag::DataModel => Ok(Self::DataModel {
83                id,
84                data: serde_json::from_value(data)?,
85            }),
86            BlockTag::Dictionary => Ok(Self::Dictionary {
87                id,
88                data: serde_json::from_value(data)?,
89            }),
90        }
91    }
92}
93
94impl<'de> Deserialize<'de> for BlockDoc {
95    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
96    where
97        D: Deserializer<'de>,
98    {
99        use serde::de::Error;
100
101        let value = serde_json::Value::deserialize(deserializer)?;
102        let tag = match value.get("type") {
103            Some(serde_json::Value::String(name)) => BlockTag::from_name(name),
104            Some(_) => return Err(Error::custom("block `type` must be a string")),
105            None => return Err(Error::missing_field("type")),
106        };
107
108        match tag {
109            Some(tag) => Self::decode_known(tag, value).map_err(Error::custom),
110            None => Ok(Self::Ignored(value)),
111        }
112    }
113}
114
115impl Serialize for BlockDoc {
116    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
117    where
118        S: Serializer,
119    {
120        match self {
121            Self::Assertion { id, data } => {
122                TaggedBlockRef::new(BlockTag::Assertion, id, data).serialize(serializer)
123            }
124            Self::DecisionTable { id, data } => {
125                TaggedBlockRef::new(BlockTag::DecisionTable, id, data).serialize(serializer)
126            }
127            Self::Expression { id, data } => {
128                TaggedBlockRef::new(BlockTag::Expression, id, data).serialize(serializer)
129            }
130            Self::Match { id, data } => {
131                TaggedBlockRef::new(BlockTag::Match, id, data).serialize(serializer)
132            }
133            Self::DataModel { id, data } => {
134                TaggedBlockRef::new(BlockTag::DataModel, id, data).serialize(serializer)
135            }
136            Self::Dictionary { id, data } => {
137                TaggedBlockRef::new(BlockTag::Dictionary, id, data).serialize(serializer)
138            }
139            Self::Ignored(value) => value.serialize(serializer),
140        }
141    }
142}
143
144#[derive(Clone, Copy)]
145enum BlockTag {
146    Assertion,
147    DecisionTable,
148    Expression,
149    Match,
150    DataModel,
151    Dictionary,
152}
153
154impl BlockTag {
155    fn from_name(name: &str) -> Option<Self> {
156        match name {
157            "assertion" => Some(Self::Assertion),
158            "decisionTable" => Some(Self::DecisionTable),
159            "expression" => Some(Self::Expression),
160            "match" => Some(Self::Match),
161            "dataModel" => Some(Self::DataModel),
162            "dictionary" => Some(Self::Dictionary),
163            _ => None,
164        }
165    }
166
167    fn name(self) -> &'static str {
168        match self {
169            Self::Assertion => "assertion",
170            Self::DecisionTable => "decisionTable",
171            Self::Expression => "expression",
172            Self::Match => "match",
173            Self::DataModel => "dataModel",
174            Self::Dictionary => "dictionary",
175        }
176    }
177}
178
179#[derive(Deserialize)]
180struct BlockEnvelope {
181    id: Arc<str>,
182    props: PropsEnvelope,
183}
184
185#[derive(Deserialize)]
186struct PropsEnvelope {
187    #[serde(default)]
188    data: Option<serde_json::Value>,
189}
190
191#[derive(Serialize)]
192struct TaggedBlockRef<'a, T> {
193    #[serde(rename = "type")]
194    kind: &'static str,
195    id: &'a Arc<str>,
196    props: PropsRef<'a, T>,
197}
198
199impl<'a, T> TaggedBlockRef<'a, T> {
200    fn new(tag: BlockTag, id: &'a Arc<str>, data: &'a T) -> Self {
201        Self {
202            kind: tag.name(),
203            id,
204            props: PropsRef { data },
205        }
206    }
207}
208
209#[derive(Serialize)]
210struct PropsRef<'a, T> {
211    data: &'a T,
212}
213
214#[derive(Debug, Clone, Deserialize, Serialize)]
215#[serde(rename_all = "camelCase")]
216pub struct DataModelDoc {
217    pub name: Arc<str>,
218    #[serde(default)]
219    pub scope: ScopeDoc,
220    #[serde(default)]
221    pub properties: Vec<PropertyDoc>,
222}
223
224#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
225#[serde(rename_all = "camelCase")]
226pub enum ScopeDoc {
227    #[default]
228    Entity,
229    Global,
230}
231
232#[derive(Debug, Clone, Deserialize, Serialize)]
233#[serde(rename_all = "camelCase")]
234pub struct PropertyDoc {
235    pub id: Arc<str>,
236    pub name: Arc<str>,
237    #[serde(flatten)]
238    pub property_type: PropertyTypeDoc,
239    #[serde(default)]
240    pub array: bool,
241    #[serde(default)]
242    pub optional: bool,
243}
244
245#[derive(Debug, Clone, Deserialize, Serialize)]
246#[serde(rename_all = "camelCase")]
247pub struct DictionaryDoc {
248    pub name: Arc<str>,
249    #[serde(default)]
250    pub entries: Vec<DictionaryEntryDoc>,
251}
252
253#[derive(Debug, Clone, Deserialize, Serialize)]
254#[serde(rename_all = "camelCase")]
255pub struct DictionaryEntryDoc {
256    #[serde(default)]
257    pub id: Arc<str>,
258    pub value: Arc<str>,
259    #[serde(default)]
260    pub label: Arc<str>,
261}
262
263#[derive(Debug, Clone, Deserialize, Serialize)]
264#[serde(tag = "type", rename_all = "camelCase")]
265pub enum PropertyTypeDoc {
266    String {
267        #[serde(default, rename = "enum", skip_serializing_if = "Option::is_none")]
268        values: Option<Vec<Arc<str>>>,
269    },
270    Number,
271    Boolean,
272    Date,
273    Relationship {
274        target: Arc<str>,
275    },
276    Reference {
277        target: Arc<str>,
278    },
279}
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284
285    #[test]
286    fn unknown_block_round_trips_losslessly() {
287        let doc_json = serde_json::json!({
288            "blocks": [
289                {"type": "someLayoutBlock", "foo": 1}
290            ]
291        });
292
293        let doc: PolicyDocument = serde_json::from_value(doc_json).unwrap();
294        assert!(matches!(doc.blocks.as_slice(), [BlockDoc::Ignored(_)]));
295
296        let serialized = serde_json::to_value(&doc).unwrap();
297        assert_eq!(
298            serialized["blocks"][0],
299            serde_json::json!({"type": "someLayoutBlock", "foo": 1})
300        );
301    }
302
303    #[test]
304    fn known_block_round_trips() {
305        let block_json = serde_json::json!({
306            "type": "expression",
307            "id": "b1",
308            "props": {"data": {"key": "a.b", "value": "1 + 1"}}
309        });
310
311        let block: BlockDoc = serde_json::from_value(block_json.clone()).unwrap();
312        assert!(matches!(block, BlockDoc::Expression { .. }));
313
314        let serialized = serde_json::to_value(&block).unwrap();
315        assert_eq!(serialized, block_json);
316    }
317
318    #[test]
319    fn ignored_block_exposes_id() {
320        let with_id: BlockDoc =
321            serde_json::from_value(serde_json::json!({"type": "someLayoutBlock", "id": "b1"}))
322                .unwrap();
323        assert_eq!(with_id.id(), Some("b1"));
324
325        let without_id: BlockDoc =
326            serde_json::from_value(serde_json::json!({"type": "someLayoutBlock"})).unwrap();
327        assert_eq!(without_id.id(), None);
328
329        let non_string_id: BlockDoc =
330            serde_json::from_value(serde_json::json!({"type": "someLayoutBlock", "id": 1}))
331                .unwrap();
332        assert_eq!(non_string_id.id(), None);
333    }
334
335    #[test]
336    fn upsert_by_id_replaces_ignored_block() {
337        let mut doc: PolicyDocument = serde_json::from_value(serde_json::json!({
338            "blocks": [
339                {"type": "someLayoutBlock", "id": "b1"}
340            ]
341        }))
342        .unwrap();
343
344        let new_block: BlockDoc = serde_json::from_value(serde_json::json!({
345            "type": "expression",
346            "id": "b1",
347            "props": {"data": {"key": "a.b", "value": "1 + 1"}}
348        }))
349        .unwrap();
350        let new_id = new_block.id().unwrap().to_string();
351
352        match doc
353            .blocks
354            .iter()
355            .position(|b| b.id() == Some(new_id.as_str()))
356        {
357            Some(pos) => doc.blocks[pos] = new_block,
358            None => doc.blocks.push(new_block),
359        }
360
361        assert_eq!(doc.blocks.len(), 1);
362        assert!(matches!(doc.blocks[0], BlockDoc::Expression { .. }));
363    }
364
365    #[test]
366    fn block_without_type_errors() {
367        let missing = serde_json::json!({"id": "b1", "props": {"data": {}}});
368        let non_string = serde_json::json!({"type": 1, "id": "b1"});
369
370        assert!(serde_json::from_value::<BlockDoc>(missing).is_err());
371        assert!(serde_json::from_value::<BlockDoc>(non_string).is_err());
372    }
373
374    #[test]
375    fn known_block_with_bad_payload_errors() {
376        let block_json = serde_json::json!({
377            "type": "expression",
378            "id": "b1",
379            "props": {}
380        });
381
382        assert!(serde_json::from_value::<BlockDoc>(block_json).is_err());
383    }
384}