1use serde_json::Value;
10
11use crate::errors;
12use crate::spec::validate::{self, VRule};
13
14#[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 Duration,
32 Time,
34 Bytes,
36 Ptr,
38 Slice,
40 Struct,
42 Union,
44}
45
46impl FieldKind {
47 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#[derive(Debug, Clone)]
95pub struct UnionSpec {
96 pub tag: &'static str,
98 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#[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 pub meta: Vec<FieldMeta>,
120}
121
122#[derive(Debug, Clone)]
124pub struct FieldSpec {
125 pub rust_name: &'static str,
127 pub json_name: &'static str,
129 pub kind: FieldKind,
130 pub desc: &'static str,
131 pub required: bool,
132 pub secret: bool,
133 pub skip: bool,
135 pub validate_s: &'static str,
137 pub enum_s: &'static str,
139 pub default_s: &'static str,
141 pub cli_s: &'static str,
143 pub http_s: &'static str,
145 pub http_name: Option<&'static str>,
147 pub elem: Option<Box<FieldSpec>>,
149 pub children: Vec<FieldSpec>,
151 pub union: Option<UnionSpec>,
153}
154
155pub 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#[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 pub default: Option<Value>,
191}
192
193#[derive(Debug, Clone, Default)]
195pub struct HTTPField {
196 pub location: String,
198 pub name: String,
199 pub default: Option<Value>,
200}
201
202#[derive(Debug, Clone, Default)]
204pub struct MCPField {
205 pub default: Option<Value>,
206}
207
208#[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 pub enum_values: Vec<Value>,
221 pub default: Option<Value>,
223 pub skip: bool,
224 pub cli: CliField,
225 pub http: HTTPField,
226 pub mcp: MCPField,
227 pub elem: Option<Box<FieldMeta>>,
229 pub children: Vec<FieldMeta>,
231 pub union: Option<UnionMeta>,
233}
234
235pub 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
243pub 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
257pub 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
323pub 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
364pub 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
383pub 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
434pub 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}