Something went wrong. Try again.
atproto made easy crates.io/crates/jacquard
atproto rust
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242use crate::error::Result;use crate::lexicon::{ LexArrayItem, LexInteger, LexObject, LexObjectProperty, LexRecord, LexString,};use heck::ToSnakeCase;use jacquard_common::deps::smol_str::SmolStr;use proc_macro2::TokenStream;use quote::quote;use std::collections::BTreeMap;
use super::CodeGenerator;use super::prettify::GeneratedCode;use super::utils::{known_value_to_variant_name, make_ident, value_to_variant_name};
/// Enum variant kind for IntoStatic generation#[derive(Debug, Clone)]#[allow(dead_code)]pub(super) enum EnumVariantKind { Unit, Tuple, Struct(Vec<String>),}
impl<'c> CodeGenerator<'c> { /// Generate all nested type definitions (unions, objects) for an object's properties. /// This consolidates the pattern of iterating properties to find unions and nested objects /// that need their own type definitions. /// /// # Parameters /// - `include_nested_objects`: If false, skips generating nested object types (used by XRPC) pub(super) fn generate_nested_types( &self, nsid: &str, parent_type_name: &str, properties: &BTreeMap<SmolStr, LexObjectProperty<'static>>, include_nested_objects: bool, resolved: &super::prettify::ResolvedImports, ) -> Result<Vec<GeneratedCode>> { let mut nested = Vec::new();
for (field_name, field_type) in properties { match field_type { LexObjectProperty::Union(union) => { // Skip empty, single-variant unions unless they're self-referential. if !union.refs.is_empty() && (union.refs.len() > 1 || self.is_self_referential_union(nsid, parent_type_name, &union)) { let union_name = self.generate_field_type_name(nsid, parent_type_name, field_name, ""); let refs: Vec<_> = union.refs.iter().cloned().collect(); nested.push(self.generate_union( nsid, &union_name, &refs, None, union.closed, resolved, )?); } } LexObjectProperty::Object(nested_obj) if include_nested_objects => { let object_name = self.generate_field_type_name(nsid, parent_type_name, field_name, ""); nested.push(self.generate_object(nsid, &object_name, &nested_obj, resolved)?); } LexObjectProperty::Array(array) => { if let LexArrayItem::Union(union) = &array.items { // Skip single-variant array unions. if union.refs.len() > 1 { let union_name = self.generate_field_type_name( nsid, parent_type_name, field_name, "Item", ); let refs: Vec<_> = union.refs.iter().cloned().collect(); nested.push(self.generate_union( nsid, &union_name, &refs, None, union.closed, resolved, )?); } } } LexObjectProperty::String(s) if s.known_values.is_some() => { let enum_name = self.generate_field_type_name(nsid, parent_type_name, field_name, ""); nested.push(self.generate_inline_known_values_enum(&enum_name, s, resolved)?); } _ => {} } }
Ok(nested) }
pub(super) fn generate_record( &self, nsid: &str, def_name: &str, record: &LexRecord<'static>, resolved: &super::prettify::ResolvedImports, ) -> Result<GeneratedCode> { match &record.record { crate::lexicon::LexRecordRecord::Object(obj) => { let type_name = self.def_to_type_name(nsid, def_name); let ident = syn::Ident::new(&type_name, proc_macro2::Span::call_site());
// Records always get a type param since they have extra_data: BTreeMap<..., Data<S>>. // Skip custom builder for types that conflict with the macro's unqualified type references. let has_builder = !super::builder_heuristics::conflicts_with_builder_macro(&type_name);
// Generate main struct fields. let (fields, default_fns) = self.generate_object_fields(nsid, &type_name, obj, has_builder, resolved)?; let doc = self.generate_doc_comment(record.description.as_ref()); let manual_default = self.generate_manual_default(&type_name, obj, resolved);
let derive_attr = resolved.derive_standard(); let bosstr_path = resolved.external_type_tokens(&super::prettify::ExternalImport::BosStr); let default_str_path = resolved.external_type_tokens(&super::prettify::ExternalImport::DefaultStr); let bosstr_serde = resolved.serde_external_path(&super::prettify::ExternalImport::BosStr); let de_serde = resolved.serde_external_path(&super::prettify::ExternalImport::Deserialize); let serde_de_bound = format!("S: {}<'de> + {}", de_serde, bosstr_serde);
// Generate the extra_data field directly instead of using #[lexicon] attribute. let smolstr_type = resolved.type_tokens(&super::prettify::CommonType::SmolStr); let data_type = resolved.type_tokens(&super::prettify::CommonType::Data); let btree_map = resolved.btree_map_path(); let is_none_path = resolved.option_is_none_path(); let extra_data_type = resolved.option_type(quote! { #btree_map<#smolstr_type, #data_type> }); let struct_def = quote! { #doc #derive_attr #[serde(rename_all = "camelCase", rename = #nsid, tag = "$type", bound( deserialize = #serde_de_bound ))] pub struct #ident<S: #bosstr_path = #default_str_path> { #fields #[serde(flatten, default, skip_serializing_if = #is_none_path)] pub extra_data: #extra_data_type, } };
// Generate custom builder if needed. let builder = if has_builder { let ctx = super::builder_gen::BuilderGenContext::from_object( self, nsid, &type_name, obj, true, // records always have type param resolved, ); ctx.generate() } else { quote! {} };
// Generate union types and nested object types for this record. let unions = self.generate_nested_types(nsid, &type_name, &obj.properties, true, resolved)?;
// Generate typed GetRecordOutput wrapper. let output_type_name = format!("{}GetRecordOutput", type_name); let output_type_ident = syn::Ident::new(&output_type_name, proc_macro2::Span::call_site());
let is_none_path = resolved.option_is_none_path(); let cid_type = resolved.type_tokens(&super::prettify::CommonType::Cid); let at_uri_type = resolved.type_tokens(&super::prettify::CommonType::AtUri); let option_cid = resolved.option_type(cid_type); let output_wrapper = quote! { /// Typed wrapper for GetRecord response with this collection's record type. #derive_attr #[serde(rename_all = "camelCase")] pub struct #output_type_ident<S: #bosstr_path = #default_str_path> { #[serde(skip_serializing_if = #is_none_path)] pub cid: #option_cid, pub uri: #at_uri_type, pub value: #ident<S>, } };
// Generate marker struct for XrpcResp. let record_marker_name = format!("{}Record", type_name); let record_marker_ident = syn::Ident::new(&record_marker_name, proc_macro2::Span::call_site());
let ser_path = resolved.external_type_tokens(&super::prettify::ExternalImport::Serialize); let de_path = resolved.external_type_tokens(&super::prettify::ExternalImport::Deserialize); let xrpc_resp_path = resolved.external_type_tokens(&super::prettify::ExternalImport::XrpcResp); let record_error_type = resolved.type_tokens(&super::prettify::CommonType::RecordError); let record_marker = quote! { /// Marker type for deserializing records from this collection. #[derive(Debug, #ser_path, #de_path)] pub struct #record_marker_ident;
impl #xrpc_resp_path for #record_marker_ident { const NSID: &'static str = #nsid; const ENCODING: &'static str = "application/json"; type Output<S: #bosstr_path> = #output_type_ident<S>; type Err = #record_error_type; } }; let from_impl = quote! { impl<S: #bosstr_path> From<#output_type_ident<S>> for #ident<S> { fn from(output: #output_type_ident<S>) -> Self { output.value } } };
// Generate Collection trait impl. let collection_path = resolved.type_path(&super::prettify::CommonType::Collection); let collection_impl = quote! { impl<S: #bosstr_path> #collection_path for #ident<S> { const NSID: &'static str = #nsid; type Record = #record_marker_ident; } };
// Generate collection impl for the marker struct to drive fetch_record(). let collection_marker_impl = quote! { impl #collection_path for #record_marker_ident { const NSID: &'static str = #nsid; type Record = #record_marker_ident; } };
// Generate LexiconSchema impl with shared lexicon_doc function let (shared_fn, schema_impl) = self.generate_schema_impl_with_shared(&type_name, nsid, "main", true, resolved);
// Merge nested type buckets into parent buckets. let mut nested_type_defs = TokenStream::new(); let mut nested_internals = TokenStream::new(); for nested in unions { nested_type_defs.extend(nested.type_defs); nested_internals.extend(nested.inherent_impls); nested_internals.extend(nested.trait_impls); nested_internals.extend(nested.internals); }
// Categorize tokens into buckets. let type_defs = quote! { #struct_def #nested_type_defs #output_wrapper };
let at_uri_path = resolved.type_path(&super::prettify::CommonType::AtUri); let record_uri_path = resolved.external_type_tokens(&super::prettify::ExternalImport::RecordUri); let uri_error_path = resolved.external_type_tokens(&super::prettify::ExternalImport::UriError); let inherent_impls = quote! { impl<S: #bosstr_path> #ident<S> { pub fn uri(uri: S) -> Result<#record_uri_path<S, #record_marker_ident>, #uri_error_path> { #record_uri_path::try_from_uri(#at_uri_path::new(uri)?) } } };
let trait_impls = quote! { #record_marker #from_impl #collection_impl #collection_marker_impl #schema_impl };
let internals = quote! { #(#default_fns)* #manual_default #nested_internals #builder #shared_fn };
Ok(GeneratedCode { type_defs, inherent_impls, trait_impls, internals, imports: Default::default(), }) } } }
/// Generate an object type pub(super) fn generate_object( &self, nsid: &str, def_name: &str, obj: &LexObject<'static>, resolved: &super::prettify::ResolvedImports, ) -> Result<GeneratedCode> { let type_name = self.def_to_type_name(nsid, def_name); let ident = syn::Ident::new(&type_name, proc_macro2::Span::call_site());
// Objects always get a type param since they have extra_data: BTreeMap<..., Data<S>>.
// Smart heuristics for builder generation: // - 0 required fields: Default instead of builder // - All required fields are bare strings: Default instead of builder // - 1+ required fields (not all strings): custom builder (but not if name conflicts) let decision = super::builder_heuristics::should_generate_builder(&type_name, obj); let has_builder = decision.has_builder;
let (fields, default_fns) = self.generate_object_fields(nsid, &type_name, obj, has_builder, resolved)?; let doc = self.generate_doc_comment(obj.description.as_ref());
// Determine Default strategy: // 1. Manual impl if schema defaults cover all required fields. // 2. derive(Default) if heuristic says so (0 required, or all-string required). // 3. No Default otherwise. let manual_default = self.generate_manual_default(&type_name, obj, resolved); let use_derive_default = manual_default.is_none() && decision.has_default;
let bosstr_path = resolved.external_type_tokens(&super::prettify::ExternalImport::BosStr); let default_str_path = resolved.external_type_tokens(&super::prettify::ExternalImport::DefaultStr); let bosstr_serde = resolved.serde_external_path(&super::prettify::ExternalImport::BosStr); let de_serde = resolved.serde_external_path(&super::prettify::ExternalImport::Deserialize); let serde_de_bound = format!("S: {}<'de> + {}", de_serde, bosstr_serde); let derive_attr = if use_derive_default { resolved.derive_standard_with(quote! { Default }) } else { resolved.derive_standard() };
// Generate the extra_data field directly instead of using #[lexicon] attribute. let smolstr_type = resolved.type_tokens(&super::prettify::CommonType::SmolStr); let data_type = resolved.type_tokens(&super::prettify::CommonType::Data); let btree_map = resolved.btree_map_path(); let is_none_path = resolved.option_is_none_path(); let extra_data_type = resolved.option_type(quote! { #btree_map<#smolstr_type, #data_type> }); let struct_def = quote! { #doc #derive_attr #[serde(rename_all = "camelCase", bound(deserialize = #serde_de_bound))] pub struct #ident<S: #bosstr_path = #default_str_path> { #fields #[serde(flatten, default, skip_serializing_if = #is_none_path)] pub extra_data: #extra_data_type, } };
// Generate custom builder if needed. let builder = if has_builder { let ctx = super::builder_gen::BuilderGenContext::from_object( self, nsid, &type_name, obj, true, // objects always have type param resolved, ); ctx.generate() } else { quote! {} };
// Generate union types and nested object types for this object. let nested_items = self.generate_nested_types(nsid, &type_name, &obj.properties, true, resolved)?;
// Merge nested type buckets into parent buckets. let mut nested_type_defs = TokenStream::new(); let mut nested_internals = TokenStream::new(); for nested in nested_items { nested_type_defs.extend(nested.type_defs); nested_internals.extend(nested.inherent_impls); nested_internals.extend(nested.trait_impls); nested_internals.extend(nested.internals); }
// Generate LexiconSchema impl with shared lexicon_doc function. let (shared_fn, schema_impl) = self.generate_schema_impl_with_shared(&type_name, nsid, def_name, true, resolved);
// Categorize tokens into buckets. let type_defs = quote! { #struct_def #nested_type_defs };
let trait_impls = quote! { #schema_impl };
let internals = quote! { #(#default_fns)* #manual_default #nested_internals #builder #shared_fn };
Ok(GeneratedCode { type_defs, inherent_impls: TokenStream::new(), trait_impls, internals, imports: Default::default(), }) }
/// Generate fields for an object. /// Returns (field tokens, companion default functions). pub(super) fn generate_object_fields( &self, nsid: &str, parent_type_name: &str, obj: &LexObject<'static>, _is_builder: bool, resolved: &super::prettify::ResolvedImports, ) -> Result<(TokenStream, Vec<TokenStream>)> { let required = obj.required.as_ref().map(|r| r.as_slice()).unwrap_or(&[]); let nullable = obj.nullable.as_ref().map(|n| n.as_slice()).unwrap_or(&[]);
let mut fields = Vec::new(); let mut default_fns = Vec::new(); for (field_name, field_type) in &obj.properties { let is_required = required.contains(field_name); let is_nullable = nullable.contains(field_name); let (field_tokens, default_fn) = self.generate_field( nsid, parent_type_name, field_name, field_type, is_required, is_nullable, resolved, )?; fields.push(field_tokens); if let Some(f) = default_fn { default_fns.push(f); } }
Ok((quote! { #(#fields)* }, default_fns)) }
/// Generate a single field. /// Returns (field tokens, optional companion default function). pub(super) fn generate_field( &self, nsid: &str, parent_type_name: &str, field_name: &str, field_type: &LexObjectProperty<'static>, is_required: bool, is_nullable: bool, resolved: &super::prettify::ResolvedImports, ) -> Result<(TokenStream, Option<TokenStream>)> { if field_name.is_empty() { eprintln!( "Warning: Empty field name in lexicon '{}' type '{}', using 'unknown' as fallback", nsid, parent_type_name ); } let field_ident = make_ident(&field_name.to_snake_case());
let rust_type = self.property_to_rust_type(nsid, parent_type_name, field_name, field_type, resolved)?;
let is_optional = !is_required || is_nullable; let rust_type = if !is_optional { rust_type } else { resolved.option_type(rust_type) };
// Extract description from field type. let description = match field_type { LexObjectProperty::Ref(r) => r.description.as_ref(), LexObjectProperty::Union(u) => u.description.as_ref(), LexObjectProperty::Bytes(b) => b.description.as_ref(), LexObjectProperty::CidLink(c) => c.description.as_ref(), LexObjectProperty::Array(a) => a.description.as_ref(), LexObjectProperty::Blob(b) => b.description.as_ref(), LexObjectProperty::Object(o) => o.description.as_ref(), LexObjectProperty::Boolean(b) => b.description.as_ref(), LexObjectProperty::Integer(i) => i.description.as_ref(), LexObjectProperty::String(s) => s.description.as_ref(), LexObjectProperty::Unknown(u) => u.description.as_ref(), };
// Extract schema default and generate companion function + serde attr. let (default_doc, serde_default_attr, default_fn) = self.extract_field_default( parent_type_name, field_name, field_type, is_optional, resolved, );
// Combine description with default doc suffix. let combined_desc = match (description, &default_doc) { (Some(desc), Some(def_doc)) => Some(format!("{} {}", desc.as_ref(), def_doc)), (Some(desc), None) => Some(desc.as_ref().to_string()), (None, Some(def_doc)) => Some(def_doc.clone()), (None, None) => None, }; let doc = combined_desc .as_ref() .map(|d| { let d = d.as_str(); quote! { #[doc = #d] } }) .unwrap_or_default();
let mut attrs = Vec::new();
if is_optional { let is_none_path = resolved.option_is_none_path(); attrs.push(quote! { #[serde(skip_serializing_if = #is_none_path)] }); }
if let Some(serde_attr) = serde_default_attr { attrs.push(serde_attr); }
if matches!(field_type, LexObjectProperty::Bytes(_)) { if !is_optional { attrs.push(quote! { #[serde(with = "jacquard_common::serde_bytes_helper")] }); } else { attrs.push( quote! {#[serde(default, with = "jacquard_common::opt_serde_bytes_helper")] }, ); } }
Ok(( quote! { #doc #(#attrs)* pub #field_ident: #rust_type, }, default_fn, )) }
/// Extract schema default value from a field type and generate the companion /// default function and serde attribute. /// /// Returns (doc_suffix, serde_attr, companion_fn). fn extract_field_default( &self, parent_type_name: &str, field_name: &str, field_type: &LexObjectProperty<'static>, is_optional: bool, resolved: &super::prettify::ResolvedImports, ) -> (Option<String>, Option<TokenStream>, Option<TokenStream>) { let fn_name = format!( "_default_{}_{}", parent_type_name.to_snake_case(), field_name.to_snake_case() ); let fn_ident = syn::Ident::new(&fn_name, proc_macro2::Span::call_site()); let serde_attr = quote! { #[serde(default = #fn_name)] };
match field_type { LexObjectProperty::Boolean(b) if b.default.is_some() => { let v = b.default.unwrap(); let doc = format!(" Defaults to `{}`.", v); if is_optional { let opt_bool = resolved.option_type(quote! { bool }); ( Some(doc), Some(serde_attr), Some(quote! { fn #fn_ident() -> #opt_bool { Some(#v) } }), ) } else { ( Some(doc), Some(serde_attr), Some(quote! { fn #fn_ident() -> bool { #v } }), ) } } LexObjectProperty::Integer(i) if i.default.is_some() => { let v = i.default.unwrap(); let doc = format!(" Defaults to `{}`.", v); if is_optional { let opt_i64 = resolved.option_type(quote! { i64 }); ( Some(doc), Some(serde_attr), Some(quote! { fn #fn_ident() -> #opt_i64 { Some(#v) } }), ) } else { ( Some(doc), Some(serde_attr), Some(quote! { fn #fn_ident() -> i64 { #v } }), ) } } LexObjectProperty::String(s) if s.default.is_some() && s.known_values.is_none() => { let v = s.default.as_ref().unwrap().as_ref(); let doc = format!(" Defaults to `\"{}\"`.", v); // The default function is generic over S: FromStaticStr. // Type inference picks up S from the field's expected type, // and the serde(bound) on the struct ensures the bound is met. let from_static_path = resolved.external_type_tokens(&super::prettify::ExternalImport::FromStaticStr); if is_optional { ( Some(doc), Some(serde_attr), Some(quote! { fn #fn_ident<S: #from_static_path>() -> ::core::option::Option<S> { Some(S::from_static(#v)) } }), ) } else { ( Some(doc), Some(serde_attr), Some(quote! { fn #fn_ident<S: #from_static_path>() -> S { S::from_static(#v) } }), ) } } _ => (None, None, None), } }
/// Generate a manual `impl Default` for a struct when all required fields have /// schema defaults. Optional fields default to `None` or `Some(schema_default)`. pub(super) fn generate_manual_default( &self, type_name: &str, obj: &LexObject<'static>, resolved: &super::prettify::ResolvedImports, ) -> Option<TokenStream> { if !super::builder_heuristics::eligible_for_schema_default(obj) { return None; }
// Check if any field actually has a schema default. If none do, // the existing derive(Default) is sufficient. let any_schema_default = obj .properties .values() .any(|p| super::builder_heuristics::has_schema_default(p)); if !any_schema_default { return None; }
let ident = syn::Ident::new(type_name, proc_macro2::Span::call_site()); let required = obj.required.as_ref().map(|r| r.as_slice()).unwrap_or(&[]); let nullable = obj.nullable.as_ref().map(|n| n.as_slice()).unwrap_or(&[]);
let field_defaults: Vec<_> = obj .properties .iter() .map(|(field_name, field_type)| { let field_ident = make_ident(&field_name.to_snake_case()); let is_required = required.contains(field_name); let is_nullable = nullable.contains(field_name); let is_optional = !is_required || is_nullable;
let value = self.schema_default_value(field_type, is_optional, resolved); quote! { #field_ident: #value } }) .collect();
// Manual Default impl uses DefaultStr (SmolStr) since string fields // with schema defaults need a concrete string type for construction. Some(quote! { impl Default for #ident { fn default() -> Self { Self { #(#field_defaults,)* extra_data: Default::default(), } } } }) }
/// Generate the default value expression for a field. fn schema_default_value( &self, field_type: &LexObjectProperty<'static>, is_optional: bool, resolved: &super::prettify::ResolvedImports, ) -> TokenStream { let inner = match field_type { LexObjectProperty::Boolean(b) if b.default.is_some() => { let v = b.default.unwrap(); Some(quote! { #v }) } LexObjectProperty::Integer(i) if i.default.is_some() => { let v = i.default.unwrap(); Some(quote! { #v }) } LexObjectProperty::String(s) if s.default.is_some() && s.known_values.is_none() => { let v = s.default.as_ref().unwrap().as_ref(); let smolstr_path = resolved.type_path(&super::prettify::CommonType::SmolStr); Some(quote! { #smolstr_path::from(#v) }) } _ => None, };
match (inner, is_optional) { (Some(val), true) => quote! { Some(#val) }, (Some(val), false) => val, (None, true) => quote! { None }, (None, false) => quote! { Default::default() }, } }
/// Generate a union enum for refs pub fn generate_union( &self, current_nsid: &str, union_name: &str, refs: &[jacquard_common::CowStr<'static>], description: Option<&str>, closed: Option<bool>, resolved: &super::prettify::ResolvedImports, ) -> Result<GeneratedCode> { let enum_ident = syn::Ident::new(union_name, proc_macro2::Span::call_site());
// Build variants using the union_codegen module let ctx = super::union_codegen::UnionGenContext { corpus: self.corpus, namespace_deps: &self.namespace_deps, current_nsid, };
let union_variants = ctx.build_union_variants(refs, |ref_str| self.ref_to_rust_type(ref_str, resolved))?; let variants = super::union_codegen::generate_variant_tokens(&union_variants);
let doc = description .map(|d| quote! { #[doc = #d] }) .unwrap_or_else(|| quote! {});
// Only add open_union if not closed. let is_open = closed != Some(true); let derive_attr = resolved.derive_standard(); let bosstr_path = resolved.external_type_tokens(&super::prettify::ExternalImport::BosStr); let default_str_path = resolved.external_type_tokens(&super::prettify::ExternalImport::DefaultStr); let bosstr_serde = resolved.serde_external_path(&super::prettify::ExternalImport::BosStr); let de_serde = resolved.serde_external_path(&super::prettify::ExternalImport::Deserialize); let serde_de_bound = format!("S: {}<'de> + {}", de_serde, bosstr_serde);
let serde_bound = quote! { #[serde(tag = "$type", bound(deserialize = #serde_de_bound))] };
let enum_def = if is_open { let open_union_attr = resolved.attribute_tokens(&super::prettify::ExternalImport::OpenUnion); quote! { #doc #open_union_attr #derive_attr #serde_bound pub enum #enum_ident<S: #bosstr_path = #default_str_path> { #(#variants,)* } } } else { quote! { #doc #derive_attr #serde_bound pub enum #enum_ident<S: #bosstr_path = #default_str_path> { #(#variants,)* } } };
Ok(GeneratedCode::type_only(enum_def)) }
/// Generate enum for string with known values. pub(super) fn generate_known_values_enum( &self, nsid: &str, def_name: &str, string: &LexString<'static>, resolved: &super::prettify::ResolvedImports, ) -> Result<GeneratedCode> { let type_name = self.def_to_type_name(nsid, def_name); let ident = syn::Ident::new(&type_name, proc_macro2::Span::call_site());
let known_values = string.known_values.as_ref().unwrap(); let mut variants = Vec::new(); let mut from_str_arms = Vec::new(); let mut as_str_arms = Vec::new();
let mut known_variant_names = std::collections::HashSet::new(); for value in known_values { // Convert value to valid Rust identifier let value_str = value.as_ref(); let variant_name = value_to_variant_name(value_str); known_variant_names.insert(variant_name.clone()); let variant_ident = syn::Ident::new(&variant_name, proc_macro2::Span::call_site());
variants.push(quote! { #variant_ident });
from_str_arms.push(quote! { #value_str => Self::#variant_ident });
as_str_arms.push(quote! { Self::#variant_ident => #value_str }); }
// Choose catch-all name, falling back if "Other" collides with a known value variant. let catchall_name = if known_variant_names.contains("Other") { "UnknownValue" } else { "Other" }; let catchall_ident = syn::Ident::new(catchall_name, proc_macro2::Span::call_site());
let doc = self.generate_doc_comment(string.description.as_ref());
// Generate IntoStatic impl let variant_info: Vec<(String, EnumVariantKind)> = known_values .iter() .map(|value| { let variant_name = value_to_variant_name(value.as_ref()); (variant_name, EnumVariantKind::Unit) }) .chain(std::iter::once(( catchall_name.to_string(), EnumVariantKind::Tuple, ))) .collect(); let into_static_impl = self.generate_into_static_for_enum(&type_name, &variant_info, true, false, resolved);
let cowstr_type = resolved.type_tokens(&super::prettify::CommonType::CowStr); let bosstr_path = resolved.external_type_tokens(&super::prettify::ExternalImport::BosStr); let default_str_path = resolved.external_type_tokens(&super::prettify::ExternalImport::DefaultStr); let ser_serde = resolved.external_type_tokens(&super::prettify::ExternalImport::Serialize); let de_serde = resolved.external_type_tokens(&super::prettify::ExternalImport::Deserialize); let enum_def = quote! { #doc #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum #ident<S: #bosstr_path = #default_str_path> { #(#variants,)* #catchall_ident(#cowstr_type), }
impl<S: #bosstr_path> #ident<S> { pub fn as_str(&self) -> &str { match self { #(#as_str_arms,)* Self::#catchall_ident(s) => s.as_ref(), } }
/// Construct from a string-like value, matching known values. pub fn from_value(s: S) -> Self { match s.as_ref() { #(#from_str_arms,)* _ => Self::#catchall_ident(s), } } }
impl<S: #bosstr_path> AsRef<str> for #ident<S> { fn as_ref(&self) -> &str { self.as_str() } }
impl<S: #bosstr_path> core::fmt::Display for #ident<S> { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { write!(f, "{}", self.as_str()) } }
impl<S: #bosstr_path> #ser_serde for #ident<S> { fn serialize<Ser>(&self, serializer: Ser) -> Result<Ser::Ok, Ser::Error> where Ser: serde::Serializer, { serializer.serialize_str(self.as_str()) } }
impl<'de, S: #de_serde<'de> + #bosstr_path> #de_serde<'de> for #ident<S> { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: serde::Deserializer<'de>, { let s = S::deserialize(deserializer)?; Ok(Self::from_value(s)) } }
#into_static_impl };
Ok(GeneratedCode::type_only(enum_def)) }
/// Generate enum for inline string property with known values. /// Unlike `generate_known_values_enum`, this takes the type name directly /// and uses fragment extraction for NSID#fragment values. pub(super) fn generate_inline_known_values_enum( &self, type_name: &str, string: &LexString<'static>, resolved: &super::prettify::ResolvedImports, ) -> Result<GeneratedCode> { let ident = syn::Ident::new(type_name, proc_macro2::Span::call_site());
let known_values = string.known_values.as_ref().unwrap(); let mut variants = Vec::new(); let mut from_str_arms = Vec::new(); let mut as_str_arms = Vec::new(); let mut known_variant_names = std::collections::HashSet::new();
for value in known_values { let value_str = value.as_ref(); // Use known_value_to_variant_name to extract fragment from NSID#fragment let variant_name = known_value_to_variant_name(value_str); known_variant_names.insert(variant_name.clone()); let variant_ident = syn::Ident::new(&variant_name, proc_macro2::Span::call_site());
variants.push(quote! { #variant_ident });
from_str_arms.push(quote! { #value_str => Self::#variant_ident });
as_str_arms.push(quote! { Self::#variant_ident => #value_str }); }
// Choose catch-all name, falling back if "Other" collides with a known value variant. let catchall_name = if known_variant_names.contains("Other") { "UnknownValue" } else { "Other" }; let catchall_ident = syn::Ident::new(catchall_name, proc_macro2::Span::call_site());
let doc = self.generate_doc_comment(string.description.as_ref());
// Generate IntoStatic impl let variant_info: Vec<(String, EnumVariantKind)> = known_values .iter() .map(|value| { let variant_name = known_value_to_variant_name(value.as_ref()); (variant_name, EnumVariantKind::Unit) }) .chain(std::iter::once(( catchall_name.to_string(), EnumVariantKind::Tuple, ))) .collect(); let into_static_impl = self.generate_into_static_for_enum(type_name, &variant_info, true, false, resolved);
let cowstr_type = resolved.type_tokens(&super::prettify::CommonType::CowStr); let bosstr_path = resolved.external_type_tokens(&super::prettify::ExternalImport::BosStr); let default_str_path = resolved.external_type_tokens(&super::prettify::ExternalImport::DefaultStr); let ser_serde = resolved.external_type_tokens(&super::prettify::ExternalImport::Serialize); let de_serde = resolved.external_type_tokens(&super::prettify::ExternalImport::Deserialize); let enum_def = quote! { #doc #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum #ident<S: #bosstr_path = #default_str_path> { #(#variants,)* #catchall_ident(#cowstr_type), }
impl<S: #bosstr_path> #ident<S> { pub fn as_str(&self) -> &str { match self { #(#as_str_arms,)* Self::#catchall_ident(s) => s.as_ref(), } }
/// Construct from a string-like value, matching known values. pub fn from_value(s: S) -> Self { match s.as_ref() { #(#from_str_arms,)* _ => Self::#catchall_ident(s), } } }
impl<S: #bosstr_path> core::fmt::Display for #ident<S> { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { write!(f, "{}", self.as_str()) } }
impl<S: #bosstr_path> AsRef<str> for #ident<S> { fn as_ref(&self) -> &str { self.as_str() } }
impl<S: #bosstr_path> #ser_serde for #ident<S> { fn serialize<Ser>(&self, serializer: Ser) -> Result<Ser::Ok, Ser::Error> where Ser: serde::Serializer, { serializer.serialize_str(self.as_str()) } }
impl<'de, S: #de_serde<'de> + #bosstr_path> #de_serde<'de> for #ident<S> { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: serde::Deserializer<'de>, { let s = S::deserialize(deserializer)?; Ok(Self::from_value(s)) } }
impl<S: #bosstr_path + Default> Default for #ident<S> { fn default() -> Self { Self::#catchall_ident(Default::default()) } }
#into_static_impl };
Ok(GeneratedCode::type_only(enum_def)) }
/// Generate enum for integer with enum values pub(super) fn generate_integer_enum( &self, nsid: &str, def_name: &str, integer: &LexInteger<'static>, ) -> Result<GeneratedCode> { let type_name = self.def_to_type_name(nsid, def_name); let ident = syn::Ident::new(&type_name, proc_macro2::Span::call_site());
let enum_values = integer.r#enum.as_ref().unwrap(); let mut variants = Vec::new(); let mut from_i64_arms = Vec::new(); let mut to_i64_arms = Vec::new();
for value in enum_values { let variant_name = format!("Value{}", value.abs()); let variant_ident = syn::Ident::new(&variant_name, proc_macro2::Span::call_site());
variants.push(quote! { #[serde(rename = #value)] #variant_ident });
from_i64_arms.push(quote! { #value => Self::#variant_ident });
to_i64_arms.push(quote! { Self::#variant_ident => #value }); }
let doc = self.generate_doc_comment(integer.description.as_ref());
let enum_def = quote! { #doc #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum #ident { #(#variants,)* #[serde(untagged)] Other(i64), }
impl #ident { pub fn as_i64(&self) -> i64 { match self { #(#to_i64_arms,)* Self::Other(n) => *n, } } }
impl From<i64> for #ident { fn from(n: i64) -> Self { match n { #(#from_i64_arms,)* _ => Self::Other(n), } } }
impl serde::Serialize for #ident { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: serde::Serializer, { serializer.serialize_i64(self.as_i64()) } }
impl<'de> serde::Deserialize<'de> for #ident { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: serde::Deserializer<'de>, { let n = i64::deserialize(deserializer)?; Ok(Self::from(n)) } } };
Ok(GeneratedCode::type_only(enum_def)) }
/// Generate IntoStatic impl for an enum. pub(super) fn generate_into_static_for_enum( &self, type_name: &str, variant_info: &[(String, EnumVariantKind)], has_type_param: bool, is_open: bool, resolved: &super::prettify::ResolvedImports, ) -> TokenStream { let ident = syn::Ident::new(type_name, proc_macro2::Span::call_site()); let bosstr_path = resolved.external_type_tokens(&super::prettify::ExternalImport::BosStr);
if has_type_param { let variant_conversions: Vec<_> = variant_info .iter() .map(|(variant_name, kind)| { let variant_ident = syn::Ident::new(variant_name, proc_macro2::Span::call_site()); match kind { EnumVariantKind::Unit => { quote! { #ident::#variant_ident => #ident::#variant_ident } } EnumVariantKind::Tuple => { quote! { #ident::#variant_ident(v) => #ident::#variant_ident(v.into_static()) } } EnumVariantKind::Struct(fields) => { let field_idents: Vec<_> = fields .iter() .map(|f| make_ident(&f.to_snake_case())) .collect(); let field_conversions: Vec<_> = field_idents .iter() .map(|f| quote! { #f: #f.into_static() }) .collect(); quote! { #ident::#variant_ident { #(#field_idents,)* } => #ident::#variant_ident { #(#field_conversions,)* } } } } }) .collect();
let unknown_conversion = if is_open { quote! { #ident::Unknown(v) => #ident::Unknown(v.into_static()), } } else { quote! {} };
quote! { impl<S: #bosstr_path> jacquard_common::IntoStatic for #ident<S> where S: #bosstr_path + jacquard_common::IntoStatic, S::Output: #bosstr_path { type Output = #ident<S::Output>;
fn into_static(self) -> Self::Output { match self { #(#variant_conversions,)* #unknown_conversion } } } } } else { quote! { impl jacquard_common::IntoStatic for #ident { type Output = #ident;
fn into_static(self) -> Self::Output { self } } } } }}