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