1use std::collections::BTreeMap;
27
28use serde::{Deserialize, Serialize};
29
30use crate::contract::fnv1a64;
31use crate::ir::{
32 Api, CallbackInterfaceDef, EnumDef, ErrorDomain, Function, InterfaceDef, Module, StructDef,
33 CURRENT_SCHEMA_VERSION,
34};
35
36pub const SECTION: &str = "weaveffi_meta";
38
39const SYMBOL_INFIX: &str = "_META_";
41
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
44#[serde(deny_unknown_fields)]
45pub struct Frame {
46 pub schema: String,
49 pub prefix: String,
52 pub module: Vec<String>,
55 pub index: u32,
59 pub item: Item,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
65#[serde(tag = "kind", content = "def", rename_all = "snake_case")]
66pub enum Item {
67 Module(ModuleHeader),
70 Error(ErrorDomain),
72 Function(Function),
74 Interface(InterfaceDef),
77 Member(Member),
79 Record(StructDef),
81 Enum(EnumDef),
83 Callback(CallbackInterfaceDef),
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
89#[serde(deny_unknown_fields)]
90pub struct ModuleHeader {
91 pub name: String,
93 #[serde(default, skip_serializing_if = "Option::is_none")]
95 pub doc: Option<String>,
96}
97
98#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
100#[serde(deny_unknown_fields)]
101pub struct Member {
102 pub interface: String,
105 pub role: Role,
107 pub function: Function,
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
113#[serde(rename_all = "snake_case")]
114pub enum Role {
115 Constructor,
117 Method,
119 Static,
121}
122
123#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
125pub enum MetaError {
126 #[error("metadata frame at byte {offset} is truncated")]
128 Truncated {
129 offset: usize,
131 },
132 #[error("metadata frame at byte {offset} is malformed: {message}")]
134 Malformed {
135 offset: usize,
137 message: String,
139 },
140 #[error(
142 "`{path}` was built with WeaveFFI schema {found}, but this CLI reads schema {expected}; \
143 build the producer with the same WeaveFFI version as the CLI"
144 )]
145 Schema {
146 path: String,
148 found: String,
150 expected: String,
152 },
153 #[error("the library describes `{path}` twice, differently")]
155 Conflict {
156 path: String,
158 },
159 #[error("`{path}` belongs to `{parent}`, which the library doesn't describe")]
161 Orphan {
162 path: String,
164 parent: String,
166 },
167}
168
169impl Frame {
170 #[must_use]
172 pub fn name(&self) -> &str {
173 match &self.item {
174 Item::Module(m) => &m.name,
175 Item::Error(e) => &e.name,
176 Item::Function(f) => &f.name,
177 Item::Interface(i) => &i.name,
178 Item::Member(m) => &m.function.name,
179 Item::Record(r) => &r.name,
180 Item::Enum(e) => &e.name,
181 Item::Callback(c) => &c.name,
182 }
183 }
184
185 #[must_use]
188 pub fn local_path(&self) -> Vec<&str> {
189 match &self.item {
190 Item::Module(_) => Vec::new(),
191 Item::Member(m) => vec![&m.interface, &m.function.name],
192 _ => vec![self.name()],
193 }
194 }
195
196 #[must_use]
198 pub fn path(&self) -> String {
199 let mut parts: Vec<&str> = self.module.iter().map(String::as_str).collect();
200 match &self.item {
201 Item::Module(m) => parts.push(&m.name),
202 _ => parts.extend(self.local_path()),
203 }
204 parts.join(".")
205 }
206
207 #[must_use]
213 pub fn symbol(&self) -> String {
214 let mut key = self.path();
215 if matches!(self.item, Item::Module(_)) {
216 key.push('.');
217 }
218 format!(
219 "{}{SYMBOL_INFIX}{:016X}",
220 self.prefix.to_ascii_uppercase(),
221 fnv1a64(key.as_bytes())
222 )
223 }
224
225 #[must_use]
231 pub fn encode(&self) -> Vec<u8> {
232 let json = serde_json::to_vec(self).expect("a frame always serializes");
233 let len = u32::try_from(json.len()).expect("a frame fits in u32::MAX bytes");
234 let mut out = Vec::with_capacity(4 + json.len());
235 out.extend_from_slice(&len.to_le_bytes());
236 out.extend_from_slice(&json);
237 out
238 }
239}
240
241#[must_use]
245pub fn is_symbol(symbol: &str) -> bool {
246 let Some((head, hash)) = symbol.rsplit_once(SYMBOL_INFIX) else {
247 return false;
248 };
249 !head.is_empty()
250 && hash.len() == 16
251 && hash
252 .bytes()
253 .all(|b| b.is_ascii_digit() || (b'A'..=b'F').contains(&b))
254}
255
256pub fn frame_len(bytes: &[u8]) -> Result<usize, MetaError> {
264 let prefix: [u8; 4] = bytes
265 .get(..4)
266 .and_then(|b| b.try_into().ok())
267 .ok_or(MetaError::Truncated { offset: 0 })?;
268 Ok(4 + u32::from_le_bytes(prefix) as usize)
269}
270
271pub fn decode(bytes: &[u8]) -> Result<Vec<Frame>, MetaError> {
278 let mut frames = Vec::new();
279 let mut offset = 0;
280 while offset < bytes.len() {
281 let rest = &bytes[offset..];
282 let len = frame_len(rest).map_err(|_| MetaError::Truncated { offset })?;
283 let json = rest.get(4..len).ok_or(MetaError::Truncated { offset })?;
284 let frame = serde_json::from_slice(json).map_err(|e| MetaError::Malformed {
285 offset,
286 message: e.to_string(),
287 })?;
288 frames.push(frame);
289 offset += len;
290 }
291 Ok(frames)
292}
293
294#[must_use]
299pub fn module_frames(module: &Module, parent: &[String], index: u32, prefix: &str) -> Vec<Frame> {
300 let mut path = parent.to_vec();
301 path.push(module.name.clone());
302 let frame = |module: &[String], index: usize, item: Item| Frame {
303 schema: CURRENT_SCHEMA_VERSION.to_string(),
304 prefix: prefix.to_string(),
305 module: module.to_vec(),
306 index: u32::try_from(index).unwrap_or(u32::MAX),
307 item,
308 };
309 let mut out = vec![frame(
310 parent,
311 index as usize,
312 Item::Module(ModuleHeader {
313 name: module.name.clone(),
314 doc: module.doc.clone(),
315 }),
316 )];
317 if let Some(errors) = &module.errors {
318 out.push(frame(&path, 0, Item::Error(errors.clone())));
319 }
320 for (i, f) in module.functions.iter().enumerate() {
321 out.push(frame(&path, i, Item::Function(f.clone())));
322 }
323 for (i, iface) in module.interfaces.iter().enumerate() {
324 out.push(frame(
325 &path,
326 i,
327 Item::Interface(InterfaceDef {
328 constructors: Vec::new(),
329 methods: Vec::new(),
330 statics: Vec::new(),
331 ..iface.clone()
332 }),
333 ));
334 for (role, members) in [
335 (Role::Constructor, &iface.constructors),
336 (Role::Method, &iface.methods),
337 (Role::Static, &iface.statics),
338 ] {
339 for (j, function) in members.iter().enumerate() {
340 out.push(frame(
341 &path,
342 j,
343 Item::Member(Member {
344 interface: iface.name.clone(),
345 role,
346 function: function.clone(),
347 }),
348 ));
349 }
350 }
351 }
352 for (i, r) in module.structs.iter().enumerate() {
353 out.push(frame(&path, i, Item::Record(r.clone())));
354 }
355 for (i, e) in module.enums.iter().enumerate() {
356 out.push(frame(&path, i, Item::Enum(e.clone())));
357 }
358 for (i, c) in module.callback_interfaces.iter().enumerate() {
359 out.push(frame(&path, i, Item::Callback(c.clone())));
360 }
361 out
362}
363
364#[must_use]
367pub fn frames(api: &Api, prefix: &str) -> Vec<Frame> {
368 fn walk(modules: &[Module], parent: &[String], prefix: &str, out: &mut Vec<Frame>) {
369 for (i, m) in modules.iter().enumerate() {
370 out.extend(module_frames(
371 m,
372 parent,
373 u32::try_from(i).unwrap_or(u32::MAX),
374 prefix,
375 ));
376 let mut path = parent.to_vec();
377 path.push(m.name.clone());
378 walk(&m.modules, &path, prefix, out);
379 }
380 }
381 let mut out = Vec::new();
382 walk(&api.modules, &[], prefix, &mut out);
383 out
384}
385
386#[derive(Default)]
389struct Node {
390 header: Option<(u32, ModuleHeader)>,
391 errors: Option<ErrorDomain>,
392 functions: Vec<(u32, Function)>,
393 interfaces: Vec<(u32, InterfaceDef)>,
394 members: Vec<(u32, Member)>,
395 structs: Vec<(u32, StructDef)>,
396 enums: Vec<(u32, EnumDef)>,
397 callbacks: Vec<(u32, CallbackInterfaceDef)>,
398}
399
400pub fn assemble(frames: &[Frame], prefix: &str) -> Result<Api, MetaError> {
412 let mut seen: BTreeMap<(String, &'static str), &Frame> = BTreeMap::new();
413 let mut nodes: BTreeMap<Vec<String>, Node> = BTreeMap::new();
414 for frame in frames.iter().filter(|f| f.prefix == prefix) {
415 let path = frame.path();
416 if frame.schema != CURRENT_SCHEMA_VERSION {
417 return Err(MetaError::Schema {
418 path,
419 found: frame.schema.clone(),
420 expected: CURRENT_SCHEMA_VERSION.to_string(),
421 });
422 }
423 let kind = match &frame.item {
424 Item::Module(_) => "module",
425 Item::Member(_) => "member",
426 _ => "declaration",
427 };
428 match seen.get(&(path.clone(), kind)) {
429 Some(prev) if *prev == frame => continue,
430 Some(_) => return Err(MetaError::Conflict { path }),
431 None => {
432 seen.insert((path, kind), frame);
433 }
434 }
435 let i = frame.index;
436 match &frame.item {
437 Item::Module(h) => {
438 let mut full = frame.module.clone();
439 full.push(h.name.clone());
440 nodes.entry(full).or_default().header = Some((i, h.clone()));
441 }
442 Item::Error(e) => {
443 nodes.entry(frame.module.clone()).or_default().errors = Some(e.clone());
444 }
445 Item::Function(f) => nodes
446 .entry(frame.module.clone())
447 .or_default()
448 .functions
449 .push((i, f.clone())),
450 Item::Interface(d) => nodes
451 .entry(frame.module.clone())
452 .or_default()
453 .interfaces
454 .push((i, d.clone())),
455 Item::Member(m) => nodes
456 .entry(frame.module.clone())
457 .or_default()
458 .members
459 .push((i, m.clone())),
460 Item::Record(r) => nodes
461 .entry(frame.module.clone())
462 .or_default()
463 .structs
464 .push((i, r.clone())),
465 Item::Enum(e) => nodes
466 .entry(frame.module.clone())
467 .or_default()
468 .enums
469 .push((i, e.clone())),
470 Item::Callback(c) => nodes
471 .entry(frame.module.clone())
472 .or_default()
473 .callbacks
474 .push((i, c.clone())),
475 }
476 }
477
478 for (path, node) in &nodes {
481 if node.header.is_none() {
482 let (child, parent) = orphan_of(path, node);
483 return Err(MetaError::Orphan {
484 path: child,
485 parent,
486 });
487 }
488 if path.len() > 1 && !nodes.contains_key(&path[..path.len() - 1]) {
489 return Err(MetaError::Orphan {
490 path: path.join("."),
491 parent: path[..path.len() - 1].join("."),
492 });
493 }
494 }
495
496 let mut modules = build(&mut nodes, &[])?;
497 modules.sort_by(|a, b| a.name.cmp(&b.name));
500 Ok(Api {
501 version: CURRENT_SCHEMA_VERSION.to_string(),
502 modules,
503 })
504}
505
506fn orphan_of(path: &[String], node: &Node) -> (String, String) {
508 let module = path.join(".");
509 let name = node
510 .functions
511 .first()
512 .map(|(_, f)| f.name.clone())
513 .or_else(|| node.interfaces.first().map(|(_, d)| d.name.clone()))
514 .or_else(|| node.members.first().map(|(_, m)| m.interface.clone()))
515 .or_else(|| node.structs.first().map(|(_, d)| d.name.clone()))
516 .or_else(|| node.enums.first().map(|(_, d)| d.name.clone()))
517 .or_else(|| node.callbacks.first().map(|(_, d)| d.name.clone()))
518 .or_else(|| node.errors.as_ref().map(|e| e.name.clone()))
519 .unwrap_or_default();
520 (format!("{module}.{name}"), module)
521}
522
523fn ordered<T>(mut items: Vec<(u32, T)>, name: impl Fn(&T) -> &str) -> Vec<T> {
526 items.sort_by(|(i, a), (j, b)| i.cmp(j).then_with(|| name(a).cmp(name(b))));
527 items.into_iter().map(|(_, t)| t).collect()
528}
529
530fn build(
532 nodes: &mut BTreeMap<Vec<String>, Node>,
533 parent: &[String],
534) -> Result<Vec<Module>, MetaError> {
535 let children: Vec<Vec<String>> = nodes
536 .keys()
537 .filter(|p| p.len() == parent.len() + 1 && p.starts_with(parent))
538 .cloned()
539 .collect();
540 let mut modules = Vec::new();
541 for path in children {
542 let node = nodes.remove(&path).unwrap_or_default();
543 let Some((index, header)) = node.header else {
544 continue;
545 };
546 let mut interfaces = ordered(node.interfaces, |d| &d.name);
547 for member in ordered(node.members, |m| &m.function.name) {
548 let Some(iface) = interfaces.iter_mut().find(|d| d.name == member.interface) else {
549 return Err(MetaError::Orphan {
550 path: format!(
551 "{}.{}.{}",
552 path.join("."),
553 member.interface,
554 member.function.name
555 ),
556 parent: format!("{}.{}", path.join("."), member.interface),
557 });
558 };
559 let list = match member.role {
560 Role::Constructor => &mut iface.constructors,
561 Role::Method => &mut iface.methods,
562 Role::Static => &mut iface.statics,
563 };
564 list.push(member.function);
565 }
566 let mut module = Module {
567 name: header.name,
568 doc: header.doc,
569 functions: ordered(node.functions, |f| &f.name),
570 interfaces,
571 callback_interfaces: ordered(node.callbacks, |d| &d.name),
572 structs: ordered(node.structs, |d| &d.name),
573 enums: ordered(node.enums, |d| &d.name),
574 errors: node.errors,
575 modules: Vec::new(),
576 };
577 module.modules = build(nodes, &path)?;
578 modules.push((index, module));
579 }
580 Ok(ordered(modules, |m| &m.name))
581}
582
583#[cfg(all(test, feature = "idl"))]
584mod tests {
585 use super::*;
586 use crate::parse::parse_api_str;
587
588 const API: &str = r#"
589version: "0.11.0"
590modules:
591 - name: kv
592 doc: The store.
593 errors:
594 name: KvError
595 codes:
596 - { name: Missing, code: 1, message: missing, fields: [{ name: key, type: string }] }
597 functions:
598 - { name: open_store, params: [{ name: path, type: string }], return: Store, throws: true }
599 - { name: version, return: u32 }
600 interfaces:
601 - name: Store
602 constructors:
603 - { name: new }
604 - { name: open, params: [{ name: path, type: string }], throws: true }
605 methods:
606 - { name: put, params: [{ name: key, type: string }] }
607 - { name: count, return: u32 }
608 statics:
609 - { name: default_capacity, return: u32 }
610 callback_interfaces:
611 - name: Listener
612 methods:
613 - { name: on_change, params: [{ name: key, type: string }] }
614 structs:
615 - { name: Entry, fields: [{ name: key, type: string }, { name: tags, type: "[string]" }] }
616 enums:
617 - { name: Kind, variants: [{ name: A, value: 0 }, { name: B, value: 1 }] }
618 modules:
619 - name: stats
620 functions:
621 - { name: summarize, params: [{ name: store, type: Store }], return: u32 }
622 - name: admin
623 functions:
624 - { name: wipe, params: [{ name: store, type: Store }] }
625 - name: report
626 functions:
627 - { name: render, params: [{ name: entries, type: "[Entry]" }], return: "[string]" }
628"#;
629
630 fn api() -> Api {
631 parse_api_str(API, "yaml").unwrap()
632 }
633
634 fn section(frames: &[Frame]) -> Vec<u8> {
635 frames.iter().flat_map(Frame::encode).collect()
636 }
637
638 #[test]
639 fn frames_round_trip_to_the_same_api() {
640 let api = api();
641 let mut frames = frames(&api, "kvstore");
642 frames.reverse();
644 let decoded = decode(§ion(&frames)).unwrap();
645 assert_eq!(decoded, frames);
646 assert_eq!(assemble(&decoded, "kvstore").unwrap(), api);
647 }
648
649 #[test]
650 fn foreign_and_repeated_frames() {
651 let api = api();
652 let mut all = frames(&api, "kvstore");
653 all.extend(frames(&api, "kvstore"));
654 all.extend(frames(&api, "other"));
655 assert_eq!(assemble(&all, "kvstore").unwrap(), api);
656 assert!(assemble(&all, "nobody").unwrap().modules.is_empty());
657 }
658
659 #[test]
660 fn symbols_paths_and_names() {
661 let all = frames(&api(), "kvstore");
662 let put = all
663 .iter()
664 .find(|f| matches!(&f.item, Item::Member(m) if m.function.name == "put"))
665 .unwrap();
666 assert_eq!(put.path(), "kv.Store.put");
667 assert_eq!(put.local_path(), ["Store", "put"]);
668 let symbol = put.symbol();
669 assert_eq!(
670 symbol,
671 format!("KVSTORE_META_{:016X}", fnv1a64(b"kv.Store.put"))
672 );
673 assert!(is_symbol(&symbol));
674 assert!(is_symbol(&format!("_{symbol}")));
675 assert!(!is_symbol("KVSTORE_META_0123"));
676 assert!(!is_symbol("KVSTORE_META_0123456789abcdef"));
677 assert!(!is_symbol("_META_0123456789ABCDEF"));
678 let stats = all
679 .iter()
680 .find(|f| matches!(&f.item, Item::Module(m) if m.name == "stats"))
681 .unwrap();
682 assert_eq!(stats.path(), "kv.stats");
683 assert_ne!(
684 stats.symbol(),
685 format!("KVSTORE_META_{:016X}", fnv1a64(b"kv.stats"))
686 );
687 let mut symbols: Vec<String> = all.iter().map(Frame::symbol).collect();
688 symbols.sort();
689 symbols.dedup();
690 assert_eq!(symbols.len(), all.len(), "every symbol is unique");
691 }
692
693 #[test]
694 fn malformed_and_inconsistent_frames_are_errors() {
695 let all = frames(&api(), "kvstore");
696 let bytes = section(&all[..2]);
697 assert!(matches!(
698 decode(&bytes[..bytes.len() - 1]),
699 Err(MetaError::Truncated { .. })
700 ));
701 assert!(matches!(
702 decode(&[2, 0, 0, 0, b'{', b'}']),
703 Err(MetaError::Malformed { offset: 0, .. })
704 ));
705
706 let mut stale = all.clone();
707 stale[3].schema = "0.10.0".into();
708 let err = assemble(&stale, "kvstore").unwrap_err();
709 assert!(err.to_string().contains("schema 0.10.0"), "{err}");
710
711 let mut conflict = all.clone();
712 let mut changed = all[2].clone();
713 changed.index += 7;
714 conflict.push(changed);
715 assert!(matches!(
716 assemble(&conflict, "kvstore"),
717 Err(MetaError::Conflict { .. })
718 ));
719
720 let headless: Vec<Frame> = all
721 .iter()
722 .filter(|f| !matches!(&f.item, Item::Module(m) if m.name == "stats"))
723 .cloned()
724 .collect();
725 let err = assemble(&headless, "kvstore").unwrap_err();
726 assert_eq!(
727 err,
728 MetaError::Orphan {
729 path: "kv.stats.summarize".into(),
730 parent: "kv.stats".into()
731 }
732 );
733
734 let memberless: Vec<Frame> = all
735 .iter()
736 .filter(|f| !matches!(&f.item, Item::Interface(_)))
737 .cloned()
738 .collect();
739 assert!(matches!(
740 assemble(&memberless, "kvstore"),
741 Err(MetaError::Orphan { .. })
742 ));
743 }
744}