Skip to main content

rorpc_parse/codegen/
zod_ts.rs

1//! Code generation for `#[derive(ZodTs)]`.
2//!
3//! Generates a `fn zod_ts() -> String` method that returns a complete
4//! TypeScript block with a Zod schema and a `z.infer` type alias,
5//! plus an `inventory::submit!` for `SchemaRegistration`.
6
7use proc_macro2::TokenStream;
8use quote::quote;
9use syn::{Data, DeriveInput, Fields};
10
11use crate::{
12    attributes::{ZodAttrs, apply_rename_rule, parse_serde_attrs, parse_zod_attrs},
13    errors::Result,
14    types::{OPTION, VEC, is_primitive, try_extract_wrapper},
15};
16
17// ---------------------------------------------------------------------------
18// Public entry point
19// ---------------------------------------------------------------------------
20
21/// Generate the `#[derive(ZodTs)]` expansion.
22pub fn derive_zod_ts(input: DeriveInput) -> Result<TokenStream> {
23    let name = &input.ident;
24    let name_str = name.to_string();
25
26    match &input.data {
27        Data::Struct(data) => match &data.fields {
28            Fields::Named(fields) => expand_named_struct(name, &name_str, fields, &input),
29            Fields::Unnamed(_) | Fields::Unit => Err(syn::Error::new_spanned(
30                name,
31                "ZodTs: only structs with named fields are supported",
32            )
33            .into()),
34        },
35        Data::Enum(data) => expand_enum(name, &name_str, data, &input),
36        Data::Union(_) => {
37            Err(syn::Error::new_spanned(name, "ZodTs cannot be derived for unions").into())
38        }
39    }
40}
41
42// ---------------------------------------------------------------------------
43// Struct expansion
44// ---------------------------------------------------------------------------
45
46fn expand_named_struct(
47    name: &syn::Ident,
48    name_str: &str,
49    fields: &syn::FieldsNamed,
50    _input: &DeriveInput,
51) -> Result<TokenStream> {
52    let mut field_tokens: Vec<TokenStream> = Vec::new();
53    let mut dep_type_names: Vec<String> = Vec::new();
54
55    for field in &fields.named {
56        let field_name = field.ident.as_ref().unwrap().to_string();
57        let serde = parse_serde_attrs(&field.attrs)?;
58
59        if serde.skip {
60            continue;
61        }
62
63        let ts_key = serde.rename.as_deref().unwrap_or(&field_name);
64        let zod = parse_zod_attrs(&field.attrs)?;
65        let is_opt = is_option_type(&field.ty);
66
67        let base_ty = if is_opt {
68            option_inner(&field.ty).unwrap_or(&field.ty)
69        } else {
70            &field.ty
71        };
72
73        // Collect non-primitive custom types for dependency tracking
74        let custom = innermost_custom_name(base_ty);
75        if let Some(ref c) = custom {
76            // Store only the bare name for the dep list (last segment)
77            let bare = c.rsplit("::").next().unwrap_or(c.as_str());
78            dep_type_names.push(bare.to_string());
79        }
80
81        // Build FieldDef token: either zod_expr (primitive) or type_ref (custom)
82        let field_tok = if let Some(ref type_ref) = custom {
83            // Custom type — emit bare name as type_ref; zod_expr is empty.
84            // The resolution pass will replace type_ref with the correct schema name.
85            let bare_ref = type_ref.rsplit("::").next().unwrap_or(type_ref.as_str());
86            quote! {
87                ::rorpc::FieldDef {
88                    ts_name:   #ts_key,
89                    zod_expr:  "",
90                    type_ref:  #bare_ref,
91                    optional:  #is_opt,
92                }
93            }
94        } else {
95            // Primitive — compute the full Zod expression now.
96            let zod_expr = rust_type_to_zod(base_ty, &zod);
97            // Wrap in .optional() at the expression level so the resolver
98            // doesn't need to know about optionality for primitives.
99            let zod_expr = if is_opt {
100                format!("{}.optional()", zod_expr)
101            } else {
102                zod_expr
103            };
104            quote! {
105                ::rorpc::FieldDef {
106                    ts_name:   #ts_key,
107                    zod_expr:  #zod_expr,
108                    type_ref:  "",
109                    optional:  #is_opt,
110                }
111            }
112        };
113
114        field_tokens.push(field_tok);
115    }
116
117    Ok(emit_registration(
118        name,
119        name_str,
120        field_tokens,
121        &dep_type_names,
122    ))
123}
124
125// ---------------------------------------------------------------------------
126// Enum expansion
127// ---------------------------------------------------------------------------
128
129fn expand_enum(
130    name: &syn::Ident,
131    name_str: &str,
132    data: &syn::DataEnum,
133    input: &DeriveInput,
134) -> Result<TokenStream> {
135    let serde_container = parse_serde_attrs(&input.attrs)?;
136    let rename_all = serde_container.rename_all.as_deref();
137
138    let mut variant_tokens: Vec<TokenStream> = Vec::new();
139
140    for variant in &data.variants {
141        let serde_variant = parse_serde_attrs(&variant.attrs)?;
142        if serde_variant.skip {
143            continue;
144        }
145
146        let raw_name = variant.ident.to_string();
147        let variant_name = serde_variant
148            .rename
149            .as_deref()
150            .map(str::to_string)
151            .unwrap_or_else(|| {
152                rename_all
153                    .map(|rule| apply_rename_rule(rule, &raw_name))
154                    .unwrap_or(raw_name)
155            });
156
157        let kind_tok = generate_variant_def(&variant.fields)?;
158        variant_tokens.push(quote! {
159            ::rorpc::VariantDef {
160                serialized_name: #variant_name,
161                kind: #kind_tok,
162            }
163        });
164    }
165
166    Ok(emit_enum_registration(name, name_str, variant_tokens))
167}
168
169// ---------------------------------------------------------------------------
170// Variant code generation — returns a VariantKind TokenStream
171// ---------------------------------------------------------------------------
172
173fn generate_variant_def(fields: &Fields) -> Result<TokenStream> {
174    match fields {
175        Fields::Unit => Ok(quote! { ::rorpc::VariantKind::Unit }),
176
177        Fields::Unnamed(fields_unnamed) => {
178            let count = fields_unnamed.unnamed.len();
179            if count == 1 {
180                let field = fields_unnamed.unnamed.first().unwrap();
181                let zod = parse_zod_attrs(&field.attrs)?;
182                if let Some(custom) = innermost_custom_name(&field.ty) {
183                    let bare_ref = custom.rsplit("::").next().unwrap_or(custom.as_str());
184                    Ok(quote! { ::rorpc::VariantKind::NewtypeRef { type_ref: #bare_ref } })
185                } else {
186                    let schema = rust_type_to_zod(&field.ty, &zod);
187                    Ok(quote! { ::rorpc::VariantKind::NewtypeZod { zod_expr: #schema } })
188                }
189            } else {
190                // Multi-field tuple variants not yet supported — emit Unknown
191                Ok(quote! { ::rorpc::VariantKind::Unit })
192            }
193        }
194
195        Fields::Named(fields_named) => {
196            let mut field_tokens: Vec<TokenStream> = Vec::new();
197            for field in &fields_named.named {
198                let field_name = field.ident.as_ref().unwrap().to_string();
199                let serde = parse_serde_attrs(&field.attrs)?;
200                if serde.skip {
201                    continue;
202                }
203                let ts_key = serde.rename.as_deref().unwrap_or(&field_name);
204                let zod_attrs = parse_zod_attrs(&field.attrs)?;
205                let is_opt = is_option_type(&field.ty);
206                let base_ty = if is_opt {
207                    option_inner(&field.ty).unwrap_or(&field.ty)
208                } else {
209                    &field.ty
210                };
211
212                let field_tok = if let Some(custom) = innermost_custom_name(base_ty) {
213                    let bare_ref = custom.rsplit("::").next().unwrap_or(custom.as_str());
214                    quote! {
215                        ::rorpc::FieldDef {
216                            ts_name:  #ts_key,
217                            zod_expr: "",
218                            type_ref: #bare_ref,
219                            optional: #is_opt,
220                        }
221                    }
222                } else {
223                    let zod_expr = rust_type_to_zod(base_ty, &zod_attrs);
224                    let zod_expr = if is_opt {
225                        format!("{}.optional()", zod_expr)
226                    } else {
227                        zod_expr
228                    };
229                    quote! {
230                        ::rorpc::FieldDef {
231                            ts_name:  #ts_key,
232                            zod_expr: #zod_expr,
233                            type_ref: "",
234                            optional: #is_opt,
235                        }
236                    }
237                };
238                field_tokens.push(field_tok);
239            }
240            Ok(quote! {
241                ::rorpc::VariantKind::Struct {
242                    fields: &[ #(#field_tokens),* ]
243                }
244            })
245        }
246    }
247}
248
249// ---------------------------------------------------------------------------
250// inventory::submit! emission
251// ---------------------------------------------------------------------------
252
253/// Emit the `inventory::submit!` block for a type.
254///
255/// `items`    — `FieldDef` tokens for structs, `VariantDef` tokens for enums.
256/// `is_enum`  — selects `SchemaDef::Enum` vs `SchemaDef::Object`.
257fn emit_registration(
258    name: &syn::Ident,
259    name_str: &str,
260    items: Vec<TokenStream>,
261    dep_type_names: &[String],
262) -> TokenStream {
263    let dep_strs: Vec<&str> = dep_type_names.iter().map(String::as_str).collect();
264
265    quote! {
266        impl #name {
267            pub fn dependent_types() -> Vec<&'static str> {
268                vec![#(#dep_strs),*]
269            }
270        }
271
272        const _: () = {
273            static __FIELDS: &[::rorpc::FieldDef] = &[ #(#items),* ];
274            ::rorpc::inventory::submit! {
275                ::rorpc::SchemaRegistration {
276                    type_name: #name_str,
277                    module_path: concat!(module_path!(), "::", #name_str),
278                    schema_def: ::rorpc::SchemaDef::Object { fields: __FIELDS },
279                    dependent_types: #name::dependent_types,
280                }
281            }
282        };
283    }
284}
285
286/// Emit the `inventory::submit!` block for an enum type.
287fn emit_enum_registration(
288    name: &syn::Ident,
289    name_str: &str,
290    items: Vec<TokenStream>,
291) -> TokenStream {
292    quote! {
293        impl #name {
294            pub fn dependent_types() -> Vec<&'static str> {
295                vec![]
296            }
297        }
298
299        const _: () = {
300            static __VARIANTS: &[::rorpc::VariantDef] = &[ #(#items),* ];
301            ::rorpc::inventory::submit! {
302                ::rorpc::SchemaRegistration {
303                    type_name: #name_str,
304                    module_path: concat!(module_path!(), "::", #name_str),
305                    schema_def: ::rorpc::SchemaDef::Enum { variants: __VARIANTS },
306                    dependent_types: #name::dependent_types,
307                }
308            }
309        };
310    }
311}
312
313// ---------------------------------------------------------------------------
314// Type → Zod expression
315// ---------------------------------------------------------------------------
316
317/// Map a `syn::Type` to a Zod schema expression string.
318///
319/// Uses AST-based wrapper detection for `Option<T>` and `Vec<T>` —
320/// never string prefix matching.
321pub fn rust_type_to_zod(ty: &syn::Type, attrs: &ZodAttrs) -> String {
322    // Option<T> — recurse on inner, then .optional()
323    if is_option_type(ty)
324        && let Some(inner) = option_inner(ty)
325    {
326        let inner_schema = rust_type_to_zod(inner, &ZodAttrs::default());
327        return format!("{}.optional()", inner_schema);
328    }
329
330    // Vec<T>
331    if let Some(m) = try_extract_wrapper(ty, VEC)
332        && let Some(inner) = m.first_type()
333    {
334        let inner_schema = rust_type_to_zod(inner, &ZodAttrs::default());
335        let mut chain = format!("z.array({})", inner_schema);
336        if let Some(n) = attrs.length {
337            chain.push_str(&format!(".length({})", n));
338        }
339        if let Some(n) = attrs.min_length {
340            chain.push_str(&format!(".min({})", n));
341        }
342        if let Some(n) = attrs.max_length {
343            chain.push_str(&format!(".max({})", n));
344        }
345        return chain;
346    }
347
348    // Primitives — match on the final path segment ident
349    if let syn::Type::Path(type_path) = ty
350        && let Some(seg) = type_path.path.segments.last()
351    {
352        let name = seg.ident.to_string();
353        return match name.as_str() {
354            "String" | "str" => build_string_schema(attrs),
355            "i8" | "i16" | "i32" | "i64" | "i128" | "isize" | "u8" | "u16" | "u32" | "u64"
356            | "u128" | "usize" => build_integer_schema(attrs),
357            "f32" | "f64" => build_float_schema(attrs),
358            "bool" => "z.boolean()".to_string(),
359            // uuid::Uuid → z.uuid()
360            "Uuid" => "z.uuid()".to_string(),
361            // chrono::DateTime<Utc> → z.iso.datetime()
362            "DateTime" => "z.iso.datetime({ offset: true })".to_string(),
363            // serde_json::Value → z.record(z.string(), z.unknown())
364            "Value" => "z.record(z.string(), z.unknown())".to_string(),
365            // Custom type — reference its schema by name
366            other => format!("{}Schema", other),
367        };
368    }
369
370    // Unit type ()
371    if let syn::Type::Tuple(t) = ty
372        && t.elems.is_empty()
373    {
374        return "z.void()".to_string();
375    }
376
377    "z.unknown()".to_string()
378}
379
380// ---------------------------------------------------------------------------
381// Schema builders
382// ---------------------------------------------------------------------------
383
384fn build_string_schema(attrs: &ZodAttrs) -> String {
385    let mut chain = String::from("z.string()");
386    if let Some(n) = attrs.length {
387        chain.push_str(&format!(".length({})", n));
388    }
389    if let Some(n) = attrs.min_length {
390        chain.push_str(&format!(".min({})", n));
391    }
392    if let Some(n) = attrs.max_length {
393        chain.push_str(&format!(".max({})", n));
394    }
395    if attrs.email {
396        chain.push_str(".email()");
397    }
398    if attrs.url {
399        chain.push_str(".url()");
400    }
401    if let Some(ref p) = attrs.regex {
402        chain.push_str(&format!(".regex(/{}/)", p));
403    }
404    if let Some(ref p) = attrs.starts_with {
405        chain.push_str(&format!(".startsWith(\"{}\")", p));
406    }
407    if let Some(ref p) = attrs.ends_with {
408        chain.push_str(&format!(".endsWith(\"{}\")", p));
409    }
410    if let Some(ref p) = attrs.includes {
411        chain.push_str(&format!(".includes(\"{}\")", p));
412    }
413    chain
414}
415
416fn build_integer_schema(attrs: &ZodAttrs) -> String {
417    let mut chain = String::from("z.number().int()");
418    append_number_validators(&mut chain, attrs);
419    chain
420}
421
422fn build_float_schema(attrs: &ZodAttrs) -> String {
423    let mut chain = String::from("z.number()");
424    if attrs.int {
425        chain.push_str(".int()");
426    }
427    append_number_validators(&mut chain, attrs);
428    chain
429}
430
431fn append_number_validators(chain: &mut String, attrs: &ZodAttrs) {
432    if let Some(n) = attrs.min {
433        chain.push_str(&format!(".min({})", n));
434    }
435    if let Some(n) = attrs.max {
436        chain.push_str(&format!(".max({})", n));
437    }
438    if attrs.positive {
439        chain.push_str(".positive()");
440    }
441    if attrs.negative {
442        chain.push_str(".negative()");
443    }
444    if attrs.nonnegative {
445        chain.push_str(".nonnegative()");
446    }
447    if attrs.nonpositive {
448        chain.push_str(".nonpositive()");
449    }
450    if attrs.finite {
451        chain.push_str(".finite()");
452    }
453}
454
455// ---------------------------------------------------------------------------
456// Type helpers — all AST-based, no string matching on type names
457// ---------------------------------------------------------------------------
458
459fn is_option_type(ty: &syn::Type) -> bool {
460    try_extract_wrapper(ty, OPTION).is_some()
461}
462
463fn option_inner(ty: &syn::Type) -> Option<&syn::Type> {
464    try_extract_wrapper(ty, OPTION)?.first_type()
465}
466
467/// Return the simple name of the innermost non-primitive, non-wrapper type,
468/// for dependency tracking in `dependent_types()`.
469fn innermost_custom_name(ty: &syn::Type) -> Option<String> {
470    // Strip Vec<T>
471    if let Some(m) = try_extract_wrapper(ty, VEC) {
472        return m.first_type().and_then(innermost_custom_name);
473    }
474    if is_primitive(ty) {
475        return None;
476    }
477    if let syn::Type::Path(tp) = ty {
478        // Extract full path by joining all segments
479        let segments: Vec<String> = tp
480            .path
481            .segments
482            .iter()
483            .map(|seg| seg.ident.to_string())
484            .collect();
485
486        if segments.is_empty() {
487            return None;
488        }
489
490        let last = segments.last().unwrap();
491        // Exclude Value (serde_json) from dependency tracking
492        if last == "Value" {
493            return None;
494        }
495
496        // Return full path joined with ::
497        return Some(segments.join("::"));
498    }
499    None
500}
501
502// ---------------------------------------------------------------------------
503// Runtime type-to-zod conversion (for contract generation)
504// ---------------------------------------------------------------------------
505
506/// Convert a Rust type name string to a TypeScript Zod schema reference.
507///
508/// This is for runtime contract generation when you have type names as strings
509/// from handler metadata, not `syn::Type` ASTs. For compile-time AST-based
510/// conversion, use [`rust_type_to_zod`] instead.
511///
512/// # String-based parsing
513///
514/// This function uses string prefix/suffix matching because it operates on
515/// type name strings collected at link time via `inventory`. It handles:
516///
517/// - Wrapper unwrapping: `"Json<Planet>"` → `"PlanetSchema"`
518/// - Result unwrapping: `"Result<Json<Planet>, E>"` → `"PlanetSchema"`
519/// - Vec mapping: `"Json<Vec<Planet>>"` → `"z.array(PlanetSchema)"`
520/// - Primitive mapping: `"String"` → `"z.string()"`
521/// - SSE streams: `"Sse<...>"` → `"asyncIteratorObject(z.unknown())"`
522///
523/// # Examples
524///
525/// ```
526/// use rorpc_parse::codegen::rust_type_to_ts_schema;
527///
528/// assert_eq!(rust_type_to_ts_schema("Json<Planet>"), "PlanetSchema");
529/// assert_eq!(rust_type_to_ts_schema("Json<Vec<Planet>>"), "z.array(PlanetSchema)");
530/// assert_eq!(rust_type_to_ts_schema("Result<Json<Planet>, E>"), "PlanetSchema");
531/// assert_eq!(rust_type_to_ts_schema("String"), "z.string()");
532/// assert_eq!(rust_type_to_ts_schema("()"), "z.void()");
533/// ```
534pub fn rust_type_to_ts_schema(raw: &str) -> String {
535    let raw = raw.replace(' ', "");
536
537    if raw.starts_with("Sse<") {
538        return "asyncIteratorObject(z.unknown() /* TODO: add #[derive(ZodTs)] to your stream event type */)".to_string();
539    }
540
541    // Unwrap Result<T, E> → T
542    let inner = if raw.starts_with("Result<") {
543        extract_first_generic_arg_string(&raw).unwrap_or(raw.clone())
544    } else {
545        raw.clone()
546    };
547
548    // Unwrap Json<T> → T
549    let inner = if inner.starts_with("Json<") && inner.ends_with('>') {
550        inner[5..inner.len() - 1].to_string()
551    } else {
552        inner
553    };
554
555    type_name_to_zod_ref(&inner)
556}
557
558/// Map a bare type name to its Zod schema reference.
559fn type_name_to_zod_ref(type_name: &str) -> String {
560    match type_name {
561        "()" => "z.void()".to_string(),
562        "" => String::new(),
563        "String" | "str" => "z.string()".to_string(),
564        "bool" => "z.boolean()".to_string(),
565        "i8" | "i16" | "i32" | "i64" | "i128" | "isize" | "u8" | "u16" | "u32" | "u64" | "u128"
566        | "usize" => "z.number().int()".to_string(),
567        "f32" | "f64" => "z.number()".to_string(),
568        "Uuid" => "z.uuid()".to_string(),
569        "DateTime" => "z.iso.datetime({ offset: true })".to_string(),
570        "serde_json::Value" | "Value" => "z.record(z.string(), z.unknown())".to_string(),
571        _ if type_name.starts_with("Vec<") && type_name.ends_with('>') => {
572            let inner = &type_name[4..type_name.len() - 1];
573            format!("z.array({})", type_name_to_zod_ref(inner))
574        }
575        _ if type_name.starts_with("Option<") && type_name.ends_with('>') => {
576            let inner = &type_name[7..type_name.len() - 1];
577            format!("{}.optional()", type_name_to_zod_ref(inner))
578        }
579        _ => {
580            let base = type_name.rsplit("::").next().unwrap_or(type_name);
581            format!("{}Schema", base)
582        }
583    }
584}
585
586/// Extract the first generic argument from a type string.
587///
588/// `"Result<Json<Planet>, E>"` → `Some("Json<Planet>")`
589fn extract_first_generic_arg_string(type_str: &str) -> Option<String> {
590    let start = type_str.find('<')? + 1;
591    let mut depth = 0;
592    let mut end = start;
593
594    for (i, ch) in type_str[start..].char_indices() {
595        match ch {
596            '<' => depth += 1,
597            '>' if depth == 0 => {
598                end = start + i;
599                break;
600            }
601            '>' => depth -= 1,
602            ',' if depth == 0 => {
603                end = start + i;
604                break;
605            }
606            _ => {}
607        }
608    }
609
610    if end > start {
611        Some(type_str[start..end].to_string())
612    } else {
613        None
614    }
615}
616
617/// Convert type name to schema constant name: `"Planet"` → `"PlanetSchema"`
618pub fn to_schema_name(rust_type: &str) -> String {
619    format!("{}Schema", base_type_name(rust_type))
620}
621
622/// Extract the base type name, stripping all wrappers.
623///
624/// `"Result<Json<Vec<Planet>>, E>"` → `"Planet"`
625pub fn base_type_name(rust_type: &str) -> String {
626    let mut base = rust_type.trim();
627
628    if base.starts_with("Result<")
629        && let Some(inner) = extract_first_generic_arg_string(base)
630    {
631        base = Box::leak(inner.into_boxed_str());
632    }
633    if base.starts_with("Json<") && base.ends_with('>') {
634        base = &base[5..base.len() - 1];
635    }
636    if base.starts_with("Vec<") && base.ends_with('>') {
637        base = &base[4..base.len() - 1];
638    }
639    if base.starts_with("Option<") && base.ends_with('>') {
640        base = &base[7..base.len() - 1];
641    }
642
643    base.rsplit("::").next().unwrap_or(base).to_string()
644}
645
646#[cfg(test)]
647mod runtime_conversion_tests {
648    use super::*;
649
650    #[test]
651    fn json_planet() {
652        assert_eq!(rust_type_to_ts_schema("Json<Planet>"), "PlanetSchema");
653    }
654
655    #[test]
656    fn json_vec_planet() {
657        assert_eq!(
658            rust_type_to_ts_schema("Json<Vec<Planet>>"),
659            "z.array(PlanetSchema)"
660        );
661    }
662
663    #[test]
664    fn result_json_planet() {
665        assert_eq!(
666            rust_type_to_ts_schema("Result<Json<Planet>, StatusCode>"),
667            "PlanetSchema"
668        );
669    }
670
671    #[test]
672    fn json_string() {
673        assert_eq!(rust_type_to_ts_schema("Json<String>"), "z.string()");
674    }
675
676    #[test]
677    fn unit_type() {
678        assert_eq!(rust_type_to_ts_schema("()"), "z.void()");
679    }
680
681    #[test]
682    fn serde_json_value() {
683        assert_eq!(
684            rust_type_to_ts_schema("Json<serde_json::Value>"),
685            "z.record(z.string(), z.unknown())"
686        );
687    }
688
689    #[test]
690    fn schema_name_simple() {
691        assert_eq!(to_schema_name("Planet"), "PlanetSchema");
692    }
693
694    #[test]
695    fn schema_name_vec() {
696        assert_eq!(to_schema_name("Vec<Planet>"), "PlanetSchema");
697    }
698
699    #[test]
700    fn base_type_unwraps_wrappers() {
701        assert_eq!(base_type_name("Result<Json<Vec<Planet>>, E>"), "Planet");
702        assert_eq!(base_type_name("Json<Planet>"), "Planet");
703        assert_eq!(base_type_name("Vec<Planet>"), "Planet");
704        assert_eq!(base_type_name("Option<Planet>"), "Planet");
705    }
706
707    #[test]
708    fn base_type_strips_module_path() {
709        assert_eq!(base_type_name("models::Planet"), "Planet");
710        assert_eq!(base_type_name("crate::domain::Planet"), "Planet");
711    }
712}