1use std::fs;
2use std::path::{Path, PathBuf};
3
4use anyhow::Error;
5use semver::Version;
6use serde::{Deserialize, Serialize};
7use serde_json::{Map, Value, json};
8use thiserror::Error;
9use wit_parser::{Resolve, WorldId, WorldItem, WorldKey};
10
11use crate::manifest::ComponentManifest;
12use crate::wasm;
13
14#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
15pub struct DescribePayload {
16 pub name: String,
17 pub versions: Vec<DescribeVersion>,
18 #[serde(default, skip_serializing_if = "Option::is_none")]
19 pub schema_id: Option<String>,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
23pub struct DescribeVersion {
24 pub version: Version,
25 pub schema: Value,
26 #[serde(default, skip_serializing_if = "Option::is_none")]
27 pub defaults: Option<Value>,
28}
29
30#[derive(Debug, Error)]
31pub enum DescribeError {
32 #[error("failed to read describe payload at {path}: {source}")]
33 Io {
34 path: PathBuf,
35 #[source]
36 source: std::io::Error,
37 },
38 #[error("invalid describe payload at {path}: {source}")]
39 Json {
40 path: PathBuf,
41 #[source]
42 source: serde_json::Error,
43 },
44 #[error("failed to decode component metadata: {0}")]
45 Metadata(Error),
46 #[error("describe payload not found: {0}")]
47 NotFound(String),
48}
49
50pub fn from_exported_func(
51 wasm_path: &Path,
52 symbol: &str,
53) -> Result<DescribePayload, DescribeError> {
54 let dir = wasm_path
55 .parent()
56 .ok_or_else(|| DescribeError::NotFound(symbol.to_string()))?;
57 let candidate = dir.join(format!("{symbol}.describe.json"));
58 read_payload(&candidate)
59}
60
61pub fn from_wit_world(wasm_path: &Path, _world: &str) -> Result<DescribePayload, DescribeError> {
62 let bytes = fs::read(wasm_path).map_err(|source| DescribeError::Io {
63 path: wasm_path.to_path_buf(),
64 source,
65 })?;
66 let decoded = wasm::decode_world(&bytes).map_err(DescribeError::Metadata)?;
67 build_payload_from_world(&decoded.resolve, decoded.world)
68}
69
70pub fn from_embedded(manifest_dir: &Path) -> Option<DescribePayload> {
71 let schema_dir = manifest_dir.join("schemas").join("v1");
72 let entries = fs::read_dir(schema_dir).ok()?;
73 let mut files = Vec::new();
74 for entry in entries.flatten() {
75 files.push(entry.path());
76 }
77 files.sort();
78 for path in files {
79 if path.extension().and_then(|s| s.to_str()) == Some("json")
80 && let Ok(payload) = read_payload(&path)
81 {
82 return Some(payload);
83 }
84 }
85 None
86}
87
88pub fn load(
89 wasm_path: &Path,
90 manifest: &ComponentManifest,
91) -> Result<DescribePayload, DescribeError> {
92 if let Ok(payload) = from_wit_world(wasm_path, manifest.world.as_str()) {
93 return Ok(payload);
94 }
95 if let Ok(payload) = from_exported_func(wasm_path, manifest.describe_export.as_str()) {
96 return Ok(payload);
97 }
98 if let Some(dir) = wasm_path.parent()
99 && let Some(payload) = from_embedded(dir)
100 {
101 return Ok(payload);
102 }
103 Err(DescribeError::NotFound(manifest.id.as_str().to_string()))
104}
105
106fn read_payload(path: &Path) -> Result<DescribePayload, DescribeError> {
107 let data = fs::read_to_string(path).map_err(|source| DescribeError::Io {
108 path: path.to_path_buf(),
109 source,
110 })?;
111 serde_json::from_str(&data).map_err(|source| DescribeError::Json {
112 path: path.to_path_buf(),
113 source,
114 })
115}
116
117fn build_payload_from_world(
118 resolve: &Resolve,
119 world_id: WorldId,
120) -> Result<DescribePayload, DescribeError> {
121 let world = &resolve.worlds[world_id];
122 let world_ref = format_world(resolve, world_id);
123 let version = world
124 .package
125 .and_then(|pkg_id| resolve.packages[pkg_id].name.version.clone())
126 .map(|ver| Version::new(ver.major, ver.minor, ver.patch))
127 .unwrap_or_else(|| Version::new(0, 0, 0));
128
129 let mut functions = Vec::new();
130 for (key, item) in &world.exports {
131 match item {
132 WorldItem::Function(func) => {
133 let mut entry = Map::new();
134 entry.insert("name".into(), Value::String(func.name.clone()));
135 entry.insert("key".into(), Value::String(label_for_key(resolve, key)));
136 if let Some(doc) = func.docs.contents.clone() {
137 entry.insert("docs".into(), Value::String(doc));
138 }
139 functions.push(Value::Object(entry));
140 }
141 WorldItem::Interface { id, .. } => {
142 let iface = &resolve.interfaces[*id];
143 for (name, func) in iface.functions.iter() {
144 let mut entry = Map::new();
145 entry.insert("name".into(), Value::String(name.clone()));
146 if let Some(doc) = func.docs.contents.clone() {
147 entry.insert("docs".into(), Value::String(doc));
148 }
149 if let Some(iface_name) = &iface.name {
150 entry.insert("interface".into(), Value::String(iface_name.clone()));
151 }
152 functions.push(Value::Object(entry));
153 }
154 }
155 WorldItem::Type(_) => {}
156 }
157 }
158
159 let schema = json!({
160 "world": world_ref,
161 "functions": functions,
162 });
163
164 Ok(DescribePayload {
165 name: world.name.clone(),
166 schema_id: Some(world_ref.clone()),
167 versions: vec![DescribeVersion {
168 version,
169 schema,
170 defaults: None,
171 }],
172 })
173}
174
175fn format_world(resolve: &Resolve, world_id: WorldId) -> String {
176 let world = &resolve.worlds[world_id];
177 if let Some(pkg_id) = world.package {
178 let pkg = &resolve.packages[pkg_id];
179 if let Some(version) = &pkg.name.version {
180 format!(
181 "{}:{}/{}@{}",
182 pkg.name.namespace, pkg.name.name, world.name, version
183 )
184 } else {
185 format!("{}:{}/{}", pkg.name.namespace, pkg.name.name, world.name)
186 }
187 } else {
188 world.name.clone()
189 }
190}
191
192fn label_for_key(resolve: &Resolve, key: &WorldKey) -> String {
193 match key {
194 WorldKey::Name(name) => name.to_string(),
195 WorldKey::Interface(id) => {
196 let iface = &resolve.interfaces[*id];
197 iface
198 .name
199 .as_ref()
200 .map(|s| s.to_string())
201 .unwrap_or_else(|| format!("interface-{}", id.index()))
202 }
203 }
204}