1use serde_json::Value;
42use std::sync::Arc;
43
44use crate::error::{IssueCode, ValidationIssue, VldError};
45
46pub trait Rule<T: ?Sized>: Send + Sync + 'static {
52 fn key(&self) -> &str;
55
56 fn check(&self, value: &T) -> bool;
58
59 fn message(&self) -> Option<String> {
62 None
63 }
64
65 #[cfg(feature = "openapi")]
68 fn json_schema(&self) -> Option<Value> {
69 None
70 }
71}
72
73pub(crate) fn failure<T: ?Sized, R: Rule<T> + ?Sized>(
76 rule: &R,
77 msg: Option<String>,
78) -> ValidationIssue {
79 let code = IssueCode::Custom {
80 code: rule.key().to_string(),
81 };
82 let msg = msg.or_else(|| rule.message());
83 ValidationIssue::with_override(code, msg.as_ref())
84}
85
86pub fn from_fn<T: ?Sized, F>(key: impl Into<String>, check: F) -> FnRule<F>
88where
89 F: Fn(&T) -> bool + Send + Sync + 'static,
90{
91 FnRule {
92 key: key.into(),
93 check,
94 message: None,
95 #[cfg(feature = "openapi")]
96 schema: None,
97 }
98}
99
100pub struct FnRule<F> {
102 key: String,
103 check: F,
104 message: Option<String>,
105 #[cfg(feature = "openapi")]
106 schema: Option<Value>,
107}
108
109impl<F> FnRule<F> {
110 pub fn message(mut self, msg: impl Into<String>) -> Self {
112 self.message = Some(msg.into());
113 self
114 }
115
116 #[cfg(feature = "openapi")]
118 pub fn json_schema(mut self, keywords: Value) -> Self {
119 self.schema = Some(keywords);
120 self
121 }
122}
123
124impl<T: ?Sized, F> Rule<T> for FnRule<F>
125where
126 F: Fn(&T) -> bool + Send + Sync + 'static,
127{
128 fn key(&self) -> &str {
129 &self.key
130 }
131 fn check(&self, value: &T) -> bool {
132 (self.check)(value)
133 }
134 fn message(&self) -> Option<String> {
135 self.message.clone()
136 }
137 #[cfg(feature = "openapi")]
138 fn json_schema(&self) -> Option<Value> {
139 self.schema.clone()
140 }
141}
142
143impl<T: ?Sized, R: Rule<T> + ?Sized> Rule<T> for Arc<R> {
144 fn key(&self) -> &str {
145 (**self).key()
146 }
147 fn check(&self, value: &T) -> bool {
148 (**self).check(value)
149 }
150 fn message(&self) -> Option<String> {
151 (**self).message()
152 }
153 #[cfg(feature = "openapi")]
154 fn json_schema(&self) -> Option<Value> {
155 (**self).json_schema()
156 }
157}
158
159#[cfg(feature = "regex")]
167#[cfg_attr(docsrs, doc(cfg(feature = "regex")))]
168#[derive(Clone)]
169pub struct Pattern(regex_lite::Regex);
170
171#[cfg(feature = "regex")]
172impl Pattern {
173 pub fn new(pattern: &str) -> Result<Self, PatternError> {
175 regex_lite::Regex::new(pattern)
176 .map(Self)
177 .map_err(|e| PatternError(e.to_string()))
178 }
179
180 pub fn as_str(&self) -> &str {
182 self.0.as_str()
183 }
184
185 pub fn is_match(&self, s: &str) -> bool {
187 self.0.is_match(s)
188 }
189}
190
191#[cfg(feature = "regex")]
192impl std::fmt::Debug for Pattern {
193 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
194 f.debug_tuple("Pattern").field(&self.as_str()).finish()
195 }
196}
197
198#[cfg(feature = "regex")]
201#[cfg_attr(docsrs, doc(cfg(feature = "regex")))]
202pub trait IntoPattern {
203 fn into_pattern(self) -> Pattern;
205}
206
207#[cfg(feature = "regex")]
208impl IntoPattern for Pattern {
209 fn into_pattern(self) -> Pattern {
210 self
211 }
212}
213
214#[cfg(feature = "regex")]
215impl IntoPattern for &str {
216 #[track_caller]
217 fn into_pattern(self) -> Pattern {
218 Pattern::new(self).unwrap_or_else(|e| panic!("vld: {e} (`{self}`)"))
219 }
220}
221
222#[cfg(feature = "regex")]
223impl IntoPattern for String {
224 #[track_caller]
225 fn into_pattern(self) -> Pattern {
226 self.as_str().into_pattern()
227 }
228}
229
230#[cfg(feature = "regex")]
232#[cfg_attr(docsrs, doc(cfg(feature = "regex")))]
233#[derive(Debug, Clone, PartialEq, Eq)]
234pub struct PatternError(String);
235
236#[cfg(feature = "regex")]
237impl std::fmt::Display for PatternError {
238 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
239 write!(f, "invalid pattern: {}", self.0)
240 }
241}
242
243#[cfg(feature = "regex")]
244impl std::error::Error for PatternError {}
245
246pub struct ZRule<S, R> {
249 inner: S,
250 rule: R,
251 msg: Option<String>,
252}
253
254impl<S, R> ZRule<S, R> {
255 pub(crate) fn new(inner: S, rule: R, msg: Option<String>) -> Self {
256 Self { inner, rule, msg }
257 }
258}
259
260impl<S: crate::schema::VldSchema, R: Rule<S::Output>> ZRule<S, R> {
261 fn finish(&self, out: S::Output) -> Result<S::Output, VldError> {
262 if self.rule.check(&out) {
263 Ok(out)
264 } else {
265 Err(failure::<S::Output, R>(&self.rule, self.msg.clone()).into())
266 }
267 }
268}
269
270impl<S, R> crate::schema::VldSchema for ZRule<S, R>
271where
272 S: crate::schema::VldSchema,
273 R: Rule<S::Output>,
274{
275 type Output = S::Output;
276
277 fn parse_value(&self, value: &Value) -> Result<S::Output, VldError> {
278 self.finish(self.inner.parse_value(value)?)
279 }
280
281 fn parse_missing(&self) -> Result<S::Output, VldError> {
282 self.finish(self.inner.parse_missing()?)
283 }
284
285 fn output_json(&self, o: &Self::Output) -> Option<Value> {
286 self.inner.output_json(o)
287 }
288}
289
290#[cfg(feature = "openapi")]
291impl<S, R> crate::openapi::JsonSchema for ZRule<S, R>
292where
293 S: crate::openapi::JsonSchema,
294 R: Rule<<S as crate::schema::VldSchema>::Output>,
295 S: crate::schema::VldSchema,
296{
297 fn json_schema(&self) -> Value {
298 let mut schema = self.inner.json_schema();
299 if let Some(extra) = self.rule.json_schema() {
300 merge_keywords(&mut schema, extra);
301 }
302 schema
303 }
304}
305
306#[cfg(feature = "openapi")]
307impl<S: crate::openapi::CollectNestedSchemas, R> crate::openapi::CollectNestedSchemas
308 for ZRule<S, R>
309{
310 fn collect_nested_schemas(&self, out: &mut Vec<crate::openapi::NestedSchemaEntry>) {
311 self.inner.collect_nested_schemas(out);
312 }
313}
314
315#[cfg(feature = "openapi")]
318pub(crate) fn merge_keywords(schema: &mut Value, extra: Value) {
319 let (Some(base), Value::Object(extra)) = (schema.as_object_mut(), extra) else {
320 return;
321 };
322 let mut clash = serde_json::Map::new();
323 for (k, v) in extra {
324 if base.contains_key(&k) {
325 clash.insert(k, v);
326 } else {
327 base.insert(k, v);
328 }
329 }
330 if !clash.is_empty() {
331 let all_of = base
332 .entry("allOf")
333 .or_insert_with(|| Value::Array(Vec::new()));
334 if let Value::Array(items) = all_of {
335 items.push(Value::Object(clash));
336 }
337 }
338}