greentic_component/
describe.rs

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}