arcella_types/
spec.rs

1// arcella/arcella-types/src/spec.rs
2//
3// Copyright (c) 2025 Alexey Rybakov, Arcella Team
4//
5// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE>
6// or the MIT license <LICENSE-MIT>, at your option.
7// This file may not be copied, modified, or distributed
8// except according to those terms.
9
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12
13/// A serializable and inspectable representation of a WebAssembly Component Model item.
14///
15/// This enum captures the structure of component imports and exports in a way that can be
16/// serialized to TOML/JSON, displayed in CLI output, or used for interface validation.
17/// It abstracts over low-level `wasmtime::component::types::ComponentItem` to provide
18/// a stable, human-readable format.
19///
20/// Note: This representation is intentionally lossy for MVP. Full WIT type fidelity
21/// will be added in later versions using `wit-parser`.
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23pub enum ComponentItemSpec {
24    /// A WebAssembly component function with named parameters and result types.
25    #[serde(rename = "func")]
26    ComponentFunc {
27        /// List of `(parameter_name, type_name)` pairs.
28        #[serde(default)]
29        params: Vec<(String, String)>,
30
31        /// List of result type names (empty for void functions).
32        #[serde(default)]
33        results: Vec<String>,
34    },
35
36    /// A core WebAssembly function (not part of the Component Model).
37    ///
38    /// Should generally not appear in valid components, but included for completeness.
39    #[serde(rename = "core_func")]
40    CoreFunc(String), // TODO: Registered type
41
42    /// A core WebAssembly module embedded within a component.
43    ///
44    /// Represented as a placeholder string in MVP.
45    #[serde(rename = "module")]
46    Module(String), // TODO: Extern type
47
48    /// A nested WebAssembly component.
49    ///
50    /// Contains its own imports and exports, forming a hierarchical structure.
51    #[serde(rename = "component")]
52    Component{
53        /// Imports declared by the nested component.
54        #[serde(default)]
55        imports: HashMap<String, ComponentItemSpec>,
56
57        /// Exports provided by the nested component.
58        #[serde(default)]
59        exports: HashMap<String, ComponentItemSpec>,
60    },
61
62    /// An instantiated component (e.g., a resolved instance like `wasi:cli/stdio`).
63    ///
64    /// Only contains exports, as instances are the result of linking.
65    #[serde(rename = "instance")]
66    ComponentInstance {
67        /// The exported items of this instance.
68        #[serde(default)]
69        exports: HashMap<String, ComponentItemSpec>,
70    },
71
72    /// A user-defined type (record, variant, enum, flags, etc.).
73    ///
74    /// Represented as a placeholder string in MVP.
75    #[serde(rename = "type_def")]
76    Type (String),
77    
78    /// A resource handle (e.g., file descriptor, socket).
79    ///
80    /// Represented as a placeholder string in MVP.
81    #[serde(rename = "resource")]
82    Resource(String),
83
84    /// A fallback for unrecognized or unrepresentable component items.
85    ///
86    /// Used to prevent parsing failures when encountering new or malformed items.
87    #[serde(rename = "unknown")]
88    Unknown{
89        /// Optional debug information about the unrecognized item.
90        #[serde(skip_serializing_if = "Option::is_none")]
91        debug: Option<String>,
92    },
93}
94
95impl std::fmt::Display for ComponentItemSpec {
96    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97        match self {
98            Self::ComponentFunc { params, results } => {
99                write!(f, "func(")?;
100                for (i, (name, ty)) in params.iter().enumerate() {
101                    if i > 0 { write!(f, ", ")?; }
102                    write!(f, "{}: {}", name, ty)?;
103                }
104                write!(f, ")")?;
105                if !results.is_empty() {
106                    write!(f, " -> ")?;
107                    for (i, ty) in results.iter().enumerate() {
108                        if i > 0 { write!(f, ", ")?; }
109                        write!(f, "{}", ty)?;
110                    }
111                }
112                Ok(())
113            }
114            Self::ComponentInstance { .. } => write!(f, "instance"),
115            Self::Component { .. } => write!(f, "component"),
116            Self::Module(_) => write!(f, "module"),
117            Self::CoreFunc(_) => write!(f, "core-func"),
118            Self::Type(t) => write!(f, "type({})", t),
119            Self::Resource(r) => write!(f, "resource({})", r),
120            Self::Unknown { debug: Some(d) } => write!(f, "unknown({})", d),
121            Self::Unknown { debug: None } => write!(f, "unknown"),
122        }
123    }
124}
125
126/// Flattens a hierarchical component item map into a flat map with dot-separated keys.
127///
128/// This transformation is useful for:
129/// - Displaying component interfaces in CLI (`arcella list --exports`)
130/// - Generating flat dependency lists
131/// - Simplifying manifest validation
132///
133/// # Example
134///
135/// Input:
136/// ```text
137/// {
138///   "logger": ComponentInstance {
139///     exports: { "log": ComponentFunc(...) }
140///   }
141/// }
142/// ```
143///
144/// Output:
145/// ```text
146/// {
147///   "logger": ComponentInstance(...),
148///   "logger.log": ComponentFunc(...)
149/// }
150/// ```
151pub fn flatten_component_tree(
152    tree: &HashMap<String, ComponentItemSpec>,
153) -> HashMap<String, ComponentItemSpec> {
154    let mut flat = HashMap::new();
155    flatten_component_tree_recursive(tree, "", &mut flat);
156    flat
157}
158
159/// Recursive helper for `flatten_component_tree`.
160///
161/// Internal use only.
162fn flatten_component_tree_recursive(
163    tree: &HashMap<String, ComponentItemSpec>,
164    prefix: &str,
165    output: &mut HashMap<String, ComponentItemSpec>,
166) {
167    for (name, item) in tree {
168        let key = if prefix.is_empty() {
169            name.clone()
170        } else {
171            format!("{}.{}", prefix, name)
172        };
173
174        // Insert the current node
175        output.insert(key.clone(), item.clone());
176
177        // Recurse into nested structures
178        match item {
179            ComponentItemSpec::ComponentInstance { exports } => {
180                flatten_component_tree_recursive(exports, &key, output);
181            }
182            ComponentItemSpec::Component { imports: _, exports } => {
183                // For components, we flatten both imports and exports under the same key?
184                // But imports are usually not nested in exports.
185                // For now, flatten only exports (imports are top-level in practice).
186                flatten_component_tree_recursive(exports, &key, output);
187                // Optionally: flatten imports under "key.imports.*" — but likely unnecessary.
188            }
189            _ => {
190                // Leaf node — nothing to recurse into
191            }
192        }
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    #[test]
201    fn test_serialize_deserialize_spec() {
202        let spec = ComponentItemSpec::ComponentFunc {
203            params: vec![("msg".to_string(), "string".to_string())],
204            results: vec!["bool".to_string()],
205        };
206
207        let json = serde_json::to_string(&spec).unwrap();
208        let restored: ComponentItemSpec = serde_json::from_str(&json).unwrap();
209
210        assert_eq!(spec, restored);
211    }
212
213    #[test]
214    fn test_deserialize_map() {
215        let json = r#"{
216            "handler": { "func": { "params": [], "results": ["string"] } },
217            "logger": { "unknown": {} }
218        }"#;
219
220        let map: HashMap<String, ComponentItemSpec> = serde_json::from_str(json).unwrap();
221        assert!(map.contains_key("handler"));
222        assert!(matches!(map.get("logger"), Some(ComponentItemSpec::Unknown { .. })));
223    }
224}