Skip to main content

fml_xml/
write.rs

1//! JSON of the logical model → XML.
2
3use std::collections::BTreeMap;
4
5use serde_json::{Map, Value};
6
7use crate::datetime;
8use crate::model::{Element, Model, Representation, Root, TypeDef};
9use crate::{Error, Issue, IssueKind, TYPE_KEY, XSI_NS};
10
11/// XML written from JSON, and what could not be written.
12#[derive(Debug, Clone)]
13pub struct Written {
14    pub xml: String,
15    /// Properties the model does not have, values of the wrong shape, types it does not know.
16    pub issues: Vec<Issue>,
17}
18
19/// Writes JSON of the logical model as an XML document whose root element has the type at
20/// `root_type` (a canonical URL or a type name). Elements are written in definition order,
21/// attributes as attributes, the `"@type"` of a value as `xsi:type`.
22pub fn write(json: &Value, model: &Model, root_type: &str) -> Result<Written, Error> {
23    let root = model.root(root_type).ok_or_else(|| Error {
24        message: format!("the model has no type or element `{root_type}`"),
25        position: None,
26    })?;
27    let (namespace, name) = match root {
28        Root::Type(ty) => (
29            ty.namespace.clone(),
30            ty.xml_name.clone().unwrap_or_else(|| ty.name.clone()),
31        ),
32        Root::Element(_, element) => (element.namespace.clone(), element.xml_name.clone()),
33    };
34    let mut writer = Writer {
35        model,
36        default_ns: namespace.clone(),
37        prefixes: BTreeMap::new(),
38        issues: Vec::new(),
39    };
40    let mut body = String::new();
41    let path = root.path();
42    match root {
43        Root::Type(ty) => {
44            let shape = Shape::of(ty);
45            writer.element(
46                &mut body,
47                0,
48                namespace.as_deref(),
49                &name,
50                json,
51                &shape,
52                None,
53                &path,
54            );
55        }
56        Root::Element(ty, element) => {
57            let shape = Shape::of(ty);
58            writer.child(
59                &mut body,
60                0,
61                element,
62                json,
63                &shape,
64                namespace.as_deref(),
65                &path,
66            );
67        }
68    }
69    // Namespace declarations go on the root, now that it is known which are used.
70    let mut declarations = String::new();
71    if let Some(ns) = &writer.default_ns {
72        declarations.push_str(" xmlns=\"");
73        escape_attribute(ns, &mut declarations);
74        declarations.push('"');
75    }
76    for (ns, prefix) in &writer.prefixes {
77        declarations.push_str(&format!(" xmlns:{prefix}=\""));
78        escape_attribute(ns, &mut declarations);
79        declarations.push('"');
80    }
81    let at = body[1..]
82        .find([' ', '>', '/'])
83        .map_or(body.len(), |i| i + 1);
84    body.insert_str(at, &declarations);
85    let mut xml = String::with_capacity(body.len() + 40);
86    xml.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
87    xml.push_str(&body);
88    xml.push('\n');
89    Ok(Written {
90        xml,
91        issues: writer.issues,
92    })
93}
94
95struct Shape<'m> {
96    elements: &'m [Element],
97    owner: String,
98}
99
100impl<'m> Shape<'m> {
101    fn of(ty: &'m TypeDef) -> Self {
102        Shape {
103            elements: &ty.elements,
104            owner: ty.url.clone(),
105        }
106    }
107}
108
109/// A child element to write.
110enum Child<'v, 'm> {
111    Element(&'m Element, &'v Value, String),
112    Text(&'v str),
113}
114
115struct Writer<'m> {
116    model: &'m Model,
117    default_ns: Option<String>,
118    /// Namespace → prefix, for namespaces other than the default.
119    prefixes: BTreeMap<String, String>,
120    issues: Vec<Issue>,
121}
122
123impl<'m> Writer<'m> {
124    fn issue(&mut self, kind: IssueKind, path: &str, message: String) {
125        self.issues.push(Issue {
126            kind,
127            path: path.to_owned(),
128            position: None,
129            message,
130        });
131    }
132
133    /// A qualified name for an element or attribute in `namespace`.
134    fn qname(&mut self, namespace: Option<&str>, local: &str, attribute: bool) -> String {
135        match namespace {
136            None => local.to_owned(),
137            Some(ns) if !attribute && Some(ns) == self.default_ns.as_deref() => local.to_owned(),
138            Some(ns) => format!("{}:{local}", self.prefix(ns)),
139        }
140    }
141
142    fn prefix(&mut self, namespace: &str) -> String {
143        if let Some(prefix) = self.prefixes.get(namespace) {
144            return prefix.clone();
145        }
146        let wanted = if namespace == XSI_NS {
147            "xsi".to_owned()
148        } else {
149            let tail = namespace
150                .rsplit([':', '/'])
151                .find(|s| !s.is_empty())
152                .unwrap_or("ns");
153            if tail.chars().next().is_some_and(|c| c.is_ascii_alphabetic())
154                && tail.chars().all(|c| c.is_ascii_alphanumeric())
155            {
156                tail.to_owned()
157            } else {
158                "ns".to_owned()
159            }
160        };
161        let mut prefix = wanted.clone();
162        let mut n = 1;
163        while prefix == "xml" || self.prefixes.values().any(|p| *p == prefix) {
164            n += 1;
165            prefix = format!("{wanted}{n}");
166        }
167        self.prefixes.insert(namespace.to_owned(), prefix.clone());
168        prefix
169    }
170
171    #[allow(clippy::too_many_arguments)]
172    fn element(
173        &mut self,
174        out: &mut String,
175        depth: usize,
176        namespace: Option<&str>,
177        name: &str,
178        value: &Value,
179        shape: &Shape<'m>,
180        xsi_type: Option<&TypeDef>,
181        path: &str,
182    ) {
183        let Value::Object(object) = value else {
184            self.issue(
185                IssueKind::InvalidValue,
186                path,
187                format!("`{name}` needs an object, not `{value}`"),
188            );
189            return;
190        };
191        let tag = self.qname(namespace, name, false);
192        out.push('<');
193        out.push_str(&tag);
194        if let Some(ty) = xsi_type {
195            let type_name = self.qname(ty.namespace.as_deref(), &ty.name, false);
196            let attr = self.qname(Some(XSI_NS), "type", true);
197            out.push_str(&format!(" {attr}=\"{type_name}\""));
198        }
199        self.check_keys(object, shape, path);
200        // Attributes in definition order.
201        for element in shape.elements {
202            if element.representation != Representation::Attribute {
203                continue;
204            }
205            let Some(value) = object.get(&element.name) else {
206                continue;
207            };
208            let attr_path = format!("{path}.{}", element.name);
209            let Some(text) = self.attribute_text(value, element, &attr_path) else {
210                continue;
211            };
212            let ns = element
213                .explicit_namespace
214                .then_some(element.namespace.as_deref())
215                .flatten();
216            let attr = self.qname(ns, &element.xml_name, true);
217            out.push(' ');
218            out.push_str(&attr);
219            out.push_str("=\"");
220            escape_attribute(&text, out);
221            out.push('"');
222        }
223        let children = self.children(object, shape, path);
224        if children.is_empty() {
225            out.push_str("/>");
226            return;
227        }
228        out.push('>');
229        let mixed = children.iter().any(|c| matches!(c, Child::Text(_)));
230        let element_ns = namespace.map(str::to_owned);
231        for child in &children {
232            match child {
233                Child::Text(text) => escape_text(text, out),
234                Child::Element(element, value, child_path) => {
235                    if !mixed {
236                        newline(out, depth + 1);
237                    }
238                    self.child(
239                        out,
240                        depth + 1,
241                        element,
242                        value,
243                        shape,
244                        element_ns.as_deref(),
245                        child_path,
246                    );
247                }
248            }
249        }
250        if !mixed {
251            newline(out, depth);
252        }
253        out.push_str("</");
254        out.push_str(&tag);
255        out.push('>');
256    }
257
258    /// Properties the model does not have are reported, not written.
259    fn check_keys(&mut self, object: &Map<String, Value>, shape: &Shape<'m>, path: &str) {
260        for key in object.keys() {
261            if key == TYPE_KEY {
262                continue;
263            }
264            if !shape.elements.iter().any(|e| e.name == *key) {
265                self.issue(
266                    IssueKind::UnknownElement,
267                    path,
268                    format!("`{key}` is not an element of the model; left out"),
269                );
270            }
271        }
272    }
273
274    /// The content of an element, in order: text and child elements in definition order, the
275    /// members of a choice group in the order they are listed.
276    fn children<'v>(
277        &mut self,
278        object: &'v Map<String, Value>,
279        shape: &Shape<'m>,
280        path: &str,
281    ) -> Vec<Child<'v, 'm>> {
282        let mut children = Vec::new();
283        for element in shape.elements {
284            let Some(value) = object.get(&element.name) else {
285                continue;
286            };
287            match element.representation {
288                Representation::Attribute => {}
289                Representation::Text => match value {
290                    Value::String(text) => children.push(Child::Text(text)),
291                    other => self.issue(
292                        IssueKind::InvalidValue,
293                        &format!("{path}.{}", element.name),
294                        format!("text needs a string, not `{other}`"),
295                    ),
296                },
297                Representation::Element | Representation::Markup if element.choice_group => {
298                    for (index, item) in as_list(value).into_iter().enumerate() {
299                        let item_path = format!("{path}.{}[{index}]", element.name);
300                        let Value::Object(item) = item else {
301                            self.issue(
302                                IssueKind::InvalidValue,
303                                &item_path,
304                                "a choice group item needs an object".to_owned(),
305                            );
306                            continue;
307                        };
308                        for (key, member_value) in item {
309                            match element.child(key) {
310                                Some(member) if member.representation == Representation::Text => {
311                                    if let Value::String(text) = member_value {
312                                        children.push(Child::Text(text));
313                                    }
314                                }
315                                // A member made by a map may come as a list.
316                                Some(member) => {
317                                    for (n, value) in as_list(member_value).into_iter().enumerate()
318                                    {
319                                        let member_path = match member_value {
320                                            Value::Array(_) => format!("{item_path}.{key}[{n}]"),
321                                            _ => format!("{item_path}.{key}"),
322                                        };
323                                        children.push(Child::Element(member, value, member_path));
324                                    }
325                                }
326                                None => self.issue(
327                                    IssueKind::UnknownElement,
328                                    &item_path,
329                                    format!("`{key}` is not a member of `{}`", element.name),
330                                ),
331                            }
332                        }
333                    }
334                }
335                Representation::Element | Representation::Markup => {
336                    let values = as_list(value);
337                    if !element.many && values.len() > 1 {
338                        self.issue(
339                            IssueKind::TooMany,
340                            &format!("{path}.{}", element.name),
341                            format!("`{}` allows one value; the first is written", element.name),
342                        );
343                    }
344                    let many = element.many;
345                    for (index, value) in values.into_iter().enumerate() {
346                        if !many && index > 0 {
347                            break;
348                        }
349                        let child_path = if many {
350                            format!("{path}.{}[{index}]", element.name)
351                        } else {
352                            format!("{path}.{}", element.name)
353                        };
354                        children.push(Child::Element(element, value, child_path));
355                    }
356                }
357            }
358        }
359        children
360    }
361
362    #[allow(clippy::too_many_arguments)]
363    fn child(
364        &mut self,
365        out: &mut String,
366        depth: usize,
367        element: &'m Element,
368        value: &Value,
369        context: &Shape<'m>,
370        parent_ns: Option<&str>,
371        path: &str,
372    ) {
373        let namespace = element.namespace.as_deref().or(parent_ns);
374        if element.representation == Representation::Markup {
375            match value {
376                Value::String(markup) if markup.trim_start().starts_with('<') => {
377                    out.push_str(markup.trim());
378                }
379                Value::String(text) => {
380                    // Plain text: an element holding it.
381                    let tag = self.qname(namespace, &element.xml_name, false);
382                    out.push_str(&format!("<{tag}>"));
383                    escape_text(text, out);
384                    out.push_str(&format!("</{tag}>"));
385                }
386                other => self.issue(
387                    IssueKind::InvalidValue,
388                    path,
389                    format!("markup needs a string, not `{other}`"),
390                ),
391            }
392            return;
393        }
394        let model: &'m Model = self.model;
395        let declared: Option<&'m TypeDef> = element
396            .single_type()
397            .and_then(|code| model.type_of(code, &context.owner))
398            .map(|t| &**t);
399        let stated = value.get(TYPE_KEY).and_then(Value::as_str);
400        let actual: Option<&'m TypeDef> = match stated {
401            Some(name) => {
402                let found = element
403                    .types
404                    .iter()
405                    .filter_map(|code| model.type_of(code, &context.owner))
406                    .find(|t| t.name == name || t.url == name)
407                    .or_else(|| model.get(name))
408                    .or_else(|| model.by_type_name(None, name))
409                    .map(|t| &**t);
410                if found.is_none() {
411                    self.issue(
412                        IssueKind::UnknownType,
413                        path,
414                        format!("`{TYPE_KEY}` `{name}` is not a type of the model"),
415                    );
416                }
417                found
418            }
419            None => None,
420        };
421        if actual.is_none() && !element.children.is_empty() {
422            let shape = Shape {
423                elements: &element.children,
424                owner: context.owner.clone(),
425            };
426            self.element(
427                out,
428                depth,
429                namespace,
430                &element.xml_name,
431                value,
432                &shape,
433                None,
434                path,
435            );
436            return;
437        }
438        let default: Option<&'m TypeDef> = element
439            .default_type
440            .as_deref()
441            .and_then(|code| model.type_of(code, &context.owner))
442            .map(|t| &**t);
443        let Some(ty) = actual.or(default).or(declared) else {
444            self.issue(
445                IssueKind::MissingType,
446                path,
447                format!(
448                    "`{}` can be several types; give one in `{TYPE_KEY}`",
449                    element.name
450                ),
451            );
452            return;
453        };
454        // xsi:type when the value names its type, the element is polymorphic, or the value is a
455        // subtype of what the element declares.
456        // xsi:type unless the type goes without saying: the element's default type, or its only
457        // type when the value does not name one.
458        let write_type = match (default, declared) {
459            (Some(default), _) => default.url != ty.url,
460            (None, Some(declared)) => stated.is_some() || declared.url != ty.url,
461            (None, None) => true,
462        };
463        let shape = Shape::of(ty);
464        let xsi = write_type.then_some(ty);
465        self.element(
466            out,
467            depth,
468            namespace,
469            &element.xml_name,
470            value,
471            &shape,
472            xsi,
473            path,
474        );
475    }
476
477    /// The text of an attribute value.
478    fn attribute_text(&mut self, value: &Value, element: &Element, path: &str) -> Option<String> {
479        let one = |writer: &mut Self, value: &Value| -> Option<String> {
480            let text = match value {
481                Value::String(text) => text.clone(),
482                Value::Bool(b) => b.to_string(),
483                Value::Number(n) => n.to_string(),
484                Value::Null => return None,
485                other => {
486                    writer.issue(
487                        IssueKind::InvalidValue,
488                        path,
489                        format!("an attribute needs a primitive, not `{other}`"),
490                    );
491                    return None;
492                }
493            };
494            if element.date_format {
495                return match datetime::to_compact(&text) {
496                    Some(compact) => Some(compact),
497                    None => {
498                        // Already compact, or not a date: written as it is.
499                        if datetime::from_compact(&text).is_none() {
500                            writer.issue(
501                                IssueKind::InvalidValue,
502                                path,
503                                format!("`{text}` is not a date or time; written as it is"),
504                            );
505                        }
506                        Some(text)
507                    }
508                };
509            }
510            Some(text)
511        };
512        match value {
513            Value::Array(items) => {
514                let parts: Vec<String> = items.iter().filter_map(|v| one(self, v)).collect();
515                (!parts.is_empty()).then(|| parts.join(" "))
516            }
517            single => one(self, single),
518        }
519    }
520}
521
522fn as_list(value: &Value) -> Vec<&Value> {
523    match value {
524        Value::Array(items) => items.iter().collect(),
525        single => vec![single],
526    }
527}
528
529fn newline(out: &mut String, depth: usize) {
530    out.push('\n');
531    for _ in 0..depth {
532        out.push_str("  ");
533    }
534}
535
536pub(crate) fn escape_text(text: &str, out: &mut String) {
537    for c in text.chars() {
538        match c {
539            '&' => out.push_str("&amp;"),
540            '<' => out.push_str("&lt;"),
541            '>' => out.push_str("&gt;"),
542            '\r' => out.push_str("&#13;"),
543            _ => out.push(c),
544        }
545    }
546}
547
548pub(crate) fn escape_attribute(text: &str, out: &mut String) {
549    for c in text.chars() {
550        match c {
551            '&' => out.push_str("&amp;"),
552            '<' => out.push_str("&lt;"),
553            '"' => out.push_str("&quot;"),
554            '\n' => out.push_str("&#10;"),
555            '\r' => out.push_str("&#13;"),
556            '\t' => out.push_str("&#9;"),
557            _ => out.push(c),
558        }
559    }
560}