Skip to main content

vld/schema/
nested.rs

1use serde_json::Value;
2use std::marker::PhantomData;
3
4use crate::error::VldError;
5use crate::schema::VldSchema;
6
7/// A `(component_name, json_schema_fn)` pair describing a named nested schema.
8///
9/// Re-exported as `vld::openapi::NestedSchemaEntry`.
10pub type NestedSchemaEntry = (&'static str, fn() -> serde_json::Value);
11
12/// Collector for transitive nested OpenAPI component schemas.
13///
14/// Shared by `NestedSchema` and the `nested!` macro.
15pub type NestedSchemaCollectFn = fn(&mut Vec<NestedSchemaEntry>);
16
17/// Schema for parsing nested structures. Created via [`vld::nested()`](crate::nested())
18/// or the [`vld::nested!`](crate::nested!) macro.
19pub struct NestedSchema<T, F>
20where
21    F: Fn(&Value) -> Result<T, VldError>,
22{
23    parse_fn: F,
24    #[cfg_attr(not(feature = "openapi"), allow(dead_code))]
25    pub(crate) name: Option<&'static str>,
26    /// Returns the full JSON Schema of the nested type (for OpenAPI component registration).
27    #[cfg_attr(not(feature = "openapi"), allow(dead_code))]
28    pub(crate) json_schema_fn: Option<fn() -> serde_json::Value>,
29    /// Collects transitive nested schemas of the nested type into a shared buffer.
30    #[cfg_attr(not(feature = "openapi"), allow(dead_code))]
31    pub(crate) collect_nested_fn: Option<NestedSchemaCollectFn>,
32    _phantom: PhantomData<T>,
33}
34
35impl<T, F> NestedSchema<T, F>
36where
37    F: Fn(&Value) -> Result<T, VldError>,
38{
39    /// Create the schema (the same as its `vld::` constructor function).
40    pub fn new(f: F) -> Self {
41        Self {
42            parse_fn: f,
43            name: None,
44            json_schema_fn: None,
45            collect_nested_fn: None,
46            _phantom: PhantomData,
47        }
48    }
49
50    /// Wrap a schema factory under a name used for `$ref` in JSON Schema.
51    pub fn new_named(
52        f: F,
53        name: &'static str,
54        json_schema_fn: Option<fn() -> serde_json::Value>,
55        collect_nested_fn: Option<NestedSchemaCollectFn>,
56    ) -> Self {
57        Self {
58            parse_fn: f,
59            name: Some(name),
60            json_schema_fn,
61            collect_nested_fn,
62            _phantom: PhantomData,
63        }
64    }
65}
66
67impl<T, F> VldSchema for NestedSchema<T, F>
68where
69    F: Fn(&Value) -> Result<T, VldError>,
70{
71    type Output = T;
72    fn parse_value(&self, value: &Value) -> Result<T, VldError> {
73        (self.parse_fn)(value)
74    }
75}
76
77#[cfg(feature = "openapi")]
78impl<T, F> crate::openapi::JsonSchema for NestedSchema<T, F>
79where
80    F: Fn(&Value) -> Result<T, VldError>,
81{
82    fn json_schema(&self) -> Value {
83        match self.name {
84            Some(name) => serde_json::json!({ "$ref": format!("#/components/schemas/{}", name) }),
85            // Unnamed nested schemas are opaque closures.
86            None => serde_json::json!({ "type": "object" }),
87        }
88    }
89}
90
91#[cfg(feature = "openapi")]
92impl<T, F> crate::openapi::CollectNestedSchemas for NestedSchema<T, F>
93where
94    F: Fn(&Value) -> Result<T, VldError>,
95{
96    fn collect_nested_schemas(&self, out: &mut Vec<NestedSchemaEntry>) {
97        if let (Some(name), Some(f)) = (self.name, self.json_schema_fn) {
98            // Dedup by name: skip cycles and diamond references already visited.
99            if out.iter().any(|(n, _)| *n == name) {
100                return;
101            }
102            out.push((name, f));
103            if let Some(collect) = self.collect_nested_fn {
104                collect(out);
105            }
106        }
107    }
108}