Skip to main content

code0_flow/flow_definition/
mod.rs

1mod error;
2mod feature;
3
4use crate::flow_definition::error::ReaderError;
5use crate::flow_definition::feature::Feature;
6use crate::flow_definition::feature::version::HasVersion;
7use serde::de::DeserializeOwned;
8use serde::{Deserialize, Serialize};
9use std::fs;
10use std::path::{Path, PathBuf};
11use tucana::shared::{
12    DefinitionDataType, FlowType, FunctionDefinition, Module, ModuleConfigurationDefinition,
13    RuntimeFlowType, RuntimeFunctionDefinition, Translation,
14};
15use walkdir::WalkDir;
16
17pub struct Reader {
18    should_break: bool,
19    accepted_features: Vec<String>,
20    accepted_version: Option<String>,
21    path: String,
22}
23
24#[derive(Serialize, Deserialize, Clone, Debug, Default)]
25pub struct ModuleConfiguration {
26    pub identifier: String,
27    pub name: Vec<Translation>,
28    pub description: Vec<Translation>,
29    pub documentation: String,
30    pub author: String,
31    pub icon: String,
32    #[serde(default, skip_serializing_if = "String::is_empty")]
33    pub version: String,
34}
35
36#[derive(Clone, Debug, Default)]
37struct LoadedModule {
38    name: String,
39    config: ModuleConfiguration,
40    data_types: Vec<DefinitionDataType>,
41    flow_types: Vec<FlowType>,
42    runtime_flow_types: Vec<RuntimeFlowType>,
43    functions: Vec<FunctionDefinition>,
44    runtime_functions: Vec<RuntimeFunctionDefinition>,
45    configurations: Vec<ModuleConfigurationDefinition>,
46}
47
48impl LoadedModule {
49    fn into_module(mut self) -> Module {
50        if self.config.identifier.is_empty() {
51            self.config.identifier = self.name.clone();
52        }
53
54        Module {
55            identifier: self.config.identifier,
56            name: self.config.name,
57            description: self.config.description,
58            documentation: self.config.documentation,
59            author: self.config.author,
60            icon: self.config.icon,
61            version: self.config.version,
62            flow_types: self.flow_types,
63            runtime_flow_types: self.runtime_flow_types,
64            function_definitions: self.functions,
65            runtime_function_definitions: self.runtime_functions,
66            definition_data_types: self.data_types,
67            configurations: self.configurations,
68            definitions: Vec::new(),
69        }
70    }
71}
72
73impl Reader {
74    pub fn configure(
75        path: String,
76        should_break: bool,
77        accepted_features: Vec<String>,
78        accepted_version: Option<String>,
79    ) -> Self {
80        Self {
81            should_break,
82            accepted_features,
83            accepted_version,
84            path,
85        }
86    }
87
88    pub fn read_features(&self) -> Result<Vec<Feature>, ReaderError> {
89        let modules = self
90            .read_loaded_modules()
91            .map_err(|err| ReaderError::ReadFeatureError {
92                path: self.path.clone(),
93                source: Box::new(err),
94            })?;
95
96        Ok(modules
97            .into_iter()
98            .map(|module| Feature {
99                name: module.name,
100                data_types: module.data_types,
101                flow_types: module.flow_types,
102                runtime_functions: module.runtime_functions,
103                functions: module.functions,
104            })
105            .collect())
106    }
107
108    pub fn read_modules(&self) -> Result<Vec<Module>, ReaderError> {
109        let modules = self
110            .read_loaded_modules()
111            .map_err(|err| ReaderError::ReadFeatureError {
112                path: self.path.clone(),
113                source: Box::new(err),
114            })?;
115
116        Ok(modules.into_iter().map(LoadedModule::into_module).collect())
117    }
118
119    fn read_loaded_modules(&self) -> Result<Vec<LoadedModule>, ReaderError> {
120        let root = Path::new(&self.path);
121        if !root.exists() || !root.is_dir() {
122            return Err(ReaderError::ReadDirectoryError {
123                path: root.to_path_buf(),
124                error: std::io::Error::new(
125                    std::io::ErrorKind::NotFound,
126                    format!("Definition path {} does not exist", root.display()),
127                ),
128            });
129        }
130
131        let mut modules = Vec::new();
132        for module_dir in find_module_directories(root) {
133            let module_name = module_name_from_paths(root, &module_dir);
134            if !self.feature_allowed(&module_name, &module_dir) {
135                continue;
136            }
137
138            let mut module = LoadedModule {
139                name: module_name,
140                ..Default::default()
141            };
142
143            let module_file = module_dir.join("module.json");
144            if module_file.is_file() {
145                match read_json_file::<ModuleConfiguration>(&module_file) {
146                    Ok(config) => module.config = config,
147                    Err(err) if self.should_break => return Err(err),
148                    Err(err) => log::warn!(
149                        "Skipping invalid module definition {}: {:?}",
150                        module_file.display(),
151                        err
152                    ),
153                }
154            }
155
156            let entries =
157                fs::read_dir(&module_dir).map_err(|error| ReaderError::ReadDirectoryError {
158                    path: module_dir.clone(),
159                    error,
160                })?;
161
162            for entry in entries {
163                let entry = entry.map_err(ReaderError::DirectoryEntryError)?;
164                let file_type = entry
165                    .file_type()
166                    .map_err(ReaderError::DirectoryEntryError)?;
167                if !file_type.is_dir() {
168                    continue;
169                }
170
171                let path = entry.path();
172                let dir_name = entry.file_name().to_string_lossy().to_string();
173
174                match dir_name.as_str() {
175                    "flow_type" | "flow_types" => {
176                        module
177                            .flow_types
178                            .extend(load_json_dir::<FlowType>(&path, self.should_break)?);
179                    }
180                    "runtime_flow_type" | "runtime_flow_types" => {
181                        module
182                            .runtime_flow_types
183                            .extend(load_json_dir::<RuntimeFlowType>(&path, self.should_break)?);
184                    }
185                    "data_type" | "data_types" => {
186                        module
187                            .data_types
188                            .extend(load_json_dir::<DefinitionDataType>(
189                                &path,
190                                self.should_break,
191                            )?);
192                    }
193                    "runtime_definition" | "runtime_definitions" | "runtime_functions" => {
194                        module.runtime_functions.extend(
195                            load_json_dir::<RuntimeFunctionDefinition>(&path, self.should_break)?,
196                        );
197                    }
198                    "function" | "functions" => {
199                        module.functions.extend(load_json_dir::<FunctionDefinition>(
200                            &path,
201                            self.should_break,
202                        )?);
203                    }
204                    "configuration" | "configurations" => {
205                        module.configurations.extend(
206                            load_json_dir::<ModuleConfigurationDefinition>(
207                                &path,
208                                self.should_break,
209                            )?,
210                        );
211                    }
212                    _ => {}
213                }
214            }
215
216            module
217                .data_types
218                .retain(|item| item.is_accepted(&self.accepted_version));
219            module
220                .flow_types
221                .retain(|item| item.is_accepted(&self.accepted_version));
222            module
223                .runtime_flow_types
224                .retain(|item| item.is_accepted(&self.accepted_version));
225            module
226                .functions
227                .retain(|item| item.is_accepted(&self.accepted_version));
228            module
229                .runtime_functions
230                .retain(|item| item.is_accepted(&self.accepted_version));
231
232            modules.push(module);
233        }
234
235        Ok(modules)
236    }
237
238    fn feature_allowed(&self, module_name: &str, module_path: &Path) -> bool {
239        if self.accepted_features.is_empty() {
240            return true;
241        }
242
243        let short_name = module_path
244            .file_name()
245            .and_then(|name| name.to_str())
246            .unwrap_or_default();
247
248        self.accepted_features
249            .iter()
250            .any(|feature| feature == module_name || feature == short_name)
251    }
252}
253
254fn read_json_file<T: DeserializeOwned>(path: &Path) -> Result<T, ReaderError> {
255    let content = fs::read_to_string(path).map_err(|error| ReaderError::ReadFileError {
256        path: path.to_path_buf(),
257        error,
258    })?;
259
260    serde_json::from_str::<T>(&content).map_err(|error| ReaderError::JsonError {
261        path: path.to_path_buf(),
262        error,
263    })
264}
265
266fn load_json_dir<T: DeserializeOwned>(
267    dir: &Path,
268    should_break: bool,
269) -> Result<Vec<T>, ReaderError> {
270    let mut items = Vec::new();
271    for file in WalkDir::new(dir)
272        .into_iter()
273        .filter_map(Result::ok)
274        .map(|entry| entry.into_path())
275        .filter(|path| {
276            path.is_file()
277                && path
278                    .extension()
279                    .and_then(|ext| ext.to_str())
280                    .is_some_and(|ext| ext.eq_ignore_ascii_case("json"))
281        })
282    {
283        match read_json_file::<T>(file.as_path()) {
284            Ok(item) => items.push(item),
285            Err(err) if should_break => return Err(err),
286            Err(err) => log::warn!("Skipping invalid definition {}: {:?}", file.display(), err),
287        }
288    }
289
290    Ok(items)
291}
292
293fn find_module_directories(root: &Path) -> Vec<PathBuf> {
294    let mut modules = WalkDir::new(root)
295        .into_iter()
296        .filter_map(Result::ok)
297        .filter(|entry| entry.file_type().is_dir())
298        .map(|entry| entry.into_path())
299        .filter(|path| looks_like_module(path))
300        .collect::<Vec<_>>();
301    modules.sort();
302    modules
303}
304
305fn looks_like_module(path: &Path) -> bool {
306    let entries = match fs::read_dir(path) {
307        Ok(entries) => entries,
308        Err(_) => return false,
309    };
310
311    entries.flatten().any(|entry| {
312        let file_type = match entry.file_type() {
313            Ok(file_type) => file_type,
314            Err(_) => return false,
315        };
316
317        let name = entry.file_name().to_string_lossy().to_string();
318        name == "module.json" || (file_type.is_dir() && is_definition_dir(&name))
319    })
320}
321
322fn is_definition_dir(name: &str) -> bool {
323    matches!(
324        name,
325        "flow_type"
326            | "flow_types"
327            | "runtime_flow_type"
328            | "runtime_flow_types"
329            | "data_type"
330            | "data_types"
331            | "runtime_definition"
332            | "runtime_definitions"
333            | "runtime_functions"
334            | "function"
335            | "functions"
336            | "configuration"
337            | "configurations"
338    )
339}
340
341fn module_name_from_paths(root: &Path, module_path: &Path) -> String {
342    let relative = module_path
343        .strip_prefix(root)
344        .ok()
345        .and_then(|path| path.to_str())
346        .unwrap_or_default();
347
348    if relative.is_empty() || relative == "." {
349        module_path
350            .file_name()
351            .and_then(|name| name.to_str())
352            .unwrap_or("module")
353            .to_string()
354    } else {
355        relative.to_string()
356    }
357}