1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::fmt::Write as _;
5use proc_macro::TokenStream;
6use quote::{format_ident, quote};
7use syn::parse::{Parse, ParseStream};
8use syn::{
9 Attribute, Fields, FnArg, Ident, ItemEnum, ItemFn, ItemStruct, LitBool, LitInt, LitStr, Pat,
10 PatType, Path as SynPath, ReturnType, Token, Type, TypePath, bracketed, parse_macro_input,
11};
12
13struct OperationArgs {
14 path: LitStr,
15 id: LitStr,
16 summary: LitStr,
17}
18
19struct UniversalOperationArgs {
20 method: HttpMethodArgument,
21 operation: OperationArgs,
22}
23
24struct HttpMethodArgument {
25 value: String,
26 span: proc_macro2::Span,
27}
28
29#[derive(Clone, Copy)]
30enum ProviderLifetimeArgument {
31 Singleton,
32 Request,
33 Transient,
34}
35
36struct ProviderArgs {
37 lifetime: ProviderLifetimeArgument,
38}
39
40struct SecurityArgs {
41 scheme: LitStr,
42 scopes: Vec<LitStr>,
43}
44
45#[derive(Default)]
46struct McpArgs {
47 name: Option<LitStr>,
48 description: Option<LitStr>,
49 risk: Option<LitStr>,
50 confirmation: Option<LitStr>,
51 idempotent: Option<LitBool>,
52 expose_output: Option<LitStr>,
53}
54
55#[derive(Default)]
56struct ModelArgs {
57 rename_all: Option<LitStr>,
58 validator: Option<SynPath>,
59 borrowed: Option<Ident>,
61}
62
63impl Parse for ModelArgs {
64 fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
65 let mut arguments = Self::default();
66
67 while !input.is_empty() {
68 let key = input.parse::<Ident>()?;
69
70 match key.to_string().as_str() {
71 "borrowed" => {
74 if arguments.borrowed.is_some() {
75 return Err(syn::Error::new(
76 key.span(),
77 "`borrowed` was specified twice",
78 ));
79 }
80 arguments.borrowed = Some(key);
81 }
82 "rename_all" => {
83 input.parse::<Token![=]>()?;
84 if arguments.rename_all.is_some() {
85 return Err(syn::Error::new(
86 key.span(),
87 "`rename_all` was specified twice",
88 ));
89 }
90 arguments.rename_all = Some(input.parse::<LitStr>()?);
91 }
92 "validate_with" => {
93 input.parse::<Token![=]>()?;
94 if arguments.validator.is_some() {
95 return Err(syn::Error::new(
96 key.span(),
97 "only one model `validate_with` function may be declared",
98 ));
99 }
100 arguments.validator = Some(input.parse::<SynPath>()?);
101 }
102 _ => {
103 return Err(syn::Error::new(
104 key.span(),
105 "the supported model options are `borrowed`, `rename_all`, \
106 and `validate_with`",
107 ));
108 }
109 }
110
111 if !input.is_empty() {
112 input.parse::<Token![,]>()?;
113 }
114 }
115
116 Ok(arguments)
117 }
118}
119
120#[derive(Clone, Copy, Debug, PartialEq)]
122enum NumericLiteral {
123 Integer(i128),
124 Float(f64),
125}
126
127impl NumericLiteral {
128 fn tokens(self) -> proc_macro2::TokenStream {
129 match self {
130 Self::Integer(value) => {
131 let literal = proc_macro2::Literal::i128_suffixed(value);
132 quote!(::blazingly::validation::NumericValue::Integer(#literal))
133 }
134 Self::Float(value) => {
135 let literal = proc_macro2::Literal::f64_suffixed(value);
136 quote!(::blazingly::validation::NumericValue::Float(#literal))
137 }
138 }
139 }
140
141 fn literal_tokens(self) -> proc_macro2::TokenStream {
143 match self {
144 Self::Integer(value) => {
145 let literal = proc_macro2::Literal::i128_unsuffixed(value);
146 quote!(#literal)
147 }
148 Self::Float(value) => {
149 let literal = proc_macro2::Literal::f64_unsuffixed(value);
150 quote!(#literal)
151 }
152 }
153 }
154
155 fn encoded(self) -> String {
156 match self {
157 Self::Integer(value) => value.to_string(),
158 Self::Float(value) if value.fract() == 0.0 => format!("{value:.1}"),
159 Self::Float(value) => value.to_string(),
160 }
161 }
162
163 #[allow(clippy::cast_precision_loss)]
165 fn widened(self) -> f64 {
166 match self {
167 Self::Integer(value) => value as f64,
168 Self::Float(value) => value,
169 }
170 }
171
172 fn exceeds(self, other: Self) -> bool {
173 if let (Self::Integer(left), Self::Integer(right)) = (self, other) {
174 return left > right;
175 }
176 self.widened() > other.widened()
177 }
178
179 fn is_zero(self) -> bool {
180 match self {
181 Self::Integer(value) => value == 0,
182 Self::Float(value) => value == 0.0,
183 }
184 }
185}
186
187#[derive(Clone)]
193enum DefaultLiteral {
194 Text(LitStr),
195 Number(NumericLiteral),
196 Boolean(LitBool),
197}
198
199impl DefaultLiteral {
200 fn expression(&self) -> proc_macro2::TokenStream {
201 match self {
202 Self::Text(value) => quote!(::std::string::String::from(#value)),
203 Self::Number(value) => value.literal_tokens(),
204 Self::Boolean(value) => quote!(#value),
205 }
206 }
207
208 fn encoded(&self) -> String {
210 match self {
211 Self::Text(value) => json_string(&value.value()),
212 Self::Number(value) => value.encoded(),
213 Self::Boolean(value) => value.value().to_string(),
214 }
215 }
216
217 const fn expectation(&self) -> (&'static str, &'static str) {
219 match self {
220 Self::Text(_) => ("a string literal", "a `String` field"),
221 Self::Number(NumericLiteral::Integer(_)) => {
222 ("an integer literal", "an integer or floating-point field")
223 }
224 Self::Number(NumericLiteral::Float(_)) => {
225 ("a floating-point literal", "a floating-point field")
226 }
227 Self::Boolean(_) => ("a boolean literal", "a `bool` field"),
228 }
229 }
230}
231
232fn json_string(value: &str) -> String {
234 let mut encoded = String::with_capacity(value.len() + 2);
235 encoded.push('"');
236 for character in value.chars() {
237 match character {
238 '"' => encoded.push_str("\\\""),
239 '\\' => encoded.push_str("\\\\"),
240 '\n' => encoded.push_str("\\n"),
241 '\r' => encoded.push_str("\\r"),
242 '\t' => encoded.push_str("\\t"),
243 control if control < ' ' => {
244 let _ = write!(encoded, "\\u{:04x}", control as u32);
245 }
246 other => encoded.push(other),
247 }
248 }
249 encoded.push('"');
250 encoded
251}
252
253#[derive(Clone, Copy, Debug, Eq, PartialEq)]
255enum FieldShape {
256 Text,
257 Integer,
258 Float,
259 Collection,
260 Other,
261}
262
263impl FieldShape {
264 const fn is_numeric(self) -> bool {
265 matches!(self, Self::Integer | Self::Float)
266 }
267
268 const fn may_be_model(self) -> bool {
269 matches!(self, Self::Collection | Self::Other)
270 }
271}
272
273#[derive(Default)]
274struct FieldRules {
275 min_length: Option<(usize, proc_macro2::Span)>,
276 max_length: Option<(usize, proc_macro2::Span)>,
277 email: Option<proc_macro2::Span>,
278 aliases: Vec<LitStr>,
279 validator: Option<SynPath>,
280 nested: bool,
281 minimum: Option<(NumericLiteral, proc_macro2::Span)>,
282 maximum: Option<(NumericLiteral, proc_macro2::Span)>,
283 exclusive_minimum: Option<(NumericLiteral, proc_macro2::Span)>,
284 exclusive_maximum: Option<(NumericLiteral, proc_macro2::Span)>,
285 multiple_of: Option<(NumericLiteral, proc_macro2::Span)>,
286 pattern: Option<LitStr>,
287 min_items: Option<(usize, proc_macro2::Span)>,
288 max_items: Option<(usize, proc_macro2::Span)>,
289 unique_items: Option<proc_macro2::Span>,
290 default: Option<(DefaultLiteral, proc_macro2::Span)>,
291}
292
293struct OperationOutput {
294 status: u16,
295 success: Option<Type>,
296 error: Option<Type>,
297}
298
299#[derive(Clone, Copy)]
300enum OperationInputKind {
301 Path,
302 Query,
303 Header,
304 Cookie,
305 Json,
306 Form,
307 Multipart,
308 File,
309 Stream,
310 WebSocket,
311 Extension,
312 Extract,
313 Dependency,
314 DirectDependency,
315}
316
317struct OperationInput {
318 name: LitStr,
319 kind: OperationInputKind,
320 argument_type: Type,
321 inner: Type,
322 required: bool,
323 by_reference: bool,
329}
330
331struct ErrorVariant {
332 status: u16,
333 code: LitStr,
334 message: LitStr,
335 identifier: Ident,
336 payload: Option<Type>,
337 headers: Vec<(LitStr, LitStr)>,
338}
339
340impl Parse for McpArgs {
341 fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
342 let mut arguments = Self::default();
343
344 while !input.is_empty() {
345 let key = input.parse::<Ident>()?;
346 input.parse::<Token![=]>()?;
347
348 match key.to_string().as_str() {
349 "name" => arguments.name = Some(input.parse()?),
350 "description" => arguments.description = Some(input.parse()?),
351 "risk" => arguments.risk = Some(input.parse()?),
352 "confirmation" => arguments.confirmation = Some(input.parse()?),
353 "idempotent" => arguments.idempotent = Some(input.parse()?),
354 "expose_output" => arguments.expose_output = Some(input.parse()?),
355 _ => {
356 return Err(syn::Error::new(
357 key.span(),
358 "supported MCP keys are `name`, `description`, `risk`, \
359 `confirmation`, `idempotent`, and `expose_output`",
360 ));
361 }
362 }
363
364 if !input.is_empty() {
365 input.parse::<Token![,]>()?;
366 }
367 }
368
369 Ok(arguments)
370 }
371}
372
373impl Parse for OperationArgs {
374 fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
375 let path = input.parse::<LitStr>()?;
376 let mut id = None;
377 let mut summary = None;
378
379 while !input.is_empty() {
380 input.parse::<Token![,]>()?;
381 if input.is_empty() {
382 break;
383 }
384
385 let key = input.parse::<Ident>()?;
386 input.parse::<Token![=]>()?;
387 let value = input.parse::<LitStr>()?;
388
389 match key.to_string().as_str() {
390 "id" => id = Some(value),
391 "summary" => summary = Some(value),
392 _ => {
393 return Err(syn::Error::new(
394 key.span(),
395 "supported keys are `id` and `summary`",
396 ));
397 }
398 }
399 }
400
401 let id = id.ok_or_else(|| {
402 syn::Error::new(path.span(), "an explicit stable `id = \"...\"` is required")
403 })?;
404 let summary = summary.unwrap_or_else(|| LitStr::new("", path.span()));
405
406 Ok(Self { path, id, summary })
407 }
408}
409
410impl Parse for UniversalOperationArgs {
411 fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
412 let mut method = None;
413 let mut path = None;
414 let mut id = None;
415 let mut summary = None;
416
417 while !input.is_empty() {
418 let key = input.parse::<Ident>()?;
419 input.parse::<Token![=]>()?;
420 match key.to_string().as_str() {
421 "method" => {
422 if method.is_some() {
423 return Err(syn::Error::new(key.span(), "`method` was specified twice"));
424 }
425 method = Some(if input.peek(LitStr) {
426 let value = input.parse::<LitStr>()?;
427 HttpMethodArgument {
428 value: value.value(),
429 span: value.span(),
430 }
431 } else {
432 let value = input.parse::<Ident>()?;
433 HttpMethodArgument {
434 value: value.to_string(),
435 span: value.span(),
436 }
437 });
438 }
439 "path" => {
440 if path.is_some() {
441 return Err(syn::Error::new(key.span(), "`path` was specified twice"));
442 }
443 path = Some(input.parse::<LitStr>()?);
444 }
445 "id" => {
446 if id.is_some() {
447 return Err(syn::Error::new(key.span(), "`id` was specified twice"));
448 }
449 id = Some(input.parse::<LitStr>()?);
450 }
451 "summary" => {
452 if summary.is_some() {
453 return Err(syn::Error::new(key.span(), "`summary` was specified twice"));
454 }
455 summary = Some(input.parse::<LitStr>()?);
456 }
457 _ => {
458 return Err(syn::Error::new(
459 key.span(),
460 "supported keys are `method`, `path`, `id`, and `summary`",
461 ));
462 }
463 }
464
465 if !input.is_empty() {
466 input.parse::<Token![,]>()?;
467 }
468 }
469
470 let method =
471 method.ok_or_else(|| syn::Error::new(input.span(), "`method = ...` is required"))?;
472 let path =
473 path.ok_or_else(|| syn::Error::new(input.span(), "`path = \"...\"` is required"))?;
474 let id = id.ok_or_else(|| {
475 syn::Error::new(
476 input.span(),
477 "an explicit stable `id = \"...\"` is required",
478 )
479 })?;
480 let summary = summary.unwrap_or_else(|| LitStr::new("", path.span()));
481
482 Ok(Self {
483 method,
484 operation: OperationArgs { path, id, summary },
485 })
486 }
487}
488
489impl Parse for ProviderArgs {
490 fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
491 if input.is_empty() {
492 return Ok(Self {
493 lifetime: ProviderLifetimeArgument::Request,
494 });
495 }
496
497 let lifetime = input.parse::<Ident>()?;
498 if !input.is_empty() {
499 return Err(input.error(
500 "use `#[provider]`, `#[provider(singleton)]`, \
501 `#[provider(request)]`, or `#[provider(transient)]`",
502 ));
503 }
504 let lifetime = match lifetime.to_string().as_str() {
505 "singleton" => ProviderLifetimeArgument::Singleton,
506 "request" => ProviderLifetimeArgument::Request,
507 "transient" => ProviderLifetimeArgument::Transient,
508 _ => {
509 return Err(syn::Error::new(
510 lifetime.span(),
511 "provider lifetime must be `singleton`, `request`, or `transient`",
512 ));
513 }
514 };
515 Ok(Self { lifetime })
516 }
517}
518
519impl Parse for SecurityArgs {
520 fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
521 let scheme = input.parse::<LitStr>()?;
522 let mut scopes = Vec::new();
523 if !input.is_empty() {
524 input.parse::<Token![,]>()?;
525 let key = input.parse::<Ident>()?;
526 if key != "scopes" {
527 return Err(syn::Error::new(
528 key.span(),
529 "the supported security option is `scopes = [\"...\"]`",
530 ));
531 }
532 input.parse::<Token![=]>()?;
533 let content;
534 bracketed!(content in input);
535 while !content.is_empty() {
536 scopes.push(content.parse()?);
537 if !content.is_empty() {
538 content.parse::<Token![,]>()?;
539 }
540 }
541 }
542 if !input.is_empty() {
543 return Err(input.error("unexpected security option"));
544 }
545 Ok(Self { scheme, scopes })
546 }
547}
548
549#[proc_macro_attribute]
554pub fn operation(arguments: TokenStream, item: TokenStream) -> TokenStream {
555 let arguments = parse_macro_input!(arguments as UniversalOperationArgs);
556 let method = match http_method_tokens(&arguments.method) {
557 Ok(method) => method,
558 Err(error) => return error.into_compile_error().into(),
559 };
560 let mut function = parse_macro_input!(item as ItemFn);
561
562 match operation_tokens(arguments.operation, &mut function, &method) {
563 Ok(tokens) => tokens.into(),
564 Err(error) => error.into_compile_error().into(),
565 }
566}
567
568#[proc_macro_attribute]
591pub fn get(arguments: TokenStream, item: TokenStream) -> TokenStream {
592 expand_operation(arguments, item, "e!(::blazingly::HttpMethod::Get))
593}
594
595#[proc_macro_attribute]
597pub fn head(arguments: TokenStream, item: TokenStream) -> TokenStream {
598 expand_operation(arguments, item, "e!(::blazingly::HttpMethod::Head))
599}
600
601#[proc_macro_attribute]
608pub fn post(arguments: TokenStream, item: TokenStream) -> TokenStream {
609 expand_operation(arguments, item, "e!(::blazingly::HttpMethod::Post))
610}
611
612#[proc_macro_attribute]
614pub fn put(arguments: TokenStream, item: TokenStream) -> TokenStream {
615 expand_operation(arguments, item, "e!(::blazingly::HttpMethod::Put))
616}
617
618#[proc_macro_attribute]
620pub fn patch(arguments: TokenStream, item: TokenStream) -> TokenStream {
621 expand_operation(arguments, item, "e!(::blazingly::HttpMethod::Patch))
622}
623
624#[proc_macro_attribute]
626pub fn delete(arguments: TokenStream, item: TokenStream) -> TokenStream {
627 expand_operation(arguments, item, "e!(::blazingly::HttpMethod::Delete))
628}
629
630#[proc_macro_attribute]
632pub fn options(arguments: TokenStream, item: TokenStream) -> TokenStream {
633 expand_operation(arguments, item, "e!(::blazingly::HttpMethod::Options))
634}
635
636#[proc_macro_attribute]
638pub fn trace(arguments: TokenStream, item: TokenStream) -> TokenStream {
639 expand_operation(arguments, item, "e!(::blazingly::HttpMethod::Trace))
640}
641
642#[proc_macro_attribute]
644pub fn connect(arguments: TokenStream, item: TokenStream) -> TokenStream {
645 expand_operation(arguments, item, "e!(::blazingly::HttpMethod::Connect))
646}
647
648#[proc_macro_attribute]
690pub fn api_model(arguments: TokenStream, item: TokenStream) -> TokenStream {
691 let arguments = parse_macro_input!(arguments as ModelArgs);
692 let mut model = parse_macro_input!(item as syn::Item);
693
694 match api_model_tokens(&arguments, &mut model) {
695 Ok(tokens) => tokens.into(),
696 Err(error) => error.into_compile_error().into(),
697 }
698}
699
700#[proc_macro_attribute]
731pub fn api_error(_arguments: TokenStream, item: TokenStream) -> TokenStream {
732 let mut error = parse_macro_input!(item as ItemEnum);
733
734 match error_tokens(&mut error) {
735 Ok(tokens) => tokens.into(),
736 Err(error) => error.into_compile_error().into(),
737 }
738}
739
740#[proc_macro_attribute]
746pub fn provider(arguments: TokenStream, item: TokenStream) -> TokenStream {
747 let arguments = parse_macro_input!(arguments as ProviderArgs);
748 let function = parse_macro_input!(item as ItemFn);
749
750 match provider_tokens(&arguments, &function) {
751 Ok(tokens) => tokens.into(),
752 Err(error) => error.into_compile_error().into(),
753 }
754}
755
756#[proc_macro_attribute]
784pub fn tool(_arguments: TokenStream, item: TokenStream) -> TokenStream {
785 let function = parse_macro_input!(item as ItemFn);
786 syn::Error::new_spanned(
787 function.sig.ident,
788 "place `#[post(...)]` or `#[operation(...)]` above `#[mcp::tool(...)]`",
789 )
790 .into_compile_error()
791 .into()
792}
793
794#[proc_macro_attribute]
816pub fn security(_arguments: TokenStream, item: TokenStream) -> TokenStream {
817 let function = parse_macro_input!(item as ItemFn);
818 syn::Error::new_spanned(
819 function.sig.ident,
820 "place an HTTP method macro or `#[operation(...)]` above `#[security(...)]`",
821 )
822 .into_compile_error()
823 .into()
824}
825
826fn expand_operation(
827 arguments: TokenStream,
828 item: TokenStream,
829 method: &proc_macro2::TokenStream,
830) -> TokenStream {
831 let arguments = parse_macro_input!(arguments as OperationArgs);
832 let mut function = parse_macro_input!(item as ItemFn);
833
834 match operation_tokens(arguments, &mut function, method) {
835 Ok(tokens) => tokens.into(),
836 Err(error) => error.into_compile_error().into(),
837 }
838}
839
840fn http_method_tokens(method: &HttpMethodArgument) -> syn::Result<proc_macro2::TokenStream> {
841 let method = match method.value.to_ascii_uppercase().as_str() {
842 "GET" => quote!(::blazingly::HttpMethod::Get),
843 "HEAD" => quote!(::blazingly::HttpMethod::Head),
844 "POST" => quote!(::blazingly::HttpMethod::Post),
845 "PUT" => quote!(::blazingly::HttpMethod::Put),
846 "PATCH" => quote!(::blazingly::HttpMethod::Patch),
847 "DELETE" => quote!(::blazingly::HttpMethod::Delete),
848 "OPTIONS" => quote!(::blazingly::HttpMethod::Options),
849 "TRACE" => quote!(::blazingly::HttpMethod::Trace),
850 "CONNECT" => quote!(::blazingly::HttpMethod::Connect),
851 _ => {
852 return Err(syn::Error::new(
853 method.span,
854 "unsupported HTTP method; expected GET, HEAD, POST, PUT, PATCH, \
855 DELETE, OPTIONS, TRACE, or CONNECT",
856 ));
857 }
858 };
859 Ok(method)
860}
861
862#[allow(clippy::too_many_lines)]
863fn provider_tokens(
864 arguments: &ProviderArgs,
865 function: &ItemFn,
866) -> syn::Result<proc_macro2::TokenStream> {
867 if function.sig.constness.is_some()
868 || matches!(&function.sig.safety, syn::Safety::Unsafe(_))
869 || function.sig.abi.is_some()
870 || function.sig.variadic.is_some()
871 || !function.sig.generics.params.is_empty()
872 {
873 return Err(syn::Error::new_spanned(
874 &function.sig,
875 "Blazingly providers must be plain, non-generic Rust functions",
876 ));
877 }
878 if function.sig.inputs.len() > 8 {
879 return Err(syn::Error::new_spanned(
880 &function.sig.inputs,
881 "Blazingly providers accept at most eight arguments",
882 ));
883 }
884 let mut provider_arguments = Vec::with_capacity(function.sig.inputs.len());
885 for input in &function.sig.inputs {
886 let FnArg::Typed(argument) = input else {
887 return Err(syn::Error::new_spanned(
888 input,
889 "provider arguments must be `Depends<T>` or a typed request \
890 input: `Path<T>`, `Query<T>`, `Header<T>`, or `Cookie<T>`",
891 ));
892 };
893 let name = operation_argument_name(&argument.pat)?;
894 let Some((kind, inner)) = OperationInputKind::from_type(&argument.ty) else {
895 return Err(syn::Error::new_spanned(
896 &argument.ty,
897 "provider arguments must be `Depends<T>` or a typed request \
898 input: `Path<T>`, `Query<T>`, `Header<T>`, or `Cookie<T>`",
899 ));
900 };
901 match kind {
902 OperationInputKind::Dependency
903 | OperationInputKind::Path
904 | OperationInputKind::Query
905 | OperationInputKind::Header
906 | OperationInputKind::Cookie => {}
907 _ => {
908 return Err(syn::Error::new_spanned(
909 &argument.ty,
910 "a provider reads scalar request inputs; body payloads \
911 belong to the handler that owns the request body",
912 ));
913 }
914 }
915 let required = wrapper_inner(&inner, "Option").is_none();
916 if matches!(kind, OperationInputKind::Path) && !required {
917 return Err(syn::Error::new_spanned(
918 &argument.ty,
919 "Path<T> arguments are always required and cannot wrap Option<T>",
920 ));
921 }
922 provider_arguments.push((
923 LitStr::new(&name.to_string(), name.span()),
924 kind,
925 (*argument.ty).clone(),
926 inner,
927 required,
928 ));
929 }
930 let request_input_count = provider_arguments
931 .iter()
932 .filter(|(_, kind, ..)| !kind.is_dependency())
933 .count();
934 if request_input_count > 0 && matches!(arguments.lifetime, ProviderLifetimeArgument::Singleton)
935 {
936 return Err(syn::Error::new_spanned(
937 &function.sig,
938 "a singleton provider cannot declare request inputs; it is built \
939 once at compile time, before any request exists",
940 ));
941 }
942
943 let ReturnType::Type(_, output) = &function.sig.output else {
944 return Err(syn::Error::new_spanned(
945 &function.sig,
946 "providers require an explicit output type",
947 ));
948 };
949 let fallible = if let Some((_, error)) = result_types(output) {
950 if !type_is(&error, "DependencyError") {
951 return Err(syn::Error::new_spanned(
952 error,
953 "fallible providers must return `Result<T, DependencyError>`",
954 ));
955 }
956 true
957 } else {
958 false
959 };
960 let asynchronous = function.sig.asyncness.is_some();
961 if asynchronous && matches!(arguments.lifetime, ProviderLifetimeArgument::Singleton) {
962 return Err(syn::Error::new_spanned(
963 &function.sig,
964 "async singleton providers are unsupported because singleton \
965 initialization is deterministic and synchronous at build time",
966 ));
967 }
968
969 let function_name = &function.sig.ident;
970 let provider_module = format_ident!("{function_name}");
971 let visibility = &function.vis;
972
973 if request_input_count > 0 {
974 let body = request_provider_body(
975 arguments,
976 function_name,
977 output,
978 &provider_arguments,
979 asynchronous,
980 fallible,
981 );
982 return Ok(quote! {
983 #function
984
985 #[doc(hidden)]
986 #visibility mod #provider_module {
987 #[allow(unused_imports)]
988 use super::*;
989
990 #[must_use]
991 pub fn provider() -> ::blazingly::RequestProvider {
992 #body
993 }
994 }
995 });
996 }
997
998 let constructor = match (arguments.lifetime, asynchronous, fallible) {
999 (ProviderLifetimeArgument::Singleton, false, false) => format_ident!("singleton"),
1000 (ProviderLifetimeArgument::Singleton, false, true) => format_ident!("try_singleton"),
1001 (ProviderLifetimeArgument::Request, false, false) => format_ident!("request"),
1002 (ProviderLifetimeArgument::Request, false, true) => format_ident!("try_request"),
1003 (ProviderLifetimeArgument::Transient, false, false) => format_ident!("transient"),
1004 (ProviderLifetimeArgument::Transient, false, true) => format_ident!("try_transient"),
1005 (ProviderLifetimeArgument::Request, true, false) => format_ident!("request_async"),
1006 (ProviderLifetimeArgument::Request, true, true) => format_ident!("try_request_async"),
1007 (ProviderLifetimeArgument::Transient, true, false) => format_ident!("transient_async"),
1008 (ProviderLifetimeArgument::Transient, true, true) => {
1009 format_ident!("try_transient_async")
1010 }
1011 (ProviderLifetimeArgument::Singleton, true, _) => unreachable!(),
1012 };
1013
1014 Ok(quote! {
1015 #function
1016
1017 #[doc(hidden)]
1018 #visibility mod #provider_module {
1019 #[allow(unused_imports)]
1020 use super::*;
1021
1022 #[must_use]
1023 pub fn provider() -> ::blazingly::Provider {
1024 ::blazingly::Provider::#constructor(super::#function_name)
1025 }
1026 }
1027 })
1028}
1029
1030#[allow(clippy::too_many_lines)]
1037fn request_provider_body(
1038 arguments: &ProviderArgs,
1039 function_name: &Ident,
1040 output: &Type,
1041 provider_arguments: &[(LitStr, OperationInputKind, Type, Type, bool)],
1042 asynchronous: bool,
1043 fallible: bool,
1044) -> proc_macro2::TokenStream {
1045 let value_type = if fallible {
1046 result_types(output).map_or_else(|| output.clone(), |(success, _)| success)
1047 } else {
1048 output.clone()
1049 };
1050
1051 let mut dependency_keys = Vec::new();
1052 let mut bindings = Vec::new();
1053 let mut call_arguments = Vec::new();
1054 let mut input_records = Vec::new();
1055 let mut depends_index = 0_usize;
1056 let mut input_index = 0_usize;
1057 for (position, (name, kind, wrapper, inner, required)) in provider_arguments.iter().enumerate()
1058 {
1059 let binding = format_ident!("__blazingly_argument_{position}");
1060 if kind.is_dependency() {
1061 let index = depends_index;
1062 depends_index += 1;
1063 dependency_keys.push(quote!(::blazingly::DependencyKey::of::<#inner>()));
1064 bindings.push(quote! {
1065 let #binding = reader.depends::<#inner>(#index)?;
1066 });
1067 } else {
1068 let index = input_index;
1069 input_index += 1;
1070 let source = kind
1071 .source_tokens()
1072 .expect("request provider inputs always have a source");
1073 bindings.push(quote! {
1074 let #binding = (*reader.input::<#wrapper>(#index)?).clone();
1075 });
1076 input_records.push(quote! {
1077 ::blazingly::ProviderInput::new::<#wrapper>(
1078 ::blazingly::InputDescriptor::new(
1079 #name,
1080 #source,
1081 #required,
1082 <#inner as ::blazingly::ApiSchema>::type_descriptor(),
1083 ),
1084 ::std::rc::Rc::new(|invocation: &::blazingly::InvocationInput<'_>| {
1085 <#wrapper as ::blazingly::FromInvocation>::from_invocation(
1086 invocation, #name, #required,
1087 )
1088 .map(|value| {
1089 ::std::rc::Rc::new(value) as ::blazingly::DependencyValue
1090 })
1091 }),
1092 ),
1093 });
1094 }
1095 call_arguments.push(binding);
1096 }
1097
1098 let invocation = if fallible {
1099 quote!(super::#function_name(#(#call_arguments),*))
1100 } else {
1101 quote!(::core::result::Result::Ok(super::#function_name(#(#call_arguments),*)))
1102 };
1103 let (constructor, factory) = if asynchronous {
1104 let invocation = if fallible {
1105 quote!(super::#function_name(#(#call_arguments),*).await)
1106 } else {
1107 quote!(::core::result::Result::Ok(
1108 super::#function_name(#(#call_arguments),*).await
1109 ))
1110 };
1111 let constructor = match arguments.lifetime {
1112 ProviderLifetimeArgument::Transient => format_ident!("transient_from_slots_async"),
1113 _ => format_ident!("request_from_slots_async"),
1114 };
1115 let factory = quote! {
1116 ::std::rc::Rc::new(|reader: &::blazingly::SlotReader<'_>| {
1117 let assemble = move || -> ::core::result::Result<_, ::blazingly::DependencyError> {
1118 #(#bindings)*
1119 ::core::result::Result::Ok((#(#call_arguments,)*))
1120 };
1121 match assemble() {
1122 ::core::result::Result::Ok((#(#call_arguments,)*)) => {
1123 ::std::boxed::Box::pin(async move { #invocation })
1124 }
1125 ::core::result::Result::Err(error) => ::std::boxed::Box::pin(async move {
1126 ::core::result::Result::Err(error)
1127 }),
1128 }
1129 })
1130 };
1131 (constructor, factory)
1132 } else {
1133 let constructor = match arguments.lifetime {
1134 ProviderLifetimeArgument::Transient => format_ident!("transient_from_slots"),
1135 _ => format_ident!("request_from_slots"),
1136 };
1137 let factory = quote! {
1138 ::std::rc::Rc::new(|reader: &::blazingly::SlotReader<'_>| {
1139 #(#bindings)*
1140 #invocation
1141 })
1142 };
1143 (constructor, factory)
1144 };
1145
1146 quote! {
1147 ::blazingly::RequestProvider::new(
1148 ::blazingly::Provider::#constructor::<#value_type>(
1149 ::std::vec![#(#dependency_keys),*],
1150 #input_index,
1151 #factory,
1152 ),
1153 ::std::vec![#(#input_records)*],
1154 )
1155 }
1156}
1157
1158#[allow(clippy::too_many_lines)]
1159fn operation_tokens(
1160 arguments: OperationArgs,
1161 function: &mut ItemFn,
1162 method: &proc_macro2::TokenStream,
1163) -> syn::Result<proc_macro2::TokenStream> {
1164 let asynchronous = function.sig.asyncness.is_some();
1165 let mcp = take_mcp_arguments(&mut function.attrs)?;
1166 let security = take_security_arguments(&mut function.attrs)?;
1167 let inputs = operation_inputs(&function.sig.inputs)?;
1168 if mcp.is_some()
1169 && let Some(stream) = inputs
1170 .iter()
1171 .find(|input| matches!(input.kind, OperationInputKind::Stream))
1172 {
1173 return Err(syn::Error::new_spanned(
1174 &stream.argument_type,
1175 "streaming request bodies are HTTP-only and cannot be exposed as an MCP tool",
1176 ));
1177 }
1178 let output = operation_output(&function.sig.output)?;
1179 let function_name = &function.sig.ident;
1180 let descriptor_module = format_ident!("{function_name}");
1181 let visibility = &function.vis;
1182 let path = arguments.path;
1183 let id = arguments.id;
1184 let summary = arguments.summary;
1185
1186 let input_descriptors = inputs.iter().filter_map(|input| {
1187 let source = input.kind.source_tokens()?;
1188 let name = &input.name;
1189 let required = input.required;
1190 let inner = &input.inner;
1191 Some(quote! {
1192 ::blazingly::InputDescriptor::new(
1193 #name,
1194 #source,
1195 #required,
1196 <#inner as ::blazingly::ApiSchema>::type_descriptor(),
1197 )
1198 })
1199 });
1200 let dependency_descriptors = inputs
1201 .iter()
1202 .filter(|input| input.kind.is_dependency())
1203 .map(|input| {
1204 let inner = &input.inner;
1205 quote! {
1206 ::blazingly::DependencyDescriptor::new(
1207 ::core::any::type_name::<#inner>()
1208 )
1209 }
1210 });
1211 let mcp_projection = mcp_projection(mcp, function_name, &summary)?;
1212 let security_requirements = security.iter().map(|security| {
1213 let scheme = &security.scheme;
1214 let scopes = &security.scopes;
1215 quote! {
1216 ::blazingly::SecurityRequirement::new(#scheme)
1217 .with_scopes(::std::vec![#(#scopes.to_owned()),*])
1218 }
1219 });
1220 let status = output.status;
1221 let success_descriptor = output.success.as_ref().map_or_else(
1222 || quote!(::core::option::Option::None),
1223 |success| {
1224 let success = documented_type(success);
1229 quote!(
1230 ::core::option::Option::Some(
1231 <#success as ::blazingly::ApiSchema>::type_descriptor()
1232 )
1233 )
1234 },
1235 );
1236 let error_responses = output.error.map_or_else(
1237 || quote!(),
1238 |error| {
1239 quote! {
1240 responses.extend(
1241 <#error as ::blazingly::ApiError>::response_descriptors()
1242 );
1243 }
1244 },
1245 );
1246 let executable = operation_executable(&inputs, function_name, asynchronous);
1247
1248 Ok(quote! {
1249 #function
1250
1251 #[doc(hidden)]
1252 #visibility mod #descriptor_module {
1253 #[allow(unused_imports)]
1254 use super::*;
1255
1256 #[must_use]
1257 pub fn descriptor() -> ::blazingly::OperationDescriptor {
1258 let mut responses = ::std::vec![
1259 ::blazingly::ResponseDescriptor::success(
1260 #status,
1261 #success_descriptor,
1262 )
1263 ];
1264 #error_responses
1265 let descriptor = ::blazingly::OperationDescriptor::new(
1266 #method,
1267 #path,
1268 #id,
1269 #summary,
1270 ::core::option::Option::None,
1271 responses,
1272 )
1273 .expect("the operation id was validated by the Blazingly macro")
1274 .with_inputs(::std::vec![#(#input_descriptors),*])
1275 .with_dependencies(::std::vec![#(#dependency_descriptors),*])
1276 .with_security(::std::vec![#(#security_requirements),*]);
1277 #mcp_projection
1278 }
1279
1280 #[must_use]
1281 pub fn executable() -> ::blazingly::ExecutableOperation {
1282 #executable
1283 }
1284 }
1285 })
1286}
1287
1288struct ExecutableParts {
1291 extracted_arguments: Vec<proc_macro2::TokenStream>,
1292 dependency_requests: Vec<proc_macro2::TokenStream>,
1293 call: proc_macro2::TokenStream,
1294 input_binding: proc_macro2::TokenStream,
1295 dependency_binding: proc_macro2::TokenStream,
1296}
1297
1298fn operation_executable(
1299 inputs: &[OperationInput],
1300 function_name: &Ident,
1301 asynchronous: bool,
1302) -> proc_macro2::TokenStream {
1303 let parts = executable_parts(inputs, function_name);
1304 if asynchronous {
1305 asynchronous_executable(&parts)
1306 } else {
1307 synchronous_executable(&parts)
1308 }
1309}
1310
1311fn executable_parts(inputs: &[OperationInput], function_name: &Ident) -> ExecutableParts {
1312 let mut dependency_index = 0_usize;
1313 let extracted_arguments = inputs
1314 .iter()
1315 .enumerate()
1316 .map(|(index, input)| {
1317 let binding = format_ident!("__blazingly_argument_{index}");
1318 if input.kind.is_dependency() {
1319 let inner = &input.inner;
1320 let index = dependency_index;
1321 dependency_index += 1;
1322 if input.by_reference || matches!(input.kind, OperationInputKind::Dependency) {
1326 quote! {
1327 let #binding = dependencies
1328 .get::<#inner>(#index)
1329 .map_err(::blazingly::dependency_error_outcome)?;
1330 }
1331 } else {
1332 quote! {
1333 let #binding = dependencies
1334 .get_cloned::<#inner>(#index)
1335 .map_err(::blazingly::dependency_error_outcome)?;
1336 }
1337 }
1338 } else {
1339 let argument_type = &input.argument_type;
1340 let name = &input.name;
1341 let required = input.required;
1342 quote! {
1343 let #binding = <#argument_type as ::blazingly::FromInvocation>::from_invocation(
1344 &input,
1345 #name,
1346 #required,
1347 )
1348 .map_err(::blazingly::InputRejection::into_execution_outcome)?;
1349 }
1350 }
1351 })
1352 .collect::<Vec<_>>();
1353 let dependency_requests = inputs
1354 .iter()
1355 .filter(|input| input.kind.is_dependency())
1356 .map(|input| {
1357 let inner = &input.inner;
1358 quote!(::blazingly::DependencyRequest::of::<#inner>())
1359 })
1360 .collect::<Vec<_>>();
1361 let handler_arguments = inputs
1362 .iter()
1363 .enumerate()
1364 .map(|(index, input)| {
1365 let binding = format_ident!("__blazingly_argument_{index}");
1366 if input.by_reference {
1367 quote!(&#binding)
1370 } else {
1371 quote!(#binding)
1372 }
1373 })
1374 .collect::<Vec<_>>();
1375 let call = quote!(super::#function_name(#(#handler_arguments),*));
1376 let input_binding = if inputs.iter().any(|input| !input.kind.is_dependency()) {
1379 quote!(input)
1380 } else {
1381 quote!(_)
1382 };
1383 let dependency_binding = if dependency_requests.is_empty() {
1384 quote!(_)
1385 } else {
1386 quote!(dependencies)
1387 };
1388
1389 ExecutableParts {
1390 extracted_arguments,
1391 dependency_requests,
1392 call,
1393 input_binding,
1394 dependency_binding,
1395 }
1396}
1397
1398fn asynchronous_executable(parts: &ExecutableParts) -> proc_macro2::TokenStream {
1399 let ExecutableParts {
1400 extracted_arguments,
1401 dependency_requests,
1402 call,
1403 input_binding,
1404 dependency_binding,
1405 } = parts;
1406 quote! {
1407 ::blazingly::ExecutableOperation::typed_with_dependencies(
1408 descriptor(),
1409 ::std::vec![#(#dependency_requests),*],
1410 |#input_binding, #dependency_binding| {
1411 #(#extracted_arguments)*
1412 ::core::result::Result::Ok(
1413 ::std::boxed::Box::pin(async move {
1414 let output = #call.await;
1415 ::blazingly::OperationOutput::into_execution_outcome(output)
1416 }) as ::blazingly::OperationFuture
1417 )
1418 },
1419 )
1420 }
1421}
1422
1423fn synchronous_executable(parts: &ExecutableParts) -> proc_macro2::TokenStream {
1431 let ExecutableParts {
1432 extracted_arguments,
1433 dependency_requests,
1434 call,
1435 input_binding,
1436 dependency_binding,
1437 } = parts;
1438 quote! {
1439 ::blazingly::ExecutableOperation::typed_sync_with_dependencies(
1440 descriptor(),
1441 ::std::vec![#(#dependency_requests),*],
1442 |#input_binding, #dependency_binding| {
1443 #(#extracted_arguments)*
1444 ::core::result::Result::Ok(
1445 ::blazingly::OperationOutput::into_execution_outcome(#call)
1446 )
1447 },
1448 |#input_binding, #dependency_binding| {
1449 #(#extracted_arguments)*
1450 ::core::result::Result::Ok(
1451 ::std::boxed::Box::pin(async move {
1452 ::blazingly::OperationOutput::into_execution_outcome(#call)
1453 }) as ::blazingly::OperationFuture
1454 )
1455 },
1456 )
1457 }
1458}
1459
1460fn error_tokens(error: &mut ItemEnum) -> syn::Result<proc_macro2::TokenStream> {
1461 let variants = error
1462 .variants
1463 .iter_mut()
1464 .map(parse_error_variant)
1465 .collect::<syn::Result<Vec<_>>>()?;
1466
1467 let name = &error.ident;
1468 let descriptors = variants.iter().map(error_descriptor_tokens);
1469 let failures = variants.iter().map(error_failure_tokens);
1470
1471 Ok(quote! {
1472 #error
1473
1474 impl ::blazingly::ApiError for #name {
1475 fn response_descriptors() -> ::std::vec::Vec<::blazingly::ResponseDescriptor> {
1476 ::std::vec![#(#descriptors),*]
1477 }
1478
1479 fn into_failure(
1480 self,
1481 ) -> ::core::result::Result<
1482 ::blazingly::OperationFailure,
1483 ::blazingly::ResponseBuildError,
1484 > {
1485 match self {
1486 #(#failures),*
1487 }
1488 }
1489 }
1490 })
1491}
1492
1493fn parse_error_variant(variant: &mut syn::Variant) -> syn::Result<ErrorVariant> {
1494 let payload = match &variant.fields {
1495 Fields::Unit => None,
1496 Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
1497 fields.unnamed.first().map(|field| field.ty.clone())
1498 }
1499 Fields::Unnamed(_) | Fields::Named(_) => {
1500 return Err(syn::Error::new_spanned(
1501 &variant.fields,
1502 "typed errors support unit variants or one unnamed payload",
1503 ));
1504 }
1505 };
1506 let mut status = None;
1507 let mut code = None;
1508 let mut message = None;
1509 let mut headers = Vec::new();
1510 let mut retained = Vec::new();
1511
1512 for attribute in variant.attrs.drain(..) {
1513 if attribute.path().is_ident("status") {
1514 status = Some(attribute.parse_args::<LitInt>()?);
1515 } else if attribute.path().is_ident("code") {
1516 code = Some(attribute.parse_args::<LitStr>()?);
1517 } else if attribute.path().is_ident("message") {
1518 message = Some(attribute.parse_args::<LitStr>()?);
1519 } else if attribute.path().is_ident("header") {
1520 headers.push(parse_error_header(attribute)?);
1521 } else {
1522 retained.push(attribute);
1523 }
1524 }
1525 variant.attrs = retained;
1526 let status = status.ok_or_else(|| {
1527 syn::Error::new_spanned(&variant.ident, "typed errors require `#[status(...)]`")
1528 })?;
1529 let status = status.base10_parse::<u16>()?;
1530 if !(400..=599).contains(&status) {
1531 return Err(syn::Error::new_spanned(
1532 &variant.ident,
1533 "typed error status must be between 400 and 599",
1534 ));
1535 }
1536 let code = code.ok_or_else(|| {
1537 syn::Error::new_spanned(&variant.ident, "typed errors require `#[code(\"...\")]`")
1538 })?;
1539 let message = message.unwrap_or_else(|| LitStr::new(&code.value(), code.span()));
1540 Ok(ErrorVariant {
1541 status,
1542 code,
1543 message,
1544 identifier: variant.ident.clone(),
1545 payload,
1546 headers,
1547 })
1548}
1549
1550fn parse_error_header(attribute: Attribute) -> syn::Result<(LitStr, LitStr)> {
1551 let values = attribute
1552 .parse_args_with(syn::punctuated::Punctuated::<LitStr, Token![,]>::parse_terminated)?;
1553 if values.len() != 2 {
1554 return Err(syn::Error::new_spanned(
1555 attribute,
1556 "response headers require `#[header(\"name\", \"value\")]`",
1557 ));
1558 }
1559 let mut values = values.into_iter();
1560 let name = values
1561 .next()
1562 .ok_or_else(|| syn::Error::new(proc_macro2::Span::call_site(), "missing header name"))?;
1563 let value = values
1564 .next()
1565 .ok_or_else(|| syn::Error::new(proc_macro2::Span::call_site(), "missing header value"))?;
1566 validate_response_header(&name, &value)?;
1567 Ok((name, value))
1568}
1569
1570fn error_descriptor_tokens(variant: &ErrorVariant) -> proc_macro2::TokenStream {
1571 let status = variant.status;
1572 let code = &variant.code;
1573 let message = &variant.message;
1574 let body = variant.payload.as_ref().map_or_else(
1575 || quote!(::core::option::Option::None),
1576 |payload| {
1577 quote!(
1578 ::core::option::Option::Some(
1579 <#payload as ::blazingly::ApiSchema>::type_descriptor()
1580 )
1581 )
1582 },
1583 );
1584 let headers = variant
1585 .headers
1586 .iter()
1587 .map(|(name, value)| quote!(::blazingly::ResponseHeader::new(#name, #value)));
1588 quote!(
1589 ::blazingly::ResponseDescriptor::error(#status, #code, #message, #body)
1590 .with_headers(::std::vec![#(#headers),*])
1591 )
1592}
1593
1594fn error_failure_tokens(variant: &ErrorVariant) -> proc_macro2::TokenStream {
1595 let identifier = &variant.identifier;
1596 let status = variant.status;
1597 let code = &variant.code;
1598 let message = &variant.message;
1599 let pattern = variant.payload.as_ref().map_or_else(
1600 || quote!(Self::#identifier),
1601 |_| quote!(Self::#identifier(payload)),
1602 );
1603 let serialize_payload = variant.payload.as_ref().map_or_else(
1604 || quote!(),
1605 |_| {
1606 quote! {
1607 let details = ::blazingly::__private::blazingly_json::to_vec(&payload)
1608 .map_err(|_| ::blazingly::ResponseBuildError::serialization_failed())?;
1609 failure = failure.with_details(details);
1610 }
1611 },
1612 );
1613 let apply_headers = variant.headers.iter().map(|(name, value)| {
1614 quote! {
1615 failure = failure.with_header(#name, #value);
1616 }
1617 });
1618 quote! {
1619 #pattern => {
1620 let mut failure = ::blazingly::OperationFailure::new(#status, #code, #message);
1621 #serialize_payload
1622 #(#apply_headers)*
1623 ::core::result::Result::Ok(failure)
1624 }
1625 }
1626}
1627
1628fn validate_response_header(name: &LitStr, value: &LitStr) -> syn::Result<()> {
1629 let valid_name = !name.value().is_empty()
1630 && name.value().bytes().all(|byte| {
1631 byte.is_ascii_alphanumeric()
1632 || matches!(
1633 byte,
1634 b'!' | b'#'
1635 | b'$'
1636 | b'%'
1637 | b'&'
1638 | b'\''
1639 | b'*'
1640 | b'+'
1641 | b'-'
1642 | b'.'
1643 | b'^'
1644 | b'_'
1645 | b'`'
1646 | b'|'
1647 | b'~'
1648 )
1649 });
1650 if !valid_name {
1651 return Err(syn::Error::new(
1652 name.span(),
1653 "response header name contains invalid bytes",
1654 ));
1655 }
1656 if !value
1657 .value()
1658 .bytes()
1659 .all(|byte| byte == b'\t' || (byte >= b' ' && byte != 127))
1660 {
1661 return Err(syn::Error::new(
1662 value.span(),
1663 "response header value contains control bytes",
1664 ));
1665 }
1666 Ok(())
1667}
1668
1669#[allow(clippy::too_many_lines)]
1670fn api_model_tokens(
1671 arguments: &ModelArgs,
1672 item: &mut syn::Item,
1673) -> syn::Result<proc_macro2::TokenStream> {
1674 match item {
1675 syn::Item::Struct(model) => model_tokens(arguments, model),
1676 syn::Item::Enum(model) => enum_model_tokens(arguments, model),
1677 other => Err(syn::Error::new_spanned(
1678 other,
1679 "`#[api_model]` describes a struct, a one-field value type, or a \
1680 unit-variant enum",
1681 )),
1682 }
1683}
1684
1685fn model_tokens(
1686 arguments: &ModelArgs,
1687 model: &mut ItemStruct,
1688) -> syn::Result<proc_macro2::TokenStream> {
1689 if matches!(model.fields, Fields::Unnamed(_)) {
1690 return value_model_tokens(arguments, model);
1691 }
1692 if arguments.borrowed.is_some() {
1693 return borrowed_model_tokens(arguments, model);
1694 }
1695 owned_model_tokens(arguments, model)
1696}
1697
1698fn value_model_tokens(
1707 arguments: &ModelArgs,
1708 model: &mut ItemStruct,
1709) -> syn::Result<proc_macro2::TokenStream> {
1710 let inner = value_type_inner(arguments, model)?;
1711 let mut rules = take_field_rules(&mut model.attrs)?;
1712 reject_field_only_rules(&rules, &model.ident)?;
1713
1714 let shape = field_shape(&inner);
1715 reject_incompatible_rules(&rules, &inner, shape)?;
1716 normalize_collection_rules(&mut rules, shape);
1717
1718 let model_name = &model.ident;
1719 let anonymous = LitStr::new("", model_name.span());
1722 let declared_rules = rule_descriptors(&rules, false);
1723 let checks = field_checks(&rules, shape, &anonymous);
1724 let custom = rules.validator.as_ref().map(|validator| {
1725 quote! {
1726 if let ::core::result::Result::Err(custom_errors) = #validator(value) {
1727 ::blazingly::merge_field_validation_errors(
1728 &mut errors,
1729 #anonymous,
1730 &custom_errors,
1731 );
1732 }
1733 }
1734 });
1735
1736 Ok(quote! {
1737 #[derive(
1738 ::blazingly::__private::serde::Serialize,
1739 ::blazingly::__private::serde::Deserialize
1740 )]
1741 #[serde(crate = "::blazingly::__private::serde")]
1742 #[serde(transparent)]
1743 #model
1744
1745 impl #model_name {
1746 #[must_use]
1749 pub fn new(value: #inner) -> Self {
1750 Self(value)
1751 }
1752
1753 #[must_use]
1755 pub const fn as_inner(&self) -> &#inner {
1756 &self.0
1757 }
1758
1759 #[must_use]
1761 pub fn into_inner(self) -> #inner {
1762 self.0
1763 }
1764 }
1765
1766 const _: () = {
1767 impl ::blazingly::ApiSchema for #model_name {
1768 fn type_descriptor() -> ::blazingly::TypeDescriptor {
1769 let mut descriptor =
1770 <#inner as ::blazingly::ApiSchema>::type_descriptor();
1771 descriptor.rust_name = ::std::string::String::from(
1772 stringify!(#model_name)
1773 );
1774 descriptor.constraints.extend(
1777 <Self as ::blazingly::ApiConstrained>::constraint_rules(),
1778 );
1779 descriptor
1780 }
1781
1782 fn validate_input(
1783 &self,
1784 ) -> ::core::result::Result<(), ::blazingly::ValidationErrors> {
1785 <Self as ::blazingly::ApiConstrained>::validate_constraints(self)
1786 }
1787 }
1788
1789 impl ::blazingly::ApiConstrained for #model_name {
1790 fn constraint_rules() -> ::std::vec::Vec<::blazingly::ValidationRule> {
1791 ::std::vec![#(#declared_rules),*]
1792 }
1793
1794 fn validate_constraints(
1795 &self,
1796 ) -> ::core::result::Result<(), ::blazingly::ValidationErrors> {
1797 let mut errors = ::blazingly::ValidationErrors::new();
1798 {
1799 let value = &self.0;
1800 #checks
1801 #custom
1802 }
1803
1804 if errors.is_empty() {
1805 ::core::result::Result::Ok(())
1806 } else {
1807 ::core::result::Result::Err(errors)
1808 }
1809 }
1810 }
1811 };
1812 })
1813}
1814
1815fn value_type_inner(arguments: &ModelArgs, model: &ItemStruct) -> syn::Result<Type> {
1817 let Fields::Unnamed(fields) = &model.fields else {
1818 unreachable!("the value form is selected by an unnamed field list");
1819 };
1820 if fields.unnamed.len() != 1 {
1821 return Err(syn::Error::new_spanned(
1822 &model.fields,
1823 "a value type wraps exactly one field; declare a struct with named \
1824 fields for anything else",
1825 ));
1826 }
1827 if let Some(borrowed) = &arguments.borrowed {
1828 return Err(syn::Error::new_spanned(
1829 borrowed,
1830 "a value type is validated wherever it appears, which a borrowed \
1831 output view never is",
1832 ));
1833 }
1834 if let Some(rename) = &arguments.rename_all {
1835 return Err(syn::Error::new_spanned(
1836 rename,
1837 "`rename_all` renames fields, and a value type has none",
1838 ));
1839 }
1840 if let Some(validator) = &arguments.validator {
1841 return Err(syn::Error::new_spanned(
1842 validator,
1843 "declare `#[validate_with(...)]` beside the other rules; a value type \
1844 has no cross-field check to run",
1845 ));
1846 }
1847 reject_owned_generics(&model.generics)?;
1848
1849 let inner = fields.unnamed[0].ty.clone();
1850 if wrapper_inner(&inner, "Option").is_some() {
1851 return Err(syn::Error::new_spanned(
1852 &inner,
1853 "a value type wraps the value its rules describe; declare the field \
1854 that uses it as `Option<_>` instead",
1855 ));
1856 }
1857 Ok(inner)
1858}
1859
1860fn reject_field_only_rules(rules: &FieldRules, model_name: &Ident) -> syn::Result<()> {
1862 if let Some(alias) = rules.aliases.first() {
1863 return Err(syn::Error::new_spanned(
1864 alias,
1865 "`alias` names an extra wire key for one field, which a value type \
1866 does not have",
1867 ));
1868 }
1869 if let Some((_, span)) = &rules.default {
1870 return Err(syn::Error::new(
1871 *span,
1872 "a `default` belongs to the field that may be absent, not to the type \
1873 the field is declared with",
1874 ));
1875 }
1876 if rules.nested {
1877 return Err(syn::Error::new_spanned(
1878 model_name,
1879 "`nested` recurses into a model; a value type validates itself",
1880 ));
1881 }
1882 Ok(())
1883}
1884
1885fn enum_model_tokens(
1887 arguments: &ModelArgs,
1888 model: &mut ItemEnum,
1889) -> syn::Result<proc_macro2::TokenStream> {
1890 if let Some(borrowed) = &arguments.borrowed {
1891 return Err(syn::Error::new_spanned(
1892 borrowed,
1893 "an enumeration owns its variants and never borrows",
1894 ));
1895 }
1896 if let Some(validator) = &arguments.validator {
1897 return Err(syn::Error::new_spanned(
1898 validator,
1899 "an enumeration accepts its declared variants and nothing else, so \
1900 there is nothing left to validate",
1901 ));
1902 }
1903 if let Some(parameter) = model.generics.params.first() {
1904 return Err(syn::Error::new_spanned(
1905 parameter,
1906 "an API enumeration is a closed set of strings and cannot be generic",
1907 ));
1908 }
1909 if model.variants.is_empty() {
1910 return Err(syn::Error::new_spanned(
1911 &model.ident,
1912 "an API enumeration needs at least one variant",
1913 ));
1914 }
1915
1916 let wire_names = enum_wire_names(arguments, model)?;
1917 let identifiers = model
1918 .variants
1919 .iter()
1920 .map(|variant| variant.ident.clone())
1921 .collect::<Vec<_>>();
1922 let model_name = &model.ident;
1923 let encoded = format!(
1924 "enum={}",
1925 wire_names
1926 .iter()
1927 .map(LitStr::value)
1928 .collect::<Vec<_>>()
1929 .join("|")
1930 );
1931
1932 Ok(quote! {
1933 #[derive(
1934 ::blazingly::__private::serde::Serialize,
1935 ::blazingly::__private::serde::Deserialize
1936 )]
1937 #[serde(crate = "::blazingly::__private::serde")]
1938 #model
1939
1940 impl #model_name {
1941 pub const VARIANTS: &'static [&'static str] = &[#(#wire_names),*];
1943
1944 #[must_use]
1946 pub const fn as_str(&self) -> &'static str {
1947 match self {
1948 #(Self::#identifiers => #wire_names,)*
1949 }
1950 }
1951 }
1952
1953 const _: () = {
1954 impl ::blazingly::ApiSchema for #model_name {
1955 fn type_descriptor() -> ::blazingly::TypeDescriptor {
1956 ::blazingly::TypeDescriptor::scalar(
1957 stringify!(#model_name),
1958 ::blazingly::SchemaKind::String,
1959 )
1960 .with_constraints(
1961 <Self as ::blazingly::ApiConstrained>::constraint_rules(),
1962 )
1963 }
1964 }
1965
1966 impl ::blazingly::ApiConstrained for #model_name {
1967 fn constraint_rules() -> ::std::vec::Vec<::blazingly::ValidationRule> {
1968 ::std::vec![::blazingly::ValidationRule::Custom(#encoded.to_owned())]
1969 }
1970
1971 fn validate_constraints(
1972 &self,
1973 ) -> ::core::result::Result<(), ::blazingly::ValidationErrors> {
1974 ::core::result::Result::Ok(())
1975 }
1976 }
1977 };
1978 })
1979}
1980
1981fn enum_wire_names(arguments: &ModelArgs, model: &mut ItemEnum) -> syn::Result<Vec<LitStr>> {
1983 let rename_rule = enum_rename_rule(arguments)?;
1984 let mut wire_names: Vec<LitStr> = Vec::new();
1985
1986 for variant in &mut model.variants {
1987 if !matches!(variant.fields, Fields::Unit) {
1988 return Err(syn::Error::new_spanned(
1989 &variant.fields,
1990 "an API enumeration projects to a string, so a variant cannot \
1991 carry data",
1992 ));
1993 }
1994 let wire = variant_wire_name(variant, rename_rule)?;
1995 if wire.value().contains('|') {
1996 return Err(syn::Error::new(
1997 wire.span(),
1998 "`|` separates the variants in the recorded schema and cannot \
1999 appear in one",
2000 ));
2001 }
2002 if wire_names
2003 .iter()
2004 .any(|declared| declared.value() == wire.value())
2005 {
2006 return Err(syn::Error::new(
2007 wire.span(),
2008 format!("the wire value {:?} is declared twice", wire.value()),
2009 ));
2010 }
2011 variant
2012 .attrs
2013 .push(syn::parse_quote!(#[serde(rename = #wire)]));
2014 wire_names.push(wire);
2015 }
2016
2017 Ok(wire_names)
2018}
2019
2020fn variant_wire_name(variant: &mut syn::Variant, rule: RenameRule) -> syn::Result<LitStr> {
2022 let mut explicit = None;
2023 let mut retained = Vec::new();
2024 for attribute in variant.attrs.drain(..) {
2025 if attribute.path().is_ident("rename") {
2026 if explicit.is_some() {
2027 return Err(syn::Error::new_spanned(
2028 attribute,
2029 "only one `rename` may be declared per variant",
2030 ));
2031 }
2032 explicit = Some(attribute.parse_args::<LitStr>()?);
2033 } else {
2034 retained.push(attribute);
2035 }
2036 }
2037 variant.attrs = retained;
2038
2039 Ok(explicit.unwrap_or_else(|| {
2040 LitStr::new(
2041 &rule.apply(&variant.ident.to_string()),
2042 variant.ident.span(),
2043 )
2044 }))
2045}
2046
2047#[derive(Clone, Copy)]
2052enum RenameRule {
2053 Pascal,
2054 Lower,
2055 Upper,
2056 Camel,
2057 Snake,
2058 ScreamingSnake,
2059 Kebab,
2060 ScreamingKebab,
2061}
2062
2063impl RenameRule {
2064 const SUPPORTED: &'static str = "`PascalCase`, `lowercase`, `UPPERCASE`, `camelCase`, \
2065 `snake_case`, `SCREAMING_SNAKE_CASE`, `kebab-case`, \
2066 or `SCREAMING-KEBAB-CASE`";
2067
2068 fn parse(value: &str) -> Option<Self> {
2069 Some(match value {
2070 "PascalCase" => Self::Pascal,
2071 "lowercase" => Self::Lower,
2072 "UPPERCASE" => Self::Upper,
2073 "camelCase" => Self::Camel,
2074 "snake_case" => Self::Snake,
2075 "SCREAMING_SNAKE_CASE" => Self::ScreamingSnake,
2076 "kebab-case" => Self::Kebab,
2077 "SCREAMING-KEBAB-CASE" => Self::ScreamingKebab,
2078 _ => return None,
2079 })
2080 }
2081
2082 fn apply(self, variant: &str) -> String {
2083 match self {
2084 Self::Pascal => variant.to_owned(),
2085 Self::Lower => variant.to_ascii_lowercase(),
2086 Self::Upper => variant.to_ascii_uppercase(),
2087 Self::Camel => {
2088 let mut characters = variant.chars();
2089 characters.next().map_or_else(String::new, |first| {
2090 first.to_ascii_lowercase().to_string() + characters.as_str()
2091 })
2092 }
2093 Self::Snake => snake_case(variant),
2094 Self::ScreamingSnake => snake_case(variant).to_ascii_uppercase(),
2095 Self::Kebab => snake_case(variant).replace('_', "-"),
2096 Self::ScreamingKebab => snake_case(variant).to_ascii_uppercase().replace('_', "-"),
2097 }
2098 }
2099}
2100
2101fn snake_case(variant: &str) -> String {
2102 let mut output = String::with_capacity(variant.len());
2103 for (index, character) in variant.char_indices() {
2104 if index > 0 && character.is_uppercase() {
2105 output.push('_');
2106 }
2107 output.push(character.to_ascii_lowercase());
2108 }
2109 output
2110}
2111
2112fn enum_rename_rule(arguments: &ModelArgs) -> syn::Result<RenameRule> {
2113 let Some(rename) = &arguments.rename_all else {
2114 return Ok(RenameRule::Pascal);
2115 };
2116 RenameRule::parse(&rename.value()).ok_or_else(|| {
2117 syn::Error::new(
2118 rename.span(),
2119 format!(
2120 "an enumeration renames its variants with {}",
2121 RenameRule::SUPPORTED
2122 ),
2123 )
2124 })
2125}
2126
2127fn owned_model_tokens(
2130 arguments: &ModelArgs,
2131 model: &mut ItemStruct,
2132) -> syn::Result<proc_macro2::TokenStream> {
2133 let Fields::Named(fields) = &mut model.fields else {
2134 return Err(syn::Error::new_spanned(
2135 &model.fields,
2136 "`#[api_model]` requires a struct with named fields",
2137 ));
2138 };
2139 reject_owned_generics(&model.generics)?;
2140
2141 let rename_rule = model_rename_rule(arguments)?;
2142 let model_name = model.ident.clone();
2143 let OwnedFields {
2144 descriptors,
2145 validations,
2146 defaults,
2147 needs_probes,
2148 } = owned_field_tokens(fields, &rename_rule, &model_name)?;
2149
2150 let serde_rename = arguments
2151 .rename_all
2152 .as_ref()
2153 .map_or_else(|| quote!(), |rename| quote!(#[serde(rename_all = #rename)]));
2154 let model_validation = arguments.validator.as_ref().map(|validator| {
2155 quote! {
2156 if let ::core::result::Result::Err(failure) = #validator(self) {
2157 ::blazingly::validation::merge_model_violations(&mut errors, failure);
2158 }
2159 }
2160 });
2161 let probes = if needs_probes {
2162 nested_probe_definitions()
2163 } else {
2164 quote!()
2165 };
2166
2167 Ok(quote! {
2168 #[derive(
2169 ::blazingly::__private::serde::Serialize,
2170 ::blazingly::__private::serde::Deserialize
2171 )]
2172 #[serde(crate = "::blazingly::__private::serde")]
2173 #serde_rename
2174 #model
2175
2176 #(#defaults)*
2177
2178 const _: () = {
2179 #probes
2180
2181 impl ::blazingly::ApiModel for #model_name {
2182 fn model_descriptor() -> ::blazingly::ModelDescriptor {
2183 ::blazingly::ModelDescriptor::new(
2184 stringify!(#model_name),
2185 ::std::vec![#(#descriptors),*],
2186 )
2187 }
2188
2189 fn validate(
2190 &self,
2191 ) -> ::core::result::Result<(), ::blazingly::ValidationErrors> {
2192 let mut errors = ::blazingly::ValidationErrors::new();
2193 #(#validations)*
2194 #model_validation
2195
2196 if errors.is_empty() {
2197 ::core::result::Result::Ok(())
2198 } else {
2199 ::core::result::Result::Err(errors)
2200 }
2201 }
2202 }
2203 };
2204 })
2205}
2206
2207#[derive(Default)]
2208struct OwnedFields {
2209 descriptors: Vec<proc_macro2::TokenStream>,
2210 validations: Vec<proc_macro2::TokenStream>,
2211 defaults: Vec<proc_macro2::TokenStream>,
2214 needs_probes: bool,
2215}
2216
2217fn owned_field_tokens(
2218 fields: &mut syn::FieldsNamed,
2219 rename_rule: &str,
2220 model_name: &Ident,
2221) -> syn::Result<OwnedFields> {
2222 let mut owned = OwnedFields::default();
2223
2224 for field in &mut fields.named {
2225 let identifier = field
2226 .ident
2227 .clone()
2228 .expect("named fields always have identifiers");
2229 let mut rules = take_field_rules(&mut field.attrs)?;
2230 for alias in &rules.aliases {
2231 field
2232 .attrs
2233 .push(syn::parse_quote!(#[serde(alias = #alias)]));
2234 }
2235 let field_type = field.ty.clone();
2236 let optional = wrapper_inner(&field_type, "Option");
2237 let validation_type = optional.as_ref().unwrap_or(&field_type);
2238 let shape = field_shape(validation_type);
2239 reject_incompatible_rules(&rules, validation_type, shape)?;
2240 normalize_collection_rules(&mut rules, shape);
2241 owned.needs_probes |= shape.may_be_model();
2242
2243 if let Some((literal, span)) = &rules.default {
2244 if optional.is_some() {
2245 return Err(syn::Error::new(
2246 *span,
2247 "a field with a `default` is never absent from the handler; \
2248 declare it without `Option`",
2249 ));
2250 }
2251 let function = default_function_name(model_name, &identifier);
2252 let expression = literal.expression();
2253 let path = LitStr::new(&function.to_string(), *span);
2254 field
2255 .attrs
2256 .push(syn::parse_quote!(#[serde(default = #path)]));
2257 owned.defaults.push(quote! {
2258 #[doc(hidden)]
2259 fn #function() -> #field_type {
2260 #expression
2261 }
2262 });
2263 }
2264
2265 let public_name = public_field_name(&identifier, rename_rule);
2266 let required = optional.is_none() && rules.default.is_none();
2267 let declared_rules = rule_descriptors(&rules, optional.is_some());
2268 let inherited_rules = inherited_rule_descriptor(validation_type, shape);
2269 let nested_descriptor = nested_rule_descriptor(validation_type, shape, rules.nested);
2270 owned.descriptors.push(quote! {
2271 ::blazingly::FieldDescriptor::new(
2272 #public_name,
2273 #required,
2274 <#field_type as ::blazingly::ApiSchema>::type_descriptor(),
2275 {
2276 let mut rules = ::std::vec::Vec::new();
2277 #inherited_rules
2278 #(rules.push(#declared_rules);)*
2279 #nested_descriptor
2280 rules
2281 },
2282 )
2283 });
2284
2285 let checks = field_checks(&rules, shape, &public_name);
2286 let nested_checks = nested_validation_checks(shape, &public_name, &rules);
2287 if checks.is_empty() && nested_checks.is_empty() {
2288 continue;
2289 }
2290 if optional.is_some() {
2291 owned.validations.push(quote! {
2292 if let ::core::option::Option::Some(value) = &self.#identifier {
2293 #checks
2294 #nested_checks
2295 }
2296 });
2297 } else {
2298 owned.validations.push(quote! {
2299 {
2300 let value = &self.#identifier;
2301 #checks
2302 #nested_checks
2303 }
2304 });
2305 }
2306 }
2307
2308 Ok(owned)
2309}
2310
2311fn default_function_name(model_name: &Ident, field: &Ident) -> Ident {
2316 let model = model_name.to_string().to_lowercase();
2317 let field = field.to_string();
2318 let field = field.trim_start_matches("r#");
2319 format_ident!("__blazingly_default_{model}_{field}")
2320}
2321
2322fn public_field_name(identifier: &Ident, rename_rule: &str) -> LitStr {
2323 let name = if rename_rule == "camelCase" {
2324 snake_to_camel(&identifier.to_string())
2325 } else {
2326 identifier.to_string()
2327 };
2328 LitStr::new(&name, identifier.span())
2329}
2330
2331fn borrowed_model_tokens(
2337 arguments: &ModelArgs,
2338 model: &mut ItemStruct,
2339) -> syn::Result<proc_macro2::TokenStream> {
2340 let Fields::Named(fields) = &mut model.fields else {
2341 return Err(syn::Error::new_spanned(
2342 &model.fields,
2343 "`#[api_model]` requires a struct with named fields",
2344 ));
2345 };
2346 if let Some(validator) = &arguments.validator {
2347 return Err(syn::Error::new_spanned(
2348 validator,
2349 "a borrowed view is an output type and is never validated; declare \
2350 `validate_with` on the owning model the client sends",
2351 ));
2352 }
2353
2354 let rename_rule = model_rename_rule(arguments)?;
2355 let mut descriptors = Vec::new();
2356
2357 for field in &mut fields.named {
2358 let identifier = field
2359 .ident
2360 .clone()
2361 .expect("named fields always have identifiers");
2362 reject_borrowed_field_rules(&mut field.attrs)?;
2363
2364 let public_name = public_field_name(&identifier, &rename_rule);
2365 let required = wrapper_inner(&field.ty, "Option").is_none();
2366 let schema_type = schema_type(&field.ty);
2371 let metadata = if required {
2374 quote!(::std::vec::Vec::new())
2375 } else {
2376 quote!(::std::vec![::blazingly::ValidationRule::Custom(
2377 "nullable=true".to_owned()
2378 )])
2379 };
2380 descriptors.push(quote! {
2381 ::blazingly::FieldDescriptor::new(
2382 #public_name,
2383 #required,
2384 <#schema_type as ::blazingly::ApiSchema>::type_descriptor(),
2385 #metadata,
2386 )
2387 });
2388 }
2389
2390 let model_name = &model.ident;
2391 let serde_rename = arguments
2392 .rename_all
2393 .as_ref()
2394 .map_or_else(|| quote!(), |rename| quote!(#[serde(rename_all = #rename)]));
2395 let (impl_generics, type_generics, where_clause) = model.generics.split_for_impl();
2396 let schema_bounds = schema_parameter_bounds(&model.generics);
2397 let where_clause = merge_where_predicates(where_clause, &schema_bounds);
2398 let descriptor_name = borrowed_descriptor_name(&model.generics, model_name);
2399
2400 Ok(quote! {
2401 #[derive(::blazingly::__private::serde::Serialize)]
2402 #[serde(crate = "::blazingly::__private::serde")]
2403 #serde_rename
2404 #model
2405
2406 const _: () = {
2407 #[allow(dead_code)]
2408 fn __blazingly_schema_name(
2409 base: &str,
2410 parameters: &[::blazingly::TypeDescriptor],
2411 ) -> ::std::string::String {
2412 let mut name = ::std::string::String::from(base);
2413 for parameter in parameters {
2414 name.push('_');
2415 for character in parameter.rust_name.chars() {
2418 if character.is_ascii_alphanumeric()
2419 || character == '_'
2420 || character == '.'
2421 || character == '-'
2422 {
2423 name.push(character);
2424 } else {
2425 name.push('_');
2426 }
2427 }
2428 }
2429 name
2430 }
2431
2432 impl #impl_generics ::blazingly::ApiSchema for #model_name #type_generics
2433 #where_clause
2434 {
2435 fn type_descriptor() -> ::blazingly::TypeDescriptor {
2436 ::blazingly::TypeDescriptor::model(
2437 ::blazingly::ModelDescriptor::new(
2438 #descriptor_name,
2439 ::std::vec![#(#descriptors),*],
2440 )
2441 )
2442 }
2443 }
2444 };
2445 })
2446}
2447
2448fn reject_owned_generics(generics: &syn::Generics) -> syn::Result<()> {
2451 let Some(parameter) = generics.params.first() else {
2452 return Ok(());
2453 };
2454 let reason = match parameter {
2455 syn::GenericParam::Lifetime(_) => {
2456 "an owning model deserializes into itself and cannot borrow from the \
2457 request buffer"
2458 }
2459 syn::GenericParam::Type(_) | syn::GenericParam::Const(_) => {
2460 "field rules cannot recurse into an unbounded parameter, so an owning \
2461 model would silently skip validating it"
2462 }
2463 };
2464 Err(syn::Error::new_spanned(
2465 parameter,
2466 format!("{reason}; declare `#[api_model(borrowed)]` for a generic output view"),
2467 ))
2468}
2469
2470fn reject_borrowed_field_rules(attributes: &mut Vec<Attribute>) -> syn::Result<()> {
2473 let mut retained = Vec::new();
2474 for attribute in attributes.drain(..) {
2475 if BORROWED_REJECTED_ATTRIBUTES
2476 .iter()
2477 .any(|name| attribute.path().is_ident(name))
2478 {
2479 let name = attribute
2480 .path()
2481 .get_ident()
2482 .map_or_else(String::new, ToString::to_string);
2483 let purpose = if name == "default" {
2484 "fills in an absent request field"
2485 } else {
2486 "validates a request"
2487 };
2488 return Err(syn::Error::new(
2489 attribute_span(&attribute),
2490 format!(
2491 "`#[{name}]` {purpose}; a borrowed view is an output type and is \
2492 never validated. Declare the rule on the owning model the client \
2493 sends"
2494 ),
2495 ));
2496 }
2497 retained.push(attribute);
2498 }
2499 *attributes = retained;
2500 Ok(())
2501}
2502
2503const BORROWED_REJECTED_ATTRIBUTES: &[&str] = &[
2504 "min_length",
2505 "max_length",
2506 "email",
2507 "alias",
2508 "validate_with",
2509 "nested",
2510 "default",
2511 "minimum",
2512 "maximum",
2513 "exclusive_minimum",
2514 "exclusive_maximum",
2515 "multiple_of",
2516 "pattern",
2517 "min_items",
2518 "max_items",
2519 "unique_items",
2520];
2521
2522fn schema_parameter_bounds(generics: &syn::Generics) -> Vec<syn::WherePredicate> {
2525 generics
2526 .params
2527 .iter()
2528 .filter_map(|parameter| match parameter {
2529 syn::GenericParam::Type(parameter) => {
2530 let identifier = ¶meter.ident;
2531 Some(syn::parse_quote!(#identifier: ::blazingly::ApiSchema))
2532 }
2533 syn::GenericParam::Lifetime(_) | syn::GenericParam::Const(_) => None,
2534 })
2535 .collect()
2536}
2537
2538fn merge_where_predicates(
2539 existing: Option<&syn::WhereClause>,
2540 added: &[syn::WherePredicate],
2541) -> Option<syn::WhereClause> {
2542 if added.is_empty() {
2543 return existing.cloned();
2544 }
2545 let mut clause = existing.cloned().unwrap_or_else(|| syn::WhereClause {
2546 where_token: <Token![where]>::default(),
2547 predicates: syn::punctuated::Punctuated::new(),
2548 });
2549 clause.predicates.extend(added.iter().cloned());
2550 Some(clause)
2551}
2552
2553fn borrowed_descriptor_name(
2557 generics: &syn::Generics,
2558 model_name: &Ident,
2559) -> proc_macro2::TokenStream {
2560 let parameters = generics
2561 .params
2562 .iter()
2563 .filter_map(|parameter| match parameter {
2564 syn::GenericParam::Type(parameter) => {
2565 let identifier = ¶meter.ident;
2566 Some(quote!(<#identifier as ::blazingly::ApiSchema>::type_descriptor()))
2567 }
2568 syn::GenericParam::Lifetime(_) | syn::GenericParam::Const(_) => None,
2569 })
2570 .collect::<Vec<_>>();
2571 if parameters.is_empty() {
2572 return quote!(stringify!(#model_name));
2573 }
2574 quote! {
2575 __blazingly_schema_name(
2576 stringify!(#model_name),
2577 &[#(#parameters),*],
2578 )
2579 }
2580}
2581
2582fn schema_type(ty: &Type) -> Type {
2589 match ty {
2590 Type::Reference(reference) => borrowed_schema_type(&reference.elem),
2591 Type::Slice(slice) => vector_of(&schema_type(&slice.elem)),
2592 Type::Array(array) => vector_of(&schema_type(&array.elem)),
2593 Type::Paren(inner) => schema_type(&inner.elem),
2594 Type::Group(inner) => schema_type(&inner.elem),
2595 Type::Path(path) => path_schema_type(path),
2596 other => other.clone(),
2597 }
2598}
2599
2600fn borrowed_schema_type(referent: &Type) -> Type {
2601 if bare_type_matches(referent, &["str"]) {
2603 return syn::parse_quote!(&str);
2604 }
2605 match referent {
2606 Type::Slice(slice) => vector_of(&schema_type(&slice.elem)),
2607 Type::Array(array) => vector_of(&schema_type(&array.elem)),
2608 other => schema_type(other),
2609 }
2610}
2611
2612fn path_schema_type(path: &TypePath) -> Type {
2613 let mut path = path.clone();
2614 if let Some(segment) = path.path.segments.last()
2615 && segment.ident == "Cow"
2616 && let syn::PathArguments::AngleBracketed(arguments) = &segment.arguments
2617 {
2618 let owned = arguments.args.iter().find_map(|argument| match argument {
2619 syn::GenericArgument::Type(ty) => Some(ty),
2620 _ => None,
2621 });
2622 if let Some(owned) = owned {
2623 return borrowed_schema_type(owned);
2624 }
2625 }
2626 for segment in &mut path.path.segments {
2627 let syn::PathArguments::AngleBracketed(arguments) = &mut segment.arguments else {
2628 continue;
2629 };
2630 let resolved = arguments
2631 .args
2632 .iter()
2633 .filter_map(|argument| match argument {
2634 syn::GenericArgument::Lifetime(_) => None,
2635 syn::GenericArgument::Type(ty) => Some(syn::GenericArgument::Type(schema_type(ty))),
2636 other => Some(other.clone()),
2637 })
2638 .collect::<syn::punctuated::Punctuated<_, Token![,]>>();
2639 if resolved.is_empty() {
2640 segment.arguments = syn::PathArguments::None;
2641 } else {
2642 arguments.args = resolved;
2643 }
2644 }
2645 Type::Path(path)
2646}
2647
2648fn vector_of(item: &Type) -> Type {
2649 syn::parse_quote!(::std::vec::Vec<#item>)
2650}
2651
2652fn documented_type(ty: &Type) -> Type {
2658 match ty {
2659 Type::Reference(reference) => {
2660 let mut reference = reference.clone();
2661 reference.lifetime = Some(syn::Lifetime::new(
2662 "'static",
2663 proc_macro2::Span::call_site(),
2664 ));
2665 reference.elem = Box::new(documented_type(&reference.elem));
2666 Type::Reference(reference)
2667 }
2668 Type::Path(path) => {
2669 let mut path = path.clone();
2670 for segment in &mut path.path.segments {
2671 let syn::PathArguments::AngleBracketed(arguments) = &mut segment.arguments else {
2672 continue;
2673 };
2674 for argument in &mut arguments.args {
2675 match argument {
2676 syn::GenericArgument::Lifetime(lifetime) => {
2677 *lifetime =
2678 syn::Lifetime::new("'static", proc_macro2::Span::call_site());
2679 }
2680 syn::GenericArgument::Type(inner) => *inner = documented_type(inner),
2681 _ => {}
2682 }
2683 }
2684 }
2685 Type::Path(path)
2686 }
2687 Type::Paren(inner) => documented_type(&inner.elem),
2688 Type::Group(inner) => documented_type(&inner.elem),
2689 Type::Slice(slice) => {
2690 let mut slice = slice.clone();
2691 slice.elem = Box::new(documented_type(&slice.elem));
2692 Type::Slice(slice)
2693 }
2694 other => other.clone(),
2695 }
2696}
2697
2698fn nested_probe_definitions() -> proc_macro2::TokenStream {
2704 let value = value_probe_definitions();
2705 let items = items_probe_definitions();
2706 let kind = kind_probe_definitions();
2707 let constrained = constrained_probe_definitions();
2708 quote! {
2709 #[allow(dead_code)]
2710 struct __BlazinglyValue<'probe, T>(&'probe T);
2711 #[allow(dead_code)]
2712 struct __BlazinglyItems<'probe, T>(&'probe [T]);
2713 #[allow(dead_code)]
2714 struct __BlazinglyKind<T>(::core::marker::PhantomData<T>);
2715
2716 #value
2717 #items
2718 #kind
2719 #constrained
2720 }
2721}
2722
2723fn constrained_probe_definitions() -> proc_macro2::TokenStream {
2729 let rules = declared_rules_probe_definitions();
2730 let checks = constrained_check_probe_definitions();
2731 quote! {
2732 #rules
2733 #checks
2734 }
2735}
2736
2737fn declared_rules_probe_definitions() -> proc_macro2::TokenStream {
2738 quote! {
2739 #[allow(dead_code)]
2740 trait __BlazinglyDeclaredRules {
2741 fn __blazingly_declared_rules(
2742 &self,
2743 ) -> ::std::vec::Vec<::blazingly::ValidationRule>;
2744 }
2745
2746 impl<T: ::blazingly::ApiConstrained> __BlazinglyDeclaredRules for __BlazinglyKind<T> {
2747 fn __blazingly_declared_rules(
2748 &self,
2749 ) -> ::std::vec::Vec<::blazingly::ValidationRule> {
2750 <T as ::blazingly::ApiConstrained>::constraint_rules()
2751 }
2752 }
2753
2754 #[allow(dead_code)]
2755 trait __BlazinglyPlainRules {
2756 fn __blazingly_declared_rules(
2757 &self,
2758 ) -> ::std::vec::Vec<::blazingly::ValidationRule>;
2759 }
2760
2761 impl<T> __BlazinglyPlainRules for &__BlazinglyKind<T> {
2762 fn __blazingly_declared_rules(
2763 &self,
2764 ) -> ::std::vec::Vec<::blazingly::ValidationRule> {
2765 ::std::vec::Vec::new()
2766 }
2767 }
2768 }
2769}
2770
2771fn constrained_check_probe_definitions() -> proc_macro2::TokenStream {
2772 quote! {
2773 #[allow(dead_code)]
2774 trait __BlazinglyConstrainedValue {
2775 fn __blazingly_constrained(
2776 &self,
2777 errors: &mut ::blazingly::ValidationErrors,
2778 field: &str,
2779 );
2780 }
2781
2782 impl<T: ::blazingly::ApiConstrained> __BlazinglyConstrainedValue
2783 for __BlazinglyValue<'_, T>
2784 {
2785 fn __blazingly_constrained(
2786 &self,
2787 errors: &mut ::blazingly::ValidationErrors,
2788 field: &str,
2789 ) {
2790 if let ::core::result::Result::Err(declared) =
2791 ::blazingly::ApiConstrained::validate_constraints(self.0)
2792 {
2793 ::blazingly::merge_field_validation_errors(errors, field, &declared);
2794 }
2795 }
2796 }
2797
2798 #[allow(dead_code)]
2799 trait __BlazinglyPlainConstrained {
2800 fn __blazingly_constrained(
2801 &self,
2802 errors: &mut ::blazingly::ValidationErrors,
2803 field: &str,
2804 );
2805 }
2806
2807 impl<T> __BlazinglyPlainConstrained for &__BlazinglyValue<'_, T> {
2808 fn __blazingly_constrained(
2809 &self,
2810 _errors: &mut ::blazingly::ValidationErrors,
2811 _field: &str,
2812 ) {
2813 }
2814 }
2815
2816 #[allow(dead_code)]
2817 trait __BlazinglyConstrainedItems {
2818 fn __blazingly_constrained_items(
2819 &self,
2820 errors: &mut ::blazingly::ValidationErrors,
2821 field: &str,
2822 );
2823 }
2824
2825 impl<T: ::blazingly::ApiConstrained> __BlazinglyConstrainedItems
2826 for __BlazinglyItems<'_, T>
2827 {
2828 fn __blazingly_constrained_items(
2829 &self,
2830 errors: &mut ::blazingly::ValidationErrors,
2831 field: &str,
2832 ) {
2833 for (index, item) in self.0.iter().enumerate() {
2834 if let ::core::result::Result::Err(declared) =
2835 ::blazingly::ApiConstrained::validate_constraints(item)
2836 {
2837 let prefix = ::std::format!("{}[{}]", field, index);
2838 ::blazingly::merge_field_validation_errors(errors, &prefix, &declared);
2839 }
2840 }
2841 }
2842 }
2843
2844 #[allow(dead_code)]
2845 trait __BlazinglyPlainConstrainedItems {
2846 fn __blazingly_constrained_items(
2847 &self,
2848 errors: &mut ::blazingly::ValidationErrors,
2849 field: &str,
2850 );
2851 }
2852
2853 impl<T> __BlazinglyPlainConstrainedItems for &__BlazinglyItems<'_, T> {
2854 fn __blazingly_constrained_items(
2855 &self,
2856 _errors: &mut ::blazingly::ValidationErrors,
2857 _field: &str,
2858 ) {
2859 }
2860 }
2861 }
2862}
2863
2864fn value_probe_definitions() -> proc_macro2::TokenStream {
2865 quote! {
2866 #[allow(dead_code)]
2867 trait __BlazinglyNestedValue {
2868 fn __blazingly_nested(
2869 &self,
2870 errors: &mut ::blazingly::ValidationErrors,
2871 field: &str,
2872 );
2873 }
2874
2875 impl<T: ::blazingly::ApiModel> __BlazinglyNestedValue for __BlazinglyValue<'_, T> {
2876 fn __blazingly_nested(
2877 &self,
2878 errors: &mut ::blazingly::ValidationErrors,
2879 field: &str,
2880 ) {
2881 if let ::core::result::Result::Err(nested) =
2882 ::blazingly::ApiModel::validate(self.0)
2883 {
2884 ::blazingly::merge_validation_errors(errors, field, &nested);
2885 }
2886 }
2887 }
2888
2889 #[allow(dead_code)]
2890 trait __BlazinglyPlainValue {
2891 fn __blazingly_nested(
2892 &self,
2893 errors: &mut ::blazingly::ValidationErrors,
2894 field: &str,
2895 );
2896 }
2897
2898 impl<T> __BlazinglyPlainValue for &__BlazinglyValue<'_, T> {
2899 fn __blazingly_nested(
2900 &self,
2901 _errors: &mut ::blazingly::ValidationErrors,
2902 _field: &str,
2903 ) {
2904 }
2905 }
2906 }
2907}
2908
2909fn items_probe_definitions() -> proc_macro2::TokenStream {
2910 quote! {
2911 #[allow(dead_code)]
2912 trait __BlazinglyNestedItems {
2913 fn __blazingly_nested_items(
2914 &self,
2915 errors: &mut ::blazingly::ValidationErrors,
2916 field: &str,
2917 );
2918 }
2919
2920 impl<T: ::blazingly::ApiModel> __BlazinglyNestedItems for __BlazinglyItems<'_, T> {
2921 fn __blazingly_nested_items(
2922 &self,
2923 errors: &mut ::blazingly::ValidationErrors,
2924 field: &str,
2925 ) {
2926 for (index, item) in self.0.iter().enumerate() {
2927 if let ::core::result::Result::Err(nested) =
2928 ::blazingly::ApiModel::validate(item)
2929 {
2930 let prefix = ::std::format!("{}[{}]", field, index);
2931 ::blazingly::merge_validation_errors(errors, &prefix, &nested);
2932 }
2933 }
2934 }
2935 }
2936
2937 #[allow(dead_code)]
2938 trait __BlazinglyPlainItems {
2939 fn __blazingly_nested_items(
2940 &self,
2941 errors: &mut ::blazingly::ValidationErrors,
2942 field: &str,
2943 );
2944 }
2945
2946 impl<T> __BlazinglyPlainItems for &__BlazinglyItems<'_, T> {
2947 fn __blazingly_nested_items(
2948 &self,
2949 _errors: &mut ::blazingly::ValidationErrors,
2950 _field: &str,
2951 ) {
2952 }
2953 }
2954 }
2955}
2956
2957fn kind_probe_definitions() -> proc_macro2::TokenStream {
2958 quote! {
2959 #[allow(dead_code)]
2960 trait __BlazinglyModelKind {
2961 fn __blazingly_is_model(&self) -> bool;
2962 }
2963
2964 impl<T: ::blazingly::ApiModel> __BlazinglyModelKind for __BlazinglyKind<T> {
2965 fn __blazingly_is_model(&self) -> bool {
2966 true
2967 }
2968 }
2969
2970 #[allow(dead_code)]
2971 trait __BlazinglyPlainKind {
2972 fn __blazingly_is_model(&self) -> bool;
2973 }
2974
2975 impl<T> __BlazinglyPlainKind for &__BlazinglyKind<T> {
2976 fn __blazingly_is_model(&self) -> bool {
2977 false
2978 }
2979 }
2980 }
2981}
2982
2983fn nested_rule_descriptor(
2984 validation_type: &Type,
2985 shape: FieldShape,
2986 explicit: bool,
2987) -> proc_macro2::TokenStream {
2988 if explicit {
2989 return quote!(rules.push(::blazingly::ValidationRule::Nested););
2990 }
2991 if !shape.may_be_model() {
2992 return quote!();
2993 }
2994 let probe_type = model_probe_type(validation_type, shape);
2995 quote! {
2996 if (&__BlazinglyKind::<#probe_type>(::core::marker::PhantomData))
2997 .__blazingly_is_model()
2998 {
2999 rules.push(::blazingly::ValidationRule::Nested);
3000 }
3001 }
3002}
3003
3004fn inherited_rule_descriptor(
3010 validation_type: &Type,
3011 shape: FieldShape,
3012) -> proc_macro2::TokenStream {
3013 if shape != FieldShape::Other {
3014 return quote!();
3015 }
3016 quote! {
3017 rules.extend(
3018 (&__BlazinglyKind::<#validation_type>(::core::marker::PhantomData))
3019 .__blazingly_declared_rules(),
3020 );
3021 }
3022}
3023
3024fn model_probe_type(validation_type: &Type, shape: FieldShape) -> Type {
3025 if shape == FieldShape::Collection {
3026 wrapper_inner(validation_type, "Vec").unwrap_or_else(|| validation_type.clone())
3027 } else {
3028 validation_type.clone()
3029 }
3030}
3031
3032fn model_rename_rule(arguments: &ModelArgs) -> syn::Result<String> {
3033 let rename_rule = arguments
3034 .rename_all
3035 .as_ref()
3036 .map_or_else(|| "none".to_owned(), LitStr::value);
3037 if matches!(rename_rule.as_str(), "none" | "camelCase") {
3038 return Ok(rename_rule);
3039 }
3040
3041 Err(syn::Error::new(
3042 arguments
3043 .rename_all
3044 .as_ref()
3045 .map_or_else(proc_macro2::Span::call_site, LitStr::span),
3046 "the first milestone supports only `rename_all = \"camelCase\"`",
3047 ))
3048}
3049
3050#[allow(clippy::too_many_lines)]
3051fn take_field_rules(attributes: &mut Vec<Attribute>) -> syn::Result<FieldRules> {
3052 let mut retained = Vec::new();
3053 let mut rules = FieldRules::default();
3054
3055 for attribute in attributes.drain(..) {
3056 let path = attribute.path().clone();
3057 if path.is_ident("min_length") {
3058 let value = attribute.parse_args::<LitInt>()?;
3059 rules.min_length = Some((value.base10_parse()?, value.span()));
3060 } else if path.is_ident("max_length") {
3061 let value = attribute.parse_args::<LitInt>()?;
3062 rules.max_length = Some((value.base10_parse()?, value.span()));
3063 } else if path.is_ident("min_items") {
3064 let value = attribute.parse_args::<LitInt>()?;
3065 rules.min_items = Some((value.base10_parse()?, value.span()));
3066 } else if path.is_ident("max_items") {
3067 let value = attribute.parse_args::<LitInt>()?;
3068 rules.max_items = Some((value.base10_parse()?, value.span()));
3069 } else if path.is_ident("unique_items") {
3070 rules.unique_items = Some(attribute_span(&attribute));
3071 } else if path.is_ident("minimum") {
3072 rules.minimum = Some(numeric_attribute(&attribute)?);
3073 } else if path.is_ident("maximum") {
3074 rules.maximum = Some(numeric_attribute(&attribute)?);
3075 } else if path.is_ident("exclusive_minimum") {
3076 rules.exclusive_minimum = Some(numeric_attribute(&attribute)?);
3077 } else if path.is_ident("exclusive_maximum") {
3078 rules.exclusive_maximum = Some(numeric_attribute(&attribute)?);
3079 } else if path.is_ident("multiple_of") {
3080 let (factor, span) = numeric_attribute(&attribute)?;
3081 if factor.is_zero() {
3082 return Err(syn::Error::new(span, "`multiple_of` cannot be zero"));
3083 }
3084 rules.multiple_of = Some((factor, span));
3085 } else if path.is_ident("pattern") {
3086 let pattern = attribute.parse_args::<LitStr>()?;
3087 if let Err(reason) = lint_pattern_syntax(&pattern.value()) {
3088 return Err(syn::Error::new(pattern.span(), reason));
3089 }
3090 rules.pattern = Some(pattern);
3091 } else if path.is_ident("default") {
3092 if rules.default.is_some() {
3093 return Err(syn::Error::new_spanned(
3094 attribute,
3095 "only one `default` may be declared per field",
3096 ));
3097 }
3098 rules.default = Some(default_attribute(&attribute)?);
3099 } else if path.is_ident("email") {
3100 rules.email = Some(attribute_span(&attribute));
3101 } else if path.is_ident("alias") {
3102 rules.aliases.push(attribute.parse_args::<LitStr>()?);
3103 } else if path.is_ident("validate_with") {
3104 if rules.validator.is_some() {
3105 return Err(syn::Error::new_spanned(
3106 attribute,
3107 "only one `validate_with` function may be declared per field",
3108 ));
3109 }
3110 rules.validator = Some(attribute.parse_args::<SynPath>()?);
3111 } else if path.is_ident("nested") {
3112 rules.nested = true;
3113 } else {
3114 retained.push(attribute);
3115 }
3116 }
3117
3118 reject_inverted_bounds(&rules)?;
3119
3120 *attributes = retained;
3121 Ok(rules)
3122}
3123
3124fn attribute_span(attribute: &Attribute) -> proc_macro2::Span {
3125 attribute
3126 .path()
3127 .segments
3128 .last()
3129 .map_or_else(proc_macro2::Span::call_site, |segment| segment.ident.span())
3130}
3131
3132fn default_attribute(attribute: &Attribute) -> syn::Result<(DefaultLiteral, proc_macro2::Span)> {
3133 let expression = attribute.parse_args::<syn::Expr>()?;
3134 let unsupported = || {
3135 syn::Error::new_spanned(
3136 &expression,
3137 "`default` requires a string, integer, floating-point, or boolean literal",
3138 )
3139 };
3140 let (negative, literal) = match &expression {
3141 syn::Expr::Lit(literal) => (false, &literal.lit),
3142 syn::Expr::Unary(unary) if matches!(unary.op, syn::UnOp::Neg(_)) => {
3143 let syn::Expr::Lit(literal) = unary.expr.as_ref() else {
3144 return Err(unsupported());
3145 };
3146 (true, &literal.lit)
3147 }
3148 _ => return Err(unsupported()),
3149 };
3150 let span = literal.span();
3151 let value = match literal {
3152 syn::Lit::Str(value) if !negative => DefaultLiteral::Text(value.clone()),
3153 syn::Lit::Bool(value) if !negative => DefaultLiteral::Boolean(value.clone()),
3154 syn::Lit::Int(value) => {
3155 let magnitude = value.base10_parse::<i128>()?;
3156 DefaultLiteral::Number(NumericLiteral::Integer(if negative {
3157 -magnitude
3158 } else {
3159 magnitude
3160 }))
3161 }
3162 syn::Lit::Float(value) => {
3163 let magnitude = value.base10_parse::<f64>()?;
3164 let magnitude = if negative { -magnitude } else { magnitude };
3165 if !magnitude.is_finite() {
3166 return Err(syn::Error::new(span, "a `default` must be finite"));
3167 }
3168 DefaultLiteral::Number(NumericLiteral::Float(magnitude))
3169 }
3170 _ => return Err(unsupported()),
3171 };
3172 Ok((value, span))
3173}
3174
3175fn numeric_attribute(attribute: &Attribute) -> syn::Result<(NumericLiteral, proc_macro2::Span)> {
3176 let expression = attribute.parse_args::<syn::Expr>()?;
3177 let (negative, literal) = match &expression {
3178 syn::Expr::Lit(literal) => (false, &literal.lit),
3179 syn::Expr::Unary(unary) if matches!(unary.op, syn::UnOp::Neg(_)) => {
3180 let syn::Expr::Lit(literal) = unary.expr.as_ref() else {
3181 return Err(syn::Error::new_spanned(
3182 &expression,
3183 "numeric bounds require an integer or floating-point literal",
3184 ));
3185 };
3186 (true, &literal.lit)
3187 }
3188 _ => {
3189 return Err(syn::Error::new_spanned(
3190 &expression,
3191 "numeric bounds require an integer or floating-point literal",
3192 ));
3193 }
3194 };
3195 let span = literal.span();
3196 let value = match literal {
3197 syn::Lit::Int(value) => {
3198 let magnitude = value.base10_parse::<i128>()?;
3199 NumericLiteral::Integer(if negative { -magnitude } else { magnitude })
3200 }
3201 syn::Lit::Float(value) => {
3202 let magnitude = value.base10_parse::<f64>()?;
3203 let magnitude = if negative { -magnitude } else { magnitude };
3204 if !magnitude.is_finite() {
3205 return Err(syn::Error::new(span, "numeric bounds must be finite"));
3206 }
3207 NumericLiteral::Float(magnitude)
3208 }
3209 _ => {
3210 return Err(syn::Error::new(
3211 span,
3212 "numeric bounds require an integer or floating-point literal",
3213 ));
3214 }
3215 };
3216 Ok((value, span))
3217}
3218
3219fn reject_inverted_bounds(rules: &FieldRules) -> syn::Result<()> {
3220 if let (Some((minimum, span)), Some((maximum, _))) = (rules.min_length, rules.max_length)
3221 && minimum > maximum
3222 {
3223 return Err(syn::Error::new(
3224 span,
3225 "`min_length` cannot be greater than `max_length`",
3226 ));
3227 }
3228 if let (Some((minimum, span)), Some((maximum, _))) = (rules.min_items, rules.max_items)
3229 && minimum > maximum
3230 {
3231 return Err(syn::Error::new(
3232 span,
3233 "`min_items` cannot be greater than `max_items`",
3234 ));
3235 }
3236 if let (Some((minimum, span)), Some((maximum, _))) = (rules.minimum, rules.maximum)
3237 && minimum.exceeds(maximum)
3238 {
3239 return Err(syn::Error::new(
3240 span,
3241 "`minimum` cannot be greater than `maximum`",
3242 ));
3243 }
3244 if let (Some((minimum, span)), Some((maximum, _))) =
3245 (rules.exclusive_minimum, rules.exclusive_maximum)
3246 && minimum.exceeds(maximum)
3247 {
3248 return Err(syn::Error::new(
3249 span,
3250 "`exclusive_minimum` cannot be greater than `exclusive_maximum`",
3251 ));
3252 }
3253 Ok(())
3254}
3255
3256fn reject_incompatible_rules(
3257 rules: &FieldRules,
3258 validation_type: &Type,
3259 shape: FieldShape,
3260) -> syn::Result<()> {
3261 let numeric = [
3262 (rules.minimum.map(|(_, span)| span), "minimum"),
3263 (rules.maximum.map(|(_, span)| span), "maximum"),
3264 (
3265 rules.exclusive_minimum.map(|(_, span)| span),
3266 "exclusive_minimum",
3267 ),
3268 (
3269 rules.exclusive_maximum.map(|(_, span)| span),
3270 "exclusive_maximum",
3271 ),
3272 (rules.multiple_of.map(|(_, span)| span), "multiple_of"),
3273 ];
3274 for (span, name) in numeric {
3275 if let Some(span) = span
3276 && !shape.is_numeric()
3277 {
3278 return Err(syn::Error::new(
3279 span,
3280 format!(
3281 "`{name}` requires an integer or floating-point field, \
3282 but `{}` is not numeric",
3283 type_label(validation_type)
3284 ),
3285 ));
3286 }
3287 }
3288
3289 let collection = [
3290 (rules.min_items.map(|(_, span)| span), "min_items"),
3291 (rules.max_items.map(|(_, span)| span), "max_items"),
3292 (rules.unique_items, "unique_items"),
3293 ];
3294 for (span, name) in collection {
3295 if let Some(span) = span
3296 && shape != FieldShape::Collection
3297 {
3298 return Err(syn::Error::new(
3299 span,
3300 format!(
3301 "`{name}` requires a `Vec<T>` or `Option<Vec<T>>` field, \
3302 but `{}` is not a collection",
3303 type_label(validation_type)
3304 ),
3305 ));
3306 }
3307 }
3308
3309 if let Some(pattern) = &rules.pattern
3310 && shape != FieldShape::Text
3311 {
3312 return Err(syn::Error::new(
3313 pattern.span(),
3314 format!(
3315 "`pattern` requires a `String` or `Option<String>` field, \
3316 but `{}` is not a string",
3317 type_label(validation_type)
3318 ),
3319 ));
3320 }
3321
3322 if let Some(span) = rules.email
3323 && shape != FieldShape::Text
3324 {
3325 return Err(syn::Error::new(
3326 span,
3327 format!(
3328 "`email` requires a `String` or `Option<String>` field, \
3329 but `{}` is not a string",
3330 type_label(validation_type)
3331 ),
3332 ));
3333 }
3334
3335 reject_incompatible_default(rules, validation_type, shape)?;
3336
3337 let lengths = [
3338 (rules.min_length.map(|(_, span)| span), "min_length"),
3339 (rules.max_length.map(|(_, span)| span), "max_length"),
3340 ];
3341 for (span, name) in lengths {
3342 if let Some(span) = span
3343 && !matches!(shape, FieldShape::Text | FieldShape::Collection)
3344 {
3345 return Err(syn::Error::new(
3346 span,
3347 format!(
3348 "`{name}` requires a `String`, `Option<String>`, or `Vec<T>` field, \
3349 but `{}` is neither",
3350 type_label(validation_type)
3351 ),
3352 ));
3353 }
3354 }
3355
3356 Ok(())
3357}
3358
3359fn reject_incompatible_default(
3362 rules: &FieldRules,
3363 validation_type: &Type,
3364 shape: FieldShape,
3365) -> syn::Result<()> {
3366 let Some((literal, span)) = &rules.default else {
3367 return Ok(());
3368 };
3369 let accepted = match literal {
3370 DefaultLiteral::Text(_) => shape == FieldShape::Text,
3371 DefaultLiteral::Number(NumericLiteral::Integer(_)) => shape.is_numeric(),
3372 DefaultLiteral::Number(NumericLiteral::Float(_)) => shape == FieldShape::Float,
3373 DefaultLiteral::Boolean(_) => bare_type_matches(validation_type, &["bool"]),
3374 };
3375 if accepted {
3376 return Ok(());
3377 }
3378 let (written, expected) = literal.expectation();
3379 Err(syn::Error::new(
3380 *span,
3381 format!(
3382 "`default` with {written} requires {expected}, but `{}` is not one",
3383 type_label(validation_type)
3384 ),
3385 ))
3386}
3387
3388fn normalize_collection_rules(rules: &mut FieldRules, shape: FieldShape) {
3390 if shape != FieldShape::Collection {
3391 return;
3392 }
3393 if rules.min_items.is_none() {
3394 rules.min_items = rules.min_length;
3395 }
3396 if rules.max_items.is_none() {
3397 rules.max_items = rules.max_length;
3398 }
3399 rules.min_length = None;
3400 rules.max_length = None;
3401}
3402
3403fn rule_descriptors(rules: &FieldRules, nullable: bool) -> Vec<proc_macro2::TokenStream> {
3404 let mut descriptors = Vec::new();
3405
3406 if let Some((minimum, _)) = rules.min_length {
3407 descriptors.push(quote!(::blazingly::ValidationRule::MinLength(#minimum)));
3408 }
3409 if let Some((maximum, _)) = rules.max_length {
3410 descriptors.push(quote!(::blazingly::ValidationRule::MaxLength(#maximum)));
3411 }
3412 if rules.email.is_some() {
3413 descriptors.push(quote!(::blazingly::ValidationRule::Email));
3414 }
3415 for encoded in constraint_encodings(rules) {
3416 descriptors.push(quote!(::blazingly::ValidationRule::Custom(#encoded.to_owned())));
3417 }
3418 for encoded in metadata_encodings(rules, nullable) {
3419 descriptors.push(quote!(::blazingly::ValidationRule::Custom(#encoded.to_owned())));
3420 }
3421 for alias in &rules.aliases {
3422 descriptors.push(quote!(::blazingly::ValidationRule::Alias(#alias.to_owned())));
3423 }
3424 if let Some(validator) = &rules.validator {
3425 descriptors.push(quote!(::blazingly::ValidationRule::Custom(
3426 stringify!(#validator).to_owned()
3427 )));
3428 }
3429
3430 descriptors
3431}
3432
3433fn constraint_encodings(rules: &FieldRules) -> Vec<String> {
3435 let mut encodings = Vec::new();
3436 let numeric = [
3437 (rules.minimum, "minimum"),
3438 (rules.maximum, "maximum"),
3439 (rules.exclusive_minimum, "exclusive_minimum"),
3440 (rules.exclusive_maximum, "exclusive_maximum"),
3441 (rules.multiple_of, "multiple_of"),
3442 ];
3443 for (rule, keyword) in numeric {
3444 if let Some((value, _)) = rule {
3445 encodings.push(format!("{keyword}={}", value.encoded()));
3446 }
3447 }
3448 if let Some(pattern) = &rules.pattern {
3449 encodings.push(format!("pattern={}", pattern.value()));
3450 }
3451 if let Some((minimum, _)) = rules.min_items {
3452 encodings.push(format!("min_items={minimum}"));
3453 }
3454 if let Some((maximum, _)) = rules.max_items {
3455 encodings.push(format!("max_items={maximum}"));
3456 }
3457 if rules.unique_items.is_some() {
3458 encodings.push("unique_items=true".to_owned());
3459 }
3460 encodings
3461}
3462
3463fn metadata_encodings(rules: &FieldRules, nullable: bool) -> Vec<String> {
3468 let mut encodings = Vec::new();
3469 if let Some((literal, _)) = &rules.default {
3470 encodings.push(format!("default={}", literal.encoded()));
3471 }
3472 if nullable {
3473 encodings.push("nullable=true".to_owned());
3474 }
3475 encodings
3476}
3477
3478fn field_checks(
3479 rules: &FieldRules,
3480 shape: FieldShape,
3481 public_name: &LitStr,
3482) -> proc_macro2::TokenStream {
3483 match shape {
3484 FieldShape::Text => text_checks(rules, public_name),
3485 FieldShape::Integer | FieldShape::Float => numeric_checks(rules, public_name),
3486 FieldShape::Collection => collection_checks(rules, public_name),
3487 FieldShape::Other => quote!(),
3488 }
3489}
3490
3491fn text_checks(rules: &FieldRules, public_name: &LitStr) -> proc_macro2::TokenStream {
3492 let minimum = rules.min_length.map(|(minimum, _)| {
3493 quote! {
3494 if value.chars().count() < #minimum {
3495 errors.push(
3496 #public_name,
3497 "min_length",
3498 ::std::format!("must contain at least {} characters", #minimum),
3499 );
3500 }
3501 }
3502 });
3503 let maximum = rules.max_length.map(|(maximum, _)| {
3504 quote! {
3505 if value.chars().count() > #maximum {
3506 errors.push(
3507 #public_name,
3508 "max_length",
3509 ::std::format!("must contain at most {} characters", #maximum),
3510 );
3511 }
3512 }
3513 });
3514 let email = rules.email.map(|_| {
3515 quote! {
3516 if !::blazingly::is_email(value) {
3517 errors.push(
3518 #public_name,
3519 "email",
3520 "must be a valid email address",
3521 );
3522 }
3523 }
3524 });
3525 let pattern = rules.pattern.as_ref().map(|pattern| {
3526 quote! {
3527 ::blazingly::validation::check_pattern(
3528 &mut errors,
3529 #public_name,
3530 value.as_str(),
3531 #pattern,
3532 );
3533 }
3534 });
3535
3536 quote! {
3537 #minimum
3538 #maximum
3539 #email
3540 #pattern
3541 }
3542}
3543
3544fn numeric_checks(rules: &FieldRules, public_name: &LitStr) -> proc_macro2::TokenStream {
3545 let checks = [
3546 (rules.minimum, quote!(check_minimum)),
3547 (rules.maximum, quote!(check_maximum)),
3548 (rules.exclusive_minimum, quote!(check_exclusive_minimum)),
3549 (rules.exclusive_maximum, quote!(check_exclusive_maximum)),
3550 (rules.multiple_of, quote!(check_multiple_of)),
3551 ]
3552 .into_iter()
3553 .filter_map(|(rule, function)| {
3554 let (bound, _) = rule?;
3555 let bound = bound.tokens();
3556 Some(quote! {
3557 ::blazingly::validation::#function(&mut errors, #public_name, *value, #bound);
3558 })
3559 });
3560
3561 quote!(#(#checks)*)
3562}
3563
3564fn collection_checks(rules: &FieldRules, public_name: &LitStr) -> proc_macro2::TokenStream {
3565 let minimum = rules.min_items.map(|(minimum, _)| {
3566 quote! {
3567 ::blazingly::validation::check_min_items(
3568 &mut errors,
3569 #public_name,
3570 value.as_slice(),
3571 #minimum,
3572 );
3573 }
3574 });
3575 let maximum = rules.max_items.map(|(maximum, _)| {
3576 quote! {
3577 ::blazingly::validation::check_max_items(
3578 &mut errors,
3579 #public_name,
3580 value.as_slice(),
3581 #maximum,
3582 );
3583 }
3584 });
3585 let unique = rules.unique_items.map(|_| {
3586 quote! {
3587 ::blazingly::validation::check_unique_items(
3588 &mut errors,
3589 #public_name,
3590 value.as_slice(),
3591 );
3592 }
3593 });
3594
3595 quote! {
3596 #minimum
3597 #maximum
3598 #unique
3599 }
3600}
3601
3602fn nested_validation_checks(
3603 shape: FieldShape,
3604 public_name: &LitStr,
3605 rules: &FieldRules,
3606) -> proc_macro2::TokenStream {
3607 let nested = shape.may_be_model().then(|| {
3608 if shape == FieldShape::Collection {
3609 quote! {
3610 (&__BlazinglyItems(value.as_slice()))
3611 .__blazingly_nested_items(&mut errors, #public_name);
3612 (&__BlazinglyItems(value.as_slice()))
3613 .__blazingly_constrained_items(&mut errors, #public_name);
3614 }
3615 } else {
3616 quote! {
3617 (&__BlazinglyValue(value)).__blazingly_nested(&mut errors, #public_name);
3618 (&__BlazinglyValue(value)).__blazingly_constrained(&mut errors, #public_name);
3619 }
3620 }
3621 });
3622 let custom = rules.validator.as_ref().map(|validator| {
3623 quote! {
3624 if let ::core::result::Result::Err(custom_errors) = #validator(value) {
3625 ::blazingly::merge_field_validation_errors(
3626 &mut errors,
3627 #public_name,
3628 &custom_errors,
3629 );
3630 }
3631 }
3632 });
3633 quote! {
3634 #nested
3635 #custom
3636 }
3637}
3638
3639const INTEGER_TYPES: &[&str] = &[
3640 "i8", "i16", "i32", "i64", "i128", "isize", "u8", "u16", "u32", "u64", "u128", "usize",
3641];
3642
3643const FLOAT_TYPES: &[&str] = &["f32", "f64"];
3644
3645fn field_shape(ty: &Type) -> FieldShape {
3646 if is_string_type(ty) {
3647 return FieldShape::Text;
3648 }
3649 if bare_type_matches(ty, INTEGER_TYPES) {
3650 return FieldShape::Integer;
3651 }
3652 if bare_type_matches(ty, FLOAT_TYPES) {
3653 return FieldShape::Float;
3654 }
3655 if wrapper_inner(ty, "Vec").is_some() {
3656 return FieldShape::Collection;
3657 }
3658 FieldShape::Other
3659}
3660
3661fn bare_type_matches(ty: &Type, names: &[&str]) -> bool {
3662 let Type::Path(path) = ty else {
3663 return false;
3664 };
3665 path.path.segments.last().is_some_and(|segment| {
3666 segment.arguments.is_none() && names.contains(&segment.ident.to_string().as_str())
3667 })
3668}
3669
3670fn type_label(ty: &Type) -> String {
3671 let Type::Path(path) = ty else {
3672 return "this field type".to_owned();
3673 };
3674 path.path.segments.last().map_or_else(
3675 || "this field type".to_owned(),
3676 |segment| segment.ident.to_string(),
3677 )
3678}
3679
3680fn is_string_type(ty: &Type) -> bool {
3681 let Type::Path(path) = ty else {
3682 return false;
3683 };
3684 path.path
3685 .segments
3686 .last()
3687 .is_some_and(|segment| segment.ident == "String")
3688}
3689
3690const MAX_PATTERN_CHARS: usize = 512;
3692
3693const MAX_PATTERN_DEPTH: i32 = 16;
3695
3696const fn is_supported_escape(value: char) -> bool {
3697 matches!(
3698 value,
3699 'd' | 'D'
3700 | 'w'
3701 | 'W'
3702 | 's'
3703 | 'S'
3704 | 't'
3705 | 'n'
3706 | 'r'
3707 | '\\'
3708 | '.'
3709 | '*'
3710 | '+'
3711 | '?'
3712 | '('
3713 | ')'
3714 | '['
3715 | ']'
3716 | '{'
3717 | '}'
3718 | '|'
3719 | '^'
3720 | '$'
3721 | '-'
3722 | '/'
3723 )
3724}
3725
3726#[allow(clippy::too_many_lines)]
3731fn lint_pattern_syntax(pattern: &str) -> Result<(), String> {
3732 if pattern.is_empty() {
3733 return Err("the pattern is empty".to_owned());
3734 }
3735 let characters = pattern.chars().collect::<Vec<_>>();
3736 if characters.len() > MAX_PATTERN_CHARS {
3737 return Err(format!(
3738 "the pattern exceeds {MAX_PATTERN_CHARS} characters"
3739 ));
3740 }
3741 let last = characters.len() - 1;
3742 let mut depth = 0_i32;
3743 let mut deepest = 0_i32;
3744 let mut in_class = false;
3745 let mut quantifiable = false;
3746 let mut index = 0;
3747
3748 while let Some(&value) = characters.get(index) {
3749 match value {
3750 '\\' => {
3751 let Some(&escaped) = characters.get(index + 1) else {
3752 return Err("the pattern ends with a lone backslash".to_owned());
3753 };
3754 if !is_supported_escape(escaped) {
3755 return Err(format!("the escape `\\{escaped}` is not supported"));
3756 }
3757 quantifiable = true;
3758 index += 2;
3759 continue;
3760 }
3761 '[' if !in_class => {
3762 in_class = true;
3763 quantifiable = false;
3764 }
3765 ']' if in_class => {
3766 in_class = false;
3767 quantifiable = true;
3768 }
3769 _ if in_class => {}
3770 '(' => {
3771 if characters.get(index + 1) == Some(&'?') {
3772 return Err(
3773 "group flags, lookaround, and non-capturing groups are not supported"
3774 .to_owned(),
3775 );
3776 }
3777 depth += 1;
3778 deepest = deepest.max(depth);
3779 quantifiable = false;
3780 }
3781 ')' => {
3782 depth -= 1;
3783 if depth < 0 {
3784 return Err("the pattern has an unbalanced group".to_owned());
3785 }
3786 quantifiable = true;
3787 }
3788 '*' | '+' | '?' => {
3789 if !quantifiable {
3790 return Err("a quantifier has no preceding expression".to_owned());
3791 }
3792 quantifiable = false;
3793 }
3794 '{' | '}' => {
3795 return Err("counted repetition `{m,n}` is not supported".to_owned());
3796 }
3797 '^' if index != 0 => {
3798 return Err("`^` is supported only at the start of a pattern".to_owned());
3799 }
3800 '$' if index != last => {
3801 return Err("`$` is supported only at the end of a pattern".to_owned());
3802 }
3803 '^' | '$' | '|' => quantifiable = false,
3804 _ => quantifiable = true,
3805 }
3806 index += 1;
3807 }
3808
3809 if in_class {
3810 return Err("the pattern has an unterminated character class".to_owned());
3811 }
3812 if depth != 0 {
3813 return Err("the pattern has an unbalanced group".to_owned());
3814 }
3815 if deepest > MAX_PATTERN_DEPTH {
3816 return Err(format!("groups nest deeper than {MAX_PATTERN_DEPTH}"));
3817 }
3818 Ok(())
3819}
3820
3821fn snake_to_camel(value: &str) -> String {
3822 let mut output = String::new();
3823 let mut uppercase = false;
3824
3825 for character in value.chars() {
3826 if character == '_' {
3827 uppercase = true;
3828 } else if uppercase {
3829 output.extend(character.to_uppercase());
3830 uppercase = false;
3831 } else {
3832 output.push(character);
3833 }
3834 }
3835
3836 output
3837}
3838
3839fn take_mcp_arguments(attributes: &mut Vec<Attribute>) -> syn::Result<Option<McpArgs>> {
3840 let Some(index) = attributes.iter().position(|attribute| {
3841 let segments = &attribute.path().segments;
3842 segments.len() == 2 && segments[0].ident == "mcp" && segments[1].ident == "tool"
3843 }) else {
3844 return Ok(None);
3845 };
3846
3847 let attribute = attributes.remove(index);
3848 attribute.parse_args().map(Some)
3849}
3850
3851fn take_security_arguments(attributes: &mut Vec<Attribute>) -> syn::Result<Vec<SecurityArgs>> {
3852 let mut parsed = Vec::new();
3853 let mut retained = Vec::with_capacity(attributes.len());
3854 for attribute in attributes.drain(..) {
3855 if attribute.path().is_ident("security") {
3856 parsed.push(attribute.parse_args()?);
3857 } else {
3858 retained.push(attribute);
3859 }
3860 }
3861 *attributes = retained;
3862 Ok(parsed)
3863}
3864
3865fn mcp_projection(
3866 arguments: Option<McpArgs>,
3867 function_name: &Ident,
3868 summary: &LitStr,
3869) -> syn::Result<proc_macro2::TokenStream> {
3870 let Some(arguments) = arguments else {
3871 return Ok(quote!(descriptor));
3872 };
3873
3874 let name = arguments
3875 .name
3876 .unwrap_or_else(|| LitStr::new(&function_name.to_string(), function_name.span()));
3877 let description = arguments.description.unwrap_or_else(|| summary.clone());
3878 let risk = enum_variant(
3879 arguments.risk.as_ref(),
3880 "read",
3881 &[
3882 ("read", quote!(::blazingly::OperationRisk::Read)),
3883 ("write", quote!(::blazingly::OperationRisk::Write)),
3884 (
3885 "destructive",
3886 quote!(::blazingly::OperationRisk::Destructive),
3887 ),
3888 ],
3889 "risk",
3890 )?;
3891 let confirmation = enum_variant(
3892 arguments.confirmation.as_ref(),
3893 "never",
3894 &[
3895 ("never", quote!(::blazingly::Confirmation::Never)),
3896 ("required", quote!(::blazingly::Confirmation::Required)),
3897 ],
3898 "confirmation",
3899 )?;
3900 let exposure = enum_variant(
3901 arguments.expose_output.as_ref(),
3902 "full",
3903 &[
3904 ("full", quote!(::blazingly::OutputExposure::Full)),
3905 (
3906 "summary_only",
3907 quote!(::blazingly::OutputExposure::SummaryOnly),
3908 ),
3909 ("none", quote!(::blazingly::OutputExposure::None)),
3910 ],
3911 "expose_output",
3912 )?;
3913 let idempotent = arguments
3914 .idempotent
3915 .map_or_else(|| quote!(false), |value| quote!(#value));
3916
3917 Ok(quote! {
3918 descriptor.with_mcp_tool(
3919 ::blazingly::McpToolDescriptor::new(#name, #description)
3920 .with_output_exposure(#exposure),
3921 ::blazingly::AgentPolicy {
3922 risk: #risk,
3923 confirmation: #confirmation,
3924 idempotent: #idempotent,
3925 },
3926 )
3927 })
3928}
3929
3930fn enum_variant(
3931 value: Option<&LitStr>,
3932 default: &str,
3933 variants: &[(&str, proc_macro2::TokenStream)],
3934 key: &str,
3935) -> syn::Result<proc_macro2::TokenStream> {
3936 let selected = value.map_or_else(|| default.to_owned(), LitStr::value);
3937 variants
3938 .iter()
3939 .find(|(name, _)| *name == selected)
3940 .map(|(_, tokens)| tokens.clone())
3941 .ok_or_else(|| {
3942 let message = format!("unsupported `{key}` value `{selected}`");
3943 let span = value.map_or_else(proc_macro2::Span::call_site, LitStr::span);
3944 syn::Error::new(span, message)
3945 })
3946}
3947
3948fn operation_inputs(
3949 inputs: &syn::punctuated::Punctuated<FnArg, Token![,]>,
3950) -> syn::Result<Vec<OperationInput>> {
3951 let mut operation_inputs = Vec::new();
3952 let mut body_inputs = 0;
3953
3954 for input in inputs {
3955 let FnArg::Typed(PatType { pat, ty, .. }) = input else {
3956 return Err(syn::Error::new_spanned(
3957 input,
3958 "methods with a `self` receiver are not supported",
3959 ));
3960 };
3961 let name = operation_argument_name(pat)?;
3962 let (declared, by_reference) = match &**ty {
3963 Type::Reference(reference) if reference.mutability.is_some() => {
3964 return Err(syn::Error::new_spanned(
3965 ty,
3966 "operation arguments cannot be taken by unique reference; a \
3967 dependency is shared across the request",
3968 ));
3969 }
3970 Type::Reference(reference) => (&*reference.elem, true),
3971 other => (other, false),
3972 };
3973 let (kind, inner) = OperationInputKind::from_type(declared)
3974 .unwrap_or_else(|| (OperationInputKind::DirectDependency, declared.clone()));
3975 if by_reference && !kind.is_dependency() {
3976 return Err(syn::Error::new_spanned(
3977 ty,
3978 "only dependencies may be taken by reference; an extracted \
3979 argument is decoded from the request and owns its data",
3980 ));
3981 }
3982 if matches!(
3983 kind,
3984 OperationInputKind::Json
3985 | OperationInputKind::Form
3986 | OperationInputKind::Multipart
3987 | OperationInputKind::File
3988 | OperationInputKind::Stream
3989 ) {
3990 body_inputs += 1;
3991 if body_inputs > 1 {
3992 return Err(syn::Error::new_spanned(
3993 ty,
3994 "an operation may declare only one body extractor",
3995 ));
3996 }
3997 }
3998 let required = wrapper_inner(&inner, "Option").is_none();
3999 if matches!(kind, OperationInputKind::Path) && !required {
4000 return Err(syn::Error::new_spanned(
4001 ty,
4002 "Path<T> arguments are always required and cannot wrap Option<T>",
4003 ));
4004 }
4005 operation_inputs.push(OperationInput {
4006 name: LitStr::new(&name.to_string(), name.span()),
4007 kind,
4008 argument_type: declared.clone(),
4009 inner,
4010 required,
4011 by_reference,
4012 });
4013 }
4014
4015 Ok(operation_inputs)
4016}
4017
4018fn operation_argument_name(pattern: &Pat) -> syn::Result<&Ident> {
4019 match pattern {
4020 Pat::Ident(pattern) => Ok(&pattern.ident),
4021 Pat::TupleStruct(pattern) if pattern.elems.len() == 1 => {
4022 let Some(Pat::Ident(pattern)) = pattern.elems.first() else {
4023 return Err(syn::Error::new_spanned(
4024 pattern,
4025 "extractor tuple patterns must contain one identifier",
4026 ));
4027 };
4028 Ok(&pattern.ident)
4029 }
4030 _ => Err(syn::Error::new_spanned(
4031 pattern,
4032 "operation arguments require an identifier or `Extractor(identifier)` pattern",
4033 )),
4034 }
4035}
4036
4037impl OperationInputKind {
4038 fn from_type(ty: &Type) -> Option<(Self, Type)> {
4039 if type_is(ty, "WebSocketRequest") {
4040 return Some((Self::WebSocket, ty.clone()));
4041 }
4042 if type_is(ty, "UploadBody") {
4043 return Some((Self::Stream, ty.clone()));
4044 }
4045 [
4046 (Self::Path, "Path"),
4047 (Self::Query, "Query"),
4048 (Self::Header, "Header"),
4049 (Self::Cookie, "Cookie"),
4050 (Self::Json, "Json"),
4051 (Self::Form, "Form"),
4052 (Self::Multipart, "Multipart"),
4053 (Self::File, "File"),
4054 (Self::Extension, "Extension"),
4055 (Self::Extract, "Extract"),
4056 (Self::Dependency, "Depends"),
4057 ]
4058 .into_iter()
4059 .find_map(|(kind, wrapper)| wrapper_inner(ty, wrapper).map(|inner| (kind, inner)))
4060 }
4061
4062 fn source_tokens(self) -> Option<proc_macro2::TokenStream> {
4063 match self {
4064 Self::Path => Some(quote!(::blazingly::InputSource::Path)),
4065 Self::Query => Some(quote!(::blazingly::InputSource::Query)),
4066 Self::Header => Some(quote!(::blazingly::InputSource::Header)),
4067 Self::Cookie => Some(quote!(::blazingly::InputSource::Cookie)),
4068 Self::Json => Some(quote!(::blazingly::InputSource::Json)),
4069 Self::Form => Some(quote!(::blazingly::InputSource::Form)),
4070 Self::Multipart => Some(quote!(::blazingly::InputSource::Multipart)),
4071 Self::File => Some(quote!(::blazingly::InputSource::File)),
4072 Self::Stream => Some(quote!(::blazingly::InputSource::Stream)),
4073 Self::WebSocket
4074 | Self::Extension
4075 | Self::Extract
4076 | Self::Dependency
4077 | Self::DirectDependency => None,
4078 }
4079 }
4080
4081 const fn is_dependency(self) -> bool {
4082 matches!(self, Self::Dependency | Self::DirectDependency)
4083 }
4084}
4085
4086fn operation_output(output: &ReturnType) -> syn::Result<OperationOutput> {
4087 let ReturnType::Type(_, ty) = output else {
4088 return Err(syn::Error::new_spanned(
4089 output,
4090 "an explicit typed response is required",
4091 ));
4092 };
4093
4094 if let Some((success, error)) = result_types(ty) {
4095 let (status, success) = success_output(&success)?;
4096 return Ok(OperationOutput {
4097 status,
4098 success,
4099 error: Some(error),
4100 });
4101 }
4102 let (status, success) = success_output(ty)?;
4103 Ok(OperationOutput {
4104 status,
4105 success,
4106 error: None,
4107 })
4108}
4109
4110fn success_output(ty: &Type) -> syn::Result<(u16, Option<Type>)> {
4111 if type_is(ty, "NoContent") {
4112 return Ok((204, None));
4113 }
4114 if let Some(inner) = wrapper_inner(ty, "WithHeaders") {
4115 return success_output(&inner);
4116 }
4117 if let Some(inner) = wrapper_inner(ty, "Background") {
4118 return success_output(&inner);
4119 }
4120 if let Some((status, inner)) = status_wrapper(ty)? {
4121 let (_, body) = success_output(&inner)?;
4122 if matches!(status, 204 | 304) && body.is_some() {
4123 return Err(syn::Error::new_spanned(
4124 ty,
4125 "HTTP status 204 and 304 responses cannot contain a body",
4126 ));
4127 }
4128 return Ok((status, body));
4129 }
4130 if let Some(inner) = wrapper_inner(ty, "Accepted") {
4131 return Ok((202, Some(inner)));
4132 }
4133 if let Some(inner) = wrapper_inner(ty, "Created") {
4134 return Ok((201, Some(inner)));
4135 }
4136 if let Some(inner) = wrapper_inner(ty, "Json") {
4137 return Ok((200, Some(inner)));
4138 }
4139 Ok((200, Some(ty.clone())))
4140}
4141
4142fn status_wrapper(ty: &Type) -> syn::Result<Option<(u16, Type)>> {
4143 let Type::Path(TypePath { path, .. }) = ty else {
4144 return Ok(None);
4145 };
4146 let Some(segment) = path.segments.last() else {
4147 return Ok(None);
4148 };
4149 if segment.ident != "Status" {
4150 return Ok(None);
4151 }
4152 let syn::PathArguments::AngleBracketed(arguments) = &segment.arguments else {
4153 return Err(syn::Error::new_spanned(
4154 ty,
4155 "Status requires `Status<CODE, Response>`",
4156 ));
4157 };
4158 let mut arguments = arguments.args.iter();
4159 let Some(syn::GenericArgument::Const(syn::Expr::Lit(status))) = arguments.next() else {
4160 return Err(syn::Error::new_spanned(
4161 ty,
4162 "Status code must be an integer literal",
4163 ));
4164 };
4165 let syn::Lit::Int(status) = &status.lit else {
4166 return Err(syn::Error::new_spanned(
4167 status,
4168 "Status code must be an integer literal",
4169 ));
4170 };
4171 let status = status.base10_parse::<u16>()?;
4172 if !(200..=399).contains(&status) {
4173 return Err(syn::Error::new_spanned(
4174 ty,
4175 "typed success status must be between 200 and 399",
4176 ));
4177 }
4178 let Some(syn::GenericArgument::Type(inner)) = arguments.next() else {
4179 return Err(syn::Error::new_spanned(
4180 ty,
4181 "Status requires an inner typed response",
4182 ));
4183 };
4184 Ok(Some((status, inner.clone())))
4185}
4186
4187fn type_is(ty: &Type, expected: &str) -> bool {
4188 let Type::Path(TypePath { path, .. }) = ty else {
4189 return false;
4190 };
4191 path.segments
4192 .last()
4193 .is_some_and(|segment| segment.ident == expected)
4194}
4195
4196fn result_types(ty: &Type) -> Option<(Type, Type)> {
4197 let Type::Path(TypePath { path, .. }) = ty else {
4198 return None;
4199 };
4200 let segment = path.segments.last()?;
4201 if segment.ident != "Result" {
4202 return None;
4203 }
4204 let syn::PathArguments::AngleBracketed(arguments) = &segment.arguments else {
4205 return None;
4206 };
4207 let mut types = arguments.args.iter().filter_map(|argument| {
4208 if let syn::GenericArgument::Type(ty) = argument {
4209 Some(ty.clone())
4210 } else {
4211 None
4212 }
4213 });
4214 Some((types.next()?, types.next()?))
4215}
4216
4217fn wrapper_inner(ty: &Type, wrapper: &str) -> Option<Type> {
4218 let Type::Path(TypePath { path, .. }) = ty else {
4219 return None;
4220 };
4221 let segment = path.segments.last()?;
4222 if segment.ident != wrapper {
4223 return None;
4224 }
4225 let syn::PathArguments::AngleBracketed(arguments) = &segment.arguments else {
4226 return None;
4227 };
4228 let syn::GenericArgument::Type(inner) = arguments.args.first()? else {
4229 return None;
4230 };
4231 Some(inner.clone())
4232}
4233
4234#[cfg(test)]
4235mod tests {
4236 use super::{
4237 Attribute, FieldRules, FieldShape, Fields, ItemEnum, ItemStruct, ModelArgs, NumericLiteral,
4238 ProviderArgs, ProviderLifetimeArgument, RenameRule, Type, constraint_encodings,
4239 enum_model_tokens, field_shape, lint_pattern_syntax, metadata_encodings, model_tokens,
4240 normalize_collection_rules, provider_tokens, quote, reject_incompatible_rules, schema_type,
4241 snake_to_camel, take_field_rules,
4242 };
4243
4244 #[test]
4245 fn a_provider_may_read_request_inputs_and_a_singleton_may_not() {
4246 let request = ProviderArgs {
4247 lifetime: ProviderLifetimeArgument::Request,
4248 };
4249 let function = syn::parse_str::<syn::ItemFn>(
4250 "fn tenant(Header(tenant): Header<String>, repo: Depends<Repo>) -> Tenant { todo!() }",
4251 )
4252 .expect("provider fixture parses");
4253 let expansion = provider_tokens(&request, &function)
4254 .expect("a request provider with inputs expands")
4255 .to_string();
4256 assert!(expansion.contains("RequestProvider"), "{expansion}");
4257 assert!(expansion.contains("request_from_slots"), "{expansion}");
4258 assert!(expansion.contains("InputSource :: Header"), "{expansion}");
4259
4260 let singleton = ProviderArgs {
4261 lifetime: ProviderLifetimeArgument::Singleton,
4262 };
4263 let error = provider_tokens(&singleton, &function)
4264 .expect_err("a singleton is built before any request exists");
4265 assert!(
4266 error.to_string().contains("cannot declare request inputs"),
4267 "{error}"
4268 );
4269
4270 let body = syn::parse_str::<syn::ItemFn>(
4271 "fn bad(Json(payload): Json<Model>) -> Model { todo!() }",
4272 )
4273 .expect("body fixture parses");
4274 let error = provider_tokens(&request, &body).expect_err("a provider must not own the body");
4275 assert!(error.to_string().contains("body payloads"), "{error}");
4276 }
4277
4278 fn field_attributes(declaration: &str) -> Vec<Attribute> {
4279 let model = syn::parse_str::<ItemStruct>(&format!("struct Probe {{ {declaration} }}"))
4280 .expect("fixture struct parses");
4281 let Fields::Named(fields) = model.fields else {
4282 panic!("fixture struct must use named fields");
4283 };
4284 fields
4285 .named
4286 .into_iter()
4287 .next()
4288 .expect("fixture struct declares one field")
4289 .attrs
4290 }
4291
4292 fn rules_of(declaration: &str) -> syn::Result<FieldRules> {
4293 let mut attributes = field_attributes(declaration);
4294 take_field_rules(&mut attributes)
4295 }
4296
4297 fn parse_type(source: &str) -> Type {
4298 syn::parse_str::<Type>(source).expect("fixture type parses")
4299 }
4300
4301 fn rejection_message<T>(result: syn::Result<T>) -> String {
4302 match result {
4303 Ok(_) => panic!("the declaration must be rejected"),
4304 Err(error) => error.to_string(),
4305 }
4306 }
4307
4308 fn reject(declaration: &str, field_type: &str) -> String {
4309 let rules = rules_of(declaration).expect("attributes parse");
4310 let field_type = parse_type(field_type);
4311 let shape = field_shape(&field_type);
4312 reject_incompatible_rules(&rules, &field_type, shape)
4313 .expect_err("the rule must be rejected")
4314 .to_string()
4315 }
4316
4317 fn expand(source: &str, arguments: &ModelArgs) -> syn::Result<String> {
4318 let mut model = syn::parse_str::<ItemStruct>(source).expect("fixture model parses");
4319 model_tokens(arguments, &mut model).map(|tokens| tokens.to_string())
4320 }
4321
4322 fn expand_default(source: &str) -> syn::Result<String> {
4323 expand(source, &ModelArgs::default())
4324 }
4325
4326 #[test]
4327 fn field_shapes_classify_strings_numbers_and_collections() {
4328 assert_eq!(field_shape(&parse_type("String")), FieldShape::Text);
4329 assert_eq!(field_shape(&parse_type("u8")), FieldShape::Integer);
4330 assert_eq!(field_shape(&parse_type("i128")), FieldShape::Integer);
4331 assert_eq!(field_shape(&parse_type("usize")), FieldShape::Integer);
4332 assert_eq!(field_shape(&parse_type("f32")), FieldShape::Float);
4333 assert_eq!(
4334 field_shape(&parse_type("Vec<Address>")),
4335 FieldShape::Collection
4336 );
4337 assert_eq!(field_shape(&parse_type("Uuid")), FieldShape::Other);
4338 assert_eq!(field_shape(&parse_type("bool")), FieldShape::Other);
4339 }
4340
4341 #[test]
4342 fn numeric_rules_are_rejected_on_non_numeric_fields() {
4343 for rule in [
4344 "#[minimum(1)]",
4345 "#[maximum(1)]",
4346 "#[exclusive_minimum(1)]",
4347 "#[exclusive_maximum(1)]",
4348 "#[multiple_of(2)]",
4349 ] {
4350 let message = reject(&format!("{rule} name: String"), "String");
4351 assert!(
4352 message.contains("requires an integer or floating-point field"),
4353 "{rule} produced {message}"
4354 );
4355 assert!(message.contains("String"), "{rule} produced {message}");
4356 }
4357 }
4358
4359 #[test]
4360 fn collection_rules_are_rejected_outside_collections() {
4361 for rule in ["#[min_items(1)]", "#[max_items(1)]", "#[unique_items]"] {
4362 let message = reject(&format!("{rule} name: String"), "String");
4363 assert!(
4364 message.contains("requires a `Vec<T>` or `Option<Vec<T>>` field"),
4365 "{rule} produced {message}"
4366 );
4367 }
4368 }
4369
4370 #[test]
4371 fn string_rules_are_rejected_outside_strings() {
4372 let message = reject("#[pattern(\"^a$\")] count: u32", "u32");
4373 assert!(
4374 message.contains("`pattern` requires a `String`"),
4375 "{message}"
4376 );
4377 let message = reject("#[email] count: u32", "u32");
4378 assert!(message.contains("`email` requires a `String`"), "{message}");
4379 }
4380
4381 #[test]
4382 fn length_rules_accept_strings_and_collections_but_not_numbers() {
4383 let rules = rules_of("#[min_length(2)] tags: Vec<String>").expect("attributes parse");
4384 let collection = parse_type("Vec<String>");
4385 reject_incompatible_rules(&rules, &collection, FieldShape::Collection)
4386 .expect("collections accept length rules");
4387
4388 let message = reject("#[min_length(2)] count: u32", "u32");
4389 assert!(
4390 message.contains("`min_length` requires a `String`, `Option<String>`, or `Vec<T>`"),
4391 "{message}"
4392 );
4393 }
4394
4395 #[test]
4396 fn inverted_and_degenerate_bounds_are_rejected() {
4397 let message = rejection_message(rules_of("#[min_length(5)] #[max_length(2)] name: String"));
4398 assert!(message.contains("`min_length` cannot be greater than `max_length`"));
4399
4400 let message = rejection_message(rules_of(
4401 "#[min_items(5)] #[max_items(2)] tags: Vec<String>",
4402 ));
4403 assert!(message.contains("`min_items` cannot be greater than `max_items`"));
4404
4405 let message = rejection_message(rules_of("#[minimum(5)] #[maximum(2)] count: u32"));
4406 assert!(message.contains("`minimum` cannot be greater than `maximum`"));
4407
4408 let message = rejection_message(rules_of("#[multiple_of(0)] count: u32"));
4409 assert!(message.contains("`multiple_of` cannot be zero"));
4410 }
4411
4412 #[test]
4413 fn numeric_attributes_accept_negative_and_floating_point_literals() {
4414 let rules = rules_of("#[minimum(-3)] #[maximum(2.5)] ratio: f64").expect("bounds parse");
4415 assert_eq!(
4416 rules.minimum.map(|(value, _)| value),
4417 Some(NumericLiteral::Integer(-3))
4418 );
4419 assert_eq!(
4420 rules.maximum.map(|(value, _)| value),
4421 Some(NumericLiteral::Float(2.5))
4422 );
4423
4424 let message = rejection_message(rules_of("#[minimum(\"one\")] count: u32"));
4425 assert!(
4426 message.contains("integer or floating-point literal"),
4427 "{message}"
4428 );
4429 }
4430
4431 #[test]
4432 fn constraints_use_a_canonical_key_value_encoding() {
4433 let rules = rules_of(
4434 "#[minimum(1)] #[maximum(10.0)] #[exclusive_minimum(0)] \
4435 #[exclusive_maximum(11)] #[multiple_of(2)] count: u32",
4436 )
4437 .expect("bounds parse");
4438 assert_eq!(
4439 constraint_encodings(&rules),
4440 [
4441 "minimum=1",
4442 "maximum=10.0",
4443 "exclusive_minimum=0",
4444 "exclusive_maximum=11",
4445 "multiple_of=2"
4446 ]
4447 );
4448
4449 let rules = rules_of("#[pattern(\"^[a-z]+$\")] slug: String").expect("pattern parses");
4450 assert_eq!(constraint_encodings(&rules), ["pattern=^[a-z]+$"]);
4451
4452 let rules = rules_of("#[min_items(1)] #[max_items(4)] #[unique_items] tags: Vec<String>")
4453 .expect("collection rules parse");
4454 assert_eq!(
4455 constraint_encodings(&rules),
4456 ["min_items=1", "max_items=4", "unique_items=true"]
4457 );
4458 }
4459
4460 #[test]
4461 fn collection_length_rules_are_folded_into_item_bounds() {
4462 let mut rules = rules_of("#[min_length(2)] #[max_length(4)] tags: Vec<String>")
4463 .expect("length rules parse");
4464 normalize_collection_rules(&mut rules, FieldShape::Collection);
4465 assert!(rules.min_length.is_none());
4466 assert!(rules.max_length.is_none());
4467 assert_eq!(constraint_encodings(&rules), ["min_items=2", "max_items=4"]);
4468 }
4469
4470 #[test]
4471 fn unsupported_patterns_are_rejected_when_the_macro_expands() {
4472 for (pattern, fragment) in [
4473 ("", "the pattern is empty"),
4474 ("^a{2,3}$", "counted repetition"),
4475 ("^(?:a)$", "not supported"),
4476 ("^(a$", "unbalanced group"),
4477 ("^a)$", "unbalanced group"),
4478 ("^[a-z$", "unterminated character class"),
4479 ("^*a$", "no preceding expression"),
4480 (r"^\q$", "the escape `\\q` is not supported"),
4481 (r"^a\", "lone backslash"),
4482 ("^a$b$", "`$` is supported only at the end"),
4483 ("a^b", "`^` is supported only at the start"),
4484 ] {
4485 let error = lint_pattern_syntax(pattern)
4486 .expect_err(&format!("{pattern} must be rejected"))
4487 .to_string();
4488 assert!(error.contains(fragment), "{pattern} produced {error}");
4489 }
4490 }
4491
4492 #[test]
4493 fn supported_patterns_pass_the_compile_time_lint() {
4494 for pattern in [
4495 "^[a-z][a-z0-9_]*$",
4496 r"^(cat|dog)-\d+$",
4497 r"\w+@\w+\.\w+",
4498 "^a[^0-9]?$",
4499 r"^cost\$",
4500 "^[a-z-]+$",
4501 ] {
4502 assert!(
4503 lint_pattern_syntax(pattern).is_ok(),
4504 "{pattern} must be accepted"
4505 );
4506 }
4507 }
4508
4509 #[test]
4510 fn nested_models_recurse_without_an_explicit_attribute() {
4511 let expansion = expand_default("struct Order { address: Address, items: Vec<Line> }")
4512 .expect("model expands");
4513 assert!(expansion.contains("__blazingly_nested"));
4514 assert!(expansion.contains("__blazingly_nested_items"));
4515 assert!(expansion.contains("__blazingly_is_model"));
4516 }
4517
4518 #[test]
4519 fn explicit_nested_stays_accepted_and_still_marks_the_descriptor() {
4520 let expansion =
4521 expand_default("struct Order { #[nested] address: Address }").expect("model expands");
4522 assert!(expansion.contains("ValidationRule :: Nested"));
4523 }
4524
4525 #[test]
4526 fn scalar_fields_without_rules_emit_no_validation_body() {
4527 let expansion =
4528 expand_default("struct Order { name: String, count: u32 }").expect("model expands");
4529 assert!(!expansion.contains("__BlazinglyValue"));
4530 assert!(!expansion.contains("self . name"));
4531 }
4532
4533 #[test]
4534 fn model_level_validate_with_runs_after_the_field_rules() {
4535 let arguments = ModelArgs {
4536 validator: Some(syn::parse_str("checks::validate_window").expect("path parses")),
4537 ..ModelArgs::default()
4538 };
4539 let expansion = expand(
4540 "struct Window { #[minimum(0)] start: i64, #[minimum(0)] end: i64 }",
4541 &arguments,
4542 )
4543 .expect("model expands");
4544 let validator = expansion
4545 .find("checks :: validate_window")
4546 .expect("the model validator is called");
4547 let last_field_rule = expansion
4548 .rfind("check_minimum")
4549 .expect("field rules are emitted");
4550 assert!(validator > last_field_rule);
4551 assert!(expansion.contains("merge_model_violations"));
4552 }
4553
4554 #[test]
4555 fn model_arguments_reject_unknown_keys() {
4556 let error = rejection_message(syn::parse_str::<ModelArgs>(
4557 "rename_all = \"camelCase\", frobnicate = \"x\"",
4558 ));
4559 assert!(
4560 error.contains("`borrowed`, `rename_all`, and `validate_with`"),
4561 "{error}"
4562 );
4563 }
4564
4565 #[test]
4566 fn snake_case_names_become_camel_case() {
4567 assert_eq!(snake_to_camel("public_name"), "publicName");
4568 assert_eq!(snake_to_camel("id"), "id");
4569 assert_eq!(snake_to_camel("a_b_c"), "aBC");
4570 }
4571
4572 fn borrowed_arguments() -> ModelArgs {
4573 syn::parse_str::<ModelArgs>("borrowed").expect("`borrowed` is a bare flag")
4574 }
4575
4576 fn expand_borrowed(source: &str) -> syn::Result<String> {
4577 expand(source, &borrowed_arguments())
4578 }
4579
4580 #[test]
4581 fn a_borrowed_view_serializes_and_describes_itself_but_never_parses() {
4582 let expansion = expand_borrowed("struct View<'store> { title: &'store str }")
4583 .expect("a borrowed view expands");
4584 assert!(expansion.contains("Serialize"));
4585 assert!(
4586 !expansion.contains("Deserialize"),
4587 "a borrowed view is an output type"
4588 );
4589 assert!(expansion.contains("ApiSchema for View"));
4590 assert!(
4591 !expansion.contains("ApiModel for"),
4592 "a borrowed view is never validated"
4593 );
4594 assert!(!expansion.contains("ValidationErrors"));
4595 }
4596
4597 #[test]
4598 fn borrowed_field_types_document_the_schema_their_owned_form_documents() {
4599 let resolved = |source: &str| {
4600 let ty = schema_type(&parse_type(source));
4601 quote!(#ty).to_string().replace(' ', "")
4602 };
4603 assert_eq!(resolved("&'store str"), "&str");
4604 assert_eq!(resolved("Vec<&'store Tag>"), "Vec<Tag>");
4605 assert_eq!(resolved("Option<&'store str>"), "Option<&str>");
4606 assert_eq!(resolved("&'store [Tag]"), "::std::vec::Vec<Tag>");
4607 assert_eq!(resolved("Cow<'store, str>"), "&str");
4608 assert_eq!(resolved("Page<'store, Tag>"), "Page<Tag>");
4609 assert_eq!(resolved("Vec<Tag>"), "Vec<Tag>");
4611 }
4612
4613 #[test]
4614 fn a_generic_borrowed_envelope_names_one_schema_per_item_type() {
4615 let expansion = expand_borrowed("struct Page<'store, T> { items: Vec<&'store T> }")
4616 .expect("a generic borrowed view expands");
4617 assert!(expansion.contains("__blazingly_schema_name"));
4618 assert!(expansion.contains("T : :: blazingly :: ApiSchema"));
4619 }
4620
4621 #[test]
4622 fn validation_rules_are_rejected_on_a_borrowed_view() {
4623 let error = rejection_message(expand_borrowed(
4624 "struct View<'store> { #[min_length(2)] title: &'store str }",
4625 ));
4626 assert!(error.contains("`#[min_length]`"), "{error}");
4627 assert!(error.contains("never validated"), "{error}");
4628
4629 let arguments = syn::parse_str::<ModelArgs>("borrowed, validate_with = checks::window")
4630 .expect("parses");
4631 let error = rejection_message(expand("struct View<'a> { title: &'a str }", &arguments));
4632 assert!(error.contains("never validated"), "{error}");
4633 }
4634
4635 fn expand_enum(source: &str, arguments: &ModelArgs) -> syn::Result<String> {
4636 let mut model = syn::parse_str::<ItemEnum>(source).expect("fixture enum parses");
4637 enum_model_tokens(arguments, &mut model).map(|tokens| tokens.to_string())
4638 }
4639
4640 fn model_arguments(source: &str) -> ModelArgs {
4641 syn::parse_str::<ModelArgs>(source).expect("fixture arguments parse")
4642 }
4643
4644 #[test]
4645 fn a_default_is_recorded_as_json_beside_the_field_rules() {
4646 let rules = rules_of("#[default(20)] limit: u32").expect("the default parses");
4647 assert_eq!(metadata_encodings(&rules, false), ["default=20"]);
4648
4649 let rules = rules_of("#[default(-2.5)] ratio: f64").expect("the default parses");
4650 assert_eq!(metadata_encodings(&rules, false), ["default=-2.5"]);
4651
4652 let rules = rules_of("#[default(\"dr\\\"aft\")] status: String").expect("parses");
4653 assert_eq!(metadata_encodings(&rules, false), [r#"default="dr\"aft""#]);
4654
4655 let rules = rules_of("#[default(false)] verbose: bool").expect("the default parses");
4656 assert_eq!(
4657 metadata_encodings(&rules, true),
4658 ["default=false", "nullable=true"]
4659 );
4660 }
4661
4662 #[test]
4663 fn a_default_must_match_the_field_it_fills_in() {
4664 let message = reject("#[default(\"draft\")] limit: u32", "u32");
4665 assert!(
4666 message.contains("a string literal requires a `String` field"),
4667 "{message}"
4668 );
4669
4670 let message = reject("#[default(20)] status: String", "String");
4671 assert!(
4672 message.contains("an integer literal requires an integer or floating-point field"),
4673 "{message}"
4674 );
4675
4676 let message = reject("#[default(true)] limit: u32", "u32");
4677 assert!(
4678 message.contains("a boolean literal requires a `bool` field"),
4679 "{message}"
4680 );
4681
4682 let message = rejection_message(rules_of("#[default(limit())] limit: u32"));
4683 assert!(
4684 message.contains("string, integer, floating-point, or boolean literal"),
4685 "{message}"
4686 );
4687 }
4688
4689 #[test]
4690 fn a_defaulted_field_is_not_optional_and_is_no_longer_required() {
4691 let message = rejection_message(expand_default(
4692 "struct List { #[default(20)] limit: Option<u32> }",
4693 ));
4694 assert!(message.contains("declare it without `Option`"), "{message}");
4695
4696 let expansion =
4697 expand_default("struct List { #[default(20)] limit: u32 }").expect("model expands");
4698 assert!(expansion.contains("__blazingly_default_list_limit"));
4699 assert!(expansion.contains("serde (default = \"__blazingly_default_list_limit\")"));
4700 assert!(
4701 expansion.contains("FieldDescriptor :: new (\"limit\" , false ,"),
4702 "a field with a default is not required of the client: {expansion}"
4703 );
4704 }
4705
4706 #[test]
4707 fn an_optional_field_records_its_nullability() {
4708 let expansion =
4709 expand_default("struct Article { summary: Option<String> }").expect("model expands");
4710 assert!(expansion.contains("\"nullable=true\""));
4711
4712 let expansion = expand_default("struct Article { summary: String }").expect("expands");
4713 assert!(!expansion.contains("nullable"));
4714 }
4715
4716 #[test]
4717 fn a_value_type_declares_one_reusable_bundle_of_rules() {
4718 let expansion = expand_default("struct Title (String) ;").expect("a value type expands");
4719 assert!(expansion.contains("serde (transparent)"));
4720 assert!(expansion.contains("impl :: blazingly :: ApiConstrained for Title"));
4721 assert!(expansion.contains("fn into_inner"));
4722
4723 let mut model = syn::parse_str::<ItemStruct>("#[min_length(8)] struct Title (String) ;")
4724 .expect("parses");
4725 let expansion = model_tokens(&ModelArgs::default(), &mut model)
4726 .expect("a value type with rules expands")
4727 .to_string();
4728 assert!(expansion.contains("ValidationRule :: MinLength (8usize)"));
4729 assert!(expansion.contains("min_length"));
4730 }
4731
4732 #[test]
4733 fn a_value_type_rejects_what_only_a_field_can_carry() {
4734 let message = rejection_message(expand_default("struct Pair (String , u32) ;"));
4735 assert!(message.contains("wraps exactly one field"), "{message}");
4736
4737 let message = rejection_message(expand_default("#[alias(\"t\")] struct Title (String) ;"));
4738 assert!(
4739 message.contains("`alias` names an extra wire key"),
4740 "{message}"
4741 );
4742
4743 let message =
4744 rejection_message(expand_default("#[default(\"x\")] struct Title (String) ;"));
4745 assert!(message.contains("belongs to the field"), "{message}");
4746
4747 let message = rejection_message(expand_default("struct Title (Option<String>) ;"));
4748 assert!(
4749 message.contains("declare the field that uses it"),
4750 "{message}"
4751 );
4752
4753 let message = rejection_message(expand(
4754 "struct Title (String) ;",
4755 &model_arguments("rename_all = \"camelCase\""),
4756 ));
4757 assert!(message.contains("`rename_all` renames fields"), "{message}");
4758 }
4759
4760 #[test]
4761 fn an_enumeration_pins_every_variant_to_an_explicit_wire_value() {
4762 let expansion = expand_enum(
4763 "enum Language { Uk, Ru, En }",
4764 &model_arguments("rename_all = \"lowercase\""),
4765 )
4766 .expect("an enumeration expands");
4767 assert!(expansion.contains("serde (rename = \"uk\")"));
4768 assert!(expansion.contains("\"enum=uk|ru|en\""));
4769 assert!(expansion.contains("SchemaKind :: String"));
4770 assert!(expansion.contains("const VARIANTS"));
4771
4772 let expansion = expand_enum(
4773 "enum Status { NotFound, #[rename(\"ok\")] Fine }",
4774 &ModelArgs::default(),
4775 )
4776 .expect("an enumeration expands");
4777 assert!(expansion.contains("serde (rename = \"NotFound\")"));
4778 assert!(expansion.contains("serde (rename = \"ok\")"));
4779 assert!(expansion.contains("\"enum=NotFound|ok\""));
4780 }
4781
4782 #[test]
4783 fn enum_rename_rules_match_the_serde_spelling() {
4784 let cases = [
4785 ("PascalCase", "NotFound"),
4786 ("lowercase", "notfound"),
4787 ("UPPERCASE", "NOTFOUND"),
4788 ("camelCase", "notFound"),
4789 ("snake_case", "not_found"),
4790 ("SCREAMING_SNAKE_CASE", "NOT_FOUND"),
4791 ("kebab-case", "not-found"),
4792 ("SCREAMING-KEBAB-CASE", "NOT-FOUND"),
4793 ];
4794 for (rule, expected) in cases {
4795 let rule = RenameRule::parse(rule).expect("the rule is supported");
4796 assert_eq!(rule.apply("NotFound"), expected);
4797 }
4798 assert!(RenameRule::parse("Train-Case").is_none());
4799 }
4800
4801 #[test]
4802 fn an_enumeration_rejects_data_carrying_and_ambiguous_variants() {
4803 let message = rejection_message(expand_enum(
4804 "enum Payload { Text(String) }",
4805 &ModelArgs::default(),
4806 ));
4807 assert!(message.contains("a variant cannot carry data"), "{message}");
4808
4809 let message = rejection_message(expand_enum(
4810 "enum Language { Uk, #[rename(\"Uk\")] Ukrainian }",
4811 &ModelArgs::default(),
4812 ));
4813 assert!(message.contains("declared twice"), "{message}");
4814
4815 let message = rejection_message(expand_enum(
4816 "enum Language { #[rename(\"a|b\")] Both }",
4817 &ModelArgs::default(),
4818 ));
4819 assert!(message.contains("`|` separates the variants"), "{message}");
4820
4821 let message = rejection_message(expand_enum("enum Empty { }", &ModelArgs::default()));
4822 assert!(message.contains("at least one variant"), "{message}");
4823
4824 let message = rejection_message(expand_enum(
4825 "enum Language { Uk }",
4826 &model_arguments("rename_all = \"Train-Case\""),
4827 ));
4828 assert!(message.contains("SCREAMING-KEBAB-CASE"), "{message}");
4829 }
4830
4831 #[test]
4832 fn a_field_validator_reports_through_the_undoubled_merge() {
4833 let expansion = expand_default("struct Window { #[validate_with(checks::at)] at: u32 }")
4834 .expect("model expands");
4835 assert!(expansion.contains("merge_field_validation_errors"));
4836 assert!(!expansion.contains("merge_validation_errors (& mut errors"));
4837 }
4838
4839 #[test]
4840 fn an_owning_model_rejects_generics_and_points_at_the_borrowed_form() {
4841 let error = rejection_message(expand_default("struct Page<T> { items: Vec<T> }"));
4842 assert!(error.contains("silently skip validating it"), "{error}");
4843 assert!(error.contains("#[api_model(borrowed)]"), "{error}");
4844
4845 let error = rejection_message(expand_default("struct View<'a> { title: &'a str }"));
4846 assert!(error.contains("cannot borrow from the request"), "{error}");
4847 assert!(error.contains("#[api_model(borrowed)]"), "{error}");
4848 }
4849}