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}