Skip to main content

svugy_parse/
json.rs

1//! Load a model from id-keyed JSON, rejecting duplicate keys and v3 keys and admitting it through every check, and print a model back as JSON.
2
3use std::collections::BTreeMap;
4use std::fmt;
5
6use serde::de::{self, SeqAccess, Visitor};
7use serde::{Deserialize, Deserializer, Serialize, Serializer};
8use svugy_model::model::{ByteRange, Edge, EdgeId, Element, Model, Traveler, ViewAngle};
9use svugy_model::path::ElementPath;
10use svugy_model::unique_key_map::UniqueKeyMapVisitor;
11use svugy_model::validate::{ModelAdmission, ModelReport, UncheckedModel};
12use svugy_model::workflow::{ElementOverride, Step, StepId, StepItem, Workflow, WorkflowId};
13
14use crate::diagnostic::{Import, NoWarning, SourceError, SourceLanguage};
15
16/// Mark JSON as a source language: its one error is the text not parsing as a model document, and it raises no warnings.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct JsonLanguage;
19
20/// Report JSON that does not parse as a model document, before any model check runs.
21pub type JsonError = SourceError<JsonLanguage>;
22
23/// Collect the import of a JSON model: the model's admission and the malformed-JSON error, if any.
24pub type JsonImport = Import<JsonLanguage>;
25
26/// Print a model as pretty-printed, deterministic JSON, which says every model.
27pub struct JsonPrinter;
28
29/// Shape a model as its JSON file: the view angle, elements keyed by path and workflows in written order; v3 root keys are rejected with their fix.
30#[derive(Default, Serialize, Deserialize)]
31#[serde(deny_unknown_fields)]
32struct ModelDocument {
33    #[serde(default, skip_serializing_if = "Option::is_none")]
34    view: Option<ViewAngle>,
35    #[serde(deserialize_with = "UniqueKeyMapVisitor::deserialize")]
36    elements: BTreeMap<ElementPath, Element>,
37    workflows: Vec<WorkflowRecord>,
38    #[serde(
39        default,
40        rename = "title",
41        skip_serializing,
42        deserialize_with = "V3RootKey::reject_title"
43    )]
44    _v3_title: (),
45    #[serde(
46        default,
47        rename = "subtitle",
48        skip_serializing,
49        deserialize_with = "V3RootKey::reject_subtitle"
50    )]
51    _v3_subtitle: (),
52    #[serde(
53        default,
54        rename = "description",
55        skip_serializing,
56        deserialize_with = "V3RootKey::reject_description"
57    )]
58    _v3_description: (),
59    #[serde(
60        default,
61        rename = "legend",
62        skip_serializing,
63        deserialize_with = "V3RootKey::reject_legend"
64    )]
65    _v3_legend: (),
66    #[serde(
67        default,
68        rename = "edges",
69        skip_serializing,
70        deserialize_with = "V3RootKey::reject_edges"
71    )]
72    _v3_edges: (),
73}
74
75/// Record one workflow as written in JSON.
76#[derive(Serialize, Deserialize)]
77#[serde(deny_unknown_fields)]
78struct WorkflowRecord {
79    id: WorkflowId,
80    title: String,
81    #[serde(default, skip_serializing_if = "Option::is_none")]
82    subtitle: Option<String>,
83    #[serde(default, skip_serializing_if = "Option::is_none")]
84    description: Option<String>,
85    #[serde(
86        default,
87        skip_serializing_if = "BTreeMap::is_empty",
88        deserialize_with = "UniqueKeyMapVisitor::deserialize"
89    )]
90    overrides: BTreeMap<ElementPath, ElementOverride>,
91    #[serde(
92        default,
93        skip_serializing_if = "BTreeMap::is_empty",
94        serialize_with = "EdgeRecord::serialize_all",
95        deserialize_with = "UniqueEdgeListVisitor::deserialize"
96    )]
97    edges: BTreeMap<EdgeId, Edge>,
98    steps: Vec<StepRecord>,
99    #[serde(default, skip_serializing_if = "Option::is_none")]
100    span: Option<ByteRange>,
101}
102
103/// Record one step as written in JSON.
104#[derive(Serialize, Deserialize)]
105#[serde(deny_unknown_fields)]
106struct StepRecord {
107    id: StepId,
108    title: String,
109    #[serde(default, skip_serializing_if = "Option::is_none")]
110    description: Option<String>,
111    items: Vec<StepItemRecord>,
112    #[serde(default, skip_serializing_if = "Option::is_none")]
113    span: Option<ByteRange>,
114}
115
116/// Record one step item as written in JSON: `{"edge": {from, to, ordinal}}` or `{"element": PATH}`.
117#[derive(Serialize, Deserialize)]
118#[serde(rename_all = "lowercase")]
119enum StepItemRecord {
120    Edge(EdgeReference),
121    Element(ElementPath),
122}
123
124/// Name an edge of the step's workflow by its endpoints and ordinal, 0 when omitted.
125#[derive(Serialize, Deserialize)]
126#[serde(deny_unknown_fields)]
127struct EdgeReference {
128    from: ElementPath,
129    to: ElementPath,
130    #[serde(default)]
131    ordinal: u32,
132}
133
134/// Record one edge as written in JSON.
135#[derive(Serialize, Deserialize)]
136#[serde(deny_unknown_fields)]
137struct EdgeRecord {
138    from: ElementPath,
139    to: ElementPath,
140    #[serde(default)]
141    ordinal: u32,
142    #[serde(default, skip_serializing_if = "Option::is_none")]
143    label: Option<String>,
144    #[serde(default, skip_serializing_if = "Option::is_none")]
145    description: Option<String>,
146    #[serde(
147        default,
148        skip_serializing_if = "BTreeMap::is_empty",
149        deserialize_with = "UniqueKeyMapVisitor::deserialize"
150    )]
151    style: BTreeMap<String, String>,
152    #[serde(default, skip_serializing_if = "Option::is_none")]
153    traveler: Option<Traveler>,
154    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
155    badge: bool,
156    #[serde(default, skip_serializing_if = "Option::is_none")]
157    span: Option<ByteRange>,
158}
159
160/// Collect edge records, rejecting an id already seen.
161struct UniqueEdgeListVisitor;
162
163/// Reject each root key v3 accepted, whatever its value, with its v4 fix.
164struct V3RootKey;
165
166impl SourceLanguage for JsonLanguage {
167    const ERROR_CODE: &'static str = "svugy::malformed_json";
168    type Warning = NoWarning;
169
170    fn import(source_text: &str) -> JsonImport {
171        let read_document = ModelDocument::read(source_text);
172        let document = match read_document {
173            Ok(document) => document,
174            Err(serde_error) => return Self::reject_malformed(source_text, &serde_error),
175        };
176        let unchecked_model = document.into_unchecked_model();
177        let admission = unchecked_model.admit();
178        JsonImport {
179            admission,
180            errors: Vec::new(),
181            warnings: Vec::new(),
182        }
183    }
184}
185
186impl JsonLanguage {
187    /// Withhold the model behind one error at where serde stopped reading `json`, its message serde's own, and no model problem.
188    fn reject_malformed(json: &str, serde_error: &serde_json::Error) -> JsonImport {
189        let withheld_admission = ModelAdmission {
190            model: None,
191            report: ModelReport::default(),
192        };
193        let line = serde_error.line();
194        let column = serde_error.column();
195        let offset = Self::byte_offset_at(json, line, column);
196        let message = serde_error.to_string();
197        let malformed_json_error = JsonError::new(message, offset..offset);
198        let errors = vec![malformed_json_error];
199        JsonImport {
200            admission: withheld_admission,
201            errors,
202            warnings: Vec::new(),
203        }
204    }
205
206    /// Turn serde's 1-based `line` and byte `column` into a byte offset into `json`, clamped to its end; line 0 means no position.
207    fn byte_offset_at(json: &str, line: usize, column: usize) -> usize {
208        let preceding_line_count = line.saturating_sub(1);
209        let preceding_lines = json.split_inclusive('\n').take(preceding_line_count);
210        let line_start: usize = preceding_lines.map(str::len).sum();
211        let column_offset = column.saturating_sub(1);
212        let offset = line_start + column_offset;
213        offset.min(json.len())
214    }
215}
216
217impl JsonPrinter {
218    /// Print `model` as pretty-printed JSON, elements keyed by path, workflows in written order, edges in edge-id order.
219    pub fn print(model: &Model) -> String {
220        let document = ModelDocument::of(model);
221        serde_json::to_string_pretty(&document)
222            .expect("a model serializes: every map key is a string")
223    }
224}
225
226impl ModelDocument {
227    /// Copy `model` into the document its JSON file holds.
228    fn of(model: &Model) -> Self {
229        let view = model.view();
230        let elements = model.elements().clone();
231        let model_workflows = model.workflows().iter();
232        let workflows = model_workflows.map(WorkflowRecord::of).collect();
233        Self {
234            view,
235            elements,
236            workflows,
237            ..Self::default()
238        }
239    }
240
241    /// Read `json` as one model document and nothing after it.
242    fn read(json: &str) -> Result<Self, serde_json::Error> {
243        let mut deserializer = serde_json::Deserializer::from_str(json);
244        let document = Self::deserialize(&mut deserializer)?;
245        deserializer.end()?;
246        Ok(document)
247    }
248
249    /// Hold the document's model unchecked, as written, ordinals included.
250    fn into_unchecked_model(self) -> UncheckedModel {
251        let records = self.workflows.into_iter();
252        let workflows = records.map(WorkflowRecord::into_workflow).collect();
253        let unchecked_model = UncheckedModel::from_parts(self.elements, workflows);
254        let Some(view) = self.view else {
255            return unchecked_model;
256        };
257        unchecked_model.with_view(view)
258    }
259}
260
261impl WorkflowRecord {
262    /// Record `workflow` as JSON writes it.
263    fn of(workflow: &Workflow) -> Self {
264        let id = workflow.id().clone();
265        let title = workflow.title().to_owned();
266        let subtitle = workflow.subtitle().map(str::to_owned);
267        let description = workflow.description().map(str::to_owned);
268        let overrides = workflow.overrides().clone();
269        let edges = workflow.edges().clone();
270        let workflow_steps = workflow.steps().iter();
271        let steps = workflow_steps.map(StepRecord::of).collect();
272        let span = workflow.span();
273        Self {
274            id,
275            title,
276            subtitle,
277            description,
278            overrides,
279            edges,
280            steps,
281            span,
282        }
283    }
284
285    /// Turn the record into the workflow it describes.
286    fn into_workflow(self) -> Workflow {
287        let mut workflow = Workflow::new(self.id, self.title)
288            .with_overrides(self.overrides)
289            .with_edges(self.edges);
290        if let Some(subtitle) = self.subtitle {
291            workflow = workflow.with_subtitle(subtitle);
292        }
293        if let Some(description) = self.description {
294            workflow = workflow.with_description(description);
295        }
296        for step_record in self.steps {
297            let step = step_record.into_step();
298            workflow = workflow.with_step(step);
299        }
300        if let Some(span) = self.span {
301            workflow = workflow.with_span(span);
302        }
303        workflow
304    }
305}
306
307impl StepRecord {
308    /// Record `step` as JSON writes it.
309    fn of(step: &Step) -> Self {
310        let id = step.id().clone();
311        let title = step.title().to_owned();
312        let description = step.description().map(str::to_owned);
313        let step_items = step.items();
314        let items = step_items.map(StepItemRecord::of).collect();
315        let span = step.span();
316        Self {
317            id,
318            title,
319            description,
320            items,
321            span,
322        }
323    }
324
325    /// Turn the record into the step it describes.
326    fn into_step(self) -> Step {
327        let mut step = Step::new(self.id, self.title);
328        if let Some(description) = self.description {
329            step = step.with_description(description);
330        }
331        for item_record in self.items {
332            let item = item_record.into_item();
333            step = step.with_item(item);
334        }
335        if let Some(span) = self.span {
336            step = step.with_span(span);
337        }
338        step
339    }
340}
341
342impl StepItemRecord {
343    /// Record `item` as JSON writes it.
344    fn of(item: &StepItem) -> Self {
345        match item {
346            StepItem::Edge(id) => {
347                let from = id.from().clone();
348                let to = id.to().clone();
349                let ordinal = id.ordinal();
350                let reference = EdgeReference { from, to, ordinal };
351                Self::Edge(reference)
352            }
353            StepItem::Element(path) => Self::Element(path.clone()),
354        }
355    }
356
357    /// Turn the record into the item it names.
358    fn into_item(self) -> StepItem {
359        match self {
360            Self::Edge(reference) => {
361                let id = EdgeId::new(reference.from, reference.to, reference.ordinal);
362                StepItem::Edge(id)
363            }
364            Self::Element(path) => StepItem::Element(path),
365        }
366    }
367}
368
369impl EdgeRecord {
370    /// Write the edge map as a list of records, in edge-id order.
371    fn serialize_all<S: Serializer>(
372        edges: &BTreeMap<EdgeId, Edge>,
373        serializer: S,
374    ) -> Result<S::Ok, S::Error> {
375        let records = edges
376            .iter()
377            .map(|(id, edge)| EdgeRecord::from_parts(id, edge));
378        serializer.collect_seq(records)
379    }
380
381    /// Record the edge `id` names, as JSON writes it.
382    fn from_parts(id: &EdgeId, edge: &Edge) -> Self {
383        let from = id.from().clone();
384        let to = id.to().clone();
385        let ordinal = id.ordinal();
386        let label = edge.label().map(str::to_owned);
387        let description = edge.description().map(str::to_owned);
388        let style = edge.style().clone();
389        let traveler = edge.traveler();
390        let badge = edge.is_badge();
391        let span = edge.span();
392        Self {
393            from,
394            to,
395            ordinal,
396            label,
397            description,
398            style,
399            traveler,
400            badge,
401            span,
402        }
403    }
404
405    /// Split the record into the id and the edge it describes.
406    fn into_parts(self) -> (EdgeId, Edge) {
407        let id = EdgeId::new(self.from, self.to, self.ordinal);
408        let mut edge = Edge::new();
409        if let Some(label) = self.label {
410            edge = edge.with_label(label);
411        }
412        if let Some(description) = self.description {
413            edge = edge.with_description(description);
414        }
415        for (key, value) in self.style {
416            edge = edge.with_style(key, value);
417        }
418        if let Some(traveler) = self.traveler {
419            edge = edge.with_traveler(traveler);
420        }
421        if self.badge {
422            edge = edge.with_badge();
423        }
424        if let Some(span) = self.span {
425            edge = edge.with_span(span);
426        }
427        (id, edge)
428    }
429}
430
431impl UniqueEdgeListVisitor {
432    /// Read the edge list into a map keyed by edge id, failing on a repeated id.
433    fn deserialize<'de, D: Deserializer<'de>>(
434        deserializer: D,
435    ) -> Result<BTreeMap<EdgeId, Edge>, D::Error> {
436        deserializer.deserialize_seq(Self)
437    }
438}
439
440impl<'de> Visitor<'de> for UniqueEdgeListVisitor {
441    type Value = BTreeMap<EdgeId, Edge>;
442
443    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
444        formatter.write_str("a list of edges with unique (from, to, ordinal)")
445    }
446
447    fn visit_seq<A: SeqAccess<'de>>(self, mut records: A) -> Result<Self::Value, A::Error> {
448        let mut edges = BTreeMap::new();
449        while let Some(record) = records.next_element::<EdgeRecord>()? {
450            let (id, edge) = record.into_parts();
451            let is_duplicate = edges.contains_key(&id);
452            if is_duplicate {
453                let duplicate_edge_message = format!("duplicate edge `{id}`");
454                let duplicate_edge_error = de::Error::custom(duplicate_edge_message);
455                return Err(duplicate_edge_error);
456            }
457            edges.insert(id, edge);
458        }
459        Ok(edges)
460    }
461}
462
463impl V3RootKey {
464    /// Reject a root `title`, which moved into workflows.
465    fn reject_title<'de, D: Deserializer<'de>>(_value: D) -> Result<(), D::Error> {
466        Self::reject(
467            "`title` moved from the root into workflows: set it as the first workflow's `title`",
468        )
469    }
470
471    /// Reject a root `subtitle`, which moved into workflows.
472    fn reject_subtitle<'de, D: Deserializer<'de>>(_value: D) -> Result<(), D::Error> {
473        Self::reject(
474            "`subtitle` moved from the root into workflows: set it as the first workflow's `subtitle`",
475        )
476    }
477
478    /// Reject a root `description`, which moved into workflows.
479    fn reject_description<'de, D: Deserializer<'de>>(_value: D) -> Result<(), D::Error> {
480        Self::reject(
481            "`description` moved from the root into workflows: set it as the first workflow's `description`",
482        )
483    }
484
485    /// Reject a root `legend`, which v4 removed.
486    fn reject_legend<'de, D: Deserializer<'de>>(_value: D) -> Result<(), D::Error> {
487        Self::reject(
488            "`legend` was removed: say what each entry meant in a step's or an edge's `description`",
489        )
490    }
491
492    /// Reject root `edges`, which moved into workflows.
493    fn reject_edges<'de, D: Deserializer<'de>>(_value: D) -> Result<(), D::Error> {
494        Self::reject(
495            "`edges` moved from the root into workflows: list each edge in a workflow's `edges` and name it in a step's `items`",
496        )
497    }
498
499    /// Fail reading the key with `message`.
500    fn reject<E: de::Error>(message: &str) -> Result<(), E> {
501        let v3_key_error = E::custom(message);
502        Err(v3_key_error)
503    }
504}