jacquard_derive/lib.rs
1//! # Derive macros for jacquard lexicon types
2//!
3//! This crate provides attribute and derive macros for working with Jacquard types.
4//! The code generator uses `#[lexicon]` and `#[open_union]` to add lexicon-specific behavior.
5//! You'll use `#[derive(IntoStatic)]` frequently, `#[derive(XrpcRequest)]` when defining
6//! custom XRPC endpoints, and `#[derive(LexiconSchema)]` for reverse codegen (Rust → lexicon).
7//!
8//! ## Macros
9//!
10//! ### `#[lexicon]`
11//!
12//! Adds an `extra_data` field to structs to capture unknown fields during deserialization.
13//! This makes objects "open" - they'll accept and preserve fields not defined in the schema.
14//!
15//! ```ignore
16//! #[lexicon]
17//! struct Post<'s> {
18//! text: &'s str,
19//! }
20//! // Expands to add:
21//! // #[serde(flatten)]
22//! // pub extra_data: BTreeMap<SmolStr, Data<'s>>
23//! ```
24//!
25//! ### `#[open_union]`
26//!
27//! Adds an `Unknown(Data)` variant to enums to make them extensible unions. This lets
28//! enums accept variants not defined in your code, storing them as loosely typed atproto `Data`.
29//!
30//! ```ignore
31//! #[open_union]
32//! enum RecordEmbed<'s> {
33//! #[serde(rename = "app.bsky.embed.images")]
34//! Images(Images),
35//! }
36//! // Expands to add:
37//! // #[serde(untagged)]
38//! // Unknown(Data<'s>)
39//! ```
40//!
41//! ### `#[derive(IntoStatic)]`
42//!
43//! Derives conversion from borrowed (`'a`) to owned (`'static`) types by recursively calling
44//! `.into_static()` on all fields. Works with structs and enums.
45//!
46//! ```ignore
47//! #[derive(IntoStatic)]
48//! struct Post<'a> {
49//! text: CowStr<'a>,
50//! }
51//! // Generates:
52//! // impl IntoStatic for Post<'_> {
53//! // type Output = Post<'static>;
54//! // fn into_static(self) -> Self::Output { ... }
55//! // }
56//! ```
57//!
58//! ### `#[derive(XrpcRequest)]`
59//!
60//! Derives XRPC request traits for custom endpoints. Generates the response marker struct
61//! and implements `XrpcRequest` (and optionally `XrpcEndpoint` for server-side).
62//!
63//! ```ignore
64//! #[derive(Serialize, Deserialize, XrpcRequest)]
65//! #[xrpc(
66//! nsid = "com.example.getThing",
67//! method = Query,
68//! output = GetThingOutput,
69//! )]
70//! struct GetThing<'a> {
71//! #[serde(borrow)]
72//! pub id: CowStr<'a>,
73//! }
74//! // Generates:
75//! // - GetThingResponse struct
76//! // - impl XrpcResp for GetThingResponse
77//! // - impl XrpcRequest for GetThing
78//! ```
79//!
80//! ### `#[derive(LexiconSchema)]`
81//!
82//! Derives `LexiconSchema` trait for reverse codegen (Rust → lexicon JSON). Generate
83//! lexicon schemas from your Rust types for rapid prototyping and custom lexicons.
84//!
85//! **Type-level attributes** (`#[lexicon(...)]`):
86//! - `nsid = "..."`: The lexicon NSID (required)
87//! - `record`: Mark as a record type
88//! - `key = "..."`: Record key type (`"tid"`, `"literal:self"`, or custom) - optional
89//! - `object`: Mark as an object type (default if neither record/procedure/query)
90//! - `fragment = "..."`: Fragment name for non-main defs (e.g., `fragment = "textSlice"`)
91//!
92//! **Field-level attributes** (`#[lexicon(...)]`):
93//! - `max_length = N`: Max byte length for strings
94//! - `max_graphemes = N`: Max grapheme count for strings
95//! - `min_length = N`, `min_graphemes = N`: Minimum constraints
96//! - `minimum = N`, `maximum = N`: Integer range constraints
97//! - `max_items = N`: Max array length
98//! - `item_max_length = N`, `item_max_graphemes = N`: Constraints on array items
99//! - `ref = "..."`: Explicit type ref (e.g., `ref = "com.atproto.repo.strongRef"` or `ref = "#textSlice"`)
100//! - `union`: Mark field as union type (use with `#[lexicon_union]` enum)
101//!
102//! **Serde integration**: Respects `#[serde(rename)]`, `#[serde(rename_all)]`, and
103//! `#[serde(skip)]`. Defaults to camelCase for field names (lexicon standard).
104//!
105//! **Unions**: Use `#[lexicon_union]` attribute macro, not `#[derive(LexiconSchema)]`.
106//! Mark union fields with `#[lexicon(union)]`.
107//!
108//! ```ignore
109//! // Record with constraints and fragments
110//! #[derive(LexiconSchema)]
111//! #[lexicon(nsid = "app.bsky.feed.post", record, key = "tid")]
112//! #[serde(rename_all = "camelCase")]
113//! struct Post<'a> {
114//! #[lexicon(max_graphemes = 300, max_length = 3000)]
115//! pub text: CowStr<'a>,
116//!
117//! pub created_at: Datetime, // -> "createdAt" (camelCase)
118//!
119//! #[lexicon(union)]
120//! pub embed: Option<PostEmbed<'a>>,
121//!
122//! #[lexicon(max_items = 8, item_max_length = 640, item_max_graphemes = 64)]
123//! pub tags: Option<Vec<CowStr<'a>>>,
124//!
125//! #[lexicon(ref = "app.bsky.richtext.facet")]
126//! pub facets: Option<Vec<CowStr<'a>>>,
127//! }
128//!
129//! // Fragment (non-main def)
130//! #[derive(LexiconSchema)]
131//! #[lexicon(nsid = "app.bsky.feed.post", fragment = "textSlice")]
132//! #[serde(rename_all = "camelCase")]
133//! struct TextSlice {
134//! #[lexicon(minimum = 0)]
135//! pub start: i64,
136//! #[lexicon(minimum = 0)]
137//! pub end: i64,
138//! }
139//!
140//! // Union (uses lexicon_union, not LexiconSchema)
141//! #[lexicon_union]
142//! #[serde(tag = "$type")]
143//! enum PostEmbed<'a> {
144//! #[serde(rename = "app.bsky.embed.images")]
145//! Images(CowStr<'a>),
146//! #[serde(rename = "app.bsky.embed.video")]
147//! Video(CowStr<'a>),
148//! }
149//! ```
150
151use proc_macro::TokenStream;
152
153/// Attribute macro that adds an `extra_data` field to structs to capture unknown fields
154/// during deserialization.
155///
156/// See crate documentation for examples.
157#[proc_macro_attribute]
158pub fn lexicon(attr: TokenStream, item: TokenStream) -> TokenStream {
159 jacquard_lexicon::derive_impl::impl_lexicon(attr.into(), item.into()).into()
160}
161
162/// Attribute macro that adds an `Unknown(Data)` variant to enums to make them open unions.
163///
164/// See crate documentation for examples.
165#[proc_macro_attribute]
166pub fn open_union(attr: TokenStream, item: TokenStream) -> TokenStream {
167 jacquard_lexicon::derive_impl::impl_open_union(attr.into(), item.into()).into()
168}
169
170/// Derive macro for `IntoStatic` trait.
171///
172/// Automatically implements conversion from borrowed to owned ('static) types.
173/// See crate documentation for examples.
174#[proc_macro_derive(IntoStatic)]
175pub fn derive_into_static(input: TokenStream) -> TokenStream {
176 jacquard_lexicon::derive_impl::impl_derive_into_static(input.into()).into()
177}
178
179/// Derive macro for `XrpcRequest` trait.
180///
181/// Automatically generates the response marker struct, `XrpcResp` impl, and `XrpcRequest` impl
182/// for an XRPC endpoint. See crate documentation for examples.
183#[proc_macro_derive(XrpcRequest, attributes(xrpc))]
184pub fn derive_xrpc_request(input: TokenStream) -> TokenStream {
185 jacquard_lexicon::derive_impl::impl_derive_xrpc_request(input.into()).into()
186}
187
188/// Derive macro for `LexiconSchema` trait.
189///
190/// Generates `LexiconSchema` trait impl from Rust types for reverse codegen (Rust → lexicon JSON).
191/// Produces lexicon schema definitions and runtime validation code from your type definitions.
192///
193/// **What it generates:**
194/// - `impl LexiconSchema` with `nsid()`, `schema_id()`, and `lexicon_doc()` methods
195/// - `validate()` method that checks constraints at runtime
196/// - `inventory::submit!` registration for schema discovery
197///
198/// **Attributes:** `#[lexicon(...)]` and `#[nsid = "..."]` on types and fields.
199/// See crate docs for full attribute reference and examples.
200#[proc_macro_derive(LexiconSchema, attributes(lexicon, nsid))]
201pub fn derive_lexicon_schema(input: TokenStream) -> TokenStream {
202 jacquard_lexicon::derive_impl::impl_derive_lexicon_schema(input.into()).into()
203}
204
205/// Attribute macro for union enums.
206///
207/// Marks an enum as a lexicon union type and generates a const containing the union refs
208/// extracted from `#[nsid = "..."]` or `#[serde(rename = "...")]` attributes on variants.
209///
210/// ```ignore
211/// #[lexicon_union]
212/// #[serde(tag = "$type")]
213/// pub enum PostEmbed<'a> {
214/// #[serde(rename = "app.bsky.embed.images")]
215/// Images(Images<'a>),
216/// #[nsid = "app.bsky.embed.video"]
217/// Video(Video<'a>),
218/// }
219/// // Generates: PostEmbed::LEXICON_UNION_REFS const
220/// ```
221#[proc_macro_attribute]
222pub fn lexicon_union(attr: TokenStream, item: TokenStream) -> TokenStream {
223 jacquard_lexicon::derive_impl::impl_lexicon_union(attr.into(), item.into()).into()
224}