Skip to main content

microcad_lang/builtin/
import.rs

1// Copyright © 2025-2026 The µcad authors <info@microcad.xyz>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4//! Value importer
5
6use crate::{Id, Identifier, builtin::file_io::*, eval::ParameterValueList, value::*};
7use microcad_core::hash::HashMap;
8use miette::{Diagnostic, Report};
9use std::rc::Rc;
10
11use thiserror::Error;
12
13/// Export error stub.
14#[derive(Error, Debug, Diagnostic)]
15pub enum ImportError {
16    /// IO Error.
17    #[error("IO Error")]
18    IoError(#[from] std::io::Error),
19
20    /// The file was not found.
21    #[error("File not found: {0}")]
22    FileNotFound(std::path::PathBuf),
23
24    /// No importer found for file.
25    #[error("No importer found for file `{0}`")]
26    NoImporterForFile(std::path::PathBuf),
27
28    /// No importer with id.
29    #[error("No importer found with id `{0}`")]
30    NoImporterWithId(Id),
31
32    /// Found multiple importers with same file extensions.
33    #[error("Multiple importers for file extension: {0:?}")]
34    MultipleImportersForFileExtension(Vec<Id>),
35
36    /// Custom error.
37    #[error("{0}")]
38    #[diagnostic(transparent)]
39    CustomError(Report),
40
41    /// IO Error.
42    #[error("Value Error")]
43    #[diagnostic(transparent)]
44    ValueError(#[from] ValueError),
45}
46
47/// An importer trait to import files of a specific type.
48pub trait Importer: FileIoInterface {
49    /// The parameters this importer accepts (empty by default).
50    fn parameters(&self) -> ParameterValueList {
51        ParameterValueList::default()
52    }
53
54    /// Import a value with parameters as argument map.
55    fn import(&self, args: &Tuple) -> Result<Value, ImportError>;
56}
57
58/// Importer registry stores all importers.
59#[derive(Default)]
60pub struct ImporterRegistry {
61    io: FileIoRegistry<Rc<dyn Importer + 'static>>,
62    cache: HashMap<(String, String), Value>,
63}
64
65impl ImporterRegistry {
66    /// Add new importer to the registry.
67    ///
68    /// TODO Error handling.
69    pub fn insert(mut self, importer: impl Importer + 'static) -> Self {
70        let rc = Rc::new(importer);
71        self.io.insert(rc);
72        self
73    }
74
75    /// Get importer by id.
76    pub fn by_id(&self, id: &Id) -> Result<Rc<dyn Importer>, ImportError> {
77        self.io
78            .by_id(id)
79            .ok_or(ImportError::NoImporterWithId(id.clone()))
80    }
81
82    /// Get importer by filename.
83    pub fn by_filename(
84        &self,
85        filename: impl AsRef<std::path::Path>,
86    ) -> Result<Rc<dyn Importer>, ImportError> {
87        let importers = self.io.by_filename(filename.as_ref());
88        match importers.len() {
89            0 => Err(ImportError::NoImporterForFile(std::path::PathBuf::from(
90                filename.as_ref(),
91            ))),
92            1 => Ok(importers.first().expect("One importer").clone()),
93            _ => Err(ImportError::MultipleImportersForFileExtension(
94                importers.iter().map(|importer| importer.id()).collect(),
95            )),
96        }
97    }
98
99    pub(crate) fn get_cached(&self, filename: String, id: String) -> Option<Value> {
100        self.cache.get(&(filename, id)).cloned()
101    }
102
103    pub(crate) fn cache(&mut self, filename: String, id: String, value: Value) {
104        self.cache.insert((filename, id), value);
105    }
106}
107
108/// Importer Registry Access.
109pub trait ImporterRegistryAccess {
110    /// Error type.
111    type Error;
112
113    /// Import a value from an argument map
114    fn import(
115        &mut self,
116        args: &Tuple,
117        search_paths: &[std::path::PathBuf],
118    ) -> Result<Value, Self::Error>;
119}
120
121impl ImporterRegistryAccess for ImporterRegistry {
122    type Error = ImportError;
123
124    fn import(
125        &mut self,
126        args: &Tuple,
127        search_paths: &[std::path::PathBuf],
128    ) -> Result<Value, Self::Error> {
129        let filename: String = args.get("filename");
130
131        match [".".into()] // Search working dir first
132            .iter()
133            .chain(search_paths.iter())
134            .map(|path| path.join(&filename))
135            .find(|path| path.exists())
136        {
137            Some(path) => {
138                let mut arg_map = args.clone();
139                let filename = path.to_string_lossy().to_string();
140                arg_map.insert(
141                    Identifier::no_ref("filename"),
142                    Value::String(filename.clone()),
143                );
144                let id: String = arg_map.get("id");
145
146                // Check if value is in cache
147                if let Some(value) = self.get_cached(filename.clone(), id.clone()) {
148                    return Ok(value);
149                }
150
151                let value = if id.is_empty() {
152                    self.by_filename(&filename)
153                } else {
154                    self.by_id(&id.clone().into())
155                }?
156                .import(&arg_map)?;
157                self.cache(filename, id, value.clone());
158                Ok(value)
159            }
160            None => Err(ImportError::FileNotFound(std::path::PathBuf::from(
161                &filename,
162            ))),
163        }
164    }
165}
166
167#[test]
168fn importer() {
169    struct DummyImporter;
170
171    use crate::{builtin::Importer, parameter};
172    use microcad_core::Integer;
173
174    impl Importer for DummyImporter {
175        fn parameters(&self) -> ParameterValueList {
176            [parameter!(some_arg: Integer = 32)].into_iter().collect()
177        }
178
179        fn import(&self, args: &Tuple) -> Result<Value, ImportError> {
180            let some_arg: Integer = args.get("some_arg");
181            if some_arg == 32 {
182                Ok(Value::Integer(32))
183            } else {
184                Ok(Value::Integer(42))
185            }
186        }
187    }
188
189    impl FileIoInterface for DummyImporter {
190        fn id(&self) -> Id {
191            Id::new("dummy")
192        }
193
194        fn file_extensions(&self) -> Vec<Id> {
195            vec![Id::new("dummy"), Id::new("dmy")]
196        }
197    }
198
199    let registry = ImporterRegistry::default().insert(DummyImporter);
200
201    let by_id = registry.by_id(&"dummy".into()).expect("Dummy importer");
202
203    let mut args = crate::create_tuple!(some_arg = 32 as Integer);
204    let value = by_id.import(&args).expect("Value");
205    assert!(matches!(value, Value::Integer(32)));
206
207    let by_filename = registry.by_filename("test.dmy").expect("Filename");
208
209    args.insert(Identifier::no_ref("some_arg"), Value::Integer(42));
210    let value = by_filename.import(&args).expect("Value");
211
212    assert!(matches!(value, Value::Integer(42)));
213}