1use crate::corpus::LexiconCorpus;
2use crate::error::{CodegenError, Result};
3use crate::lexicon::{
4 LexArrayItem, LexInteger, LexObject, LexObjectProperty, LexRecord, LexString, LexStringFormat,
5 LexUserType, LexXrpcBody, LexXrpcBodySchema, LexXrpcError, LexXrpcProcedure, LexXrpcQuery,
6 LexXrpcSubscription, LexXrpcSubscriptionMessageSchema,
7};
8use heck::{ToPascalCase, ToSnakeCase};
9use proc_macro2::TokenStream;
10use quote::quote;
11
12/// Convert a value string to a valid Rust variant name
13fn value_to_variant_name(value: &str) -> String {
14 // Remove leading special chars and convert to pascal case
15 let clean = value.trim_start_matches(|c: char| !c.is_alphanumeric());
16 let variant = clean.replace('-', "_").to_pascal_case();
17
18 // Prefix with underscore if starts with digit
19 if variant.chars().next().map_or(false, |c| c.is_ascii_digit()) {
20 format!("_{}", variant)
21 } else if variant.is_empty() {
22 "Unknown".to_string()
23 } else {
24 variant
25 }
26}
27
28/// Sanitize a string to be safe for identifiers and filenames
29fn sanitize_name(s: &str) -> String {
30 if s.is_empty() {
31 return "unknown".to_string();
32 }
33
34 // Replace invalid characters with underscores
35 let mut sanitized: String = s
36 .chars()
37 .map(|c| {
38 if c.is_alphanumeric() || c == '_' {
39 c
40 } else {
41 '_'
42 }
43 })
44 .collect();
45
46 // Ensure it doesn't start with a digit
47 if sanitized
48 .chars()
49 .next()
50 .map_or(false, |c| c.is_ascii_digit())
51 {
52 sanitized = format!("_{}", sanitized);
53 }
54
55 sanitized
56}
57
58/// Create an identifier, using raw identifier if necessary for keywords
59fn make_ident(s: &str) -> syn::Ident {
60 if s.is_empty() {
61 eprintln!("Warning: Empty identifier encountered, using 'unknown' as fallback");
62 return syn::Ident::new("unknown", proc_macro2::Span::call_site());
63 }
64
65 let sanitized = sanitize_name(s);
66
67 // Try to parse as ident, fall back to raw ident if needed
68 syn::parse_str::<syn::Ident>(&sanitized).unwrap_or_else(|_| {
69 eprintln!(
70 "Warning: Invalid identifier '{}' sanitized to '{}'",
71 s, sanitized
72 );
73 syn::Ident::new_raw(&sanitized, proc_macro2::Span::call_site())
74 })
75}
76
77/// Code generator for lexicon types
78pub struct CodeGenerator<'c> {
79 corpus: &'c LexiconCorpus,
80 root_module: String,
81}
82
83impl<'c> CodeGenerator<'c> {
84 /// Create a new code generator
85 pub fn new(corpus: &'c LexiconCorpus, root_module: impl Into<String>) -> Self {
86 Self {
87 corpus,
88 root_module: root_module.into(),
89 }
90 }
91
92 /// Generate code for a lexicon def
93 pub fn generate_def(
94 &self,
95 nsid: &str,
96 def_name: &str,
97 def: &LexUserType<'static>,
98 ) -> Result<TokenStream> {
99 match def {
100 LexUserType::Record(record) => self.generate_record(nsid, def_name, record),
101 LexUserType::Object(obj) => self.generate_object(nsid, def_name, obj),
102 LexUserType::XrpcQuery(query) => self.generate_query(nsid, def_name, query),
103 LexUserType::XrpcProcedure(proc) => self.generate_procedure(nsid, def_name, proc),
104 LexUserType::Token(_) => {
105 // Token types are marker types used in knownValues enums.
106 // We don't generate anything for them - the knownValues enum
107 // is the actual type that gets used.
108 Ok(quote! {})
109 }
110 LexUserType::String(s) if s.known_values.is_some() => {
111 self.generate_known_values_enum(nsid, def_name, s)
112 }
113 LexUserType::String(s) => {
114 // Plain string type alias
115 let type_name = self.def_to_type_name(nsid, def_name);
116 let ident = syn::Ident::new(&type_name, proc_macro2::Span::call_site());
117 let rust_type = self.string_to_rust_type(s);
118 let doc = self.generate_doc_comment(s.description.as_ref());
119 Ok(quote! {
120 #doc
121 pub type #ident<'a> = #rust_type;
122 })
123 }
124 LexUserType::Integer(i) if i.r#enum.is_some() => {
125 self.generate_integer_enum(nsid, def_name, i)
126 }
127 LexUserType::Array(array) => {
128 // Top-level array becomes type alias to Vec<ItemType>
129 let type_name = self.def_to_type_name(nsid, def_name);
130 let ident = syn::Ident::new(&type_name, proc_macro2::Span::call_site());
131 let item_type = self.array_item_to_rust_type(nsid, &array.items)?;
132 let doc = self.generate_doc_comment(array.description.as_ref());
133 let needs_lifetime = self.array_item_needs_lifetime(&array.items);
134 if needs_lifetime {
135 Ok(quote! {
136 #doc
137 pub type #ident<'a> = Vec<#item_type>;
138 })
139 } else {
140 Ok(quote! {
141 #doc
142 pub type #ident = Vec<#item_type>;
143 })
144 }
145 }
146 LexUserType::Boolean(_)
147 | LexUserType::Integer(_)
148 | LexUserType::Bytes(_)
149 | LexUserType::CidLink(_)
150 | LexUserType::Unknown(_) => {
151 // These are rarely top-level defs, but if they are, make type aliases
152 let type_name = self.def_to_type_name(nsid, def_name);
153 let ident = syn::Ident::new(&type_name, proc_macro2::Span::call_site());
154 let (rust_type, needs_lifetime) = match def {
155 LexUserType::Boolean(_) => (quote! { bool }, false),
156 LexUserType::Integer(_) => (quote! { i64 }, false),
157 LexUserType::Bytes(_) => (quote! { bytes::Bytes }, false),
158 LexUserType::CidLink(_) => {
159 (quote! { jacquard_common::types::cid::CidLink<'a> }, true)
160 }
161 LexUserType::Unknown(_) => {
162 (quote! { jacquard_common::types::value::Data<'a> }, true)
163 }
164 _ => unreachable!(),
165 };
166 if needs_lifetime {
167 Ok(quote! {
168 pub type #ident<'a> = #rust_type;
169 })
170 } else {
171 Ok(quote! {
172 pub type #ident = #rust_type;
173 })
174 }
175 }
176 LexUserType::Blob(_) => Err(CodegenError::unsupported(
177 format!("top-level def type {:?}", def),
178 nsid,
179 None::<String>,
180 )),
181 LexUserType::XrpcSubscription(sub) => self.generate_subscription(nsid, def_name, sub),
182 }
183 }
184
185 /// Generate a record type
186 fn generate_record(
187 &self,
188 nsid: &str,
189 def_name: &str,
190 record: &LexRecord<'static>,
191 ) -> Result<TokenStream> {
192 match &record.record {
193 crate::lexicon::LexRecordRecord::Object(obj) => {
194 let type_name = self.def_to_type_name(nsid, def_name);
195 let ident = syn::Ident::new(&type_name, proc_macro2::Span::call_site());
196
197 // Generate main struct fields
198 let fields = self.generate_object_fields(nsid, &type_name, obj, false)?;
199 let doc = self.generate_doc_comment(record.description.as_ref());
200
201 // Records always get a lifetime since they have the #[lexicon] attribute
202 // which adds extra_data: BTreeMap<..., Data<'a>>
203 let struct_def = quote! {
204 #doc
205 #[jacquard_derive::lexicon]
206 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, jacquard_derive::IntoStatic)]
207 #[serde(rename_all = "camelCase")]
208 pub struct #ident<'a> {
209 #fields
210 }
211 };
212
213 // Generate union types and nested object types for this record
214 let mut unions = Vec::new();
215 for (field_name, field_type) in &obj.properties {
216 match field_type {
217 LexObjectProperty::Union(union) => {
218 let union_name =
219 format!("{}Record{}", type_name, field_name.to_pascal_case());
220 // Clone refs to avoid lifetime issues
221 let refs: Vec<_> = union.refs.iter().cloned().collect();
222 let union_def =
223 self.generate_union(&union_name, &refs, None, union.closed)?;
224 unions.push(union_def);
225 }
226 LexObjectProperty::Object(nested_obj) => {
227 let object_name =
228 format!("{}Record{}", type_name, field_name.to_pascal_case());
229 let obj_def = self.generate_object(nsid, &object_name, nested_obj)?;
230 unions.push(obj_def);
231 }
232 _ => {}
233 }
234 }
235
236 // Generate Collection trait impl
237 let collection_impl = quote! {
238 impl jacquard_common::types::collection::Collection for #ident<'_> {
239 const NSID: &'static str = #nsid;
240 }
241 };
242
243 // Generate IntoStatic impl
244 // let field_names: Vec<&str> = obj.properties.keys().map(|k| k.as_str()).collect();
245 // let into_static_impl =
246 // self.generate_into_static_for_struct(&type_name, &field_names, true, true);
247
248 Ok(quote! {
249 #struct_def
250 #(#unions)*
251 #collection_impl
252 //#into_static_impl
253 })
254 }
255 }
256 }
257
258 /// Generate an object type
259 fn generate_object(
260 &self,
261 nsid: &str,
262 def_name: &str,
263 obj: &LexObject<'static>,
264 ) -> Result<TokenStream> {
265 let type_name = self.def_to_type_name(nsid, def_name);
266 let ident = syn::Ident::new(&type_name, proc_macro2::Span::call_site());
267
268 let fields = self.generate_object_fields(nsid, &type_name, obj, false)?;
269 let doc = self.generate_doc_comment(obj.description.as_ref());
270
271 // Objects always get a lifetime since they have the #[lexicon] attribute
272 // which adds extra_data: BTreeMap<..., Data<'a>>
273 let struct_def = quote! {
274 #doc
275 #[jacquard_derive::lexicon]
276 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, jacquard_derive::IntoStatic)]
277 #[serde(rename_all = "camelCase")]
278 pub struct #ident<'a> {
279 #fields
280 }
281 };
282
283 // Generate union types and nested object types for this object
284 let mut unions = Vec::new();
285 for (field_name, field_type) in &obj.properties {
286 match field_type {
287 LexObjectProperty::Union(union) => {
288 let union_name = format!("{}Record{}", type_name, field_name.to_pascal_case());
289 let refs: Vec<_> = union.refs.iter().cloned().collect();
290 let union_def = self.generate_union(&union_name, &refs, None, union.closed)?;
291 unions.push(union_def);
292 }
293 LexObjectProperty::Object(nested_obj) => {
294 let object_name = format!("{}Record{}", type_name, field_name.to_pascal_case());
295 let obj_def = self.generate_object(nsid, &object_name, nested_obj)?;
296 unions.push(obj_def);
297 }
298 _ => {}
299 }
300 }
301
302 // Generate IntoStatic impl
303 // let field_names: Vec<&str> = obj.properties.keys().map(|k| k.as_str()).collect();
304 // let into_static_impl =
305 // self.generate_into_static_for_struct(&type_name, &field_names, true, true);
306
307 Ok(quote! {
308 #struct_def
309 #(#unions)*
310 //#into_static_impl
311 })
312 }
313
314 /// Generate fields for an object
315 fn generate_object_fields(
316 &self,
317 nsid: &str,
318 parent_type_name: &str,
319 obj: &LexObject<'static>,
320 is_builder: bool,
321 ) -> Result<TokenStream> {
322 let required = obj.required.as_ref().map(|r| r.as_slice()).unwrap_or(&[]);
323
324 let mut fields = Vec::new();
325 for (field_name, field_type) in &obj.properties {
326 let is_required = required.contains(field_name);
327 let field_tokens = self.generate_field(
328 nsid,
329 parent_type_name,
330 field_name,
331 field_type,
332 is_required,
333 is_builder,
334 )?;
335 fields.push(field_tokens);
336 }
337
338 Ok(quote! { #(#fields)* })
339 }
340
341 /// Generate a single field
342 fn generate_field(
343 &self,
344 nsid: &str,
345 parent_type_name: &str,
346 field_name: &str,
347 field_type: &LexObjectProperty<'static>,
348 is_required: bool,
349 is_builder: bool,
350 ) -> Result<TokenStream> {
351 if field_name.is_empty() {
352 eprintln!(
353 "Warning: Empty field name in lexicon '{}' type '{}', using 'unknown' as fallback",
354 nsid, parent_type_name
355 );
356 }
357 let field_ident = make_ident(&field_name.to_snake_case());
358
359 let rust_type =
360 self.property_to_rust_type(nsid, parent_type_name, field_name, field_type)?;
361 let needs_lifetime = self.property_needs_lifetime(field_type);
362
363 // Check if this is a CowStr field for builder(into) attribute
364 let is_cowstr = matches!(field_type, LexObjectProperty::String(s) if s.format.is_none());
365
366 let rust_type = if is_required {
367 rust_type
368 } else {
369 quote! { std::option::Option<#rust_type> }
370 };
371
372 // Extract description from field type
373 let description = match field_type {
374 LexObjectProperty::Ref(r) => r.description.as_ref(),
375 LexObjectProperty::Union(u) => u.description.as_ref(),
376 LexObjectProperty::Bytes(b) => b.description.as_ref(),
377 LexObjectProperty::CidLink(c) => c.description.as_ref(),
378 LexObjectProperty::Array(a) => a.description.as_ref(),
379 LexObjectProperty::Blob(b) => b.description.as_ref(),
380 LexObjectProperty::Object(o) => o.description.as_ref(),
381 LexObjectProperty::Boolean(b) => b.description.as_ref(),
382 LexObjectProperty::Integer(i) => i.description.as_ref(),
383 LexObjectProperty::String(s) => s.description.as_ref(),
384 LexObjectProperty::Unknown(u) => u.description.as_ref(),
385 };
386 let doc = self.generate_doc_comment(description);
387
388 let mut attrs = Vec::new();
389
390 if !is_required {
391 attrs.push(quote! { #[serde(skip_serializing_if = "std::option::Option::is_none")] });
392 }
393
394 // Add serde(borrow) to all fields with lifetimes
395 if needs_lifetime {
396 attrs.push(quote! { #[serde(borrow)] });
397 }
398
399 // Add builder(into) for CowStr fields to allow String, &str, etc., but only for builder structs
400 if is_builder && is_cowstr {
401 attrs.push(quote! { #[builder(into)] });
402 }
403
404 Ok(quote! {
405 #doc
406 #(#attrs)*
407 pub #field_ident: #rust_type,
408 })
409 }
410
411 /// Check if a property type needs a lifetime parameter
412 fn property_needs_lifetime(&self, prop: &LexObjectProperty<'static>) -> bool {
413 match prop {
414 LexObjectProperty::Boolean(_) | LexObjectProperty::Integer(_) => false,
415 LexObjectProperty::String(s) => self.string_needs_lifetime(s),
416 LexObjectProperty::Bytes(_) => false, // Bytes is owned
417 LexObjectProperty::CidLink(_)
418 | LexObjectProperty::Blob(_)
419 | LexObjectProperty::Unknown(_) => true,
420 LexObjectProperty::Array(array) => self.array_item_needs_lifetime(&array.items),
421 LexObjectProperty::Object(_) => true, // Nested objects have lifetimes
422 LexObjectProperty::Ref(ref_type) => {
423 // Check if the ref target actually needs a lifetime
424 self.ref_needs_lifetime(&ref_type.r#ref)
425 }
426 LexObjectProperty::Union(_) => true, // Unions generally have lifetimes
427 }
428 }
429
430 /// Check if an array item type needs a lifetime parameter
431 fn array_item_needs_lifetime(&self, item: &LexArrayItem) -> bool {
432 match item {
433 LexArrayItem::Boolean(_) | LexArrayItem::Integer(_) => false,
434 LexArrayItem::String(s) => self.string_needs_lifetime(s),
435 LexArrayItem::Bytes(_) => false,
436 LexArrayItem::CidLink(_) | LexArrayItem::Blob(_) | LexArrayItem::Unknown(_) => true,
437 LexArrayItem::Object(_) => true, // Nested objects have lifetimes
438 LexArrayItem::Ref(ref_type) => self.ref_needs_lifetime(&ref_type.r#ref),
439 LexArrayItem::Union(_) => true,
440 }
441 }
442
443 /// Check if a string type needs a lifetime parameter
444 fn string_needs_lifetime(&self, s: &LexString) -> bool {
445 match s.format {
446 Some(LexStringFormat::Datetime)
447 | Some(LexStringFormat::Language)
448 | Some(LexStringFormat::Tid) => false,
449 _ => true, // Most string types borrow
450 }
451 }
452
453 /// Check if a ref needs a lifetime parameter
454 fn ref_needs_lifetime(&self, ref_str: &str) -> bool {
455 // Try to resolve the ref
456 if let Some((_doc, def)) = self.corpus.resolve_ref(ref_str) {
457 self.def_needs_lifetime(def)
458 } else {
459 // If we can't resolve it, assume it needs a lifetime (safe default)
460 true
461 }
462 }
463
464 /// Check if a lexicon def needs a lifetime parameter
465 fn def_needs_lifetime(&self, def: &LexUserType<'static>) -> bool {
466 match def {
467 // Records and Objects always have lifetimes now since they get #[lexicon] attribute
468 LexUserType::Record(_) => true,
469 LexUserType::Object(_) => true,
470 LexUserType::Token(_) => false,
471 LexUserType::String(s) => {
472 // Check if it's a known values enum or a regular string
473 if s.known_values.is_some() {
474 // Known values enums have Other(CowStr<'a>) variant
475 true
476 } else {
477 self.string_needs_lifetime(s)
478 }
479 }
480 LexUserType::Integer(_) => false,
481 LexUserType::Boolean(_) => false,
482 LexUserType::Bytes(_) => false,
483 LexUserType::CidLink(_) | LexUserType::Blob(_) | LexUserType::Unknown(_) => true,
484 LexUserType::Array(array) => self.array_item_needs_lifetime(&array.items),
485 LexUserType::XrpcQuery(_)
486 | LexUserType::XrpcProcedure(_)
487 | LexUserType::XrpcSubscription(_) => {
488 // XRPC types generate multiple structs, not a single type we can reference
489 // Shouldn't be referenced directly
490 true
491 }
492 }
493 }
494
495 /// Check if xrpc params need a lifetime parameter
496 fn params_need_lifetime(&self, params: &crate::lexicon::LexXrpcParameters<'static>) -> bool {
497 params.properties.values().any(|prop| {
498 use crate::lexicon::LexXrpcParametersProperty;
499 match prop {
500 LexXrpcParametersProperty::Boolean(_) | LexXrpcParametersProperty::Integer(_) => {
501 false
502 }
503 LexXrpcParametersProperty::String(s) => self.string_needs_lifetime(s),
504 LexXrpcParametersProperty::Unknown(_) => true,
505 LexXrpcParametersProperty::Array(arr) => {
506 use crate::lexicon::LexPrimitiveArrayItem;
507 match &arr.items {
508 LexPrimitiveArrayItem::Boolean(_) | LexPrimitiveArrayItem::Integer(_) => {
509 false
510 }
511 LexPrimitiveArrayItem::String(s) => self.string_needs_lifetime(s),
512 LexPrimitiveArrayItem::Unknown(_) => true,
513 }
514 }
515 }
516 })
517 }
518
519 /// Convert a property type to Rust type
520 fn property_to_rust_type(
521 &self,
522 nsid: &str,
523 parent_type_name: &str,
524 field_name: &str,
525 prop: &LexObjectProperty<'static>,
526 ) -> Result<TokenStream> {
527 match prop {
528 LexObjectProperty::Boolean(_) => Ok(quote! { bool }),
529 LexObjectProperty::Integer(_) => Ok(quote! { i64 }),
530 LexObjectProperty::String(s) => Ok(self.string_to_rust_type(s)),
531 LexObjectProperty::Bytes(_) => Ok(quote! { bytes::Bytes }),
532 LexObjectProperty::CidLink(_) => {
533 Ok(quote! { jacquard_common::types::cid::CidLink<'a> })
534 }
535 LexObjectProperty::Blob(_) => Ok(quote! { jacquard_common::types::blob::Blob<'a> }),
536 LexObjectProperty::Unknown(_) => Ok(quote! { jacquard_common::types::value::Data<'a> }),
537 LexObjectProperty::Array(array) => {
538 let item_type = self.array_item_to_rust_type(nsid, &array.items)?;
539 Ok(quote! { Vec<#item_type> })
540 }
541 LexObjectProperty::Object(_object) => {
542 // Generate unique nested object type name: StatusView + metadata -> StatusViewRecordMetadata
543 let object_name =
544 format!("{}Record{}", parent_type_name, field_name.to_pascal_case());
545 let object_ident = syn::Ident::new(&object_name, proc_macro2::Span::call_site());
546 Ok(quote! { #object_ident<'a> })
547 }
548 LexObjectProperty::Ref(ref_type) => {
549 // Handle local refs (starting with #) by prepending the current NSID
550 let ref_str = if ref_type.r#ref.starts_with('#') {
551 format!("{}{}", nsid, ref_type.r#ref)
552 } else {
553 ref_type.r#ref.to_string()
554 };
555 self.ref_to_rust_type(&ref_str)
556 }
557 LexObjectProperty::Union(_union) => {
558 // Generate unique union type name: StatusView + embed -> StatusViewRecordEmbed
559 let union_name =
560 format!("{}Record{}", parent_type_name, field_name.to_pascal_case());
561 let union_ident = syn::Ident::new(&union_name, proc_macro2::Span::call_site());
562 Ok(quote! { #union_ident<'a> })
563 }
564 }
565 }
566
567 /// Convert array item to Rust type
568 fn array_item_to_rust_type(&self, nsid: &str, item: &LexArrayItem) -> Result<TokenStream> {
569 match item {
570 LexArrayItem::Boolean(_) => Ok(quote! { bool }),
571 LexArrayItem::Integer(_) => Ok(quote! { i64 }),
572 LexArrayItem::String(s) => Ok(self.string_to_rust_type(s)),
573 LexArrayItem::Bytes(_) => Ok(quote! { bytes::Bytes }),
574 LexArrayItem::CidLink(_) => Ok(quote! { jacquard_common::types::cid::CidLink<'a> }),
575 LexArrayItem::Blob(_) => Ok(quote! { jacquard_common::types::blob::Blob<'a> }),
576 LexArrayItem::Unknown(_) => Ok(quote! { jacquard_common::types::value::Data<'a> }),
577 LexArrayItem::Object(_) => {
578 // For inline objects in arrays, use Data since we can't generate a unique type name
579 Ok(quote! { jacquard_common::types::value::Data<'a> })
580 }
581 LexArrayItem::Ref(ref_type) => {
582 // Handle local refs (starting with #) by prepending the current NSID
583 let ref_str = if ref_type.r#ref.starts_with('#') {
584 format!("{}{}", nsid, ref_type.r#ref)
585 } else {
586 ref_type.r#ref.to_string()
587 };
588 self.ref_to_rust_type(&ref_str)
589 }
590 LexArrayItem::Union(_) => {
591 // For now, use Data
592 Ok(quote! { jacquard_common::types::value::Data<'a> })
593 }
594 }
595 }
596
597 /// Convert string type to Rust type
598 fn string_to_rust_type(&self, s: &LexString) -> TokenStream {
599 match s.format {
600 Some(LexStringFormat::Datetime) => {
601 quote! { jacquard_common::types::string::Datetime }
602 }
603 Some(LexStringFormat::Did) => quote! { jacquard_common::types::string::Did<'a> },
604 Some(LexStringFormat::Handle) => quote! { jacquard_common::types::string::Handle<'a> },
605 Some(LexStringFormat::AtIdentifier) => {
606 quote! { jacquard_common::types::ident::AtIdentifier<'a> }
607 }
608 Some(LexStringFormat::Nsid) => quote! { jacquard_common::types::string::Nsid<'a> },
609 Some(LexStringFormat::AtUri) => quote! { jacquard_common::types::string::AtUri<'a> },
610 Some(LexStringFormat::Uri) => quote! { jacquard_common::types::string::Uri<'a> },
611 Some(LexStringFormat::Cid) => quote! { jacquard_common::types::string::Cid<'a> },
612 Some(LexStringFormat::Language) => {
613 quote! { jacquard_common::types::string::Language }
614 }
615 Some(LexStringFormat::Tid) => quote! { jacquard_common::types::string::Tid },
616 Some(LexStringFormat::RecordKey) => {
617 quote! { jacquard_common::types::string::RecordKey<jacquard_common::types::string::Rkey<'a>> }
618 }
619 _ => quote! { jacquard_common::CowStr<'a> },
620 }
621 }
622
623 /// Convert ref to Rust type path
624 fn ref_to_rust_type(&self, ref_str: &str) -> Result<TokenStream> {
625 // Parse NSID and fragment
626 let (ref_nsid, ref_def) = if let Some((nsid, fragment)) = ref_str.split_once('#') {
627 (nsid, fragment)
628 } else {
629 (ref_str, "main")
630 };
631
632 // Check if ref exists
633 if !self.corpus.ref_exists(ref_str) {
634 // Fallback to Data
635 return Ok(quote! { jacquard_common::types::value::Data<'a> });
636 }
637
638 // Convert NSID to module path
639 // com.atproto.repo.strongRef -> com_atproto::repo::strong_ref::StrongRef
640 // app.bsky.richtext.facet -> app_bsky::richtext::facet::Facet
641 // app.bsky.actor.defs#nux -> app_bsky::actor::Nux (defs go in parent module)
642 let parts: Vec<&str> = ref_nsid.split('.').collect();
643 let last_segment = parts.last().unwrap();
644
645 let type_name = self.def_to_type_name(ref_nsid, ref_def);
646
647 let path_str = if *last_segment == "defs" && parts.len() >= 3 {
648 // defs types go in parent module
649 let first_two = format!("{}_{}", sanitize_name(parts[0]), sanitize_name(parts[1]));
650 if parts.len() == 3 {
651 // com.atproto.defs -> com_atproto::TypeName
652 format!("{}::{}::{}", self.root_module, first_two, type_name)
653 } else {
654 // app.bsky.actor.defs -> app_bsky::actor::TypeName
655 let middle: Vec<_> = parts[2..parts.len() - 1]
656 .iter()
657 .copied()
658 .map(|s| sanitize_name(s))
659 .collect();
660 format!(
661 "{}::{}::{}::{}",
662 self.root_module,
663 first_two,
664 middle.join("::"),
665 type_name
666 )
667 }
668 } else {
669 // Regular types go in their own module file
670 let (module_path, file_module) = if parts.len() >= 3 {
671 // Join first two segments with underscore
672 let first_two = format!("{}_{}", sanitize_name(parts[0]), sanitize_name(parts[1]));
673 let file_name = sanitize_name(last_segment).to_snake_case();
674
675 if parts.len() > 3 {
676 // Middle segments form the module path
677 let middle: Vec<_> = parts[2..parts.len() - 1]
678 .iter()
679 .copied()
680 .map(|s| sanitize_name(s))
681 .collect();
682 let base_path = format!("{}::{}", first_two, middle.join("::"));
683 (base_path, file_name)
684 } else {
685 // Only 3 parts: com.atproto.label -> com_atproto, file: label
686 (first_two, file_name)
687 }
688 } else if parts.len() == 2 {
689 // e.g., "com.example" -> "com_example", file: example
690 let first = sanitize_name(parts[0]);
691 let file_name = sanitize_name(parts[1]).to_snake_case();
692 (first, file_name)
693 } else {
694 (parts[0].to_string(), "main".to_string())
695 };
696
697 format!(
698 "{}::{}::{}::{}",
699 self.root_module, module_path, file_module, type_name
700 )
701 };
702
703 let path: syn::Path = syn::parse_str(&path_str).map_err(|e| CodegenError::Other {
704 message: format!("Failed to parse path: {} {}", path_str, e),
705 source: None,
706 })?;
707
708 // Only add lifetime if the target type needs it
709 if self.ref_needs_lifetime(ref_str) {
710 Ok(quote! { #path<'a> })
711 } else {
712 Ok(quote! { #path })
713 }
714 }
715
716 /// Generate query type
717 fn generate_query(
718 &self,
719 nsid: &str,
720 def_name: &str,
721 query: &LexXrpcQuery<'static>,
722 ) -> Result<TokenStream> {
723 let type_base = self.def_to_type_name(nsid, def_name);
724 let mut output = Vec::new();
725
726 let params_has_lifetime = query
727 .parameters
728 .as_ref()
729 .map(|p| match p {
730 crate::lexicon::LexXrpcQueryParameter::Params(params) => {
731 self.params_need_lifetime(params)
732 }
733 })
734 .unwrap_or(false);
735 let has_params = query.parameters.is_some();
736 let has_output = query.output.is_some();
737 let has_errors = query.errors.is_some();
738
739 if let Some(params) = &query.parameters {
740 let params_struct = self.generate_params_struct(&type_base, params)?;
741 output.push(params_struct);
742 }
743
744 if let Some(body) = &query.output {
745 let output_struct = self.generate_output_struct(&type_base, body)?;
746 output.push(output_struct);
747 }
748
749 if let Some(errors) = &query.errors {
750 let error_enum = self.generate_error_enum(&type_base, errors)?;
751 output.push(error_enum);
752 }
753
754 // Generate XrpcRequest impl
755 let output_encoding = query
756 .output
757 .as_ref()
758 .map(|o| o.encoding.as_ref())
759 .unwrap_or("application/json");
760 let xrpc_impl = self.generate_xrpc_request_impl(
761 nsid,
762 &type_base,
763 quote! { jacquard_common::xrpc::XrpcMethod::Query },
764 output_encoding,
765 has_params,
766 params_has_lifetime,
767 has_output,
768 has_errors,
769 false, // queries never have binary inputs
770 )?;
771 output.push(xrpc_impl);
772
773 Ok(quote! {
774 #(#output)*
775 })
776 }
777
778 /// Generate procedure type
779 fn generate_procedure(
780 &self,
781 nsid: &str,
782 def_name: &str,
783 proc: &LexXrpcProcedure<'static>,
784 ) -> Result<TokenStream> {
785 let type_base = self.def_to_type_name(nsid, def_name);
786 let mut output = Vec::new();
787
788 // Check if input is a binary body (no schema)
789 let is_binary_input = proc
790 .input
791 .as_ref()
792 .map(|i| i.schema.is_none())
793 .unwrap_or(false);
794
795 // Input bodies with schemas have lifetimes (they get #[lexicon] attribute)
796 // Binary inputs don't have lifetimes
797 let params_has_lifetime = proc.input.is_some() && !is_binary_input;
798 let has_input = proc.input.is_some();
799 let has_output = proc.output.is_some();
800 let has_errors = proc.errors.is_some();
801
802 if let Some(params) = &proc.parameters {
803 let params_struct = self.generate_params_struct_proc(&type_base, params)?;
804 output.push(params_struct);
805 }
806
807 if let Some(body) = &proc.input {
808 let input_struct = self.generate_input_struct(&type_base, body)?;
809 output.push(input_struct);
810 }
811
812 if let Some(body) = &proc.output {
813 let output_struct = self.generate_output_struct(&type_base, body)?;
814 output.push(output_struct);
815 }
816
817 if let Some(errors) = &proc.errors {
818 let error_enum = self.generate_error_enum(&type_base, errors)?;
819 output.push(error_enum);
820 }
821
822 // Generate XrpcRequest impl
823 let input_encoding = proc
824 .input
825 .as_ref()
826 .map(|i| i.encoding.as_ref())
827 .unwrap_or("application/json");
828 let output_encoding = proc
829 .output
830 .as_ref()
831 .map(|o| o.encoding.as_ref())
832 .unwrap_or("application/json");
833 let xrpc_impl = self.generate_xrpc_request_impl(
834 nsid,
835 &type_base,
836 quote! { jacquard_common::xrpc::XrpcMethod::Procedure(#input_encoding) },
837 output_encoding,
838 has_input,
839 params_has_lifetime,
840 has_output,
841 has_errors,
842 is_binary_input,
843 )?;
844 output.push(xrpc_impl);
845
846 Ok(quote! {
847 #(#output)*
848 })
849 }
850
851 fn generate_subscription(
852 &self,
853 nsid: &str,
854 def_name: &str,
855 sub: &LexXrpcSubscription<'static>,
856 ) -> Result<TokenStream> {
857 let type_base = self.def_to_type_name(nsid, def_name);
858 let mut output = Vec::new();
859
860 if let Some(params) = &sub.parameters {
861 // Extract LexXrpcParameters from the enum
862 match params {
863 crate::lexicon::LexXrpcSubscriptionParameter::Params(params_inner) => {
864 let params_struct =
865 self.generate_params_struct_inner(&type_base, params_inner)?;
866 output.push(params_struct);
867 }
868 }
869 }
870
871 if let Some(message) = &sub.message {
872 if let Some(schema) = &message.schema {
873 let message_type = self.generate_subscription_message(&type_base, schema)?;
874 output.push(message_type);
875 }
876 }
877
878 if let Some(errors) = &sub.errors {
879 let error_enum = self.generate_error_enum(&type_base, errors)?;
880 output.push(error_enum);
881 }
882
883 Ok(quote! {
884 #(#output)*
885 })
886 }
887
888 fn generate_subscription_message(
889 &self,
890 type_base: &str,
891 schema: &LexXrpcSubscriptionMessageSchema<'static>,
892 ) -> Result<TokenStream> {
893 use crate::lexicon::LexXrpcSubscriptionMessageSchema;
894
895 match schema {
896 LexXrpcSubscriptionMessageSchema::Union(union) => {
897 // Generate a union enum for the message
898 let enum_name = format!("{}Message", type_base);
899 let enum_ident = syn::Ident::new(&enum_name, proc_macro2::Span::call_site());
900
901 let mut variants = Vec::new();
902 for ref_str in &union.refs {
903 let ref_str_s = ref_str.as_ref();
904 // Parse ref to get NSID and def name
905 let (ref_nsid, ref_def) =
906 if let Some((nsid, fragment)) = ref_str.split_once('#') {
907 (nsid, fragment)
908 } else {
909 (ref_str.as_ref(), "main")
910 };
911
912 let variant_name = if ref_def == "main" {
913 ref_nsid.split('.').last().unwrap().to_pascal_case()
914 } else {
915 ref_def.to_pascal_case()
916 };
917 let variant_ident =
918 syn::Ident::new(&variant_name, proc_macro2::Span::call_site());
919 let type_path = self.ref_to_rust_type(ref_str)?;
920
921 variants.push(quote! {
922 #[serde(rename = #ref_str_s)]
923 #variant_ident(Box<#type_path>)
924 });
925 }
926
927 let doc = self.generate_doc_comment(union.description.as_ref());
928
929 // Generate IntoStatic impl for the enum
930 // let variant_info: Vec<(String, EnumVariantKind)> = union
931 // .refs
932 // .iter()
933 // .map(|ref_str| {
934 // let ref_def = if let Some((_, fragment)) = ref_str.split_once('#') {
935 // fragment
936 // } else {
937 // "main"
938 // };
939 // let variant_name = if ref_def == "main" {
940 // ref_str.split('.').last().unwrap().to_pascal_case()
941 // } else {
942 // ref_def.to_pascal_case()
943 // };
944 // (variant_name, EnumVariantKind::Tuple)
945 // })
946 // .collect();
947 // let into_static_impl = self.generate_into_static_for_enum(
948 // &enum_name,
949 // &variant_info,
950 // true,
951 // true, // open union
952 // );
953
954 Ok(quote! {
955 #doc
956 #[jacquard_derive::open_union]
957 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, jacquard_derive::IntoStatic)]
958 #[serde(tag = "$type")]
959 #[serde(bound(deserialize = "'de: 'a"))]
960 pub enum #enum_ident<'a> {
961 #(#variants,)*
962 }
963
964 //#into_static_impl
965 })
966 }
967 LexXrpcSubscriptionMessageSchema::Object(obj) => {
968 // Generate a struct for the message
969 let struct_name = format!("{}Message", type_base);
970 let struct_ident = syn::Ident::new(&struct_name, proc_macro2::Span::call_site());
971
972 let fields = self.generate_object_fields("", &struct_name, obj, false)?;
973 let doc = self.generate_doc_comment(obj.description.as_ref());
974
975 // Subscription message structs always get a lifetime since they have the #[lexicon] attribute
976 // which adds extra_data: BTreeMap<..., Data<'a>>
977 let struct_def = quote! {
978 #doc
979 #[jacquard_derive::lexicon]
980 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, jacquard_derive::IntoStatic)]
981 #[serde(rename_all = "camelCase")]
982 pub struct #struct_ident<'a> {
983 #fields
984 }
985 };
986
987 // Generate union types for this message
988 let mut unions = Vec::new();
989 for (field_name, field_type) in &obj.properties {
990 if let LexObjectProperty::Union(union) = field_type {
991 let union_name =
992 format!("{}Record{}", struct_name, field_name.to_pascal_case());
993 let refs: Vec<_> = union.refs.iter().cloned().collect();
994 let union_def =
995 self.generate_union(&union_name, &refs, None, union.closed)?;
996 unions.push(union_def);
997 }
998 }
999
1000 // Generate IntoStatic impl
1001 // let field_names: Vec<&str> = obj.properties.keys().map(|k| k.as_str()).collect();
1002 // let into_static_impl =
1003 // self.generate_into_static_for_struct(&struct_name, &field_names, true, true);
1004
1005 Ok(quote! {
1006 #struct_def
1007 #(#unions)*
1008 //#into_static_impl
1009 })
1010 }
1011 LexXrpcSubscriptionMessageSchema::Ref(ref_type) => {
1012 // Just a type alias to the referenced type
1013 // Refs generally have lifetimes, so always add <'a>
1014 let type_name = format!("{}Message", type_base);
1015 let ident = syn::Ident::new(&type_name, proc_macro2::Span::call_site());
1016 let rust_type = self.ref_to_rust_type(&ref_type.r#ref)?;
1017 let doc = self.generate_doc_comment(ref_type.description.as_ref());
1018
1019 Ok(quote! {
1020 #doc
1021 pub type #ident<'a> = #rust_type;
1022 })
1023 }
1024 }
1025 }
1026
1027 /// Convert def name to Rust type name
1028 fn def_to_type_name(&self, nsid: &str, def_name: &str) -> String {
1029 if def_name == "main" {
1030 // Use last segment of NSID
1031 let base_name = nsid.split('.').last().unwrap().to_pascal_case();
1032
1033 // Check if any other def would collide with this name
1034 if let Some(doc) = self.corpus.get(nsid) {
1035 let has_collision = doc.defs.keys().any(|other_def| {
1036 let other_def_str: &str = other_def.as_ref();
1037 other_def_str != "main" && other_def_str.to_pascal_case() == base_name
1038 });
1039
1040 if has_collision {
1041 return format!("{}Record", base_name);
1042 }
1043 }
1044
1045 base_name
1046 } else {
1047 def_name.to_pascal_case()
1048 }
1049 }
1050
1051 /// Convert NSID to file path relative to output directory
1052 ///
1053 /// - `app.bsky.feed.post` → `app_bsky/feed/post.rs`
1054 /// - `com.atproto.label.defs` → `com_atproto/label.rs` (defs go in parent)
1055 fn nsid_to_file_path(&self, nsid: &str) -> std::path::PathBuf {
1056 let parts: Vec<&str> = nsid.split('.').collect();
1057
1058 if parts.len() < 2 {
1059 // Shouldn't happen with valid NSIDs, but handle gracefully
1060 return format!("{}.rs", sanitize_name(parts[0])).into();
1061 }
1062
1063 let last = parts.last().unwrap();
1064
1065 if *last == "defs" && parts.len() >= 3 {
1066 // defs go in parent module: com.atproto.label.defs → com_atproto/label.rs
1067 let first_two = format!("{}_{}", sanitize_name(parts[0]), sanitize_name(parts[1]));
1068 if parts.len() == 3 {
1069 // com.atproto.defs → com_atproto.rs
1070 format!("{}.rs", first_two).into()
1071 } else {
1072 // com.atproto.label.defs → com_atproto/label.rs
1073 let middle: Vec<&str> = parts[2..parts.len() - 1].iter().copied().collect();
1074 let mut path = std::path::PathBuf::from(first_two);
1075 for segment in &middle[..middle.len() - 1] {
1076 path.push(sanitize_name(segment));
1077 }
1078 path.push(format!("{}.rs", sanitize_name(middle.last().unwrap())));
1079 path
1080 }
1081 } else {
1082 // Regular path: app.bsky.feed.post → app_bsky/feed/post.rs
1083 let first_two = format!("{}_{}", sanitize_name(parts[0]), sanitize_name(parts[1]));
1084 let mut path = std::path::PathBuf::from(first_two);
1085
1086 for segment in &parts[2..parts.len() - 1] {
1087 path.push(sanitize_name(segment));
1088 }
1089
1090 path.push(format!("{}.rs", sanitize_name(&last.to_snake_case())));
1091 path
1092 }
1093 }
1094
1095 /// Generate all code for the corpus, organized by file
1096 /// Returns a map of file paths to (tokens, optional NSID)
1097 pub fn generate_all(
1098 &self,
1099 ) -> Result<std::collections::BTreeMap<std::path::PathBuf, (TokenStream, Option<String>)>> {
1100 use std::collections::BTreeMap;
1101
1102 let mut file_contents: BTreeMap<std::path::PathBuf, Vec<TokenStream>> = BTreeMap::new();
1103 let mut file_nsids: BTreeMap<std::path::PathBuf, String> = BTreeMap::new();
1104
1105 // Generate code for all lexicons
1106 for (nsid, doc) in self.corpus.iter() {
1107 let file_path = self.nsid_to_file_path(nsid.as_ref());
1108
1109 // Track which NSID this file is for
1110 file_nsids.insert(file_path.clone(), nsid.to_string());
1111
1112 for (def_name, def) in &doc.defs {
1113 let tokens = self.generate_def(nsid.as_ref(), def_name.as_ref(), def)?;
1114 file_contents
1115 .entry(file_path.clone())
1116 .or_default()
1117 .push(tokens);
1118 }
1119 }
1120
1121 // Combine all tokens for each file
1122 let mut result = BTreeMap::new();
1123 for (path, tokens_vec) in file_contents {
1124 let nsid = file_nsids.get(&path).cloned();
1125 result.insert(path, (quote! { #(#tokens_vec)* }, nsid));
1126 }
1127
1128 Ok(result)
1129 }
1130
1131 /// Generate parent module files with pub mod declarations
1132 pub fn generate_module_tree(
1133 &self,
1134 file_map: &std::collections::BTreeMap<std::path::PathBuf, (TokenStream, Option<String>)>,
1135 defs_only: &std::collections::BTreeMap<std::path::PathBuf, (TokenStream, Option<String>)>,
1136 ) -> std::collections::BTreeMap<std::path::PathBuf, (TokenStream, Option<String>)> {
1137 use std::collections::{BTreeMap, BTreeSet};
1138
1139 // Track what modules each directory needs to declare
1140 // Key: directory path, Value: set of module names (file stems)
1141 let mut dir_modules: BTreeMap<std::path::PathBuf, BTreeSet<String>> = BTreeMap::new();
1142
1143 // Collect all parent directories that have files
1144 let mut all_dirs: BTreeSet<std::path::PathBuf> = BTreeSet::new();
1145 for path in file_map.keys() {
1146 if let Some(parent_dir) = path.parent() {
1147 all_dirs.insert(parent_dir.to_path_buf());
1148 }
1149 }
1150
1151 for path in file_map.keys() {
1152 if let Some(parent_dir) = path.parent() {
1153 if let Some(file_stem) = path.file_stem().and_then(|s| s.to_str()) {
1154 // Skip mod.rs and lib.rs - they're module files, not modules to declare
1155 if file_stem == "mod" || file_stem == "lib" {
1156 continue;
1157 }
1158
1159 // Always add the module declaration to parent
1160 dir_modules
1161 .entry(parent_dir.to_path_buf())
1162 .or_default()
1163 .insert(file_stem.to_string());
1164 }
1165 }
1166 }
1167
1168 // Generate module files
1169 let mut result = BTreeMap::new();
1170
1171 for (dir, module_names) in dir_modules {
1172 let mod_file_path = if dir.components().count() == 0 {
1173 // Root directory -> lib.rs for library crates
1174 std::path::PathBuf::from("lib.rs")
1175 } else {
1176 // Subdirectory: app_bsky/feed -> app_bsky/feed.rs (Rust 2018 style)
1177 let dir_name = dir.file_name().and_then(|s| s.to_str()).unwrap_or("mod");
1178 let sanitized_dir_name = sanitize_name(dir_name);
1179 let mut path = dir
1180 .parent()
1181 .unwrap_or_else(|| std::path::Path::new(""))
1182 .to_path_buf();
1183 path.push(format!("{}.rs", sanitized_dir_name));
1184 path
1185 };
1186
1187 let is_root = dir.components().count() == 0;
1188 let mods: Vec<_> = module_names
1189 .iter()
1190 .map(|name| {
1191 let ident = make_ident(name);
1192 if is_root {
1193 // Top-level modules get feature gates
1194 quote! {
1195 #[cfg(feature = #name)]
1196 pub mod #ident;
1197 }
1198 } else {
1199 quote! { pub mod #ident; }
1200 }
1201 })
1202 .collect();
1203
1204 // If this file already exists in defs_only (e.g., from defs), merge the content
1205 let module_tokens = quote! { #(#mods)* };
1206 if let Some((existing_tokens, nsid)) = defs_only.get(&mod_file_path) {
1207 // Put module declarations FIRST, then existing defs content
1208 result.insert(
1209 mod_file_path,
1210 (quote! { #module_tokens #existing_tokens }, nsid.clone()),
1211 );
1212 } else {
1213 result.insert(mod_file_path, (module_tokens, None));
1214 }
1215 }
1216
1217 result
1218 }
1219
1220 /// Write all generated code to disk
1221 pub fn write_to_disk(&self, output_dir: &std::path::Path) -> Result<()> {
1222 // Generate all code (defs only)
1223 let defs_files = self.generate_all()?;
1224 let mut all_files = defs_files.clone();
1225
1226 // Generate module tree iteratively until no new files appear
1227 loop {
1228 let module_map = self.generate_module_tree(&all_files, &defs_files);
1229 let old_count = all_files.len();
1230
1231 // Merge new module files
1232 for (path, tokens) in module_map {
1233 all_files.insert(path, tokens);
1234 }
1235
1236 if all_files.len() == old_count {
1237 // No new files added
1238 break;
1239 }
1240 }
1241
1242 // Write to disk
1243 for (path, (tokens, nsid)) in all_files {
1244 let full_path = output_dir.join(&path);
1245
1246 // Create parent directories
1247 if let Some(parent) = full_path.parent() {
1248 std::fs::create_dir_all(parent).map_err(|e| CodegenError::Other {
1249 message: format!("Failed to create directory {:?}: {}", parent, e),
1250 source: None,
1251 })?;
1252 }
1253
1254 // Format code
1255 let file: syn::File = syn::parse2(tokens.clone()).map_err(|e| CodegenError::Other {
1256 message: format!(
1257 "Failed to parse tokens for {:?}: {}\nTokens: {}",
1258 path, e, tokens
1259 ),
1260 source: None,
1261 })?;
1262 let mut formatted = prettyplease::unparse(&file);
1263
1264 // Add blank lines between top-level items for better readability
1265 let lines: Vec<&str> = formatted.lines().collect();
1266 let mut result_lines = Vec::new();
1267
1268 for (i, line) in lines.iter().enumerate() {
1269 result_lines.push(*line);
1270
1271 // Add blank line after closing braces that are at column 0 (top-level items)
1272 if *line == "}" && i + 1 < lines.len() && !lines[i + 1].is_empty() {
1273 result_lines.push("");
1274 }
1275
1276 // Add blank line after last pub mod declaration before structs/enums
1277 if line.starts_with("pub mod ") && i + 1 < lines.len() {
1278 let next_line = lines[i + 1];
1279 if !next_line.starts_with("pub mod ") && !next_line.is_empty() {
1280 result_lines.push("");
1281 }
1282 }
1283 }
1284
1285 formatted = result_lines.join("\n");
1286
1287 // Add header comment
1288 let header = if let Some(nsid) = nsid {
1289 format!(
1290 "// @generated by jacquard-lexicon. DO NOT EDIT.\n//\n// Lexicon: {}\n//\n// This file was automatically generated from Lexicon schemas.\n// Any manual changes will be overwritten on the next regeneration.\n\n",
1291 nsid
1292 )
1293 } else {
1294 "// @generated by jacquard-lexicon. DO NOT EDIT.\n//\n// This file was automatically generated from Lexicon schemas.\n// Any manual changes will be overwritten on the next regeneration.\n\n".to_string()
1295 };
1296 formatted = format!("{}{}", header, formatted);
1297
1298 // Write file
1299 std::fs::write(&full_path, formatted).map_err(|e| CodegenError::Other {
1300 message: format!("Failed to write file {:?}: {}", full_path, e),
1301 source: None,
1302 })?;
1303 }
1304
1305 Ok(())
1306 }
1307
1308 /// Generate doc comment from description
1309 fn generate_doc_comment(&self, desc: Option<&jacquard_common::CowStr>) -> TokenStream {
1310 if let Some(desc) = desc {
1311 let doc = desc.as_ref();
1312 quote! { #[doc = #doc] }
1313 } else {
1314 quote! {}
1315 }
1316 }
1317
1318 /// Generate params struct from XRPC query parameters
1319 fn generate_params_struct(
1320 &self,
1321 type_base: &str,
1322 params: &crate::lexicon::LexXrpcQueryParameter<'static>,
1323 ) -> Result<TokenStream> {
1324 use crate::lexicon::LexXrpcQueryParameter;
1325 match params {
1326 LexXrpcQueryParameter::Params(p) => self.generate_params_struct_inner(type_base, p),
1327 }
1328 }
1329
1330 /// Generate params struct from XRPC procedure parameters (query string params)
1331 fn generate_params_struct_proc(
1332 &self,
1333 type_base: &str,
1334 params: &crate::lexicon::LexXrpcProcedureParameter<'static>,
1335 ) -> Result<TokenStream> {
1336 use crate::lexicon::LexXrpcProcedureParameter;
1337 match params {
1338 // For procedures, query string params still get "Params" suffix since the main struct is the input
1339 LexXrpcProcedureParameter::Params(p) => {
1340 let struct_name = format!("{}Params", type_base);
1341 let ident = syn::Ident::new(&struct_name, proc_macro2::Span::call_site());
1342 self.generate_params_struct_inner_with_name(&ident, p)
1343 }
1344 }
1345 }
1346
1347 /// Generate params struct inner (shared implementation)
1348 fn generate_params_struct_inner(
1349 &self,
1350 type_base: &str,
1351 p: &crate::lexicon::LexXrpcParameters<'static>,
1352 ) -> Result<TokenStream> {
1353 let ident = syn::Ident::new(type_base, proc_macro2::Span::call_site());
1354 self.generate_params_struct_inner_with_name(&ident, p)
1355 }
1356
1357 /// Generate params struct with custom name
1358 fn generate_params_struct_inner_with_name(
1359 &self,
1360 ident: &syn::Ident,
1361 p: &crate::lexicon::LexXrpcParameters<'static>,
1362 ) -> Result<TokenStream> {
1363 let required = p.required.as_ref().map(|r| r.as_slice()).unwrap_or(&[]);
1364 let mut fields = Vec::new();
1365 let mut default_fns = Vec::new();
1366
1367 for (field_name, field_type) in &p.properties {
1368 let is_required = required.contains(field_name);
1369 let (field_tokens, default_fn) =
1370 self.generate_param_field_with_default("", field_name, field_type, is_required)?;
1371 fields.push(field_tokens);
1372 if let Some(fn_def) = default_fn {
1373 default_fns.push(fn_def);
1374 }
1375 }
1376
1377 let doc = self.generate_doc_comment(p.description.as_ref());
1378 let needs_lifetime = self.params_need_lifetime(p);
1379
1380 let derives = quote! {
1381 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, bon::Builder, jacquard_derive::IntoStatic)]
1382 #[builder(start_fn = new)]
1383 };
1384
1385 // Generate IntoStatic impl
1386 // let field_names: Vec<&str> = p.properties.keys().map(|k| k.as_str()).collect();
1387 // let type_name = ident.to_string();
1388 // let into_static_impl =
1389 // self.generate_into_static_for_struct(&type_name, &field_names, needs_lifetime, false);
1390
1391 if needs_lifetime {
1392 Ok(quote! {
1393 #(#default_fns)*
1394
1395 #doc
1396 #derives
1397 #[serde(rename_all = "camelCase")]
1398 pub struct #ident<'a> {
1399 #(#fields)*
1400 }
1401
1402 //#into_static_impl
1403 })
1404 } else {
1405 Ok(quote! {
1406 #(#default_fns)*
1407
1408 #doc
1409 #derives
1410 #[serde(rename_all = "camelCase")]
1411 pub struct #ident {
1412 #(#fields)*
1413 }
1414
1415 //#into_static_impl
1416 })
1417 }
1418 }
1419
1420 /// Generate param field with serde default if present
1421 /// Returns (field_tokens, optional_default_function)
1422 fn generate_param_field_with_default(
1423 &self,
1424 nsid: &str,
1425 field_name: &str,
1426 field_type: &crate::lexicon::LexXrpcParametersProperty<'static>,
1427 is_required: bool,
1428 ) -> Result<(TokenStream, Option<TokenStream>)> {
1429 use crate::lexicon::LexXrpcParametersProperty;
1430 use heck::ToSnakeCase;
1431
1432 // Get base field
1433 let base_field = self.generate_param_field(nsid, field_name, field_type, is_required)?;
1434
1435 // Generate default function and attribute for required fields with defaults
1436 // For optional fields, just add doc comments
1437 let (doc_comment, serde_attr, default_fn) = if is_required {
1438 match field_type {
1439 LexXrpcParametersProperty::Boolean(b) if b.default.is_some() => {
1440 let v = b.default.unwrap();
1441 let fn_name = format!("_default_{}", field_name.to_snake_case());
1442 let fn_ident = syn::Ident::new(&fn_name, proc_macro2::Span::call_site());
1443 (
1444 Some(format!("Defaults to `{}`", v)),
1445 Some(quote! { #[serde(default = #fn_name)] }),
1446 Some(quote! {
1447 fn #fn_ident() -> bool { #v }
1448 }),
1449 )
1450 }
1451 LexXrpcParametersProperty::Integer(i) if i.default.is_some() => {
1452 let v = i.default.unwrap();
1453 let fn_name = format!("_default_{}", field_name.to_snake_case());
1454 let fn_ident = syn::Ident::new(&fn_name, proc_macro2::Span::call_site());
1455 (
1456 Some(format!("Defaults to `{}`", v)),
1457 Some(quote! { #[serde(default = #fn_name)] }),
1458 Some(quote! {
1459 fn #fn_ident() -> i64 { #v }
1460 }),
1461 )
1462 }
1463 LexXrpcParametersProperty::String(s) if s.default.is_some() => {
1464 let v = s.default.as_ref().unwrap().as_ref();
1465 let fn_name = format!("_default_{}", field_name.to_snake_case());
1466 let fn_ident = syn::Ident::new(&fn_name, proc_macro2::Span::call_site());
1467 (
1468 Some(format!("Defaults to `\"{}\"`", v)),
1469 Some(quote! { #[serde(default = #fn_name)] }),
1470 Some(quote! {
1471 fn #fn_ident() -> jacquard_common::CowStr<'static> {
1472 jacquard_common::CowStr::from(#v)
1473 }
1474 }),
1475 )
1476 }
1477 _ => (None, None, None),
1478 }
1479 } else {
1480 // Optional fields - just doc comments, no serde defaults
1481 let doc = match field_type {
1482 LexXrpcParametersProperty::Integer(i) => {
1483 let mut parts = Vec::new();
1484 if let Some(def) = i.default {
1485 parts.push(format!("default: {}", def));
1486 }
1487 if let Some(min) = i.minimum {
1488 parts.push(format!("min: {}", min));
1489 }
1490 if let Some(max) = i.maximum {
1491 parts.push(format!("max: {}", max));
1492 }
1493 if !parts.is_empty() {
1494 Some(format!("({})", parts.join(", ")))
1495 } else {
1496 None
1497 }
1498 }
1499 LexXrpcParametersProperty::String(s) => {
1500 let mut parts = Vec::new();
1501 if let Some(def) = s.default.as_ref() {
1502 parts.push(format!("default: \"{}\"", def.as_ref()));
1503 }
1504 if let Some(min) = s.min_length {
1505 parts.push(format!("min length: {}", min));
1506 }
1507 if let Some(max) = s.max_length {
1508 parts.push(format!("max length: {}", max));
1509 }
1510 if !parts.is_empty() {
1511 Some(format!("({})", parts.join(", ")))
1512 } else {
1513 None
1514 }
1515 }
1516 LexXrpcParametersProperty::Boolean(b) => {
1517 b.default.map(|v| format!("(default: {})", v))
1518 }
1519 _ => None,
1520 };
1521 (doc, None, None)
1522 };
1523
1524 let doc = doc_comment.as_ref().map(|d| quote! { #[doc = #d] });
1525 let field_with_attrs = match (doc, serde_attr) {
1526 (Some(doc), Some(attr)) => quote! {
1527 #doc
1528 #attr
1529 #base_field
1530 },
1531 (Some(doc), None) => quote! {
1532 #doc
1533 #base_field
1534 },
1535 (None, Some(attr)) => quote! {
1536 #attr
1537 #base_field
1538 },
1539 (None, None) => base_field,
1540 };
1541
1542 Ok((field_with_attrs, default_fn))
1543 }
1544
1545 /// Generate input struct from XRPC body
1546 fn generate_input_struct(
1547 &self,
1548 type_base: &str,
1549 body: &LexXrpcBody<'static>,
1550 ) -> Result<TokenStream> {
1551 let ident = syn::Ident::new(type_base, proc_macro2::Span::call_site());
1552
1553 // Check if this is a binary body (no schema, just raw bytes)
1554 let is_binary_body = body.schema.is_none();
1555
1556 let fields = if let Some(schema) = &body.schema {
1557 self.generate_body_fields("", type_base, schema, true)?
1558 } else {
1559 // Binary body: just a bytes field
1560 quote! {
1561 pub body: bytes::Bytes,
1562 }
1563 };
1564
1565 let doc = self.generate_doc_comment(body.description.as_ref());
1566
1567 // Binary bodies don't need #[lexicon] attribute or lifetime
1568 let struct_def = if is_binary_body {
1569 quote! {
1570 #doc
1571 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, bon::Builder, jacquard_derive::IntoStatic)]
1572 #[builder(start_fn = new)]
1573 #[serde(rename_all = "camelCase")]
1574 pub struct #ident {
1575 #fields
1576 }
1577 }
1578 } else {
1579 // Input structs with schemas: manually add extra_data field with #[builder(default)]
1580 // for bon compatibility. The #[lexicon] macro will see it exists and skip adding it.
1581 quote! {
1582 #doc
1583 #[jacquard_derive::lexicon]
1584 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, bon::Builder, jacquard_derive::IntoStatic)]
1585 #[serde(rename_all = "camelCase")]
1586 #[builder(start_fn = new)]
1587 pub struct #ident<'a> {
1588 #fields
1589 #[serde(flatten)]
1590 #[serde(borrow)]
1591 #[builder(default)]
1592 pub extra_data: ::std::collections::BTreeMap<
1593 ::jacquard_common::smol_str::SmolStr,
1594 ::jacquard_common::types::value::Data<'a>
1595 >,
1596 }
1597 }
1598 };
1599
1600 // Generate union types if schema is an Object
1601 let mut unions = Vec::new();
1602 if let Some(crate::lexicon::LexXrpcBodySchema::Object(obj)) = &body.schema {
1603 for (field_name, field_type) in &obj.properties {
1604 if let LexObjectProperty::Union(union) = field_type {
1605 let union_name = format!("{}Record{}", type_base, field_name.to_pascal_case());
1606 let refs: Vec<_> = union.refs.iter().cloned().collect();
1607 let union_def = self.generate_union(&union_name, &refs, None, union.closed)?;
1608 unions.push(union_def);
1609 }
1610 }
1611 }
1612
1613 // Generate IntoStatic impl
1614 // let into_static_impl = if is_binary_body {
1615 // // Binary bodies: simple clone of the Bytes field
1616 // quote! {
1617 // impl jacquard_common::IntoStatic for #ident {
1618 // type Output = #ident;
1619 // fn into_static(self) -> Self::Output {
1620 // self
1621 // }
1622 // }
1623 // }
1624 // } else {
1625 // let field_names: Vec<&str> = match &body.schema {
1626 // Some(crate::lexicon::LexXrpcBodySchema::Object(obj)) => {
1627 // obj.properties.keys().map(|k| k.as_str()).collect()
1628 // }
1629 // Some(_) => {
1630 // // For Ref or Union schemas, there's just a single flattened field
1631 // vec!["value"]
1632 // }
1633 // None => {
1634 // // No schema means no fields, just extra_data
1635 // vec![]
1636 // }
1637 // };
1638 // self.generate_into_static_for_struct(type_base, &field_names, true, true)
1639 // };
1640
1641 Ok(quote! {
1642 #struct_def
1643 #(#unions)*
1644 //#into_static_impl
1645 })
1646 }
1647
1648 /// Generate output struct from XRPC body
1649 fn generate_output_struct(
1650 &self,
1651 type_base: &str,
1652 body: &LexXrpcBody<'static>,
1653 ) -> Result<TokenStream> {
1654 let struct_name = format!("{}Output", type_base);
1655 let ident = syn::Ident::new(&struct_name, proc_macro2::Span::call_site());
1656
1657 let fields = if let Some(schema) = &body.schema {
1658 self.generate_body_fields("", &struct_name, schema, false)?
1659 } else {
1660 quote! {}
1661 };
1662
1663 let doc = self.generate_doc_comment(body.description.as_ref());
1664
1665 // Output structs always get a lifetime since they have the #[lexicon] attribute
1666 // which adds extra_data: BTreeMap<..., Data<'a>>
1667 let struct_def = quote! {
1668 #doc
1669 #[jacquard_derive::lexicon]
1670 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, jacquard_derive::IntoStatic)]
1671 #[serde(rename_all = "camelCase")]
1672 pub struct #ident<'a> {
1673 #fields
1674 }
1675 };
1676
1677 // Generate union types if schema is an Object
1678 let mut unions = Vec::new();
1679 if let Some(crate::lexicon::LexXrpcBodySchema::Object(obj)) = &body.schema {
1680 for (field_name, field_type) in &obj.properties {
1681 if let LexObjectProperty::Union(union) = field_type {
1682 let union_name =
1683 format!("{}Record{}", struct_name, field_name.to_pascal_case());
1684 let refs: Vec<_> = union.refs.iter().cloned().collect();
1685 let union_def = self.generate_union(&union_name, &refs, None, union.closed)?;
1686 unions.push(union_def);
1687 }
1688 }
1689 }
1690
1691 // Generate IntoStatic impl
1692 // let field_names: Vec<&str> = match &body.schema {
1693 // Some(crate::lexicon::LexXrpcBodySchema::Object(obj)) => {
1694 // obj.properties.keys().map(|k| k.as_str()).collect()
1695 // }
1696 // Some(_) => {
1697 // // For Ref or Union schemas, there's just a single flattened field
1698 // vec!["value"]
1699 // }
1700 // None => {
1701 // // No schema means no fields, just extra_data
1702 // vec![]
1703 // }
1704 // };
1705 // let into_static_impl =
1706 // self.generate_into_static_for_struct(&struct_name, &field_names, true, true);
1707
1708 Ok(quote! {
1709 #struct_def
1710 #(#unions)*
1711 //#into_static_impl
1712 })
1713 }
1714
1715 /// Generate fields from XRPC body schema
1716 fn generate_body_fields(
1717 &self,
1718 nsid: &str,
1719 parent_type_name: &str,
1720 schema: &LexXrpcBodySchema<'static>,
1721 is_builder: bool,
1722 ) -> Result<TokenStream> {
1723 use crate::lexicon::LexXrpcBodySchema;
1724
1725 match schema {
1726 LexXrpcBodySchema::Object(obj) => {
1727 self.generate_object_fields(nsid, parent_type_name, obj, is_builder)
1728 }
1729 LexXrpcBodySchema::Ref(ref_type) => {
1730 let rust_type = self.ref_to_rust_type(&ref_type.r#ref)?;
1731 Ok(quote! {
1732 #[serde(flatten)]
1733 #[serde(borrow)]
1734 pub value: #rust_type,
1735 })
1736 }
1737 LexXrpcBodySchema::Union(_union) => {
1738 let rust_type = quote! { jacquard_common::types::value::Data<'a> };
1739 Ok(quote! {
1740 #[serde(flatten)]
1741 #[serde(borrow)]
1742 pub value: #rust_type,
1743 })
1744 }
1745 }
1746 }
1747
1748 /// Generate a field for XRPC parameters
1749 fn generate_param_field(
1750 &self,
1751 _nsid: &str,
1752 field_name: &str,
1753 field_type: &crate::lexicon::LexXrpcParametersProperty<'static>,
1754 is_required: bool,
1755 ) -> Result<TokenStream> {
1756 use crate::lexicon::LexXrpcParametersProperty;
1757
1758 let field_ident = make_ident(&field_name.to_snake_case());
1759
1760 let (rust_type, needs_lifetime, is_cowstr) = match field_type {
1761 LexXrpcParametersProperty::Boolean(_) => (quote! { bool }, false, false),
1762 LexXrpcParametersProperty::Integer(_) => (quote! { i64 }, false, false),
1763 LexXrpcParametersProperty::String(s) => {
1764 let is_cowstr = s.format.is_none(); // CowStr for plain strings
1765 (
1766 self.string_to_rust_type(s),
1767 self.string_needs_lifetime(s),
1768 is_cowstr,
1769 )
1770 }
1771 LexXrpcParametersProperty::Unknown(_) => (
1772 quote! { jacquard_common::types::value::Data<'a> },
1773 true,
1774 false,
1775 ),
1776 LexXrpcParametersProperty::Array(arr) => {
1777 let needs_lifetime = match &arr.items {
1778 crate::lexicon::LexPrimitiveArrayItem::Boolean(_)
1779 | crate::lexicon::LexPrimitiveArrayItem::Integer(_) => false,
1780 crate::lexicon::LexPrimitiveArrayItem::String(s) => {
1781 self.string_needs_lifetime(s)
1782 }
1783 crate::lexicon::LexPrimitiveArrayItem::Unknown(_) => true,
1784 };
1785 let item_type = match &arr.items {
1786 crate::lexicon::LexPrimitiveArrayItem::Boolean(_) => quote! { bool },
1787 crate::lexicon::LexPrimitiveArrayItem::Integer(_) => quote! { i64 },
1788 crate::lexicon::LexPrimitiveArrayItem::String(s) => self.string_to_rust_type(s),
1789 crate::lexicon::LexPrimitiveArrayItem::Unknown(_) => {
1790 quote! { jacquard_common::types::value::Data<'a> }
1791 }
1792 };
1793 (quote! { Vec<#item_type> }, needs_lifetime, false)
1794 }
1795 };
1796
1797 let rust_type = if is_required {
1798 rust_type
1799 } else {
1800 quote! { std::option::Option<#rust_type> }
1801 };
1802
1803 let mut attrs = Vec::new();
1804
1805 if !is_required {
1806 attrs.push(quote! { #[serde(skip_serializing_if = "std::option::Option::is_none")] });
1807 }
1808
1809 // Add serde(borrow) to all fields with lifetimes
1810 if needs_lifetime {
1811 attrs.push(quote! { #[serde(borrow)] });
1812 }
1813
1814 // Add builder(into) for CowStr fields to allow String, &str, etc.
1815 if is_cowstr {
1816 attrs.push(quote! { #[builder(into)] });
1817 }
1818
1819 Ok(quote! {
1820 #(#attrs)*
1821 pub #field_ident: #rust_type,
1822 })
1823 }
1824
1825 /// Generate error enum from XRPC errors
1826 fn generate_error_enum(
1827 &self,
1828 type_base: &str,
1829 errors: &[LexXrpcError<'static>],
1830 ) -> Result<TokenStream> {
1831 let enum_name = format!("{}Error", type_base);
1832 let ident = syn::Ident::new(&enum_name, proc_macro2::Span::call_site());
1833
1834 let mut variants = Vec::new();
1835 let mut display_arms = Vec::new();
1836
1837 for error in errors {
1838 let variant_name = error.name.to_pascal_case();
1839 let variant_ident = syn::Ident::new(&variant_name, proc_macro2::Span::call_site());
1840
1841 let error_name = error.name.as_ref();
1842 let doc = self.generate_doc_comment(error.description.as_ref());
1843
1844 variants.push(quote! {
1845 #doc
1846 #[serde(rename = #error_name)]
1847 #variant_ident(std::option::Option<String>)
1848 });
1849
1850 display_arms.push(quote! {
1851 Self::#variant_ident(msg) => {
1852 write!(f, #error_name)?;
1853 if let Some(msg) = msg {
1854 write!(f, ": {}", msg)?;
1855 }
1856 Ok(())
1857 }
1858 });
1859 }
1860
1861 // Generate IntoStatic impl
1862 let variant_info: Vec<(String, EnumVariantKind)> = errors
1863 .iter()
1864 .map(|e| (e.name.to_pascal_case(), EnumVariantKind::Tuple))
1865 .collect();
1866 let into_static_impl =
1867 self.generate_into_static_for_enum(&enum_name, &variant_info, true, true);
1868
1869 Ok(quote! {
1870 #[jacquard_derive::open_union]
1871 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, thiserror::Error, miette::Diagnostic)]
1872 #[serde(tag = "error", content = "message")]
1873 #[serde(bound(deserialize = "'de: 'a"))]
1874 pub enum #ident<'a> {
1875 #(#variants,)*
1876 }
1877
1878 impl std::fmt::Display for #ident<'_> {
1879 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1880 match self {
1881 #(#display_arms)*
1882 Self::Unknown(err) => write!(f, "Unknown error: {:?}", err),
1883 }
1884 }
1885 }
1886
1887 #into_static_impl
1888 })
1889 }
1890
1891 /// Generate enum for string with known values
1892 fn generate_known_values_enum(
1893 &self,
1894 nsid: &str,
1895 def_name: &str,
1896 string: &LexString<'static>,
1897 ) -> Result<TokenStream> {
1898 let type_name = self.def_to_type_name(nsid, def_name);
1899 let ident = syn::Ident::new(&type_name, proc_macro2::Span::call_site());
1900
1901 let known_values = string.known_values.as_ref().unwrap();
1902 let mut variants = Vec::new();
1903 let mut from_str_arms = Vec::new();
1904 let mut as_str_arms = Vec::new();
1905
1906 for value in known_values {
1907 // Convert value to valid Rust identifier
1908 let value_str = value.as_ref();
1909 let variant_name = value_to_variant_name(value_str);
1910 let variant_ident = syn::Ident::new(&variant_name, proc_macro2::Span::call_site());
1911
1912 variants.push(quote! {
1913 #variant_ident
1914 });
1915
1916 from_str_arms.push(quote! {
1917 #value_str => Self::#variant_ident
1918 });
1919
1920 as_str_arms.push(quote! {
1921 Self::#variant_ident => #value_str
1922 });
1923 }
1924
1925 let doc = self.generate_doc_comment(string.description.as_ref());
1926
1927 // Generate IntoStatic impl
1928 let variant_info: Vec<(String, EnumVariantKind)> = known_values
1929 .iter()
1930 .map(|value| {
1931 let variant_name = value_to_variant_name(value.as_ref());
1932 (variant_name, EnumVariantKind::Unit)
1933 })
1934 .chain(std::iter::once((
1935 "Other".to_string(),
1936 EnumVariantKind::Tuple,
1937 )))
1938 .collect();
1939 let into_static_impl =
1940 self.generate_into_static_for_enum(&type_name, &variant_info, true, false);
1941
1942 Ok(quote! {
1943 #doc
1944 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
1945 pub enum #ident<'a> {
1946 #(#variants,)*
1947 Other(jacquard_common::CowStr<'a>),
1948 }
1949
1950 impl<'a> #ident<'a> {
1951 pub fn as_str(&self) -> &str {
1952 match self {
1953 #(#as_str_arms,)*
1954 Self::Other(s) => s.as_ref(),
1955 }
1956 }
1957 }
1958
1959 impl<'a> From<&'a str> for #ident<'a> {
1960 fn from(s: &'a str) -> Self {
1961 match s {
1962 #(#from_str_arms,)*
1963 _ => Self::Other(jacquard_common::CowStr::from(s)),
1964 }
1965 }
1966 }
1967
1968 impl<'a> From<String> for #ident<'a> {
1969 fn from(s: String) -> Self {
1970 match s.as_str() {
1971 #(#from_str_arms,)*
1972 _ => Self::Other(jacquard_common::CowStr::from(s)),
1973 }
1974 }
1975 }
1976
1977 impl<'a> AsRef<str> for #ident<'a> {
1978 fn as_ref(&self) -> &str {
1979 self.as_str()
1980 }
1981 }
1982
1983 impl<'a> serde::Serialize for #ident<'a> {
1984 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1985 where
1986 S: serde::Serializer,
1987 {
1988 serializer.serialize_str(self.as_str())
1989 }
1990 }
1991
1992 impl<'de, 'a> serde::Deserialize<'de> for #ident<'a>
1993 where
1994 'de: 'a,
1995 {
1996 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1997 where
1998 D: serde::Deserializer<'de>,
1999 {
2000 let s = <&'de str>::deserialize(deserializer)?;
2001 Ok(Self::from(s))
2002 }
2003 }
2004
2005 #into_static_impl
2006 })
2007 }
2008
2009 /// Generate enum for integer with enum values
2010 fn generate_integer_enum(
2011 &self,
2012 nsid: &str,
2013 def_name: &str,
2014 integer: &LexInteger<'static>,
2015 ) -> Result<TokenStream> {
2016 let type_name = self.def_to_type_name(nsid, def_name);
2017 let ident = syn::Ident::new(&type_name, proc_macro2::Span::call_site());
2018
2019 let enum_values = integer.r#enum.as_ref().unwrap();
2020 let mut variants = Vec::new();
2021 let mut from_i64_arms = Vec::new();
2022 let mut to_i64_arms = Vec::new();
2023
2024 for value in enum_values {
2025 let variant_name = format!("Value{}", value.abs());
2026 let variant_ident = syn::Ident::new(&variant_name, proc_macro2::Span::call_site());
2027
2028 variants.push(quote! {
2029 #[serde(rename = #value)]
2030 #variant_ident
2031 });
2032
2033 from_i64_arms.push(quote! {
2034 #value => Self::#variant_ident
2035 });
2036
2037 to_i64_arms.push(quote! {
2038 Self::#variant_ident => #value
2039 });
2040 }
2041
2042 let doc = self.generate_doc_comment(integer.description.as_ref());
2043
2044 Ok(quote! {
2045 #doc
2046 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2047 pub enum #ident {
2048 #(#variants,)*
2049 #[serde(untagged)]
2050 Other(i64),
2051 }
2052
2053 impl #ident {
2054 pub fn as_i64(&self) -> i64 {
2055 match self {
2056 #(#to_i64_arms,)*
2057 Self::Other(n) => *n,
2058 }
2059 }
2060 }
2061
2062 impl From<i64> for #ident {
2063 fn from(n: i64) -> Self {
2064 match n {
2065 #(#from_i64_arms,)*
2066 _ => Self::Other(n),
2067 }
2068 }
2069 }
2070
2071 impl serde::Serialize for #ident {
2072 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2073 where
2074 S: serde::Serializer,
2075 {
2076 serializer.serialize_i64(self.as_i64())
2077 }
2078 }
2079
2080 impl<'de> serde::Deserialize<'de> for #ident {
2081 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2082 where
2083 D: serde::Deserializer<'de>,
2084 {
2085 let n = i64::deserialize(deserializer)?;
2086 Ok(Self::from(n))
2087 }
2088 }
2089 })
2090 }
2091
2092 /// Generate XrpcRequest trait impl for a query or procedure
2093 fn generate_xrpc_request_impl(
2094 &self,
2095 nsid: &str,
2096 type_base: &str,
2097 method: TokenStream,
2098 output_encoding: &str,
2099 has_params: bool,
2100 params_has_lifetime: bool,
2101 has_output: bool,
2102 has_errors: bool,
2103 is_binary_input: bool,
2104 ) -> Result<TokenStream> {
2105 let output_type = if has_output {
2106 let output_ident = syn::Ident::new(
2107 &format!("{}Output", type_base),
2108 proc_macro2::Span::call_site(),
2109 );
2110 quote! { #output_ident<'de> }
2111 } else {
2112 quote! { () }
2113 };
2114
2115 let error_type = if has_errors {
2116 let error_ident = syn::Ident::new(
2117 &format!("{}Error", type_base),
2118 proc_macro2::Span::call_site(),
2119 );
2120 quote! { #error_ident<'de> }
2121 } else {
2122 quote! { jacquard_common::xrpc::GenericError<'de> }
2123 };
2124
2125 // Generate the response type that implements XrpcResp
2126 let response_ident = syn::Ident::new(
2127 &format!("{}Response", type_base),
2128 proc_macro2::Span::call_site(),
2129 );
2130
2131 // Generate the endpoint type that implements XrpcEndpoint
2132 let endpoint_ident = syn::Ident::new(
2133 &format!("{}Request", type_base),
2134 proc_macro2::Span::call_site(),
2135 );
2136
2137 let response_type = quote! {
2138 #[doc = "Response type for "]
2139 #[doc = #nsid]
2140 pub struct #response_ident;
2141
2142 impl jacquard_common::xrpc::XrpcResp for #response_ident {
2143 const NSID: &'static str = #nsid;
2144 const ENCODING: &'static str = #output_encoding;
2145 type Output<'de> = #output_type;
2146 type Err<'de> = #error_type;
2147 }
2148 };
2149
2150 // Generate encode_body() method for binary inputs
2151 let encode_body_method = if is_binary_input {
2152 quote! {
2153 fn encode_body(&self) -> Result<Vec<u8>, jacquard_common::xrpc::EncodeError> {
2154 Ok(self.body.to_vec())
2155 }
2156 }
2157 } else {
2158 quote! {}
2159 };
2160
2161 // Generate decode_body() method for binary inputs
2162 let decode_body_method = if is_binary_input {
2163 quote! {
2164 fn decode_body(
2165 body: &'de [u8],
2166 ) -> Result<Box<Self>, jacquard_common::error::DecodeError> {
2167 Ok(Box::new(Self {
2168 body: bytes::Bytes::copy_from_slice(body),
2169 }))
2170 }
2171 }
2172 } else {
2173 quote! {}
2174 };
2175
2176 let endpoint_path = format!("/xrpc/{}", nsid);
2177
2178 if has_params {
2179 // Implement on the params/input struct itself
2180 let request_ident = syn::Ident::new(type_base, proc_macro2::Span::call_site());
2181 let impl_target = if params_has_lifetime {
2182 quote! { #request_ident<'de> }
2183 } else {
2184 quote! { #request_ident }
2185 };
2186
2187 Ok(quote! {
2188 #response_type
2189
2190 impl<'de> jacquard_common::xrpc::XrpcRequest<'de> for #impl_target {
2191 const NSID: &'static str = #nsid;
2192 const METHOD: jacquard_common::xrpc::XrpcMethod = #method;
2193
2194 type Response = #response_ident;
2195
2196 #encode_body_method
2197 #decode_body_method
2198 }
2199
2200 #[doc = "Endpoint type for "]
2201 #[doc = #nsid]
2202 pub struct #endpoint_ident;
2203
2204 impl jacquard_common::xrpc::XrpcEndpoint for #endpoint_ident {
2205 const PATH: &'static str = #endpoint_path;
2206 const METHOD: jacquard_common::xrpc::XrpcMethod = #method;
2207
2208 type Request<'de> = #impl_target;
2209 type Response = #response_ident;
2210 }
2211 })
2212 } else {
2213 // No params - generate a marker struct
2214 let request_ident = syn::Ident::new(type_base, proc_macro2::Span::call_site());
2215
2216 Ok(quote! {
2217 /// XRPC request marker type
2218 #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, jacquard_derive::IntoStatic)]
2219 pub struct #request_ident;
2220
2221 #response_type
2222
2223 impl<'de> jacquard_common::xrpc::XrpcRequest<'de> for #request_ident {
2224 const NSID: &'static str = #nsid;
2225 const METHOD: jacquard_common::xrpc::XrpcMethod = #method;
2226
2227 type Response = #response_ident;
2228 }
2229
2230 #[doc = "Endpoint type for "]
2231 #[doc = #nsid]
2232 pub struct #endpoint_ident;
2233
2234 impl jacquard_common::xrpc::XrpcEndpoint for #endpoint_ident {
2235 const PATH: &'static str = #endpoint_path;
2236 const METHOD: jacquard_common::xrpc::XrpcMethod = #method;
2237
2238 type Request<'de> = #request_ident;
2239 type Response = #response_ident;
2240 }
2241 })
2242 }
2243 }
2244
2245 /// Generate a union enum
2246 pub fn generate_union(
2247 &self,
2248 union_name: &str,
2249 refs: &[jacquard_common::CowStr<'static>],
2250 description: Option<&str>,
2251 closed: Option<bool>,
2252 ) -> Result<TokenStream> {
2253 let enum_ident = syn::Ident::new(union_name, proc_macro2::Span::call_site());
2254
2255 let mut variants = Vec::new();
2256 for ref_str in refs {
2257 // Parse ref to get NSID and def name
2258 let (ref_nsid, ref_def) = if let Some((nsid, fragment)) = ref_str.split_once('#') {
2259 (nsid, fragment)
2260 } else {
2261 (ref_str.as_ref(), "main")
2262 };
2263
2264 // Skip unknown refs - they'll be handled by Unknown variant
2265 if !self.corpus.ref_exists(ref_str.as_ref()) {
2266 continue;
2267 }
2268
2269 // Generate variant name from def name (or last NSID segment if main)
2270 // For non-main refs, include the last NSID segment to avoid collisions
2271 // e.g. app.bsky.embed.images#view -> ImagesView
2272 // app.bsky.embed.video#view -> VideoView
2273 let variant_name = if ref_def == "main" {
2274 ref_nsid.split('.').last().unwrap().to_pascal_case()
2275 } else {
2276 let last_segment = ref_nsid.split('.').last().unwrap().to_pascal_case();
2277 format!("{}{}", last_segment, ref_def.to_pascal_case())
2278 };
2279 let variant_ident = syn::Ident::new(&variant_name, proc_macro2::Span::call_site());
2280
2281 // Get the Rust type for this ref
2282 let rust_type = self.ref_to_rust_type(ref_str.as_ref())?;
2283
2284 // Add serde rename for the full NSID
2285 let ref_str_literal = ref_str.as_ref();
2286 variants.push(quote! {
2287 #[serde(rename = #ref_str_literal)]
2288 #variant_ident(Box<#rust_type>)
2289 });
2290 }
2291
2292 let doc = description
2293 .map(|d| quote! { #[doc = #d] })
2294 .unwrap_or_else(|| quote! {});
2295
2296 // Only add open_union if not closed
2297 let is_open = closed != Some(true);
2298
2299 // Generate IntoStatic impl
2300 // let variant_info: Vec<(String, EnumVariantKind)> = refs
2301 // .iter()
2302 // .filter_map(|ref_str| {
2303 // // Skip unknown refs
2304 // if !self.corpus.ref_exists(ref_str.as_ref()) {
2305 // return None;
2306 // }
2307
2308 // let (ref_nsid, ref_def) = if let Some((nsid, fragment)) = ref_str.split_once('#') {
2309 // (nsid, fragment)
2310 // } else {
2311 // (ref_str.as_ref(), "main")
2312 // };
2313
2314 // let variant_name = if ref_def == "main" {
2315 // ref_nsid.split('.').last().unwrap().to_pascal_case()
2316 // } else {
2317 // let last_segment = ref_nsid.split('.').last().unwrap().to_pascal_case();
2318 // format!("{}{}", last_segment, ref_def.to_pascal_case())
2319 // };
2320 // Some((variant_name, EnumVariantKind::Tuple))
2321 // })
2322 // .collect();
2323 // let into_static_impl =
2324 // self.generate_into_static_for_enum(union_name, &variant_info, true, is_open);
2325
2326 if is_open {
2327 Ok(quote! {
2328 #doc
2329 #[jacquard_derive::open_union]
2330 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, jacquard_derive::IntoStatic)]
2331 #[serde(tag = "$type")]
2332 #[serde(bound(deserialize = "'de: 'a"))]
2333 pub enum #enum_ident<'a> {
2334 #(#variants,)*
2335 }
2336
2337 //#into_static_impl
2338 })
2339 } else {
2340 Ok(quote! {
2341 #doc
2342 #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, jacquard_derive::IntoStatic)]
2343 #[serde(tag = "$type")]
2344 #[serde(bound(deserialize = "'de: 'a"))]
2345 pub enum #enum_ident<'a> {
2346 #(#variants,)*
2347 }
2348
2349 //#into_static_impl
2350 })
2351 }
2352 }
2353
2354 /// Generate IntoStatic impl for a struct
2355 #[allow(dead_code)]
2356 fn generate_into_static_for_struct(
2357 &self,
2358 type_name: &str,
2359 field_names: &[&str],
2360 has_lifetime: bool,
2361 has_extra_data: bool,
2362 ) -> TokenStream {
2363 let ident = syn::Ident::new(type_name, proc_macro2::Span::call_site());
2364
2365 let field_idents: Vec<_> = field_names
2366 .iter()
2367 .map(|name| make_ident(&name.to_snake_case()))
2368 .collect();
2369
2370 if has_lifetime {
2371 let field_conversions: Vec<_> = field_idents
2372 .iter()
2373 .map(|field| quote! { #field: self.#field.into_static() })
2374 .collect();
2375
2376 let extra_data_conversion = if has_extra_data {
2377 quote! { extra_data: self.extra_data.into_static(), }
2378 } else {
2379 quote! {}
2380 };
2381
2382 quote! {
2383 impl jacquard_common::IntoStatic for #ident<'_> {
2384 type Output = #ident<'static>;
2385
2386 fn into_static(self) -> Self::Output {
2387 #ident {
2388 #(#field_conversions,)*
2389 #extra_data_conversion
2390 }
2391 }
2392 }
2393 }
2394 } else {
2395 quote! {
2396 impl jacquard_common::IntoStatic for #ident {
2397 type Output = #ident;
2398
2399 fn into_static(self) -> Self::Output {
2400 self
2401 }
2402 }
2403 }
2404 }
2405 }
2406
2407 /// Generate IntoStatic impl for an enum
2408 fn generate_into_static_for_enum(
2409 &self,
2410 type_name: &str,
2411 variant_info: &[(String, EnumVariantKind)],
2412 has_lifetime: bool,
2413 is_open: bool,
2414 ) -> TokenStream {
2415 let ident = syn::Ident::new(type_name, proc_macro2::Span::call_site());
2416
2417 if has_lifetime {
2418 let variant_conversions: Vec<_> = variant_info
2419 .iter()
2420 .map(|(variant_name, kind)| {
2421 let variant_ident = syn::Ident::new(variant_name, proc_macro2::Span::call_site());
2422 match kind {
2423 EnumVariantKind::Unit => {
2424 quote! {
2425 #ident::#variant_ident => #ident::#variant_ident
2426 }
2427 }
2428 EnumVariantKind::Tuple => {
2429 quote! {
2430 #ident::#variant_ident(v) => #ident::#variant_ident(v.into_static())
2431 }
2432 }
2433 EnumVariantKind::Struct(fields) => {
2434 let field_idents: Vec<_> = fields
2435 .iter()
2436 .map(|f| make_ident(&f.to_snake_case()))
2437 .collect();
2438 let field_conversions: Vec<_> = field_idents
2439 .iter()
2440 .map(|f| quote! { #f: #f.into_static() })
2441 .collect();
2442 quote! {
2443 #ident::#variant_ident { #(#field_idents,)* } => #ident::#variant_ident {
2444 #(#field_conversions,)*
2445 }
2446 }
2447 }
2448 }
2449 })
2450 .collect();
2451
2452 let unknown_conversion = if is_open {
2453 quote! {
2454 #ident::Unknown(v) => #ident::Unknown(v.into_static()),
2455 }
2456 } else {
2457 quote! {}
2458 };
2459
2460 quote! {
2461 impl jacquard_common::IntoStatic for #ident<'_> {
2462 type Output = #ident<'static>;
2463
2464 fn into_static(self) -> Self::Output {
2465 match self {
2466 #(#variant_conversions,)*
2467 #unknown_conversion
2468 }
2469 }
2470 }
2471 }
2472 } else {
2473 quote! {
2474 impl jacquard_common::IntoStatic for #ident {
2475 type Output = #ident;
2476
2477 fn into_static(self) -> Self::Output {
2478 self
2479 }
2480 }
2481 }
2482 }
2483 }
2484}
2485
2486/// Enum variant kind for IntoStatic generation
2487#[derive(Debug, Clone)]
2488#[allow(dead_code)]
2489enum EnumVariantKind {
2490 Unit,
2491 Tuple,
2492 Struct(Vec<String>),
2493}
2494
2495#[cfg(test)]
2496mod tests {
2497 use super::*;
2498
2499 #[test]
2500 fn test_generate_record() {
2501 let corpus =
2502 LexiconCorpus::load_from_dir("tests/fixtures/test_lexicons").expect("load corpus");
2503 let codegen = CodeGenerator::new(&corpus, "jacquard_api");
2504
2505 let doc = corpus.get("app.bsky.feed.post").expect("get post");
2506 let def = doc.defs.get("main").expect("get main def");
2507
2508 let tokens = codegen
2509 .generate_def("app.bsky.feed.post", "main", def)
2510 .expect("generate");
2511
2512 // Format and print for inspection
2513 let file: syn::File = syn::parse2(tokens).expect("parse tokens");
2514 let formatted = prettyplease::unparse(&file);
2515 println!("\n{}\n", formatted);
2516
2517 // Check basic structure
2518 assert!(formatted.contains("struct Post"));
2519 assert!(formatted.contains("pub text"));
2520 assert!(formatted.contains("CowStr<'a>"));
2521 }
2522
2523 #[test]
2524 fn test_generate_union() {
2525 let corpus =
2526 LexiconCorpus::load_from_dir("tests/fixtures/test_lexicons").expect("load corpus");
2527 let codegen = CodeGenerator::new(&corpus, "jacquard_api");
2528
2529 // Create a union with embed types
2530 let refs = vec![
2531 "app.bsky.embed.images".into(),
2532 "app.bsky.embed.video".into(),
2533 "app.bsky.embed.external".into(),
2534 ];
2535
2536 let tokens = codegen
2537 .generate_union("RecordEmbed", &refs, Some("Post embed union"), None)
2538 .expect("generate union");
2539
2540 let file: syn::File = syn::parse2(tokens).expect("parse tokens");
2541 let formatted = prettyplease::unparse(&file);
2542 println!("\n{}\n", formatted);
2543
2544 // Check structure
2545 assert!(formatted.contains("enum RecordEmbed"));
2546 assert!(formatted.contains("Images"));
2547 assert!(formatted.contains("Video"));
2548 assert!(formatted.contains("External"));
2549 assert!(formatted.contains("#[serde(tag = \"$type\")]"));
2550 assert!(formatted.contains("#[jacquard_derive::open_union]"));
2551 }
2552
2553 #[test]
2554 fn test_generate_query() {
2555 let corpus =
2556 LexiconCorpus::load_from_dir("tests/fixtures/test_lexicons").expect("load corpus");
2557 let codegen = CodeGenerator::new(&corpus, "jacquard_api");
2558
2559 let doc = corpus
2560 .get("app.bsky.feed.getAuthorFeed")
2561 .expect("get getAuthorFeed");
2562 let def = doc.defs.get("main").expect("get main def");
2563
2564 let tokens = codegen
2565 .generate_def("app.bsky.feed.getAuthorFeed", "main", def)
2566 .expect("generate");
2567
2568 let file: syn::File = syn::parse2(tokens).expect("parse tokens");
2569 let formatted = prettyplease::unparse(&file);
2570 println!("\n{}\n", formatted);
2571
2572 // Check structure
2573 assert!(formatted.contains("struct GetAuthorFeed"));
2574 assert!(formatted.contains("struct GetAuthorFeedOutput"));
2575 assert!(formatted.contains("enum GetAuthorFeedError"));
2576 assert!(formatted.contains("pub actor"));
2577 assert!(formatted.contains("pub limit"));
2578 assert!(formatted.contains("pub cursor"));
2579 assert!(formatted.contains("pub feed"));
2580 assert!(formatted.contains("BlockedActor"));
2581 assert!(formatted.contains("BlockedByActor"));
2582 }
2583
2584 #[test]
2585 fn test_generate_known_values_enum() {
2586 let corpus =
2587 LexiconCorpus::load_from_dir("tests/fixtures/test_lexicons").expect("load corpus");
2588 let codegen = CodeGenerator::new(&corpus, "jacquard_api");
2589
2590 let doc = corpus
2591 .get("com.atproto.label.defs")
2592 .expect("get label defs");
2593 let def = doc.defs.get("labelValue").expect("get labelValue def");
2594
2595 let tokens = codegen
2596 .generate_def("com.atproto.label.defs", "labelValue", def)
2597 .expect("generate");
2598
2599 let file: syn::File = syn::parse2(tokens).expect("parse tokens");
2600 let formatted = prettyplease::unparse(&file);
2601 println!("\n{}\n", formatted);
2602
2603 // Check structure
2604 assert!(formatted.contains("enum LabelValue"));
2605 assert!(formatted.contains("Hide"));
2606 assert!(formatted.contains("NoPromote"));
2607 assert!(formatted.contains("Warn"));
2608 assert!(formatted.contains("DmcaViolation"));
2609 assert!(formatted.contains("Other(jacquard_common::CowStr"));
2610 assert!(formatted.contains("impl<'a> From<&'a str>"));
2611 assert!(formatted.contains("fn as_str(&self)"));
2612 }
2613
2614 #[test]
2615 fn test_nsid_to_file_path() {
2616 let corpus =
2617 LexiconCorpus::load_from_dir("tests/fixtures/test_lexicons").expect("load corpus");
2618 let codegen = CodeGenerator::new(&corpus, "jacquard_api");
2619
2620 // Regular paths
2621 assert_eq!(
2622 codegen.nsid_to_file_path("app.bsky.feed.post"),
2623 std::path::PathBuf::from("app_bsky/feed/post.rs")
2624 );
2625
2626 assert_eq!(
2627 codegen.nsid_to_file_path("app.bsky.feed.getAuthorFeed"),
2628 std::path::PathBuf::from("app_bsky/feed/get_author_feed.rs")
2629 );
2630
2631 // Defs paths - should go in parent
2632 assert_eq!(
2633 codegen.nsid_to_file_path("com.atproto.label.defs"),
2634 std::path::PathBuf::from("com_atproto/label.rs")
2635 );
2636 }
2637
2638 #[test]
2639 fn test_write_to_disk() {
2640 let corpus =
2641 LexiconCorpus::load_from_dir("tests/fixtures/test_lexicons").expect("load corpus");
2642 let codegen = CodeGenerator::new(&corpus, "test_generated");
2643
2644 let tmp_dir =
2645 tempfile::tempdir().expect("should be able to create temp directory for output");
2646 let output_dir = std::path::PathBuf::from(tmp_dir.path());
2647
2648 // Clean up any previous test output
2649 let _ = std::fs::remove_dir_all(&output_dir);
2650
2651 // Generate and write
2652 codegen.write_to_disk(&output_dir).expect("write to disk");
2653
2654 // Verify some files were created
2655 assert!(output_dir.join("app_bsky/feed/post.rs").exists());
2656 assert!(output_dir.join("app_bsky/feed/get_author_feed.rs").exists());
2657 assert!(output_dir.join("com_atproto/label.rs").exists());
2658
2659 // Verify module files were created
2660 assert!(output_dir.join("lib.rs").exists());
2661 assert!(output_dir.join("app_bsky.rs").exists());
2662
2663 // Read and verify post.rs contains expected content
2664 let post_content = std::fs::read_to_string(output_dir.join("app_bsky/feed/post.rs"))
2665 .expect("read post.rs");
2666 assert!(post_content.contains("pub struct Post"));
2667 assert!(post_content.contains("jacquard_common"));
2668 }
2669}