A better Rust ATProto crate
at main 530 lines 14 kB view raw
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}