1use std::collections::BTreeMap;
18use std::fmt;
19
20use weaveffi_ir::ir::{Api, Module, TypeRef};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
27pub enum Feature {
28 AsyncFunctions,
30 Callbacks,
32 Listeners,
34 Iterators,
36}
37
38impl Feature {
39 pub const ALL: [Feature; 4] = [
41 Feature::AsyncFunctions,
42 Feature::Callbacks,
43 Feature::Listeners,
44 Feature::Iterators,
45 ];
46
47 pub fn idl_name(&self) -> &'static str {
49 match self {
50 Feature::AsyncFunctions => "async functions",
51 Feature::Callbacks => "callbacks",
52 Feature::Listeners => "listeners",
53 Feature::Iterators => "iterator returns (iter<T>)",
54 }
55 }
56}
57
58impl fmt::Display for Feature {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 f.write_str(self.idl_name())
61 }
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub struct TargetCapabilities {
72 pub async_functions: bool,
74 pub callbacks: bool,
76 pub listeners: bool,
78 pub iterators: bool,
80}
81
82impl TargetCapabilities {
83 pub const fn full() -> Self {
86 Self {
87 async_functions: true,
88 callbacks: true,
89 listeners: true,
90 iterators: true,
91 }
92 }
93
94 pub const fn supports(&self, feature: Feature) -> bool {
96 match feature {
97 Feature::AsyncFunctions => self.async_functions,
98 Feature::Callbacks => self.callbacks,
99 Feature::Listeners => self.listeners,
100 Feature::Iterators => self.iterators,
101 }
102 }
103}
104
105pub fn used_features(api: &Api) -> BTreeMap<Feature, Vec<String>> {
108 let mut used: BTreeMap<Feature, Vec<String>> = BTreeMap::new();
109 for module in &api.modules {
110 collect_module(module, "", &mut used);
111 }
112 used
113}
114
115fn collect_module(module: &Module, parent: &str, used: &mut BTreeMap<Feature, Vec<String>>) {
116 let path = if parent.is_empty() {
117 module.name.clone()
118 } else {
119 format!("{parent}.{}", module.name)
120 };
121 for cb in &module.callbacks {
122 used.entry(Feature::Callbacks)
123 .or_default()
124 .push(format!("{path}.{}", cb.name));
125 }
126 for l in &module.listeners {
127 used.entry(Feature::Listeners)
128 .or_default()
129 .push(format!("{path}.{}", l.name));
130 }
131 for f in &module.functions {
132 let loc = format!("{path}.{}", f.name);
133 if f.r#async {
134 used.entry(Feature::AsyncFunctions)
135 .or_default()
136 .push(loc.clone());
137 }
138 if matches!(f.returns, Some(TypeRef::Iterator(_))) {
139 used.entry(Feature::Iterators).or_default().push(loc);
140 }
141 }
142 for child in &module.modules {
143 collect_module(child, &path, used);
144 }
145}
146
147#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
151pub struct UnsupportedFeatures {
152 pub target: String,
154 pub violations: Vec<(Feature, Vec<String>)>,
156}
157
158impl fmt::Display for UnsupportedFeatures {
159 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160 writeln!(
161 f,
162 "target '{}' does not support every feature this IDL uses:",
163 self.target
164 )?;
165 for (feature, locations) in &self.violations {
166 writeln!(f, " - {feature} (used by: {})", locations.join(", "))?;
167 }
168 write!(
169 f,
170 "remove the unsupported declarations, drop '{}' from --target, or set \
171 `generators.{}.allow_unsupported: true` in the IDL to generate the supported \
172 surface anyway (unsupported entry points become explicit throwing stubs)",
173 self.target, self.target
174 )
175 }
176}
177
178pub fn check(
188 api: &Api,
189 target: &str,
190 caps: &TargetCapabilities,
191) -> Result<(), UnsupportedFeatures> {
192 let violations: Vec<(Feature, Vec<String>)> = used_features(api)
193 .into_iter()
194 .filter(|(feature, _)| !caps.supports(*feature))
195 .collect();
196 if violations.is_empty() {
197 Ok(())
198 } else {
199 Err(UnsupportedFeatures {
200 target: target.to_string(),
201 violations,
202 })
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209 use weaveffi_ir::ir::{CallbackDef, Function, ListenerDef, Param};
210
211 fn func(name: &str, is_async: bool, returns: Option<TypeRef>) -> Function {
212 Function {
213 name: name.into(),
214 params: vec![Param {
215 name: "x".into(),
216 ty: TypeRef::I32,
217 mutable: false,
218 doc: None,
219 }],
220 returns,
221 doc: None,
222 throws: false,
223 r#async: is_async,
224 cancellable: false,
225 deprecated: None,
226 since: None,
227 }
228 }
229
230 fn module(name: &str) -> Module {
231 Module {
232 name: name.into(),
233 functions: vec![],
234 interfaces: vec![],
235 structs: vec![],
236 enums: vec![],
237 callbacks: vec![],
238 listeners: vec![],
239 errors: None,
240 modules: vec![],
241 }
242 }
243
244 fn api(modules: Vec<Module>) -> Api {
245 Api {
246 version: "0.5.0".into(),
247 modules,
248 generators: None,
249 package: None,
250 }
251 }
252
253 fn events_api() -> Api {
254 api(vec![Module {
255 callbacks: vec![CallbackDef {
256 name: "OnMessage".into(),
257 params: vec![],
258 doc: None,
259 }],
260 listeners: vec![ListenerDef {
261 name: "message_listener".into(),
262 event_callback: "OnMessage".into(),
263 doc: None,
264 }],
265 functions: vec![
266 func("send", false, None),
267 func("fetch", true, Some(TypeRef::StringUtf8)),
268 func(
269 "all",
270 false,
271 Some(TypeRef::Iterator(Box::new(TypeRef::StringUtf8))),
272 ),
273 ],
274 ..module("events")
275 }])
276 }
277
278 #[test]
279 fn full_capabilities_pass_everything() {
280 assert!(check(&events_api(), "c", &TargetCapabilities::full()).is_ok());
281 }
282
283 #[test]
284 fn plain_api_uses_no_gated_features() {
285 let plain = api(vec![Module {
286 functions: vec![func("add", false, Some(TypeRef::I32))],
287 ..module("math")
288 }]);
289 assert!(used_features(&plain).is_empty());
290 }
291
292 #[test]
293 fn used_features_collects_locations() {
294 let used = used_features(&events_api());
295 assert_eq!(
296 used[&Feature::Callbacks],
297 vec!["events.OnMessage".to_string()]
298 );
299 assert_eq!(
300 used[&Feature::Listeners],
301 vec!["events.message_listener".to_string()]
302 );
303 assert_eq!(
304 used[&Feature::AsyncFunctions],
305 vec!["events.fetch".to_string()]
306 );
307 assert_eq!(used[&Feature::Iterators], vec!["events.all".to_string()]);
308 }
309
310 #[test]
311 fn nested_modules_use_dotted_paths() {
312 let nested = api(vec![Module {
313 modules: vec![Module {
314 functions: vec![func("fetch", true, None)],
315 ..module("inner")
316 }],
317 ..module("outer")
318 }]);
319 let used = used_features(&nested);
320 assert_eq!(
321 used[&Feature::AsyncFunctions],
322 vec!["outer.inner.fetch".to_string()]
323 );
324 }
325
326 #[test]
327 fn missing_capability_is_reported_with_locations() {
328 let caps = TargetCapabilities {
329 async_functions: false,
330 listeners: false,
331 ..TargetCapabilities::full()
332 };
333 let err = check(&events_api(), "go", &caps).unwrap_err();
334 assert_eq!(err.target, "go");
335 assert_eq!(err.violations.len(), 2);
336 let msg = err.to_string();
337 assert!(msg.contains("target 'go' does not support"), "{msg}");
338 assert!(
339 msg.contains("async functions (used by: events.fetch)"),
340 "{msg}"
341 );
342 assert!(
343 msg.contains("listeners (used by: events.message_listener)"),
344 "{msg}"
345 );
346 }
347}