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 | |
17 | use crate::{ |
18 | Docs, Function, InterfaceId, PackageId, Resolve, Stability, TypeDefKind, TypeId, WorldId, |
19 | WorldItem, WorldKey, |
20 | }; |
21 | use anyhow::{bail, Result}; |
22 | use indexmap::IndexMap; |
23 | #[cfg (feature = "serde" )] |
24 | use serde_derive::{Deserialize, Serialize}; |
25 | |
26 | type 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" )] |
40 | const 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. |
46 | const 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))] |
51 | pub 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 | |
69 | impl 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))] |
166 | struct 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 | |
262 | impl 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))] |
503 | struct 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 | |
526 | impl 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))] |
589 | enum 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 | |
614 | impl 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))] |
659 | struct 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 | |
678 | impl 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 | } |
773 | |