A better Rust ATProto crate
1//! Custom builder generation for lexicon types
2//!
3//! This module generates type-safe builders following bon's layered typestate pattern,
4//! but at lexicon codegen time rather than via proc macros. This eliminates proc macro
5//! overhead in the API crate while maintaining identical API surface.
6
7use crate::lexicon::{LexObject, LexXrpcParameters};
8use proc_macro2::TokenStream;
9use quote::quote;
10
11/// Schema type that can have a builder generated
12#[derive(Debug, Clone)]
13pub(crate) enum BuilderSchema<'a> {
14 /// XRPC query parameters or nested objects
15 Object(&'a LexObject<'static>),
16 /// XRPC query parameters
17 Parameters(&'a LexXrpcParameters<'static>),
18}
19
20impl<'a> BuilderSchema<'a> {
21 /// Get the required fields list
22 pub fn required(&self) -> Option<&[jacquard_common::smol_str::SmolStr]> {
23 match self {
24 BuilderSchema::Object(obj) => obj.required.as_deref(),
25 BuilderSchema::Parameters(params) => params.required.as_deref(),
26 }
27 }
28
29 pub fn nullable(&self) -> Option<&[jacquard_common::smol_str::SmolStr]> {
30 match self {
31 BuilderSchema::Object(obj) => obj.nullable.as_deref(),
32 BuilderSchema::Parameters(_) => None,
33 }
34 }
35
36 /// Get the property names (field names)
37 pub fn property_names(&self) -> Vec<&jacquard_common::smol_str::SmolStr> {
38 match self {
39 BuilderSchema::Object(obj) => obj.properties.keys().collect(),
40 BuilderSchema::Parameters(params) => params.properties.keys().collect(),
41 }
42 }
43}
44
45pub(crate) mod build_method;
46pub(crate) mod builder_struct;
47pub(crate) mod common;
48pub(crate) mod setters;
49pub(crate) mod state_mod;
50
51#[cfg(test)]
52mod tests;
53
54/// Context for builder generation
55pub(crate) struct BuilderGenContext<'a, 'c> {
56 /// Code generator reference for type conversion
57 pub codegen: &'a crate::codegen::CodeGenerator<'c>,
58 /// NSID of the lexicon being processed
59 pub nsid: &'a str,
60 /// Type name for the struct
61 pub type_name: &'a str,
62 /// Schema to generate builder for
63 pub schema: BuilderSchema<'a>,
64 /// Whether the struct has a lifetime parameter
65 pub has_lifetime: bool,
66}
67
68impl<'a, 'c> BuilderGenContext<'a, 'c> {
69 /// Create a new builder generation context from a LexObject
70 pub fn from_object(
71 codegen: &'a crate::codegen::CodeGenerator<'c>,
72 nsid: &'a str,
73 type_name: &'a str,
74 object: &'a LexObject<'static>,
75 has_lifetime: bool,
76 ) -> Self {
77 Self {
78 codegen,
79 nsid,
80 type_name,
81 schema: BuilderSchema::Object(object),
82 has_lifetime,
83 }
84 }
85
86 /// Create a new builder generation context from LexXrpcParameters
87 pub fn from_parameters(
88 codegen: &'a crate::codegen::CodeGenerator<'c>,
89 nsid: &'a str,
90 type_name: &'a str,
91 parameters: &'a LexXrpcParameters<'static>,
92 has_lifetime: bool,
93 ) -> Self {
94 Self {
95 codegen,
96 nsid,
97 type_name,
98 schema: BuilderSchema::Parameters(parameters),
99 has_lifetime,
100 }
101 }
102
103 /// Generate the complete builder for this type
104 pub fn generate(&self) -> TokenStream {
105 // if we got literally no params, don't bother.
106 if self.schema.property_names().is_empty() {
107 return quote! {};
108 }
109 // Collect required fields
110 let required_fields = state_mod::collect_required_fields(&self.schema);
111
112 // Generate state module
113 let state_module = state_mod::generate_state_module(self.type_name, &required_fields);
114
115 // Generate builder struct with constructors
116 let builder_struct = builder_struct::generate_builder_struct(
117 self.codegen,
118 self.nsid,
119 self.type_name,
120 &self.schema,
121 self.has_lifetime,
122 );
123
124 // Generate setters
125 let setters = setters::generate_setters(
126 self.codegen,
127 self.nsid,
128 self.type_name,
129 &self.schema,
130 &required_fields,
131 self.has_lifetime,
132 );
133
134 // Generate build method
135 let build_method = build_method::generate_build_method(
136 self.type_name,
137 &self.schema,
138 &required_fields,
139 self.has_lifetime,
140 );
141
142 quote! {
143 #state_module
144 #builder_struct
145 #setters
146 #build_method
147 }
148 }
149}