+1
Cargo.lock
+1
Cargo.lock
+4
-6
crates/jacquard-api/src/app_rocksky/charts.rs
+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
+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
+1
-1
crates/jacquard-axum/tests/extractor_tests.rs
+2
-2
crates/jacquard-axum/tests/service_auth_tests.rs
+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
+1
crates/jacquard-derive/Cargo.toml
+11
-535
crates/jacquard-derive/src/lib.rs
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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;