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