Skip to main content

xbrl_rs/instance/
writer.rs

1//! XBRL instance XML writer (serialization).
2
3use crate::{
4    Context, Fact, InstanceDocument, ItemFact, NamespacePrefix, NamespaceUri, Period, QName,
5    TupleFact, error::Result, instance::Unit,
6};
7use quick_xml::{
8    Writer,
9    events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event},
10};
11use std::{collections::HashMap, io};
12
13/// The writer for XBRL instance documents.
14pub struct InstanceWriter<W> {
15    /// The XML writer for the instance document.
16    writer: Writer<W>,
17    /// Flag to indicate if the root element is an XBRL instance element.
18    is_xbrl_root: bool,
19}
20
21impl<W: io::Write> InstanceWriter<W> {
22    pub fn new(writer: Writer<W>, is_xbrl_root: bool) -> Self {
23        Self {
24            writer,
25            is_xbrl_root,
26        }
27    }
28
29    /// Consume the writer and return the underlying writer.
30    pub fn into_inner(self) -> W {
31        self.writer.into_inner()
32    }
33
34    /// Serialize [`InstanceDocument`] to an XBRL XML document.
35    ///
36    /// When `xbrl_root` is `true`, the XML declaration is omitted (e.g. when embedding
37    /// the XBRL element inside an outer document).
38    pub fn write(&mut self, instance: &InstanceDocument) -> Result<()> {
39        // XML declaration is only needed if the root element is <xbrli:xbrl>.
40        if self.is_xbrl_root {
41            self.writer
42                .write_event(Event::Decl(BytesDecl::new("1.0", Some("utf-8"), None)))?;
43        }
44
45        let prefix_to_uri = instance.namespaces();
46        let uri_to_prefix: HashMap<NamespaceUri, NamespacePrefix> = prefix_to_uri
47            .iter()
48            .map(|(prefix, uri)| (uri.clone(), prefix.clone()))
49            .collect();
50        let mut namespaces: Vec<_> = prefix_to_uri.iter().collect();
51        namespaces.sort_by_key(|(prefix, _)| *prefix);
52
53        // <xbrli:xbrl> root element with namespace declarations
54        let mut root = BytesStart::new("xbrli:xbrl");
55
56        for (prefix, uri) in &namespaces {
57            let attr_name = format!("xmlns:{prefix}");
58            root.push_attribute((attr_name.as_str(), uri.as_str()));
59        }
60
61        self.writer.write_event(Event::Start(root))?;
62
63        // <link:schemaRef> elements
64        for href in instance.schema_refs() {
65            let mut elem = BytesStart::new("link:schemaRef");
66            elem.push_attribute(("xlink:type", "simple"));
67            elem.push_attribute(("xlink:href", href.as_str()));
68            self.writer.write_event(Event::Empty(elem))?;
69        }
70
71        // <link:roleRef> elements
72        for role_uri in instance.role_refs() {
73            let mut elem = BytesStart::new("link:roleRef");
74            elem.push_attribute(("roleURI", role_uri.as_str()));
75            elem.push_attribute(("xlink:type", "simple"));
76            elem.push_attribute(("xlink:href", ""));
77            self.writer.write_event(Event::Empty(elem))?;
78        }
79
80        // <link:arcroleRef> elements
81        for arcrole_uri in instance.arcrole_refs() {
82            let mut elem = BytesStart::new("link:arcroleRef");
83            elem.push_attribute(("arcroleURI", arcrole_uri.as_str()));
84            elem.push_attribute(("xlink:type", "simple"));
85            elem.push_attribute(("xlink:href", ""));
86            self.writer.write_event(Event::Empty(elem))?;
87        }
88
89        // <xbrli:context> elements
90        let mut ctx_sorted: Vec<_> = instance.contexts().iter().collect();
91        ctx_sorted.sort_by_key(|(id, _)| *id);
92        for (_, context) in &ctx_sorted {
93            write_context(&mut self.writer, context, &uri_to_prefix)?;
94        }
95
96        // <xbrli:unit> elements
97        let mut unit_sorted: Vec<_> = instance.units().iter().collect();
98        unit_sorted.sort_by_key(|(id, _)| *id);
99        for (_, unit) in &unit_sorted {
100            write_unit(&mut self.writer, unit, &uri_to_prefix)?;
101        }
102
103        for fact in instance.facts() {
104            write_fact(&mut self.writer, fact, &uri_to_prefix)?;
105        }
106
107        // </xbrli:xbrl>
108        self.writer
109            .write_event(Event::End(BytesEnd::new("xbrli:xbrl")))?;
110
111        Ok(())
112    }
113}
114
115fn write_context<W: std::io::Write>(
116    writer: &mut Writer<W>,
117    context: &Context,
118    uri_to_prefix: &HashMap<NamespaceUri, NamespacePrefix>,
119) -> Result<()> {
120    let mut element = BytesStart::new("xbrli:context");
121    element.push_attribute(("id", context.id.as_str()));
122    writer.write_event(Event::Start(element))?;
123
124    // Entity
125    writer.write_event(Event::Start(BytesStart::new("xbrli:entity")))?;
126    let mut identifier = BytesStart::new("xbrli:identifier");
127    identifier.push_attribute(("scheme", context.entity.scheme.as_str()));
128    writer.write_event(Event::Start(identifier))?;
129    writer.write_event(Event::Text(BytesText::new(&context.entity.value)))?;
130    writer.write_event(Event::End(BytesEnd::new("xbrli:identifier")))?;
131    writer.write_event(Event::End(BytesEnd::new("xbrli:entity")))?;
132
133    // Period
134    writer.write_event(Event::Start(BytesStart::new("xbrli:period")))?;
135    match &context.period {
136        Period::Instant { date } => {
137            writer.write_event(Event::Start(BytesStart::new("xbrli:instant")))?;
138            writer.write_event(Event::Text(BytesText::new(date)))?;
139            writer.write_event(Event::End(BytesEnd::new("xbrli:instant")))?;
140        }
141        Period::Duration { start, end } => {
142            writer.write_event(Event::Start(BytesStart::new("xbrli:startDate")))?;
143            writer.write_event(Event::Text(BytesText::new(start)))?;
144            writer.write_event(Event::End(BytesEnd::new("xbrli:startDate")))?;
145            writer.write_event(Event::Start(BytesStart::new("xbrli:endDate")))?;
146            writer.write_event(Event::Text(BytesText::new(end)))?;
147            writer.write_event(Event::End(BytesEnd::new("xbrli:endDate")))?;
148        }
149        Period::Forever => {
150            writer.write_event(Event::Empty(BytesStart::new("xbrli:forever")))?;
151        }
152    }
153    writer.write_event(Event::End(BytesEnd::new("xbrli:period")))?;
154
155    // Dimensions (scenario)
156    if !context.dimensions.is_empty() {
157        writer.write_event(Event::Start(BytesStart::new("xbrli:scenario")))?;
158        let mut dim_sorted: Vec<_> = context.dimensions.iter().collect();
159        dim_sorted.sort_by_key(|(dim, _)| *dim);
160        for (dimension, member) in &dim_sorted {
161            let mut explicit = BytesStart::new("xbrldi:explicitMember");
162            let dimension = QName {
163                prefix: uri_to_prefix.get(&dimension.namespace_uri).cloned(),
164                local_name: dimension.local_name.clone(),
165            }
166            .to_string();
167            let member = QName {
168                prefix: uri_to_prefix.get(&member.namespace_uri).cloned(),
169                local_name: member.local_name.clone(),
170            }
171            .to_string();
172
173            explicit.push_attribute(("dimension", dimension.as_str()));
174
175            if member.is_empty() {
176                writer.write_event(Event::Empty(explicit))?;
177            } else {
178                writer.write_event(Event::Start(explicit))?;
179                writer.write_event(Event::Text(BytesText::new(&member)))?;
180                writer.write_event(Event::End(BytesEnd::new("xbrldi:explicitMember")))?;
181            }
182        }
183        writer.write_event(Event::End(BytesEnd::new("xbrli:scenario")))?;
184    }
185
186    writer.write_event(Event::End(BytesEnd::new("xbrli:context")))?;
187
188    Ok(())
189}
190
191fn write_unit<W: io::Write>(
192    writer: &mut Writer<W>,
193    unit: &Unit,
194    uri_to_prefix: &HashMap<NamespaceUri, NamespacePrefix>,
195) -> Result<()> {
196    let mut elem = BytesStart::new("xbrli:unit");
197    elem.push_attribute(("id", unit.id.as_str()));
198    writer.write_event(Event::Start(elem))?;
199
200    if unit.has_denominator() {
201        writer.write_event(Event::Start(BytesStart::new("xbrli:divide")))?;
202        writer.write_event(Event::Start(BytesStart::new("xbrli:numerator")))?;
203
204        for measure in &unit.numerator {
205            let qname = QName {
206                prefix: uri_to_prefix.get(&measure.namespace_uri).cloned(),
207                local_name: measure.local_name.clone(),
208            };
209            writer.write_event(Event::Start(BytesStart::new("xbrli:measure")))?;
210            writer.write_event(Event::Text(BytesText::new(&qname.to_string())))?;
211            writer.write_event(Event::End(BytesEnd::new("xbrli:measure")))?;
212        }
213
214        writer.write_event(Event::End(BytesEnd::new("xbrli:numerator")))?;
215        writer.write_event(Event::Start(BytesStart::new("xbrli:denominator")))?;
216
217        for measure in &unit.denominator {
218            let qname = QName {
219                prefix: uri_to_prefix.get(&measure.namespace_uri).cloned(),
220                local_name: measure.local_name.clone(),
221            };
222            writer.write_event(Event::Start(BytesStart::new("xbrli:measure")))?;
223            writer.write_event(Event::Text(BytesText::new(&qname.to_string())))?;
224            writer.write_event(Event::End(BytesEnd::new("xbrli:measure")))?;
225        }
226
227        writer.write_event(Event::End(BytesEnd::new("xbrli:denominator")))?;
228        writer.write_event(Event::End(BytesEnd::new("xbrli:divide")))?;
229    } else {
230        for measure in &unit.numerator {
231            let qname = QName {
232                prefix: uri_to_prefix.get(&measure.namespace_uri).cloned(),
233                local_name: measure.local_name.clone(),
234            };
235            writer.write_event(Event::Start(BytesStart::new("xbrli:measure")))?;
236            writer.write_event(Event::Text(BytesText::new(&qname.to_string())))?;
237            writer.write_event(Event::End(BytesEnd::new("xbrli:measure")))?;
238        }
239    }
240
241    writer.write_event(Event::End(BytesEnd::new("xbrli:unit")))?;
242
243    Ok(())
244}
245
246fn write_fact<W: io::Write>(
247    writer: &mut Writer<W>,
248    fact: &Fact,
249    namespaces: &HashMap<NamespaceUri, NamespacePrefix>,
250) -> Result<()> {
251    match fact {
252        Fact::Item(item) => write_item_fact(writer, item, namespaces),
253        Fact::Tuple(tuple) => write_tuple_fact(writer, tuple, namespaces),
254    }
255}
256
257fn write_tuple_fact<W: io::Write>(
258    writer: &mut Writer<W>,
259    fact: &TupleFact,
260    namespaces: &HashMap<NamespaceUri, NamespacePrefix>,
261) -> Result<()> {
262    let concept_name = fact.concept_name();
263    let prefix = namespaces.get(&concept_name.namespace_uri);
264    let local_name = &concept_name.local_name;
265    let concept_name = QName {
266        prefix: prefix.cloned(),
267        local_name: local_name.clone(),
268    }
269    .to_string();
270    let mut element = BytesStart::new(&concept_name);
271
272    if let Some(id) = fact.id() {
273        element.push_attribute(("id", id));
274    }
275
276    if fact.is_nil() {
277        element.push_attribute(("xsi:nil", "true"));
278        writer.write_event(Event::Empty(element))?;
279        return Ok(());
280    }
281
282    if fact.children().is_empty() {
283        writer.write_event(Event::Empty(element))?;
284        return Ok(());
285    }
286
287    writer.write_event(Event::Start(element))?;
288    for child in fact.children() {
289        write_fact(writer, child, namespaces)?;
290    }
291    writer.write_event(Event::End(BytesEnd::new(concept_name)))?;
292    Ok(())
293}
294
295fn write_item_fact<W: std::io::Write>(
296    writer: &mut Writer<W>,
297    fact: &ItemFact,
298    namespaces: &HashMap<NamespaceUri, NamespacePrefix>,
299) -> Result<()> {
300    let concept_name = fact.concept_name();
301    let prefix = namespaces.get(&concept_name.namespace_uri);
302    let local_name = &concept_name.local_name;
303    let concept_name = QName {
304        prefix: prefix.cloned(),
305        local_name: local_name.clone(),
306    }
307    .to_string();
308    let mut element = BytesStart::new(&concept_name);
309
310    if let Some(id) = fact.id() {
311        element.push_attribute(("id", id));
312    }
313
314    element.push_attribute(("contextRef", fact.context_ref()));
315
316    if let Some(unit_ref) = fact.unit_ref() {
317        element.push_attribute(("unitRef", unit_ref));
318    }
319
320    if !fact.is_nil() {
321        if let Some(decimals) = fact.decimals() {
322            element.push_attribute(("decimals", decimals.to_string().as_str()));
323        }
324
325        if let Some(precision) = fact.precision() {
326            element.push_attribute(("precision", precision.to_string().as_str()));
327        }
328    }
329
330    if fact.is_nil() {
331        element.push_attribute(("xsi:nil", "true"));
332        writer.write_event(Event::Empty(element))?;
333    } else {
334        writer.write_event(Event::Start(element))?;
335        writer.write_event(Event::Text(BytesText::new(fact.value())))?;
336        writer.write_event(Event::End(BytesEnd::new(&concept_name)))?;
337    }
338
339    Ok(())
340}
341
342#[cfg(test)]
343mod tests {
344    use super::*;
345    use quick_xml::Writer;
346
347    fn write_instance(instance: &InstanceDocument, is_xbrl_root: bool) -> String {
348        let mut output = Vec::new();
349        let mut writer = InstanceWriter::new(Writer::new(&mut output), is_xbrl_root);
350        writer.write(instance).unwrap();
351        String::from_utf8(output).unwrap()
352    }
353
354    #[test]
355    fn test_write_instance_xbrl_root() {
356        let instance = InstanceDocument::default();
357        let output = write_instance(&instance, true);
358        assert!(output.starts_with("<?xml version=\"1.0\" encoding=\"utf-8\"?>"));
359    }
360
361    #[test]
362    fn test_write_instance_non_xbrl_root() {
363        let instance = InstanceDocument::default();
364        let output = write_instance(&instance, false);
365        assert!(!output.starts_with("<?xml version=\"1.0\" encoding=\"utf-8\"?>"));
366        assert!(output.starts_with("<xbrli:xbrl"));
367    }
368}