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