1use 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#[derive(Debug, Clone)]
13pub struct Written {
14 pub xml: String,
15 pub issues: Vec<Issue>,
17}
18
19pub 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 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
109enum 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 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 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 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 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 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 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 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 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 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 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("&"),
540 '<' => out.push_str("<"),
541 '>' => out.push_str(">"),
542 '\r' => out.push_str(" "),
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("&"),
552 '<' => out.push_str("<"),
553 '"' => out.push_str("""),
554 '\n' => out.push_str(" "),
555 '\r' => out.push_str(" "),
556 '\t' => out.push_str("	"),
557 _ => out.push(c),
558 }
559 }
560}