microcad_lang/builtin/
import.rs

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