Skip to main content

clayers_xml/rnc/
model.rs

1//! RNC data model: structs and enums encodable to text via `Display`.
2
3use std::fmt;
4
5/// Wrap text into `# ` comment lines at the given width.
6#[must_use]
7pub fn wrap_comment(text: &str, width: usize) -> Vec<String> {
8    let effective = if width > 2 { width - 2 } else { width };
9    let mut lines = Vec::new();
10    for paragraph in text.split('\n') {
11        let paragraph = paragraph.trim();
12        if paragraph.is_empty() {
13            lines.push("# ".to_string());
14            continue;
15        }
16        let words: Vec<&str> = paragraph.split_whitespace().collect();
17        let mut current_line = String::new();
18        for word in words {
19            if current_line.is_empty() {
20                current_line.push_str(word);
21            } else if current_line.len() + 1 + word.len() <= effective {
22                current_line.push(' ');
23                current_line.push_str(word);
24            } else {
25                lines.push(format!("# {current_line}"));
26                current_line = word.to_string();
27            }
28        }
29        if !current_line.is_empty() {
30            lines.push(format!("# {current_line}"));
31        }
32    }
33    lines
34}
35
36/// Complete RNC document.
37#[derive(Debug, Clone)]
38pub struct RncSchema {
39    pub header_comments: Vec<String>,
40    pub namespaces: Vec<RncNamespace>,
41    pub layers: Vec<RncLayer>,
42}
43
44/// A namespace declaration: `namespace pfx = "uri"`.
45#[derive(Debug, Clone)]
46pub struct RncNamespace {
47    pub prefix: String,
48    pub uri: String,
49}
50
51/// One layer's definitions.
52#[derive(Debug, Clone)]
53pub struct RncLayer {
54    pub name: String,
55    pub prefix: String,
56    pub description: Option<String>,
57    pub patterns: Vec<RncPattern>,
58    pub elements: Vec<RncGlobalElement>,
59    pub enum_summaries: Vec<RncEnumSummary>,
60}
61
62/// A named pattern: `TypeName = body`.
63#[derive(Debug, Clone)]
64pub struct RncPattern {
65    pub name: String,
66    pub body: Vec<RncBodyItem>,
67    pub description: Option<String>,
68}
69
70/// A global element: `pfx:name = element pfx:name { body }`.
71#[derive(Debug, Clone)]
72pub struct RncGlobalElement {
73    pub prefix: String,
74    pub name: String,
75    pub body: Vec<RncBodyItem>,
76    pub description: Option<String>,
77}
78
79/// Enum value summary: `# TypeName: val1 | val2`.
80#[derive(Debug, Clone)]
81pub struct RncEnumSummary {
82    pub type_name: String,
83    pub values: Vec<String>,
84}
85
86/// A body item in an RNC definition (recursive).
87#[derive(Debug, Clone)]
88pub enum RncBodyItem {
89    Attribute(RncAttribute),
90    Element(RncElement),
91    PatternRef(String),
92    Choice {
93        options: Vec<RncBodyItem>,
94        quantifier: RncQuantifier,
95    },
96    Mixed(Vec<RncBodyItem>),
97    Type(String),
98    Empty,
99    AnyElement(RncQuantifier),
100    PatternedText(String),
101    InlineEnum(Vec<String>),
102}
103
104/// An attribute declaration.
105#[derive(Debug, Clone)]
106pub struct RncAttribute {
107    pub name: String,
108    pub type_str: String,
109    pub quantifier: RncQuantifier,
110    pub default: Option<String>,
111}
112
113/// A child element declaration.
114#[derive(Debug, Clone)]
115pub struct RncElement {
116    pub prefix: String,
117    pub name: String,
118    pub body: Vec<RncBodyItem>,
119    pub quantifier: RncQuantifier,
120}
121
122/// Occurrence quantifier.
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub enum RncQuantifier {
125    One,
126    Optional,
127    ZeroOrMore,
128    OneOrMore,
129}
130
131// --- Display implementations ---
132
133impl fmt::Display for RncQuantifier {
134    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135        match self {
136            Self::One => Ok(()),
137            Self::Optional => f.write_str("?"),
138            Self::ZeroOrMore => f.write_str("*"),
139            Self::OneOrMore => f.write_str("+"),
140        }
141    }
142}
143
144impl fmt::Display for RncAttribute {
145    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146        write!(
147            f,
148            "attribute {} {{ {} }}{}",
149            self.name, self.type_str, self.quantifier
150        )?;
151        if let Some(ref d) = self.default {
152            write!(f, "  # default: {d}")?;
153        }
154        Ok(())
155    }
156}
157
158impl fmt::Display for RncElement {
159    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160        let ns_name = format!("{}:{}", self.prefix, self.name);
161        let inner = format_body_items(&self.body);
162        write!(f, "element {ns_name} {{ {inner} }}{}", self.quantifier)
163    }
164}
165
166impl fmt::Display for RncBodyItem {
167    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168        match self {
169            Self::Attribute(a) => write!(f, "{a}"),
170            Self::Element(e) => write!(f, "{e}"),
171            Self::PatternRef(name) => write!(f, "{name}"),
172            Self::Choice {
173                options,
174                quantifier,
175            } => {
176                let opts: Vec<String> = options.iter().map(ToString::to_string).collect();
177                write!(f, "({}){quantifier}", opts.join(" | "))
178            }
179            Self::Mixed(items) => {
180                let inner = format_body_items(items);
181                write!(f, "mixed {{ {inner} }}")
182            }
183            Self::Type(t) => write!(f, "{t}"),
184            Self::Empty => write!(f, "empty"),
185            Self::AnyElement(q) => write!(f, "anyElement{q}"),
186            Self::PatternedText(pat) => write!(f, "text  # pattern: {pat}"),
187            Self::InlineEnum(vals) => {
188                let parts: Vec<String> = vals.iter().map(|v| format!("\"{v}\"")).collect();
189                write!(f, "{}", parts.join(" | "))
190            }
191        }
192    }
193}
194
195fn format_body_items(items: &[RncBodyItem]) -> String {
196    items
197        .iter()
198        .map(ToString::to_string)
199        .collect::<Vec<_>>()
200        .join(", ")
201}
202
203impl fmt::Display for RncEnumSummary {
204    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205        write!(f, "# {}: {}", self.type_name, self.values.join(" | "))
206    }
207}
208
209impl fmt::Display for RncNamespace {
210    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211        write!(f, "namespace {} = \"{}\"", self.prefix, self.uri)
212    }
213}
214
215impl fmt::Display for RncPattern {
216    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
217        if let Some(ref desc) = self.description {
218            for line in wrap_comment(desc, 78) {
219                writeln!(f, "{line}")?;
220            }
221        }
222        let body_str = format_body_items(&self.body);
223        write!(f, "{} = {body_str}", self.name)
224    }
225}
226
227impl fmt::Display for RncGlobalElement {
228    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229        if let Some(ref desc) = self.description {
230            for line in wrap_comment(desc, 78) {
231                writeln!(f, "{line}")?;
232            }
233        }
234        let ns_name = format!("{}:{}", self.prefix, self.name);
235        let flat = format_body_items(&self.body);
236        if flat.len() < 70 {
237            write!(f, "{ns_name} = element {ns_name} {{ {flat} }}")
238        } else {
239            writeln!(f, "{ns_name} = element {ns_name} {{")?;
240            for item in &self.body {
241                writeln!(f, "  {item}")?;
242            }
243            write!(f, "}}")
244        }
245    }
246}
247
248impl fmt::Display for RncLayer {
249    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250        writeln!(f, "# {}", "=".repeat(50))?;
251        writeln!(f, "# {} LAYER ({}:)", self.name.to_uppercase(), self.prefix)?;
252        if let Some(ref desc) = self.description {
253            for line in wrap_comment(desc, 78) {
254                writeln!(f, "{line}")?;
255            }
256        }
257        writeln!(f)?;
258
259        for pat in &self.patterns {
260            writeln!(f, "{pat}")?;
261            writeln!(f)?;
262        }
263
264        for elem in &self.elements {
265            writeln!(f, "{elem}")?;
266            writeln!(f)?;
267        }
268
269        if !self.enum_summaries.is_empty() {
270            for es in &self.enum_summaries {
271                writeln!(f, "{es}")?;
272            }
273            writeln!(f)?;
274        }
275        Ok(())
276    }
277}
278
279impl fmt::Display for RncSchema {
280    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
281        for comment in &self.header_comments {
282            writeln!(f, "# {comment}")?;
283        }
284        writeln!(f)?;
285
286        for ns in &self.namespaces {
287            writeln!(f, "{ns}")?;
288        }
289        writeln!(f)?;
290
291        for layer in &self.layers {
292            write!(f, "{layer}")?;
293        }
294        Ok(())
295    }
296}
297
298#[cfg(test)]
299mod tests {
300    use super::*;
301
302    #[test]
303    fn quantifier_display() {
304        assert_eq!(RncQuantifier::One.to_string(), "");
305        assert_eq!(RncQuantifier::Optional.to_string(), "?");
306        assert_eq!(RncQuantifier::ZeroOrMore.to_string(), "*");
307        assert_eq!(RncQuantifier::OneOrMore.to_string(), "+");
308    }
309
310    #[test]
311    fn attribute_display_required() {
312        let attr = RncAttribute {
313            name: "id".to_string(),
314            type_str: "xsd:ID".to_string(),
315            quantifier: RncQuantifier::One,
316            default: None,
317        };
318        assert_eq!(attr.to_string(), "attribute id { xsd:ID }");
319    }
320
321    #[test]
322    fn attribute_display_optional_with_default() {
323        let attr = RncAttribute {
324            name: "type".to_string(),
325            type_str: "text".to_string(),
326            quantifier: RncQuantifier::Optional,
327            default: Some("info".to_string()),
328        };
329        assert_eq!(
330            attr.to_string(),
331            "attribute type { text }?  # default: info"
332        );
333    }
334
335    #[test]
336    fn element_display() {
337        let elem = RncElement {
338            prefix: "pr".to_string(),
339            name: "title".to_string(),
340            body: vec![RncBodyItem::Type("text".to_string())],
341            quantifier: RncQuantifier::One,
342        };
343        assert_eq!(elem.to_string(), "element pr:title { text }");
344    }
345
346    #[test]
347    fn element_display_optional() {
348        let elem = RncElement {
349            prefix: "pr".to_string(),
350            name: "shortdesc".to_string(),
351            body: vec![RncBodyItem::Type("text".to_string())],
352            quantifier: RncQuantifier::Optional,
353        };
354        assert_eq!(elem.to_string(), "element pr:shortdesc { text }?");
355    }
356
357    #[test]
358    fn body_item_choice() {
359        let choice = RncBodyItem::Choice {
360            options: vec![
361                RncBodyItem::Element(RncElement {
362                    prefix: "pr".to_string(),
363                    name: "p".to_string(),
364                    body: vec![RncBodyItem::Type("text".to_string())],
365                    quantifier: RncQuantifier::One,
366                }),
367                RncBodyItem::Element(RncElement {
368                    prefix: "pr".to_string(),
369                    name: "ul".to_string(),
370                    body: vec![RncBodyItem::Empty],
371                    quantifier: RncQuantifier::One,
372                }),
373            ],
374            quantifier: RncQuantifier::ZeroOrMore,
375        };
376        assert_eq!(
377            choice.to_string(),
378            "(element pr:p { text } | element pr:ul { empty })*"
379        );
380    }
381
382    #[test]
383    fn body_item_mixed() {
384        let mixed = RncBodyItem::Mixed(vec![RncBodyItem::Type("text".to_string())]);
385        assert_eq!(mixed.to_string(), "mixed { text }");
386    }
387
388    #[test]
389    fn body_item_any_element() {
390        assert_eq!(
391            RncBodyItem::AnyElement(RncQuantifier::ZeroOrMore).to_string(),
392            "anyElement*"
393        );
394    }
395
396    #[test]
397    fn body_item_patterned_text() {
398        let pt = RncBodyItem::PatternedText("[a-z]+".to_string());
399        assert_eq!(pt.to_string(), "text  # pattern: [a-z]+");
400    }
401
402    #[test]
403    fn body_item_inline_enum() {
404        let ie = RncBodyItem::InlineEnum(vec!["info".to_string(), "warning".to_string()]);
405        assert_eq!(ie.to_string(), "\"info\" | \"warning\"");
406    }
407
408    #[test]
409    fn namespace_display() {
410        let ns = RncNamespace {
411            prefix: "pr".to_string(),
412            uri: "urn:clayers:prose".to_string(),
413        };
414        assert_eq!(ns.to_string(), "namespace pr = \"urn:clayers:prose\"");
415    }
416
417    #[test]
418    fn global_element_single_line() {
419        let elem = RncGlobalElement {
420            prefix: "org".to_string(),
421            name: "concept".to_string(),
422            body: vec![
423                RncBodyItem::Attribute(RncAttribute {
424                    name: "ref".to_string(),
425                    type_str: "text".to_string(),
426                    quantifier: RncQuantifier::One,
427                    default: None,
428                }),
429                RncBodyItem::Type("text".to_string()),
430            ],
431            description: None,
432        };
433        assert_eq!(
434            elem.to_string(),
435            "org:concept = element org:concept { attribute ref { text }, text }"
436        );
437    }
438
439    #[test]
440    fn global_element_multi_line() {
441        let elem = RncGlobalElement {
442            prefix: "pr".to_string(),
443            name: "section".to_string(),
444            body: vec![
445                RncBodyItem::Attribute(RncAttribute {
446                    name: "id".to_string(),
447                    type_str: "xsd:ID".to_string(),
448                    quantifier: RncQuantifier::One,
449                    default: None,
450                }),
451                RncBodyItem::Element(RncElement {
452                    prefix: "pr".to_string(),
453                    name: "title".to_string(),
454                    body: vec![RncBodyItem::Type("text".to_string())],
455                    quantifier: RncQuantifier::One,
456                }),
457                RncBodyItem::Choice {
458                    options: vec![
459                        RncBodyItem::Element(RncElement {
460                            prefix: "pr".to_string(),
461                            name: "p".to_string(),
462                            body: vec![RncBodyItem::Type("text".to_string())],
463                            quantifier: RncQuantifier::One,
464                        }),
465                        RncBodyItem::Element(RncElement {
466                            prefix: "pr".to_string(),
467                            name: "section".to_string(),
468                            body: vec![RncBodyItem::PatternRef("SectionType".to_string())],
469                            quantifier: RncQuantifier::One,
470                        }),
471                    ],
472                    quantifier: RncQuantifier::ZeroOrMore,
473                },
474            ],
475            description: None,
476        };
477        let s = elem.to_string();
478        assert!(s.contains("pr:section = element pr:section {"));
479        assert!(s.contains('}'));
480    }
481
482    #[test]
483    fn pattern_with_description() {
484        let pat = RncPattern {
485            name: "SectionType".to_string(),
486            body: vec![RncBodyItem::Type("text".to_string())],
487            description: Some("A structural section.".to_string()),
488        };
489        let s = pat.to_string();
490        assert!(s.contains("# A structural section."));
491        assert!(s.contains("SectionType = text"));
492    }
493
494    #[test]
495    fn enum_summary_display() {
496        let es = RncEnumSummary {
497            type_name: "NoteKind".to_string(),
498            values: vec![
499                "info".to_string(),
500                "important".to_string(),
501                "warning".to_string(),
502            ],
503        };
504        assert_eq!(es.to_string(), "# NoteKind: info | important | warning");
505    }
506
507    #[test]
508    fn wrap_comment_short() {
509        let lines = wrap_comment("Short text.", 78);
510        assert_eq!(lines, vec!["# Short text."]);
511    }
512
513    #[test]
514    fn wrap_comment_long() {
515        let text = "This is a very long description that should be wrapped across multiple lines because it exceeds the maximum width.";
516        let lines = wrap_comment(text, 40);
517        assert!(lines.len() > 1);
518        for line in &lines {
519            assert!(line.starts_with("# "));
520        }
521    }
522
523    #[test]
524    fn schema_display() {
525        let schema = RncSchema {
526            header_comments: vec!["Test schema".to_string()],
527            namespaces: vec![RncNamespace {
528                prefix: "pr".to_string(),
529                uri: "urn:test:prose".to_string(),
530            }],
531            layers: vec![],
532        };
533        let s = schema.to_string();
534        assert!(s.contains("# Test schema"));
535        assert!(s.contains("namespace pr = \"urn:test:prose\""));
536    }
537}