code0_flow/flow_definition/
mod.rs1mod 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}