greentic_flow/
component_catalog.rs

1use std::{collections::HashMap, fs, path::Path};
2
3use serde::Deserialize;
4use serde_json::{Value, json};
5/// Minimal metadata needed to validate that a component exists and which config keys
6/// are required.
7#[derive(Debug, Clone)]
8pub struct ComponentMetadata {
9    pub id: String,
10    pub required_fields: Vec<String>,
11}
12
13pub trait ComponentCatalog: Send + Sync {
14    fn resolve(&self, component_id: &str) -> Option<ComponentMetadata>;
15}
16
17/// Catalog backed by component.manifest.json files on disk.
18#[derive(Debug, Default, Clone)]
19pub struct ManifestCatalog {
20    entries: HashMap<String, ComponentMetadata>,
21}
22
23#[derive(Deserialize)]
24struct Manifest {
25    id: String,
26    #[serde(default)]
27    config_schema: Option<Schema>,
28}
29
30#[derive(Deserialize, Default)]
31struct Schema {
32    #[serde(default)]
33    required: Vec<String>,
34}
35
36impl ManifestCatalog {
37    pub fn load_from_paths(paths: &[impl AsRef<Path>]) -> Self {
38        let mut entries = HashMap::new();
39        for path in paths {
40            let path = path.as_ref();
41            if let Ok(text) = fs::read_to_string(path)
42                && let Ok(mut value) = serde_json::from_str::<Value>(&text)
43            {
44                normalize_manifest_value(&mut value);
45                if let Ok(manifest) = serde_json::from_value::<Manifest>(value) {
46                    entries.insert(
47                        manifest.id.clone(),
48                        ComponentMetadata {
49                            id: manifest.id,
50                            required_fields: manifest
51                                .config_schema
52                                .unwrap_or_default()
53                                .required
54                                .clone(),
55                        },
56                    );
57                    continue;
58                }
59            }
60            // Continue without crashing on unreadable manifests to keep the catalog usable.
61        }
62        ManifestCatalog { entries }
63    }
64}
65
66impl ComponentCatalog for ManifestCatalog {
67    fn resolve(&self, component_id: &str) -> Option<ComponentMetadata> {
68        self.entries.get(component_id).cloned()
69    }
70}
71
72/// Catalog that can be seeded programmatically for tests.
73#[derive(Debug, Default, Clone)]
74pub struct MemoryCatalog {
75    entries: HashMap<String, ComponentMetadata>,
76}
77
78impl MemoryCatalog {
79    pub fn insert(&mut self, meta: ComponentMetadata) {
80        self.entries.insert(meta.id.clone(), meta);
81    }
82}
83
84impl ComponentCatalog for MemoryCatalog {
85    fn resolve(&self, component_id: &str) -> Option<ComponentMetadata> {
86        self.entries.get(component_id).cloned()
87    }
88}
89
90impl ComponentCatalog for Box<dyn ComponentCatalog> {
91    fn resolve(&self, component_id: &str) -> Option<ComponentMetadata> {
92        self.as_ref().resolve(component_id)
93    }
94}
95
96/// Normalize legacy manifest shapes in-place (e.g., operations as an array of strings).
97pub fn normalize_manifest_value(value: &mut Value) {
98    if let Some(ops) = value.get_mut("operations").and_then(Value::as_array_mut) {
99        let mut normalized = Vec::with_capacity(ops.len());
100        for entry in ops.drain(..) {
101            if let Value::String(s) = entry {
102                normalized.push(json!({ "name": s }));
103            } else {
104                normalized.push(entry);
105            }
106        }
107        *ops = normalized;
108    }
109}