use super::object::{Field, Object, field, object};
use super::string::{StringDecoder, string};
use crate::decoder::Decoder;
use crate::issue::{Issue, Issues};
use crate::path::Path;
use crate::{codes, message_keys};
use serde_json::Value;
use std::borrow::Cow;
pub fn enum_of<'a, T: Clone>(variants: impl IntoIterator<Item = (&'a str, T)>) -> EnumOf<T> {
let variants: Vec<(String, T)> = variants
.into_iter()
.map(|(name, value)| (name.to_ascii_lowercase(), value))
.collect();
for (i, (name, _)) in variants.iter().enumerate() {
if variants[..i].iter().any(|(earlier, _)| earlier == name) {
panic!("enum names equal to '{name}' under ASCII case folding appear twice");
}
}
let mut allowed: Vec<String> = variants.iter().map(|(name, _)| name.clone()).collect();
allowed.sort();
allowed.dedup();
EnumOf { variants, allowed }
}
#[derive(Clone, Debug)]
pub struct EnumOf<T> {
variants: Vec<(String, T)>,
allowed: Vec<String>,
}
impl<T: Clone> Decoder<Value> for EnumOf<T> {
type Output = T;
fn decode_at(&self, input: &Value, path: &Path<'_>) -> Result<T, Issues> {
let name = string().decode_at(input, path)?.to_ascii_lowercase();
self.variants
.iter()
.find(|(candidate, _)| *candidate == name)
.map(|(_, value)| value.clone())
.ok_or_else(|| {
Issue::at_path(path, codes::INVALID_FORMAT)
.with_message_key(message_keys::INVALID_FORMAT_ENUM)
.with_meta("allowed", self.allowed.clone())
.into()
})
}
}
pub fn literal(expected: impl Into<String>) -> Literal {
Literal(expected.into())
}
#[derive(Clone, Debug)]
pub struct Literal(String);
impl Decoder<Value> for Literal {
type Output = String;
fn decode_at(&self, input: &Value, path: &Path<'_>) -> Result<String, Issues> {
let found = string().decode_at(input, path)?;
if found == self.0 {
Ok(found)
} else {
Err(Issue::at_path(path, codes::INVALID_FORMAT)
.with_message_key(message_keys::INVALID_FORMAT_LITERAL)
.with_meta("expected", self.0.clone())
.into())
}
}
}
pub fn discriminate<V: Variants>(
tag_field: impl Into<Cow<'static, str>>,
variants: V,
) -> Discriminate<V> {
let tag_field = tag_field.into();
let tags = variants.tags();
for (i, tag) in tags.iter().enumerate() {
if tags[..i].contains(tag) {
panic!("duplicate variant tag '{tag}'");
}
}
let mut allowed: Vec<String> = tags.into_iter().map(str::to_owned).collect();
allowed.sort();
Discriminate {
tag: object((field(tag_field.clone(), string()),)),
tag_field,
variants,
allowed,
}
}
#[derive(Clone, Debug)]
pub struct Discriminate<V> {
tag: Object<(Field<StringDecoder>,)>,
tag_field: Cow<'static, str>,
variants: V,
allowed: Vec<String>,
}
impl<V: Variants> Decoder<Value> for Discriminate<V> {
type Output = V::Output;
fn decode_at(&self, input: &Value, path: &Path<'_>) -> Result<V::Output, Issues> {
let (tag,) = self.tag.decode_at(input, path)?;
self.variants
.decode_variant(&tag, input, path)
.unwrap_or_else(|| {
Err(
Issue::at_path(&path.key(&self.tag_field), codes::NOT_ALLOWED)
.with_meta("allowed", self.allowed.clone())
.into(),
)
})
}
}
pub fn variant<D>(tag: impl Into<Cow<'static, str>>, decoder: D) -> Variant<D> {
Variant {
tag: tag.into(),
decoder,
}
}
#[derive(Clone, Debug)]
pub struct Variant<D> {
tag: Cow<'static, str>,
decoder: D,
}
mod sealed {
pub trait Sealed {}
}
pub trait Variants: sealed::Sealed {
type Output;
#[doc(hidden)]
fn tags(&self) -> Vec<&str>;
#[doc(hidden)]
fn decode_variant(
&self,
tag: &str,
input: &Value,
path: &Path<'_>,
) -> Option<Result<Self::Output, Issues>>;
}
macro_rules! variants {
($First:ident $first:tt $(, $T:ident $idx:tt)*) => {
impl<$First, $($T),*> sealed::Sealed for (Variant<$First>, $(Variant<$T>,)*) {}
impl<$First: Decoder<Value>, $($T: Decoder<Value, Output = $First::Output>),*> Variants
for (Variant<$First>, $(Variant<$T>,)*)
{
type Output = $First::Output;
fn tags(&self) -> Vec<&str> {
vec![&self.$first.tag $(, &self.$idx.tag)*]
}
fn decode_variant(
&self,
tag: &str,
input: &Value,
path: &Path<'_>,
) -> Option<Result<Self::Output, Issues>> {
if self.$first.tag == tag {
return Some(self.$first.decoder.decode_at(input, path));
}
$(
if self.$idx.tag == tag {
return Some(self.$idx.decoder.decode_at(input, path));
}
)*
None
}
}
};
}
variants!(A 0);
variants!(A 0, B 1);
variants!(A 0, B 1, C 2);
variants!(A 0, B 1, C 2, D 3);
variants!(A 0, B 1, C 2, D 3, E 4);
variants!(A 0, B 1, C 2, D 3, E 4, F 5);
variants!(A 0, B 1, C 2, D 3, E 4, F 5, G 6);
variants!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7);
variants!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, J 8);
variants!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, J 8, K 9);
variants!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, J 8, K 9, L 10);
variants!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, J 8, K 9, L 10, M 11);
variants!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, J 8, K 9, L 10, M 11, N 12);
variants!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, J 8, K 9, L 10, M 11, N 12, O 13);
variants!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, J 8, K 9, L 10, M 11, N 12, O 13, P 14);
variants!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, J 8, K 9, L 10, M 11, N 12, O 13, P 14, Q 15);
#[cfg(test)]
mod tests {
use super::*;
use crate::json::i64;
use serde_json::json;
fn shape() -> impl Decoder<Value, Output = i64> {
discriminate(
"kind",
(
variant("square", object((field("side", i64()),)).map(|(s,)| s * s)),
variant(
"rect",
object((field("w", i64()), field("h", i64()))).map(|(w, h)| w * h),
),
),
)
}
fn first(result: Result<i64, Issues>) -> Issue {
result.unwrap_err().into_iter().next().unwrap()
}
#[test]
fn the_tag_picks_the_variant() {
assert_eq!(
shape()
.decode(&json!({"kind": "rect", "w": 2, "h": 3}))
.unwrap(),
6
);
}
#[test]
fn an_unknown_tag_is_not_allowed_at_the_tag_path() {
let issue = first(shape().decode(&json!({"kind": "circle"})));
assert_eq!(issue.code(), "not_allowed");
assert_eq!(issue.path().to_string(), "/kind");
assert_eq!(issue.meta()["allowed"], json!(["rect", "square"]));
assert_eq!(issue.message(), "must be one of [rect, square]");
}
#[test]
fn a_missing_tag_is_required_and_a_non_object_is_rejected_once() {
assert_eq!(first(shape().decode(&json!({}))).code(), "required");
let issue = first(shape().decode(&json!("rect")));
assert_eq!(issue.code(), "type_mismatch");
assert!(issue.path().is_root());
}
#[test]
#[should_panic(expected = "duplicate variant tag 'a'")]
fn duplicate_tags_panic() {
discriminate("t", (variant("a", i64()), variant("a", i64())));
}
#[test]
fn enum_names_fold_ascii_case_only() {
let decoder = enum_of([("red", 1), ("straße", 2), ("K", 3)]);
assert_eq!(decoder.decode(&json!("RED")).unwrap(), 1);
assert!(decoder.decode(&json!("STRASSE")).is_err());
assert_eq!(decoder.decode(&json!("Straße")).unwrap(), 2);
assert!(decoder.decode(&json!("\u{212a}")).is_err());
let issue = decoder
.decode(&json!("blue"))
.unwrap_err()
.into_iter()
.next()
.unwrap();
assert_eq!(issue.meta()["allowed"], json!(["k", "red", "straße"]));
}
#[test]
#[should_panic(expected = "under ASCII case folding appear twice")]
fn enum_names_equal_under_folding_panic() {
enum_of([("a", 1), ("A", 2)]);
}
#[test]
fn literal_requires_the_exact_string() {
let issues = literal("v1").decode(&json!("v2")).unwrap_err();
let issue = issues.iter().next().unwrap();
assert_eq!(issue.meta()["expected"], "v1");
assert_eq!(issue.message(), "invalid value");
}
}