A better Rust ATProto crate

rework schema derive

Orual 92bfd6b5 16691f29

+713 -894
+683
crates/jacquard-lexicon/src/derive_impl/doc_to_tokens.rs
··· 1 + //! Serialize LexiconDoc values to TokenStream for macro codegen 2 + 3 + use crate::lexicon::*; 4 + use crate::schema::from_ast::{ConstraintCheck, ValidationCheck}; 5 + use jacquard_common::smol_str::SmolStr; 6 + use proc_macro2::TokenStream; 7 + use quote::quote; 8 + use std::collections::BTreeMap; 9 + 10 + /// Convert LexiconDoc to TokenStream for compile-time codegen 11 + pub fn doc_to_tokens(doc: &LexiconDoc) -> TokenStream { 12 + let id = doc.id.as_ref(); 13 + let defs_tokens = defs_map_to_tokens(&doc.defs); 14 + 15 + quote! { 16 + ::jacquard_lexicon::lexicon::LexiconDoc { 17 + lexicon: ::jacquard_lexicon::lexicon::Lexicon::Lexicon1, 18 + id: #id.into(), 19 + revision: None, 20 + description: None, 21 + defs: #defs_tokens, 22 + } 23 + } 24 + } 25 + 26 + /// Convert defs BTreeMap to tokens 27 + fn defs_map_to_tokens(defs: &BTreeMap<SmolStr, LexUserType>) -> TokenStream { 28 + let def_entries: Vec<_> = defs 29 + .iter() 30 + .map(|(name, def)| { 31 + let name_str = name.as_str(); 32 + let def_tokens = user_type_to_tokens(def); 33 + quote! { map.insert(#name_str.into(), #def_tokens) } 34 + }) 35 + .collect(); 36 + 37 + quote! { 38 + { 39 + let mut map = ::std::collections::BTreeMap::new(); 40 + #(#def_entries;)* 41 + map 42 + } 43 + } 44 + } 45 + 46 + /// Convert LexUserType to tokens 47 + fn user_type_to_tokens(ut: &LexUserType) -> TokenStream { 48 + match ut { 49 + LexUserType::Record(rec) => { 50 + let key = option_cow_str_to_tokens(&rec.key); 51 + let record_tokens = match &rec.record { 52 + LexRecordRecord::Object(obj) => { 53 + let obj_tokens = object_to_tokens(obj); 54 + quote! { 55 + ::jacquard_lexicon::lexicon::LexRecordRecord::Object(#obj_tokens) 56 + } 57 + } 58 + }; 59 + quote! { 60 + ::jacquard_lexicon::lexicon::LexUserType::Record( 61 + ::jacquard_lexicon::lexicon::LexRecord { 62 + description: None, 63 + key: #key, 64 + record: #record_tokens, 65 + } 66 + ) 67 + } 68 + } 69 + LexUserType::XrpcQuery(query) => { 70 + let params = option_to_tokens(&query.parameters, |p| match p { 71 + LexXrpcQueryParameter::Params(params) => { 72 + let params_tokens = xrpc_parameters_to_tokens(params); 73 + quote! { 74 + ::jacquard_lexicon::lexicon::LexXrpcQueryParameter::Params(#params_tokens) 75 + } 76 + } 77 + }); 78 + quote! { 79 + ::jacquard_lexicon::lexicon::LexUserType::XrpcQuery( 80 + ::jacquard_lexicon::lexicon::LexXrpcQuery { 81 + description: None, 82 + parameters: #params, 83 + output: None, 84 + errors: None, 85 + } 86 + ) 87 + } 88 + } 89 + LexUserType::XrpcProcedure(proc) => { 90 + let input = option_to_tokens(&proc.input, xrpc_body_to_tokens); 91 + quote! { 92 + ::jacquard_lexicon::lexicon::LexUserType::XrpcProcedure( 93 + ::jacquard_lexicon::lexicon::LexXrpcProcedure { 94 + description: None, 95 + parameters: None, 96 + input: #input, 97 + output: None, 98 + errors: None, 99 + } 100 + ) 101 + } 102 + } 103 + LexUserType::XrpcSubscription(sub) => { 104 + let params = option_to_tokens(&sub.parameters, |p| match p { 105 + LexXrpcSubscriptionParameter::Params(params) => { 106 + let params_tokens = xrpc_parameters_to_tokens(params); 107 + quote! { 108 + ::jacquard_lexicon::lexicon::LexXrpcSubscriptionParameter::Params(#params_tokens) 109 + } 110 + } 111 + }); 112 + quote! { 113 + ::jacquard_lexicon::lexicon::LexUserType::XrpcSubscription( 114 + ::jacquard_lexicon::lexicon::LexXrpcSubscription { 115 + description: None, 116 + parameters: #params, 117 + message: None, 118 + infos: None, 119 + errors: None, 120 + } 121 + ) 122 + } 123 + } 124 + LexUserType::Object(obj) => { 125 + let obj_tokens = object_to_tokens(obj); 126 + quote! { 127 + ::jacquard_lexicon::lexicon::LexUserType::Object(#obj_tokens) 128 + } 129 + } 130 + LexUserType::Union(union) => { 131 + let refs: Vec<_> = union.refs.iter().map(|r| r.as_ref()).collect(); 132 + let closed = option_to_tokens(&union.closed, |c| quote! { #c }); 133 + quote! { 134 + ::jacquard_lexicon::lexicon::LexUserType::Union( 135 + ::jacquard_lexicon::lexicon::LexRefUnion { 136 + description: None, 137 + refs: vec![#(#refs.into()),*], 138 + closed: #closed, 139 + } 140 + ) 141 + } 142 + } 143 + _ => quote! { todo!("unsupported user type variant") }, 144 + } 145 + } 146 + 147 + /// Convert LexObject to tokens 148 + fn object_to_tokens(obj: &LexObject) -> TokenStream { 149 + let props = properties_to_tokens(&obj.properties); 150 + let required = option_vec_smol_str_to_tokens(&obj.required); 151 + 152 + quote! { 153 + ::jacquard_lexicon::lexicon::LexObject { 154 + description: None, 155 + required: #required, 156 + nullable: None, 157 + properties: #props, 158 + } 159 + } 160 + } 161 + 162 + /// Convert properties map to tokens 163 + fn properties_to_tokens(props: &BTreeMap<SmolStr, LexObjectProperty>) -> TokenStream { 164 + let prop_entries: Vec<_> = props 165 + .iter() 166 + .map(|(name, prop)| { 167 + let name_str = name.as_str(); 168 + let prop_tokens = object_property_to_tokens(prop); 169 + quote! { map.insert(#name_str.into(), #prop_tokens) } 170 + }) 171 + .collect(); 172 + 173 + quote! { 174 + { 175 + let mut map = ::std::collections::BTreeMap::new(); 176 + #(#prop_entries;)* 177 + map 178 + } 179 + } 180 + } 181 + 182 + /// Convert LexObjectProperty to tokens 183 + fn object_property_to_tokens(prop: &LexObjectProperty) -> TokenStream { 184 + match prop { 185 + LexObjectProperty::Boolean(b) => quote! { 186 + ::jacquard_lexicon::lexicon::LexObjectProperty::Boolean( 187 + ::jacquard_lexicon::lexicon::LexBoolean { 188 + description: None, 189 + default: None, 190 + r#const: None, 191 + } 192 + ) 193 + }, 194 + LexObjectProperty::Integer(i) => { 195 + let min = option_to_tokens(&i.minimum, |v| quote! { #v }); 196 + let max = option_to_tokens(&i.maximum, |v| quote! { #v }); 197 + quote! { 198 + ::jacquard_lexicon::lexicon::LexObjectProperty::Integer( 199 + ::jacquard_lexicon::lexicon::LexInteger { 200 + description: None, 201 + default: None, 202 + minimum: #min, 203 + maximum: #max, 204 + r#enum: None, 205 + r#const: None, 206 + } 207 + ) 208 + } 209 + } 210 + LexObjectProperty::String(s) => { 211 + let string_tokens = lex_string_to_tokens(s); 212 + quote! { 213 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(#string_tokens) 214 + } 215 + } 216 + LexObjectProperty::Bytes(b) => { 217 + let min = option_to_tokens(&b.min_length, |v| quote! { #v }); 218 + let max = option_to_tokens(&b.max_length, |v| quote! { #v }); 219 + quote! { 220 + ::jacquard_lexicon::lexicon::LexObjectProperty::Bytes( 221 + ::jacquard_lexicon::lexicon::LexBytes { 222 + description: None, 223 + max_length: #max, 224 + min_length: #min, 225 + } 226 + ) 227 + } 228 + } 229 + LexObjectProperty::CidLink(_) => quote! { 230 + ::jacquard_lexicon::lexicon::LexObjectProperty::CidLink( 231 + ::jacquard_lexicon::lexicon::LexCidLink { description: None } 232 + ) 233 + }, 234 + LexObjectProperty::Blob(_) => quote! { 235 + ::jacquard_lexicon::lexicon::LexObjectProperty::Blob( 236 + ::jacquard_lexicon::lexicon::LexBlob { 237 + description: None, 238 + accept: None, 239 + max_size: None, 240 + } 241 + ) 242 + }, 243 + LexObjectProperty::Unknown(_) => quote! { 244 + ::jacquard_lexicon::lexicon::LexObjectProperty::Unknown( 245 + ::jacquard_lexicon::lexicon::LexUnknown { description: None } 246 + ) 247 + }, 248 + LexObjectProperty::Array(arr) => { 249 + let items = array_item_to_tokens(&arr.items); 250 + let min = option_to_tokens(&arr.min_length, |v| quote! { #v }); 251 + let max = option_to_tokens(&arr.max_length, |v| quote! { #v }); 252 + quote! { 253 + ::jacquard_lexicon::lexicon::LexObjectProperty::Array( 254 + ::jacquard_lexicon::lexicon::LexArray { 255 + description: None, 256 + items: #items, 257 + min_length: #min, 258 + max_length: #max, 259 + } 260 + ) 261 + } 262 + } 263 + LexObjectProperty::Ref(r) => { 264 + let ref_str = r.r#ref.as_ref(); 265 + quote! { 266 + ::jacquard_lexicon::lexicon::LexObjectProperty::Ref( 267 + ::jacquard_lexicon::lexicon::LexRef { 268 + description: None, 269 + r#ref: #ref_str.into(), 270 + } 271 + ) 272 + } 273 + } 274 + _ => quote! { todo!("unsupported object property variant") }, 275 + } 276 + } 277 + 278 + /// Convert LexArrayItem to tokens 279 + fn array_item_to_tokens(item: &LexArrayItem) -> TokenStream { 280 + match item { 281 + LexArrayItem::Boolean(_) => quote! { 282 + ::jacquard_lexicon::lexicon::LexArrayItem::Boolean( 283 + ::jacquard_lexicon::lexicon::LexBoolean { 284 + description: None, 285 + default: None, 286 + r#const: None, 287 + } 288 + ) 289 + }, 290 + LexArrayItem::Integer(_) => quote! { 291 + ::jacquard_lexicon::lexicon::LexArrayItem::Integer( 292 + ::jacquard_lexicon::lexicon::LexInteger { 293 + description: None, 294 + default: None, 295 + minimum: None, 296 + maximum: None, 297 + r#enum: None, 298 + r#const: None, 299 + } 300 + ) 301 + }, 302 + LexArrayItem::String(s) => { 303 + let string_tokens = lex_string_to_tokens(s); 304 + quote! { 305 + ::jacquard_lexicon::lexicon::LexArrayItem::String(#string_tokens) 306 + } 307 + } 308 + LexArrayItem::Bytes(_) => quote! { 309 + ::jacquard_lexicon::lexicon::LexArrayItem::Bytes( 310 + ::jacquard_lexicon::lexicon::LexBytes { 311 + description: None, 312 + max_length: None, 313 + min_length: None, 314 + } 315 + ) 316 + }, 317 + LexArrayItem::CidLink(_) => quote! { 318 + ::jacquard_lexicon::lexicon::LexArrayItem::CidLink( 319 + ::jacquard_lexicon::lexicon::LexCidLink { description: None } 320 + ) 321 + }, 322 + LexArrayItem::Blob(_) => quote! { 323 + ::jacquard_lexicon::lexicon::LexArrayItem::Blob( 324 + ::jacquard_lexicon::lexicon::LexBlob { 325 + description: None, 326 + accept: None, 327 + max_size: None, 328 + } 329 + ) 330 + }, 331 + LexArrayItem::Unknown(_) => quote! { 332 + ::jacquard_lexicon::lexicon::LexArrayItem::Unknown( 333 + ::jacquard_lexicon::lexicon::LexUnknown { description: None } 334 + ) 335 + }, 336 + LexArrayItem::Ref(r) => { 337 + let ref_str = r.r#ref.as_ref(); 338 + quote! { 339 + ::jacquard_lexicon::lexicon::LexArrayItem::Ref( 340 + ::jacquard_lexicon::lexicon::LexRef { 341 + description: None, 342 + r#ref: #ref_str.into(), 343 + } 344 + ) 345 + } 346 + } 347 + LexArrayItem::Object(obj) => { 348 + let obj_tokens = object_to_tokens(obj); 349 + quote! { 350 + ::jacquard_lexicon::lexicon::LexArrayItem::Object(#obj_tokens) 351 + } 352 + } 353 + LexArrayItem::Union(union) => { 354 + let refs: Vec<_> = union.refs.iter().map(|r| r.as_ref()).collect(); 355 + let closed = option_to_tokens(&union.closed, |c| quote! { #c }); 356 + quote! { 357 + ::jacquard_lexicon::lexicon::LexArrayItem::Union( 358 + ::jacquard_lexicon::lexicon::LexRefUnion { 359 + description: None, 360 + refs: vec![#(#refs.into()),*], 361 + closed: #closed, 362 + } 363 + ) 364 + } 365 + } 366 + } 367 + } 368 + 369 + /// Convert LexString to tokens 370 + fn lex_string_to_tokens(s: &LexString) -> TokenStream { 371 + let format = option_to_tokens(&s.format, |f| match f { 372 + LexStringFormat::Did => quote! { ::jacquard_lexicon::lexicon::LexStringFormat::Did }, 373 + LexStringFormat::Handle => quote! { ::jacquard_lexicon::lexicon::LexStringFormat::Handle }, 374 + LexStringFormat::AtUri => quote! { ::jacquard_lexicon::lexicon::LexStringFormat::AtUri }, 375 + LexStringFormat::Nsid => quote! { ::jacquard_lexicon::lexicon::LexStringFormat::Nsid }, 376 + LexStringFormat::Cid => quote! { ::jacquard_lexicon::lexicon::LexStringFormat::Cid }, 377 + LexStringFormat::Datetime => quote! { ::jacquard_lexicon::lexicon::LexStringFormat::Datetime }, 378 + LexStringFormat::Language => quote! { ::jacquard_lexicon::lexicon::LexStringFormat::Language }, 379 + LexStringFormat::Tid => quote! { ::jacquard_lexicon::lexicon::LexStringFormat::Tid }, 380 + LexStringFormat::RecordKey => quote! { ::jacquard_lexicon::lexicon::LexStringFormat::RecordKey }, 381 + LexStringFormat::AtIdentifier => quote! { ::jacquard_lexicon::lexicon::LexStringFormat::AtIdentifier }, 382 + LexStringFormat::Uri => quote! { ::jacquard_lexicon::lexicon::LexStringFormat::Uri }, 383 + }); 384 + let min_len = option_to_tokens(&s.min_length, |v| quote! { #v }); 385 + let max_len = option_to_tokens(&s.max_length, |v| quote! { #v }); 386 + let min_graph = option_to_tokens(&s.min_graphemes, |v| quote! { #v }); 387 + let max_graph = option_to_tokens(&s.max_graphemes, |v| quote! { #v }); 388 + 389 + quote! { 390 + ::jacquard_lexicon::lexicon::LexString { 391 + description: None, 392 + format: #format, 393 + default: None, 394 + min_length: #min_len, 395 + max_length: #max_len, 396 + min_graphemes: #min_graph, 397 + max_graphemes: #max_graph, 398 + r#enum: None, 399 + r#const: None, 400 + known_values: None, 401 + } 402 + } 403 + } 404 + 405 + /// Convert LexXrpcParameters to tokens 406 + fn xrpc_parameters_to_tokens(params: &LexXrpcParameters) -> TokenStream { 407 + let props: Vec<_> = params 408 + .properties 409 + .iter() 410 + .map(|(name, prop)| { 411 + let name_str = name.as_str(); 412 + let prop_tokens = xrpc_param_property_to_tokens(prop); 413 + quote! { map.insert(#name_str.into(), #prop_tokens) } 414 + }) 415 + .collect(); 416 + let required = option_vec_smol_str_to_tokens(&params.required); 417 + 418 + quote! { 419 + ::jacquard_lexicon::lexicon::LexXrpcParameters { 420 + description: None, 421 + required: #required, 422 + properties: { 423 + let mut map = ::std::collections::BTreeMap::new(); 424 + #(#props;)* 425 + map 426 + }, 427 + } 428 + } 429 + } 430 + 431 + /// Convert LexXrpcParametersProperty to tokens 432 + fn xrpc_param_property_to_tokens(prop: &LexXrpcParametersProperty) -> TokenStream { 433 + match prop { 434 + LexXrpcParametersProperty::Boolean(_) => quote! { 435 + ::jacquard_lexicon::lexicon::LexXrpcParametersProperty::Boolean( 436 + ::jacquard_lexicon::lexicon::LexBoolean { 437 + description: None, 438 + default: None, 439 + r#const: None, 440 + } 441 + ) 442 + }, 443 + LexXrpcParametersProperty::Integer(_) => quote! { 444 + ::jacquard_lexicon::lexicon::LexXrpcParametersProperty::Integer( 445 + ::jacquard_lexicon::lexicon::LexInteger { 446 + description: None, 447 + default: None, 448 + minimum: None, 449 + maximum: None, 450 + r#enum: None, 451 + r#const: None, 452 + } 453 + ) 454 + }, 455 + LexXrpcParametersProperty::String(s) => { 456 + let string_tokens = lex_string_to_tokens(s); 457 + quote! { 458 + ::jacquard_lexicon::lexicon::LexXrpcParametersProperty::String(#string_tokens) 459 + } 460 + }, 461 + LexXrpcParametersProperty::Unknown(_) => quote! { 462 + ::jacquard_lexicon::lexicon::LexXrpcParametersProperty::Unknown( 463 + ::jacquard_lexicon::lexicon::LexUnknown { description: None } 464 + ) 465 + }, 466 + LexXrpcParametersProperty::Array(arr) => { 467 + let items = match &arr.items { 468 + LexPrimitiveArrayItem::Boolean(_) => quote! { 469 + ::jacquard_lexicon::lexicon::LexPrimitiveArrayItem::Boolean( 470 + ::jacquard_lexicon::lexicon::LexBoolean { 471 + description: None, 472 + default: None, 473 + r#const: None, 474 + } 475 + ) 476 + }, 477 + LexPrimitiveArrayItem::Integer(_) => quote! { 478 + ::jacquard_lexicon::lexicon::LexPrimitiveArrayItem::Integer( 479 + ::jacquard_lexicon::lexicon::LexInteger { 480 + description: None, 481 + default: None, 482 + minimum: None, 483 + maximum: None, 484 + r#enum: None, 485 + r#const: None, 486 + } 487 + ) 488 + }, 489 + LexPrimitiveArrayItem::String(s) => { 490 + let string_tokens = lex_string_to_tokens(s); 491 + quote! { 492 + ::jacquard_lexicon::lexicon::LexPrimitiveArrayItem::String(#string_tokens) 493 + } 494 + } 495 + LexPrimitiveArrayItem::Unknown(_) => quote! { 496 + ::jacquard_lexicon::lexicon::LexPrimitiveArrayItem::Unknown( 497 + ::jacquard_lexicon::lexicon::LexUnknown { description: None } 498 + ) 499 + }, 500 + }; 501 + let min = option_to_tokens(&arr.min_length, |v| quote! { #v }); 502 + let max = option_to_tokens(&arr.max_length, |v| quote! { #v }); 503 + quote! { 504 + ::jacquard_lexicon::lexicon::LexXrpcParametersProperty::Array( 505 + ::jacquard_lexicon::lexicon::LexPrimitiveArray { 506 + description: None, 507 + items: #items, 508 + min_length: #min, 509 + max_length: #max, 510 + } 511 + ) 512 + } 513 + }, 514 + } 515 + } 516 + 517 + /// Convert LexXrpcBody to tokens 518 + fn xrpc_body_to_tokens(body: &LexXrpcBody) -> TokenStream { 519 + let encoding = body.encoding.as_ref(); 520 + let schema = option_to_tokens(&body.schema, |s| match s { 521 + LexXrpcBodySchema::Object(obj) => { 522 + let obj_tokens = object_to_tokens(obj); 523 + quote! { 524 + ::jacquard_lexicon::lexicon::LexXrpcBodySchema::Object(#obj_tokens) 525 + } 526 + } 527 + LexXrpcBodySchema::Ref(r) => { 528 + let ref_str = r.r#ref.as_ref(); 529 + quote! { 530 + ::jacquard_lexicon::lexicon::LexXrpcBodySchema::Ref( 531 + ::jacquard_lexicon::lexicon::LexRef { 532 + description: None, 533 + r#ref: #ref_str.into(), 534 + } 535 + ) 536 + } 537 + } 538 + LexXrpcBodySchema::Union(union) => { 539 + let refs: Vec<_> = union.refs.iter().map(|r| r.as_ref()).collect(); 540 + let closed = option_to_tokens(&union.closed, |c| quote! { #c }); 541 + quote! { 542 + ::jacquard_lexicon::lexicon::LexXrpcBodySchema::Union( 543 + ::jacquard_lexicon::lexicon::LexRefUnion { 544 + description: None, 545 + refs: vec![#(#refs.into()),*], 546 + closed: #closed, 547 + } 548 + ) 549 + } 550 + } 551 + }); 552 + 553 + quote! { 554 + ::jacquard_lexicon::lexicon::LexXrpcBody { 555 + description: None, 556 + encoding: #encoding.into(), 557 + schema: #schema, 558 + } 559 + } 560 + } 561 + 562 + /// Convert validation checks to tokens 563 + pub fn validations_to_tokens(checks: &[ValidationCheck]) -> TokenStream { 564 + if checks.is_empty() { 565 + return quote! { Ok(()) }; 566 + } 567 + 568 + let check_tokens: Vec<_> = checks 569 + .iter() 570 + .map(|check| { 571 + let field_ident = syn::Ident::new(&check.field_name, proc_macro2::Span::call_site()); 572 + let field_name_static = &check.field_name; 573 + match &check.check { 574 + ConstraintCheck::MaxLength { max } => quote! { 575 + if self.#field_ident.len() > #max { 576 + return Err(::jacquard_lexicon::schema::ValidationError::MaxLength { 577 + field: #field_name_static, 578 + max: #max, 579 + actual: self.#field_ident.len(), 580 + }); 581 + } 582 + }, 583 + ConstraintCheck::MinLength { min } => quote! { 584 + if self.#field_ident.len() < #min { 585 + return Err(::jacquard_lexicon::schema::ValidationError::MinLength { 586 + field: #field_name_static, 587 + min: #min, 588 + actual: self.#field_ident.len(), 589 + }); 590 + } 591 + }, 592 + ConstraintCheck::MaxGraphemes { max } => quote! { 593 + { 594 + let count = ::unicode_segmentation::UnicodeSegmentation::graphemes( 595 + self.#field_ident.as_ref(), 596 + true 597 + ).count(); 598 + if count > #max { 599 + return Err(::jacquard_lexicon::schema::ValidationError::MaxGraphemes { 600 + field: #field_name_static, 601 + max: #max, 602 + actual: count, 603 + }); 604 + } 605 + } 606 + }, 607 + ConstraintCheck::MinGraphemes { min } => quote! { 608 + { 609 + let count = ::unicode_segmentation::UnicodeSegmentation::graphemes( 610 + self.#field_ident.as_ref(), 611 + true 612 + ).count(); 613 + if count < #min { 614 + return Err(::jacquard_lexicon::schema::ValidationError::MinGraphemes { 615 + field: #field_name_static, 616 + min: #min, 617 + actual: count, 618 + }); 619 + } 620 + } 621 + }, 622 + ConstraintCheck::Maximum { max } => quote! { 623 + if self.#field_ident > #max { 624 + return Err(::jacquard_lexicon::schema::ValidationError::Maximum { 625 + field: #field_name_static, 626 + max: #max, 627 + actual: self.#field_ident, 628 + }); 629 + } 630 + }, 631 + ConstraintCheck::Minimum { min } => quote! { 632 + if self.#field_ident < #min { 633 + return Err(::jacquard_lexicon::schema::ValidationError::Minimum { 634 + field: #field_name_static, 635 + min: #min, 636 + actual: self.#field_ident, 637 + }); 638 + } 639 + }, 640 + } 641 + }) 642 + .collect(); 643 + 644 + quote! { 645 + #(#check_tokens)* 646 + Ok(()) 647 + } 648 + } 649 + 650 + // Helper functions 651 + 652 + fn option_to_tokens<T, F>(opt: &Option<T>, f: F) -> TokenStream 653 + where 654 + F: FnOnce(&T) -> TokenStream, 655 + { 656 + match opt { 657 + Some(v) => { 658 + let tokens = f(v); 659 + quote! { Some(#tokens) } 660 + } 661 + None => quote! { None }, 662 + } 663 + } 664 + 665 + fn option_cow_str_to_tokens(opt: &Option<jacquard_common::CowStr>) -> TokenStream { 666 + match opt { 667 + Some(s) => { 668 + let s_str = s.as_ref(); 669 + quote! { Some(#s_str.into()) } 670 + } 671 + None => quote! { None }, 672 + } 673 + } 674 + 675 + fn option_vec_smol_str_to_tokens(opt: &Option<Vec<SmolStr>>) -> TokenStream { 676 + match opt { 677 + Some(v) => { 678 + let strs: Vec<_> = v.iter().map(|s| s.as_str()).collect(); 679 + quote! { Some(vec![#(#strs.into()),*]) } 680 + } 681 + None => quote! { None }, 682 + } 683 + }
+29 -894
crates/jacquard-lexicon/src/derive_impl/lexicon_schema.rs
··· 1 1 //! Implementation of #[derive(LexiconSchema)] macro 2 2 3 - use crate::schema::from_ast::{ 4 - LexiconFieldAttrs, LexiconTypeAttrs, LexiconTypeKind, RenameRule, SerdeAttrs, determine_nsid, 5 - extract_option_inner, parse_field_attrs, parse_serde_attrs, parse_serde_rename_all, 6 - parse_type_attrs, 7 - }; 8 - use crate::schema::type_mapping::{LexiconPrimitiveType, StringFormat, rust_type_to_lexicon_type}; 9 - use heck::{ToKebabCase, ToLowerCamelCase, ToPascalCase, ToShoutySnakeCase, ToSnakeCase}; 10 3 use proc_macro2::TokenStream; 11 4 use quote::quote; 12 - use syn::{Attribute, Data, DeriveInput, Fields, Ident, LitStr, Type, parse2}; 5 + use syn::{Data, DeriveInput, parse2}; 13 6 14 7 /// Implementation for the LexiconSchema derive macro 15 8 pub fn impl_derive_lexicon_schema(input: TokenStream) -> TokenStream { ··· 25 18 } 26 19 27 20 fn lexicon_schema_impl(input: &DeriveInput) -> syn::Result<TokenStream> { 28 - // Parse type-level attributes 29 - let type_attrs = parse_type_attrs(&input.attrs)?; 30 - 31 - // Determine NSID 32 - let nsid = determine_nsid(&type_attrs, input)?; 33 - 34 21 // Generate based on data type 35 22 match &input.data { 36 - Data::Struct(data_struct) => impl_for_struct(input, &type_attrs, &nsid, data_struct), 37 - Data::Enum(data_enum) => impl_for_enum(input, &type_attrs, &nsid, data_enum), 23 + Data::Struct(_) => impl_for_struct(input), 24 + Data::Enum(_) => impl_for_enum(input), 38 25 Data::Union(_) => Err(syn::Error::new_spanned( 39 26 input, 40 27 "LexiconSchema cannot be derived for unions", ··· 42 29 } 43 30 } 44 31 45 - /// Extract NSID from XrpcRequest attributes (cross-derive coordination) 46 - fn extract_xrpc_nsid(attrs: &[Attribute]) -> syn::Result<Option<String>> { 47 - for attr in attrs { 48 - if !attr.path().is_ident("xrpc") { 49 - continue; 50 - } 51 - 52 - let mut nsid = None; 53 - attr.parse_nested_meta(|meta| { 54 - if meta.path.is_ident("nsid") { 55 - let value = meta.value()?; 56 - let lit: LitStr = value.parse()?; 57 - nsid = Some(lit.value()); 58 - } 59 - Ok(()) 60 - })?; 61 - 62 - if let Some(nsid) = nsid { 63 - return Ok(Some(nsid)); 64 - } 65 - } 66 - Ok(None) 67 - } 68 - 69 32 /// Struct implementation 70 - fn impl_for_struct( 71 - input: &DeriveInput, 72 - type_attrs: &LexiconTypeAttrs, 73 - nsid: &str, 74 - data_struct: &syn::DataStruct, 75 - ) -> syn::Result<TokenStream> { 33 + fn impl_for_struct(input: &DeriveInput) -> syn::Result<TokenStream> { 76 34 let name = &input.ident; 77 35 let generics = &input.generics; 78 36 ··· 84 42 quote! {} 85 43 }; 86 44 87 - // Parse fields 88 - let fields = match &data_struct.fields { 89 - Fields::Named(fields) => &fields.named, 90 - _ => { 91 - return Err(syn::Error::new_spanned( 92 - input, 93 - "LexiconSchema only supports structs with named fields", 94 - )); 95 - } 96 - }; 45 + // Use schema builder to get actual data 46 + let built = crate::schema::from_ast::build_struct_schema(input)?; 97 47 98 - // Parse serde container attributes (defaults to camelCase) 99 - let rename_all = parse_serde_rename_all(&input.attrs)?; 100 - 101 - // Generate field definitions 102 - let field_defs = generate_field_definitions(fields, rename_all)?; 103 - 104 - // Generate validation code 105 - let validation_code = generate_validation(fields, rename_all)?; 48 + // Convert to tokens for code generation 49 + let doc_tokens = super::doc_to_tokens::doc_to_tokens(&built.doc); 50 + let validation_tokens = super::doc_to_tokens::validations_to_tokens(&built.validation_checks); 106 51 107 - // Build lexicon_doc() implementation 108 - let doc_impl = generate_doc_impl(nsid, type_attrs, &field_defs)?; 109 - 110 - // Determine schema_id (add fragment suffix if needed) 111 - let schema_id = if let Some(fragment) = &type_attrs.fragment { 112 - let frag_name = if fragment.is_empty() { 113 - // Infer from type name 114 - name.to_string().to_lower_camel_case() 115 - } else { 116 - fragment.clone() 117 - }; 118 - quote! { 119 - format_smolstr!("{}#{}", #nsid, #frag_name).to_string() 120 - } 52 + let nsid = &built.nsid; 53 + let schema_id_expr = if built.schema_id != built.nsid { 54 + let sid = &built.schema_id; 55 + quote! { ::jacquard_common::CowStr::from(#sid) } 121 56 } else { 122 - quote! { 123 - ::jacquard_common::CowStr::new_static(#nsid) 124 - } 57 + quote! { ::jacquard_common::CowStr::new_static(#nsid) } 125 58 }; 126 59 127 60 // Generate trait impl ··· 132 65 } 133 66 134 67 fn schema_id() -> ::jacquard_common::CowStr<'static> { 135 - #schema_id 68 + #schema_id_expr 136 69 } 137 70 138 71 fn lexicon_doc( 139 - generator: &mut ::jacquard_lexicon::schema::LexiconGenerator 72 + _generator: &mut ::jacquard_lexicon::schema::LexiconGenerator 140 73 ) -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { 141 - #doc_impl 74 + #doc_tokens 142 75 } 143 76 144 77 fn validate(&self) -> ::std::result::Result<(), ::jacquard_lexicon::schema::ValidationError> { 145 - #validation_code 78 + #validation_tokens 146 79 } 147 80 } 148 81 149 - // Generate inventory submission for Phase 3 discovery 82 + // Generate inventory submission for workspace discovery 150 83 ::inventory::submit! { 151 84 ::jacquard_lexicon::schema::LexiconSchemaRef { 152 85 nsid: #nsid, ··· 159 92 }) 160 93 } 161 94 162 - struct FieldDef { 163 - name: String, // Rust field name 164 - schema_name: String, // JSON field name (after serde rename) 165 - rust_type: Type, // Rust type 166 - lex_type: TokenStream, // LexObjectProperty tokens 167 - required: bool, 168 - } 169 - 170 - fn generate_field_definitions( 171 - fields: &syn::punctuated::Punctuated<syn::Field, syn::Token![,]>, 172 - rename_all: Option<RenameRule>, 173 - ) -> syn::Result<Vec<FieldDef>> { 174 - let mut defs = Vec::new(); 175 - 176 - for field in fields { 177 - let field_name = field.ident.as_ref().unwrap().to_string(); 178 - 179 - // Skip extra_data field (added by #[lexicon] attribute macro) 180 - if field_name == "extra_data" { 181 - continue; 182 - } 183 - 184 - // Parse attributes 185 - let serde_attrs = parse_serde_attrs(&field.attrs)?; 186 - let lex_attrs = parse_field_attrs(&field.attrs)?; 187 - 188 - // Skip if serde(skip) 189 - if serde_attrs.skip { 190 - continue; 191 - } 192 - 193 - // Determine schema name 194 - let schema_name = if let Some(rename) = serde_attrs.rename { 195 - rename 196 - } else if let Some(rule) = rename_all { 197 - rule.apply(&field_name) 198 - } else { 199 - field_name.clone() 200 - }; 201 - 202 - // Determine if required (Option<T> = optional) 203 - let (inner_type, required) = extract_option_inner(&field.ty); 204 - let rust_type = inner_type.clone(); 205 - 206 - // Generate LexObjectProperty based on type + constraints 207 - let lex_type = generate_lex_property(&rust_type, &lex_attrs)?; 208 - 209 - defs.push(FieldDef { 210 - name: field_name, 211 - schema_name, 212 - rust_type, 213 - lex_type, 214 - required, 215 - }); 216 - } 217 - 218 - Ok(defs) 219 - } 220 - 221 - /// Generate LexObjectProperty tokens for a field 222 - fn generate_lex_property( 223 - rust_type: &Type, 224 - constraints: &LexiconFieldAttrs, 225 - ) -> syn::Result<TokenStream> { 226 - // Try to detect primitive type 227 - let lex_type = rust_type_to_lexicon_type(rust_type); 228 - 229 - match lex_type { 230 - Some(LexiconPrimitiveType::Boolean) => Ok(quote! { 231 - ::jacquard_lexicon::lexicon::LexObjectProperty::Boolean( 232 - ::jacquard_lexicon::lexicon::LexBoolean { 233 - description: None, 234 - default: None, 235 - r#const: None, 236 - } 237 - ) 238 - }), 239 - Some(LexiconPrimitiveType::Integer) => { 240 - let minimum = constraints 241 - .minimum 242 - .map(|v| quote! { Some(#v) }) 243 - .unwrap_or(quote! { None }); 244 - let maximum = constraints 245 - .maximum 246 - .map(|v| quote! { Some(#v) }) 247 - .unwrap_or(quote! { None }); 248 - 249 - Ok(quote! { 250 - ::jacquard_lexicon::lexicon::LexObjectProperty::Integer( 251 - ::jacquard_lexicon::lexicon::LexInteger { 252 - description: None, 253 - default: None, 254 - minimum: #minimum, 255 - maximum: #maximum, 256 - r#enum: None, 257 - r#const: None, 258 - } 259 - ) 260 - }) 261 - } 262 - Some(LexiconPrimitiveType::String(format)) => generate_string_property(format, constraints), 263 - Some(LexiconPrimitiveType::Bytes) => { 264 - let max_length = constraints 265 - .max_length 266 - .map(|v| quote! { Some(#v) }) 267 - .unwrap_or(quote! { None }); 268 - let min_length = constraints 269 - .min_length 270 - .map(|v| quote! { Some(#v) }) 271 - .unwrap_or(quote! { None }); 272 - 273 - Ok(quote! { 274 - ::jacquard_lexicon::lexicon::LexObjectProperty::Bytes( 275 - ::jacquard_lexicon::lexicon::LexBytes { 276 - description: None, 277 - max_length: #max_length, 278 - min_length: #min_length, 279 - } 280 - ) 281 - }) 282 - } 283 - Some(LexiconPrimitiveType::CidLink) => Ok(quote! { 284 - ::jacquard_lexicon::lexicon::LexObjectProperty::CidLink( 285 - ::jacquard_lexicon::lexicon::LexCidLink { 286 - description: None, 287 - } 288 - ) 289 - }), 290 - Some(LexiconPrimitiveType::Blob) => Ok(quote! { 291 - ::jacquard_lexicon::lexicon::LexObjectProperty::Blob( 292 - ::jacquard_lexicon::lexicon::LexBlob { 293 - description: None, 294 - accept: None, 295 - max_size: None, 296 - } 297 - ) 298 - }), 299 - Some(LexiconPrimitiveType::Unknown) => Ok(quote! { 300 - ::jacquard_lexicon::lexicon::LexObjectProperty::Unknown( 301 - ::jacquard_lexicon::lexicon::LexUnknown { 302 - description: None, 303 - } 304 - ) 305 - }), 306 - Some(LexiconPrimitiveType::Array(item_type)) => { 307 - let item_prop = generate_array_item(*item_type, constraints)?; 308 - let max_length = constraints 309 - .max_length 310 - .map(|v| quote! { Some(#v) }) 311 - .unwrap_or(quote! { None }); 312 - let min_length = constraints 313 - .min_length 314 - .map(|v| quote! { Some(#v) }) 315 - .unwrap_or(quote! { None }); 316 - 317 - Ok(quote! { 318 - ::jacquard_lexicon::lexicon::LexObjectProperty::Array( 319 - ::jacquard_lexicon::lexicon::LexArray { 320 - description: None, 321 - items: #item_prop, 322 - min_length: #min_length, 323 - max_length: #max_length, 324 - } 325 - ) 326 - }) 327 - } 328 - None => { 329 - // Not a recognized primitive - check for explicit ref or trait bound 330 - if let Some(ref_nsid) = &constraints.explicit_ref { 331 - Ok(quote! { 332 - ::jacquard_lexicon::lexicon::LexObjectProperty::Ref( 333 - ::jacquard_lexicon::lexicon::LexRef { 334 - description: None, 335 - r#ref: #ref_nsid.into(), 336 - } 337 - ) 338 - }) 339 - } else { 340 - // Try to use type's LexiconSchema impl 341 - Ok(quote! { 342 - { 343 - // Use the type's schema_id method 344 - let ref_nsid = <#rust_type as ::jacquard_lexicon::schema::LexiconSchema>::schema_id(); 345 - ::jacquard_lexicon::lexicon::LexObjectProperty::Ref( 346 - ::jacquard_lexicon::lexicon::LexRef { 347 - description: None, 348 - r#ref: ref_nsid.to_string().into(), 349 - } 350 - ) 351 - } 352 - }) 353 - } 354 - } 355 - _ => Err(syn::Error::new_spanned( 356 - rust_type, 357 - "unsupported type for lexicon schema generation", 358 - )), 359 - } 360 - } 361 - 362 - fn generate_array_item( 363 - item_type: LexiconPrimitiveType, 364 - _constraints: &LexiconFieldAttrs, 365 - ) -> syn::Result<TokenStream> { 366 - match item_type { 367 - LexiconPrimitiveType::String(format) => { 368 - let format_token = string_format_token(format); 369 - Ok(quote! { 370 - ::jacquard_lexicon::lexicon::LexArrayItem::String( 371 - ::jacquard_lexicon::lexicon::LexString { 372 - description: None, 373 - format: #format_token, 374 - default: None, 375 - min_length: None, 376 - max_length: None, 377 - min_graphemes: None, 378 - max_graphemes: None, 379 - r#enum: None, 380 - r#const: None, 381 - known_values: None, 382 - } 383 - ) 384 - }) 385 - } 386 - LexiconPrimitiveType::Integer => Ok(quote! { 387 - ::jacquard_lexicon::lexicon::LexArrayItem::Integer( 388 - ::jacquard_lexicon::lexicon::LexInteger { 389 - description: None, 390 - default: None, 391 - minimum: None, 392 - maximum: None, 393 - r#enum: None, 394 - r#const: None, 395 - } 396 - ) 397 - }), 398 - _ => Ok(quote! { 399 - ::jacquard_lexicon::lexicon::LexArrayItem::Unknown( 400 - ::jacquard_lexicon::lexicon::LexUnknown { 401 - description: None, 402 - } 403 - ) 404 - }), 405 - } 406 - } 407 - 408 - fn generate_string_property( 409 - format: StringFormat, 410 - constraints: &LexiconFieldAttrs, 411 - ) -> syn::Result<TokenStream> { 412 - let format_token = string_format_token(format); 413 - 414 - let max_length = constraints 415 - .max_length 416 - .map(|v| quote! { Some(#v) }) 417 - .unwrap_or(quote! { None }); 418 - let max_graphemes = constraints 419 - .max_graphemes 420 - .map(|v| quote! { Some(#v) }) 421 - .unwrap_or(quote! { None }); 422 - let min_length = constraints 423 - .min_length 424 - .map(|v| quote! { Some(#v) }) 425 - .unwrap_or(quote! { None }); 426 - let min_graphemes = constraints 427 - .min_graphemes 428 - .map(|v| quote! { Some(#v) }) 429 - .unwrap_or(quote! { None }); 430 - 431 - Ok(quote! { 432 - ::jacquard_lexicon::lexicon::LexObjectProperty::String( 433 - ::jacquard_lexicon::lexicon::LexString { 434 - description: None, 435 - format: #format_token, 436 - default: None, 437 - min_length: #min_length, 438 - max_length: #max_length, 439 - min_graphemes: #min_graphemes, 440 - max_graphemes: #max_graphemes, 441 - r#enum: None, 442 - r#const: None, 443 - known_values: None, 444 - } 445 - ) 446 - }) 447 - } 448 - 449 - fn string_format_token(format: StringFormat) -> TokenStream { 450 - match format { 451 - StringFormat::Plain => quote! { None }, 452 - StringFormat::Did => { 453 - quote! { Some(::jacquard_lexicon::lexicon::LexStringFormat::Did) } 454 - } 455 - StringFormat::Handle => { 456 - quote! { Some(::jacquard_lexicon::lexicon::LexStringFormat::Handle) } 457 - } 458 - StringFormat::AtUri => { 459 - quote! { Some(::jacquard_lexicon::lexicon::LexStringFormat::AtUri) } 460 - } 461 - StringFormat::Nsid => { 462 - quote! { Some(::jacquard_lexicon::lexicon::LexStringFormat::Nsid) } 463 - } 464 - StringFormat::Cid => { 465 - quote! { Some(::jacquard_lexicon::lexicon::LexStringFormat::Cid) } 466 - } 467 - StringFormat::Datetime => { 468 - quote! { Some(::jacquard_lexicon::lexicon::LexStringFormat::Datetime) } 469 - } 470 - StringFormat::Language => { 471 - quote! { Some(::jacquard_lexicon::lexicon::LexStringFormat::Language) } 472 - } 473 - StringFormat::Tid => { 474 - quote! { Some(::jacquard_lexicon::lexicon::LexStringFormat::Tid) } 475 - } 476 - StringFormat::RecordKey => { 477 - quote! { Some(::jacquard_lexicon::lexicon::LexStringFormat::RecordKey) } 478 - } 479 - StringFormat::AtIdentifier => { 480 - quote! { Some(::jacquard_lexicon::lexicon::LexStringFormat::AtIdentifier) } 481 - } 482 - StringFormat::Uri => { 483 - quote! { Some(::jacquard_lexicon::lexicon::LexStringFormat::Uri) } 484 - } 485 - } 486 - } 487 - 488 - fn generate_doc_impl( 489 - nsid: &str, 490 - type_attrs: &LexiconTypeAttrs, 491 - field_defs: &[FieldDef], 492 - ) -> syn::Result<TokenStream> { 493 - // Build properties map 494 - let properties: Vec<_> = field_defs 495 - .iter() 496 - .map(|def| { 497 - let name = &def.schema_name; 498 - let lex_type = &def.lex_type; 499 - quote! { 500 - (#name.into(), #lex_type) 501 - } 502 - }) 503 - .collect(); 504 - 505 - // Build required array 506 - let required: Vec<_> = field_defs 507 - .iter() 508 - .filter(|def| def.required) 509 - .map(|def| { 510 - let name = &def.schema_name; 511 - quote! { #name.into() } 512 - }) 513 - .collect(); 514 - 515 - let required_field = if required.is_empty() { 516 - quote! { None } 517 - } else { 518 - quote! { Some(vec![#(#required),*]) } 519 - }; 520 - 521 - // Determine user type based on kind 522 - let user_type = match type_attrs.kind { 523 - Some(LexiconTypeKind::Record) => { 524 - let key = type_attrs 525 - .key 526 - .as_ref() 527 - .map(|k| quote! { Some(#k.into()) }) 528 - .unwrap_or(quote! { None }); 529 - 530 - quote! { 531 - ::jacquard_lexicon::lexicon::LexUserType::Record( 532 - ::jacquard_lexicon::lexicon::LexRecord { 533 - description: None, 534 - key: #key, 535 - record: ::jacquard_lexicon::lexicon::LexRecordRecord::Object( 536 - ::jacquard_lexicon::lexicon::LexObject { 537 - description: None, 538 - required: #required_field, 539 - nullable: None, 540 - properties: [#(#properties),*].into(), 541 - } 542 - ), 543 - } 544 - ) 545 - } 546 - } 547 - Some(LexiconTypeKind::Query) => { 548 - quote! { 549 - ::jacquard_lexicon::lexicon::LexUserType::Query( 550 - ::jacquard_lexicon::lexicon::LexQuery { 551 - description: None, 552 - parameters: Some(::jacquard_lexicon::lexicon::LexObject { 553 - description: None, 554 - required: #required_field, 555 - nullable: None, 556 - properties: [#(#properties),*].into(), 557 - }), 558 - output: None, 559 - errors: None, 560 - } 561 - ) 562 - } 563 - } 564 - Some(LexiconTypeKind::Procedure) => { 565 - quote! { 566 - ::jacquard_lexicon::lexicon::LexUserType::Procedure( 567 - ::jacquard_lexicon::lexicon::LexProcedure { 568 - description: None, 569 - input: Some(::jacquard_lexicon::lexicon::LexProcedureIO { 570 - description: None, 571 - encoding: "application/json".into(), 572 - schema: Some(::jacquard_lexicon::lexicon::LexProcedureSchema::Object( 573 - ::jacquard_lexicon::lexicon::LexObject { 574 - description: None, 575 - required: #required_field, 576 - nullable: None, 577 - properties: [#(#properties),*].into(), 578 - } 579 - )), 580 - }), 581 - output: None, 582 - errors: None, 583 - } 584 - ) 585 - } 586 - } 587 - _ => { 588 - // Default: Object type 589 - quote! { 590 - ::jacquard_lexicon::lexicon::LexUserType::Object( 591 - ::jacquard_lexicon::lexicon::LexObject { 592 - description: None, 593 - required: #required_field, 594 - nullable: None, 595 - properties: [#(#properties),*].into(), 596 - } 597 - ) 598 - } 599 - } 600 - }; 601 - 602 - Ok(quote! { 603 - { 604 - let mut defs = ::std::collections::BTreeMap::new(); 605 - defs.insert("main".into(), #user_type); 606 - 607 - ::jacquard_lexicon::lexicon::LexiconDoc { 608 - lexicon: ::jacquard_lexicon::lexicon::Lexicon::Lexicon1, 609 - id: #nsid.into(), 610 - revision: None, 611 - description: None, 612 - defs, 613 - } 614 - } 615 - }) 616 - } 617 - 618 - fn generate_validation( 619 - fields: &syn::punctuated::Punctuated<syn::Field, syn::Token![,]>, 620 - rename_all: Option<RenameRule>, 621 - ) -> syn::Result<TokenStream> { 622 - let mut checks = Vec::new(); 623 - 624 - for field in fields { 625 - let field_name = field.ident.as_ref().unwrap(); 626 - let field_name_str = field_name.to_string(); 627 - 628 - // Skip extra_data 629 - if field_name_str == "extra_data" { 630 - continue; 631 - } 632 - 633 - let lex_attrs = parse_field_attrs(&field.attrs)?; 634 - let serde_attrs = parse_serde_attrs(&field.attrs)?; 635 - 636 - if serde_attrs.skip { 637 - continue; 638 - } 639 - 640 - // Get actual field name for errors 641 - let display_name = if let Some(rename) = serde_attrs.rename { 642 - rename 643 - } else if let Some(rule) = rename_all { 644 - rule.apply(&field_name_str) 645 - } else { 646 - field_name_str.clone() 647 - }; 648 - 649 - // Extract inner type if Option 650 - let (inner_type, is_required) = extract_option_inner(&field.ty); 651 - 652 - // Generate checks based on type and constraints 653 - let field_checks = generate_field_validation( 654 - field_name, 655 - &display_name, 656 - inner_type, 657 - is_required, 658 - &lex_attrs, 659 - )?; 660 - 661 - checks.extend(field_checks); 662 - } 663 - 664 - if checks.is_empty() { 665 - Ok(quote! { Ok(()) }) 666 - } else { 667 - Ok(quote! { 668 - let mut errors = Vec::new(); 669 - 670 - #(#checks)* 671 - 672 - if errors.is_empty() { 673 - Ok(()) 674 - } else if errors.len() == 1 { 675 - Err(errors.into_iter().next().unwrap()) 676 - } else { 677 - Err(::jacquard_lexicon::schema::ValidationError::Multiple(errors)) 678 - } 679 - }) 680 - } 681 - } 682 - 683 - fn generate_field_validation( 684 - field_ident: &Ident, 685 - display_name: &str, 686 - field_type: &Type, 687 - is_required: bool, 688 - constraints: &LexiconFieldAttrs, 689 - ) -> syn::Result<Vec<TokenStream>> { 690 - let mut checks = Vec::new(); 691 - 692 - // Determine base type 693 - let lex_type = rust_type_to_lexicon_type(field_type); 694 - 695 - // Build accessor for the field value 696 - let (value_binding, value_expr) = if is_required { 697 - (quote! { let value = &self.#field_ident; }, quote! { value }) 698 - } else { 699 - ( 700 - quote! {}, 701 - quote! { 702 - match &self.#field_ident { 703 - Some(v) => v, 704 - None => continue, 705 - } 706 - }, 707 - ) 708 - }; 709 - 710 - match lex_type { 711 - Some(LexiconPrimitiveType::String(_)) => { 712 - // String constraints 713 - if let Some(max_len) = constraints.max_length { 714 - checks.push(quote! { 715 - #value_binding 716 - if #value_expr.len() > #max_len { 717 - errors.push(::jacquard_lexicon::schema::ValidationError::MaxLength { 718 - field: #display_name, 719 - max: #max_len, 720 - actual: #value_expr.len(), 721 - }); 722 - } 723 - }); 724 - } 725 - 726 - if let Some(max_graphemes) = constraints.max_graphemes { 727 - checks.push(quote! { 728 - #value_binding 729 - let count = ::unicode_segmentation::UnicodeSegmentation::graphemes( 730 - #value_expr.as_ref(), 731 - true 732 - ).count(); 733 - if count > #max_graphemes { 734 - errors.push(::jacquard_lexicon::schema::ValidationError::MaxGraphemes { 735 - field: #display_name, 736 - max: #max_graphemes, 737 - actual: count, 738 - }); 739 - } 740 - }); 741 - } 742 - 743 - if let Some(min_len) = constraints.min_length { 744 - checks.push(quote! { 745 - #value_binding 746 - if #value_expr.len() < #min_len { 747 - errors.push(::jacquard_lexicon::schema::ValidationError::MinLength { 748 - field: #display_name, 749 - min: #min_len, 750 - actual: #value_expr.len(), 751 - }); 752 - } 753 - }); 754 - } 755 - 756 - if let Some(min_graphemes) = constraints.min_graphemes { 757 - checks.push(quote! { 758 - #value_binding 759 - let count = ::unicode_segmentation::UnicodeSegmentation::graphemes( 760 - #value_expr.as_ref(), 761 - true 762 - ).count(); 763 - if count < #min_graphemes { 764 - errors.push(::jacquard_lexicon::schema::ValidationError::MinGraphemes { 765 - field: #display_name, 766 - min: #min_graphemes, 767 - actual: count, 768 - }); 769 - } 770 - }); 771 - } 772 - } 773 - Some(LexiconPrimitiveType::Integer) => { 774 - if let Some(maximum) = constraints.maximum { 775 - checks.push(quote! { 776 - #value_binding 777 - if *#value_expr > #maximum { 778 - errors.push(::jacquard_lexicon::schema::ValidationError::Maximum { 779 - field: #display_name, 780 - max: #maximum, 781 - actual: *#value_expr, 782 - }); 783 - } 784 - }); 785 - } 786 - 787 - if let Some(minimum) = constraints.minimum { 788 - checks.push(quote! { 789 - #value_binding 790 - if *#value_expr < #minimum { 791 - errors.push(::jacquard_lexicon::schema::ValidationError::Minimum { 792 - field: #display_name, 793 - min: #minimum, 794 - actual: *#value_expr, 795 - }); 796 - } 797 - }); 798 - } 799 - } 800 - Some(LexiconPrimitiveType::Array(_)) => { 801 - if let Some(max_len) = constraints.max_length { 802 - checks.push(quote! { 803 - #value_binding 804 - if #value_expr.len() > #max_len { 805 - errors.push(::jacquard_lexicon::schema::ValidationError::MaxLength { 806 - field: #display_name, 807 - max: #max_len, 808 - actual: #value_expr.len(), 809 - }); 810 - } 811 - }); 812 - } 813 - 814 - if let Some(min_len) = constraints.min_length { 815 - checks.push(quote! { 816 - #value_binding 817 - if #value_expr.len() < #min_len { 818 - errors.push(::jacquard_lexicon::schema::ValidationError::MinLength { 819 - field: #display_name, 820 - min: #min_len, 821 - actual: #value_expr.len(), 822 - }); 823 - } 824 - }); 825 - } 826 - } 827 - _ => { 828 - // No built-in validation for this type 829 - } 830 - } 831 - 832 - Ok(checks) 833 - } 834 - 835 95 /// Enum implementation (union support) 836 - fn impl_for_enum( 837 - input: &DeriveInput, 838 - type_attrs: &LexiconTypeAttrs, 839 - nsid: &str, 840 - data_enum: &syn::DataEnum, 841 - ) -> syn::Result<TokenStream> { 96 + fn impl_for_enum(input: &DeriveInput) -> syn::Result<TokenStream> { 842 97 let name = &input.ident; 843 98 let generics = &input.generics; 844 99 ··· 850 105 quote! {} 851 106 }; 852 107 853 - // Check if this is an open union (has #[open_union] attribute) 854 - let is_open = has_open_union_attr(&input.attrs); 855 - 856 - // Extract variant refs 857 - let mut refs = Vec::new(); 858 - for variant in &data_enum.variants { 859 - // Skip Unknown variant (added by #[open_union] macro) 860 - if variant.ident == "Unknown" { 861 - continue; 862 - } 863 - 864 - // Get NSID for this variant 865 - let variant_ref = extract_variant_ref(variant, nsid)?; 866 - refs.push(variant_ref); 867 - } 108 + // Use schema builder to get actual data 109 + let built = crate::schema::from_ast::build_enum_schema(input)?; 868 110 869 - // Generate union def 870 - // Only set closed: true for explicitly closed unions (no #[open_union]) 871 - // Open unions omit the field (defaults to open per spec) 872 - let closed_field = if !is_open { 873 - quote! { Some(true) } 874 - } else { 875 - quote! { None } 876 - }; 111 + // Convert to tokens for code generation 112 + let doc_tokens = super::doc_to_tokens::doc_to_tokens(&built.doc); 877 113 878 - let user_type = quote! { 879 - ::jacquard_lexicon::lexicon::LexUserType::Union( 880 - ::jacquard_lexicon::lexicon::LexRefUnion { 881 - description: None, 882 - refs: vec![#(#refs.into()),*], 883 - closed: #closed_field, 884 - } 885 - ) 886 - }; 114 + let nsid = &built.nsid; 887 115 888 116 Ok(quote! { 889 117 impl #generics ::jacquard_lexicon::schema::LexiconSchema for #name #lifetime { ··· 898 126 fn lexicon_doc( 899 127 _generator: &mut ::jacquard_lexicon::schema::LexiconGenerator 900 128 ) -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { 901 - let mut defs = ::std::collections::BTreeMap::new(); 902 - defs.insert("main".into(), #user_type); 129 + #doc_tokens 130 + } 903 131 904 - ::jacquard_lexicon::lexicon::LexiconDoc { 905 - lexicon: ::jacquard_lexicon::lexicon::Lexicon::Lexicon1, 906 - id: #nsid.into(), 907 - revision: None, 908 - description: None, 909 - defs, 910 - } 132 + fn validate(&self) -> ::std::result::Result<(), ::jacquard_lexicon::schema::ValidationError> { 133 + Ok(()) 911 134 } 912 135 } 913 136 ··· 922 145 } 923 146 }) 924 147 } 925 - 926 - /// Check if type has #[open_union] attribute 927 - fn has_open_union_attr(attrs: &[Attribute]) -> bool { 928 - attrs.iter().any(|attr| attr.path().is_ident("open_union")) 929 - } 930 - 931 - /// Extract NSID ref for a variant 932 - fn extract_variant_ref(variant: &syn::Variant, base_nsid: &str) -> syn::Result<String> { 933 - // Priority 1: Check for #[nsid = "..."] attribute 934 - for attr in &variant.attrs { 935 - if attr.path().is_ident("nsid") { 936 - if let syn::Meta::NameValue(meta) = &attr.meta { 937 - if let syn::Expr::Lit(expr_lit) = &meta.value { 938 - if let syn::Lit::Str(lit_str) = &expr_lit.lit { 939 - return Ok(lit_str.value()); 940 - } 941 - } 942 - } 943 - } 944 - } 945 - 946 - // Priority 2: Check for #[serde(rename = "...")] attribute 947 - for attr in &variant.attrs { 948 - if !attr.path().is_ident("serde") { 949 - continue; 950 - } 951 - 952 - let mut rename = None; 953 - let _ = attr.parse_nested_meta(|meta| { 954 - if meta.path.is_ident("rename") { 955 - let value = meta.value()?; 956 - let lit: LitStr = value.parse()?; 957 - rename = Some(lit.value()); 958 - } 959 - Ok(()) 960 - }); 961 - 962 - if let Some(rename) = rename { 963 - return Ok(rename); 964 - } 965 - } 966 - 967 - // Priority 3: For variants with non-primitive inner types, error 968 - // (caller should use #[nsid] or type must impl LexiconSchema) 969 - match &variant.fields { 970 - Fields::Unit => { 971 - // Unit variant - generate fragment ref: baseNsid#variantName 972 - let variant_name = variant.ident.to_string().to_lower_camel_case(); 973 - Ok(format!("{}#{}", base_nsid, variant_name)) 974 - } 975 - Fields::Unnamed(fields) if fields.unnamed.len() == 1 => { 976 - let ty = &fields.unnamed.first().unwrap().ty; 977 - 978 - // Check if primitive - if so, error (unions need refs) 979 - if let Some(prim) = rust_type_to_lexicon_type(ty) { 980 - if is_primitive(&prim) { 981 - return Err(syn::Error::new_spanned( 982 - variant, 983 - "union variants with primitive inner types must use #[nsid] or #[serde(rename)] attribute", 984 - )); 985 - } 986 - } 987 - 988 - // Non-primitive - error, must have explicit attribute 989 - // (we can't call schema_id() at compile time) 990 - Err(syn::Error::new_spanned( 991 - variant, 992 - "union variants with non-primitive types must use #[nsid] or #[serde(rename)] attribute to specify the ref", 993 - )) 994 - } 995 - _ => Err(syn::Error::new_spanned( 996 - variant, 997 - "union variants must be unit variants or have single unnamed field", 998 - )), 999 - } 1000 - } 1001 - 1002 - /// Check if a lexicon primitive type is actually a primitive (not a ref-able type) 1003 - fn is_primitive(prim: &LexiconPrimitiveType) -> bool { 1004 - matches!( 1005 - prim, 1006 - LexiconPrimitiveType::Boolean 1007 - | LexiconPrimitiveType::Integer 1008 - | LexiconPrimitiveType::String(_) 1009 - | LexiconPrimitiveType::Bytes 1010 - | LexiconPrimitiveType::Unknown 1011 - ) 1012 - }
+1
crates/jacquard-lexicon/src/derive_impl/mod.rs
··· 3 3 //! These functions are used by the `jacquard-derive` proc-macro crate but are also 4 4 //! available for runtime code generation in `jacquard-lexicon`. 5 5 6 + mod doc_to_tokens; 6 7 pub mod helpers; 7 8 pub mod into_static; 8 9 pub mod lexicon_attr;