Skip to main content

xyz_rust/spec/
field.rs

1// FieldSpec 是宏生成的静态字段描述(tag 原串 + 类型形状);FieldMeta 是
2// 注册期解析出的运行时元数据(typed 默认值、枚举、校验规则)。两者分离
3// 对应 Go 的「tag 原串在 reflect 里、解析在 Entry 构建期」。
4//
5// 嵌套 struct 的字段树由宏递归生成(XyzField::xyz_spec_of),无需任何
6// 运行时类型分析;递归类型在注册期经深度护栏报错(Go 的 maxAnalyzeDepth
7// 对应物)。
8
9use serde_json::Value;
10
11use crate::errors;
12use crate::spec::validate::{self, VRule};
13
14/// 类型形状(Go reflect.Kind 的对齐物,按 xyz 关心的子集收窄)。
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16pub enum FieldKind {
17    #[default]
18    String,
19    Bool,
20    I8,
21    I16,
22    I32,
23    I64,
24    U8,
25    U16,
26    U32,
27    U64,
28    F32,
29    F64,
30    /// std::time::Duration(Go time.Duration)。
31    Duration,
32    /// chrono::DateTime<Utc>(Go time.Time)。
33    Time,
34    /// Vec<u8>(Go []byte)。
35    Bytes,
36    /// Option<T>(Go *T)。
37    Ptr,
38    /// Vec<T>(Go []T)。
39    Slice,
40    /// 嵌套 struct / 命名 newtype 的归宿。
41    Struct,
42    /// spec §4.7 邻接带标签联合(enum 字段);变体树放 FieldMeta.union。
43    Union,
44}
45
46impl FieldKind {
47    /// JSON Schema 基础类型(OpenAPI 参数表与 schema 生成共用)。
48    pub fn schema_type(&self) -> &'static str {
49        match self {
50            FieldKind::String | FieldKind::Duration | FieldKind::Time | FieldKind::Bytes => {
51                "string"
52            }
53            FieldKind::Bool => "boolean",
54            FieldKind::I8
55            | FieldKind::I16
56            | FieldKind::I32
57            | FieldKind::I64
58            | FieldKind::U8
59            | FieldKind::U16
60            | FieldKind::U32
61            | FieldKind::U64 => "integer",
62            FieldKind::F32 | FieldKind::F64 => "number",
63            FieldKind::Slice => "array",
64            FieldKind::Ptr => "pointer",
65            FieldKind::Struct | FieldKind::Union => "object",
66        }
67    }
68
69    pub fn is_scalar(&self) -> bool {
70        matches!(
71            self,
72            FieldKind::String
73                | FieldKind::Bool
74                | FieldKind::I8
75                | FieldKind::I16
76                | FieldKind::I32
77                | FieldKind::I64
78                | FieldKind::U8
79                | FieldKind::U16
80                | FieldKind::U32
81                | FieldKind::U64
82                | FieldKind::F32
83                | FieldKind::F64
84        )
85    }
86
87    pub fn is_numeric(&self) -> bool {
88        self.is_scalar() && !matches!(self, FieldKind::String | FieldKind::Bool)
89    }
90}
91
92/// 联合的静态树(宏在注册链路上构建;tag/name 是 'static 字面量,
93/// 字段树用 owned 容器——FieldSpec 含 Vec 不可 const,宏以 vec! 生成)。
94#[derive(Debug, Clone)]
95pub struct UnionSpec {
96    /// serde `#[serde(tag = "…")]` 的判别键。
97    pub tag: &'static str,
98    /// 各变体的字段树。
99    pub variants: Vec<UnionVariantSpec>,
100}
101
102#[derive(Debug, Clone)]
103pub struct UnionVariantSpec {
104    pub name: &'static str,
105    pub fields: Vec<FieldSpec>,
106}
107
108/// 联合的运行时树(FieldSpec 解析产物)。
109#[derive(Debug, Clone)]
110pub struct UnionMeta {
111    pub tag: String,
112    pub variants: Vec<UnionVariant>,
113}
114
115#[derive(Debug, Clone)]
116pub struct UnionVariant {
117    pub name: String,
118    /// 变体字段的运行时元数据(嵌套校验/解码用)。
119    pub meta: Vec<FieldMeta>,
120}
121
122/// 静态字段描述(宏按声明序生成的字段树节点)。
123#[derive(Debug, Clone)]
124pub struct FieldSpec {
125    /// Rust 字段名(json:"-" 注入字段的投递键)。
126    pub rust_name: &'static str,
127    /// 线上名。
128    pub json_name: &'static str,
129    pub kind: FieldKind,
130    pub desc: &'static str,
131    pub required: bool,
132    pub secret: bool,
133    /// json:"-" 对应物:不进绑定与 schema(env/header 仍可按 rust_name 注入)。
134    pub skip: bool,
135    /// validate tag 原串(注册期解析)。
136    pub validate_s: &'static str,
137    /// enum tag 原串(注册期解析)。
138    pub enum_s: &'static str,
139    /// default tag 原串(注册期按 kind 解析成 typed Value)。
140    pub default_s: &'static str,
141    /// cli tag 原串(register 期解析:shorthand/positional/hidden/env/-)。
142    pub cli_s: &'static str,
143    /// http location 原串(query/path/header/form/body/空)。
144    pub http_s: &'static str,
145    /// httpName 覆盖(header 名等)。
146    pub http_name: Option<&'static str>,
147    /// Ptr/Slice 的元素形状。
148    pub elem: Option<Box<FieldSpec>>,
149    /// struct 的子字段。
150    pub children: Vec<FieldSpec>,
151    /// 联合的静态树(kind == Union 时填充)。
152    pub union: Option<UnionSpec>,
153}
154
155/// 宏生成结构的元素/裸类型节点(无名字的合成包装)。
156pub fn synthetic(
157    kind: FieldKind,
158    children: Vec<FieldSpec>,
159    elem: Option<Box<FieldSpec>>,
160) -> FieldSpec {
161    FieldSpec {
162        rust_name: "",
163        json_name: "",
164        kind,
165        desc: "",
166        required: false,
167        secret: false,
168        skip: false,
169        validate_s: "",
170        enum_s: "",
171        default_s: "",
172        cli_s: "",
173        http_s: "",
174        http_name: None,
175        elem,
176        children,
177        union: None,
178    }
179}
180
181/// 运行时字段的 CLI 绑定(cli tag / CliFieldHint 合并后的产物)。
182#[derive(Debug, Clone, Default)]
183pub struct CliField {
184    pub shorthand: Option<char>,
185    pub positional: bool,
186    pub hidden: bool,
187    pub skip: bool,
188    pub env_var: Option<String>,
189    /// CLI-only 默认值;覆盖全局 default tag(对 CLI 前端生效)。
190    pub default: Option<Value>,
191}
192
193/// 运行时字段的 HTTP 绑定。
194#[derive(Debug, Clone, Default)]
195pub struct HTTPField {
196    /// ""(未标注,默认 query)| query | path | header | form | body。
197    pub location: String,
198    pub name: String,
199    pub default: Option<Value>,
200}
201
202/// 运行时字段的 MCP 绑定。MCP 的覆盖同时替换 inputSchema 里的 default。
203#[derive(Debug, Clone, Default)]
204pub struct MCPField {
205    pub default: Option<Value>,
206}
207
208/// 运行时字段元数据(FieldSpec 解析 + Define-time hints 合并的产物)。
209#[derive(Debug, Clone, Default)]
210pub struct FieldMeta {
211    pub name: String,
212    pub json_name: String,
213    pub kind: FieldKind,
214    pub description: String,
215    pub required: bool,
216    pub secret: bool,
217    pub validate: String,
218    pub rules: Vec<VRule>,
219    /// 已解析的 typed 枚举值(仅标量)。
220    pub enum_values: Vec<Value>,
221    /// 已解析的 typed 全局默认;None 表示无默认。
222    pub default: Option<Value>,
223    pub skip: bool,
224    pub cli: CliField,
225    pub http: HTTPField,
226    pub mcp: MCPField,
227    /// Ptr/Slice 的元素元数据。
228    pub elem: Option<Box<FieldMeta>>,
229    /// struct 的子字段。
230    pub children: Vec<FieldMeta>,
231    /// 联合的运行时树(kind == Union 时填充;变体字段表)。
232    pub union: Option<UnionMeta>,
233}
234
235/// 宏内自引用的护栏:递归类型(Node 里套 Node)在生成字段树时栈溢出前
236/// 报错。Entry 构建用 catch_unwind 把它转成注册期错误。
237pub const MAX_SPEC_DEPTH: usize = 20;
238
239thread_local! {
240    static SPEC_DEPTH: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
241}
242
243/// 由宏生成的 xyz_spec() 入口调用;超深时报错(注册期被捕获)。
244pub fn spec_depth_guard() {
245    SPEC_DEPTH.with(|d| {
246        let cur = d.get();
247        if cur > MAX_SPEC_DEPTH {
248            panic!(
249                "xyz: argument struct nesting exceeds {} levels (recursive type?)",
250                MAX_SPEC_DEPTH
251            );
252        }
253        d.set(cur + 1);
254    })
255}
256
257/// 解析 FieldSpec 树为运行时 FieldMeta 树。所有 tag 解析错误都在这里
258/// 发生——注册期即报错,而不是运行时悄悄忽略。
259pub fn meta_from_spec(spec: &FieldSpec) -> errors::Result<FieldMeta> {
260    let cli = parse_cli_tag(spec.cli_s)?;
261    let http = parse_http_tag(spec.http_s, spec.http_name)?;
262    let rules = validate::parse_validate_tag(spec.validate_s)?;
263    let default = if spec.default_s.is_empty() {
264        None
265    } else {
266        Some(parse_default(spec, spec.default_s)?)
267    };
268    let enum_values = if spec.enum_s.is_empty() {
269        Vec::new()
270    } else {
271        parse_enum(spec.kind, spec.enum_s)?
272    };
273    let elem = match &spec.elem {
274        Some(e) => Some(Box::new(meta_from_spec(e)?)),
275        None => None,
276    };
277    let children = spec
278        .children
279        .iter()
280        .map(meta_from_spec)
281        .collect::<errors::Result<Vec<_>>>()?;
282    let union = match &spec.union {
283        Some(u) => Some(UnionMeta {
284            tag: u.tag.to_string(),
285            variants: u
286                .variants
287                .iter()
288                .map(|v| {
289                    Ok(UnionVariant {
290                        name: v.name.to_string(),
291                        meta: v
292                            .fields
293                            .iter()
294                            .map(meta_from_spec)
295                            .collect::<errors::Result<Vec<_>>>()?,
296                    })
297                })
298                .collect::<errors::Result<Vec<_>>>()?,
299        }),
300        None => None,
301    };
302    Ok(FieldMeta {
303        name: spec.rust_name.to_string(),
304        json_name: spec.json_name.to_string(),
305        kind: spec.kind,
306        description: spec.desc.to_string(),
307        required: spec.required,
308        secret: spec.secret,
309        validate: spec.validate_s.to_string(),
310        rules,
311        enum_values,
312        default,
313        skip: spec.skip,
314        cli,
315        http,
316        mcp: MCPField::default(),
317        elem,
318        children,
319        union,
320    })
321}
322
323/// 解析 cli tag 原串:shorthand=a, positional, hidden, env=VAR, -。
324pub fn parse_cli_tag(s: &str) -> errors::Result<CliField> {
325    let mut f = CliField::default();
326    if s.trim() == "-" {
327        f.skip = true;
328        return Ok(f);
329    }
330    for tok in s.split(',').map(str::trim).filter(|t| !t.is_empty()) {
331        if tok == "positional" {
332            f.positional = true;
333        } else if tok == "hidden" {
334            f.hidden = true;
335        } else if let Some(sh) = tok.strip_prefix("shorthand=") {
336            let mut chars = sh.chars();
337            match (chars.next(), chars.next()) {
338                (Some(c), None) => f.shorthand = Some(c),
339                _ => {
340                    return Err(errors::Error::new(
341                        errors::Kind::Internal,
342                        format!("cli shorthand must be one character, got {sh:?}"),
343                    ));
344                }
345            }
346        } else if let Some(ev) = tok.strip_prefix("env=") {
347            if ev.is_empty() {
348                return Err(errors::Error::new(
349                    errors::Kind::Internal,
350                    "cli env requires a variable name".to_string(),
351                ));
352            }
353            f.env_var = Some(ev.to_string());
354        } else {
355            return Err(errors::Error::new(
356                errors::Kind::Internal,
357                format!("unknown cli option {tok:?} (want positional|hidden|shorthand=X|env=X|-)"),
358            ));
359        }
360    }
361    Ok(f)
362}
363
364/// 解析 http location(tag 或 hint 共用的校验入口)。
365pub fn valid_http_location(s: &str) -> bool {
366    matches!(s, "" | "query" | "path" | "header" | "form" | "body")
367}
368
369pub fn parse_http_tag(s: &str, http_name: Option<&str>) -> errors::Result<HTTPField> {
370    if !valid_http_location(s) {
371        return Err(errors::Error::new(
372            errors::Kind::Internal,
373            format!("unknown http location {s:?} (want query|path|header|form|body)"),
374        ));
375    }
376    Ok(HTTPField {
377        location: s.to_string(),
378        name: http_name.unwrap_or_default().to_string(),
379        default: None,
380    })
381}
382
383/// 按类型形状把 default tag 原串解析成 typed Value(Go parseDefault 对应物:
384/// 复用与 decodeValue 相同的转换语义,Duration/Time 在注册期先验语法)。
385pub fn parse_default(spec: &FieldSpec, s: &str) -> errors::Result<Value> {
386    match spec.kind {
387        FieldKind::Duration => {
388            crate::spec::scalar::parse_duration(s).map_err(|e| {
389                errors::Error::new(errors::Kind::Internal, format!("bad default {s:?}: {e}"))
390            })?;
391            Ok(Value::String(s.to_string()))
392        }
393        FieldKind::Time => {
394            crate::spec::scalar::parse_datetime(s).map_err(|e| {
395                errors::Error::new(errors::Kind::Internal, format!("bad default {s:?}: {e}"))
396            })?;
397            Ok(Value::String(s.to_string()))
398        }
399        FieldKind::Bytes => Ok(Value::String(s.to_string())),
400        FieldKind::Slice => {
401            let elem_kind = spec.elem.as_ref().map(|e| e.kind).ok_or_else(|| {
402                errors::Error::new(
403                    errors::Kind::Internal,
404                    "slice without elem kind".to_string(),
405                )
406            })?;
407            s.split(',')
408                .map(str::trim)
409                .filter(|p| !p.is_empty())
410                .map(|p| {
411                    crate::spec::scalar::parse_scalar_literal(elem_kind, p).map_err(|e| {
412                        errors::Error::new(errors::Kind::Internal, format!("element {p:?}: {e}"))
413                    })
414                })
415                .collect()
416        }
417        FieldKind::Ptr => match &spec.elem {
418            Some(e) => parse_default(e, s),
419            None => Err(errors::Error::new(
420                errors::Kind::Internal,
421                "pointer without elem kind".to_string(),
422            )),
423        },
424        FieldKind::Struct => Err(errors::Error::new(
425            errors::Kind::Internal,
426            format!("unsupported scalar kind Struct for default {s:?}"),
427        )),
428        _ => crate::spec::scalar::parse_scalar_literal(spec.kind, s).map_err(|e| {
429            errors::Error::new(errors::Kind::Internal, format!("bad default {s:?}: {e}"))
430        }),
431    }
432}
433
434/// 按 kind 把 enum tag 原串解析成 typed Value 列表。仅标量支持枚举
435/// (Go parseEnum 同一约束:Ptr/Struct/Slice/Duration/Time 报错)。
436pub fn parse_enum(kind: FieldKind, s: &str) -> errors::Result<Vec<Value>> {
437    if !matches!(
438        kind,
439        FieldKind::String
440            | FieldKind::Bool
441            | FieldKind::I8
442            | FieldKind::I16
443            | FieldKind::I32
444            | FieldKind::I64
445            | FieldKind::U8
446            | FieldKind::U16
447            | FieldKind::U32
448            | FieldKind::U64
449            | FieldKind::F32
450            | FieldKind::F64
451    ) {
452        return Err(errors::Error::new(
453            errors::Kind::Internal,
454            format!("enum is only supported on scalar fields, got {kind:?}"),
455        ));
456    }
457    let mut out = Vec::new();
458    for p in s.split(',').map(str::trim) {
459        if p.is_empty() {
460            return Err(errors::Error::new(
461                errors::Kind::Internal,
462                "enum contains an empty value".to_string(),
463            ));
464        }
465        let v = crate::spec::scalar::parse_scalar_literal(kind, p)?;
466        out.push(v);
467    }
468    Ok(out)
469}