1use std::collections::HashMap;
29use std::fs;
30use std::io;
31use std::path::{Path, PathBuf};
32
33use quote::ToTokens;
34use syn::spanned::Spanned;
35use syn::{
36 Attribute, Fields, GenericArgument, PathArguments, Type, TypeArray, TypePath, TypeTuple,
37 UseTree,
38};
39use walkdir::WalkDir;
40
41use crate::error::{Diagnostic, DiagnosticKind, Error, SourceLocation};
42use crate::expr::{CodecExpr, codec};
43use crate::generator::{CodeGenerator, EnumVariant, OnUnknown, TypeKind};
44use crate::registry::WithWrapper;
45
46struct SourceContext {
48 imports: HashMap<String, String>,
50 globs: Vec<String>,
52 file: Option<PathBuf>,
54}
55
56impl SourceContext {
57 fn location(&self, span: proc_macro2::Span) -> SourceLocation {
58 let start = span.start();
59 SourceLocation {
60 file: self.file.clone(),
61 line: start.line,
62 column: start.column + 1,
63 }
64 }
65}
66
67fn collect_imports(
69 tree: &UseTree,
70 prefix: &[String],
71 imports: &mut HashMap<String, String>,
72 globs: &mut Vec<String>,
73) {
74 match tree {
75 UseTree::Path(p) => {
76 let mut new_prefix = prefix.to_vec();
77 new_prefix.push(p.ident.to_string());
78 collect_imports(&p.tree, &new_prefix, imports, globs);
79 }
80 UseTree::Name(n) => {
81 let name = n.ident.to_string();
82 let full_path = make_full_path(prefix, &name);
83 imports.insert(name, full_path);
84 }
85 UseTree::Rename(r) => {
86 let canonical = r.ident.to_string();
87 let alias = r.rename.to_string();
88 let full_path = make_full_path(prefix, &canonical);
89 imports.insert(alias, full_path);
90 }
91 UseTree::Glob(_) => {
92 if !prefix.is_empty() {
93 globs.push(prefix.join("::"));
94 }
95 }
96 UseTree::Group(g) => {
97 for item in &g.items {
98 collect_imports(item, prefix, imports, globs);
99 }
100 }
101 }
102}
103
104fn make_full_path(prefix: &[String], name: &str) -> String {
105 if prefix.is_empty() {
106 name.to_string()
107 } else {
108 format!("{}::{}", prefix.join("::"), name)
109 }
110}
111
112fn path_segments(path: &syn::Path) -> Vec<String> {
113 path.segments.iter().map(|s| s.ident.to_string()).collect()
114}
115
116fn build_source_context(file: &syn::File, source_file: Option<PathBuf>) -> SourceContext {
118 let mut imports = HashMap::new();
119 let mut globs = Vec::new();
120
121 for item in &file.items {
122 match item {
123 syn::Item::Use(item_use) => {
124 collect_imports(&item_use.tree, &[], &mut imports, &mut globs);
125 }
126 syn::Item::Type(item_type) => {
130 if let Type::Path(TypePath { path, .. }) = &*item_type.ty
131 && path.segments.len() > 1
132 {
133 imports.insert(
134 item_type.ident.to_string(),
135 path_segments(path).join("::"),
136 );
137 }
138 }
139 _ => {}
140 }
141 }
142
143 SourceContext {
144 imports,
145 globs,
146 file: source_file,
147 }
148}
149
150#[derive(Default)]
152struct RkyvTypeAttrs {
153 remote: Option<syn::Type>,
155 archived: Option<String>,
157}
158
159fn skip_nested_meta_value(meta: &syn::meta::ParseNestedMeta) -> syn::Result<()> {
161 if meta.input.peek(syn::Token![=]) {
162 let _eq: syn::Token![=] = meta.input.parse()?;
163 while !meta.input.is_empty() && !meta.input.peek(syn::Token![,]) {
164 let _tt: proc_macro2::TokenTree = meta.input.parse()?;
165 }
166 } else if meta.input.peek(syn::token::Paren) {
167 let content;
168 syn::parenthesized!(content in meta.input);
169 let _rest: proc_macro2::TokenStream = content.parse()?;
170 }
171 Ok(())
172}
173
174fn parse_rkyv_type_attrs(attrs: &[Attribute]) -> RkyvTypeAttrs {
175 let mut parsed = RkyvTypeAttrs::default();
176 for attr in attrs {
177 if !attr.path().is_ident("rkyv") {
178 continue;
179 }
180 let _ = attr.parse_nested_meta(|meta| {
181 if meta.path.is_ident("remote") {
182 parsed.remote = Some(meta.value()?.parse()?);
183 } else if meta.path.is_ident("archived") {
184 let path: syn::Path = meta.value()?.parse()?;
185 if let Some(last) = path.segments.last() {
186 parsed.archived = Some(last.ident.to_string());
187 }
188 } else {
189 skip_nested_meta_value(&meta)?;
190 }
191 Ok(())
192 });
193 }
194 parsed
195}
196
197fn parse_rkyv_field_with(attrs: &[Attribute]) -> Option<syn::Type> {
199 let mut with: Option<syn::Type> = None;
200 for attr in attrs {
201 if !attr.path().is_ident("rkyv") {
202 continue;
203 }
204 let _ = attr.parse_nested_meta(|meta| {
205 if meta.path.is_ident("with") {
206 if with.is_none() {
207 with = Some(meta.value()?.parse()?);
208 } else {
209 skip_nested_meta_value(&meta)?;
210 }
211 } else {
212 skip_nested_meta_value(&meta)?;
213 }
214 Ok(())
215 });
216 }
217 with
218}
219
220fn has_marker_derive(attrs: &[Attribute], ctx: &SourceContext, codegen: &CodeGenerator) -> bool {
222 let markers = &codegen.marker_paths;
223 for attr in attrs {
224 if !attr.path().is_ident("derive") {
225 continue;
226 }
227 let Ok(nested) = attr.parse_args_with(
228 syn::punctuated::Punctuated::<syn::Path, syn::Token![,]>::parse_terminated,
229 ) else {
230 continue;
231 };
232 for path in nested {
233 let segments = path_segments(&path);
234 if segments.len() == 1 {
235 let ident = &segments[0];
236 if markers.contains(ident) {
238 return true;
239 }
240 if ctx.imports.get(ident).is_some_and(|fq| markers.contains(fq)) {
242 return true;
243 }
244 if ctx
246 .globs
247 .iter()
248 .any(|glob| markers.contains(&format!("{glob}::{ident}")))
249 {
250 return true;
251 }
252 } else {
253 let joined = segments.join("::");
254 if markers.contains(&joined) {
255 return true;
256 }
257 }
258 }
259 }
260 false
261}
262
263fn type_to_string(ty: &Type) -> String {
264 ty.to_token_stream().to_string()
265}
266
267fn type_to_expr(
272 ty: &Type,
273 codegen: &CodeGenerator,
274 ctx: &SourceContext,
275) -> Result<CodecExpr, DiagnosticKind> {
276 match ty {
277 Type::Path(TypePath { qself: None, path }) => {
278 let segment = path.segments.last().expect("type paths are non-empty");
279 let raw_ident = segment.ident.to_string();
280
281 let full_path = if path.segments.len() > 1 {
284 path_segments(path).join("::")
285 } else {
286 ctx.imports
287 .get(&raw_ident)
288 .cloned()
289 .unwrap_or_else(|| raw_ident.clone())
290 };
291
292 match full_path.as_str() {
293 "u8" => Ok(codec::u8()),
294 "i8" => Ok(codec::i8()),
295 "u16" => Ok(codec::u16()),
296 "i16" => Ok(codec::i16()),
297 "u32" => Ok(codec::u32()),
298 "i32" => Ok(codec::i32()),
299 "u64" => Ok(codec::u64()),
300 "i64" => Ok(codec::i64()),
301 "f32" => Ok(codec::f32()),
302 "f64" => Ok(codec::f64()),
303 "bool" => Ok(codec::bool_()),
304 "char" => Ok(codec::char_()),
305 "String" | "std::string::String" => Ok(codec::string()),
306 "Vec" | "std::vec::Vec" => {
307 let inner = single_generic_arg(segment, ty)?;
308 Ok(codec::vec(type_to_expr(inner, codegen, ctx)?))
309 }
310 "Option" | "std::option::Option" => {
311 let inner = single_generic_arg(segment, ty)?;
312 Ok(codec::option(type_to_expr(inner, codegen, ctx)?))
313 }
314 "Box" | "std::boxed::Box" => {
315 let inner = single_generic_arg(segment, ty)?;
316 Ok(codec::boxed(type_to_expr(inner, codegen, ctx)?))
317 }
318 _ => {
319 if let Some(external) = codegen.registry.get_type(&full_path) {
320 let raw_args = collect_type_args(segment);
321 let keep = if external.allows_trailing() && raw_args.len() > external.arity()
324 {
325 external.arity()
326 } else {
327 raw_args.len()
328 };
329 let args = raw_args[..keep]
330 .iter()
331 .map(|arg| type_to_expr(arg, codegen, ctx))
332 .collect::<Result<Vec<_>, _>>()?;
333 external.instantiate(args).map_err(|kind| match kind {
334 DiagnosticKind::GenericArity {
335 expected, found, ..
336 } => DiagnosticKind::GenericArity {
337 rust_path: full_path.clone(),
338 expected,
339 found,
340 },
341 other => other,
342 })
343 } else if path.segments.len() == 1 && full_path == raw_ident {
344 Ok(codec::named(raw_ident))
347 } else {
348 Err(DiagnosticKind::UnknownType {
349 suggestion: codegen.registry.suggest_type(&full_path),
350 rust_path: full_path,
351 })
352 }
353 }
354 }
355 }
356 Type::Array(TypeArray { elem, len, .. }) => {
357 let elem_expr = type_to_expr(elem, codegen, ctx)?;
358 if let syn::Expr::Lit(syn::ExprLit {
359 lit: syn::Lit::Int(lit_int),
360 ..
361 }) = len
362 && let Ok(len_val) = lit_int.base10_parse::<u64>()
363 {
364 Ok(codec::array(elem_expr, len_val))
365 } else {
366 Err(DiagnosticKind::UnsupportedFieldType {
367 rust_type: type_to_string(ty),
368 })
369 }
370 }
371 Type::Tuple(TypeTuple { elems, .. }) => {
372 let elem_exprs = elems
373 .iter()
374 .map(|elem| type_to_expr(elem, codegen, ctx))
375 .collect::<Result<Vec<_>, _>>()?;
376 Ok(codec::tuple(elem_exprs))
377 }
378 Type::Reference(reference) => {
379 if let Type::Path(TypePath { path, .. }) = &*reference.elem
380 && path.is_ident("str")
381 {
382 return Ok(codec::string());
383 }
384 type_to_expr(&reference.elem, codegen, ctx)
385 }
386 Type::Paren(paren) => type_to_expr(&paren.elem, codegen, ctx),
387 Type::Group(group) => type_to_expr(&group.elem, codegen, ctx),
388 other => Err(DiagnosticKind::UnsupportedFieldType {
389 rust_type: type_to_string(other),
390 }),
391 }
392}
393
394fn single_generic_arg<'a>(
395 segment: &'a syn::PathSegment,
396 whole: &Type,
397) -> Result<&'a Type, DiagnosticKind> {
398 if let PathArguments::AngleBracketed(args) = &segment.arguments
399 && let Some(GenericArgument::Type(ty)) = args.args.first()
400 {
401 return Ok(ty);
402 }
403 Err(DiagnosticKind::UnsupportedFieldType {
404 rust_type: type_to_string(whole),
405 })
406}
407
408fn collect_type_args(segment: &syn::PathSegment) -> Vec<&Type> {
413 let PathArguments::AngleBracketed(args) = &segment.arguments else {
414 return vec![];
415 };
416
417 let mut type_args = Vec::new();
418 for arg in &args.args {
419 if let GenericArgument::Type(ty) = arg {
420 if let Type::Array(TypeArray { elem, .. }) = ty {
421 type_args.push(elem.as_ref());
422 } else {
423 type_args.push(ty);
424 }
425 }
426 }
427 type_args
428}
429
430fn resolve_wrapper_path(ty: &syn::Type, ctx: &SourceContext) -> Option<String> {
432 let Type::Path(TypePath { qself: None, path }) = ty else {
433 return None;
434 };
435 let segments = path_segments(path);
436 if segments.len() == 1 {
437 Some(
438 ctx.imports
439 .get(&segments[0])
440 .cloned()
441 .unwrap_or_else(|| segments[0].clone()),
442 )
443 } else {
444 Some(segments.join("::"))
445 }
446}
447
448fn field_expr(
452 field: &syn::Field,
453 context: &str,
454 codegen: &CodeGenerator,
455 ctx: &SourceContext,
456) -> Result<Option<CodecExpr>, Diagnostic> {
457 if let Some(with_type) = parse_rkyv_field_with(&field.attrs) {
458 let location = ctx.location(with_type.span());
459 let resolved = resolve_wrapper_path(&with_type, ctx);
460 let wrapper = resolved
461 .as_deref()
462 .and_then(|path| lookup_wrapper(codegen, ctx, path));
463 let Some(wrapper) = wrapper else {
464 return Err(Diagnostic::new(DiagnosticKind::UnknownWithWrapper {
465 wrapper_path: resolved.unwrap_or_else(|| type_to_string(&with_type)),
466 })
467 .referenced_by(context)
468 .at(Some(location)));
469 };
470 let underlying = if wrapper.needs_underlying() {
471 let expr = type_to_expr(&field.ty, codegen, ctx).map_err(|kind| {
472 Diagnostic::new(kind)
473 .referenced_by(context)
474 .at(Some(ctx.location(field.ty.span())))
475 })?;
476 Some(expr)
477 } else {
478 None
479 };
480 return Ok(wrapper.apply(underlying));
481 }
482
483 type_to_expr(&field.ty, codegen, ctx)
484 .map(Some)
485 .map_err(|kind| {
486 Diagnostic::new(kind)
487 .referenced_by(context)
488 .at(Some(ctx.location(field.ty.span())))
489 })
490}
491
492fn lookup_wrapper(
495 codegen: &CodeGenerator,
496 ctx: &SourceContext,
497 path: &str,
498) -> Option<WithWrapper> {
499 if let Some(wrapper) = codegen.registry.get_wrapper(path) {
500 return Some(wrapper.clone());
501 }
502 if !path.contains("::") {
503 for glob in &ctx.globs {
504 if let Some(wrapper) = codegen.registry.get_wrapper(&format!("{glob}::{path}")) {
505 return Some(wrapper.clone());
506 }
507 }
508 }
509 None
510}
511
512enum StructShape {
515 Record(Vec<(String, CodecExpr)>),
516 Tuple(Vec<CodecExpr>),
517}
518
519fn extract_struct_shape(
520 type_name: &str,
521 fields: &Fields,
522 codegen: &CodeGenerator,
523 ctx: &SourceContext,
524) -> Result<StructShape, Vec<Diagnostic>> {
525 let mut diagnostics = Vec::new();
526
527 let shape = match fields {
528 Fields::Named(named) => {
529 let mut out = Vec::new();
530 for field in &named.named {
531 let field_name = field
532 .ident
533 .as_ref()
534 .expect("named fields have idents")
535 .to_string();
536 let context = format!("{type_name}.{field_name}");
537 match field_expr(field, &context, codegen, ctx) {
538 Ok(Some(expr)) => out.push((field_name, expr)),
539 Ok(None) => {}
540 Err(diagnostic) => diagnostics.push(diagnostic),
541 }
542 }
543 StructShape::Record(out)
544 }
545 Fields::Unnamed(unnamed) => {
546 let mut out = Vec::new();
547 for (index, field) in unnamed.unnamed.iter().enumerate() {
548 let context = format!("{type_name}.{index}");
549 match field_expr(field, &context, codegen, ctx) {
550 Ok(Some(expr)) => out.push(expr),
551 Ok(None) => {}
552 Err(diagnostic) => diagnostics.push(diagnostic),
553 }
554 }
555 StructShape::Tuple(out)
556 }
557 Fields::Unit => StructShape::Record(Vec::new()),
558 };
559
560 if diagnostics.is_empty() {
561 Ok(shape)
562 } else {
563 Err(diagnostics)
564 }
565}
566
567fn tuple_struct_expr(mut exprs: Vec<CodecExpr>) -> CodecExpr {
571 match exprs.len() {
572 0 => CodecExpr::runtime("unit"),
573 1 => exprs.pop().expect("len checked"),
574 _ => CodecExpr::call(CodecExpr::runtime("tuple"), exprs),
575 }
576}
577
578fn extract_enum_variants(
579 type_name: &str,
580 variants: &syn::punctuated::Punctuated<syn::Variant, syn::token::Comma>,
581 codegen: &CodeGenerator,
582 ctx: &SourceContext,
583) -> Result<Vec<EnumVariant>, Vec<Diagnostic>> {
584 let mut out = Vec::new();
585 let mut diagnostics = Vec::new();
586
587 for variant in variants {
588 let variant_name = variant.ident.to_string();
589 match &variant.fields {
590 Fields::Unit => out.push(EnumVariant::Unit(variant_name)),
591 Fields::Unnamed(unnamed) => {
592 let mut exprs = Vec::new();
593 for (index, field) in unnamed.unnamed.iter().enumerate() {
594 let context = format!("{type_name}::{variant_name}.{index}");
595 match field_expr(field, &context, codegen, ctx) {
596 Ok(Some(expr)) => exprs.push(expr),
597 Ok(None) => {}
598 Err(diagnostic) => diagnostics.push(diagnostic),
599 }
600 }
601 let variant = if unnamed.unnamed.len() == 1 {
602 match exprs.pop() {
605 Some(expr) => EnumVariant::Newtype(variant_name, expr),
606 None => EnumVariant::Unit(variant_name),
607 }
608 } else {
609 EnumVariant::Tuple(variant_name, exprs)
610 };
611 out.push(variant);
612 }
613 Fields::Named(named) => {
614 let mut fields = Vec::new();
615 for field in &named.named {
616 let field_name = field
617 .ident
618 .as_ref()
619 .expect("named fields have idents")
620 .to_string();
621 let context = format!("{type_name}::{variant_name}.{field_name}");
622 match field_expr(field, &context, codegen, ctx) {
623 Ok(Some(expr)) => fields.push((field_name, expr)),
624 Ok(None) => {}
625 Err(diagnostic) => diagnostics.push(diagnostic),
626 }
627 }
628 out.push(EnumVariant::Struct(variant_name, fields));
629 }
630 }
631 }
632
633 if diagnostics.is_empty() {
634 Ok(out)
635 } else {
636 Err(diagnostics)
637 }
638}
639
640fn struct_expr(fields: Vec<(String, CodecExpr)>) -> CodecExpr {
642 CodecExpr::call(
643 CodecExpr::runtime("struct"),
644 [CodecExpr::object(fields)],
645 )
646}
647
648fn enum_expr(variants: Vec<EnumVariant>) -> CodecExpr {
650 let entries = variants.into_iter().map(|variant| match variant {
651 EnumVariant::Unit(name) => (name, CodecExpr::raw("null")),
652 EnumVariant::Newtype(name, expr) => (name, expr),
653 EnumVariant::Tuple(name, exprs) => (name, CodecExpr::array(exprs)),
654 EnumVariant::Struct(name, fields) => (name, CodecExpr::object(fields)),
655 });
656 CodecExpr::call(
657 CodecExpr::runtime("taggedEnum"),
658 [CodecExpr::object(entries)],
659 )
660}
661
662enum TypeItem<'a> {
664 Struct(&'a syn::ItemStruct),
665 Enum(&'a syn::ItemEnum),
666}
667
668impl TypeItem<'_> {
669 fn attrs(&self) -> &[Attribute] {
670 match self {
671 TypeItem::Struct(item) => &item.attrs,
672 TypeItem::Enum(item) => &item.attrs,
673 }
674 }
675
676 fn ident(&self) -> &syn::Ident {
677 match self {
678 TypeItem::Struct(item) => &item.ident,
679 TypeItem::Enum(item) => &item.ident,
680 }
681 }
682}
683
684fn type_items(file: &syn::File) -> Vec<TypeItem<'_>> {
685 file.items
686 .iter()
687 .filter_map(|item| match item {
688 syn::Item::Struct(s) => Some(TypeItem::Struct(s)),
689 syn::Item::Enum(e) => Some(TypeItem::Enum(e)),
690 _ => None,
691 })
692 .collect()
693}
694
695fn parse_source(
696 codegen: &mut CodeGenerator,
697 source: &str,
698 file: Option<PathBuf>,
699) -> Result<(), Error> {
700 let parsed = syn::parse_file(source).map_err(|source| Error::Parse {
701 file: file.clone(),
702 source,
703 })?;
704 let ctx = build_source_context(&parsed, file);
705 let items = type_items(&parsed);
706
707 for item in &items {
710 if !has_marker_derive(item.attrs(), &ctx, codegen) {
711 continue;
712 }
713 let attrs = parse_rkyv_type_attrs(item.attrs());
714 if attrs.remote.is_none() {
715 continue;
716 }
717 let name = item.ident().to_string();
718 let built = match item {
719 TypeItem::Struct(s) => {
720 extract_struct_shape(&name, &s.fields, codegen, &ctx).map(|shape| match shape {
721 StructShape::Record(fields) => struct_expr(fields),
722 StructShape::Tuple(exprs) => tuple_struct_expr(exprs),
723 })
724 }
725 TypeItem::Enum(e) => {
726 extract_enum_variants(&name, &e.variants, codegen, &ctx).map(enum_expr)
727 }
728 };
729 match built {
730 Ok(expr) => {
731 if let Some(fq_path) = ctx.imports.get(&name) {
732 codegen
733 .registry
734 .register_wrapper(fq_path.clone(), WithWrapper::replace(expr.clone()));
735 }
736 codegen
737 .registry
738 .register_wrapper(name, WithWrapper::replace(expr));
739 }
740 Err(diagnostics) => match codegen.on_unknown {
741 OnUnknown::Error => codegen.add_diagnostics.extend(diagnostics),
742 OnUnknown::SkipContainingType => {
743 for diagnostic in diagnostics {
744 eprintln!(
745 "cargo:warning=rkyv-js-codegen: skipping remote proxy `{name}`: \
746 {diagnostic}"
747 );
748 }
749 }
750 },
751 }
752 }
753
754 for item in &items {
756 if !has_marker_derive(item.attrs(), &ctx, codegen) {
757 continue;
758 }
759 let attrs = parse_rkyv_type_attrs(item.attrs());
760 if attrs.remote.is_some() {
761 continue;
762 }
763 let name = item.ident().to_string();
764 let location = Some(ctx.location(item.ident().span()));
765 let extracted = match item {
766 TypeItem::Struct(s) => {
767 extract_struct_shape(&name, &s.fields, codegen, &ctx).map(|shape| match shape {
768 StructShape::Record(fields) => TypeKind::Struct(fields),
769 StructShape::Tuple(exprs) => TypeKind::Alias(tuple_struct_expr(exprs)),
770 })
771 }
772 TypeItem::Enum(e) => {
773 extract_enum_variants(&name, &e.variants, codegen, &ctx).map(TypeKind::Enum)
774 }
775 };
776 match extracted {
777 Ok(kind) => codegen.add_type(name.clone(), kind, location),
778 Err(diagnostics) => codegen.add_failed_type(name.clone(), diagnostics, location),
779 }
780 if let Some(archived) = attrs.archived {
781 codegen.set_archived_name(name, archived);
782 }
783 }
784
785 Ok(())
786}
787
788impl CodeGenerator {
789 pub fn add_source_file(&mut self, path: impl AsRef<Path>) -> Result<&mut Self, Error> {
804 let path = path.as_ref();
805 let source = fs::read_to_string(path)?;
806 parse_source(self, &source, Some(path.to_path_buf()))?;
807 Ok(self)
808 }
809
810 pub fn add_source_str(&mut self, source: &str) -> Result<&mut Self, Error> {
812 parse_source(self, source, None)?;
813 Ok(self)
814 }
815
816 pub fn add_source_dir(&mut self, path: impl AsRef<Path>) -> Result<&mut Self, Error> {
819 let mut files: Vec<PathBuf> = Vec::new();
820 for entry in WalkDir::new(path) {
821 let entry = entry.map_err(io::Error::other)?;
822 let entry_path = entry.path();
823 if entry_path.extension().is_some_and(|ext| ext == "rs") {
824 files.push(entry_path.to_path_buf());
825 }
826 }
827 files.sort();
828 for file in files {
829 self.add_source_file(&file)?;
830 }
831 Ok(self)
832 }
833}
834
835#[cfg(test)]
836mod tests {
837 use super::*;
838 use crate::error::DiagnosticKind;
839 use crate::registry::ExternalType;
840
841 fn generate(source: &str) -> String {
842 let mut codegen = CodeGenerator::new();
843 codegen.add_source_str(source).unwrap();
844 codegen.generate().unwrap()
845 }
846
847 fn generate_diagnostics(source: &str) -> Vec<Diagnostic> {
848 let mut codegen = CodeGenerator::new();
849 codegen.add_source_str(source).unwrap();
850 match codegen.generate() {
851 Err(Error::Codegen(diagnostics)) => diagnostics,
852 other => panic!("expected codegen diagnostics, got {other:?}"),
853 }
854 }
855
856 #[test]
857 fn extracts_simple_struct() {
858 let code = generate(
859 r#"
860 use rkyv::Archive;
861 #[derive(Archive)]
862 struct Point { x: f64, y: f64 }
863 "#,
864 );
865 assert!(code.contains("export const ArchivedPoint = r.struct({\n x: r.f64,\n y: r.f64,\n});"));
866 assert!(code.contains("export type Point = r.Infer<typeof ArchivedPoint>;"));
867 }
868
869 #[test]
870 fn extracts_containers_and_str() {
871 let code = generate(
872 r#"
873 use rkyv::Archive;
874 #[derive(Archive)]
875 struct Person {
876 name: String,
877 nickname: &'static str,
878 age: u32,
879 scores: Vec<u32>,
880 email: Option<String>,
881 boxed: Box<u64>,
882 arr: [u16; 4],
883 tup: (u8, String, f64),
884 unit: (),
885 }
886 "#,
887 );
888 assert!(code.contains("name: r.string,"));
889 assert!(code.contains("nickname: r.string,"));
890 assert!(code.contains("scores: r.vec(r.u32),"));
891 assert!(code.contains("email: r.option(r.string),"));
892 assert!(code.contains("boxed: r.box(r.u64),"));
893 assert!(code.contains("arr: r.array(r.u16, 4),"));
894 assert!(code.contains("tup: r.tuple(r.u8, r.string, r.f64),"));
895 assert!(code.contains("unit: r.unit,"));
896 }
897
898 #[test]
899 fn extracts_enum_with_new_variant_shapes() {
900 let code = generate(
901 r#"
902 use rkyv::Archive;
903 #[derive(Archive)]
904 enum Message {
905 Quit,
906 Move { x: i32, y: i32 },
907 Write(String),
908 ChangeColor(u8, u8, u8),
909 }
910 "#,
911 );
912 assert!(code.contains(
913 "export const ArchivedMessage = r.taggedEnum({\n\
914 \x20 Quit: null,\n\
915 \x20 Move: { x: r.i32, y: r.i32 },\n\
916 \x20 Write: r.string,\n\
917 \x20 ChangeColor: [r.u8, r.u8, r.u8],\n\
918 });"
919 ));
920 }
921
922 #[test]
923 fn extracts_tuple_struct() {
924 let code = generate(
925 r#"
926 use rkyv::Archive;
927 #[derive(Archive)]
928 struct Pair(u32, String);
929 "#,
930 );
931 assert!(code.contains("export const ArchivedPair = r.tuple(r.u32, r.string);"));
932 }
933
934 #[test]
935 fn cross_references_resolve_to_archived_names() {
936 let code = generate(
937 r#"
938 use rkyv::Archive;
939 #[derive(Archive)]
940 struct Inner { value: u32 }
941 #[derive(Archive)]
942 struct Outer { inner: Inner, items: Vec<Inner> }
943 "#,
944 );
945 assert!(code.contains("inner: ArchivedInner,"));
946 assert!(code.contains("items: r.vec(ArchivedInner),"));
947 let inner_pos = code.find("export const ArchivedInner").unwrap();
948 let outer_pos = code.find("export const ArchivedOuter").unwrap();
949 assert!(inner_pos < outer_pos);
950 }
951
952 #[test]
953 fn marker_via_plain_import() {
954 let code = generate(
955 r#"
956 use rkyv::Archive;
957 #[derive(Debug)]
958 struct NotExported { x: i32 }
959 #[derive(Debug, Archive)]
960 struct Exported { y: i32 }
961 "#,
962 );
963 assert!(!code.contains("ArchivedNotExported"));
964 assert!(code.contains("ArchivedExported"));
965 }
966
967 #[test]
968 fn marker_via_qualified_path() {
969 let code = generate(
970 r#"
971 #[derive(rkyv::Archive)]
972 struct QualifiedPath { value: u32 }
973 #[derive(::rkyv::Archive)]
974 struct LeadingColons { value: u32 }
975 "#,
976 );
977 assert!(code.contains("ArchivedQualifiedPath"));
978 assert!(code.contains("ArchivedLeadingColons"));
979 }
980
981 #[test]
982 fn marker_via_rename() {
983 let code = generate(
984 r#"
985 use rkyv::Archive as Rkyv;
986 #[derive(Rkyv)]
987 struct AliasedMarker { value: i32 }
988 "#,
989 );
990 assert!(code.contains("ArchivedAliasedMarker"));
991 }
992
993 #[test]
994 fn marker_via_glob_import() {
995 let code = generate(
996 r#"
997 use rkyv::*;
998 #[derive(Archive)]
999 struct GlobMarked { value: i32 }
1000 "#,
1001 );
1002 assert!(code.contains("ArchivedGlobMarked"));
1003 }
1004
1005 #[test]
1006 fn marker_via_add_marker_path() {
1007 let mut codegen = CodeGenerator::new();
1008 codegen.add_marker_path("my_macros::TS");
1009 codegen
1010 .add_source_str(
1011 r#"
1012 use my_macros::TS;
1013 #[derive(TS)]
1014 struct Custom { value: u8 }
1015 #[derive(my_macros::TS)]
1016 struct CustomQualified { value: u8 }
1017 "#,
1018 )
1019 .unwrap();
1020 let code = codegen.generate().unwrap();
1021 assert!(code.contains("ArchivedCustom"));
1022 assert!(code.contains("ArchivedCustomQualified"));
1023 }
1024
1025 #[test]
1026 fn foreign_archive_paths_do_not_match() {
1027 let mut codegen = CodeGenerator::new();
1029 codegen
1030 .add_source_str(
1031 r#"
1032 #[derive(some_alias::Archive)]
1033 struct NotOurs { id: u64 }
1034 #[derive(deeply::nested::module::Archive)]
1035 struct AlsoNotOurs { data: String }
1036 "#,
1037 )
1038 .unwrap();
1039 let code = codegen.generate().unwrap();
1040 assert!(!code.contains("ArchivedNotOurs"));
1041 assert!(!code.contains("ArchivedAlsoNotOurs"));
1042 }
1043
1044 #[test]
1045 fn bare_marker_without_import_does_not_match() {
1046 let code = generate(
1047 r#"
1048 #[derive(Archive)]
1049 struct NotDetected { a: i32 }
1050 #[derive(Rkyv)]
1051 struct AlsoNotDetected { b: i32 }
1052 "#,
1053 );
1054 assert!(!code.contains("ArchivedNotDetected"));
1055 assert!(!code.contains("ArchivedAlsoNotDetected"));
1056 }
1057
1058 #[test]
1059 fn builtin_leaf_types() {
1060 let code = generate(
1061 r#"
1062 use rkyv::Archive;
1063 use uuid::Uuid;
1064 use bytes::Bytes;
1065 use smol_str::SmolStr;
1066 #[derive(Archive)]
1067 struct Record { id: Uuid, payload: Bytes, key: SmolStr }
1068 "#,
1069 );
1070 assert!(code.contains("import { bytes } from 'rkyv-js/lib/bytes';"));
1071 assert!(code.contains("import { uuid } from 'rkyv-js/lib/uuid';"));
1072 assert!(code.contains("id: uuid,"));
1073 assert!(code.contains("payload: bytes,"));
1074 assert!(code.contains("key: r.string,"));
1075 }
1076
1077 #[test]
1078 fn builtin_vec_like_types() {
1079 let code = generate(
1080 r#"
1081 use rkyv::Archive;
1082 use thin_vec::ThinVec;
1083 use arrayvec::ArrayVec;
1084 use smallvec::SmallVec;
1085 use tinyvec::TinyVec;
1086 use std::collections::VecDeque;
1087 #[derive(Archive)]
1088 struct Data {
1089 thin: ThinVec<u32>,
1090 array_vec: ArrayVec<u8, 64>,
1091 small: SmallVec<[u32; 4]>,
1092 tiny: TinyVec<[String; 8]>,
1093 deque: VecDeque<u32>,
1094 }
1095 "#,
1096 );
1097 assert!(code.contains("thin: r.vec(r.u32),"));
1098 assert!(code.contains("array_vec: r.vec(r.u8),"));
1099 assert!(code.contains("small: r.vec(r.u32),"));
1100 assert!(code.contains("tiny: r.vec(r.string),"));
1101 assert!(code.contains("deque: r.vec(r.u32),"));
1102 }
1103
1104 #[test]
1105 fn builtin_maps_and_sets() {
1106 let code = generate(
1107 r#"
1108 use rkyv::Archive;
1109 use std::collections::{HashMap, HashSet, BTreeMap, BTreeSet};
1110 use indexmap::{IndexMap, IndexSet};
1111 #[derive(Archive)]
1112 struct Collections {
1113 hm: HashMap<String, u32>,
1114 hs: HashSet<String>,
1115 bm: BTreeMap<String, u64>,
1116 bs: BTreeSet<i64>,
1117 im: IndexMap<String, u32>,
1118 is: IndexSet<String>,
1119 }
1120 "#,
1121 );
1122 assert!(code.contains("import { btreeMap, btreeSet } from 'rkyv-js/lib/btreemap';"));
1123 assert!(code.contains("import { hashMap, hashSet } from 'rkyv-js/lib/hashmap';"));
1124 assert!(code.contains("import { indexMap, indexSet } from 'rkyv-js/lib/indexmap';"));
1125 assert!(code.contains("hm: hashMap(r.string, r.u32),"));
1126 assert!(code.contains("hs: hashSet(r.string),"));
1127 assert!(code.contains("bm: btreeMap(r.string, r.u64),"));
1128 assert!(code.contains("bs: btreeSet(r.i64),"));
1129 assert!(code.contains("im: indexMap(r.string, r.u32),"));
1130 assert!(code.contains("is: indexSet(r.string),"));
1131 }
1132
1133 #[test]
1134 fn builtin_pointer_types() {
1135 let code = generate(
1136 r#"
1137 use rkyv::Archive;
1138 use std::rc::{Rc, Weak};
1139 #[derive(Archive)]
1140 struct Shared { data: Rc<String>, weak_ref: Weak<u32>, arc: triomphe::Arc<String> }
1141 "#,
1142 );
1143 assert!(code.contains("data: r.rc(r.string),"));
1144 assert!(code.contains("weak_ref: r.weak(r.u32),"));
1145 assert!(code.contains("arc: r.rc(r.string),"));
1146 }
1147
1148 #[test]
1149 fn renamed_imports_resolve() {
1150 let code = generate(
1151 r#"
1152 use rkyv::Archive;
1153 use std::collections::{HashMap as Map, BTreeSet as SortedSet};
1154 use uuid::Uuid as Id;
1155 #[derive(Archive)]
1156 struct Data { map: Map<String, u32>, set: SortedSet<String>, id: Id }
1157 "#,
1158 );
1159 assert!(code.contains("map: hashMap(r.string, r.u32),"));
1160 assert!(code.contains("set: btreeSet(r.string),"));
1161 assert!(code.contains("id: uuid,"));
1162 }
1163
1164 #[test]
1165 fn generic_type_alias_resolves_to_registry_path() {
1166 let code = generate(
1170 r#"
1171 use rkyv::Archive;
1172 pub type FixedState = std::hash::BuildHasherDefault<Hasher13>;
1173 pub type HashMap<K, V> = std::collections::HashMap<K, V, FixedState>;
1174 #[derive(Archive)]
1175 struct Data { m: HashMap<String, u32> }
1176 "#,
1177 );
1178 assert!(code.contains("m: hashMap(r.string, r.u32),"));
1179 }
1180
1181 #[test]
1182 fn inline_trailing_hasher_is_allowed() {
1183 let code = generate(
1184 r#"
1185 use rkyv::Archive;
1186 pub type State = std::hash::BuildHasherDefault<Hasher13>;
1187 #[derive(Archive)]
1188 struct Data { m: std::collections::HashMap<String, u32, State> }
1189 "#,
1190 );
1191 assert!(code.contains("m: hashMap(r.string, r.u32),"));
1192 }
1193
1194 #[test]
1195 fn generic_arity_too_few_args() {
1196 let diagnostics = generate_diagnostics(
1197 r#"
1198 use rkyv::Archive;
1199 use std::collections::HashMap;
1200 #[derive(Archive)]
1201 struct Data { m: HashMap<String> }
1202 "#,
1203 );
1204 assert_eq!(diagnostics.len(), 1);
1205 assert!(matches!(
1206 &diagnostics[0].kind,
1207 DiagnosticKind::GenericArity { rust_path, expected: 2, found: 1 }
1208 if rust_path == "std::collections::HashMap"
1209 ));
1210 assert_eq!(diagnostics[0].referenced_by.as_deref(), Some("Data.m"));
1211 }
1212
1213 #[test]
1214 fn generic_arity_too_many_args_without_trailing() {
1215 let diagnostics = generate_diagnostics(
1216 r#"
1217 use rkyv::Archive;
1218 #[derive(Archive)]
1219 struct Data { m: std::collections::BTreeMap<String, u32, Extra> }
1220 "#,
1221 );
1222 assert!(matches!(
1223 &diagnostics[0].kind,
1224 DiagnosticKind::GenericArity { rust_path, expected: 2, found: 3 }
1225 if rust_path == "std::collections::BTreeMap"
1226 ));
1227 }
1228
1229 #[test]
1230 fn unknown_type_reports_path_and_suggestion() {
1231 let diagnostics = generate_diagnostics(
1232 r#"
1233 use rkyv::Archive;
1234 #[derive(Archive)]
1235 struct Event { id: my_uuid::Uuid, at: chrono::NaiveDate }
1236 "#,
1237 );
1238 assert_eq!(diagnostics.len(), 2);
1239 assert!(diagnostics.iter().any(|diagnostic| matches!(
1240 &diagnostic.kind,
1241 DiagnosticKind::UnknownType { rust_path, suggestion: Some(suggestion) }
1242 if rust_path == "my_uuid::Uuid" && suggestion == "uuid::Uuid"
1243 )));
1244 assert!(diagnostics.iter().any(|diagnostic| matches!(
1245 &diagnostic.kind,
1246 DiagnosticKind::UnknownType { rust_path, suggestion: None }
1247 if rust_path == "chrono::NaiveDate"
1248 )));
1249 }
1250
1251 #[test]
1252 fn unknown_imported_type_is_not_a_dangling_ref() {
1253 let diagnostics = generate_diagnostics(
1256 r#"
1257 use rkyv::Archive;
1258 use chrono::NaiveDate;
1259 #[derive(Archive)]
1260 struct Event { at: NaiveDate }
1261 "#,
1262 );
1263 assert!(matches!(
1264 &diagnostics[0].kind,
1265 DiagnosticKind::UnknownType { rust_path, .. } if rust_path == "chrono::NaiveDate"
1266 ));
1267 }
1268
1269 #[test]
1270 fn diagnostics_carry_source_locations() {
1271 let diagnostics = generate_diagnostics(
1272 r#"
1273 use rkyv::Archive;
1274 #[derive(Archive)]
1275 struct Event { at: chrono::NaiveDate }
1276 "#,
1277 );
1278 let location = diagnostics[0].location.as_ref().unwrap();
1279 assert_eq!(location.file, None);
1280 assert_eq!(location.line, 4);
1281 assert!(location.column > 1);
1282 }
1283
1284 #[test]
1285 fn undeclared_local_ref_is_unresolved() {
1286 let diagnostics = generate_diagnostics(
1287 r#"
1288 use rkyv::Archive;
1289 #[derive(Archive)]
1290 struct Outer { inner: NeverDeclared }
1291 "#,
1292 );
1293 assert!(matches!(
1294 &diagnostics[0].kind,
1295 DiagnosticKind::UnresolvedTypeRef { name } if name == "NeverDeclared"
1296 ));
1297 assert_eq!(diagnostics[0].referenced_by.as_deref(), Some("Outer.inner"));
1298 }
1299
1300 #[test]
1301 fn duplicate_types_across_sources() {
1302 let mut codegen = CodeGenerator::new();
1303 codegen
1304 .add_source_str("use rkyv::Archive; #[derive(Archive)] struct Point { x: f64 }")
1305 .unwrap();
1306 codegen
1307 .add_source_str("use rkyv::Archive; #[derive(Archive)] struct Point { y: f64 }")
1308 .unwrap();
1309 let Err(Error::Codegen(diagnostics)) = codegen.generate() else {
1310 panic!("expected duplicate diagnostic");
1311 };
1312 assert!(matches!(
1313 &diagnostics[0].kind,
1314 DiagnosticKind::DuplicateType { name } if name == "Point"
1315 ));
1316 }
1317
1318 #[test]
1319 fn parse_errors_propagate() {
1320 let mut codegen = CodeGenerator::new();
1321 let error = codegen.add_source_str("struct {").unwrap_err();
1322 assert!(matches!(error, Error::Parse { file: None, .. }));
1323 }
1324
1325 #[test]
1326 fn skip_mode_omits_unknown_and_dependents() {
1327 let mut codegen = CodeGenerator::new();
1328 codegen.on_unknown_type(OnUnknown::SkipContainingType);
1329 codegen
1330 .add_source_str(
1331 r#"
1332 use rkyv::Archive;
1333 #[derive(Archive)]
1334 struct Fine { x: u32 }
1335 #[derive(Archive)]
1336 struct Broken { at: chrono::NaiveDate }
1337 #[derive(Archive)]
1338 struct UsesBroken { broken: Broken }
1339 #[derive(Archive)]
1340 struct UsesUsesBroken { nested: UsesBroken }
1341 "#,
1342 )
1343 .unwrap();
1344 let code = codegen.generate().unwrap();
1345 assert!(code.contains("ArchivedFine"));
1346 assert!(!code.contains("ArchivedBroken"));
1347 assert!(!code.contains("ArchivedUsesBroken"));
1348 assert!(!code.contains("ArchivedUsesUsesBroken"));
1349 }
1350
1351 #[test]
1352 fn with_asbox_boxes_the_underlying_codec() {
1353 let code = generate(
1354 r#"
1355 use rkyv::Archive;
1356 use rkyv::with::AsBox;
1357 #[derive(Archive)]
1358 struct Data {
1359 #[rkyv(with = AsBox)]
1360 big: String,
1361 }
1362 "#,
1363 );
1364 assert!(code.contains("big: r.box(r.string),"));
1365 }
1366
1367 #[test]
1368 fn with_inline_is_identity() {
1369 let code = generate(
1370 r#"
1371 use rkyv::Archive;
1372 #[derive(Archive)]
1373 struct Data {
1374 #[rkyv(with = rkyv::with::Inline)]
1375 value: u32,
1376 #[rkyv(with = rkyv::with::InlineAsBox)]
1377 other: u64,
1378 }
1379 "#,
1380 );
1381 assert!(code.contains("value: r.u32,"));
1382 assert!(code.contains("other: r.box(r.u64),"));
1383 }
1384
1385 #[test]
1386 fn with_skip_omits_the_field() {
1387 let code = generate(
1388 r#"
1389 use rkyv::Archive;
1390 use rkyv::with::Skip;
1391 #[derive(Archive)]
1392 struct Data {
1393 kept: u32,
1394 #[rkyv(with = Skip)]
1395 dropped: String,
1396 }
1397 "#,
1398 );
1399 assert!(code.contains("kept: r.u32,"));
1400 assert!(!code.contains("dropped"));
1401 }
1402
1403 #[test]
1404 fn with_unknown_wrapper_is_a_diagnostic() {
1405 let diagnostics = generate_diagnostics(
1406 r#"
1407 use rkyv::Archive;
1408 #[derive(Archive)]
1409 struct Data {
1410 #[rkyv(with = Mystery)]
1411 value: u32,
1412 }
1413 "#,
1414 );
1415 assert!(matches!(
1416 &diagnostics[0].kind,
1417 DiagnosticKind::UnknownWithWrapper { wrapper_path } if wrapper_path == "Mystery"
1418 ));
1419 assert_eq!(diagnostics[0].referenced_by.as_deref(), Some("Data.value"));
1420 }
1421
1422 #[test]
1423 fn with_replace_never_resolves_the_field_type() {
1424 let mut codegen = CodeGenerator::new();
1426 codegen.register_with(
1427 "AsJson",
1428 WithWrapper::replace(CodecExpr::import_from("./coord.ts", "Coord")),
1429 );
1430 codegen
1431 .add_source_str(
1432 r#"
1433 use rkyv::Archive;
1434 #[derive(Archive)]
1435 struct RemoteEvent {
1436 name: String,
1437 #[rkyv(with = AsJson)]
1438 location: remote::Coord,
1439 priority: u32,
1440 }
1441 "#,
1442 )
1443 .unwrap();
1444 let code = codegen.generate().unwrap();
1445 assert!(code.contains("import { Coord } from './coord.ts';"));
1446 assert!(code.contains("location: Coord,"));
1447 }
1448
1449 #[test]
1450 fn with_wrapper_resolves_through_glob_imports() {
1451 let code = generate(
1452 r#"
1453 use rkyv::Archive;
1454 use rkyv::with::*;
1455 #[derive(Archive)]
1456 struct Data {
1457 #[rkyv(with = AsBox)]
1458 big: String,
1459 }
1460 "#,
1461 );
1462 assert!(code.contains("big: r.box(r.string),"));
1463 }
1464
1465 #[test]
1466 fn remote_proxy_registers_itself_as_wrapper() {
1467 let code = generate(
1468 r#"
1469 use rkyv::Archive;
1470 #[derive(Archive)]
1471 #[rkyv(remote = chrono::NaiveDate)]
1472 struct NaiveDateDef {
1473 year: i32,
1474 ordinal: u32,
1475 }
1476 #[derive(Archive)]
1477 struct Event {
1478 name: String,
1479 #[rkyv(with = NaiveDateDef)]
1480 date: chrono::NaiveDate,
1481 }
1482 "#,
1483 );
1484 assert!(!code.contains("ArchivedNaiveDateDef"));
1486 assert!(code.contains("date: r.struct({ year: r.i32, ordinal: r.u32 }),"));
1488 }
1489
1490 #[test]
1491 fn remote_proxy_is_order_independent_within_a_file() {
1492 let code = generate(
1493 r#"
1494 use rkyv::Archive;
1495 #[derive(Archive)]
1496 struct Event {
1497 #[rkyv(with = CoordDef)]
1498 location: remote::Coord,
1499 }
1500 #[derive(Archive)]
1501 #[rkyv(remote = remote::Coord)]
1502 struct CoordDef { x: f32, y: f32 }
1503 "#,
1504 );
1505 assert!(code.contains("location: r.struct({ x: r.f32, y: r.f32 }),"));
1506 assert!(!code.contains("ArchivedCoordDef"));
1507 }
1508
1509 #[test]
1510 fn remote_field_without_with_is_unknown() {
1511 let diagnostics = generate_diagnostics(
1514 r#"
1515 use rkyv::Archive;
1516 #[derive(Archive)]
1517 #[rkyv(remote = chrono::NaiveDate)]
1518 struct NaiveDateDef { year: i32, ordinal: u32 }
1519 #[derive(Archive)]
1520 struct Event { date: chrono::NaiveDate }
1521 "#,
1522 );
1523 assert!(matches!(
1524 &diagnostics[0].kind,
1525 DiagnosticKind::UnknownType { rust_path, .. } if rust_path == "chrono::NaiveDate"
1526 ));
1527 }
1528
1529 #[test]
1530 fn archived_rename_attribute() {
1531 let code = generate(
1532 r#"
1533 use rkyv::Archive;
1534 #[derive(Archive)]
1535 #[rkyv(compare(PartialEq), archived = CustomPoint, derive(Debug))]
1536 struct Point { x: f64, y: f64 }
1537 #[derive(Archive)]
1538 struct Line { start: Point, end: Point }
1539 "#,
1540 );
1541 assert!(code.contains("export const CustomPoint = r.struct({"));
1542 assert!(code.contains("export type Point = r.Infer<typeof CustomPoint>;"));
1543 assert!(code.contains("start: CustomPoint,"));
1544 assert!(!code.contains("ArchivedPoint"));
1545 }
1546
1547 #[test]
1548 fn custom_external_type() {
1549 let mut codegen = CodeGenerator::new();
1550 codegen.register_external(
1551 "my_crate::CustomVec",
1552 ExternalType::generic1(|t| {
1553 CodecExpr::call(CodecExpr::import_from("my-package/codecs", "customVec"), [t])
1554 }),
1555 );
1556 codegen
1557 .add_source_str(
1558 r#"
1559 use rkyv::Archive;
1560 use my_crate::CustomVec;
1561 #[derive(Archive)]
1562 struct MyData { custom: CustomVec<u32> }
1563 "#,
1564 )
1565 .unwrap();
1566 let code = codegen.generate().unwrap();
1567 assert!(code.contains("import { customVec } from 'my-package/codecs';"));
1568 assert!(code.contains("custom: customVec(r.u32),"));
1569 }
1570
1571 #[test]
1572 fn unregister_external_removes_builtin() {
1573 let mut codegen = CodeGenerator::new();
1574 codegen.unregister_external("uuid::Uuid");
1575 codegen
1576 .add_source_str(
1577 r#"
1578 use rkyv::Archive;
1579 use uuid::Uuid;
1580 #[derive(Archive)]
1581 struct Record { id: Uuid }
1582 "#,
1583 )
1584 .unwrap();
1585 let Err(Error::Codegen(diagnostics)) = codegen.generate() else {
1586 panic!("expected unknown type diagnostic");
1587 };
1588 assert!(matches!(
1589 &diagnostics[0].kind,
1590 DiagnosticKind::UnknownType { rust_path, .. } if rust_path == "uuid::Uuid"
1591 ));
1592 }
1593}