use crate::error::{Error, ErrorKind};
use crate::{Meta, Validator};
use tanzim_value::{Value, ValueType};
struct Field {
key: String,
required: bool,
validator: Option<Box<dyn Validator>>,
}
#[derive(Default)]
pub struct StaticMap {
meta: Meta,
fields: Vec<Field>,
deny_unknown: bool,
}
impl StaticMap {
pub fn with_meta(mut self, meta: Meta) -> Self {
self.meta = meta;
self
}
pub fn new() -> Self {
Self {
meta: Meta::default(),
fields: Vec::new(),
deny_unknown: true,
}
}
pub fn required(
mut self,
key: impl Into<String>,
validator: impl Into<Box<dyn Validator>>,
) -> Self {
self.fields.push(Field {
key: key.into(),
required: true,
validator: Some(validator.into()),
});
self
}
pub fn optional(
mut self,
key: impl Into<String>,
validator: impl Into<Box<dyn Validator>>,
) -> Self {
self.fields.push(Field {
key: key.into(),
required: false,
validator: Some(validator.into()),
});
self
}
pub fn required_any(mut self, key: impl Into<String>) -> Self {
self.fields.push(Field {
key: key.into(),
required: true,
validator: None,
});
self
}
pub fn optional_any(mut self, key: impl Into<String>) -> Self {
self.fields.push(Field {
key: key.into(),
required: false,
validator: None,
});
self
}
pub fn deny_unknown(mut self) -> Self {
self.deny_unknown = true;
self
}
pub fn allow_unknown(mut self) -> Self {
self.deny_unknown = false;
self
}
}
crate::impl_meta_methods!(StaticMap);
impl Validator for StaticMap {
fn meta(&self) -> &Meta {
&self.meta
}
fn meta_mut(&mut self) -> &mut Meta {
&mut self.meta
}
fn check(&self, value: &mut Value) -> Result<(), Error> {
let map = match value.map_mut() {
Some(map) => map,
None => {
return Err(Error::new(ErrorKind::Type {
expected: ValueType::Map,
found: value.type_name(),
}));
}
};
for field in &self.fields {
if field.required && !map.contains_key(&field.key) {
return Err(Error::new(ErrorKind::MissingKey {
key: field.key.clone(),
}));
}
}
for field in &self.fields {
if let Some(validator) = &field.validator
&& let Some(entry) = map.get_mut(&field.key)
{
match validator.validate(entry.value_mut()) {
Ok(()) => {}
Err(error) => return Err(error.under_key(&field.key, entry.location())),
}
}
}
if self.deny_unknown {
for (key, entry) in map.entries() {
let mut declared = false;
for field in &self.fields {
if &field.key == key {
declared = true;
break;
}
}
if !declared {
return Err(Error::new(ErrorKind::UnknownKey { key: key.clone() })
.with_location(entry.location()));
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Integer, Str};
use tanzim_value::{LocatedValue, Location, Map};
fn entry(value: Value) -> LocatedValue {
LocatedValue::new(value, Location::at("file", "test", Some(1), Some(1), None))
}
fn map_of(pairs: &[(&str, Value)]) -> Value {
let mut map = Map::new();
for (key, value) in pairs {
map.insert((*key).to_string(), entry(value.clone()));
}
Value::Map(map)
}
#[test]
fn missing_required_key_fails() {
let schema = StaticMap::new().required("host", Str::new());
let mut value = map_of(&[]);
let error = schema.validate(&mut value).unwrap_err();
assert!(matches!(error.kind, ErrorKind::MissingKey { .. }));
}
#[test]
fn optional_absent_is_ok() {
let schema = StaticMap::new().optional("port", Integer::new());
let mut value = map_of(&[]);
assert!(schema.validate(&mut value).is_ok());
}
#[test]
fn value_validator_reports_key_path() {
let schema = StaticMap::new().required("port", Integer::new());
let mut value = map_of(&[("port", Value::String("x".into()))]);
let error = schema.validate(&mut value).unwrap_err();
assert_eq!(error.path.len(), 1);
assert!(matches!(error.kind, ErrorKind::NotConvertible { .. }));
}
#[test]
fn unknown_key_denied_by_default() {
let schema = StaticMap::new().required("host", Str::new());
let mut value = map_of(&[
("host", Value::String("h".into())),
("extra", Value::Int(1)),
]);
let error = schema.validate(&mut value).unwrap_err();
assert!(matches!(error.kind, ErrorKind::UnknownKey { .. }));
}
#[test]
fn unknown_key_allowed_when_opted_in() {
let schema = StaticMap::new()
.required("host", Str::new())
.allow_unknown();
let mut value = map_of(&[
("host", Value::String("h".into())),
("extra", Value::Int(1)),
]);
assert!(schema.validate(&mut value).is_ok());
}
}