vld/schema.rs
1use serde_json::Value;
2use std::marker::PhantomData;
3
4use crate::combinators::{
5 ZCatch, ZDescribe, ZIntersection, ZPipe, ZRefine, ZSuperRefine, ZTransform, ZUnion2,
6};
7use crate::error::VldError;
8use crate::input::VldInput;
9use crate::modifiers::{ZDefault, ZNullable, ZNullish, ZOptional};
10
11/// Core validation schema trait.
12///
13/// Every validator in `vld` implements this trait. The associated type `Output`
14/// defines what Rust type will be produced after successful parsing.
15///
16/// # Example
17/// ```
18/// use vld::prelude::*;
19///
20/// let schema = vld::string().min(3);
21/// let result = schema.parse(r#""hello""#);
22/// assert!(result.is_ok());
23/// ```
24pub trait VldSchema: Sized {
25 /// The Rust type produced by this schema after successful parsing.
26 type Output;
27
28 /// Parse and validate a `serde_json::Value`.
29 fn parse_value(&self, value: &Value) -> Result<Self::Output, VldError>;
30
31 /// Parse from any supported input (JSON string, file path, `serde_json::Value`, etc.)
32 fn parse<I: VldInput + ?Sized>(&self, input: &I) -> Result<Self::Output, VldError> {
33 let json = input.to_json_value()?;
34 self.parse_value(&json)
35 }
36
37 /// Validate an existing Rust value against this schema.
38 ///
39 /// The value is serialized to JSON via `serde`, then validated.
40 /// Returns the parsed output on success.
41 ///
42 /// Requires the `serialize` feature.
43 ///
44 /// # Example
45 /// ```ignore
46 /// use vld::prelude::*;
47 ///
48 /// let schema = vld::array(vld::number().int().positive()).min_len(1);
49 /// let data = vec![1, 2, 3];
50 /// assert!(schema.validate(&data).is_ok());
51 /// ```
52 #[cfg(feature = "serialize")]
53 fn validate<T: serde::Serialize>(&self, value: &T) -> Result<Self::Output, VldError> {
54 let json = serde_json::to_value(value).map_err(|e| {
55 VldError::single(
56 crate::error::IssueCode::ParseError,
57 format!("Serialization error: {}", e),
58 )
59 })?;
60 self.parse_value(&json)
61 }
62
63 /// Check if an existing Rust value passes this schema's validation.
64 ///
65 /// Requires the `serialize` feature.
66 ///
67 /// # Example
68 /// ```ignore
69 /// use vld::prelude::*;
70 ///
71 /// let schema = vld::string().email();
72 /// assert!(schema.is_valid(&"user@example.com"));
73 /// ```
74 #[cfg(feature = "serialize")]
75 fn is_valid<T: serde::Serialize>(&self, value: &T) -> bool {
76 self.validate(value).is_ok()
77 }
78
79 /// Make this field optional. Missing or null values become `None`.
80 fn optional(self) -> ZOptional<Self> {
81 ZOptional::new(self)
82 }
83
84 /// Allow null values. Null becomes `None`.
85 fn nullable(self) -> ZNullable<Self> {
86 ZNullable::new(self)
87 }
88
89 /// Provide a default value when the field is missing or null.
90 fn with_default(self, value: Self::Output) -> ZDefault<Self>
91 where
92 Self::Output: Clone,
93 {
94 ZDefault::new(self, value)
95 }
96
97 /// Add a custom refinement check without changing the output type.
98 fn refine<F>(self, check: F, message: &str) -> ZRefine<Self, F>
99 where
100 F: Fn(&Self::Output) -> bool,
101 {
102 ZRefine::new(self, check, message)
103 }
104
105 /// Transform the output value after successful parsing.
106 fn transform<F, U>(self, f: F) -> ZTransform<Self, F, U>
107 where
108 F: Fn(Self::Output) -> U,
109 {
110 ZTransform::new(self, f)
111 }
112
113 /// Make this field nullish (both optional and nullable).
114 fn nullish(self) -> ZNullish<Self> {
115 ZNullish::new(self)
116 }
117
118 /// Return a fallback value on ANY validation error.
119 fn catch(self, fallback: Self::Output) -> ZCatch<Self>
120 where
121 Self::Output: Clone,
122 {
123 ZCatch::new(self, fallback)
124 }
125
126 /// Chain this schema's output into another schema.
127 ///
128 /// The output of `self` is serialized to JSON, then parsed by `next`.
129 fn pipe<S: VldSchema>(self, next: S) -> ZPipe<Self, S>
130 where
131 Self::Output: serde::Serialize,
132 {
133 ZPipe::new(self, next)
134 }
135
136 /// Attach a human-readable description/label to this schema.
137 ///
138 /// The description is stored as metadata and does not affect validation.
139 fn describe(self, description: &str) -> ZDescribe<Self> {
140 ZDescribe::new(self, description)
141 }
142
143 /// Add a custom refinement that can produce multiple errors.
144 ///
145 /// Unlike `refine()` which returns a single bool, `super_refine` receives
146 /// a mutable `VldError` collector and can push multiple issues.
147 fn super_refine<F>(self, check: F) -> ZSuperRefine<Self, F>
148 where
149 F: Fn(&Self::Output, &mut VldError),
150 {
151 ZSuperRefine::new(self, check)
152 }
153
154 /// Create a union: this schema **or** another. Returns `Either<Self::Output, B::Output>`.
155 fn or<B: VldSchema>(self, other: B) -> ZUnion2<Self, B> {
156 ZUnion2::new(self, other)
157 }
158
159 /// Create an intersection: input must satisfy **both** schemas.
160 fn and<B: VldSchema>(self, other: B) -> ZIntersection<Self, B> {
161 ZIntersection::new(self, other)
162 }
163
164 /// Override the error message for this schema.
165 ///
166 /// On validation failure **all** issues produced by the inner schema
167 /// will have their message replaced with the provided string.
168 ///
169 /// Similar to Zod's `.message("...")`.
170 ///
171 /// # Example
172 /// ```
173 /// use vld::prelude::*;
174 ///
175 /// let schema = vld::string().min(3).message("Too short");
176 /// let err = schema.parse(r#""ab""#).unwrap_err();
177 /// assert_eq!(err.issues[0].message, "Too short");
178 /// ```
179 fn message(self, msg: impl Into<String>) -> crate::combinators::ZMessage<Self> {
180 crate::combinators::ZMessage::new(self, msg)
181 }
182}
183
184/// Trait for types that can be parsed from a `serde_json::Value`.
185///
186/// Auto-implemented by the [`schema!`](crate::schema!) macro and
187/// [`#[derive(Validate)]`](crate::Validate) derive macro.
188///
189/// Used by framework integration crates (e.g., `vld-axum`, `vld-actix`)
190/// to provide type-safe request body extraction.
191pub trait VldParse: Sized {
192 /// Parse and validate a `serde_json::Value` into this type.
193 fn vld_parse_value(value: &serde_json::Value) -> Result<Self, crate::error::VldError>;
194}
195
196/// Schema for parsing nested structures. Created via [`vld::nested()`](crate::nested)
197/// or the [`vld::nested!`](crate::nested!) macro.
198pub struct NestedSchema<T, F>
199where
200 F: Fn(&Value) -> Result<T, VldError>,
201{
202 parse_fn: F,
203 #[allow(dead_code)]
204 pub(crate) name: Option<&'static str>,
205 /// Returns the full JSON Schema of the nested type (for OpenAPI component registration).
206 #[allow(dead_code)]
207 pub(crate) json_schema_fn: Option<fn() -> serde_json::Value>,
208 _phantom: PhantomData<T>,
209}
210
211impl<T, F> NestedSchema<T, F>
212where
213 F: Fn(&Value) -> Result<T, VldError>,
214{
215 pub fn new(f: F) -> Self {
216 Self {
217 parse_fn: f,
218 name: None,
219 json_schema_fn: None,
220 _phantom: PhantomData,
221 }
222 }
223
224 pub fn new_named(
225 f: F,
226 name: &'static str,
227 json_schema_fn: Option<fn() -> serde_json::Value>,
228 ) -> Self {
229 Self {
230 parse_fn: f,
231 name: Some(name),
232 json_schema_fn,
233 _phantom: PhantomData,
234 }
235 }
236}
237
238impl<T, F> VldSchema for NestedSchema<T, F>
239where
240 F: Fn(&Value) -> Result<T, VldError>,
241{
242 type Output = T;
243 fn parse_value(&self, value: &Value) -> Result<T, VldError> {
244 (self.parse_fn)(value)
245 }
246}