Skip to main content

weaveffi_model/
meta.rs

1//! Library metadata: the API a Rust producer embeds in its compiled library.
2//!
3//! The `#[weaveffi::module]` macro describes every declaration it exports in
4//! a [`Frame`] and embeds the frame's [encoding](Frame::encode) in the
5//! producer's library as an exported static named [`Frame::symbol`] (on
6//! `wasm32`, in the [`SECTION`] custom section instead). The CLI reads the
7//! frames back out of the built library, [decodes](decode) them, keeps those
8//! with the crate's own prefix, and [assembles](assemble) the [`Api`] it
9//! generates from. Because the frames are compiled with the code they
10//! describe, a declaration removed by `#[cfg]` is absent from both, and the
11//! generated bindings describe exactly what the library exports.
12//!
13//! # Encoding
14//!
15//! A frame is a little-endian `u32` byte length followed by that many bytes
16//! of compact JSON (the [`Frame`] serialized with serde). Frames are
17//! self-delimiting, so a section holding several frames back to back
18//! [decodes](decode) into all of them.
19//!
20//! One frame describes one declaration: a module (its name and doc), its
21//! error domain, a free function, an interface (without its members), one
22//! interface member, a record, an enum, or a callback interface. Each frame
23//! records the path of the module it belongs to and its position among its
24//! siblings of the same kind, so [`assemble`] restores declaration order.
25
26use 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
36/// The `wasm32` custom section the frames are placed in.
37pub const SECTION: &str = "weaveffi_meta";
38
39/// The infix of every metadata symbol: `{PREFIX}_META_{HASH16}`.
40const SYMBOL_INFIX: &str = "_META_";
41
42/// One declaration of a producer's API, as embedded in its library.
43#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
44#[serde(deny_unknown_fields)]
45pub struct Frame {
46    /// The schema version the producer was built with
47    /// ([`CURRENT_SCHEMA_VERSION`] when built by this version).
48    pub schema: String,
49    /// The C symbol prefix of the crate that declared the item (its library
50    /// name), which tells a crate's own frames from a dependency's.
51    pub prefix: String,
52    /// The path of the module the item belongs to; for a module frame, its
53    /// parent's path (empty for a top-level module).
54    pub module: Vec<String>,
55    /// The item's position among its siblings of the same kind (functions
56    /// among functions, submodules among submodules, and so on), in
57    /// declaration order.
58    pub index: u32,
59    /// The declaration.
60    pub item: Item,
61}
62
63/// The declaration a [`Frame`] describes.
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
65#[serde(tag = "kind", content = "def", rename_all = "snake_case")]
66pub enum Item {
67    /// A module's own name and doc; its declarations have frames of their
68    /// own.
69    Module(ModuleHeader),
70    /// A module's error domain.
71    Error(ErrorDomain),
72    /// A free function.
73    Function(Function),
74    /// An interface, with no members: each has a [`Member`](Self::Member)
75    /// frame.
76    Interface(InterfaceDef),
77    /// One constructor, method, or static of an interface.
78    Member(Member),
79    /// A record.
80    Record(StructDef),
81    /// An enum.
82    Enum(EnumDef),
83    /// A callback interface, with its methods.
84    Callback(CallbackInterfaceDef),
85}
86
87/// The part of a [`Module`] that isn't a declaration of its own.
88#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
89#[serde(deny_unknown_fields)]
90pub struct ModuleHeader {
91    /// The module name.
92    pub name: String,
93    /// The module's documentation.
94    #[serde(default, skip_serializing_if = "Option::is_none")]
95    pub doc: Option<String>,
96}
97
98/// One member of an interface.
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
100#[serde(deny_unknown_fields)]
101pub struct Member {
102    /// The name of the interface the member belongs to, declared in the same
103    /// module.
104    pub interface: String,
105    /// Which list of the interface the member is in.
106    pub role: Role,
107    /// The member's signature.
108    pub function: Function,
109}
110
111/// Which list of an [`InterfaceDef`] a [`Member`] belongs to.
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
113#[serde(rename_all = "snake_case")]
114pub enum Role {
115    /// [`InterfaceDef::constructors`].
116    Constructor,
117    /// [`InterfaceDef::methods`].
118    Method,
119    /// [`InterfaceDef::statics`].
120    Static,
121}
122
123/// A failure to decode or assemble metadata frames.
124#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
125pub enum MetaError {
126    /// The bytes end inside a frame.
127    #[error("metadata frame at byte {offset} is truncated")]
128    Truncated {
129        /// Where the frame starts.
130        offset: usize,
131    },
132    /// A frame's JSON doesn't describe a frame.
133    #[error("metadata frame at byte {offset} is malformed: {message}")]
134    Malformed {
135        /// Where the frame starts.
136        offset: usize,
137        /// The JSON error.
138        message: String,
139    },
140    /// A frame was written for another schema version.
141    #[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        /// The declaration's dotted path.
147        path: String,
148        /// The frame's schema version.
149        found: String,
150        /// The schema version this crate reads.
151        expected: String,
152    },
153    /// Two different frames describe the same declaration.
154    #[error("the library describes `{path}` twice, differently")]
155    Conflict {
156        /// The declaration's dotted path.
157        path: String,
158    },
159    /// A frame belongs to a module or interface no frame describes.
160    #[error("`{path}` belongs to `{parent}`, which the library doesn't describe")]
161    Orphan {
162        /// The declaration's dotted path.
163        path: String,
164        /// The missing module or interface.
165        parent: String,
166    },
167}
168
169impl Frame {
170    /// The declaration's own name.
171    #[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    /// The names that locate the declaration inside its module: `[name]`,
186    /// `[interface, name]` for a member, and nothing for a module frame.
187    #[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    /// The declaration's dotted path (`kv.Store.put`, `kv.stats`).
197    #[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    /// The name of the exported static that holds the frame:
208    /// `{PREFIX}_META_{HASH16}`, where `HASH16` is 16 uppercase hex digits
209    /// of [`fnv1a64`] over the [dotted path](Self::path) (followed by a `.`
210    /// for a module, so a module and a same-named declaration beside it
211    /// differ).
212    #[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    /// The frame's bytes: a little-endian `u32` length, then compact JSON.
226    ///
227    /// # Panics
228    ///
229    /// Panics if the JSON is longer than `u32::MAX` bytes.
230    #[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/// Whether `symbol` names a metadata static: it ends in `_META_` and 16
242/// uppercase hex digits. A leading underscore (Mach-O's symbol prefix) is
243/// fine; the frame's own prefix decides whose it is.
244#[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
256/// The byte length of the frame starting at `bytes`, length prefix
257/// included, read from its prefix alone.
258///
259/// # Errors
260///
261/// Returns [`MetaError::Truncated`] when `bytes` is shorter than the length
262/// prefix.
263pub 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
271/// Decode every frame in `bytes`, which holds zero or more frames back to
272/// back (one static's contents, or a whole `wasm32` custom section).
273///
274/// # Errors
275///
276/// Returns an error when a frame is truncated or its JSON isn't a frame.
277pub 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/// The frames describing `module` itself and its own declarations (not its
295/// submodules'): what the macro embeds for one module of a tree. `parent`
296/// is the path of the module's parent and `index` its position among its
297/// parent's submodules.
298#[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/// Every frame of `api` (each module's [`module_frames`], recursively), as
365/// though one crate with `prefix` declared it.
366#[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/// One module while assembling: its header and its declarations, each with
387/// its sibling index.
388#[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
400/// Assemble the [`Api`] that the frames with `prefix` describe, ignoring
401/// every other frame (a dependency's). Declarations keep the order the
402/// frames record; top-level modules, which separate macro invocations
403/// declare, are sorted by name. A frame repeated verbatim (a library that
404/// links an object twice) counts once.
405///
406/// # Errors
407///
408/// Returns an error when a frame was built for another schema version, two
409/// different frames describe the same declaration, or a frame belongs to a
410/// module or interface no frame describes.
411pub 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    // Every declaration's module must have a header, and every module's
479    // parent too.
480    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    // Top-level modules come from separate macro invocations, whose
498    // relative order the frames can't record.
499    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
506/// The first declaration of a header-less module, for the orphan error.
507fn 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
523/// Sort `(index, item)` pairs by index (then name, for a stable order when
524/// a producer's frames disagree) and drop the indices.
525fn 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
530/// Build the children of the module at `parent`, recursively.
531fn 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        // Link order is arbitrary; declaration order comes from the indices.
643        frames.reverse();
644        let decoded = decode(&section(&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}