···11+//! Implementation of #[derive(LexiconSchema)] macro
22+33+use crate::lexicon::{
44+ LexArray, LexBlob, LexBoolean, LexBytes, LexCidLink, LexInteger, LexObject, LexObjectProperty,
55+ LexRef, LexRefUnion, LexString, LexStringFormat, LexUnknown, LexUserType,
66+};
77+use crate::schema::type_mapping::{LexiconPrimitiveType, StringFormat, rust_type_to_lexicon_type};
88+use heck::{ToKebabCase, ToLowerCamelCase, ToPascalCase, ToShoutySnakeCase, ToSnakeCase};
99+use jacquard_common::smol_str::{SmolStr, ToSmolStr};
1010+use proc_macro2::TokenStream;
1111+use quote::{ToTokens, quote};
1212+use syn::{Attribute, Data, DeriveInput, Fields, Ident, LitStr, Type, parse2};
1313+1414+/// Implementation for the LexiconSchema derive macro
1515+pub fn impl_derive_lexicon_schema(input: TokenStream) -> TokenStream {
1616+ let input = match parse2::<DeriveInput>(input) {
1717+ Ok(input) => input,
1818+ Err(e) => return e.to_compile_error(),
1919+ };
2020+2121+ match lexicon_schema_impl(&input) {
2222+ Ok(tokens) => tokens,
2323+ Err(e) => e.to_compile_error(),
2424+ }
2525+}
2626+2727+fn lexicon_schema_impl(input: &DeriveInput) -> syn::Result<TokenStream> {
2828+ // Parse type-level attributes
2929+ let type_attrs = parse_type_attrs(&input.attrs)?;
3030+3131+ // Determine NSID
3232+ let nsid = determine_nsid(&type_attrs, input)?;
3333+3434+ // Generate based on data type
3535+ match &input.data {
3636+ Data::Struct(data_struct) => impl_for_struct(input, &type_attrs, &nsid, data_struct),
3737+ Data::Enum(data_enum) => impl_for_enum(input, &type_attrs, &nsid, data_enum),
3838+ Data::Union(_) => Err(syn::Error::new_spanned(
3939+ input,
4040+ "LexiconSchema cannot be derived for unions",
4141+ )),
4242+ }
4343+}
4444+4545+/// Parsed lexicon attributes from type
4646+#[derive(Debug, Default)]
4747+struct LexiconTypeAttrs {
4848+ /// NSID for this type (required for primary types)
4949+ nsid: Option<String>,
5050+5151+ /// Fragment name (None = not a fragment, Some("") = infer from type name)
5252+ fragment: Option<String>,
5353+5454+ /// Type kind
5555+ kind: Option<LexiconTypeKind>,
5656+5757+ /// Record key type (for records)
5858+ key: Option<String>,
5959+}
6060+6161+#[derive(Debug, Clone, Copy)]
6262+enum LexiconTypeKind {
6363+ Record,
6464+ Query,
6565+ Procedure,
6666+ Subscription,
6767+ Object,
6868+ Union,
6969+}
7070+7171+/// Parse type-level lexicon attributes
7272+fn parse_type_attrs(attrs: &[Attribute]) -> syn::Result<LexiconTypeAttrs> {
7373+ let mut result = LexiconTypeAttrs::default();
7474+7575+ for attr in attrs {
7676+ if !attr.path().is_ident("lexicon") {
7777+ continue;
7878+ }
7979+8080+ attr.parse_nested_meta(|meta| {
8181+ if meta.path.is_ident("nsid") {
8282+ let value = meta.value()?;
8383+ let lit: LitStr = value.parse()?;
8484+ result.nsid = Some(lit.value());
8585+ Ok(())
8686+ } else if meta.path.is_ident("fragment") {
8787+ // Two forms: #[lexicon(fragment)] or #[lexicon(fragment = "name")]
8888+ if meta.input.peek(syn::Token![=]) {
8989+ let value = meta.value()?;
9090+ let lit: LitStr = value.parse()?;
9191+ result.fragment = Some(lit.value());
9292+ } else {
9393+ result.fragment = Some(String::new()); // Infer from type name
9494+ }
9595+ Ok(())
9696+ } else if meta.path.is_ident("record") {
9797+ result.kind = Some(LexiconTypeKind::Record);
9898+ Ok(())
9999+ } else if meta.path.is_ident("query") {
100100+ result.kind = Some(LexiconTypeKind::Query);
101101+ Ok(())
102102+ } else if meta.path.is_ident("procedure") {
103103+ result.kind = Some(LexiconTypeKind::Procedure);
104104+ Ok(())
105105+ } else if meta.path.is_ident("subscription") {
106106+ result.kind = Some(LexiconTypeKind::Subscription);
107107+ Ok(())
108108+ } else if meta.path.is_ident("key") {
109109+ let value = meta.value()?;
110110+ let lit: LitStr = value.parse()?;
111111+ result.key = Some(lit.value());
112112+ Ok(())
113113+ } else {
114114+ Err(meta.error("unknown lexicon attribute"))
115115+ }
116116+ })?;
117117+ }
118118+119119+ Ok(result)
120120+}
121121+122122+/// Parsed lexicon attributes from field
123123+#[derive(Debug, Default)]
124124+struct LexiconFieldAttrs {
125125+ max_length: Option<usize>,
126126+ max_graphemes: Option<usize>,
127127+ min_length: Option<usize>,
128128+ min_graphemes: Option<usize>,
129129+ minimum: Option<i64>,
130130+ maximum: Option<i64>,
131131+ explicit_ref: Option<String>,
132132+ format: Option<String>,
133133+}
134134+135135+/// Parse field-level lexicon attributes
136136+fn parse_field_attrs(attrs: &[Attribute]) -> syn::Result<LexiconFieldAttrs> {
137137+ let mut result = LexiconFieldAttrs::default();
138138+139139+ for attr in attrs {
140140+ if !attr.path().is_ident("lexicon") {
141141+ continue;
142142+ }
143143+144144+ attr.parse_nested_meta(|meta| {
145145+ if meta.path.is_ident("max_length") {
146146+ let value = meta.value()?;
147147+ let lit: syn::LitInt = value.parse()?;
148148+ result.max_length = Some(lit.base10_parse()?);
149149+ Ok(())
150150+ } else if meta.path.is_ident("max_graphemes") {
151151+ let value = meta.value()?;
152152+ let lit: syn::LitInt = value.parse()?;
153153+ result.max_graphemes = Some(lit.base10_parse()?);
154154+ Ok(())
155155+ } else if meta.path.is_ident("min_length") {
156156+ let value = meta.value()?;
157157+ let lit: syn::LitInt = value.parse()?;
158158+ result.min_length = Some(lit.base10_parse()?);
159159+ Ok(())
160160+ } else if meta.path.is_ident("min_graphemes") {
161161+ let value = meta.value()?;
162162+ let lit: syn::LitInt = value.parse()?;
163163+ result.min_graphemes = Some(lit.base10_parse()?);
164164+ Ok(())
165165+ } else if meta.path.is_ident("minimum") {
166166+ let value = meta.value()?;
167167+ let lit: syn::LitInt = value.parse()?;
168168+ result.minimum = Some(lit.base10_parse()?);
169169+ Ok(())
170170+ } else if meta.path.is_ident("maximum") {
171171+ let value = meta.value()?;
172172+ let lit: syn::LitInt = value.parse()?;
173173+ result.maximum = Some(lit.base10_parse()?);
174174+ Ok(())
175175+ } else if meta.path.is_ident("ref") {
176176+ let value = meta.value()?;
177177+ let lit: LitStr = value.parse()?;
178178+ result.explicit_ref = Some(lit.value());
179179+ Ok(())
180180+ } else if meta.path.is_ident("format") {
181181+ let value = meta.value()?;
182182+ let lit: LitStr = value.parse()?;
183183+ result.format = Some(lit.value());
184184+ Ok(())
185185+ } else {
186186+ Err(meta.error("unknown lexicon field attribute"))
187187+ }
188188+ })?;
189189+ }
190190+191191+ Ok(result)
192192+}
193193+194194+/// Parsed serde attributes relevant to lexicon schema
195195+#[derive(Debug, Default)]
196196+struct SerdeAttrs {
197197+ rename: Option<String>,
198198+ skip: bool,
199199+}
200200+201201+/// Parse serde attributes for a field
202202+fn parse_serde_attrs(attrs: &[Attribute]) -> syn::Result<SerdeAttrs> {
203203+ let mut result = SerdeAttrs::default();
204204+205205+ for attr in attrs {
206206+ if !attr.path().is_ident("serde") {
207207+ continue;
208208+ }
209209+210210+ attr.parse_nested_meta(|meta| {
211211+ if meta.path.is_ident("rename") {
212212+ let value = meta.value()?;
213213+ let lit: LitStr = value.parse()?;
214214+ result.rename = Some(lit.value());
215215+ Ok(())
216216+ } else if meta.path.is_ident("skip") {
217217+ result.skip = true;
218218+ Ok(())
219219+ } else {
220220+ // Ignore other serde attributes
221221+ Ok(())
222222+ }
223223+ })?;
224224+ }
225225+226226+ Ok(result)
227227+}
228228+229229+/// Parse container-level serde rename_all
230230+fn parse_serde_rename_all(attrs: &[Attribute]) -> syn::Result<Option<RenameRule>> {
231231+ for attr in attrs {
232232+ if !attr.path().is_ident("serde") {
233233+ continue;
234234+ }
235235+236236+ let mut found_rule = None;
237237+ attr.parse_nested_meta(|meta| {
238238+ if meta.path.is_ident("rename_all") {
239239+ let value = meta.value()?;
240240+ let lit: LitStr = value.parse()?;
241241+ found_rule = RenameRule::from_str(&lit.value());
242242+ Ok(())
243243+ } else {
244244+ Ok(())
245245+ }
246246+ })?;
247247+248248+ if found_rule.is_some() {
249249+ return Ok(found_rule);
250250+ }
251251+ }
252252+253253+ // Default to camelCase (lexicon standard)
254254+ Ok(Some(RenameRule::CamelCase))
255255+}
256256+257257+#[derive(Debug, Clone, Copy)]
258258+enum RenameRule {
259259+ CamelCase,
260260+ SnakeCase,
261261+ PascalCase,
262262+ ScreamingSnakeCase,
263263+ KebabCase,
264264+}
265265+266266+impl RenameRule {
267267+ fn from_str(s: &str) -> Option<Self> {
268268+ match s {
269269+ "camelCase" => Some(RenameRule::CamelCase),
270270+ "snake_case" => Some(RenameRule::SnakeCase),
271271+ "PascalCase" => Some(RenameRule::PascalCase),
272272+ "SCREAMING_SNAKE_CASE" => Some(RenameRule::ScreamingSnakeCase),
273273+ "kebab-case" => Some(RenameRule::KebabCase),
274274+ _ => None,
275275+ }
276276+ }
277277+278278+ fn apply(&self, input: &str) -> String {
279279+ match self {
280280+ RenameRule::CamelCase => input.to_lower_camel_case(),
281281+ RenameRule::SnakeCase => input.to_snake_case(),
282282+ RenameRule::PascalCase => input.to_pascal_case(),
283283+ RenameRule::ScreamingSnakeCase => input.to_shouty_snake_case(),
284284+ RenameRule::KebabCase => input.to_kebab_case(),
285285+ }
286286+ }
287287+}
288288+289289+/// Determine NSID from attributes and context
290290+fn determine_nsid(attrs: &LexiconTypeAttrs, input: &DeriveInput) -> syn::Result<String> {
291291+ // Explicit NSID in lexicon attribute
292292+ if let Some(nsid) = &attrs.nsid {
293293+ return Ok(nsid.clone());
294294+ }
295295+296296+ // Fragment - need to find module NSID (not implemented yet)
297297+ if attrs.fragment.is_some() {
298298+ return Err(syn::Error::new_spanned(
299299+ input,
300300+ "fragments require explicit nsid or module-level primary type (not yet implemented)",
301301+ ));
302302+ }
303303+304304+ // Check for XrpcRequest derive with NSID
305305+ if let Some(nsid) = extract_xrpc_nsid(&input.attrs)? {
306306+ return Ok(nsid);
307307+ }
308308+309309+ Err(syn::Error::new_spanned(
310310+ input,
311311+ "missing required `nsid` attribute (use #[lexicon(nsid = \"...\")] or #[xrpc(nsid = \"...\")])",
312312+ ))
313313+}
314314+315315+/// Extract NSID from XrpcRequest attributes (cross-derive coordination)
316316+fn extract_xrpc_nsid(attrs: &[Attribute]) -> syn::Result<Option<String>> {
317317+ for attr in attrs {
318318+ if !attr.path().is_ident("xrpc") {
319319+ continue;
320320+ }
321321+322322+ let mut nsid = None;
323323+ attr.parse_nested_meta(|meta| {
324324+ if meta.path.is_ident("nsid") {
325325+ let value = meta.value()?;
326326+ let lit: LitStr = value.parse()?;
327327+ nsid = Some(lit.value());
328328+ }
329329+ Ok(())
330330+ })?;
331331+332332+ if let Some(nsid) = nsid {
333333+ return Ok(Some(nsid));
334334+ }
335335+ }
336336+ Ok(None)
337337+}
338338+339339+/// Struct implementation
340340+fn impl_for_struct(
341341+ input: &DeriveInput,
342342+ type_attrs: &LexiconTypeAttrs,
343343+ nsid: &str,
344344+ data_struct: &syn::DataStruct,
345345+) -> syn::Result<TokenStream> {
346346+ let name = &input.ident;
347347+ let generics = &input.generics;
348348+349349+ // Detect lifetime
350350+ let has_lifetime = generics.lifetimes().next().is_some();
351351+ let lifetime = if has_lifetime {
352352+ quote! { <'_> }
353353+ } else {
354354+ quote! {}
355355+ };
356356+357357+ // Parse fields
358358+ let fields = match &data_struct.fields {
359359+ Fields::Named(fields) => &fields.named,
360360+ _ => {
361361+ return Err(syn::Error::new_spanned(
362362+ input,
363363+ "LexiconSchema only supports structs with named fields",
364364+ ));
365365+ }
366366+ };
367367+368368+ // Parse serde container attributes (defaults to camelCase)
369369+ let rename_all = parse_serde_rename_all(&input.attrs)?;
370370+371371+ // Generate field definitions
372372+ let field_defs = generate_field_definitions(fields, rename_all)?;
373373+374374+ // Generate validation code
375375+ let validation_code = generate_validation(fields, rename_all)?;
376376+377377+ // Build lexicon_doc() implementation
378378+ let doc_impl = generate_doc_impl(nsid, type_attrs, &field_defs)?;
379379+380380+ // Determine schema_id (add fragment suffix if needed)
381381+ let schema_id = if let Some(fragment) = &type_attrs.fragment {
382382+ let frag_name = if fragment.is_empty() {
383383+ // Infer from type name
384384+ name.to_string().to_lower_camel_case()
385385+ } else {
386386+ fragment.clone()
387387+ };
388388+ quote! {
389389+ format_smolstr!("{}#{}", #nsid, #frag_name).to_string()
390390+ }
391391+ } else {
392392+ quote! {
393393+ ::jacquard_common::CowStr::new_static(#nsid)
394394+ }
395395+ };
396396+397397+ // Generate trait impl
398398+ Ok(quote! {
399399+ impl #generics ::jacquard_lexicon::schema::LexiconSchema for #name #lifetime {
400400+ fn nsid() -> &'static str {
401401+ #nsid
402402+ }
403403+404404+ fn schema_id() -> ::jacquard_common::CowStr<'static> {
405405+ #schema_id
406406+ }
407407+408408+ fn lexicon_doc(
409409+ generator: &mut ::jacquard_lexicon::schema::LexiconGenerator
410410+ ) -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> {
411411+ #doc_impl
412412+ }
413413+414414+ fn validate(&self) -> ::std::result::Result<(), ::jacquard_lexicon::schema::ValidationError> {
415415+ #validation_code
416416+ }
417417+ }
418418+419419+ // Generate inventory submission for Phase 3 discovery
420420+ ::inventory::submit! {
421421+ ::jacquard_lexicon::schema::LexiconSchemaRef {
422422+ nsid: #nsid,
423423+ provider: || {
424424+ let mut generator = ::jacquard_lexicon::schema::LexiconGenerator::new(#nsid);
425425+ #name::lexicon_doc(&mut generator)
426426+ },
427427+ }
428428+ }
429429+ })
430430+}
431431+432432+struct FieldDef {
433433+ name: String, // Rust field name
434434+ schema_name: String, // JSON field name (after serde rename)
435435+ rust_type: Type, // Rust type
436436+ lex_type: TokenStream, // LexObjectProperty tokens
437437+ required: bool,
438438+}
439439+440440+fn generate_field_definitions(
441441+ fields: &syn::punctuated::Punctuated<syn::Field, syn::Token![,]>,
442442+ rename_all: Option<RenameRule>,
443443+) -> syn::Result<Vec<FieldDef>> {
444444+ let mut defs = Vec::new();
445445+446446+ for field in fields {
447447+ let field_name = field.ident.as_ref().unwrap().to_string();
448448+449449+ // Skip extra_data field (added by #[lexicon] attribute macro)
450450+ if field_name == "extra_data" {
451451+ continue;
452452+ }
453453+454454+ // Parse attributes
455455+ let serde_attrs = parse_serde_attrs(&field.attrs)?;
456456+ let lex_attrs = parse_field_attrs(&field.attrs)?;
457457+458458+ // Skip if serde(skip)
459459+ if serde_attrs.skip {
460460+ continue;
461461+ }
462462+463463+ // Determine schema name
464464+ let schema_name = if let Some(rename) = serde_attrs.rename {
465465+ rename
466466+ } else if let Some(rule) = rename_all {
467467+ rule.apply(&field_name)
468468+ } else {
469469+ field_name.clone()
470470+ };
471471+472472+ // Determine if required (Option<T> = optional)
473473+ let (inner_type, required) = extract_option_inner(&field.ty);
474474+ let rust_type = inner_type.clone();
475475+476476+ // Generate LexObjectProperty based on type + constraints
477477+ let lex_type = generate_lex_property(&rust_type, &lex_attrs)?;
478478+479479+ defs.push(FieldDef {
480480+ name: field_name,
481481+ schema_name,
482482+ rust_type,
483483+ lex_type,
484484+ required,
485485+ });
486486+ }
487487+488488+ Ok(defs)
489489+}
490490+491491+/// Extract T from Option<T>, return (type, is_required)
492492+fn extract_option_inner(ty: &Type) -> (&Type, bool) {
493493+ if let Type::Path(type_path) = ty {
494494+ if let Some(segment) = type_path.path.segments.last() {
495495+ if segment.ident == "Option" {
496496+ if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
497497+ if let Some(syn::GenericArgument::Type(inner)) = args.args.first() {
498498+ return (inner, false);
499499+ }
500500+ }
501501+ }
502502+ }
503503+ }
504504+ (ty, true)
505505+}
506506+507507+/// Generate LexObjectProperty tokens for a field
508508+fn generate_lex_property(
509509+ rust_type: &Type,
510510+ constraints: &LexiconFieldAttrs,
511511+) -> syn::Result<TokenStream> {
512512+ // Try to detect primitive type
513513+ let lex_type = rust_type_to_lexicon_type(rust_type);
514514+515515+ match lex_type {
516516+ Some(LexiconPrimitiveType::Boolean) => Ok(quote! {
517517+ ::jacquard_lexicon::lexicon::LexObjectProperty::Boolean(
518518+ ::jacquard_lexicon::lexicon::LexBoolean {
519519+ description: None,
520520+ default: None,
521521+ r#const: None,
522522+ }
523523+ )
524524+ }),
525525+ Some(LexiconPrimitiveType::Integer) => {
526526+ let minimum = constraints
527527+ .minimum
528528+ .map(|v| quote! { Some(#v) })
529529+ .unwrap_or(quote! { None });
530530+ let maximum = constraints
531531+ .maximum
532532+ .map(|v| quote! { Some(#v) })
533533+ .unwrap_or(quote! { None });
534534+535535+ Ok(quote! {
536536+ ::jacquard_lexicon::lexicon::LexObjectProperty::Integer(
537537+ ::jacquard_lexicon::lexicon::LexInteger {
538538+ description: None,
539539+ default: None,
540540+ minimum: #minimum,
541541+ maximum: #maximum,
542542+ r#enum: None,
543543+ r#const: None,
544544+ }
545545+ )
546546+ })
547547+ }
548548+ Some(LexiconPrimitiveType::String(format)) => generate_string_property(format, constraints),
549549+ Some(LexiconPrimitiveType::Bytes) => {
550550+ let max_length = constraints
551551+ .max_length
552552+ .map(|v| quote! { Some(#v) })
553553+ .unwrap_or(quote! { None });
554554+ let min_length = constraints
555555+ .min_length
556556+ .map(|v| quote! { Some(#v) })
557557+ .unwrap_or(quote! { None });
558558+559559+ Ok(quote! {
560560+ ::jacquard_lexicon::lexicon::LexObjectProperty::Bytes(
561561+ ::jacquard_lexicon::lexicon::LexBytes {
562562+ description: None,
563563+ max_length: #max_length,
564564+ min_length: #min_length,
565565+ }
566566+ )
567567+ })
568568+ }
569569+ Some(LexiconPrimitiveType::CidLink) => Ok(quote! {
570570+ ::jacquard_lexicon::lexicon::LexObjectProperty::CidLink(
571571+ ::jacquard_lexicon::lexicon::LexCidLink {
572572+ description: None,
573573+ }
574574+ )
575575+ }),
576576+ Some(LexiconPrimitiveType::Blob) => Ok(quote! {
577577+ ::jacquard_lexicon::lexicon::LexObjectProperty::Blob(
578578+ ::jacquard_lexicon::lexicon::LexBlob {
579579+ description: None,
580580+ accept: None,
581581+ max_size: None,
582582+ }
583583+ )
584584+ }),
585585+ Some(LexiconPrimitiveType::Unknown) => Ok(quote! {
586586+ ::jacquard_lexicon::lexicon::LexObjectProperty::Unknown(
587587+ ::jacquard_lexicon::lexicon::LexUnknown {
588588+ description: None,
589589+ }
590590+ )
591591+ }),
592592+ Some(LexiconPrimitiveType::Array(item_type)) => {
593593+ let item_prop = generate_array_item(*item_type, constraints)?;
594594+ let max_length = constraints
595595+ .max_length
596596+ .map(|v| quote! { Some(#v) })
597597+ .unwrap_or(quote! { None });
598598+ let min_length = constraints
599599+ .min_length
600600+ .map(|v| quote! { Some(#v) })
601601+ .unwrap_or(quote! { None });
602602+603603+ Ok(quote! {
604604+ ::jacquard_lexicon::lexicon::LexObjectProperty::Array(
605605+ ::jacquard_lexicon::lexicon::LexArray {
606606+ description: None,
607607+ items: #item_prop,
608608+ min_length: #min_length,
609609+ max_length: #max_length,
610610+ }
611611+ )
612612+ })
613613+ }
614614+ None => {
615615+ // Not a recognized primitive - check for explicit ref or trait bound
616616+ if let Some(ref_nsid) = &constraints.explicit_ref {
617617+ Ok(quote! {
618618+ ::jacquard_lexicon::lexicon::LexObjectProperty::Ref(
619619+ ::jacquard_lexicon::lexicon::LexRef {
620620+ description: None,
621621+ r#ref: #ref_nsid.into(),
622622+ }
623623+ )
624624+ })
625625+ } else {
626626+ // Try to use type's LexiconSchema impl
627627+ Ok(quote! {
628628+ {
629629+ // Use the type's schema_id method
630630+ let ref_nsid = <#rust_type as ::jacquard_lexicon::schema::LexiconSchema>::schema_id();
631631+ ::jacquard_lexicon::lexicon::LexObjectProperty::Ref(
632632+ ::jacquard_lexicon::lexicon::LexRef {
633633+ description: None,
634634+ r#ref: ref_nsid.to_string().into(),
635635+ }
636636+ )
637637+ }
638638+ })
639639+ }
640640+ }
641641+ _ => Err(syn::Error::new_spanned(
642642+ rust_type,
643643+ "unsupported type for lexicon schema generation",
644644+ )),
645645+ }
646646+}
647647+648648+fn generate_array_item(
649649+ item_type: LexiconPrimitiveType,
650650+ _constraints: &LexiconFieldAttrs,
651651+) -> syn::Result<TokenStream> {
652652+ match item_type {
653653+ LexiconPrimitiveType::String(format) => {
654654+ let format_token = string_format_token(format);
655655+ Ok(quote! {
656656+ ::jacquard_lexicon::lexicon::LexArrayItem::String(
657657+ ::jacquard_lexicon::lexicon::LexString {
658658+ description: None,
659659+ format: #format_token,
660660+ default: None,
661661+ min_length: None,
662662+ max_length: None,
663663+ min_graphemes: None,
664664+ max_graphemes: None,
665665+ r#enum: None,
666666+ r#const: None,
667667+ known_values: None,
668668+ }
669669+ )
670670+ })
671671+ }
672672+ LexiconPrimitiveType::Integer => Ok(quote! {
673673+ ::jacquard_lexicon::lexicon::LexArrayItem::Integer(
674674+ ::jacquard_lexicon::lexicon::LexInteger {
675675+ description: None,
676676+ default: None,
677677+ minimum: None,
678678+ maximum: None,
679679+ r#enum: None,
680680+ r#const: None,
681681+ }
682682+ )
683683+ }),
684684+ _ => Ok(quote! {
685685+ ::jacquard_lexicon::lexicon::LexArrayItem::Unknown(
686686+ ::jacquard_lexicon::lexicon::LexUnknown {
687687+ description: None,
688688+ }
689689+ )
690690+ }),
691691+ }
692692+}
693693+694694+fn generate_string_property(
695695+ format: StringFormat,
696696+ constraints: &LexiconFieldAttrs,
697697+) -> syn::Result<TokenStream> {
698698+ let format_token = string_format_token(format);
699699+700700+ let max_length = constraints
701701+ .max_length
702702+ .map(|v| quote! { Some(#v) })
703703+ .unwrap_or(quote! { None });
704704+ let max_graphemes = constraints
705705+ .max_graphemes
706706+ .map(|v| quote! { Some(#v) })
707707+ .unwrap_or(quote! { None });
708708+ let min_length = constraints
709709+ .min_length
710710+ .map(|v| quote! { Some(#v) })
711711+ .unwrap_or(quote! { None });
712712+ let min_graphemes = constraints
713713+ .min_graphemes
714714+ .map(|v| quote! { Some(#v) })
715715+ .unwrap_or(quote! { None });
716716+717717+ Ok(quote! {
718718+ ::jacquard_lexicon::lexicon::LexObjectProperty::String(
719719+ ::jacquard_lexicon::lexicon::LexString {
720720+ description: None,
721721+ format: #format_token,
722722+ default: None,
723723+ min_length: #min_length,
724724+ max_length: #max_length,
725725+ min_graphemes: #min_graphemes,
726726+ max_graphemes: #max_graphemes,
727727+ r#enum: None,
728728+ r#const: None,
729729+ known_values: None,
730730+ }
731731+ )
732732+ })
733733+}
734734+735735+fn string_format_token(format: StringFormat) -> TokenStream {
736736+ match format {
737737+ StringFormat::Plain => quote! { None },
738738+ StringFormat::Did => {
739739+ quote! { Some(::jacquard_lexicon::lexicon::LexStringFormat::Did) }
740740+ }
741741+ StringFormat::Handle => {
742742+ quote! { Some(::jacquard_lexicon::lexicon::LexStringFormat::Handle) }
743743+ }
744744+ StringFormat::AtUri => {
745745+ quote! { Some(::jacquard_lexicon::lexicon::LexStringFormat::AtUri) }
746746+ }
747747+ StringFormat::Nsid => {
748748+ quote! { Some(::jacquard_lexicon::lexicon::LexStringFormat::Nsid) }
749749+ }
750750+ StringFormat::Cid => {
751751+ quote! { Some(::jacquard_lexicon::lexicon::LexStringFormat::Cid) }
752752+ }
753753+ StringFormat::Datetime => {
754754+ quote! { Some(::jacquard_lexicon::lexicon::LexStringFormat::Datetime) }
755755+ }
756756+ StringFormat::Language => {
757757+ quote! { Some(::jacquard_lexicon::lexicon::LexStringFormat::Language) }
758758+ }
759759+ StringFormat::Tid => {
760760+ quote! { Some(::jacquard_lexicon::lexicon::LexStringFormat::Tid) }
761761+ }
762762+ StringFormat::RecordKey => {
763763+ quote! { Some(::jacquard_lexicon::lexicon::LexStringFormat::RecordKey) }
764764+ }
765765+ StringFormat::AtIdentifier => {
766766+ quote! { Some(::jacquard_lexicon::lexicon::LexStringFormat::AtIdentifier) }
767767+ }
768768+ StringFormat::Uri => {
769769+ quote! { Some(::jacquard_lexicon::lexicon::LexStringFormat::Uri) }
770770+ }
771771+ }
772772+}
773773+774774+fn generate_doc_impl(
775775+ nsid: &str,
776776+ type_attrs: &LexiconTypeAttrs,
777777+ field_defs: &[FieldDef],
778778+) -> syn::Result<TokenStream> {
779779+ // Build properties map
780780+ let properties: Vec<_> = field_defs
781781+ .iter()
782782+ .map(|def| {
783783+ let name = &def.schema_name;
784784+ let lex_type = &def.lex_type;
785785+ quote! {
786786+ (#name.into(), #lex_type)
787787+ }
788788+ })
789789+ .collect();
790790+791791+ // Build required array
792792+ let required: Vec<_> = field_defs
793793+ .iter()
794794+ .filter(|def| def.required)
795795+ .map(|def| {
796796+ let name = &def.schema_name;
797797+ quote! { #name.into() }
798798+ })
799799+ .collect();
800800+801801+ let required_field = if required.is_empty() {
802802+ quote! { None }
803803+ } else {
804804+ quote! { Some(vec![#(#required),*]) }
805805+ };
806806+807807+ // Determine user type based on kind
808808+ let user_type = match type_attrs.kind {
809809+ Some(LexiconTypeKind::Record) => {
810810+ let key = type_attrs
811811+ .key
812812+ .as_ref()
813813+ .map(|k| quote! { Some(#k.into()) })
814814+ .unwrap_or(quote! { None });
815815+816816+ quote! {
817817+ ::jacquard_lexicon::lexicon::LexUserType::Record(
818818+ ::jacquard_lexicon::lexicon::LexRecord {
819819+ description: None,
820820+ key: #key,
821821+ record: ::jacquard_lexicon::lexicon::LexRecordRecord::Object(
822822+ ::jacquard_lexicon::lexicon::LexObject {
823823+ description: None,
824824+ required: #required_field,
825825+ nullable: None,
826826+ properties: [#(#properties),*].into(),
827827+ }
828828+ ),
829829+ }
830830+ )
831831+ }
832832+ }
833833+ Some(LexiconTypeKind::Query) => {
834834+ quote! {
835835+ ::jacquard_lexicon::lexicon::LexUserType::Query(
836836+ ::jacquard_lexicon::lexicon::LexQuery {
837837+ description: None,
838838+ parameters: Some(::jacquard_lexicon::lexicon::LexObject {
839839+ description: None,
840840+ required: #required_field,
841841+ nullable: None,
842842+ properties: [#(#properties),*].into(),
843843+ }),
844844+ output: None,
845845+ errors: None,
846846+ }
847847+ )
848848+ }
849849+ }
850850+ Some(LexiconTypeKind::Procedure) => {
851851+ quote! {
852852+ ::jacquard_lexicon::lexicon::LexUserType::Procedure(
853853+ ::jacquard_lexicon::lexicon::LexProcedure {
854854+ description: None,
855855+ input: Some(::jacquard_lexicon::lexicon::LexProcedureIO {
856856+ description: None,
857857+ encoding: "application/json".into(),
858858+ schema: Some(::jacquard_lexicon::lexicon::LexProcedureSchema::Object(
859859+ ::jacquard_lexicon::lexicon::LexObject {
860860+ description: None,
861861+ required: #required_field,
862862+ nullable: None,
863863+ properties: [#(#properties),*].into(),
864864+ }
865865+ )),
866866+ }),
867867+ output: None,
868868+ errors: None,
869869+ }
870870+ )
871871+ }
872872+ }
873873+ _ => {
874874+ // Default: Object type
875875+ quote! {
876876+ ::jacquard_lexicon::lexicon::LexUserType::Object(
877877+ ::jacquard_lexicon::lexicon::LexObject {
878878+ description: None,
879879+ required: #required_field,
880880+ nullable: None,
881881+ properties: [#(#properties),*].into(),
882882+ }
883883+ )
884884+ }
885885+ }
886886+ };
887887+888888+ Ok(quote! {
889889+ {
890890+ let mut defs = ::std::collections::BTreeMap::new();
891891+ defs.insert("main".into(), #user_type);
892892+893893+ ::jacquard_lexicon::lexicon::LexiconDoc {
894894+ lexicon: ::jacquard_lexicon::lexicon::Lexicon::Lexicon1,
895895+ id: #nsid.into(),
896896+ revision: None,
897897+ description: None,
898898+ defs,
899899+ }
900900+ }
901901+ })
902902+}
903903+904904+fn generate_validation(
905905+ fields: &syn::punctuated::Punctuated<syn::Field, syn::Token![,]>,
906906+ rename_all: Option<RenameRule>,
907907+) -> syn::Result<TokenStream> {
908908+ let mut checks = Vec::new();
909909+910910+ for field in fields {
911911+ let field_name = field.ident.as_ref().unwrap();
912912+ let field_name_str = field_name.to_string();
913913+914914+ // Skip extra_data
915915+ if field_name_str == "extra_data" {
916916+ continue;
917917+ }
918918+919919+ let lex_attrs = parse_field_attrs(&field.attrs)?;
920920+ let serde_attrs = parse_serde_attrs(&field.attrs)?;
921921+922922+ if serde_attrs.skip {
923923+ continue;
924924+ }
925925+926926+ // Get actual field name for errors
927927+ let display_name = if let Some(rename) = serde_attrs.rename {
928928+ rename
929929+ } else if let Some(rule) = rename_all {
930930+ rule.apply(&field_name_str)
931931+ } else {
932932+ field_name_str.clone()
933933+ };
934934+935935+ // Extract inner type if Option
936936+ let (inner_type, is_required) = extract_option_inner(&field.ty);
937937+938938+ // Generate checks based on type and constraints
939939+ let field_checks = generate_field_validation(
940940+ field_name,
941941+ &display_name,
942942+ inner_type,
943943+ is_required,
944944+ &lex_attrs,
945945+ )?;
946946+947947+ checks.extend(field_checks);
948948+ }
949949+950950+ if checks.is_empty() {
951951+ Ok(quote! { Ok(()) })
952952+ } else {
953953+ Ok(quote! {
954954+ let mut errors = Vec::new();
955955+956956+ #(#checks)*
957957+958958+ if errors.is_empty() {
959959+ Ok(())
960960+ } else if errors.len() == 1 {
961961+ Err(errors.into_iter().next().unwrap())
962962+ } else {
963963+ Err(::jacquard_lexicon::schema::ValidationError::Multiple(errors))
964964+ }
965965+ })
966966+ }
967967+}
968968+969969+fn generate_field_validation(
970970+ field_ident: &Ident,
971971+ display_name: &str,
972972+ field_type: &Type,
973973+ is_required: bool,
974974+ constraints: &LexiconFieldAttrs,
975975+) -> syn::Result<Vec<TokenStream>> {
976976+ let mut checks = Vec::new();
977977+978978+ // Determine base type
979979+ let lex_type = rust_type_to_lexicon_type(field_type);
980980+981981+ // Build accessor for the field value
982982+ let (value_binding, value_expr) = if is_required {
983983+ (quote! { let value = &self.#field_ident; }, quote! { value })
984984+ } else {
985985+ (
986986+ quote! {},
987987+ quote! {
988988+ match &self.#field_ident {
989989+ Some(v) => v,
990990+ None => continue,
991991+ }
992992+ },
993993+ )
994994+ };
995995+996996+ match lex_type {
997997+ Some(LexiconPrimitiveType::String(_)) => {
998998+ // String constraints
999999+ if let Some(max_len) = constraints.max_length {
10001000+ checks.push(quote! {
10011001+ #value_binding
10021002+ if #value_expr.len() > #max_len {
10031003+ errors.push(::jacquard_lexicon::schema::ValidationError::MaxLength {
10041004+ field: #display_name,
10051005+ max: #max_len,
10061006+ actual: #value_expr.len(),
10071007+ });
10081008+ }
10091009+ });
10101010+ }
10111011+10121012+ if let Some(max_graphemes) = constraints.max_graphemes {
10131013+ checks.push(quote! {
10141014+ #value_binding
10151015+ let count = ::unicode_segmentation::UnicodeSegmentation::graphemes(
10161016+ #value_expr.as_ref(),
10171017+ true
10181018+ ).count();
10191019+ if count > #max_graphemes {
10201020+ errors.push(::jacquard_lexicon::schema::ValidationError::MaxGraphemes {
10211021+ field: #display_name,
10221022+ max: #max_graphemes,
10231023+ actual: count,
10241024+ });
10251025+ }
10261026+ });
10271027+ }
10281028+10291029+ if let Some(min_len) = constraints.min_length {
10301030+ checks.push(quote! {
10311031+ #value_binding
10321032+ if #value_expr.len() < #min_len {
10331033+ errors.push(::jacquard_lexicon::schema::ValidationError::MinLength {
10341034+ field: #display_name,
10351035+ min: #min_len,
10361036+ actual: #value_expr.len(),
10371037+ });
10381038+ }
10391039+ });
10401040+ }
10411041+10421042+ if let Some(min_graphemes) = constraints.min_graphemes {
10431043+ checks.push(quote! {
10441044+ #value_binding
10451045+ let count = ::unicode_segmentation::UnicodeSegmentation::graphemes(
10461046+ #value_expr.as_ref(),
10471047+ true
10481048+ ).count();
10491049+ if count < #min_graphemes {
10501050+ errors.push(::jacquard_lexicon::schema::ValidationError::MinGraphemes {
10511051+ field: #display_name,
10521052+ min: #min_graphemes,
10531053+ actual: count,
10541054+ });
10551055+ }
10561056+ });
10571057+ }
10581058+ }
10591059+ Some(LexiconPrimitiveType::Integer) => {
10601060+ if let Some(maximum) = constraints.maximum {
10611061+ checks.push(quote! {
10621062+ #value_binding
10631063+ if *#value_expr > #maximum {
10641064+ errors.push(::jacquard_lexicon::schema::ValidationError::Maximum {
10651065+ field: #display_name,
10661066+ max: #maximum,
10671067+ actual: *#value_expr,
10681068+ });
10691069+ }
10701070+ });
10711071+ }
10721072+10731073+ if let Some(minimum) = constraints.minimum {
10741074+ checks.push(quote! {
10751075+ #value_binding
10761076+ if *#value_expr < #minimum {
10771077+ errors.push(::jacquard_lexicon::schema::ValidationError::Minimum {
10781078+ field: #display_name,
10791079+ min: #minimum,
10801080+ actual: *#value_expr,
10811081+ });
10821082+ }
10831083+ });
10841084+ }
10851085+ }
10861086+ Some(LexiconPrimitiveType::Array(_)) => {
10871087+ if let Some(max_len) = constraints.max_length {
10881088+ checks.push(quote! {
10891089+ #value_binding
10901090+ if #value_expr.len() > #max_len {
10911091+ errors.push(::jacquard_lexicon::schema::ValidationError::MaxLength {
10921092+ field: #display_name,
10931093+ max: #max_len,
10941094+ actual: #value_expr.len(),
10951095+ });
10961096+ }
10971097+ });
10981098+ }
10991099+11001100+ if let Some(min_len) = constraints.min_length {
11011101+ checks.push(quote! {
11021102+ #value_binding
11031103+ if #value_expr.len() < #min_len {
11041104+ errors.push(::jacquard_lexicon::schema::ValidationError::MinLength {
11051105+ field: #display_name,
11061106+ min: #min_len,
11071107+ actual: #value_expr.len(),
11081108+ });
11091109+ }
11101110+ });
11111111+ }
11121112+ }
11131113+ _ => {
11141114+ // No built-in validation for this type
11151115+ }
11161116+ }
11171117+11181118+ Ok(checks)
11191119+}
11201120+11211121+/// Enum implementation (union support)
11221122+fn impl_for_enum(
11231123+ input: &DeriveInput,
11241124+ type_attrs: &LexiconTypeAttrs,
11251125+ nsid: &str,
11261126+ data_enum: &syn::DataEnum,
11271127+) -> syn::Result<TokenStream> {
11281128+ let name = &input.ident;
11291129+ let generics = &input.generics;
11301130+11311131+ // Detect lifetime
11321132+ let has_lifetime = generics.lifetimes().next().is_some();
11331133+ let lifetime = if has_lifetime {
11341134+ quote! { <'_> }
11351135+ } else {
11361136+ quote! {}
11371137+ };
11381138+11391139+ // Check if this is an open union (has #[open_union] attribute)
11401140+ let is_open = has_open_union_attr(&input.attrs);
11411141+11421142+ // Extract variant refs
11431143+ let mut refs = Vec::new();
11441144+ for variant in &data_enum.variants {
11451145+ // Skip Unknown variant (added by #[open_union] macro)
11461146+ if variant.ident == "Unknown" {
11471147+ continue;
11481148+ }
11491149+11501150+ // Get NSID for this variant
11511151+ let variant_ref = extract_variant_ref(variant, nsid)?;
11521152+ refs.push(variant_ref);
11531153+ }
11541154+11551155+ // Generate union def
11561156+ // Only set closed: true for explicitly closed unions (no #[open_union])
11571157+ // Open unions omit the field (defaults to open per spec)
11581158+ let closed_field = if !is_open {
11591159+ quote! { Some(true) }
11601160+ } else {
11611161+ quote! { None }
11621162+ };
11631163+11641164+ let user_type = quote! {
11651165+ ::jacquard_lexicon::lexicon::LexUserType::Union(
11661166+ ::jacquard_lexicon::lexicon::LexRefUnion {
11671167+ description: None,
11681168+ refs: vec![#(#refs.into()),*],
11691169+ closed: #closed_field,
11701170+ }
11711171+ )
11721172+ };
11731173+11741174+ Ok(quote! {
11751175+ impl #generics ::jacquard_lexicon::schema::LexiconSchema for #name #lifetime {
11761176+ fn nsid() -> &'static str {
11771177+ #nsid
11781178+ }
11791179+11801180+ fn schema_id() -> ::jacquard_common::CowStr<'static> {
11811181+ ::jacquard_common::CowStr::new_static(#nsid)
11821182+ }
11831183+11841184+ fn lexicon_doc(
11851185+ _generator: &mut ::jacquard_lexicon::schema::LexiconGenerator
11861186+ ) -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> {
11871187+ let mut defs = ::std::collections::BTreeMap::new();
11881188+ defs.insert("main".into(), #user_type);
11891189+11901190+ ::jacquard_lexicon::lexicon::LexiconDoc {
11911191+ lexicon: ::jacquard_lexicon::lexicon::Lexicon::Lexicon1,
11921192+ id: #nsid.into(),
11931193+ revision: None,
11941194+ description: None,
11951195+ defs,
11961196+ }
11971197+ }
11981198+ }
11991199+12001200+ ::inventory::submit! {
12011201+ ::jacquard_lexicon::schema::LexiconSchemaRef {
12021202+ nsid: #nsid,
12031203+ provider: || {
12041204+ let mut generator = ::jacquard_lexicon::schema::LexiconGenerator::new(#nsid);
12051205+ #name::lexicon_doc(&mut generator)
12061206+ },
12071207+ }
12081208+ }
12091209+ })
12101210+}
12111211+12121212+/// Check if type has #[open_union] attribute
12131213+fn has_open_union_attr(attrs: &[Attribute]) -> bool {
12141214+ attrs.iter().any(|attr| attr.path().is_ident("open_union"))
12151215+}
12161216+12171217+/// Extract NSID ref for a variant
12181218+fn extract_variant_ref(variant: &syn::Variant, base_nsid: &str) -> syn::Result<String> {
12191219+ // Priority 1: Check for #[nsid = "..."] attribute
12201220+ for attr in &variant.attrs {
12211221+ if attr.path().is_ident("nsid") {
12221222+ if let syn::Meta::NameValue(meta) = &attr.meta {
12231223+ if let syn::Expr::Lit(expr_lit) = &meta.value {
12241224+ if let syn::Lit::Str(lit_str) = &expr_lit.lit {
12251225+ return Ok(lit_str.value());
12261226+ }
12271227+ }
12281228+ }
12291229+ }
12301230+ }
12311231+12321232+ // Priority 2: Check for #[serde(rename = "...")] attribute
12331233+ for attr in &variant.attrs {
12341234+ if !attr.path().is_ident("serde") {
12351235+ continue;
12361236+ }
12371237+12381238+ let mut rename = None;
12391239+ let _ = attr.parse_nested_meta(|meta| {
12401240+ if meta.path.is_ident("rename") {
12411241+ let value = meta.value()?;
12421242+ let lit: LitStr = value.parse()?;
12431243+ rename = Some(lit.value());
12441244+ }
12451245+ Ok(())
12461246+ });
12471247+12481248+ if let Some(rename) = rename {
12491249+ return Ok(rename);
12501250+ }
12511251+ }
12521252+12531253+ // Priority 3: For variants with non-primitive inner types, error
12541254+ // (caller should use #[nsid] or type must impl LexiconSchema)
12551255+ match &variant.fields {
12561256+ Fields::Unit => {
12571257+ // Unit variant - generate fragment ref: baseNsid#variantName
12581258+ let variant_name = variant.ident.to_string().to_lower_camel_case();
12591259+ Ok(format!("{}#{}", base_nsid, variant_name))
12601260+ }
12611261+ Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
12621262+ let ty = &fields.unnamed.first().unwrap().ty;
12631263+12641264+ // Check if primitive - if so, error (unions need refs)
12651265+ if let Some(prim) = rust_type_to_lexicon_type(ty) {
12661266+ if is_primitive(&prim) {
12671267+ return Err(syn::Error::new_spanned(
12681268+ variant,
12691269+ "union variants with primitive inner types must use #[nsid] or #[serde(rename)] attribute",
12701270+ ));
12711271+ }
12721272+ }
12731273+12741274+ // Non-primitive - error, must have explicit attribute
12751275+ // (we can't call schema_id() at compile time)
12761276+ Err(syn::Error::new_spanned(
12771277+ variant,
12781278+ "union variants with non-primitive types must use #[nsid] or #[serde(rename)] attribute to specify the ref",
12791279+ ))
12801280+ }
12811281+ _ => Err(syn::Error::new_spanned(
12821282+ variant,
12831283+ "union variants must be unit variants or have single unnamed field",
12841284+ )),
12851285+ }
12861286+}
12871287+12881288+/// Check if a lexicon primitive type is actually a primitive (not a ref-able type)
12891289+fn is_primitive(prim: &LexiconPrimitiveType) -> bool {
12901290+ matches!(
12911291+ prim,
12921292+ LexiconPrimitiveType::Boolean
12931293+ | LexiconPrimitiveType::Integer
12941294+ | LexiconPrimitiveType::String(_)
12951295+ | LexiconPrimitiveType::Bytes
12961296+ | LexiconPrimitiveType::Unknown
12971297+ )
12981298+}
+2
crates/jacquard-lexicon/src/derive_impl/mod.rs
···66pub mod helpers;
77pub mod into_static;
88pub mod lexicon_attr;
99+pub mod lexicon_schema;
910pub mod open_union_attr;
1011pub mod xrpc_request;
11121213// Re-export the main entry points
1314pub use into_static::impl_derive_into_static;
1415pub use lexicon_attr::impl_lexicon;
1616+pub use lexicon_schema::impl_derive_lexicon_schema;
1517pub use open_union_attr::impl_open_union;
1618pub use xrpc_request::impl_derive_xrpc_request;
···7474 /// The schema ID for this type
7575 ///
7676 /// Defaults to NSID. Override for fragments to include `#fragment` suffix.
7777- fn schema_id() -> Cow<'static, str> {
7878- Cow::Borrowed(Self::nsid())
7777+ fn schema_id() -> jacquard_common::CowStr<'static> {
7878+ jacquard_common::CowStr::new_static(Self::nsid())
7979 }
80808181 /// Whether this type should be inlined vs referenced
···308308 #[error("invalid NSID: {nsid}")]
309309 InvalidNsid { nsid: String },
310310}
311311+312312+/// Registry entry for schema discovery via inventory
313313+///
314314+/// Generated automatically by `#[derive(LexiconSchema)]` to enable runtime schema discovery.
315315+/// Phase 3 will use this to extract all schemas from a binary.
316316+pub struct LexiconSchemaRef {
317317+ /// The NSID for this schema
318318+ pub nsid: &'static str,
319319+ /// Function that generates the lexicon document
320320+ pub provider: fn() -> crate::lexicon::LexiconDoc<'static>,
321321+}
322322+323323+inventory::collect!(LexiconSchemaRef);
311324312325#[cfg(test)]
313326mod tests {