A better Rust ATProto crate
1//! Builder API for manual lexicon schema construction
2//!
3//! Provides ergonomic API for building lexicon documents without implementing the trait.
4//! Useful for prototyping, testing, and dynamic schema generation.
5
6use crate::lexicon::{
7 LexArray, LexArrayItem, LexBoolean, LexInteger, LexObject, LexObjectProperty, LexRecord,
8 LexRecordRecord, LexRef, LexString, LexStringFormat, LexUserType, LexXrpcBody,
9 LexXrpcBodySchema, LexXrpcError, LexXrpcParameters, LexXrpcParametersProperty, LexXrpcQuery,
10 LexXrpcQueryParameter, Lexicon, LexiconDoc,
11};
12use jacquard_common::CowStr;
13use jacquard_common::smol_str::SmolStr;
14use std::collections::BTreeMap;
15
16/// Builder for lexicon documents
17pub struct LexiconDocBuilder {
18 nsid: SmolStr,
19 description: Option<CowStr<'static>>,
20 defs: BTreeMap<SmolStr, LexUserType<'static>>,
21}
22
23impl LexiconDocBuilder {
24 /// Start building a lexicon document
25 pub fn new(nsid: impl Into<SmolStr>) -> Self {
26 Self {
27 nsid: nsid.into(),
28 description: None,
29 defs: BTreeMap::new(),
30 }
31 }
32
33 /// Set document description
34 pub fn description(mut self, desc: impl Into<CowStr<'static>>) -> Self {
35 self.description = Some(desc.into());
36 self
37 }
38
39 /// Add a record def (becomes "main")
40 pub fn record(self) -> RecordBuilder {
41 RecordBuilder {
42 doc_builder: self,
43 key: None,
44 description: None,
45 properties: BTreeMap::new(),
46 required: Vec::new(),
47 }
48 }
49
50 /// Add an object def
51 pub fn object(self, name: impl Into<SmolStr>) -> ObjectBuilder {
52 ObjectBuilder {
53 doc_builder: self,
54 def_name: name.into(),
55 description: None,
56 properties: BTreeMap::new(),
57 required: Vec::new(),
58 }
59 }
60
61 /// Add a query def
62 pub fn query(self) -> QueryBuilder {
63 QueryBuilder {
64 doc_builder: self,
65 description: None,
66 parameters: BTreeMap::new(),
67 required_params: Vec::new(),
68 output: None,
69 errors: Vec::new(),
70 }
71 }
72
73 /// Build the final document
74 pub fn build(self) -> LexiconDoc<'static> {
75 LexiconDoc {
76 lexicon: Lexicon::Lexicon1,
77 id: self.nsid.into(),
78 revision: None,
79 description: self.description,
80 defs: self.defs,
81 }
82 }
83}
84
85pub struct RecordBuilder {
86 doc_builder: LexiconDocBuilder,
87 key: Option<CowStr<'static>>,
88 description: Option<CowStr<'static>>,
89 properties: BTreeMap<SmolStr, LexObjectProperty<'static>>,
90 required: Vec<SmolStr>,
91}
92
93impl RecordBuilder {
94 /// Set record key type (e.g., "tid")
95 pub fn key(mut self, key: impl Into<CowStr<'static>>) -> Self {
96 self.key = Some(key.into());
97 self
98 }
99
100 /// Set description
101 pub fn description(mut self, desc: impl Into<CowStr<'static>>) -> Self {
102 self.description = Some(desc.into());
103 self
104 }
105
106 /// Add a field
107 pub fn field<F>(mut self, name: impl Into<SmolStr>, builder: F) -> Self
108 where
109 F: FnOnce(FieldBuilder) -> FieldBuilder,
110 {
111 let field_builder = FieldBuilder::new();
112 let field_builder = builder(field_builder);
113
114 let name = name.into();
115 if field_builder.required {
116 self.required.push(name.clone());
117 }
118
119 self.properties.insert(name, field_builder.build());
120 self
121 }
122
123 /// Build and add to document
124 pub fn build(mut self) -> LexiconDocBuilder {
125 let record_obj = LexObject {
126 description: self.description,
127 required: if self.required.is_empty() {
128 None
129 } else {
130 Some(self.required)
131 },
132 nullable: None,
133 properties: self.properties,
134 };
135
136 let record = LexRecord {
137 description: None,
138 key: self.key,
139 record: LexRecordRecord::Object(record_obj),
140 };
141
142 self.doc_builder
143 .defs
144 .insert("main".into(), LexUserType::Record(record));
145 self.doc_builder
146 }
147}
148
149pub struct ObjectBuilder {
150 doc_builder: LexiconDocBuilder,
151 def_name: SmolStr,
152 description: Option<CowStr<'static>>,
153 properties: BTreeMap<SmolStr, LexObjectProperty<'static>>,
154 required: Vec<SmolStr>,
155}
156
157impl ObjectBuilder {
158 /// Set description
159 pub fn description(mut self, desc: impl Into<CowStr<'static>>) -> Self {
160 self.description = Some(desc.into());
161 self
162 }
163
164 /// Add a field
165 pub fn field<F>(mut self, name: impl Into<SmolStr>, builder: F) -> Self
166 where
167 F: FnOnce(FieldBuilder) -> FieldBuilder,
168 {
169 let field_builder = FieldBuilder::new();
170 let field_builder = builder(field_builder);
171
172 let name = name.into();
173 if field_builder.required {
174 self.required.push(name.clone());
175 }
176
177 self.properties.insert(name, field_builder.build());
178 self
179 }
180
181 /// Build and add to document
182 pub fn build(mut self) -> LexiconDocBuilder {
183 let object = LexObject {
184 description: self.description,
185 required: if self.required.is_empty() {
186 None
187 } else {
188 Some(self.required)
189 },
190 nullable: None,
191 properties: self.properties,
192 };
193
194 self.doc_builder
195 .defs
196 .insert(self.def_name, LexUserType::Object(object));
197 self.doc_builder
198 }
199}
200
201pub struct QueryBuilder {
202 doc_builder: LexiconDocBuilder,
203 description: Option<CowStr<'static>>,
204 parameters: BTreeMap<SmolStr, LexXrpcParametersProperty<'static>>,
205 required_params: Vec<SmolStr>,
206 output: Option<LexXrpcBody<'static>>,
207 errors: Vec<LexXrpcError<'static>>,
208}
209
210impl QueryBuilder {
211 /// Set description
212 pub fn description(mut self, desc: impl Into<CowStr<'static>>) -> Self {
213 self.description = Some(desc.into());
214 self
215 }
216
217 /// Add a string parameter
218 pub fn param_string(mut self, name: impl Into<SmolStr>, required: bool) -> Self {
219 let param = LexXrpcParametersProperty::String(LexString {
220 description: None,
221 format: None,
222 default: None,
223 min_length: None,
224 max_length: None,
225 min_graphemes: None,
226 max_graphemes: None,
227 r#enum: None,
228 r#const: None,
229 known_values: None,
230 });
231
232 let name = name.into();
233 if required {
234 self.required_params.push(name.clone());
235 }
236 self.parameters.insert(name, param);
237 self
238 }
239
240 /// Set output schema
241 pub fn output(
242 mut self,
243 encoding: impl Into<CowStr<'static>>,
244 schema: LexXrpcBodySchema<'static>,
245 ) -> Self {
246 self.output = Some(LexXrpcBody {
247 description: None,
248 encoding: encoding.into(),
249 schema: Some(schema),
250 });
251 self
252 }
253
254 /// Build and add to document
255 pub fn build(mut self) -> LexiconDocBuilder {
256 let params = if self.parameters.is_empty() {
257 None
258 } else {
259 Some(LexXrpcQueryParameter::Params(LexXrpcParameters {
260 description: None,
261 required: if self.required_params.is_empty() {
262 None
263 } else {
264 Some(self.required_params)
265 },
266 properties: self.parameters,
267 }))
268 };
269
270 let query = LexXrpcQuery {
271 description: self.description,
272 parameters: params,
273 output: self.output,
274 errors: if self.errors.is_empty() {
275 None
276 } else {
277 Some(self.errors)
278 },
279 };
280
281 self.doc_builder
282 .defs
283 .insert("main".into(), LexUserType::XrpcQuery(query));
284 self.doc_builder
285 }
286}
287
288pub struct FieldBuilder {
289 property: Option<LexObjectProperty<'static>>,
290 required: bool,
291}
292
293impl FieldBuilder {
294 fn new() -> Self {
295 Self {
296 property: None,
297 required: false,
298 }
299 }
300
301 /// Mark field as required
302 pub fn required(mut self) -> Self {
303 self.required = true;
304 self
305 }
306
307 /// String field
308 pub fn string(self) -> StringFieldBuilder {
309 StringFieldBuilder {
310 field_builder: self,
311 format: None,
312 max_length: None,
313 max_graphemes: None,
314 min_length: None,
315 min_graphemes: None,
316 description: None,
317 }
318 }
319
320 /// Integer field
321 pub fn integer(self) -> IntegerFieldBuilder {
322 IntegerFieldBuilder {
323 field_builder: self,
324 minimum: None,
325 maximum: None,
326 description: None,
327 }
328 }
329
330 /// Boolean field
331 pub fn boolean(mut self) -> Self {
332 self.property = Some(LexObjectProperty::Boolean(LexBoolean {
333 description: None,
334 default: None,
335 r#const: None,
336 }));
337 self
338 }
339
340 /// Ref field (to another type)
341 pub fn ref_to(mut self, ref_nsid: impl Into<CowStr<'static>>) -> Self {
342 self.property = Some(LexObjectProperty::Ref(LexRef {
343 description: None,
344 r#ref: ref_nsid.into(),
345 }));
346 self
347 }
348
349 /// Array field
350 pub fn array<F>(mut self, item_builder: F) -> Self
351 where
352 F: FnOnce(ArrayItemBuilder) -> ArrayItemBuilder,
353 {
354 let builder = ArrayItemBuilder::new();
355 let builder = item_builder(builder);
356 self.property = Some(LexObjectProperty::Array(builder.build()));
357 self
358 }
359
360 pub fn build(self) -> LexObjectProperty<'static> {
361 self.property.expect("field type not set")
362 }
363}
364
365pub struct StringFieldBuilder {
366 field_builder: FieldBuilder,
367 format: Option<LexStringFormat>,
368 max_length: Option<usize>,
369 max_graphemes: Option<usize>,
370 min_length: Option<usize>,
371 min_graphemes: Option<usize>,
372 description: Option<CowStr<'static>>,
373}
374
375impl StringFieldBuilder {
376 pub fn format(mut self, format: LexStringFormat) -> Self {
377 self.format = Some(format);
378 self
379 }
380
381 pub fn max_length(mut self, max: usize) -> Self {
382 self.max_length = Some(max);
383 self
384 }
385
386 pub fn max_graphemes(mut self, max: usize) -> Self {
387 self.max_graphemes = Some(max);
388 self
389 }
390
391 pub fn min_length(mut self, min: usize) -> Self {
392 self.min_length = Some(min);
393 self
394 }
395
396 pub fn min_graphemes(mut self, min: usize) -> Self {
397 self.min_graphemes = Some(min);
398 self
399 }
400
401 pub fn description(mut self, desc: impl Into<CowStr<'static>>) -> Self {
402 self.description = Some(desc.into());
403 self
404 }
405
406 pub fn required(mut self) -> Self {
407 self.field_builder.required = true;
408 self
409 }
410
411 pub fn build(mut self) -> FieldBuilder {
412 self.field_builder.property = Some(LexObjectProperty::String(LexString {
413 description: self.description,
414 format: self.format,
415 default: None,
416 min_length: self.min_length,
417 max_length: self.max_length,
418 min_graphemes: self.min_graphemes,
419 max_graphemes: self.max_graphemes,
420 r#enum: None,
421 r#const: None,
422 known_values: None,
423 }));
424 self.field_builder
425 }
426}
427
428pub struct IntegerFieldBuilder {
429 field_builder: FieldBuilder,
430 minimum: Option<i64>,
431 maximum: Option<i64>,
432 description: Option<CowStr<'static>>,
433}
434
435impl IntegerFieldBuilder {
436 pub fn minimum(mut self, min: i64) -> Self {
437 self.minimum = Some(min);
438 self
439 }
440
441 pub fn maximum(mut self, max: i64) -> Self {
442 self.maximum = Some(max);
443 self
444 }
445
446 pub fn description(mut self, desc: impl Into<CowStr<'static>>) -> Self {
447 self.description = Some(desc.into());
448 self
449 }
450
451 pub fn build(mut self) -> FieldBuilder {
452 self.field_builder.property = Some(LexObjectProperty::Integer(LexInteger {
453 description: self.description,
454 default: None,
455 minimum: self.minimum,
456 maximum: self.maximum,
457 r#enum: None,
458 r#const: None,
459 }));
460 self.field_builder
461 }
462}
463
464pub struct ArrayItemBuilder {
465 item: Option<LexArrayItem<'static>>,
466 description: Option<CowStr<'static>>,
467 min_length: Option<usize>,
468 max_length: Option<usize>,
469}
470
471impl ArrayItemBuilder {
472 fn new() -> Self {
473 Self {
474 item: None,
475 description: None,
476 min_length: None,
477 max_length: None,
478 }
479 }
480
481 pub fn description(mut self, desc: impl Into<CowStr<'static>>) -> Self {
482 self.description = Some(desc.into());
483 self
484 }
485
486 pub fn min_length(mut self, min: usize) -> Self {
487 self.min_length = Some(min);
488 self
489 }
490
491 pub fn max_length(mut self, max: usize) -> Self {
492 self.max_length = Some(max);
493 self
494 }
495
496 /// String items
497 pub fn string_items(mut self) -> Self {
498 self.item = Some(LexArrayItem::String(LexString {
499 description: None,
500 format: None,
501 default: None,
502 min_length: None,
503 max_length: None,
504 min_graphemes: None,
505 max_graphemes: None,
506 r#enum: None,
507 r#const: None,
508 known_values: None,
509 }));
510 self
511 }
512
513 /// Ref items
514 pub fn ref_items(mut self, ref_nsid: impl Into<CowStr<'static>>) -> Self {
515 self.item = Some(LexArrayItem::Ref(LexRef {
516 description: None,
517 r#ref: ref_nsid.into(),
518 }));
519 self
520 }
521
522 fn build(self) -> LexArray<'static> {
523 LexArray {
524 description: self.description,
525 items: self.item.expect("array item type not set"),
526 min_length: self.min_length,
527 max_length: self.max_length,
528 }
529 }
530}