use serde_json::{Map, Value};
use crate::error::{IssueCode, PathSegment, SizeOrigin, VldError};
use crate::schema::{DynSchema, VldSchema};
use super::field::{OptionalDynSchema, RequiredDynSchema};
struct ObjectField {
name: String,
schema: Box<dyn DynSchema + Send + Sync>,
}
#[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 + Send + Sync>,
}
pub struct ZObject {
fields: Vec<ObjectField>,
unknown_mode: UnknownFieldMode,
catchall_schema: Option<Box<dyn DynSchema + Send + Sync>>,
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,
}
}
fn put(mut self, field: ObjectField) -> Self {
match self.fields.iter_mut().find(|f| f.name == field.name) {
Some(existing) => *existing = field,
None => self.fields.push(field),
}
self
}
pub fn field<S: DynSchema + Send + Sync + 'static>(
self,
name: impl Into<String>,
schema: S,
) -> Self {
self.put(ObjectField {
name: name.into(),
schema: Box::new(schema),
})
}
pub fn field_optional<S: DynSchema + Send + Sync + 'static>(
self,
name: impl Into<String>,
schema: S,
) -> Self {
self.put(ObjectField {
name: name.into(),
schema: Box::new(OptionalDynSchema(Box::new(schema))),
})
}
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);
}
if other.unknown_mode != UnknownFieldMode::Strip {
self.unknown_mode = other.unknown_mode;
}
self.catchall_schema = other.catchall_schema.or(self.catchall_schema);
self.conditional_rules.extend(other.conditional_rules);
self.min_keys = other.min_keys.or(self.min_keys);
self.max_keys = other.max_keys.or(self.max_keys);
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 + Send + Sync + 'static>(mut self, schema: S) -> Self {
self.catchall_schema = Some(Box::new(schema));
self
}
pub fn when<S: DynSchema + Send + Sync + '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 keyof(&self) -> Vec<String> {
self.fields.iter().map(|f| f.name.clone()).collect()
}
}
impl ZObject {
#[cfg(feature = "openapi")]
#[cfg_attr(docsrs, doc(cfg(feature = "openapi")))]
pub fn to_json_schema(&self) -> Value {
crate::openapi::JsonSchema::json_schema(self)
}
}
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 = crate::error::expect_object(value)?;
let mut result = Map::new();
let mut errors = VldError::new();
let mut failed = false;
if let Some(min) = self.min_keys {
if obj.len() < min {
errors.push_code(IssueCode::TooSmall {
origin: SizeOrigin::Object,
minimum: min.into(),
inclusive: true,
});
}
}
if let Some(max) = self.max_keys {
if obj.len() > max {
errors.push_code(IssueCode::TooBig {
origin: SizeOrigin::Object,
maximum: max.into(),
inclusive: true,
});
}
}
for field in &self.fields {
let parsed = match obj.get(&field.name) {
Some(v) => field.schema.dyn_parse(v),
None => field
.schema
.dyn_parse_missing()
.map_err(crate::error::absent_field_error),
};
match parsed {
Ok(v) => {
result.insert(field.name.clone(), v);
}
Err(e) => {
failed = true;
errors = errors.merge(e.with_prefix(PathSegment::Field(field.name.clone())));
}
}
}
let known_keys: std::collections::HashSet<&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) => {
failed = true;
errors = errors.merge(e.with_prefix(PathSegment::Field((*key).clone())));
}
}
}
} else {
match self.unknown_mode {
UnknownFieldMode::Strip => {}
UnknownFieldMode::Strict => {
let is_target =
|k: &str| self.conditional_rules.iter().any(|r| r.target_field == k);
for key in unknown_keys.iter().filter(|k| !is_target(k)) {
errors = errors.merge(
VldError::from_code(IssueCode::UnrecognizedField)
.with_prefix(PathSegment::Field((*key).clone())),
);
}
}
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) {
failed = true;
errors =
errors.merge(e.with_prefix(PathSegment::Field(rule.target_field.clone())));
}
}
}
if errors.is_empty() && !failed {
Ok(result)
} else {
Err(errors)
}
}
}
#[cfg(feature = "openapi")]
impl crate::openapi::JsonSchema for ZObject {
fn json_schema(&self) -> Value {
let required: Vec<&str> = self
.fields
.iter()
.filter(|f| f.schema.dyn_is_required())
.map(|f| f.name.as_str())
.collect();
let props: Map<String, Value> = self
.fields
.iter()
.map(|f| (f.name.clone(), f.schema.dyn_json_schema()))
.collect();
let mut schema = serde_json::json!({
"type": "object",
"properties": props,
"additionalProperties": self.unknown_mode != UnknownFieldMode::Strict,
});
if !required.is_empty() {
schema["required"] = serde_json::json!(required);
}
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
}
}
#[cfg(feature = "openapi")]
impl crate::openapi::CollectNestedSchemas for ZObject {
fn collect_nested_schemas(&self, out: &mut Vec<crate::schema::NestedSchemaEntry>) {
let fields = self.fields.iter().map(|f| &f.schema);
let rules = self.conditional_rules.iter().map(|r| &r.schema);
for s in fields.chain(rules).chain(self.catchall_schema.iter()) {
s.dyn_collect_nested_schemas(out);
}
}
}