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}