Skip to main content

xsd_schema/schema/
annotation.rs

1//! Annotations and documentation
2//!
3//! This module handles XSD annotations including:
4//! - xs:annotation elements with xs:appinfo and xs:documentation
5//! - Foreign attributes (non-XSD attributes on schema elements)
6//! - XML fragment preservation for extensibility
7
8use crate::ids::{DocumentId, NameId};
9use crate::namespace::context::NamespaceContextSnapshot;
10use crate::parser::location::{SourceRef, SourceSpan};
11
12/// XML fragment - raw XML content preserved from source
13///
14/// Used to capture the content of xs:appinfo and xs:documentation elements
15/// without parsing it, allowing later processing by consumers.
16#[derive(Debug, Clone)]
17pub struct XmlFragment {
18    /// Document containing this fragment
19    pub doc_id: DocumentId,
20
21    /// Byte span in the source document
22    pub span: SourceSpan,
23}
24
25impl XmlFragment {
26    /// Create a new XML fragment reference
27    pub fn new(doc_id: DocumentId, span: SourceSpan) -> Self {
28        Self { doc_id, span }
29    }
30
31    /// Get the byte range for this fragment
32    pub fn byte_range(&self) -> std::ops::Range<usize> {
33        self.span.start..self.span.end
34    }
35}
36
37/// Foreign attribute - a non-XSD attribute on a schema element
38///
39/// XSD allows arbitrary attributes from non-XSD namespaces on most elements.
40/// These are collected for extensibility (e.g., XSLT stylesheets, JAXB bindings).
41#[derive(Debug, Clone)]
42pub struct ForeignAttribute {
43    /// Qualified name of the attribute
44    pub namespace: Option<NameId>,
45    pub local_name: NameId,
46    pub prefix: Option<NameId>,
47
48    /// Attribute value
49    pub value: String,
50
51    /// Source location
52    pub source: Option<SourceRef>,
53}
54
55impl ForeignAttribute {
56    /// Create a new foreign attribute
57    pub fn new(namespace: Option<NameId>, local_name: NameId, value: String) -> Self {
58        Self {
59            namespace,
60            local_name,
61            prefix: None,
62            value,
63            source: None,
64        }
65    }
66
67    /// Check if this attribute is in a given namespace
68    pub fn is_in_namespace(&self, ns: Option<NameId>) -> bool {
69        self.namespace == ns
70    }
71}
72
73/// Annotation item - either appinfo or documentation
74#[derive(Debug, Clone)]
75pub enum AnnotationItem {
76    /// xs:appinfo element
77    AppInfo(AppInfoElement),
78    /// xs:documentation element
79    Documentation(DocumentationElement),
80}
81
82/// xs:appinfo element
83///
84/// Contains machine-readable information.
85#[derive(Debug, Clone)]
86pub struct AppInfoElement {
87    /// Source URI attribute
88    pub source: Option<String>,
89
90    /// Foreign attributes on the appinfo element
91    pub attributes: Vec<ForeignAttribute>,
92
93    /// Namespace bindings in scope when this was parsed
94    pub namespaces: NamespaceContextSnapshot,
95
96    /// Raw XML content (not parsed)
97    pub content: XmlFragment,
98
99    /// Source location
100    pub source_ref: Option<SourceRef>,
101}
102
103impl AppInfoElement {
104    /// Create a new appinfo element
105    pub fn new(content: XmlFragment, namespaces: NamespaceContextSnapshot) -> Self {
106        Self {
107            source: None,
108            attributes: Vec::new(),
109            namespaces,
110            content,
111            source_ref: None,
112        }
113    }
114}
115
116/// xs:documentation element
117///
118/// Contains human-readable documentation.
119#[derive(Debug, Clone)]
120pub struct DocumentationElement {
121    /// Source URI attribute
122    pub source: Option<String>,
123
124    /// Language attribute (xml:lang)
125    pub lang: Option<String>,
126
127    /// Foreign attributes on the documentation element
128    pub attributes: Vec<ForeignAttribute>,
129
130    /// Namespace bindings in scope when this was parsed
131    pub namespaces: NamespaceContextSnapshot,
132
133    /// Raw XML content (not parsed)
134    pub content: XmlFragment,
135
136    /// Source location
137    pub source_ref: Option<SourceRef>,
138}
139
140impl DocumentationElement {
141    /// Create a new documentation element
142    pub fn new(content: XmlFragment, namespaces: NamespaceContextSnapshot) -> Self {
143        Self {
144            source: None,
145            lang: None,
146            attributes: Vec::new(),
147            namespaces,
148            content,
149            source_ref: None,
150        }
151    }
152}
153
154/// Annotation - contains appinfo and documentation elements
155///
156/// Annotations can appear on most schema elements and are used for:
157/// - Human documentation
158/// - Machine-readable extensions (JAXB, XBRL, etc.)
159#[derive(Debug, Clone)]
160pub struct Annotation {
161    /// ID attribute
162    pub id: Option<String>,
163
164    /// Foreign attributes on the annotation element itself
165    pub attributes: Vec<ForeignAttribute>,
166
167    /// Annotation items (appinfo and documentation in order)
168    pub items: Vec<AnnotationItem>,
169
170    /// Source location
171    pub source: Option<SourceRef>,
172}
173
174impl Annotation {
175    /// Create a new empty annotation
176    pub fn new() -> Self {
177        Self {
178            id: None,
179            attributes: Vec::new(),
180            items: Vec::new(),
181            source: None,
182        }
183    }
184
185    /// Check if this annotation is empty
186    pub fn is_empty(&self) -> bool {
187        self.items.is_empty() && self.attributes.is_empty()
188    }
189
190    /// Add an appinfo element
191    pub fn add_appinfo(&mut self, appinfo: AppInfoElement) {
192        self.items.push(AnnotationItem::AppInfo(appinfo));
193    }
194
195    /// Add a documentation element
196    pub fn add_documentation(&mut self, doc: DocumentationElement) {
197        self.items.push(AnnotationItem::Documentation(doc));
198    }
199
200    /// Get all appinfo elements
201    pub fn appinfos(&self) -> impl Iterator<Item = &AppInfoElement> {
202        self.items.iter().filter_map(|item| match item {
203            AnnotationItem::AppInfo(a) => Some(a),
204            _ => None,
205        })
206    }
207
208    /// Get all documentation elements
209    pub fn documentations(&self) -> impl Iterator<Item = &DocumentationElement> {
210        self.items.iter().filter_map(|item| match item {
211            AnnotationItem::Documentation(d) => Some(d),
212            _ => None,
213        })
214    }
215
216    /// Get documentation in a specific language
217    pub fn documentation_for_lang(&self, lang: &str) -> Option<&DocumentationElement> {
218        self.documentations()
219            .find(|d| d.lang.as_ref().is_some_and(|l| l == lang))
220    }
221
222    /// Add a foreign attribute
223    pub fn add_foreign_attribute(&mut self, attr: ForeignAttribute) {
224        self.attributes.push(attr);
225    }
226}
227
228impl Default for Annotation {
229    fn default() -> Self {
230        Self::new()
231    }
232}
233
234/// Implicit annotation - created from foreign attributes on schema elements
235///
236/// When a schema element has foreign attributes but no explicit xs:annotation,
237/// an implicit annotation is created to hold them.
238pub fn create_implicit_annotation(
239    attrs: Vec<ForeignAttribute>,
240    source: Option<SourceRef>,
241) -> Annotation {
242    Annotation {
243        id: None,
244        attributes: attrs,
245        items: Vec::new(),
246        source,
247    }
248}
249
250/// Merge foreign attributes into an existing annotation or create an implicit one
251///
252/// This is used by frame finish() methods to ensure foreign attributes on schema
253/// elements are preserved. If no explicit annotation exists, an implicit one is
254/// created to hold the foreign attributes.
255pub fn merge_foreign_attributes(
256    annotation: Option<Annotation>,
257    foreign_attrs: Vec<ForeignAttribute>,
258    source: Option<SourceRef>,
259) -> Option<Annotation> {
260    if foreign_attrs.is_empty() {
261        return annotation;
262    }
263    match annotation {
264        Some(mut ann) => {
265            ann.attributes.extend(foreign_attrs);
266            Some(ann)
267        }
268        None => Some(create_implicit_annotation(foreign_attrs, source)),
269    }
270}
271
272/// Helper to check if an attribute is a foreign attribute
273///
274/// Foreign attributes are those not in:
275/// - The XSD namespace
276/// - The XSI namespace
277/// - No namespace (unqualified XSD attributes)
278pub fn is_foreign_attribute(namespace: Option<NameId>, xsd_ns: NameId, xsi_ns: NameId) -> bool {
279    match namespace {
280        None => false, // Unqualified attributes are XSD attributes
281        Some(ns) => ns != xsd_ns && ns != xsi_ns,
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288
289    #[test]
290    fn test_xml_fragment() {
291        let fragment = XmlFragment::new(0, SourceSpan { start: 10, end: 50 });
292        assert_eq!(fragment.byte_range(), 10..50);
293    }
294
295    #[test]
296    fn test_foreign_attribute() {
297        let attr = ForeignAttribute::new(Some(NameId(1)), NameId(2), "value".to_string());
298        assert!(attr.is_in_namespace(Some(NameId(1))));
299        assert!(!attr.is_in_namespace(None));
300    }
301
302    #[test]
303    fn test_annotation_empty() {
304        let ann = Annotation::new();
305        assert!(ann.is_empty());
306    }
307
308    #[test]
309    fn test_annotation_with_items() {
310        let mut ann = Annotation::new();
311
312        let content = XmlFragment::new(0, SourceSpan { start: 0, end: 10 });
313        let namespaces = NamespaceContextSnapshot {
314            default_ns: None,
315            bindings: vec![],
316        };
317
318        ann.add_appinfo(AppInfoElement::new(content.clone(), namespaces.clone()));
319        ann.add_documentation(DocumentationElement::new(content, namespaces));
320
321        assert!(!ann.is_empty());
322        assert_eq!(ann.appinfos().count(), 1);
323        assert_eq!(ann.documentations().count(), 1);
324    }
325
326    #[test]
327    fn test_documentation_by_lang() {
328        let mut ann = Annotation::new();
329
330        let content = XmlFragment::new(0, SourceSpan { start: 0, end: 10 });
331        let namespaces = NamespaceContextSnapshot {
332            default_ns: None,
333            bindings: vec![],
334        };
335
336        let mut doc_en = DocumentationElement::new(content.clone(), namespaces.clone());
337        doc_en.lang = Some("en".to_string());
338
339        let mut doc_fr = DocumentationElement::new(content, namespaces);
340        doc_fr.lang = Some("fr".to_string());
341
342        ann.add_documentation(doc_en);
343        ann.add_documentation(doc_fr);
344
345        assert!(ann.documentation_for_lang("en").is_some());
346        assert!(ann.documentation_for_lang("fr").is_some());
347        assert!(ann.documentation_for_lang("de").is_none());
348    }
349
350    #[test]
351    fn test_implicit_annotation() {
352        let attrs = vec![ForeignAttribute::new(
353            Some(NameId(1)),
354            NameId(2),
355            "value".to_string(),
356        )];
357
358        let ann = create_implicit_annotation(attrs, None);
359        assert!(!ann.is_empty());
360        assert_eq!(ann.attributes.len(), 1);
361    }
362
363    #[test]
364    fn test_is_foreign_attribute() {
365        let xsd_ns = NameId(1);
366        let xsi_ns = NameId(2);
367        let other_ns = NameId(3);
368
369        // Unqualified is not foreign
370        assert!(!is_foreign_attribute(None, xsd_ns, xsi_ns));
371
372        // XSD namespace is not foreign
373        assert!(!is_foreign_attribute(Some(xsd_ns), xsd_ns, xsi_ns));
374
375        // XSI namespace is not foreign
376        assert!(!is_foreign_attribute(Some(xsi_ns), xsd_ns, xsi_ns));
377
378        // Other namespace is foreign
379        assert!(is_foreign_attribute(Some(other_ns), xsd_ns, xsi_ns));
380    }
381}