vespera_macro 0.4.0

Procedural macros for Vespera - automatic route discovery and OpenAPI generation
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
//! Default-value extraction for OpenAPI schema generation.
//!
//! Handles the three sources of struct field defaults:
//! 1. Pre-extracted `SCHEMA_STORAGE` defaults (populated by `#[derive(Schema)]`)
//! 2. `#[schema(default = "...")]` attributes (generated by `schema_type!`)
//! 3. `#[serde(default)]` / `#[serde(default = "fn_name")]` attributes
//!    (the function variant needs a parsed file AST)

use std::collections::{BTreeMap, BTreeSet, HashMap};

use crate::{
    parser::{
        extract_default, extract_field_rename, extract_rename_all, rename_field,
        strip_raw_prefix_owned,
    },
    schema_macro::type_utils::get_type_default as utils_get_type_default,
};

/// Set the default value on an inline property schema, if not already set.
///
/// Looks up `field_name` in the properties map. If found as an inline schema
/// and the schema has no existing default, sets `value` as the default.
pub(super) fn set_property_default(
    properties: &mut BTreeMap<String, vespera_core::schema::SchemaRef>,
    field_name: &str,
    value: serde_json::Value,
) {
    use vespera_core::schema::{SchemaRef, SchemaType};

    if let Some(SchemaRef::Inline(prop_schema)) = properties.get_mut(field_name)
        && prop_schema.default.is_none()
    {
        // A default on a string-typed property must itself be a string — e.g. a
        // `Decimal` field (string wire type) carrying a numeric DB default value.
        let value = if prop_schema.schema_type == Some(SchemaType::String) {
            match value {
                serde_json::Value::Number(n) => serde_json::Value::String(n.to_string()),
                serde_json::Value::Bool(b) => serde_json::Value::String(b.to_string()),
                other => other,
            }
        } else {
            value
        };
        prop_schema.default = Some(value);
    }
}

/// Process default functions for struct fields
/// This function extracts default values from:
/// 1. `#[schema(default = "value")]` attributes (generated by `schema_type!` from `sea_orm(default_value)`)
/// 2. `#[serde(default = "function_name")]` by finding the function in the file AST
/// 3. `#[serde(default)]` by using type-specific defaults
pub(super) fn process_default_functions(
    struct_item: &syn::ItemStruct,
    file_ast: Option<&syn::File>,
    schema: &mut vespera_core::schema::Schema,
    stored_defaults: &BTreeMap<String, serde_json::Value>,
) {
    use syn::Fields;

    // Cheap O(field_attrs) precheck: when `stored_defaults` is empty AND no
    // field carries a `serde` or `schema` attribute, NONE of the three
    // priority paths below can fire and the `required` demotion stays empty
    // — so the rest of this function is provably a no-op. The early-return
    // is observably equivalent to the full walk: the emitted `Schema` stays
    // byte-identical (the `insta` snapshots in `crates/vespera_macro/src/
    // snapshots/` are the runtime-neutrality lock).
    //
    // Bench fixture (`benches/macro-compile-bench`) has 14 component schemas
    // and NONE carry `#[serde(default)]` / `#[schema(default)]`, so the
    // pre-extracted `field_defaults` is empty for all of them — every full
    // `macro_expand_crate` previously paid ~14 struct-level walks + ~70
    // per-field iterations of `parse_nested_meta` for pure no-op work.
    if stored_defaults.is_empty() && !any_field_carries_default_relevant_attr(struct_item) {
        return;
    }

    // Extract rename_all from struct level
    let struct_rename_all = extract_rename_all(&struct_item.attrs);

    // Locate the object schema that actually holds the fields. Flatten structs
    // are `allOf`-shaped: their own (non-flattened) fields live in the first
    // inline `allOf` member, not a top-level `properties` map — defaults (and
    // the `required` demotion below) must apply there too.
    let Some(target) = field_bearing_schema_mut(schema) else {
        return;
    };

    // Every `#[serde(default)]` field is wire-optional: serde accepts payloads
    // with that key omitted. Requiredness therefore depends on wire presence,
    // not on whether we could statically extract the default value.
    let mut serde_default_fields: BTreeSet<String> = BTreeSet::new();
    let function_index = file_ast.map(function_index_by_name);

    // Process each field in the struct
    if let Some(properties) = target.properties.as_mut()
        && let Fields::Named(fields_named) = &struct_item.fields
    {
        for field in &fields_named.named {
            let rust_field_name = field.ident.as_ref().map_or_else(
                || "unknown".to_string(),
                |i| strip_raw_prefix_owned(i.to_string()),
            );
            let field_name = extract_field_rename(&field.attrs)
                .unwrap_or_else(|| rename_field(&rust_field_name, struct_rename_all.as_deref()));

            // Priority 0: Pre-extracted defaults from SCHEMA_STORAGE (populated by #[derive(Schema)])
            if let Some(value) = stored_defaults.get(&rust_field_name) {
                set_property_default(properties, &field_name, value.clone());
                continue;
            }

            // Priority 1: #[schema(default = "value")] from schema_type! macro.
            // The attribute value is a string literal; for a string-typed field
            // use it VERBATIM so lexical form is preserved (e.g. a leading-zero
            // id `"00123"` must NOT normalise to `"123"`), otherwise infer the
            // JSON type from the string.
            if let Some(default_str) = extract_schema_default_attr(&field.attrs) {
                let is_string_field = matches!(
                    properties.get(&field_name),
                    Some(vespera_core::schema::SchemaRef::Inline(s))
                        if s.schema_type == Some(vespera_core::schema::SchemaType::String)
                );
                let value = if is_string_field {
                    serde_json::Value::String(default_str)
                } else {
                    parse_default_string_to_json_value(&default_str)
                };
                set_property_default(properties, &field_name, value);
                continue;
            }

            // Priority 2: #[serde(default)] / #[serde(default = "fn")]
            let default_info = match extract_default(&field.attrs) {
                Some(Some(func_name)) => {
                    serde_default_fields.insert(field_name.clone());
                    func_name
                }
                Some(None) => {
                    serde_default_fields.insert(field_name.clone());
                    // Simple default (no function): use the type-specific default
                    // when known. Unresolvable defaults are rejected earlier by
                    // `#[derive(Schema)]`, so this fallback only preserves unit
                    // test behaviour when called directly.
                    if let Some(default_value) = utils_get_type_default(&field.ty) {
                        set_property_default(properties, &field_name, default_value);
                    }
                    continue;
                }
                None => continue, // No default attribute
            };

            // Priority 2 (function form) is the only step that needs the AST.
            // Unresolvable values are rejected earlier by `#[derive(Schema)]`;
            // this path simply omits the `default` when invoked directly in
            // tests without that derive-time validation.
            let resolved = function_index
                .as_ref()
                .and_then(|index| find_function_in_index(index, &default_info))
                .and_then(extract_default_value_from_function);
            if let Some(default_value) = resolved {
                set_property_default(properties, &field_name, default_value);
            }
        }
    }

    // Demote every serde-default field from `required`: it is optional on the
    // wire regardless of how the default value was obtained.
    if !serde_default_fields.is_empty() {
        if let Some(required) = target.required.as_mut() {
            required.retain(|name| !serde_default_fields.contains(name));
        }
        if target.required.as_ref().is_some_and(Vec::is_empty) {
            target.required = None;
        }
    }
}

/// Returns `true` if any named field on `struct_item` carries a `#[serde(...)]`
/// or `#[schema(...)]` attribute — the only attributes that can possibly
/// trigger any of `process_default_functions`'s three priority paths.
///
/// Only inspects the attribute path (an `Ident` comparison) — never calls
/// `parse_nested_meta`, so this stays O(sum of field attr counts) byte
/// comparisons. Tuple/unit structs return `false` immediately (matching the
/// `Fields::Named` branch in the caller).
fn any_field_carries_default_relevant_attr(struct_item: &syn::ItemStruct) -> bool {
    let syn::Fields::Named(fields) = &struct_item.fields else {
        return false;
    };
    fields.named.iter().any(|field| {
        field
            .attrs
            .iter()
            .any(|attr| attr.path().is_ident("serde") || attr.path().is_ident("schema"))
    })
}

/// Return the object schema that actually carries the struct's fields: the
/// schema itself, or — for flatten/`allOf`-shaped structs — the first inline
/// `allOf` member (where the non-flattened fields live).
fn field_bearing_schema_mut(
    schema: &mut vespera_core::schema::Schema,
) -> Option<&mut vespera_core::schema::Schema> {
    if schema.properties.is_some() {
        return Some(schema);
    }
    schema
        .all_of
        .as_mut()?
        .iter_mut()
        .find_map(|member| match member {
            vespera_core::schema::SchemaRef::Inline(inline) => Some(inline.as_mut()),
            vespera_core::schema::SchemaRef::Ref(_) => None,
        })
}

/// Extract `default` value from `#[schema(default = "...")]` field attribute.
///
/// This attribute is generated by `schema_type!` when converting `sea_orm(default_value)`.
/// It carries the raw default value string for OpenAPI schema generation.
pub(super) fn extract_schema_default_attr(attrs: &[syn::Attribute]) -> Option<String> {
    attrs
        .iter()
        .filter(|attr| attr.path().is_ident("schema"))
        .find_map(|attr| {
            let mut default_value = None;
            let _ = attr.parse_nested_meta(|meta| {
                if meta.path.is_ident("default") {
                    let value = meta.value()?;
                    let lit: syn::LitStr = value.parse()?;
                    default_value = Some(lit.value());
                }
                Ok(())
            });
            default_value
        })
}

/// Parse a default value string into the appropriate `serde_json::Value`.
///
/// Tries to infer the JSON type: integer → number → bool → string (fallback).
pub(super) fn parse_default_string_to_json_value(value: &str) -> serde_json::Value {
    // Try integer first
    if let Ok(n) = value.parse::<i64>() {
        return serde_json::Value::Number(n.into());
    }
    // Try float
    if let Ok(f) = value.parse::<f64>()
        && let Some(n) = serde_json::Number::from_f64(f)
    {
        return serde_json::Value::Number(n);
    }
    // Try bool
    if let Ok(b) = value.parse::<bool>() {
        return serde_json::Value::Bool(b);
    }
    // Fallback to string
    serde_json::Value::String(value.to_string())
}

/// Find a function by name in the file AST
#[cfg(test)]
pub fn find_function_in_file<'a>(
    file_ast: &'a syn::File,
    function_name: &str,
) -> Option<&'a syn::ItemFn> {
    let index = function_index_by_name(file_ast);
    find_function_in_index(&index, function_name)
}

fn function_index_by_name(file_ast: &syn::File) -> HashMap<String, &syn::ItemFn> {
    let mut functions = HashMap::new();
    for item in &file_ast.items {
        if let syn::Item::Fn(fn_item) = item {
            functions
                .entry(fn_item.sig.ident.to_string())
                .or_insert(fn_item);
        }
    }
    functions
}

fn find_function_in_index<'a>(
    function_index: &HashMap<String, &'a syn::ItemFn>,
    function_name: &str,
) -> Option<&'a syn::ItemFn> {
    let local_name = function_name.rsplit("::").next().unwrap_or(function_name);
    function_index.get(local_name).copied()
}

/// Extract default value from function body
/// This tries to extract literal values from common patterns like:
/// - "`value".to_string()` -> "value"
/// - 42 -> 42
/// - true -> true
/// - vec![] -> []
pub fn extract_default_value_from_function(func: &syn::ItemFn) -> Option<serde_json::Value> {
    // Try to find return statement or expression
    for stmt in &func.block.stmts {
        if let syn::Stmt::Expr(expr, _) = stmt {
            // Direct expression (like "value".to_string())
            if let Some(value) = extract_value_from_expr(expr) {
                return Some(value);
            }
            // Or return statement
            if let syn::Expr::Return(ret) = expr
                && let Some(expr) = &ret.expr
                && let Some(value) = extract_value_from_expr(expr)
            {
                return Some(value);
            }
        }
    }

    None
}

/// Extract value from expression
pub(super) fn extract_value_from_expr(expr: &syn::Expr) -> Option<serde_json::Value> {
    use syn::{Expr, ExprLit, ExprMacro, Lit};

    match expr {
        // Literal values
        Expr::Lit(ExprLit { lit, .. }) => match lit {
            Lit::Str(s) => Some(serde_json::Value::String(s.value())),
            Lit::Int(i) => i
                .base10_parse::<i64>()
                .ok()
                .map(|v| serde_json::Value::Number(v.into())),
            Lit::Float(f) => f
                .base10_parse::<f64>()
                .ok()
                .and_then(serde_json::Number::from_f64)
                .map(serde_json::Value::Number),
            Lit::Bool(b) => Some(serde_json::Value::Bool(b.value)),
            _ => None,
        },
        // Method calls like "value".to_string()
        Expr::MethodCall(method_call) => {
            if method_call.method == "to_string" {
                // Get the receiver (the string literal)
                // Try direct match first
                if let Expr::Lit(ExprLit {
                    lit: Lit::Str(s), ..
                }) = method_call.receiver.as_ref()
                {
                    return Some(serde_json::Value::String(s.value()));
                }
                // Try to extract from nested expressions (e.g., if the receiver is wrapped)
                if let Some(value) = extract_value_from_expr(method_call.receiver.as_ref()) {
                    return Some(value);
                }
            }
            None
        }
        // Associated function calls like String::from("value")
        Expr::Call(call) => {
            let Expr::Path(path) = call.func.as_ref() else {
                return None;
            };
            let mut segments = path.path.segments.iter().rev();
            let last = segments.next()?;
            let prev = segments.next()?;
            if last.ident == "from"
                && prev.ident == "String"
                && call.args.len() == 1
                && let Some(first_arg) = call.args.first()
            {
                return extract_value_from_expr(first_arg).and_then(|value| match value {
                    serde_json::Value::String(_) => Some(value),
                    _ => None,
                });
            }
            None
        }
        // Macro calls like vec![...]
        Expr::Macro(ExprMacro { mac, .. }) => {
            if mac.path.is_ident("vec") {
                // `vec![]` → empty array.  `vec![a, b, ...]` → the array of its
                // element values, but ONLY when every element resolves to a
                // literal; otherwise the default is unrepresentable and we
                // return `None` (the field is then demoted from `required`)
                // rather than emitting a WRONG empty `[]` for a non-empty
                // `vec!` (the prior behaviour silently dropped the elements).
                if mac.tokens.is_empty() {
                    return Some(serde_json::Value::Array(Vec::new()));
                }
                return mac
                    .parse_body_with(
                        syn::punctuated::Punctuated::<Expr, syn::Token![,]>::parse_terminated,
                    )
                    .ok()
                    .and_then(|elems| {
                        elems
                            .iter()
                            .map(extract_value_from_expr)
                            .collect::<Option<Vec<_>>>()
                    })
                    .map(serde_json::Value::Array);
            }
            None
        }
        _ => None,
    }
}

#[cfg(test)]
mod tests;