A better Rust ATProto crate

refactored derive/attribute macros

Orual ff8b28d6 9f1fd63c

Changed files
+614 -663
crates
jacquard
jacquard-api
src
app_rocksky
tools_ozone
jacquard-axum
jacquard-derive
jacquard-identity
jacquard-lexicon
+1
Cargo.lock
··· 2417 2417 version = "0.8.0" 2418 2418 dependencies = [ 2419 2419 "jacquard-common 0.8.0", 2420 + "jacquard-lexicon 0.8.0", 2420 2421 "proc-macro2", 2421 2422 "quote", 2422 2423 "serde",
+4 -6
crates/jacquard-api/src/app_rocksky/charts.rs
··· 16 16 PartialEq, 17 17 Eq, 18 18 jacquard_derive::IntoStatic, 19 - Default 19 + Default, 20 20 )] 21 21 #[serde(rename_all = "camelCase")] 22 22 pub struct ChartsView<'a> { 23 23 #[serde(skip_serializing_if = "std::option::Option::is_none")] 24 24 #[serde(borrow)] 25 - pub scrobbles: std::option::Option< 26 - Vec<crate::app_rocksky::charts::ScrobbleViewBasic<'a>>, 27 - >, 25 + pub scrobbles: std::option::Option<Vec<crate::app_rocksky::charts::ScrobbleViewBasic<'a>>>, 28 26 } 29 27 30 28 #[jacquard_derive::lexicon] ··· 36 34 PartialEq, 37 35 Eq, 38 36 jacquard_derive::IntoStatic, 39 - Default 37 + Default, 40 38 )] 41 39 #[serde(rename_all = "camelCase")] 42 40 pub struct ScrobbleViewBasic<'a> { ··· 46 44 /// The date of the scrobble. 47 45 #[serde(skip_serializing_if = "std::option::Option::is_none")] 48 46 pub date: std::option::Option<jacquard_common::types::string::Datetime>, 49 - } 47 + }
+2 -8
crates/jacquard-api/src/tools_ozone/setting.rs
··· 11 11 12 12 #[jacquard_derive::lexicon] 13 13 #[derive( 14 - serde::Serialize, 15 - serde::Deserialize, 16 - Debug, 17 - Clone, 18 - PartialEq, 19 - Eq, 20 - jacquard_derive::IntoStatic 14 + serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, jacquard_derive::IntoStatic, 21 15 )] 22 16 #[serde(rename_all = "camelCase")] 23 17 pub struct Option<'a> { ··· 43 37 pub updated_at: std::option::Option<jacquard_common::types::string::Datetime>, 44 38 #[serde(borrow)] 45 39 pub value: jacquard_common::types::value::Data<'a>, 46 - } 40 + }
+1 -1
crates/jacquard-axum/tests/extractor_tests.rs
··· 79 79 async fn test_handler(ExtractXrpc(req): ExtractXrpc<TestQueryRequest<'_>>) -> impl IntoResponse { 80 80 Json(TestQueryResponse { 81 81 did: req.did, 82 - extra_data: BTreeMap::new(), 82 + extra_data: None, 83 83 }) 84 84 } 85 85
+2 -2
crates/jacquard-axum/tests/service_auth_tests.rs
··· 88 88 r#type: CowStr::new_static("Multikey"), 89 89 controller: Some(CowStr::Owned(did.into())), 90 90 public_key_multibase: Some(CowStr::Owned(multibase_key.into())), 91 - extra_data: BTreeMap::new(), 91 + extra_data: None, 92 92 }]), 93 93 service: None, 94 - extra_data: BTreeMap::new(), 94 + extra_data: None, 95 95 } 96 96 } 97 97
+1
crates/jacquard-derive/Cargo.toml
··· 15 15 proc-macro = true 16 16 17 17 [dependencies] 18 + jacquard-lexicon = { version = "0.8", path = "../jacquard-lexicon" } 18 19 proc-macro2.workspace = true 19 20 quote.workspace = true 20 21 syn.workspace = true
+11 -535
crates/jacquard-derive/src/lib.rs
··· 78 78 //! ``` 79 79 80 80 use proc_macro::TokenStream; 81 - use quote::{format_ident, quote}; 82 - use syn::{Attribute, Data, DeriveInput, Fields, GenericParam, Ident, LitStr, parse_macro_input}; 83 - 84 - /// Helper function to check if a struct derives bon::Builder or Builder 85 - fn has_derive_builder(attrs: &[Attribute]) -> bool { 86 - attrs.iter().any(|attr| { 87 - if !attr.path().is_ident("derive") { 88 - return false; 89 - } 90 - 91 - // Parse the derive attribute to check its contents 92 - if let Ok(list) = attr.parse_args_with( 93 - syn::punctuated::Punctuated::<syn::Path, syn::Token![,]>::parse_terminated, 94 - ) { 95 - list.iter().any(|path| { 96 - // Check for "Builder" or "bon::Builder" 97 - path.segments 98 - .last() 99 - .map(|seg| seg.ident == "Builder") 100 - .unwrap_or(false) 101 - }) 102 - } else { 103 - false 104 - } 105 - }) 106 - } 107 - 108 - /// Check if struct name conflicts with types referenced by bon::Builder macro. 109 - /// bon::Builder generates code that uses unqualified `Option` and `Result`, 110 - /// so structs with these names cause compilation errors. 111 - fn conflicts_with_builder_macro(ident: &Ident) -> bool { 112 - matches!(ident.to_string().as_str(), "Option" | "Result") 113 - } 114 81 115 82 /// Attribute macro that adds an `extra_data` field to structs to capture unknown fields 116 83 /// during deserialization. 117 84 /// 118 - /// # Example 119 - /// ```ignore 120 - /// #[lexicon] 121 - /// struct Post<'s> { 122 - /// text: &'s str, 123 - /// } 124 - /// // Expands to: 125 - /// // struct Post<'s> { 126 - /// // text: &'s str, 127 - /// // #[serde(flatten)] 128 - /// // pub extra_data: BTreeMap<SmolStr, Data<'s>>, 129 - /// // } 130 - /// ``` 85 + /// See crate documentation for examples. 131 86 #[proc_macro_attribute] 132 - pub fn lexicon(_attr: TokenStream, item: TokenStream) -> TokenStream { 133 - let mut input = parse_macro_input!(item as DeriveInput); 134 - 135 - match &mut input.data { 136 - Data::Struct(data_struct) => { 137 - if let Fields::Named(fields) = &mut data_struct.fields { 138 - // Check if extra_data field already exists 139 - let has_extra_data = fields 140 - .named 141 - .iter() 142 - .any(|f| f.ident.as_ref().map(|i| i == "extra_data").unwrap_or(false)); 143 - 144 - if !has_extra_data { 145 - // Check if the struct derives bon::Builder and doesn't conflict with builder macro 146 - let has_bon_builder = has_derive_builder(&input.attrs) 147 - && !conflicts_with_builder_macro(&input.ident); 148 - 149 - // Determine the lifetime parameter to use 150 - let lifetime = if let Some(lt) = input.generics.lifetimes().next() { 151 - quote! { #lt } 152 - } else { 153 - quote! { 'static } 154 - }; 155 - 156 - // Add the extra_data field with serde(borrow) if there's a lifetime 157 - let new_field: syn::Field = if input.generics.lifetimes().next().is_some() { 158 - if has_bon_builder { 159 - syn::parse_quote! { 160 - #[serde(flatten)] 161 - #[serde(borrow)] 162 - #[builder(default)] 163 - pub extra_data: ::std::collections::BTreeMap< 164 - ::jacquard_common::smol_str::SmolStr, 165 - ::jacquard_common::types::value::Data<#lifetime> 166 - > 167 - } 168 - } else { 169 - syn::parse_quote! { 170 - #[serde(flatten)] 171 - #[serde(borrow)] 172 - pub extra_data: ::std::collections::BTreeMap< 173 - ::jacquard_common::smol_str::SmolStr, 174 - ::jacquard_common::types::value::Data<#lifetime> 175 - > 176 - } 177 - } 178 - } else { 179 - // For types without lifetimes, make it optional to avoid lifetime conflicts 180 - if has_bon_builder { 181 - syn::parse_quote! { 182 - #[serde(flatten)] 183 - #[serde(skip_serializing_if = "std::option::Option::is_none")] 184 - #[serde(default)] 185 - #[builder(default)] 186 - pub extra_data: Option<::std::collections::BTreeMap< 187 - ::jacquard_common::smol_str::SmolStr, 188 - ::jacquard_common::types::value::Data<'static> 189 - >> 190 - } 191 - } else { 192 - syn::parse_quote! { 193 - #[serde(flatten)] 194 - #[serde(skip_serializing_if = "std::option::Option::is_none")] 195 - #[serde(default)] 196 - pub extra_data:Option<::std::collections::BTreeMap< 197 - ::jacquard_common::smol_str::SmolStr, 198 - ::jacquard_common::types::value::Data<'static> 199 - >> 200 - } 201 - } 202 - }; 203 - fields.named.push(new_field); 204 - } 205 - } else { 206 - return syn::Error::new_spanned( 207 - input, 208 - "lexicon attribute can only be used on structs with named fields", 209 - ) 210 - .to_compile_error() 211 - .into(); 212 - } 213 - 214 - quote! { #input }.into() 215 - } 216 - _ => syn::Error::new_spanned(input, "lexicon attribute can only be used on structs") 217 - .to_compile_error() 218 - .into(), 219 - } 87 + pub fn lexicon(attr: TokenStream, item: TokenStream) -> TokenStream { 88 + jacquard_lexicon::derive_impl::impl_lexicon(attr.into(), item.into()).into() 220 89 } 221 90 222 - /// Attribute macro that adds an `Other(Data)` variant to enums to make them open unions. 91 + /// Attribute macro that adds an `Unknown(Data)` variant to enums to make them open unions. 223 92 /// 224 - /// # Example 225 - /// ```ignore 226 - /// #[open_union] 227 - /// enum RecordEmbed<'s> { 228 - /// #[serde(rename = "app.bsky.embed.images")] 229 - /// Images(Images), 230 - /// } 231 - /// // Expands to: 232 - /// // enum RecordEmbed<'s> { 233 - /// // #[serde(rename = "app.bsky.embed.images")] 234 - /// // Images(Images), 235 - /// // #[serde(untagged)] 236 - /// // Unknown(Data<'s>), 237 - /// // } 238 - /// ``` 93 + /// See crate documentation for examples. 239 94 #[proc_macro_attribute] 240 - pub fn open_union(_attr: TokenStream, item: TokenStream) -> TokenStream { 241 - let mut input = parse_macro_input!(item as DeriveInput); 242 - 243 - match &mut input.data { 244 - Data::Enum(data_enum) => { 245 - // Check if Unknown variant already exists 246 - let has_other = data_enum.variants.iter().any(|v| v.ident == "Unknown"); 247 - 248 - if !has_other { 249 - // Determine the lifetime parameter to use 250 - let lifetime = if let Some(lt) = input.generics.lifetimes().next() { 251 - quote! { #lt } 252 - } else { 253 - quote! { 'static } 254 - }; 255 - 256 - // Add the Unknown variant 257 - let new_variant: syn::Variant = syn::parse_quote! { 258 - #[serde(untagged)] 259 - Unknown(::jacquard_common::types::value::Data<#lifetime>) 260 - }; 261 - data_enum.variants.push(new_variant); 262 - } 263 - 264 - quote! { #input }.into() 265 - } 266 - _ => syn::Error::new_spanned(input, "open_union attribute can only be used on enums") 267 - .to_compile_error() 268 - .into(), 269 - } 95 + pub fn open_union(attr: TokenStream, item: TokenStream) -> TokenStream { 96 + jacquard_lexicon::derive_impl::impl_open_union(attr.into(), item.into()).into() 270 97 } 271 98 272 99 /// Derive macro for `IntoStatic` trait. 273 100 /// 274 101 /// Automatically implements conversion from borrowed to owned ('static) types. 275 - /// Works with structs and enums that have lifetime parameters. 276 - /// 277 - /// # Example 278 - /// ```ignore 279 - /// #[derive(IntoStatic)] 280 - /// struct Post<'a> { 281 - /// text: CowStr<'a>, 282 - /// } 283 - /// // Generates: 284 - /// // impl IntoStatic for Post<'_> { 285 - /// // type Output = Post<'static>; 286 - /// // fn into_static(self) -> Self::Output { 287 - /// // Post { text: self.text.into_static() } 288 - /// // } 289 - /// // } 290 - /// ``` 102 + /// See crate documentation for examples. 291 103 #[proc_macro_derive(IntoStatic)] 292 104 pub fn derive_into_static(input: TokenStream) -> TokenStream { 293 - let input = parse_macro_input!(input as DeriveInput); 294 - 295 - let name = &input.ident; 296 - let generics = &input.generics; 297 - 298 - // Build impl generics and where clause 299 - let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); 300 - 301 - // Build the Output type with all lifetimes replaced by 'static 302 - let output_generics = generics.params.iter().map(|param| match param { 303 - GenericParam::Lifetime(_) => quote! { 'static }, 304 - GenericParam::Type(ty) => { 305 - let ident = &ty.ident; 306 - quote! { #ident } 307 - } 308 - GenericParam::Const(c) => { 309 - let ident = &c.ident; 310 - quote! { #ident } 311 - } 312 - }); 313 - 314 - let output_type = if generics.params.is_empty() { 315 - quote! { #name } 316 - } else { 317 - quote! { #name<#(#output_generics),*> } 318 - }; 319 - 320 - // Generate the conversion body based on struct/enum 321 - let conversion = match &input.data { 322 - Data::Struct(data_struct) => generate_struct_conversion(name, &data_struct.fields), 323 - Data::Enum(data_enum) => generate_enum_conversion(name, data_enum), 324 - Data::Union(_) => { 325 - return syn::Error::new_spanned(input, "IntoStatic cannot be derived for unions") 326 - .to_compile_error() 327 - .into(); 328 - } 329 - }; 330 - 331 - let expanded = quote! { 332 - impl #impl_generics ::jacquard_common::IntoStatic for #name #ty_generics #where_clause { 333 - type Output = #output_type; 334 - 335 - fn into_static(self) -> Self::Output { 336 - #conversion 337 - } 338 - } 339 - }; 340 - 341 - expanded.into() 342 - } 343 - 344 - fn generate_struct_conversion(name: &syn::Ident, fields: &Fields) -> proc_macro2::TokenStream { 345 - match fields { 346 - Fields::Named(fields) => { 347 - let field_conversions = fields.named.iter().map(|f| { 348 - let field_name = &f.ident; 349 - quote! { #field_name: self.#field_name.into_static() } 350 - }); 351 - quote! { 352 - #name { 353 - #(#field_conversions),* 354 - } 355 - } 356 - } 357 - Fields::Unnamed(fields) => { 358 - let field_conversions = fields.unnamed.iter().enumerate().map(|(i, _)| { 359 - let index = syn::Index::from(i); 360 - quote! { self.#index.into_static() } 361 - }); 362 - quote! { 363 - #name(#(#field_conversions),*) 364 - } 365 - } 366 - Fields::Unit => { 367 - quote! { #name } 368 - } 369 - } 370 - } 371 - 372 - fn generate_enum_conversion( 373 - name: &syn::Ident, 374 - data_enum: &syn::DataEnum, 375 - ) -> proc_macro2::TokenStream { 376 - let variants = data_enum.variants.iter().map(|variant| { 377 - let variant_name = &variant.ident; 378 - match &variant.fields { 379 - Fields::Named(fields) => { 380 - let field_names: Vec<_> = fields.named.iter().map(|f| &f.ident).collect(); 381 - let field_conversions = field_names.iter().map(|field_name| { 382 - quote! { #field_name: #field_name.into_static() } 383 - }); 384 - quote! { 385 - #name::#variant_name { #(#field_names),* } => { 386 - #name::#variant_name { 387 - #(#field_conversions),* 388 - } 389 - } 390 - } 391 - } 392 - Fields::Unnamed(fields) => { 393 - let field_bindings: Vec<_> = (0..fields.unnamed.len()) 394 - .map(|i| { 395 - syn::Ident::new(&format!("field_{}", i), proc_macro2::Span::call_site()) 396 - }) 397 - .collect(); 398 - let field_conversions = field_bindings.iter().map(|binding| { 399 - quote! { #binding.into_static() } 400 - }); 401 - quote! { 402 - #name::#variant_name(#(#field_bindings),*) => { 403 - #name::#variant_name(#(#field_conversions),*) 404 - } 405 - } 406 - } 407 - Fields::Unit => { 408 - quote! { 409 - #name::#variant_name => #name::#variant_name 410 - } 411 - } 412 - } 413 - }); 414 - 415 - quote! { 416 - match self { 417 - #(#variants),* 418 - } 419 - } 105 + jacquard_lexicon::derive_impl::impl_derive_into_static(input.into()).into() 420 106 } 421 107 422 108 /// Derive macro for `XrpcRequest` trait. 423 109 /// 424 110 /// Automatically generates the response marker struct, `XrpcResp` impl, and `XrpcRequest` impl 425 - /// for an XRPC endpoint. Optionally generates `XrpcEndpoint` impl for server-side usage. 426 - /// 427 - /// # Attributes 428 - /// 429 - /// - `nsid`: Required. The NSID string (e.g., "com.example.myMethod") 430 - /// - `method`: Required. Either `Query` or `Procedure` 431 - /// - `output`: Required. The output type (must support lifetime param if request does) 432 - /// - `error`: Optional. Error type (defaults to `GenericError`) 433 - /// - `server`: Optional flag. If present, generates `XrpcEndpoint` impl too 434 - /// 435 - /// # Example 436 - /// ```ignore 437 - /// #[derive(Serialize, Deserialize, XrpcRequest)] 438 - /// #[xrpc( 439 - /// nsid = "com.example.getThing", 440 - /// method = Query, 441 - /// output = GetThingOutput, 442 - /// )] 443 - /// struct GetThing<'a> { 444 - /// #[serde(borrow)] 445 - /// pub id: CowStr<'a>, 446 - /// } 447 - /// ``` 448 - /// 449 - /// This generates: 450 - /// - `GetThingResponse` struct implementing `XrpcResp` 451 - /// - `XrpcRequest` impl for `GetThing` 452 - /// - Optionally: `GetThingEndpoint` struct implementing `XrpcEndpoint` (if `server` flag present) 111 + /// for an XRPC endpoint. See crate documentation for examples. 453 112 #[proc_macro_derive(XrpcRequest, attributes(xrpc))] 454 113 pub fn derive_xrpc_request(input: TokenStream) -> TokenStream { 455 - let input = parse_macro_input!(input as DeriveInput); 456 - 457 - match xrpc_request_impl(&input) { 458 - Ok(tokens) => tokens.into(), 459 - Err(e) => e.to_compile_error().into(), 460 - } 461 - } 462 - 463 - fn xrpc_request_impl(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> { 464 - // Parse attributes 465 - let attrs = parse_xrpc_attrs(&input.attrs)?; 466 - 467 - let name = &input.ident; 468 - let generics = &input.generics; 469 - 470 - // Detect if type has lifetime parameter 471 - let has_lifetime = generics.lifetimes().next().is_some(); 472 - let lifetime = if has_lifetime { 473 - quote! { <'_> } 474 - } else { 475 - quote! {} 476 - }; 477 - 478 - let nsid = &attrs.nsid; 479 - let method = method_expr(&attrs.method); 480 - let output_ty = &attrs.output; 481 - let error_ty = attrs 482 - .error 483 - .as_ref() 484 - .map(|e| quote! { #e }) 485 - .unwrap_or_else(|| quote! { ::jacquard_common::xrpc::GenericError }); 486 - 487 - // Generate response marker struct name 488 - let response_name = format_ident!("{}Response", name); 489 - 490 - // Build the impls 491 - let mut output = quote! { 492 - /// Response marker for #name 493 - pub struct #response_name; 494 - 495 - impl ::jacquard_common::xrpc::XrpcResp for #response_name { 496 - const NSID: &'static str = #nsid; 497 - const ENCODING: &'static str = "application/json"; 498 - type Output<'de> = #output_ty<'de>; 499 - type Err<'de> = #error_ty<'de>; 500 - } 501 - 502 - impl #generics ::jacquard_common::xrpc::XrpcRequest for #name #lifetime { 503 - const NSID: &'static str = #nsid; 504 - const METHOD: ::jacquard_common::xrpc::XrpcMethod = #method; 505 - type Response = #response_name; 506 - } 507 - }; 508 - 509 - // Optional server-side endpoint impl 510 - if attrs.server { 511 - let endpoint_name = format_ident!("{}Endpoint", name); 512 - let path = format!("/xrpc/{}", nsid); 513 - 514 - // Request type with or without lifetime 515 - let request_type = if has_lifetime { 516 - quote! { #name<'de> } 517 - } else { 518 - quote! { #name } 519 - }; 520 - 521 - output.extend(quote! { 522 - /// Endpoint marker for #name (server-side) 523 - pub struct #endpoint_name; 524 - 525 - impl ::jacquard_common::xrpc::XrpcEndpoint for #endpoint_name { 526 - const PATH: &'static str = #path; 527 - const METHOD: ::jacquard_common::xrpc::XrpcMethod = #method; 528 - type Request<'de> = #request_type; 529 - type Response = #response_name; 530 - } 531 - }); 532 - } 533 - 534 - Ok(output) 535 - } 536 - 537 - struct XrpcAttrs { 538 - nsid: String, 539 - method: XrpcMethod, 540 - output: syn::Type, 541 - error: Option<syn::Type>, 542 - server: bool, 543 - } 544 - 545 - enum XrpcMethod { 546 - Query, 547 - Procedure, 548 - } 549 - 550 - fn parse_xrpc_attrs(attrs: &[Attribute]) -> syn::Result<XrpcAttrs> { 551 - let mut nsid = None; 552 - let mut method = None; 553 - let mut output = None; 554 - let mut error = None; 555 - let mut server = false; 556 - 557 - for attr in attrs { 558 - if !attr.path().is_ident("xrpc") { 559 - continue; 560 - } 561 - 562 - attr.parse_nested_meta(|meta| { 563 - if meta.path.is_ident("nsid") { 564 - let value = meta.value()?; 565 - let s: LitStr = value.parse()?; 566 - nsid = Some(s.value()); 567 - Ok(()) 568 - } else if meta.path.is_ident("method") { 569 - // Parse "method = Query" or "method = Procedure" 570 - let _eq = meta.input.parse::<syn::Token![=]>()?; 571 - let ident: Ident = meta.input.parse()?; 572 - match ident.to_string().as_str() { 573 - "Query" => { 574 - method = Some(XrpcMethod::Query); 575 - Ok(()) 576 - } 577 - "Procedure" => { 578 - // Always JSON, no custom encoding support 579 - method = Some(XrpcMethod::Procedure); 580 - Ok(()) 581 - } 582 - other => { 583 - Err(meta 584 - .error(format!("unknown method: {}, use Query or Procedure", other))) 585 - } 586 - } 587 - } else if meta.path.is_ident("output") { 588 - let value = meta.value()?; 589 - output = Some(value.parse()?); 590 - Ok(()) 591 - } else if meta.path.is_ident("error") { 592 - let value = meta.value()?; 593 - error = Some(value.parse()?); 594 - Ok(()) 595 - } else if meta.path.is_ident("server") { 596 - server = true; 597 - Ok(()) 598 - } else { 599 - Err(meta.error("unknown xrpc attribute")) 600 - } 601 - })?; 602 - } 603 - 604 - let nsid = nsid.ok_or_else(|| { 605 - syn::Error::new( 606 - proc_macro2::Span::call_site(), 607 - "missing required `nsid` attribute", 608 - ) 609 - })?; 610 - let method = method.ok_or_else(|| { 611 - syn::Error::new( 612 - proc_macro2::Span::call_site(), 613 - "missing required `method` attribute", 614 - ) 615 - })?; 616 - let output = output.ok_or_else(|| { 617 - syn::Error::new( 618 - proc_macro2::Span::call_site(), 619 - "missing required `output` attribute", 620 - ) 621 - })?; 622 - 623 - Ok(XrpcAttrs { 624 - nsid, 625 - method, 626 - output, 627 - error, 628 - server, 629 - }) 630 - } 631 - 632 - fn method_expr(method: &XrpcMethod) -> proc_macro2::TokenStream { 633 - match method { 634 - XrpcMethod::Query => quote! { ::jacquard_common::xrpc::XrpcMethod::Query }, 635 - XrpcMethod::Procedure => { 636 - quote! { ::jacquard_common::xrpc::XrpcMethod::Procedure("application/json") } 637 - } 638 - } 114 + jacquard_lexicon::derive_impl::impl_derive_xrpc_request(input.into()).into() 639 115 }
+16 -19
crates/jacquard-derive/tests/lexicon.rs
··· 17 17 18 18 assert_eq!(record.text, "hello"); 19 19 assert_eq!(record.count, 42); 20 - assert_eq!(record.extra_data.len(), 2); 21 - assert!(record.extra_data.contains_key("unknown")); 22 - assert!(record.extra_data.contains_key("another")); 20 + 21 + let extra_data = record.extra_data.unwrap(); 22 + assert_eq!(extra_data.len(), 2); 23 + assert!(extra_data.contains_key("unknown")); 24 + assert!(extra_data.contains_key("another")); 23 25 } 24 26 25 27 #[test] ··· 35 37 CowStr::Borrowed("value"), 36 38 )), 37 39 ); 38 - extra.insert( 39 - "number".into(), 40 - Data::Integer(42), 41 - ); 40 + extra.insert("number".into(), Data::Integer(42)); 42 41 extra.insert( 43 42 "nested".into(), 44 43 Data::Object(jacquard_common::types::value::Object({ 45 44 let mut nested_map = BTreeMap::new(); 46 - nested_map.insert( 47 - "inner".into(), 48 - Data::Boolean(true), 49 - ); 45 + nested_map.insert("inner".into(), Data::Boolean(true)); 50 46 nested_map 51 47 })), 52 48 ); ··· 54 50 let record = TestRecord { 55 51 text: "test", 56 52 count: 100, 57 - extra_data: extra, 53 + extra_data: Some(extra), 58 54 }; 59 55 60 56 let json = serde_json::to_string(&record).unwrap(); 61 57 let parsed: TestRecord = serde_json::from_str(&json).unwrap(); 62 58 63 59 assert_eq!(record, parsed); 64 - assert_eq!(parsed.extra_data.len(), 3); 60 + let extra_data = parsed.extra_data.unwrap(); 61 + assert_eq!(extra_data.len(), 3); 65 62 66 63 // Verify the extra fields were preserved 67 - assert!(parsed.extra_data.contains_key("custom")); 68 - assert!(parsed.extra_data.contains_key("number")); 69 - assert!(parsed.extra_data.contains_key("nested")); 64 + assert!(extra_data.contains_key("custom")); 65 + assert!(extra_data.contains_key("number")); 66 + assert!(extra_data.contains_key("nested")); 70 67 71 68 // Verify the values 72 - if let Some(Data::String(s)) = parsed.extra_data.get("custom") { 69 + if let Some(Data::String(s)) = extra_data.get("custom") { 73 70 assert_eq!(s.as_str(), "value"); 74 71 } else { 75 72 panic!("expected custom field to be a string"); 76 73 } 77 74 78 - if let Some(Data::Integer(n)) = parsed.extra_data.get("number") { 75 + if let Some(Data::Integer(n)) = extra_data.get("number") { 79 76 assert_eq!(*n, 42); 80 77 } else { 81 78 panic!("expected number field to be an integer"); 82 79 } 83 80 84 - if let Some(Data::Object(obj)) = parsed.extra_data.get("nested") { 81 + if let Some(Data::Object(obj)) = extra_data.get("nested") { 85 82 assert!(obj.0.contains_key("inner")); 86 83 } else { 87 84 panic!("expected nested field to be an object");
+4 -4
crates/jacquard-identity/src/resolver.rs
··· 97 97 service_endpoint: Some(Data::String(AtprotoStr::Uri(Uri::Https( 98 98 Url::from_str(&mini_doc.pds).unwrap(), 99 99 )))), 100 - extra_data: BTreeMap::new(), 100 + extra_data: None, 101 101 }]), 102 - extra_data: BTreeMap::new(), 102 + extra_data: None, 103 103 }) 104 104 } else { 105 105 Err(IdentityError::missing_pds_endpoint()) ··· 141 141 service_endpoint: Some(Data::String(AtprotoStr::Uri(Uri::Https( 142 142 Url::from_str(&mini_doc.pds).unwrap(), 143 143 )))), 144 - extra_data: BTreeMap::new(), 144 + extra_data: None, 145 145 }]), 146 - extra_data: BTreeMap::new(), 146 + extra_data: None, 147 147 } 148 148 .into_static()) 149 149 } else {
+34
crates/jacquard-lexicon/src/derive_impl/helpers.rs
··· 1 + //! Shared helper functions for derive macros 2 + 3 + use syn::{Attribute, Ident}; 4 + 5 + /// Helper function to check if a struct derives bon::Builder or Builder 6 + pub fn has_derive_builder(attrs: &[Attribute]) -> bool { 7 + attrs.iter().any(|attr| { 8 + if !attr.path().is_ident("derive") { 9 + return false; 10 + } 11 + 12 + // Parse the derive attribute to check its contents 13 + if let Ok(list) = attr.parse_args_with( 14 + syn::punctuated::Punctuated::<syn::Path, syn::Token![,]>::parse_terminated, 15 + ) { 16 + list.iter().any(|path| { 17 + // Check for "Builder" or "bon::Builder" 18 + path.segments 19 + .last() 20 + .map(|seg| seg.ident == "Builder") 21 + .unwrap_or(false) 22 + }) 23 + } else { 24 + false 25 + } 26 + }) 27 + } 28 + 29 + /// Check if struct name conflicts with types referenced by bon::Builder macro. 30 + /// bon::Builder generates code that uses unqualified `Option` and `Result`, 31 + /// so structs with these names cause compilation errors. 32 + pub fn conflicts_with_builder_macro(ident: &Ident) -> bool { 33 + matches!(ident.to_string().as_str(), "Option" | "Result") 34 + }
+136
crates/jacquard-lexicon/src/derive_impl/into_static.rs
··· 1 + //! Implementation of #[derive(IntoStatic)] macro 2 + 3 + use proc_macro2::TokenStream; 4 + use quote::quote; 5 + use syn::{parse2, Data, DeriveInput, Fields, GenericParam}; 6 + 7 + /// Implementation for the IntoStatic derive macro 8 + pub fn impl_derive_into_static(input: TokenStream) -> TokenStream { 9 + let input = match parse2::<DeriveInput>(input) { 10 + Ok(input) => input, 11 + Err(e) => return e.to_compile_error(), 12 + }; 13 + 14 + let name = &input.ident; 15 + let generics = &input.generics; 16 + 17 + // Build impl generics and where clause 18 + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); 19 + 20 + // Build the Output type with all lifetimes replaced by 'static 21 + let output_generics = generics.params.iter().map(|param| match param { 22 + GenericParam::Lifetime(_) => quote! { 'static }, 23 + GenericParam::Type(ty) => { 24 + let ident = &ty.ident; 25 + quote! { #ident } 26 + } 27 + GenericParam::Const(c) => { 28 + let ident = &c.ident; 29 + quote! { #ident } 30 + } 31 + }); 32 + 33 + let output_type = if generics.params.is_empty() { 34 + quote! { #name } 35 + } else { 36 + quote! { #name<#(#output_generics),*> } 37 + }; 38 + 39 + // Generate the conversion body based on struct/enum 40 + let conversion = match &input.data { 41 + Data::Struct(data_struct) => generate_struct_conversion(name, &data_struct.fields), 42 + Data::Enum(data_enum) => generate_enum_conversion(name, data_enum), 43 + Data::Union(_) => { 44 + return syn::Error::new_spanned(input, "IntoStatic cannot be derived for unions") 45 + .to_compile_error(); 46 + } 47 + }; 48 + 49 + quote! { 50 + impl #impl_generics ::jacquard_common::IntoStatic for #name #ty_generics #where_clause { 51 + type Output = #output_type; 52 + 53 + fn into_static(self) -> Self::Output { 54 + #conversion 55 + } 56 + } 57 + } 58 + } 59 + 60 + fn generate_struct_conversion(name: &syn::Ident, fields: &Fields) -> TokenStream { 61 + match fields { 62 + Fields::Named(fields) => { 63 + let field_conversions = fields.named.iter().map(|f| { 64 + let field_name = &f.ident; 65 + quote! { #field_name: self.#field_name.into_static() } 66 + }); 67 + quote! { 68 + #name { 69 + #(#field_conversions),* 70 + } 71 + } 72 + } 73 + Fields::Unnamed(fields) => { 74 + let field_conversions = fields.unnamed.iter().enumerate().map(|(i, _)| { 75 + let index = syn::Index::from(i); 76 + quote! { self.#index.into_static() } 77 + }); 78 + quote! { 79 + #name(#(#field_conversions),*) 80 + } 81 + } 82 + Fields::Unit => { 83 + quote! { #name } 84 + } 85 + } 86 + } 87 + 88 + fn generate_enum_conversion( 89 + name: &syn::Ident, 90 + data_enum: &syn::DataEnum, 91 + ) -> TokenStream { 92 + let variants = data_enum.variants.iter().map(|variant| { 93 + let variant_name = &variant.ident; 94 + match &variant.fields { 95 + Fields::Named(fields) => { 96 + let field_names: Vec<_> = fields.named.iter().map(|f| &f.ident).collect(); 97 + let field_conversions = field_names.iter().map(|field_name| { 98 + quote! { #field_name: #field_name.into_static() } 99 + }); 100 + quote! { 101 + #name::#variant_name { #(#field_names),* } => { 102 + #name::#variant_name { 103 + #(#field_conversions),* 104 + } 105 + } 106 + } 107 + } 108 + Fields::Unnamed(fields) => { 109 + let field_bindings: Vec<_> = (0..fields.unnamed.len()) 110 + .map(|i| { 111 + syn::Ident::new(&format!("field_{}", i), proc_macro2::Span::call_site()) 112 + }) 113 + .collect(); 114 + let field_conversions = field_bindings.iter().map(|binding| { 115 + quote! { #binding.into_static() } 116 + }); 117 + quote! { 118 + #name::#variant_name(#(#field_bindings),*) => { 119 + #name::#variant_name(#(#field_conversions),*) 120 + } 121 + } 122 + } 123 + Fields::Unit => { 124 + quote! { 125 + #name::#variant_name => #name::#variant_name 126 + } 127 + } 128 + } 129 + }); 130 + 131 + quote! { 132 + match self { 133 + #(#variants),* 134 + } 135 + } 136 + }
+76
crates/jacquard-lexicon/src/derive_impl/lexicon_attr.rs
··· 1 + //! Implementation of #[lexicon] attribute macro 2 + 3 + use proc_macro2::TokenStream; 4 + use quote::quote; 5 + use syn::{Data, DeriveInput, Fields, parse2}; 6 + 7 + use super::helpers::{conflicts_with_builder_macro, has_derive_builder}; 8 + 9 + /// Implementation for the lexicon attribute macro 10 + pub fn impl_lexicon(_attr: TokenStream, item: TokenStream) -> TokenStream { 11 + let mut input = match parse2::<DeriveInput>(item) { 12 + Ok(input) => input, 13 + Err(e) => return e.to_compile_error(), 14 + }; 15 + 16 + match &mut input.data { 17 + Data::Struct(data_struct) => { 18 + if let Fields::Named(fields) = &mut data_struct.fields { 19 + // Check if extra_data field already exists 20 + let has_extra_data = fields 21 + .named 22 + .iter() 23 + .any(|f| f.ident.as_ref().map(|i| i == "extra_data").unwrap_or(false)); 24 + 25 + if !has_extra_data { 26 + // Check if the struct derives bon::Builder and doesn't conflict with builder macro 27 + let has_bon_builder = has_derive_builder(&input.attrs) 28 + && !conflicts_with_builder_macro(&input.ident); 29 + 30 + // Determine the lifetime parameter to use 31 + let lifetime = if let Some(lt) = input.generics.lifetimes().next() { 32 + quote! { #lt } 33 + } else { 34 + quote! { 'static } 35 + }; 36 + 37 + // Add the extra_data field with serde(borrow) if there's a lifetime 38 + let new_field: syn::Field = if has_bon_builder { 39 + syn::parse_quote! { 40 + #[serde(flatten)] 41 + #[serde(borrow)] 42 + #[serde(skip_serializing_if = "std::option::Option::is_none")] 43 + #[serde(default)] 44 + pub extra_data: ::std::option::Option<::std::collections::BTreeMap< 45 + ::jacquard_common::smol_str::SmolStr, 46 + ::jacquard_common::types::value::Data<#lifetime> 47 + >> 48 + } 49 + } else { 50 + syn::parse_quote! { 51 + #[serde(flatten)] 52 + #[serde(borrow)] 53 + #[serde(skip_serializing_if = "std::option::Option::is_none")] 54 + #[serde(default)] 55 + pub extra_data: ::std::option::Option<::std::collections::BTreeMap< 56 + ::jacquard_common::smol_str::SmolStr, 57 + ::jacquard_common::types::value::Data<#lifetime> 58 + >> 59 + } 60 + }; 61 + fields.named.push(new_field); 62 + } 63 + } else { 64 + return syn::Error::new_spanned( 65 + input, 66 + "lexicon attribute can only be used on structs with named fields", 67 + ) 68 + .to_compile_error(); 69 + } 70 + 71 + quote! { #input } 72 + } 73 + _ => syn::Error::new_spanned(input, "lexicon attribute can only be used on structs") 74 + .to_compile_error(), 75 + } 76 + }
+16
crates/jacquard-lexicon/src/derive_impl/mod.rs
··· 1 + //! Implementation functions for derive macros 2 + //! 3 + //! These functions are used by the `jacquard-derive` proc-macro crate but are also 4 + //! available for runtime code generation in `jacquard-lexicon`. 5 + 6 + pub mod helpers; 7 + pub mod into_static; 8 + pub mod lexicon_attr; 9 + pub mod open_union_attr; 10 + pub mod xrpc_request; 11 + 12 + // Re-export the main entry points 13 + pub use into_static::impl_derive_into_static; 14 + pub use lexicon_attr::impl_lexicon; 15 + pub use open_union_attr::impl_open_union; 16 + pub use xrpc_request::impl_derive_xrpc_request;
+40
crates/jacquard-lexicon/src/derive_impl/open_union_attr.rs
··· 1 + //! Implementation of #[open_union] attribute macro 2 + 3 + use proc_macro2::TokenStream; 4 + use quote::quote; 5 + use syn::{parse2, Data, DeriveInput}; 6 + 7 + /// Implementation for the open_union attribute macro 8 + pub fn impl_open_union(_attr: TokenStream, item: TokenStream) -> TokenStream { 9 + let mut input = match parse2::<DeriveInput>(item) { 10 + Ok(input) => input, 11 + Err(e) => return e.to_compile_error(), 12 + }; 13 + 14 + match &mut input.data { 15 + Data::Enum(data_enum) => { 16 + // Check if Unknown variant already exists 17 + let has_other = data_enum.variants.iter().any(|v| v.ident == "Unknown"); 18 + 19 + if !has_other { 20 + // Determine the lifetime parameter to use 21 + let lifetime = if let Some(lt) = input.generics.lifetimes().next() { 22 + quote! { #lt } 23 + } else { 24 + quote! { 'static } 25 + }; 26 + 27 + // Add the Unknown variant 28 + let new_variant: syn::Variant = syn::parse_quote! { 29 + #[serde(untagged)] 30 + Unknown(::jacquard_common::types::value::Data<#lifetime>) 31 + }; 32 + data_enum.variants.push(new_variant); 33 + } 34 + 35 + quote! { #input } 36 + } 37 + _ => syn::Error::new_spanned(input, "open_union attribute can only be used on enums") 38 + .to_compile_error(), 39 + } 40 + }
+196
crates/jacquard-lexicon/src/derive_impl/xrpc_request.rs
··· 1 + //! Implementation of #[derive(XrpcRequest)] macro 2 + 3 + use proc_macro2::TokenStream; 4 + use quote::{format_ident, quote}; 5 + use syn::{parse2, Attribute, DeriveInput, Ident, LitStr}; 6 + 7 + /// Implementation for the XrpcRequest derive macro 8 + pub fn impl_derive_xrpc_request(input: TokenStream) -> TokenStream { 9 + let input = match parse2::<DeriveInput>(input) { 10 + Ok(input) => input, 11 + Err(e) => return e.to_compile_error(), 12 + }; 13 + 14 + match xrpc_request_impl(&input) { 15 + Ok(tokens) => tokens, 16 + Err(e) => e.to_compile_error(), 17 + } 18 + } 19 + 20 + fn xrpc_request_impl(input: &DeriveInput) -> syn::Result<TokenStream> { 21 + // Parse attributes 22 + let attrs = parse_xrpc_attrs(&input.attrs)?; 23 + 24 + let name = &input.ident; 25 + let generics = &input.generics; 26 + 27 + // Detect if type has lifetime parameter 28 + let has_lifetime = generics.lifetimes().next().is_some(); 29 + let lifetime = if has_lifetime { 30 + quote! { <'_> } 31 + } else { 32 + quote! {} 33 + }; 34 + 35 + let nsid = &attrs.nsid; 36 + let method = method_expr(&attrs.method); 37 + let output_ty = &attrs.output; 38 + let error_ty = attrs 39 + .error 40 + .as_ref() 41 + .map(|e| quote! { #e }) 42 + .unwrap_or_else(|| quote! { ::jacquard_common::xrpc::GenericError }); 43 + 44 + // Generate response marker struct name 45 + let response_name = format_ident!("{}Response", name); 46 + 47 + // Build the impls 48 + let mut output = quote! { 49 + /// Response marker for #name 50 + pub struct #response_name; 51 + 52 + impl ::jacquard_common::xrpc::XrpcResp for #response_name { 53 + const NSID: &'static str = #nsid; 54 + const ENCODING: &'static str = "application/json"; 55 + type Output<'de> = #output_ty<'de>; 56 + type Err<'de> = #error_ty<'de>; 57 + } 58 + 59 + impl #generics ::jacquard_common::xrpc::XrpcRequest for #name #lifetime { 60 + const NSID: &'static str = #nsid; 61 + const METHOD: ::jacquard_common::xrpc::XrpcMethod = #method; 62 + type Response = #response_name; 63 + } 64 + }; 65 + 66 + // Optional server-side endpoint impl 67 + if attrs.server { 68 + let endpoint_name = format_ident!("{}Endpoint", name); 69 + let path = format!("/xrpc/{}", nsid); 70 + 71 + // Request type with or without lifetime 72 + let request_type = if has_lifetime { 73 + quote! { #name<'de> } 74 + } else { 75 + quote! { #name } 76 + }; 77 + 78 + output.extend(quote! { 79 + /// Endpoint marker for #name (server-side) 80 + pub struct #endpoint_name; 81 + 82 + impl ::jacquard_common::xrpc::XrpcEndpoint for #endpoint_name { 83 + const PATH: &'static str = #path; 84 + const METHOD: ::jacquard_common::xrpc::XrpcMethod = #method; 85 + type Request<'de> = #request_type; 86 + type Response = #response_name; 87 + } 88 + }); 89 + } 90 + 91 + Ok(output) 92 + } 93 + 94 + struct XrpcAttrs { 95 + nsid: String, 96 + method: XrpcMethod, 97 + output: syn::Type, 98 + error: Option<syn::Type>, 99 + server: bool, 100 + } 101 + 102 + enum XrpcMethod { 103 + Query, 104 + Procedure, 105 + } 106 + 107 + fn parse_xrpc_attrs(attrs: &[Attribute]) -> syn::Result<XrpcAttrs> { 108 + let mut nsid = None; 109 + let mut method = None; 110 + let mut output = None; 111 + let mut error = None; 112 + let mut server = false; 113 + 114 + for attr in attrs { 115 + if !attr.path().is_ident("xrpc") { 116 + continue; 117 + } 118 + 119 + attr.parse_nested_meta(|meta| { 120 + if meta.path.is_ident("nsid") { 121 + let value = meta.value()?; 122 + let s: LitStr = value.parse()?; 123 + nsid = Some(s.value()); 124 + Ok(()) 125 + } else if meta.path.is_ident("method") { 126 + // Parse "method = Query" or "method = Procedure" 127 + let _eq = meta.input.parse::<syn::Token![=]>()?; 128 + let ident: Ident = meta.input.parse()?; 129 + match ident.to_string().as_str() { 130 + "Query" => { 131 + method = Some(XrpcMethod::Query); 132 + Ok(()) 133 + } 134 + "Procedure" => { 135 + // Always JSON, no custom encoding support 136 + method = Some(XrpcMethod::Procedure); 137 + Ok(()) 138 + } 139 + other => { 140 + Err(meta 141 + .error(format!("unknown method: {}, use Query or Procedure", other))) 142 + } 143 + } 144 + } else if meta.path.is_ident("output") { 145 + let value = meta.value()?; 146 + output = Some(value.parse()?); 147 + Ok(()) 148 + } else if meta.path.is_ident("error") { 149 + let value = meta.value()?; 150 + error = Some(value.parse()?); 151 + Ok(()) 152 + } else if meta.path.is_ident("server") { 153 + server = true; 154 + Ok(()) 155 + } else { 156 + Err(meta.error("unknown xrpc attribute")) 157 + } 158 + })?; 159 + } 160 + 161 + let nsid = nsid.ok_or_else(|| { 162 + syn::Error::new( 163 + proc_macro2::Span::call_site(), 164 + "missing required `nsid` attribute", 165 + ) 166 + })?; 167 + let method = method.ok_or_else(|| { 168 + syn::Error::new( 169 + proc_macro2::Span::call_site(), 170 + "missing required `method` attribute", 171 + ) 172 + })?; 173 + let output = output.ok_or_else(|| { 174 + syn::Error::new( 175 + proc_macro2::Span::call_site(), 176 + "missing required `output` attribute", 177 + ) 178 + })?; 179 + 180 + Ok(XrpcAttrs { 181 + nsid, 182 + method, 183 + output, 184 + error, 185 + server, 186 + }) 187 + } 188 + 189 + fn method_expr(method: &XrpcMethod) -> TokenStream { 190 + match method { 191 + XrpcMethod::Query => quote! { ::jacquard_common::xrpc::XrpcMethod::Query }, 192 + XrpcMethod::Procedure => { 193 + quote! { ::jacquard_common::xrpc::XrpcMethod::Procedure("application/json") } 194 + } 195 + } 196 + }
+2
crates/jacquard-lexicon/src/lib.rs
··· 12 12 //! - [`schema`] - Schema generation from Rust types (reverse codegen) 13 13 //! - [`union_registry`] - Tracks union types for collision detection 14 14 //! - [`fs`] - Filesystem utilities for lexicon storage 15 + //! - [`derive_impl`] - Implementation functions for derive macros (used by jacquard-derive) 15 16 16 17 pub mod codegen; 17 18 pub mod corpus; 19 + pub mod derive_impl; 18 20 pub mod error; 19 21 pub mod fs; 20 22 pub mod lexicon;
+1 -1
crates/jacquard-lexicon/src/schema.rs
··· 59 59 pub mod builder; 60 60 pub mod type_mapping; 61 61 62 - use crate::lexicon::{Lexicon, LexiconDoc, LexObjectProperty, LexRef, LexUserType}; 62 + use crate::lexicon::{LexObjectProperty, LexRef, LexUserType, Lexicon, LexiconDoc}; 63 63 use jacquard_common::smol_str::SmolStr; 64 64 use std::borrow::Cow; 65 65 use std::collections::{BTreeMap, HashSet};
+54 -70
crates/jacquard/src/client/credential_session.rs
··· 161 161 .with_options(opts) 162 162 .send(&RefreshSession) 163 163 .await?; 164 - let refresh = response 165 - .parse() 166 - .map_err(|_| ClientError::auth(AuthError::RefreshFailed) 164 + let refresh = response.parse().map_err(|_| { 165 + ClientError::auth(AuthError::RefreshFailed) 167 166 .with_help("ensure refresh token is valid and not expired") 168 - .with_url("com.atproto.server.refreshSession"))?; 167 + .with_url("com.atproto.server.refreshSession") 168 + })?; 169 169 170 170 let new_session: AtpSession = refresh.into(); 171 171 let token = AuthorizationToken::Bearer(new_session.access_jwt.clone()); 172 - self.store 173 - .set(key, new_session) 174 - .await 175 - .map_err(|e| ClientError::from(e) 176 - .with_context("failed to persist refreshed session to store"))?; 172 + self.store.set(key, new_session).await.map_err(|e| { 173 + ClientError::from(e).with_context("failed to persist refreshed session to store") 174 + })?; 177 175 178 176 Ok(token) 179 177 } ··· 208 206 let pds = if identifier.as_ref().starts_with("http://") 209 207 || identifier.as_ref().starts_with("https://") 210 208 { 211 - Url::parse(identifier.as_ref()) 212 - .map_err(|e: url::ParseError| ClientError::from(e) 213 - .with_help("identifier should be a valid https:// URL, handle, or DID"))? 209 + Url::parse(identifier.as_ref()).map_err(|e: url::ParseError| { 210 + ClientError::from(e) 211 + .with_help("identifier should be a valid https:// URL, handle, or DID") 212 + })? 214 213 } else if identifier.as_ref().starts_with("did:") { 215 - let did = Did::new(identifier.as_ref()) 216 - .map_err(|e| ClientError::invalid_request(format!("invalid did: {:?}", e)) 217 - .with_help("DID format should be did:method:identifier (e.g., did:plc:abc123)"))?; 218 - let resp = self 219 - .client 220 - .resolve_did_doc(&did) 221 - .await 222 - .map_err(|e| ClientError::from(e) 223 - .with_context("DID document resolution failed during login"))?; 224 - resp.into_owned()? 225 - .pds_endpoint() 226 - .ok_or_else(|| ClientError::invalid_request("missing PDS endpoint") 227 - .with_help("DID document must include a PDS service endpoint"))? 214 + let did = Did::new(identifier.as_ref()).map_err(|e| { 215 + ClientError::invalid_request(format!("invalid did: {:?}", e)) 216 + .with_help("DID format should be did:method:identifier (e.g., did:plc:abc123)") 217 + })?; 218 + let resp = self.client.resolve_did_doc(&did).await.map_err(|e| { 219 + ClientError::from(e).with_context("DID document resolution failed during login") 220 + })?; 221 + resp.into_owned()?.pds_endpoint().ok_or_else(|| { 222 + ClientError::invalid_request("missing PDS endpoint") 223 + .with_help("DID document must include a PDS service endpoint") 224 + })? 228 225 } else { 229 226 // treat as handle 230 - let handle = jacquard_common::types::string::Handle::new(identifier.as_ref()) 231 - .map_err(|e| ClientError::invalid_request(format!("invalid handle: {:?}", e)) 232 - .with_help("handle format should be domain.tld (e.g., alice.bsky.social)"))?; 233 - let did = self 234 - .client 235 - .resolve_handle(&handle) 236 - .await 237 - .map_err(|e| ClientError::from(e) 238 - .with_context("handle resolution failed during login"))?; 239 - let resp = self 240 - .client 241 - .resolve_did_doc(&did) 242 - .await 243 - .map_err(|e| ClientError::from(e) 244 - .with_context("DID document resolution failed during login"))?; 245 - resp.into_owned()? 246 - .pds_endpoint() 247 - .ok_or_else(|| ClientError::invalid_request("missing PDS endpoint") 248 - .with_help("DID document must include a PDS service endpoint"))? 227 + let handle = 228 + jacquard_common::types::string::Handle::new(identifier.as_ref()).map_err(|e| { 229 + ClientError::invalid_request(format!("invalid handle: {:?}", e)) 230 + .with_help("handle format should be domain.tld (e.g., alice.bsky.social)") 231 + })?; 232 + let did = self.client.resolve_handle(&handle).await.map_err(|e| { 233 + ClientError::from(e).with_context("handle resolution failed during login") 234 + })?; 235 + let resp = self.client.resolve_did_doc(&did).await.map_err(|e| { 236 + ClientError::from(e).with_context("DID document resolution failed during login") 237 + })?; 238 + resp.into_owned()?.pds_endpoint().ok_or_else(|| { 239 + ClientError::invalid_request("missing PDS endpoint") 240 + .with_help("DID document must include a PDS service endpoint") 241 + })? 249 242 }; 250 243 251 244 // Build and send createSession ··· 255 248 auth_factor_token, 256 249 identifier: identifier.clone().into_static(), 257 250 password: password.into_static(), 258 - extra_data: BTreeMap::new(), 251 + extra_data: None, 259 252 }; 260 253 261 254 let resp = self ··· 264 257 .with_options(self.options.read().await.clone()) 265 258 .send(&req) 266 259 .await?; 267 - let out = resp 268 - .parse() 269 - .map_err(|_| ClientError::auth(AuthError::NotAuthenticated) 260 + let out = resp.parse().map_err(|_| { 261 + ClientError::auth(AuthError::NotAuthenticated) 270 262 .with_help("check identifier and password are correct") 271 - .with_url("com.atproto.server.createSession"))?; 263 + .with_url("com.atproto.server.createSession") 264 + })?; 272 265 let session = AtpSession::from(out); 273 266 274 267 let sid = session_id.unwrap_or_else(|| CowStr::new_static("session")); ··· 276 269 self.store 277 270 .set(key.clone(), session.clone()) 278 271 .await 279 - .map_err(|e| ClientError::from(e) 280 - .with_context("failed to persist session to store"))?; 272 + .map_err(|e| ClientError::from(e).with_context("failed to persist session to store"))?; 281 273 // If using FileAuthStore, persist PDS for faster resume 282 274 if let Some(file_store) = 283 275 (&*self.store as &dyn Any).downcast_ref::<crate::client::token::FileAuthStore>() ··· 318 310 None 319 311 } 320 312 .unwrap_or({ 321 - let resp = self 322 - .client 323 - .resolve_did_doc(&did) 324 - .await?; 325 - resp.into_owned()? 326 - .pds_endpoint() 327 - .ok_or_else(|| ClientError::invalid_request("missing PDS endpoint") 328 - .with_help("DID document must include a PDS service endpoint"))? 313 + let resp = self.client.resolve_did_doc(&did).await?; 314 + resp.into_owned()?.pds_endpoint().ok_or_else(|| { 315 + ClientError::invalid_request("missing PDS endpoint") 316 + .with_help("DID document must include a PDS service endpoint") 317 + })? 329 318 }); 330 319 331 320 // Activate ··· 365 354 None 366 355 } 367 356 .unwrap_or({ 368 - let resp = self 369 - .client 370 - .resolve_did_doc(&did) 371 - .await?; 372 - resp.into_owned()? 373 - .pds_endpoint() 374 - .ok_or_else(|| ClientError::invalid_request("missing PDS endpoint") 375 - .with_help("DID document must include a PDS service endpoint"))? 357 + let resp = self.client.resolve_did_doc(&did).await?; 358 + resp.into_owned()?.pds_endpoint().ok_or_else(|| { 359 + ClientError::invalid_request("missing PDS endpoint") 360 + .with_help("DID document must include a PDS service endpoint") 361 + })? 376 362 }); 377 363 *self.key.write().await = Some(key.clone()); 378 364 *self.endpoint.write().await = Some(pds); ··· 389 375 let Some(key) = self.key.read().await.clone() else { 390 376 return Ok(()); 391 377 }; 392 - self.store 393 - .del(&key) 394 - .await?; 378 + self.store.del(&key).await?; 395 379 *self.key.write().await = None; 396 380 Ok(()) 397 381 }
+5 -5
crates/jacquard/src/moderation/tests.rs
··· 57 57 default_setting: Some(CowStr::from("hide")), 58 58 adult_only: Some(false), 59 59 locales: vec![], 60 - extra_data: BTreeMap::new(), 60 + extra_data: None, 61 61 }; 62 62 63 63 defs.insert(labeler_did.clone(), vec![spam_def]); ··· 109 109 default_setting: Some(CowStr::from("hide")), 110 110 adult_only: Some(false), 111 111 locales: vec![], 112 - extra_data: BTreeMap::new(), 112 + extra_data: None, 113 113 }; 114 114 115 115 defs.insert(labeler_did.clone(), vec![def]); ··· 228 228 default_setting: Some(CowStr::from("warn")), 229 229 adult_only: Some(false), 230 230 locales: vec![], 231 - extra_data: BTreeMap::new(), 231 + extra_data: None, 232 232 }; 233 233 234 234 // Content blur ··· 239 239 default_setting: Some(CowStr::from("warn")), 240 240 adult_only: Some(false), 241 241 locales: vec![], 242 - extra_data: BTreeMap::new(), 242 + extra_data: None, 243 243 }; 244 244 245 245 defs.insert(labeler_did.clone(), vec![media_def, content_def]); ··· 308 308 default_setting: Some(CowStr::from("warn")), 309 309 adult_only: Some(true), 310 310 locales: vec![], 311 - extra_data: BTreeMap::new(), 311 + extra_data: None, 312 312 }; 313 313 314 314 defs.insert(labeler_did.clone(), vec![adult_def]);
+12 -12
crates/jacquard/src/richtext.rs
··· 767 767 768 768 let feature = FacetFeaturesItem::Link(Box::new(Link { 769 769 uri: Uri::new_owned(&url)?, 770 - extra_data: BTreeMap::new(), 770 + extra_data: None, 771 771 })); 772 772 (display_range, feature) 773 773 } ··· 785 785 786 786 let feature = FacetFeaturesItem::Mention(Box::new(Mention { 787 787 did, 788 - extra_data: BTreeMap::new(), 788 + extra_data: None, 789 789 })); 790 790 (range, feature) 791 791 } ··· 808 808 809 809 let feature = FacetFeaturesItem::Link(Box::new(Link { 810 810 uri: Uri::new_owned(&url)?, 811 - extra_data: BTreeMap::new(), 811 + extra_data: None, 812 812 })); 813 813 (range, feature) 814 814 } ··· 832 832 833 833 let feature = FacetFeaturesItem::Tag(Box::new(Tag { 834 834 tag: CowStr::from(tag.to_smolstr()), 835 - extra_data: BTreeMap::new(), 835 + extra_data: None, 836 836 })); 837 837 (range, feature) 838 838 } ··· 856 856 index: ByteSlice { 857 857 byte_start: range.start as i64, 858 858 byte_end: range.end as i64, 859 - extra_data: BTreeMap::new(), 859 + extra_data: None, 860 860 }, 861 861 features: vec![feature], 862 - extra_data: BTreeMap::new(), 862 + extra_data: None, 863 863 }); 864 864 865 865 last_end = range.end; ··· 912 912 913 913 let feature = FacetFeaturesItem::Link(Box::new(Link { 914 914 uri: crate::types::uri::Uri::new_owned(&url)?, 915 - extra_data: BTreeMap::new(), 915 + extra_data: None, 916 916 })); 917 917 (display_range, feature) 918 918 } ··· 938 938 939 939 let feature = FacetFeaturesItem::Mention(Box::new(Mention { 940 940 did, 941 - extra_data: BTreeMap::new(), 941 + extra_data: None, 942 942 })); 943 943 (range, feature) 944 944 } ··· 962 962 963 963 let feature = FacetFeaturesItem::Link(Box::new(Link { 964 964 uri: crate::types::uri::Uri::new_owned(&url)?, 965 - extra_data: BTreeMap::new(), 965 + extra_data: None, 966 966 })); 967 967 (range, feature) 968 968 } ··· 986 986 987 987 let feature = FacetFeaturesItem::Tag(Box::new(Tag { 988 988 tag: CowStr::from(tag.to_smolstr()), 989 - extra_data: BTreeMap::new(), 989 + extra_data: None, 990 990 })); 991 991 (range, feature) 992 992 } ··· 1010 1010 index: ByteSlice { 1011 1011 byte_start: range.start as i64, 1012 1012 byte_end: range.end as i64, 1013 - extra_data: BTreeMap::new(), 1013 + extra_data: None, 1014 1014 }, 1015 1015 features: vec![feature], 1016 - extra_data: BTreeMap::new(), 1016 + extra_data: None, 1017 1017 }); 1018 1018 1019 1019 last_end = range.end;