···11//! Implementation of #[derive(LexiconSchema)] macro
2233-use crate::schema::from_ast::{
44- LexiconFieldAttrs, LexiconTypeAttrs, LexiconTypeKind, RenameRule, SerdeAttrs, determine_nsid,
55- extract_option_inner, parse_field_attrs, parse_serde_attrs, parse_serde_rename_all,
66- parse_type_attrs,
77-};
88-use crate::schema::type_mapping::{LexiconPrimitiveType, StringFormat, rust_type_to_lexicon_type};
99-use heck::{ToKebabCase, ToLowerCamelCase, ToPascalCase, ToShoutySnakeCase, ToSnakeCase};
103use proc_macro2::TokenStream;
114use quote::quote;
1212-use syn::{Attribute, Data, DeriveInput, Fields, Ident, LitStr, Type, parse2};
55+use syn::{Data, DeriveInput, parse2};
136147/// Implementation for the LexiconSchema derive macro
158pub fn impl_derive_lexicon_schema(input: TokenStream) -> TokenStream {
···2518}
26192720fn 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-3421 // Generate based on data type
3522 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),
2323+ Data::Struct(_) => impl_for_struct(input),
2424+ Data::Enum(_) => impl_for_enum(input),
3825 Data::Union(_) => Err(syn::Error::new_spanned(
3926 input,
4027 "LexiconSchema cannot be derived for unions",
···4229 }
4330}
44314545-/// Extract NSID from XrpcRequest attributes (cross-derive coordination)
4646-fn extract_xrpc_nsid(attrs: &[Attribute]) -> syn::Result<Option<String>> {
4747- for attr in attrs {
4848- if !attr.path().is_ident("xrpc") {
4949- continue;
5050- }
5151-5252- let mut nsid = None;
5353- attr.parse_nested_meta(|meta| {
5454- if meta.path.is_ident("nsid") {
5555- let value = meta.value()?;
5656- let lit: LitStr = value.parse()?;
5757- nsid = Some(lit.value());
5858- }
5959- Ok(())
6060- })?;
6161-6262- if let Some(nsid) = nsid {
6363- return Ok(Some(nsid));
6464- }
6565- }
6666- Ok(None)
6767-}
6868-6932/// Struct implementation
7070-fn impl_for_struct(
7171- input: &DeriveInput,
7272- type_attrs: &LexiconTypeAttrs,
7373- nsid: &str,
7474- data_struct: &syn::DataStruct,
7575-) -> syn::Result<TokenStream> {
3333+fn impl_for_struct(input: &DeriveInput) -> syn::Result<TokenStream> {
7634 let name = &input.ident;
7735 let generics = &input.generics;
7836···8442 quote! {}
8543 };
86448787- // Parse fields
8888- let fields = match &data_struct.fields {
8989- Fields::Named(fields) => &fields.named,
9090- _ => {
9191- return Err(syn::Error::new_spanned(
9292- input,
9393- "LexiconSchema only supports structs with named fields",
9494- ));
9595- }
9696- };
4545+ // Use schema builder to get actual data
4646+ let built = crate::schema::from_ast::build_struct_schema(input)?;
97479898- // Parse serde container attributes (defaults to camelCase)
9999- let rename_all = parse_serde_rename_all(&input.attrs)?;
100100-101101- // Generate field definitions
102102- let field_defs = generate_field_definitions(fields, rename_all)?;
103103-104104- // Generate validation code
105105- let validation_code = generate_validation(fields, rename_all)?;
4848+ // Convert to tokens for code generation
4949+ let doc_tokens = super::doc_to_tokens::doc_to_tokens(&built.doc);
5050+ let validation_tokens = super::doc_to_tokens::validations_to_tokens(&built.validation_checks);
10651107107- // Build lexicon_doc() implementation
108108- let doc_impl = generate_doc_impl(nsid, type_attrs, &field_defs)?;
109109-110110- // Determine schema_id (add fragment suffix if needed)
111111- let schema_id = if let Some(fragment) = &type_attrs.fragment {
112112- let frag_name = if fragment.is_empty() {
113113- // Infer from type name
114114- name.to_string().to_lower_camel_case()
115115- } else {
116116- fragment.clone()
117117- };
118118- quote! {
119119- format_smolstr!("{}#{}", #nsid, #frag_name).to_string()
120120- }
5252+ let nsid = &built.nsid;
5353+ let schema_id_expr = if built.schema_id != built.nsid {
5454+ let sid = &built.schema_id;
5555+ quote! { ::jacquard_common::CowStr::from(#sid) }
12156 } else {
122122- quote! {
123123- ::jacquard_common::CowStr::new_static(#nsid)
124124- }
5757+ quote! { ::jacquard_common::CowStr::new_static(#nsid) }
12558 };
1265912760 // Generate trait impl
···13265 }
1336613467 fn schema_id() -> ::jacquard_common::CowStr<'static> {
135135- #schema_id
6868+ #schema_id_expr
13669 }
1377013871 fn lexicon_doc(
139139- generator: &mut ::jacquard_lexicon::schema::LexiconGenerator
7272+ _generator: &mut ::jacquard_lexicon::schema::LexiconGenerator
14073 ) -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> {
141141- #doc_impl
7474+ #doc_tokens
14275 }
1437614477 fn validate(&self) -> ::std::result::Result<(), ::jacquard_lexicon::schema::ValidationError> {
145145- #validation_code
7878+ #validation_tokens
14679 }
14780 }
14881149149- // Generate inventory submission for Phase 3 discovery
8282+ // Generate inventory submission for workspace discovery
15083 ::inventory::submit! {
15184 ::jacquard_lexicon::schema::LexiconSchemaRef {
15285 nsid: #nsid,
···15992 })
16093}
16194162162-struct FieldDef {
163163- name: String, // Rust field name
164164- schema_name: String, // JSON field name (after serde rename)
165165- rust_type: Type, // Rust type
166166- lex_type: TokenStream, // LexObjectProperty tokens
167167- required: bool,
168168-}
169169-170170-fn generate_field_definitions(
171171- fields: &syn::punctuated::Punctuated<syn::Field, syn::Token![,]>,
172172- rename_all: Option<RenameRule>,
173173-) -> syn::Result<Vec<FieldDef>> {
174174- let mut defs = Vec::new();
175175-176176- for field in fields {
177177- let field_name = field.ident.as_ref().unwrap().to_string();
178178-179179- // Skip extra_data field (added by #[lexicon] attribute macro)
180180- if field_name == "extra_data" {
181181- continue;
182182- }
183183-184184- // Parse attributes
185185- let serde_attrs = parse_serde_attrs(&field.attrs)?;
186186- let lex_attrs = parse_field_attrs(&field.attrs)?;
187187-188188- // Skip if serde(skip)
189189- if serde_attrs.skip {
190190- continue;
191191- }
192192-193193- // Determine schema name
194194- let schema_name = if let Some(rename) = serde_attrs.rename {
195195- rename
196196- } else if let Some(rule) = rename_all {
197197- rule.apply(&field_name)
198198- } else {
199199- field_name.clone()
200200- };
201201-202202- // Determine if required (Option<T> = optional)
203203- let (inner_type, required) = extract_option_inner(&field.ty);
204204- let rust_type = inner_type.clone();
205205-206206- // Generate LexObjectProperty based on type + constraints
207207- let lex_type = generate_lex_property(&rust_type, &lex_attrs)?;
208208-209209- defs.push(FieldDef {
210210- name: field_name,
211211- schema_name,
212212- rust_type,
213213- lex_type,
214214- required,
215215- });
216216- }
217217-218218- Ok(defs)
219219-}
220220-221221-/// Generate LexObjectProperty tokens for a field
222222-fn generate_lex_property(
223223- rust_type: &Type,
224224- constraints: &LexiconFieldAttrs,
225225-) -> syn::Result<TokenStream> {
226226- // Try to detect primitive type
227227- let lex_type = rust_type_to_lexicon_type(rust_type);
228228-229229- match lex_type {
230230- Some(LexiconPrimitiveType::Boolean) => Ok(quote! {
231231- ::jacquard_lexicon::lexicon::LexObjectProperty::Boolean(
232232- ::jacquard_lexicon::lexicon::LexBoolean {
233233- description: None,
234234- default: None,
235235- r#const: None,
236236- }
237237- )
238238- }),
239239- Some(LexiconPrimitiveType::Integer) => {
240240- let minimum = constraints
241241- .minimum
242242- .map(|v| quote! { Some(#v) })
243243- .unwrap_or(quote! { None });
244244- let maximum = constraints
245245- .maximum
246246- .map(|v| quote! { Some(#v) })
247247- .unwrap_or(quote! { None });
248248-249249- Ok(quote! {
250250- ::jacquard_lexicon::lexicon::LexObjectProperty::Integer(
251251- ::jacquard_lexicon::lexicon::LexInteger {
252252- description: None,
253253- default: None,
254254- minimum: #minimum,
255255- maximum: #maximum,
256256- r#enum: None,
257257- r#const: None,
258258- }
259259- )
260260- })
261261- }
262262- Some(LexiconPrimitiveType::String(format)) => generate_string_property(format, constraints),
263263- Some(LexiconPrimitiveType::Bytes) => {
264264- let max_length = constraints
265265- .max_length
266266- .map(|v| quote! { Some(#v) })
267267- .unwrap_or(quote! { None });
268268- let min_length = constraints
269269- .min_length
270270- .map(|v| quote! { Some(#v) })
271271- .unwrap_or(quote! { None });
272272-273273- Ok(quote! {
274274- ::jacquard_lexicon::lexicon::LexObjectProperty::Bytes(
275275- ::jacquard_lexicon::lexicon::LexBytes {
276276- description: None,
277277- max_length: #max_length,
278278- min_length: #min_length,
279279- }
280280- )
281281- })
282282- }
283283- Some(LexiconPrimitiveType::CidLink) => Ok(quote! {
284284- ::jacquard_lexicon::lexicon::LexObjectProperty::CidLink(
285285- ::jacquard_lexicon::lexicon::LexCidLink {
286286- description: None,
287287- }
288288- )
289289- }),
290290- Some(LexiconPrimitiveType::Blob) => Ok(quote! {
291291- ::jacquard_lexicon::lexicon::LexObjectProperty::Blob(
292292- ::jacquard_lexicon::lexicon::LexBlob {
293293- description: None,
294294- accept: None,
295295- max_size: None,
296296- }
297297- )
298298- }),
299299- Some(LexiconPrimitiveType::Unknown) => Ok(quote! {
300300- ::jacquard_lexicon::lexicon::LexObjectProperty::Unknown(
301301- ::jacquard_lexicon::lexicon::LexUnknown {
302302- description: None,
303303- }
304304- )
305305- }),
306306- Some(LexiconPrimitiveType::Array(item_type)) => {
307307- let item_prop = generate_array_item(*item_type, constraints)?;
308308- let max_length = constraints
309309- .max_length
310310- .map(|v| quote! { Some(#v) })
311311- .unwrap_or(quote! { None });
312312- let min_length = constraints
313313- .min_length
314314- .map(|v| quote! { Some(#v) })
315315- .unwrap_or(quote! { None });
316316-317317- Ok(quote! {
318318- ::jacquard_lexicon::lexicon::LexObjectProperty::Array(
319319- ::jacquard_lexicon::lexicon::LexArray {
320320- description: None,
321321- items: #item_prop,
322322- min_length: #min_length,
323323- max_length: #max_length,
324324- }
325325- )
326326- })
327327- }
328328- None => {
329329- // Not a recognized primitive - check for explicit ref or trait bound
330330- if let Some(ref_nsid) = &constraints.explicit_ref {
331331- Ok(quote! {
332332- ::jacquard_lexicon::lexicon::LexObjectProperty::Ref(
333333- ::jacquard_lexicon::lexicon::LexRef {
334334- description: None,
335335- r#ref: #ref_nsid.into(),
336336- }
337337- )
338338- })
339339- } else {
340340- // Try to use type's LexiconSchema impl
341341- Ok(quote! {
342342- {
343343- // Use the type's schema_id method
344344- let ref_nsid = <#rust_type as ::jacquard_lexicon::schema::LexiconSchema>::schema_id();
345345- ::jacquard_lexicon::lexicon::LexObjectProperty::Ref(
346346- ::jacquard_lexicon::lexicon::LexRef {
347347- description: None,
348348- r#ref: ref_nsid.to_string().into(),
349349- }
350350- )
351351- }
352352- })
353353- }
354354- }
355355- _ => Err(syn::Error::new_spanned(
356356- rust_type,
357357- "unsupported type for lexicon schema generation",
358358- )),
359359- }
360360-}
361361-362362-fn generate_array_item(
363363- item_type: LexiconPrimitiveType,
364364- _constraints: &LexiconFieldAttrs,
365365-) -> syn::Result<TokenStream> {
366366- match item_type {
367367- LexiconPrimitiveType::String(format) => {
368368- let format_token = string_format_token(format);
369369- Ok(quote! {
370370- ::jacquard_lexicon::lexicon::LexArrayItem::String(
371371- ::jacquard_lexicon::lexicon::LexString {
372372- description: None,
373373- format: #format_token,
374374- default: None,
375375- min_length: None,
376376- max_length: None,
377377- min_graphemes: None,
378378- max_graphemes: None,
379379- r#enum: None,
380380- r#const: None,
381381- known_values: None,
382382- }
383383- )
384384- })
385385- }
386386- LexiconPrimitiveType::Integer => Ok(quote! {
387387- ::jacquard_lexicon::lexicon::LexArrayItem::Integer(
388388- ::jacquard_lexicon::lexicon::LexInteger {
389389- description: None,
390390- default: None,
391391- minimum: None,
392392- maximum: None,
393393- r#enum: None,
394394- r#const: None,
395395- }
396396- )
397397- }),
398398- _ => Ok(quote! {
399399- ::jacquard_lexicon::lexicon::LexArrayItem::Unknown(
400400- ::jacquard_lexicon::lexicon::LexUnknown {
401401- description: None,
402402- }
403403- )
404404- }),
405405- }
406406-}
407407-408408-fn generate_string_property(
409409- format: StringFormat,
410410- constraints: &LexiconFieldAttrs,
411411-) -> syn::Result<TokenStream> {
412412- let format_token = string_format_token(format);
413413-414414- let max_length = constraints
415415- .max_length
416416- .map(|v| quote! { Some(#v) })
417417- .unwrap_or(quote! { None });
418418- let max_graphemes = constraints
419419- .max_graphemes
420420- .map(|v| quote! { Some(#v) })
421421- .unwrap_or(quote! { None });
422422- let min_length = constraints
423423- .min_length
424424- .map(|v| quote! { Some(#v) })
425425- .unwrap_or(quote! { None });
426426- let min_graphemes = constraints
427427- .min_graphemes
428428- .map(|v| quote! { Some(#v) })
429429- .unwrap_or(quote! { None });
430430-431431- Ok(quote! {
432432- ::jacquard_lexicon::lexicon::LexObjectProperty::String(
433433- ::jacquard_lexicon::lexicon::LexString {
434434- description: None,
435435- format: #format_token,
436436- default: None,
437437- min_length: #min_length,
438438- max_length: #max_length,
439439- min_graphemes: #min_graphemes,
440440- max_graphemes: #max_graphemes,
441441- r#enum: None,
442442- r#const: None,
443443- known_values: None,
444444- }
445445- )
446446- })
447447-}
448448-449449-fn string_format_token(format: StringFormat) -> TokenStream {
450450- match format {
451451- StringFormat::Plain => quote! { None },
452452- StringFormat::Did => {
453453- quote! { Some(::jacquard_lexicon::lexicon::LexStringFormat::Did) }
454454- }
455455- StringFormat::Handle => {
456456- quote! { Some(::jacquard_lexicon::lexicon::LexStringFormat::Handle) }
457457- }
458458- StringFormat::AtUri => {
459459- quote! { Some(::jacquard_lexicon::lexicon::LexStringFormat::AtUri) }
460460- }
461461- StringFormat::Nsid => {
462462- quote! { Some(::jacquard_lexicon::lexicon::LexStringFormat::Nsid) }
463463- }
464464- StringFormat::Cid => {
465465- quote! { Some(::jacquard_lexicon::lexicon::LexStringFormat::Cid) }
466466- }
467467- StringFormat::Datetime => {
468468- quote! { Some(::jacquard_lexicon::lexicon::LexStringFormat::Datetime) }
469469- }
470470- StringFormat::Language => {
471471- quote! { Some(::jacquard_lexicon::lexicon::LexStringFormat::Language) }
472472- }
473473- StringFormat::Tid => {
474474- quote! { Some(::jacquard_lexicon::lexicon::LexStringFormat::Tid) }
475475- }
476476- StringFormat::RecordKey => {
477477- quote! { Some(::jacquard_lexicon::lexicon::LexStringFormat::RecordKey) }
478478- }
479479- StringFormat::AtIdentifier => {
480480- quote! { Some(::jacquard_lexicon::lexicon::LexStringFormat::AtIdentifier) }
481481- }
482482- StringFormat::Uri => {
483483- quote! { Some(::jacquard_lexicon::lexicon::LexStringFormat::Uri) }
484484- }
485485- }
486486-}
487487-488488-fn generate_doc_impl(
489489- nsid: &str,
490490- type_attrs: &LexiconTypeAttrs,
491491- field_defs: &[FieldDef],
492492-) -> syn::Result<TokenStream> {
493493- // Build properties map
494494- let properties: Vec<_> = field_defs
495495- .iter()
496496- .map(|def| {
497497- let name = &def.schema_name;
498498- let lex_type = &def.lex_type;
499499- quote! {
500500- (#name.into(), #lex_type)
501501- }
502502- })
503503- .collect();
504504-505505- // Build required array
506506- let required: Vec<_> = field_defs
507507- .iter()
508508- .filter(|def| def.required)
509509- .map(|def| {
510510- let name = &def.schema_name;
511511- quote! { #name.into() }
512512- })
513513- .collect();
514514-515515- let required_field = if required.is_empty() {
516516- quote! { None }
517517- } else {
518518- quote! { Some(vec![#(#required),*]) }
519519- };
520520-521521- // Determine user type based on kind
522522- let user_type = match type_attrs.kind {
523523- Some(LexiconTypeKind::Record) => {
524524- let key = type_attrs
525525- .key
526526- .as_ref()
527527- .map(|k| quote! { Some(#k.into()) })
528528- .unwrap_or(quote! { None });
529529-530530- quote! {
531531- ::jacquard_lexicon::lexicon::LexUserType::Record(
532532- ::jacquard_lexicon::lexicon::LexRecord {
533533- description: None,
534534- key: #key,
535535- record: ::jacquard_lexicon::lexicon::LexRecordRecord::Object(
536536- ::jacquard_lexicon::lexicon::LexObject {
537537- description: None,
538538- required: #required_field,
539539- nullable: None,
540540- properties: [#(#properties),*].into(),
541541- }
542542- ),
543543- }
544544- )
545545- }
546546- }
547547- Some(LexiconTypeKind::Query) => {
548548- quote! {
549549- ::jacquard_lexicon::lexicon::LexUserType::Query(
550550- ::jacquard_lexicon::lexicon::LexQuery {
551551- description: None,
552552- parameters: Some(::jacquard_lexicon::lexicon::LexObject {
553553- description: None,
554554- required: #required_field,
555555- nullable: None,
556556- properties: [#(#properties),*].into(),
557557- }),
558558- output: None,
559559- errors: None,
560560- }
561561- )
562562- }
563563- }
564564- Some(LexiconTypeKind::Procedure) => {
565565- quote! {
566566- ::jacquard_lexicon::lexicon::LexUserType::Procedure(
567567- ::jacquard_lexicon::lexicon::LexProcedure {
568568- description: None,
569569- input: Some(::jacquard_lexicon::lexicon::LexProcedureIO {
570570- description: None,
571571- encoding: "application/json".into(),
572572- schema: Some(::jacquard_lexicon::lexicon::LexProcedureSchema::Object(
573573- ::jacquard_lexicon::lexicon::LexObject {
574574- description: None,
575575- required: #required_field,
576576- nullable: None,
577577- properties: [#(#properties),*].into(),
578578- }
579579- )),
580580- }),
581581- output: None,
582582- errors: None,
583583- }
584584- )
585585- }
586586- }
587587- _ => {
588588- // Default: Object type
589589- quote! {
590590- ::jacquard_lexicon::lexicon::LexUserType::Object(
591591- ::jacquard_lexicon::lexicon::LexObject {
592592- description: None,
593593- required: #required_field,
594594- nullable: None,
595595- properties: [#(#properties),*].into(),
596596- }
597597- )
598598- }
599599- }
600600- };
601601-602602- Ok(quote! {
603603- {
604604- let mut defs = ::std::collections::BTreeMap::new();
605605- defs.insert("main".into(), #user_type);
606606-607607- ::jacquard_lexicon::lexicon::LexiconDoc {
608608- lexicon: ::jacquard_lexicon::lexicon::Lexicon::Lexicon1,
609609- id: #nsid.into(),
610610- revision: None,
611611- description: None,
612612- defs,
613613- }
614614- }
615615- })
616616-}
617617-618618-fn generate_validation(
619619- fields: &syn::punctuated::Punctuated<syn::Field, syn::Token![,]>,
620620- rename_all: Option<RenameRule>,
621621-) -> syn::Result<TokenStream> {
622622- let mut checks = Vec::new();
623623-624624- for field in fields {
625625- let field_name = field.ident.as_ref().unwrap();
626626- let field_name_str = field_name.to_string();
627627-628628- // Skip extra_data
629629- if field_name_str == "extra_data" {
630630- continue;
631631- }
632632-633633- let lex_attrs = parse_field_attrs(&field.attrs)?;
634634- let serde_attrs = parse_serde_attrs(&field.attrs)?;
635635-636636- if serde_attrs.skip {
637637- continue;
638638- }
639639-640640- // Get actual field name for errors
641641- let display_name = if let Some(rename) = serde_attrs.rename {
642642- rename
643643- } else if let Some(rule) = rename_all {
644644- rule.apply(&field_name_str)
645645- } else {
646646- field_name_str.clone()
647647- };
648648-649649- // Extract inner type if Option
650650- let (inner_type, is_required) = extract_option_inner(&field.ty);
651651-652652- // Generate checks based on type and constraints
653653- let field_checks = generate_field_validation(
654654- field_name,
655655- &display_name,
656656- inner_type,
657657- is_required,
658658- &lex_attrs,
659659- )?;
660660-661661- checks.extend(field_checks);
662662- }
663663-664664- if checks.is_empty() {
665665- Ok(quote! { Ok(()) })
666666- } else {
667667- Ok(quote! {
668668- let mut errors = Vec::new();
669669-670670- #(#checks)*
671671-672672- if errors.is_empty() {
673673- Ok(())
674674- } else if errors.len() == 1 {
675675- Err(errors.into_iter().next().unwrap())
676676- } else {
677677- Err(::jacquard_lexicon::schema::ValidationError::Multiple(errors))
678678- }
679679- })
680680- }
681681-}
682682-683683-fn generate_field_validation(
684684- field_ident: &Ident,
685685- display_name: &str,
686686- field_type: &Type,
687687- is_required: bool,
688688- constraints: &LexiconFieldAttrs,
689689-) -> syn::Result<Vec<TokenStream>> {
690690- let mut checks = Vec::new();
691691-692692- // Determine base type
693693- let lex_type = rust_type_to_lexicon_type(field_type);
694694-695695- // Build accessor for the field value
696696- let (value_binding, value_expr) = if is_required {
697697- (quote! { let value = &self.#field_ident; }, quote! { value })
698698- } else {
699699- (
700700- quote! {},
701701- quote! {
702702- match &self.#field_ident {
703703- Some(v) => v,
704704- None => continue,
705705- }
706706- },
707707- )
708708- };
709709-710710- match lex_type {
711711- Some(LexiconPrimitiveType::String(_)) => {
712712- // String constraints
713713- if let Some(max_len) = constraints.max_length {
714714- checks.push(quote! {
715715- #value_binding
716716- if #value_expr.len() > #max_len {
717717- errors.push(::jacquard_lexicon::schema::ValidationError::MaxLength {
718718- field: #display_name,
719719- max: #max_len,
720720- actual: #value_expr.len(),
721721- });
722722- }
723723- });
724724- }
725725-726726- if let Some(max_graphemes) = constraints.max_graphemes {
727727- checks.push(quote! {
728728- #value_binding
729729- let count = ::unicode_segmentation::UnicodeSegmentation::graphemes(
730730- #value_expr.as_ref(),
731731- true
732732- ).count();
733733- if count > #max_graphemes {
734734- errors.push(::jacquard_lexicon::schema::ValidationError::MaxGraphemes {
735735- field: #display_name,
736736- max: #max_graphemes,
737737- actual: count,
738738- });
739739- }
740740- });
741741- }
742742-743743- if let Some(min_len) = constraints.min_length {
744744- checks.push(quote! {
745745- #value_binding
746746- if #value_expr.len() < #min_len {
747747- errors.push(::jacquard_lexicon::schema::ValidationError::MinLength {
748748- field: #display_name,
749749- min: #min_len,
750750- actual: #value_expr.len(),
751751- });
752752- }
753753- });
754754- }
755755-756756- if let Some(min_graphemes) = constraints.min_graphemes {
757757- checks.push(quote! {
758758- #value_binding
759759- let count = ::unicode_segmentation::UnicodeSegmentation::graphemes(
760760- #value_expr.as_ref(),
761761- true
762762- ).count();
763763- if count < #min_graphemes {
764764- errors.push(::jacquard_lexicon::schema::ValidationError::MinGraphemes {
765765- field: #display_name,
766766- min: #min_graphemes,
767767- actual: count,
768768- });
769769- }
770770- });
771771- }
772772- }
773773- Some(LexiconPrimitiveType::Integer) => {
774774- if let Some(maximum) = constraints.maximum {
775775- checks.push(quote! {
776776- #value_binding
777777- if *#value_expr > #maximum {
778778- errors.push(::jacquard_lexicon::schema::ValidationError::Maximum {
779779- field: #display_name,
780780- max: #maximum,
781781- actual: *#value_expr,
782782- });
783783- }
784784- });
785785- }
786786-787787- if let Some(minimum) = constraints.minimum {
788788- checks.push(quote! {
789789- #value_binding
790790- if *#value_expr < #minimum {
791791- errors.push(::jacquard_lexicon::schema::ValidationError::Minimum {
792792- field: #display_name,
793793- min: #minimum,
794794- actual: *#value_expr,
795795- });
796796- }
797797- });
798798- }
799799- }
800800- Some(LexiconPrimitiveType::Array(_)) => {
801801- if let Some(max_len) = constraints.max_length {
802802- checks.push(quote! {
803803- #value_binding
804804- if #value_expr.len() > #max_len {
805805- errors.push(::jacquard_lexicon::schema::ValidationError::MaxLength {
806806- field: #display_name,
807807- max: #max_len,
808808- actual: #value_expr.len(),
809809- });
810810- }
811811- });
812812- }
813813-814814- if let Some(min_len) = constraints.min_length {
815815- checks.push(quote! {
816816- #value_binding
817817- if #value_expr.len() < #min_len {
818818- errors.push(::jacquard_lexicon::schema::ValidationError::MinLength {
819819- field: #display_name,
820820- min: #min_len,
821821- actual: #value_expr.len(),
822822- });
823823- }
824824- });
825825- }
826826- }
827827- _ => {
828828- // No built-in validation for this type
829829- }
830830- }
831831-832832- Ok(checks)
833833-}
834834-83595/// Enum implementation (union support)
836836-fn impl_for_enum(
837837- input: &DeriveInput,
838838- type_attrs: &LexiconTypeAttrs,
839839- nsid: &str,
840840- data_enum: &syn::DataEnum,
841841-) -> syn::Result<TokenStream> {
9696+fn impl_for_enum(input: &DeriveInput) -> syn::Result<TokenStream> {
84297 let name = &input.ident;
84398 let generics = &input.generics;
84499···850105 quote! {}
851106 };
852107853853- // Check if this is an open union (has #[open_union] attribute)
854854- let is_open = has_open_union_attr(&input.attrs);
855855-856856- // Extract variant refs
857857- let mut refs = Vec::new();
858858- for variant in &data_enum.variants {
859859- // Skip Unknown variant (added by #[open_union] macro)
860860- if variant.ident == "Unknown" {
861861- continue;
862862- }
863863-864864- // Get NSID for this variant
865865- let variant_ref = extract_variant_ref(variant, nsid)?;
866866- refs.push(variant_ref);
867867- }
108108+ // Use schema builder to get actual data
109109+ let built = crate::schema::from_ast::build_enum_schema(input)?;
868110869869- // Generate union def
870870- // Only set closed: true for explicitly closed unions (no #[open_union])
871871- // Open unions omit the field (defaults to open per spec)
872872- let closed_field = if !is_open {
873873- quote! { Some(true) }
874874- } else {
875875- quote! { None }
876876- };
111111+ // Convert to tokens for code generation
112112+ let doc_tokens = super::doc_to_tokens::doc_to_tokens(&built.doc);
877113878878- let user_type = quote! {
879879- ::jacquard_lexicon::lexicon::LexUserType::Union(
880880- ::jacquard_lexicon::lexicon::LexRefUnion {
881881- description: None,
882882- refs: vec![#(#refs.into()),*],
883883- closed: #closed_field,
884884- }
885885- )
886886- };
114114+ let nsid = &built.nsid;
887115888116 Ok(quote! {
889117 impl #generics ::jacquard_lexicon::schema::LexiconSchema for #name #lifetime {
···898126 fn lexicon_doc(
899127 _generator: &mut ::jacquard_lexicon::schema::LexiconGenerator
900128 ) -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> {
901901- let mut defs = ::std::collections::BTreeMap::new();
902902- defs.insert("main".into(), #user_type);
129129+ #doc_tokens
130130+ }
903131904904- ::jacquard_lexicon::lexicon::LexiconDoc {
905905- lexicon: ::jacquard_lexicon::lexicon::Lexicon::Lexicon1,
906906- id: #nsid.into(),
907907- revision: None,
908908- description: None,
909909- defs,
910910- }
132132+ fn validate(&self) -> ::std::result::Result<(), ::jacquard_lexicon::schema::ValidationError> {
133133+ Ok(())
911134 }
912135 }
913136···922145 }
923146 })
924147}
925925-926926-/// Check if type has #[open_union] attribute
927927-fn has_open_union_attr(attrs: &[Attribute]) -> bool {
928928- attrs.iter().any(|attr| attr.path().is_ident("open_union"))
929929-}
930930-931931-/// Extract NSID ref for a variant
932932-fn extract_variant_ref(variant: &syn::Variant, base_nsid: &str) -> syn::Result<String> {
933933- // Priority 1: Check for #[nsid = "..."] attribute
934934- for attr in &variant.attrs {
935935- if attr.path().is_ident("nsid") {
936936- if let syn::Meta::NameValue(meta) = &attr.meta {
937937- if let syn::Expr::Lit(expr_lit) = &meta.value {
938938- if let syn::Lit::Str(lit_str) = &expr_lit.lit {
939939- return Ok(lit_str.value());
940940- }
941941- }
942942- }
943943- }
944944- }
945945-946946- // Priority 2: Check for #[serde(rename = "...")] attribute
947947- for attr in &variant.attrs {
948948- if !attr.path().is_ident("serde") {
949949- continue;
950950- }
951951-952952- let mut rename = None;
953953- let _ = attr.parse_nested_meta(|meta| {
954954- if meta.path.is_ident("rename") {
955955- let value = meta.value()?;
956956- let lit: LitStr = value.parse()?;
957957- rename = Some(lit.value());
958958- }
959959- Ok(())
960960- });
961961-962962- if let Some(rename) = rename {
963963- return Ok(rename);
964964- }
965965- }
966966-967967- // Priority 3: For variants with non-primitive inner types, error
968968- // (caller should use #[nsid] or type must impl LexiconSchema)
969969- match &variant.fields {
970970- Fields::Unit => {
971971- // Unit variant - generate fragment ref: baseNsid#variantName
972972- let variant_name = variant.ident.to_string().to_lower_camel_case();
973973- Ok(format!("{}#{}", base_nsid, variant_name))
974974- }
975975- Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
976976- let ty = &fields.unnamed.first().unwrap().ty;
977977-978978- // Check if primitive - if so, error (unions need refs)
979979- if let Some(prim) = rust_type_to_lexicon_type(ty) {
980980- if is_primitive(&prim) {
981981- return Err(syn::Error::new_spanned(
982982- variant,
983983- "union variants with primitive inner types must use #[nsid] or #[serde(rename)] attribute",
984984- ));
985985- }
986986- }
987987-988988- // Non-primitive - error, must have explicit attribute
989989- // (we can't call schema_id() at compile time)
990990- Err(syn::Error::new_spanned(
991991- variant,
992992- "union variants with non-primitive types must use #[nsid] or #[serde(rename)] attribute to specify the ref",
993993- ))
994994- }
995995- _ => Err(syn::Error::new_spanned(
996996- variant,
997997- "union variants must be unit variants or have single unnamed field",
998998- )),
999999- }
10001000-}
10011001-10021002-/// Check if a lexicon primitive type is actually a primitive (not a ref-able type)
10031003-fn is_primitive(prim: &LexiconPrimitiveType) -> bool {
10041004- matches!(
10051005- prim,
10061006- LexiconPrimitiveType::Boolean
10071007- | LexiconPrimitiveType::Integer
10081008- | LexiconPrimitiveType::String(_)
10091009- | LexiconPrimitiveType::Bytes
10101010- | LexiconPrimitiveType::Unknown
10111011- )
10121012-}
+1
crates/jacquard-lexicon/src/derive_impl/mod.rs
···33//! These functions are used by the `jacquard-derive` proc-macro crate but are also
44//! available for runtime code generation in `jacquard-lexicon`.
5566+mod doc_to_tokens;
67pub mod helpers;
78pub mod into_static;
89pub mod lexicon_attr;