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, Some(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 preferred_world: Option<&str>,
121) -> Result<DescribePayload, DescribeError> {
122 let world = &resolve.worlds[world_id];
123 let resolved_world_ref = format_world(resolve, world_id);
124 let resolved_version = world
125 .package
126 .and_then(|pkg_id| resolve.packages[pkg_id].name.version.clone())
127 .map(|ver| Version::new(ver.major, ver.minor, ver.patch))
128 .unwrap_or_else(|| Version::new(0, 0, 0));
129 let (world_ref, name, version) = preferred_world
130 .and_then(|preferred_world| parse_preferred_world_ref(preferred_world, &resolved_version))
131 .unwrap_or_else(|| {
132 (
133 resolved_world_ref.clone(),
134 world.name.clone(),
135 resolved_version.clone(),
136 )
137 });
138
139 let mut functions = Vec::new();
140 for (key, item) in &world.exports {
141 match item {
142 WorldItem::Function(func) => {
143 let mut entry = Map::new();
144 entry.insert("name".into(), Value::String(func.name.clone()));
145 entry.insert("key".into(), Value::String(label_for_key(resolve, key)));
146 if let Some(doc) = func.docs.contents.clone() {
147 entry.insert("docs".into(), Value::String(doc));
148 }
149 functions.push(Value::Object(entry));
150 }
151 WorldItem::Interface { id, .. } => {
152 let iface = &resolve.interfaces[*id];
153 for (name, func) in iface.functions.iter() {
154 let mut entry = Map::new();
155 entry.insert("name".into(), Value::String(name.clone()));
156 if let Some(doc) = func.docs.contents.clone() {
157 entry.insert("docs".into(), Value::String(doc));
158 }
159 if let Some(iface_name) = &iface.name {
160 entry.insert("interface".into(), Value::String(iface_name.clone()));
161 }
162 functions.push(Value::Object(entry));
163 }
164 }
165 WorldItem::Type { .. } => {}
166 }
167 }
168
169 let schema = json!({
170 "world": world_ref,
171 "functions": functions,
172 });
173
174 Ok(DescribePayload {
175 name,
176 schema_id: Some(world_ref.clone()),
177 versions: vec![DescribeVersion {
178 version,
179 schema,
180 defaults: None,
181 }],
182 })
183}
184
185fn parse_preferred_world_ref(
186 world_ref: &str,
187 fallback_version: &Version,
188) -> Option<(String, String, Version)> {
189 if world_ref.trim().is_empty() {
190 return None;
191 }
192 let (name_part, version) = match world_ref.rsplit_once('@') {
193 Some((name_part, version_part)) => (name_part, Version::parse(version_part).ok()?),
194 None => (world_ref, fallback_version.clone()),
195 };
196 let name = name_part.rsplit('/').next()?.to_string();
197 Some((world_ref.to_string(), name, version))
198}
199
200fn format_world(resolve: &Resolve, world_id: WorldId) -> String {
201 let world = &resolve.worlds[world_id];
202 if let Some(pkg_id) = world.package {
203 let pkg = &resolve.packages[pkg_id];
204 if let Some(version) = &pkg.name.version {
205 format!(
206 "{}:{}/{}@{}",
207 pkg.name.namespace, pkg.name.name, world.name, version
208 )
209 } else {
210 format!("{}:{}/{}", pkg.name.namespace, pkg.name.name, world.name)
211 }
212 } else {
213 world.name.clone()
214 }
215}
216
217fn label_for_key(resolve: &Resolve, key: &WorldKey) -> String {
218 match key {
219 WorldKey::Name(name) => name.to_string(),
220 WorldKey::Interface(id) => {
221 let iface = &resolve.interfaces[*id];
222 iface
223 .name
224 .as_ref()
225 .map(|s| s.to_string())
226 .unwrap_or_else(|| format!("interface-{}", id.index()))
227 }
228 }
229}