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