···11+//! Implementation of #[lexicon] attribute macro
22+33+use proc_macro2::TokenStream;
44+use quote::quote;
55+use syn::{Data, DeriveInput, Fields, parse2};
66+77+use super::helpers::{conflicts_with_builder_macro, has_derive_builder};
88+99+/// Implementation for the lexicon attribute macro
1010+pub fn impl_lexicon(_attr: TokenStream, item: TokenStream) -> TokenStream {
1111+ let mut input = match parse2::<DeriveInput>(item) {
1212+ Ok(input) => input,
1313+ Err(e) => return e.to_compile_error(),
1414+ };
1515+1616+ match &mut input.data {
1717+ Data::Struct(data_struct) => {
1818+ if let Fields::Named(fields) = &mut data_struct.fields {
1919+ // Check if extra_data field already exists
2020+ let has_extra_data = fields
2121+ .named
2222+ .iter()
2323+ .any(|f| f.ident.as_ref().map(|i| i == "extra_data").unwrap_or(false));
2424+2525+ if !has_extra_data {
2626+ // Check if the struct derives bon::Builder and doesn't conflict with builder macro
2727+ let has_bon_builder = has_derive_builder(&input.attrs)
2828+ && !conflicts_with_builder_macro(&input.ident);
2929+3030+ // Determine the lifetime parameter to use
3131+ let lifetime = if let Some(lt) = input.generics.lifetimes().next() {
3232+ quote! { #lt }
3333+ } else {
3434+ quote! { 'static }
3535+ };
3636+3737+ // Add the extra_data field with serde(borrow) if there's a lifetime
3838+ let new_field: syn::Field = if has_bon_builder {
3939+ syn::parse_quote! {
4040+ #[serde(flatten)]
4141+ #[serde(borrow)]
4242+ #[serde(skip_serializing_if = "std::option::Option::is_none")]
4343+ #[serde(default)]
4444+ pub extra_data: ::std::option::Option<::std::collections::BTreeMap<
4545+ ::jacquard_common::smol_str::SmolStr,
4646+ ::jacquard_common::types::value::Data<#lifetime>
4747+ >>
4848+ }
4949+ } else {
5050+ syn::parse_quote! {
5151+ #[serde(flatten)]
5252+ #[serde(borrow)]
5353+ #[serde(skip_serializing_if = "std::option::Option::is_none")]
5454+ #[serde(default)]
5555+ pub extra_data: ::std::option::Option<::std::collections::BTreeMap<
5656+ ::jacquard_common::smol_str::SmolStr,
5757+ ::jacquard_common::types::value::Data<#lifetime>
5858+ >>
5959+ }
6060+ };
6161+ fields.named.push(new_field);
6262+ }
6363+ } else {
6464+ return syn::Error::new_spanned(
6565+ input,
6666+ "lexicon attribute can only be used on structs with named fields",
6767+ )
6868+ .to_compile_error();
6969+ }
7070+7171+ quote! { #input }
7272+ }
7373+ _ => syn::Error::new_spanned(input, "lexicon attribute can only be used on structs")
7474+ .to_compile_error(),
7575+ }
7676+}
+16
crates/jacquard-lexicon/src/derive_impl/mod.rs
···11+//! Implementation functions for derive macros
22+//!
33+//! These functions are used by the `jacquard-derive` proc-macro crate but are also
44+//! available for runtime code generation in `jacquard-lexicon`.
55+66+pub mod helpers;
77+pub mod into_static;
88+pub mod lexicon_attr;
99+pub mod open_union_attr;
1010+pub mod xrpc_request;
1111+1212+// Re-export the main entry points
1313+pub use into_static::impl_derive_into_static;
1414+pub use lexicon_attr::impl_lexicon;
1515+pub use open_union_attr::impl_open_union;
1616+pub use xrpc_request::impl_derive_xrpc_request;
···11+//! Implementation of #[derive(XrpcRequest)] macro
22+33+use proc_macro2::TokenStream;
44+use quote::{format_ident, quote};
55+use syn::{parse2, Attribute, DeriveInput, Ident, LitStr};
66+77+/// Implementation for the XrpcRequest derive macro
88+pub fn impl_derive_xrpc_request(input: TokenStream) -> TokenStream {
99+ let input = match parse2::<DeriveInput>(input) {
1010+ Ok(input) => input,
1111+ Err(e) => return e.to_compile_error(),
1212+ };
1313+1414+ match xrpc_request_impl(&input) {
1515+ Ok(tokens) => tokens,
1616+ Err(e) => e.to_compile_error(),
1717+ }
1818+}
1919+2020+fn xrpc_request_impl(input: &DeriveInput) -> syn::Result<TokenStream> {
2121+ // Parse attributes
2222+ let attrs = parse_xrpc_attrs(&input.attrs)?;
2323+2424+ let name = &input.ident;
2525+ let generics = &input.generics;
2626+2727+ // Detect if type has lifetime parameter
2828+ let has_lifetime = generics.lifetimes().next().is_some();
2929+ let lifetime = if has_lifetime {
3030+ quote! { <'_> }
3131+ } else {
3232+ quote! {}
3333+ };
3434+3535+ let nsid = &attrs.nsid;
3636+ let method = method_expr(&attrs.method);
3737+ let output_ty = &attrs.output;
3838+ let error_ty = attrs
3939+ .error
4040+ .as_ref()
4141+ .map(|e| quote! { #e })
4242+ .unwrap_or_else(|| quote! { ::jacquard_common::xrpc::GenericError });
4343+4444+ // Generate response marker struct name
4545+ let response_name = format_ident!("{}Response", name);
4646+4747+ // Build the impls
4848+ let mut output = quote! {
4949+ /// Response marker for #name
5050+ pub struct #response_name;
5151+5252+ impl ::jacquard_common::xrpc::XrpcResp for #response_name {
5353+ const NSID: &'static str = #nsid;
5454+ const ENCODING: &'static str = "application/json";
5555+ type Output<'de> = #output_ty<'de>;
5656+ type Err<'de> = #error_ty<'de>;
5757+ }
5858+5959+ impl #generics ::jacquard_common::xrpc::XrpcRequest for #name #lifetime {
6060+ const NSID: &'static str = #nsid;
6161+ const METHOD: ::jacquard_common::xrpc::XrpcMethod = #method;
6262+ type Response = #response_name;
6363+ }
6464+ };
6565+6666+ // Optional server-side endpoint impl
6767+ if attrs.server {
6868+ let endpoint_name = format_ident!("{}Endpoint", name);
6969+ let path = format!("/xrpc/{}", nsid);
7070+7171+ // Request type with or without lifetime
7272+ let request_type = if has_lifetime {
7373+ quote! { #name<'de> }
7474+ } else {
7575+ quote! { #name }
7676+ };
7777+7878+ output.extend(quote! {
7979+ /// Endpoint marker for #name (server-side)
8080+ pub struct #endpoint_name;
8181+8282+ impl ::jacquard_common::xrpc::XrpcEndpoint for #endpoint_name {
8383+ const PATH: &'static str = #path;
8484+ const METHOD: ::jacquard_common::xrpc::XrpcMethod = #method;
8585+ type Request<'de> = #request_type;
8686+ type Response = #response_name;
8787+ }
8888+ });
8989+ }
9090+9191+ Ok(output)
9292+}
9393+9494+struct XrpcAttrs {
9595+ nsid: String,
9696+ method: XrpcMethod,
9797+ output: syn::Type,
9898+ error: Option<syn::Type>,
9999+ server: bool,
100100+}
101101+102102+enum XrpcMethod {
103103+ Query,
104104+ Procedure,
105105+}
106106+107107+fn parse_xrpc_attrs(attrs: &[Attribute]) -> syn::Result<XrpcAttrs> {
108108+ let mut nsid = None;
109109+ let mut method = None;
110110+ let mut output = None;
111111+ let mut error = None;
112112+ let mut server = false;
113113+114114+ for attr in attrs {
115115+ if !attr.path().is_ident("xrpc") {
116116+ continue;
117117+ }
118118+119119+ attr.parse_nested_meta(|meta| {
120120+ if meta.path.is_ident("nsid") {
121121+ let value = meta.value()?;
122122+ let s: LitStr = value.parse()?;
123123+ nsid = Some(s.value());
124124+ Ok(())
125125+ } else if meta.path.is_ident("method") {
126126+ // Parse "method = Query" or "method = Procedure"
127127+ let _eq = meta.input.parse::<syn::Token![=]>()?;
128128+ let ident: Ident = meta.input.parse()?;
129129+ match ident.to_string().as_str() {
130130+ "Query" => {
131131+ method = Some(XrpcMethod::Query);
132132+ Ok(())
133133+ }
134134+ "Procedure" => {
135135+ // Always JSON, no custom encoding support
136136+ method = Some(XrpcMethod::Procedure);
137137+ Ok(())
138138+ }
139139+ other => {
140140+ Err(meta
141141+ .error(format!("unknown method: {}, use Query or Procedure", other)))
142142+ }
143143+ }
144144+ } else if meta.path.is_ident("output") {
145145+ let value = meta.value()?;
146146+ output = Some(value.parse()?);
147147+ Ok(())
148148+ } else if meta.path.is_ident("error") {
149149+ let value = meta.value()?;
150150+ error = Some(value.parse()?);
151151+ Ok(())
152152+ } else if meta.path.is_ident("server") {
153153+ server = true;
154154+ Ok(())
155155+ } else {
156156+ Err(meta.error("unknown xrpc attribute"))
157157+ }
158158+ })?;
159159+ }
160160+161161+ let nsid = nsid.ok_or_else(|| {
162162+ syn::Error::new(
163163+ proc_macro2::Span::call_site(),
164164+ "missing required `nsid` attribute",
165165+ )
166166+ })?;
167167+ let method = method.ok_or_else(|| {
168168+ syn::Error::new(
169169+ proc_macro2::Span::call_site(),
170170+ "missing required `method` attribute",
171171+ )
172172+ })?;
173173+ let output = output.ok_or_else(|| {
174174+ syn::Error::new(
175175+ proc_macro2::Span::call_site(),
176176+ "missing required `output` attribute",
177177+ )
178178+ })?;
179179+180180+ Ok(XrpcAttrs {
181181+ nsid,
182182+ method,
183183+ output,
184184+ error,
185185+ server,
186186+ })
187187+}
188188+189189+fn method_expr(method: &XrpcMethod) -> TokenStream {
190190+ match method {
191191+ XrpcMethod::Query => quote! { ::jacquard_common::xrpc::XrpcMethod::Query },
192192+ XrpcMethod::Procedure => {
193193+ quote! { ::jacquard_common::xrpc::XrpcMethod::Procedure("application/json") }
194194+ }
195195+ }
196196+}
+2
crates/jacquard-lexicon/src/lib.rs
···1212//! - [`schema`] - Schema generation from Rust types (reverse codegen)
1313//! - [`union_registry`] - Tracks union types for collision detection
1414//! - [`fs`] - Filesystem utilities for lexicon storage
1515+//! - [`derive_impl`] - Implementation functions for derive macros (used by jacquard-derive)
15161617pub mod codegen;
1718pub mod corpus;
1919+pub mod derive_impl;
1820pub mod error;
1921pub mod fs;
2022pub mod lexicon;