Skip to main content

microcad_lang/builtin/
export.rs

1// Copyright © 2025-2026 The µcad authors <info@microcad.xyz>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4//! Model export
5
6use crate::{
7    Id, builtin::file_io::*, eval::*, model::*, parameter, render::RenderError, value::Value,
8};
9use miette::Diagnostic;
10use std::rc::Rc;
11
12use thiserror::Error;
13
14/// Export error stub.
15#[derive(Error, Debug, Diagnostic)]
16pub enum ExportError {
17    /// IO Error.
18    #[error("IO Error")]
19    IoError(#[from] std::io::Error),
20
21    /// Format Error.
22    #[error("Format Error")]
23    FormatError(#[from] std::fmt::Error),
24
25    /// The model does not contain any export attribute.
26    #[error("No export attribute found in workbench (mark it with `#[export(\"filename\")`")]
27    NoExportAttribute,
28
29    /// No exporter found for file.
30    #[error("No exporter found for file `{0}`")]
31    NoExporterForFile(std::path::PathBuf),
32
33    /// No exporter for id.
34    #[error("No exporter found with id `{0}`")]
35    NoExporterWithId(Id),
36
37    /// No exporter id.
38    #[error("Multiple exporters for file extension: {0:?}")]
39    MultipleExportersForFileExtension(Vec<Id>),
40
41    /// Render error during export.
42    #[error("Render error: {0}")]
43    RenderError(#[from] RenderError),
44}
45
46/// Exporter trait.
47///
48/// Implement this trait for your custom file exporter.
49pub trait Exporter: FileIoInterface {
50    /// Parameters that add exporter specific attributes to a model.
51    ///
52    /// Let's assume an exporter `foo` has a model parameter `bar = 23` as parameter value list.
53    /// The parameter `bar` can be set to `42` with:
54    ///
55    /// ```ucad
56    /// #[export = "myfile.foo"]
57    /// #[foo = (bar = 42)]
58    /// Circle(42mm);
59    /// ```
60    fn model_parameters(&self) -> ParameterValueList {
61        ParameterValueList::default()
62    }
63
64    /// Parameters for the export attribute: `export = svg("filename.svg")`
65    fn export_parameters(&self) -> ParameterValueList {
66        [parameter!(filename: String), parameter!(resolution: Length)]
67            .into_iter()
68            .collect()
69    }
70
71    /// Export the model if the model is marked for export.
72    fn export(&self, model: &Model, filename: &std::path::Path) -> Result<Value, ExportError>;
73
74    /// The expected model output type of this exporter.
75    ///
76    /// Reimplement this function when your export output format only accepts specific model output types.
77    fn output_type(&self) -> OutputType {
78        OutputType::NotDetermined
79    }
80}
81
82/// Exporter registry.
83///
84/// A database in which all exporters are stored.
85///
86/// The registry is used to find exporters by their id and their file extension.
87#[derive(Default)]
88pub struct ExporterRegistry {
89    io: FileIoRegistry<Rc<dyn Exporter>>,
90}
91
92impl ExporterRegistry {
93    /// Create new registry.
94    pub fn new() -> Self {
95        Self {
96            io: FileIoRegistry::default(),
97        }
98    }
99
100    /// Add new exporter to the registry.
101    ///
102    /// TODO Error handling.
103    pub fn insert(mut self, exporter: impl Exporter + 'static) -> Self {
104        let rc = Rc::new(exporter);
105        self.io.insert(rc);
106        self
107    }
108
109    /// Get exporter by filename.
110    pub fn by_filename(
111        &self,
112        filename: impl AsRef<std::path::Path>,
113    ) -> Result<Rc<dyn Exporter>, ExportError> {
114        let importers = self.io.by_filename(filename.as_ref());
115        match importers.len() {
116            0 => Err(ExportError::NoExporterForFile(std::path::PathBuf::from(
117                filename.as_ref(),
118            ))),
119            1 => Ok(importers.first().expect("One importer").clone()),
120            _ => Err(ExportError::MultipleExportersForFileExtension(
121                importers.iter().map(|importer| importer.id()).collect(),
122            )),
123        }
124    }
125}
126
127/// Exporter access.
128pub trait ExporterAccess {
129    /// Get exporter by id.
130    fn exporter_by_id(&self, id: &Id) -> Result<Rc<dyn Exporter>, ExportError>;
131
132    /// Get exporter by filename.
133    fn exporter_by_filename(
134        &self,
135        filename: &std::path::Path,
136    ) -> Result<Rc<dyn Exporter>, ExportError>;
137
138    /// Find an exporter by filename, or by id.
139    fn find_exporter(
140        &self,
141        filename: &std::path::Path,
142        id: &Option<Id>,
143    ) -> Result<Rc<dyn Exporter>, ExportError> {
144        match id {
145            Some(id) => self.exporter_by_id(id),
146            None => self.exporter_by_filename(filename),
147        }
148    }
149}
150
151impl ExporterAccess for ExporterRegistry {
152    fn exporter_by_id(&self, id: &Id) -> Result<Rc<dyn Exporter>, ExportError> {
153        match self.io.by_id(id) {
154            Some(exporter) => Ok(exporter),
155            None => Err(ExportError::NoExporterWithId(id.clone())),
156        }
157    }
158
159    fn exporter_by_filename(
160        &self,
161        filename: &std::path::Path,
162    ) -> Result<Rc<dyn Exporter>, ExportError> {
163        self.by_filename(filename)
164    }
165}