wit_parser/
metadata.rs

1//! Implementation of encoding/decoding package metadata (docs/stability) in a
2//! custom section.
3//!
4//! This module contains the particulars for how this custom section is encoded
5//! and decoded at this time. As of the time of this writing the component model
6//! binary format does not have any means of storing documentation and/or item
7//! stability inline with items themselves. These are important to preserve when
8//! round-tripping WIT through the WebAssembly binary format, however, so this
9//! module implements this with a custom section.
10//!
11//! The custom section, named `SECTION_NAME`, is stored within the component
12//! that encodes a WIT package. This section is itself JSON-encoded with a small
13//! version header to help forwards/backwards compatibility. The hope is that
14//! one day this custom section will be obsoleted by extensions to the binary
15//! format to store this information inline.
16
17use crate::{
18    Docs, Function, InterfaceId, PackageId, Resolve, Stability, TypeDefKind, TypeId, WorldId,
19    WorldItem, WorldKey,
20};
21use anyhow::{bail, Result};
22use indexmap::IndexMap;
23#[cfg(feature = "serde")]
24use serde_derive::{Deserialize, Serialize};
25
26type StringMap<V> = IndexMap<String, V>;
27
28/// Current supported format of the custom section.
29///
30/// This byte is a prefix byte intended to be a general version marker for the
31/// entire custom section. This is bumped when backwards-incompatible changes
32/// are made to prevent older implementations from loading newer versions.
33///
34/// The history of this is:
35///
36/// * [????/??/??] 0 - the original format added
37/// * [2024/04/19] 1 - extensions were added for item stability and
38///   additionally having world imports/exports have the same name.
39#[cfg(feature = "serde")]
40const PACKAGE_DOCS_SECTION_VERSION: u8 = 1;
41
42/// At this time the v1 format was just written. For compatibility with older
43/// tools we'll still try to emit the v0 format by default, if the input is
44/// compatible. This will be turned off in the future once enough published
45/// versions support the v1 format.
46const TRY_TO_EMIT_V0_BY_DEFAULT: bool = true;
47
48/// Represents serializable doc comments parsed from a WIT package.
49#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
50#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
51pub struct PackageMetadata {
52    #[cfg_attr(
53        feature = "serde",
54        serde(default, skip_serializing_if = "Option::is_none")
55    )]
56    docs: Option<String>,
57    #[cfg_attr(
58        feature = "serde",
59        serde(default, skip_serializing_if = "StringMap::is_empty")
60    )]
61    worlds: StringMap<WorldMetadata>,
62    #[cfg_attr(
63        feature = "serde",
64        serde(default, skip_serializing_if = "StringMap::is_empty")
65    )]
66    interfaces: StringMap<InterfaceMetadata>,
67}
68
69impl PackageMetadata {
70    pub const SECTION_NAME: &'static str = "package-docs";
71
72    /// Extract package docs for the given package.
73    pub fn extract(resolve: &Resolve, package: PackageId) -> Self {
74        let package = &resolve.packages[package];
75
76        let worlds = package
77            .worlds
78            .iter()
79            .map(|(name, id)| (name.to_string(), WorldMetadata::extract(resolve, *id)))
80            .filter(|(_, item)| !item.is_empty())
81            .collect();
82        let interfaces = package
83            .interfaces
84            .iter()
85            .map(|(name, id)| (name.to_string(), InterfaceMetadata::extract(resolve, *id)))
86            .filter(|(_, item)| !item.is_empty())
87            .collect();
88
89        Self {
90            docs: package.docs.contents.as_deref().map(Into::into),
91            worlds,
92            interfaces,
93        }
94    }
95
96    /// Inject package docs for the given package.
97    ///
98    /// This will override any existing docs in the [`Resolve`].
99    pub fn inject(&self, resolve: &mut Resolve, package: PackageId) -> Result<()> {
100        for (name, docs) in &self.worlds {
101            let Some(&id) = resolve.packages[package].worlds.get(name) else {
102                bail!("missing world {name:?}");
103            };
104            docs.inject(resolve, id)?;
105        }
106        for (name, docs) in &self.interfaces {
107            let Some(&id) = resolve.packages[package].interfaces.get(name) else {
108                bail!("missing interface {name:?}");
109            };
110            docs.inject(resolve, id)?;
111        }
112        if let Some(docs) = &self.docs {
113            resolve.packages[package].docs.contents = Some(docs.to_string());
114        }
115        Ok(())
116    }
117
118    /// Encode package docs as a package-docs custom section.
119    #[cfg(feature = "serde")]
120    pub fn encode(&self) -> Result<Vec<u8>> {
121        // Version byte, followed by JSON encoding of docs.
122        //
123        // Note that if this document is compatible with the v0 format then
124        // that's preferred to keep older tools working at this time.
125        // Eventually this branch will be removed and v1 will unconditionally
126        // be used.
127        let mut data = vec![
128            if TRY_TO_EMIT_V0_BY_DEFAULT && self.is_compatible_with_v0() {
129                0
130            } else {
131                PACKAGE_DOCS_SECTION_VERSION
132            },
133        ];
134        serde_json::to_writer(&mut data, self)?;
135        Ok(data)
136    }
137
138    /// Decode package docs from package-docs custom section content.
139    #[cfg(feature = "serde")]
140    pub fn decode(data: &[u8]) -> Result<Self> {
141        match data.first().copied() {
142            // Our serde structures transparently support v0 and the current
143            // version, so allow either here.
144            Some(0) | Some(PACKAGE_DOCS_SECTION_VERSION) => {}
145            version => {
146                bail!(
147                    "expected package-docs version {PACKAGE_DOCS_SECTION_VERSION}, got {version:?}"
148                );
149            }
150        }
151        Ok(serde_json::from_slice(&data[1..])?)
152    }
153
154    #[cfg(feature = "serde")]
155    fn is_compatible_with_v0(&self) -> bool {
156        self.worlds.iter().all(|(_, w)| w.is_compatible_with_v0())
157            && self
158                .interfaces
159                .iter()
160                .all(|(_, w)| w.is_compatible_with_v0())
161    }
162}
163
164#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
165#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
166struct WorldMetadata {
167    #[cfg_attr(
168        feature = "serde",
169        serde(default, skip_serializing_if = "Option::is_none")
170    )]
171    docs: Option<String>,
172    #[cfg_attr(
173        feature = "serde",
174        serde(default, skip_serializing_if = "Stability::is_unknown")
175    )]
176    stability: Stability,
177
178    /// Metadata for named interface, e.g.:
179    ///
180    /// ```wit
181    /// world foo {
182    ///     import x: interface {}
183    /// }
184    /// ```
185    ///
186    /// In the v0 format this was called "interfaces", hence the
187    /// `serde(rename)`. When support was originally added here imports/exports
188    /// could not overlap in their name, but now they can. This map has thus
189    /// been repurposed as:
190    ///
191    /// * If an interface is imported, it goes here.
192    /// * If an interface is exported, and no interface was imported with the
193    ///   same name, it goes here.
194    ///
195    /// Otherwise exports go inside the `interface_exports` map.
196    ///
197    /// In the future when v0 support is dropped this should become only
198    /// imports, not either imports-or-exports.
199    #[cfg_attr(
200        feature = "serde",
201        serde(
202            default,
203            rename = "interfaces",
204            skip_serializing_if = "StringMap::is_empty"
205        )
206    )]
207    interface_imports_or_exports: StringMap<InterfaceMetadata>,
208
209    /// All types in this interface.
210    ///
211    /// Note that at this time types are only imported, never exported.
212    #[cfg_attr(
213        feature = "serde",
214        serde(default, skip_serializing_if = "StringMap::is_empty")
215    )]
216    types: StringMap<TypeMetadata>,
217
218    /// Same as `interface_imports_or_exports`, but for functions.
219    #[cfg_attr(
220        feature = "serde",
221        serde(default, rename = "funcs", skip_serializing_if = "StringMap::is_empty")
222    )]
223    func_imports_or_exports: StringMap<FunctionMetadata>,
224
225    /// The "export half" of `interface_imports_or_exports`.
226    #[cfg_attr(
227        feature = "serde",
228        serde(default, skip_serializing_if = "StringMap::is_empty")
229    )]
230    interface_exports: StringMap<InterfaceMetadata>,
231
232    /// The "export half" of `func_imports_or_exports`.
233    #[cfg_attr(
234        feature = "serde",
235        serde(default, skip_serializing_if = "StringMap::is_empty")
236    )]
237    func_exports: StringMap<FunctionMetadata>,
238
239    /// Stability annotations for interface imports that aren't inline, for
240    /// example:
241    ///
242    /// ```wit
243    /// world foo {
244    ///     @since(version = 1.0.0)
245    ///     import an-interface;
246    /// }
247    /// ```
248    #[cfg_attr(
249        feature = "serde",
250        serde(default, skip_serializing_if = "StringMap::is_empty")
251    )]
252    interface_import_stability: StringMap<Stability>,
253
254    /// Same as `interface_import_stability`, but for exports.
255    #[cfg_attr(
256        feature = "serde",
257        serde(default, skip_serializing_if = "StringMap::is_empty")
258    )]
259    interface_export_stability: StringMap<Stability>,
260}
261
262impl WorldMetadata {
263    fn extract(resolve: &Resolve, id: WorldId) -> Self {
264        let world = &resolve.worlds[id];
265
266        let mut interface_imports_or_exports = StringMap::default();
267        let mut types = StringMap::default();
268        let mut func_imports_or_exports = StringMap::default();
269        let mut interface_exports = StringMap::default();
270        let mut func_exports = StringMap::default();
271        let mut interface_import_stability = StringMap::default();
272        let mut interface_export_stability = StringMap::default();
273
274        for ((key, item), import) in world
275            .imports
276            .iter()
277            .map(|p| (p, true))
278            .chain(world.exports.iter().map(|p| (p, false)))
279        {
280            match key {
281                // For all named imports with kebab-names extract their
282                // docs/stability and insert it into one of our maps.
283                WorldKey::Name(name) => match item {
284                    WorldItem::Interface { id, .. } => {
285                        let data = InterfaceMetadata::extract(resolve, *id);
286                        if data.is_empty() {
287                            continue;
288                        }
289                        let map = if import {
290                            &mut interface_imports_or_exports
291                        } else if !TRY_TO_EMIT_V0_BY_DEFAULT
292                            || interface_imports_or_exports.contains_key(name)
293                        {
294                            &mut interface_exports
295                        } else {
296                            &mut interface_imports_or_exports
297                        };
298                        let prev = map.insert(name.to_string(), data);
299                        assert!(prev.is_none());
300                    }
301                    WorldItem::Type(id) => {
302                        let data = TypeMetadata::extract(resolve, *id);
303                        if !data.is_empty() {
304                            types.insert(name.to_string(), data);
305                        }
306                    }
307                    WorldItem::Function(f) => {
308                        let data = FunctionMetadata::extract(f);
309                        if data.is_empty() {
310                            continue;
311                        }
312                        let map = if import {
313                            &mut func_imports_or_exports
314                        } else if !TRY_TO_EMIT_V0_BY_DEFAULT
315                            || func_imports_or_exports.contains_key(name)
316                        {
317                            &mut func_exports
318                        } else {
319                            &mut func_imports_or_exports
320                        };
321                        let prev = map.insert(name.to_string(), data);
322                        assert!(prev.is_none());
323                    }
324                },
325
326                // For interface imports/exports extract the stability and
327                // record it if necessary.
328                WorldKey::Interface(_) => {
329                    let stability = match item {
330                        WorldItem::Interface { stability, .. } => stability,
331                        _ => continue,
332                    };
333                    if stability.is_unknown() {
334                        continue;
335                    }
336
337                    let map = if import {
338                        &mut interface_import_stability
339                    } else {
340                        &mut interface_export_stability
341                    };
342                    let name = resolve.name_world_key(key);
343                    map.insert(name, stability.clone());
344                }
345            }
346        }
347
348        Self {
349            docs: world.docs.contents.clone(),
350            stability: world.stability.clone(),
351            interface_imports_or_exports,
352            types,
353            func_imports_or_exports,
354            interface_exports,
355            func_exports,
356            interface_import_stability,
357            interface_export_stability,
358        }
359    }
360
361    fn inject(&self, resolve: &mut Resolve, id: WorldId) -> Result<()> {
362        // Inject docs/stability for all kebab-named interfaces, both imports
363        // and exports.
364        for ((name, data), only_export) in self
365            .interface_imports_or_exports
366            .iter()
367            .map(|p| (p, false))
368            .chain(self.interface_exports.iter().map(|p| (p, true)))
369        {
370            let key = WorldKey::Name(name.to_string());
371            let world = &mut resolve.worlds[id];
372
373            let item = if only_export {
374                world.exports.get_mut(&key)
375            } else {
376                match world.imports.get_mut(&key) {
377                    Some(item) => Some(item),
378                    None => world.exports.get_mut(&key),
379                }
380            };
381            let Some(WorldItem::Interface { id, stability }) = item else {
382                bail!("missing interface {name:?}");
383            };
384            *stability = data.stability.clone();
385            let id = *id;
386            data.inject(resolve, id)?;
387        }
388
389        // Process all types, which are always imported, for this world.
390        for (name, data) in &self.types {
391            let key = WorldKey::Name(name.to_string());
392            let Some(WorldItem::Type(id)) = resolve.worlds[id].imports.get(&key) else {
393                bail!("missing type {name:?}");
394            };
395            data.inject(resolve, *id)?;
396        }
397
398        // Build a map of `name_world_key` for interface imports/exports to the
399        // actual key. This map is then consluted in the next loop.
400        let world = &resolve.worlds[id];
401        let stabilities = world
402            .imports
403            .iter()
404            .map(|i| (i, true))
405            .chain(world.exports.iter().map(|i| (i, false)))
406            .filter_map(|((key, item), import)| match item {
407                WorldItem::Interface { .. } => {
408                    Some(((resolve.name_world_key(key), import), key.clone()))
409                }
410                _ => None,
411            })
412            .collect::<IndexMap<_, _>>();
413
414        let world = &mut resolve.worlds[id];
415
416        // Update the stability of an interface imports/exports that aren't
417        // kebab-named.
418        for ((name, stability), import) in self
419            .interface_import_stability
420            .iter()
421            .map(|p| (p, true))
422            .chain(self.interface_export_stability.iter().map(|p| (p, false)))
423        {
424            let key = match stabilities.get(&(name.clone(), import)) {
425                Some(key) => key.clone(),
426                None => bail!("missing interface `{name}`"),
427            };
428            let item = if import {
429                world.imports.get_mut(&key)
430            } else {
431                world.exports.get_mut(&key)
432            };
433            match item {
434                Some(WorldItem::Interface { stability: s, .. }) => *s = stability.clone(),
435                _ => bail!("item `{name}` wasn't an interface"),
436            }
437        }
438
439        // Update the docs/stability of all functions imported/exported from
440        // this world.
441        for ((name, data), only_export) in self
442            .func_imports_or_exports
443            .iter()
444            .map(|p| (p, false))
445            .chain(self.func_exports.iter().map(|p| (p, true)))
446        {
447            let key = WorldKey::Name(name.to_string());
448            let item = if only_export {
449                world.exports.get_mut(&key)
450            } else {
451                match world.imports.get_mut(&key) {
452                    Some(item) => Some(item),
453                    None => world.exports.get_mut(&key),
454                }
455            };
456            match item {
457                Some(WorldItem::Function(f)) => data.inject(f)?,
458                _ => bail!("missing func {name:?}"),
459            }
460        }
461        if let Some(docs) = &self.docs {
462            world.docs.contents = Some(docs.to_string());
463        }
464        world.stability = self.stability.clone();
465        Ok(())
466    }
467
468    fn is_empty(&self) -> bool {
469        self.docs.is_none()
470            && self.interface_imports_or_exports.is_empty()
471            && self.types.is_empty()
472            && self.func_imports_or_exports.is_empty()
473            && self.stability.is_unknown()
474            && self.interface_exports.is_empty()
475            && self.func_exports.is_empty()
476            && self.interface_import_stability.is_empty()
477            && self.interface_export_stability.is_empty()
478    }
479
480    #[cfg(feature = "serde")]
481    fn is_compatible_with_v0(&self) -> bool {
482        self.stability.is_unknown()
483            && self
484                .interface_imports_or_exports
485                .iter()
486                .all(|(_, w)| w.is_compatible_with_v0())
487            && self
488                .func_imports_or_exports
489                .iter()
490                .all(|(_, w)| w.is_compatible_with_v0())
491            && self.types.iter().all(|(_, w)| w.is_compatible_with_v0())
492            // These maps weren't present in v0, so we're only compatible if
493            // they're empty.
494            && self.interface_exports.is_empty()
495            && self.func_exports.is_empty()
496            && self.interface_import_stability.is_empty()
497            && self.interface_export_stability.is_empty()
498    }
499}
500
501#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
502#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
503struct InterfaceMetadata {
504    #[cfg_attr(
505        feature = "serde",
506        serde(default, skip_serializing_if = "Option::is_none")
507    )]
508    docs: Option<String>,
509    #[cfg_attr(
510        feature = "serde",
511        serde(default, skip_serializing_if = "Stability::is_unknown")
512    )]
513    stability: Stability,
514    #[cfg_attr(
515        feature = "serde",
516        serde(default, skip_serializing_if = "StringMap::is_empty")
517    )]
518    funcs: StringMap<FunctionMetadata>,
519    #[cfg_attr(
520        feature = "serde",
521        serde(default, skip_serializing_if = "StringMap::is_empty")
522    )]
523    types: StringMap<TypeMetadata>,
524}
525
526impl InterfaceMetadata {
527    fn extract(resolve: &Resolve, id: InterfaceId) -> Self {
528        let interface = &resolve.interfaces[id];
529
530        let funcs = interface
531            .functions
532            .iter()
533            .map(|(name, func)| (name.to_string(), FunctionMetadata::extract(func)))
534            .filter(|(_, item)| !item.is_empty())
535            .collect();
536        let types = interface
537            .types
538            .iter()
539            .map(|(name, id)| (name.to_string(), TypeMetadata::extract(resolve, *id)))
540            .filter(|(_, item)| !item.is_empty())
541            .collect();
542
543        Self {
544            docs: interface.docs.contents.clone(),
545            stability: interface.stability.clone(),
546            funcs,
547            types,
548        }
549    }
550
551    fn inject(&self, resolve: &mut Resolve, id: InterfaceId) -> Result<()> {
552        for (name, data) in &self.types {
553            let Some(&id) = resolve.interfaces[id].types.get(name) else {
554                bail!("missing type {name:?}");
555            };
556            data.inject(resolve, id)?;
557        }
558        let interface = &mut resolve.interfaces[id];
559        for (name, data) in &self.funcs {
560            let Some(f) = interface.functions.get_mut(name) else {
561                bail!("missing func {name:?}");
562            };
563            data.inject(f)?;
564        }
565        if let Some(docs) = &self.docs {
566            interface.docs.contents = Some(docs.to_string());
567        }
568        interface.stability = self.stability.clone();
569        Ok(())
570    }
571
572    fn is_empty(&self) -> bool {
573        self.docs.is_none()
574            && self.funcs.is_empty()
575            && self.types.is_empty()
576            && self.stability.is_unknown()
577    }
578
579    #[cfg(feature = "serde")]
580    fn is_compatible_with_v0(&self) -> bool {
581        self.stability.is_unknown()
582            && self.funcs.iter().all(|(_, w)| w.is_compatible_with_v0())
583            && self.types.iter().all(|(_, w)| w.is_compatible_with_v0())
584    }
585}
586
587#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
588#[cfg_attr(feature = "serde", serde(untagged, deny_unknown_fields))]
589enum FunctionMetadata {
590    /// In the v0 format function metadata was only a string so this variant
591    /// is preserved for the v0 format. In the future this can be removed
592    /// entirely in favor of just the below struct variant.
593    ///
594    /// Note that this is an untagged enum so the name `JustDocs` is just for
595    /// rust.
596    JustDocs(Option<String>),
597
598    /// In the v1+ format we're tracking at least docs but also the stability
599    /// of functions.
600    DocsAndStabilty {
601        #[cfg_attr(
602            feature = "serde",
603            serde(default, skip_serializing_if = "Option::is_none")
604        )]
605        docs: Option<String>,
606        #[cfg_attr(
607            feature = "serde",
608            serde(default, skip_serializing_if = "Stability::is_unknown")
609        )]
610        stability: Stability,
611    },
612}
613
614impl FunctionMetadata {
615    fn extract(func: &Function) -> Self {
616        if TRY_TO_EMIT_V0_BY_DEFAULT && func.stability.is_unknown() {
617            FunctionMetadata::JustDocs(func.docs.contents.clone())
618        } else {
619            FunctionMetadata::DocsAndStabilty {
620                docs: func.docs.contents.clone(),
621                stability: func.stability.clone(),
622            }
623        }
624    }
625
626    fn inject(&self, func: &mut Function) -> Result<()> {
627        match self {
628            FunctionMetadata::JustDocs(docs) => {
629                func.docs.contents = docs.clone();
630            }
631            FunctionMetadata::DocsAndStabilty { docs, stability } => {
632                func.docs.contents = docs.clone();
633                func.stability = stability.clone();
634            }
635        }
636        Ok(())
637    }
638
639    fn is_empty(&self) -> bool {
640        match self {
641            FunctionMetadata::JustDocs(docs) => docs.is_none(),
642            FunctionMetadata::DocsAndStabilty { docs, stability } => {
643                docs.is_none() && stability.is_unknown()
644            }
645        }
646    }
647
648    #[cfg(feature = "serde")]
649    fn is_compatible_with_v0(&self) -> bool {
650        match self {
651            FunctionMetadata::JustDocs(_) => true,
652            FunctionMetadata::DocsAndStabilty { .. } => false,
653        }
654    }
655}
656
657#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
658#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
659struct TypeMetadata {
660    #[cfg_attr(
661        feature = "serde",
662        serde(default, skip_serializing_if = "Option::is_none")
663    )]
664    docs: Option<String>,
665    #[cfg_attr(
666        feature = "serde",
667        serde(default, skip_serializing_if = "Stability::is_unknown")
668    )]
669    stability: Stability,
670    // record fields, variant cases, etc.
671    #[cfg_attr(
672        feature = "serde",
673        serde(default, skip_serializing_if = "StringMap::is_empty")
674    )]
675    items: StringMap<String>,
676}
677
678impl TypeMetadata {
679    fn extract(resolve: &Resolve, id: TypeId) -> Self {
680        fn extract_items<T>(items: &[T], f: impl Fn(&T) -> (&String, &Docs)) -> StringMap<String> {
681            items
682                .iter()
683                .flat_map(|item| {
684                    let (name, docs) = f(item);
685                    Some((name.to_string(), docs.contents.clone()?))
686                })
687                .collect()
688        }
689        let ty = &resolve.types[id];
690        let items = match &ty.kind {
691            TypeDefKind::Record(record) => {
692                extract_items(&record.fields, |item| (&item.name, &item.docs))
693            }
694            TypeDefKind::Flags(flags) => {
695                extract_items(&flags.flags, |item| (&item.name, &item.docs))
696            }
697            TypeDefKind::Variant(variant) => {
698                extract_items(&variant.cases, |item| (&item.name, &item.docs))
699            }
700            TypeDefKind::Enum(enum_) => {
701                extract_items(&enum_.cases, |item| (&item.name, &item.docs))
702            }
703            // other types don't have inner items
704            _ => IndexMap::default(),
705        };
706
707        Self {
708            docs: ty.docs.contents.clone(),
709            stability: ty.stability.clone(),
710            items,
711        }
712    }
713
714    fn inject(&self, resolve: &mut Resolve, id: TypeId) -> Result<()> {
715        let ty = &mut resolve.types[id];
716        if !self.items.is_empty() {
717            match &mut ty.kind {
718                TypeDefKind::Record(record) => {
719                    self.inject_items(&mut record.fields, |item| (&item.name, &mut item.docs))?
720                }
721                TypeDefKind::Flags(flags) => {
722                    self.inject_items(&mut flags.flags, |item| (&item.name, &mut item.docs))?
723                }
724                TypeDefKind::Variant(variant) => {
725                    self.inject_items(&mut variant.cases, |item| (&item.name, &mut item.docs))?
726                }
727                TypeDefKind::Enum(enum_) => {
728                    self.inject_items(&mut enum_.cases, |item| (&item.name, &mut item.docs))?
729                }
730                _ => {
731                    bail!("got 'items' for unexpected type {ty:?}");
732                }
733            }
734        }
735        if let Some(docs) = &self.docs {
736            ty.docs.contents = Some(docs.to_string());
737        }
738        ty.stability = self.stability.clone();
739        Ok(())
740    }
741
742    fn inject_items<T: std::fmt::Debug>(
743        &self,
744        items: &mut [T],
745        f: impl Fn(&mut T) -> (&String, &mut Docs),
746    ) -> Result<()> {
747        let mut unused_docs = self.items.len();
748        for item in items.iter_mut() {
749            let (name, item_docs) = f(item);
750            if let Some(docs) = self.items.get(name.as_str()) {
751                item_docs.contents = Some(docs.to_string());
752                unused_docs -= 1;
753            }
754        }
755        if unused_docs > 0 {
756            bail!(
757                "not all 'items' match type items; {item_docs:?} vs {items:?}",
758                item_docs = self.items
759            );
760        }
761        Ok(())
762    }
763
764    fn is_empty(&self) -> bool {
765        self.docs.is_none() && self.items.is_empty() && self.stability.is_unknown()
766    }
767
768    #[cfg(feature = "serde")]
769    fn is_compatible_with_v0(&self) -> bool {
770        self.stability.is_unknown()
771    }
772}