use serde_json::{Map, Value};
use crate::error::{value_type_name, IssueCode, PathSegment, VldError};
use crate::schema::VldSchema;
pub trait DynSchema {
fn dyn_parse(&self, value: &Value) -> Result<Value, VldError>;
#[cfg(feature = "openapi")]
fn dyn_json_schema(&self) -> Value {
serde_json::json!({})
}
}
impl<T> DynSchema for T
where
T: VldSchema,
T::Output: serde::Serialize,
{
fn dyn_parse(&self, value: &Value) -> Result<Value, VldError> {
let result = self.parse_value(value)?;
serde_json::to_value(&result).map_err(|e| {
VldError::single(
IssueCode::Custom {
code: "serialize".to_string(),
},
format!("Failed to serialize validated value: {}", e),
)
})
}
}
#[cfg(feature = "openapi")]
pub(crate) struct JsonSchemaField<T> {
inner: T,
}
#[cfg(feature = "openapi")]
impl<T> DynSchema for JsonSchemaField<T>
where
T: VldSchema + crate::json_schema::JsonSchema,
T::Output: serde::Serialize,
{
fn dyn_parse(&self, value: &Value) -> Result<Value, VldError> {
let result = self.inner.parse_value(value)?;
serde_json::to_value(&result).map_err(|e| {
VldError::single(
IssueCode::Custom {
code: "serialize".to_string(),
},
format!("Failed to serialize validated value: {}", e),
)
})
}
fn dyn_json_schema(&self) -> Value {
self.inner.json_schema()
}
}
struct ObjectField {
name: String,
schema: Box<dyn DynSchema>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum UnknownFieldMode {
Strip,
Strict,
Passthrough,
}
struct ConditionalRule {
condition_field: String,
condition_value: serde_json::Value,
target_field: String,
schema: Box<dyn DynSchema>,
}
pub struct ZObject {
fields: Vec<ObjectField>,
unknown_mode: UnknownFieldMode,
catchall_schema: Option<Box<dyn DynSchema>>,
conditional_rules: Vec<ConditionalRule>,
min_keys: Option<usize>,
max_keys: Option<usize>,
}
impl ZObject {
pub fn new() -> Self {
Self {
fields: vec![],
unknown_mode: UnknownFieldMode::Strip,
catchall_schema: None,
conditional_rules: vec![],
min_keys: None,
max_keys: None,
}
}
pub fn field<S: DynSchema + 'static>(mut self, name: impl Into<String>, schema: S) -> Self {
self.fields.push(ObjectField {
name: name.into(),
schema: Box::new(schema),
});
self
}
#[cfg(feature = "openapi")]
pub fn field_schema<S>(mut self, name: impl Into<String>, schema: S) -> Self
where
S: VldSchema + crate::json_schema::JsonSchema + 'static,
S::Output: serde::Serialize,
{
self.fields.push(ObjectField {
name: name.into(),
schema: Box::new(JsonSchemaField { inner: schema }),
});
self
}
pub fn field_optional<S: DynSchema + 'static>(
mut self,
name: impl Into<String>,
schema: S,
) -> Self {
self.fields.push(ObjectField {
name: name.into(),
schema: Box::new(OptionalDynSchema(Box::new(schema))),
});
self
}
pub fn strict(mut self) -> Self {
self.unknown_mode = UnknownFieldMode::Strict;
self
}
pub fn strip(mut self) -> Self {
self.unknown_mode = UnknownFieldMode::Strip;
self
}
pub fn passthrough(mut self) -> Self {
self.unknown_mode = UnknownFieldMode::Passthrough;
self
}
pub fn omit(mut self, name: &str) -> Self {
self.fields.retain(|f| f.name != name);
self
}
pub fn pick(mut self, names: &[&str]) -> Self {
self.fields.retain(|f| names.contains(&f.name.as_str()));
self
}
pub fn extend(mut self, other: ZObject) -> Self {
for field in other.fields {
self.fields.retain(|f| f.name != field.name);
self.fields.push(field);
}
self
}
pub fn merge(self, other: ZObject) -> Self {
self.extend(other)
}
pub fn partial(mut self) -> Self {
self.fields = self
.fields
.into_iter()
.map(|f| ObjectField {
name: f.name,
schema: Box::new(OptionalDynSchema(f.schema)),
})
.collect();
self
}
pub fn required(mut self) -> Self {
self.fields = self
.fields
.into_iter()
.map(|f| ObjectField {
name: f.name,
schema: Box::new(RequiredDynSchema(f.schema)),
})
.collect();
self
}
pub fn catchall<S: DynSchema + 'static>(mut self, schema: S) -> Self {
self.catchall_schema = Some(Box::new(schema));
self
}
pub fn when<S: DynSchema + 'static>(
mut self,
condition_field: impl Into<String>,
condition_value: impl Into<serde_json::Value>,
target_field: impl Into<String>,
schema: S,
) -> Self {
self.conditional_rules.push(ConditionalRule {
condition_field: condition_field.into(),
condition_value: condition_value.into(),
target_field: target_field.into(),
schema: Box::new(schema),
});
self
}
pub fn min_keys(mut self, n: usize) -> Self {
self.min_keys = Some(n);
self
}
pub fn max_keys(mut self, n: usize) -> Self {
self.max_keys = Some(n);
self
}
pub fn deep_partial(self) -> Self {
self.partial()
}
pub fn keyof(&self) -> Vec<String> {
self.fields.iter().map(|f| f.name.clone()).collect()
}
#[cfg(feature = "openapi")]
pub fn to_json_schema(&self) -> serde_json::Value {
let required: Vec<String> = self.fields.iter().map(|f| f.name.clone()).collect();
let mut props = serde_json::Map::new();
for f in &self.fields {
props.insert(f.name.clone(), f.schema.dyn_json_schema());
}
let mut schema = serde_json::json!({
"type": "object",
"required": required,
"properties": Value::Object(props),
"additionalProperties": self.unknown_mode != UnknownFieldMode::Strict,
});
if let Some(ref catchall) = self.catchall_schema {
schema["additionalProperties"] = catchall.dyn_json_schema();
}
if let Some(min) = self.min_keys {
schema["minProperties"] = serde_json::json!(min);
}
if let Some(max) = self.max_keys {
schema["maxProperties"] = serde_json::json!(max);
}
schema
}
}
impl Default for ZObject {
fn default() -> Self {
Self::new()
}
}
impl VldSchema for ZObject {
type Output = Map<String, Value>;
fn parse_value(&self, value: &Value) -> Result<Map<String, Value>, VldError> {
let obj = value.as_object().ok_or_else(|| {
VldError::single(
IssueCode::InvalidType {
expected: "object".to_string(),
received: value_type_name(value),
},
format!("Expected object, received {}", value_type_name(value)),
)
})?;
let mut result = Map::new();
let mut errors = VldError::new();
if let Some(min) = self.min_keys {
if obj.len() < min {
errors.push(
IssueCode::TooSmall {
minimum: min as f64,
inclusive: true,
},
format!("Object must contain at least {} keys", min),
);
}
}
if let Some(max) = self.max_keys {
if obj.len() > max {
errors.push(
IssueCode::TooBig {
maximum: max as f64,
inclusive: true,
},
format!("Object must contain at most {} keys", max),
);
}
}
for field in &self.fields {
let field_value = obj.get(&field.name).unwrap_or(&Value::Null);
match field.schema.dyn_parse(field_value) {
Ok(v) => {
result.insert(field.name.clone(), v);
}
Err(e) => {
errors = errors.merge(e.with_prefix(PathSegment::Field(field.name.clone())));
}
}
}
let known_keys: Vec<&str> = self.fields.iter().map(|f| f.name.as_str()).collect();
let unknown_keys: Vec<&String> = obj
.keys()
.filter(|k| !known_keys.contains(&k.as_str()))
.collect();
if let Some(catchall) = &self.catchall_schema {
for key in &unknown_keys {
let val = &obj[key.as_str()];
match catchall.dyn_parse(val) {
Ok(v) => {
result.insert((*key).clone(), v);
}
Err(e) => {
errors = errors.merge(e.with_prefix(PathSegment::Field((*key).clone())));
}
}
}
} else {
match self.unknown_mode {
UnknownFieldMode::Strip => {}
UnknownFieldMode::Strict => {
for key in &unknown_keys {
let mut issue_err = VldError::single(
IssueCode::UnrecognizedField,
format!("Unrecognized field: \"{}\"", key),
);
issue_err = issue_err.with_prefix(PathSegment::Field((*key).clone()));
errors = errors.merge(issue_err);
}
}
UnknownFieldMode::Passthrough => {
for key in &unknown_keys {
result.insert((*key).clone(), obj[key.as_str()].clone());
}
}
}
}
for rule in &self.conditional_rules {
let cond_val = obj.get(&rule.condition_field).unwrap_or(&Value::Null);
if *cond_val == rule.condition_value {
let target_val = obj.get(&rule.target_field).unwrap_or(&Value::Null);
if let Err(e) = rule.schema.dyn_parse(target_val) {
errors =
errors.merge(e.with_prefix(PathSegment::Field(rule.target_field.clone())));
}
}
}
if errors.is_empty() {
Ok(result)
} else {
Err(errors)
}
}
}
struct OptionalDynSchema(Box<dyn DynSchema>);
impl DynSchema for OptionalDynSchema {
fn dyn_parse(&self, value: &Value) -> Result<Value, VldError> {
if value.is_null() {
return Ok(Value::Null);
}
self.0.dyn_parse(value)
}
}
struct RequiredDynSchema(Box<dyn DynSchema>);
impl DynSchema for RequiredDynSchema {
fn dyn_parse(&self, value: &Value) -> Result<Value, VldError> {
if value.is_null() {
return Err(VldError::single(
IssueCode::MissingField,
"Required field is missing or null",
));
}
self.0.dyn_parse(value)
}
}