Skip to content
Merged
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 178 additions & 2 deletions rust/routee-compass-core/src/config/one_or_many.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,73 @@
use serde::de::{self, Deserializer, MapAccess, SeqAccess, Visitor};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::marker::PhantomData;

/// Helper type that can deserialize either a single item or a vector of items
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(untagged)]
#[derive(Serialize, Clone, Debug)]
pub enum OneOrMany<T: Clone> {
/// first attempt: this is a Vector of T
Many(Vec<T>),
/// second attempt: this is a single instance of T
One(T),
Comment thread
robfitzgerald marked this conversation as resolved.
}

struct OneOrManyVisitor<T>(PhantomData<T>);

impl<'de, T: Clone + Deserialize<'de>> Visitor<'de> for OneOrManyVisitor<T> {
type Value = OneOrMany<T>;

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a single item or a sequence of items")
}

fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
let mut items = Vec::new();
while let Some(item) = seq.next_element::<T>()? {
items.push(item);
}
Ok(OneOrMany::Many(items))
}

fn visit_map<A: MapAccess<'de>>(self, map: A) -> Result<Self::Value, A::Error> {
T::deserialize(de::value::MapAccessDeserializer::new(map)).map(OneOrMany::One)
}

fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
T::deserialize(de::value::StrDeserializer::new(v)).map(OneOrMany::One)
}

fn visit_string<E: de::Error>(self, v: String) -> Result<Self::Value, E> {
T::deserialize(de::value::StringDeserializer::new(v)).map(OneOrMany::One)
}

fn visit_bool<E: de::Error>(self, v: bool) -> Result<Self::Value, E> {
T::deserialize(de::value::BoolDeserializer::new(v)).map(OneOrMany::One)
}

fn visit_i64<E: de::Error>(self, v: i64) -> Result<Self::Value, E> {
T::deserialize(de::value::I64Deserializer::new(v)).map(OneOrMany::One)
}

fn visit_u64<E: de::Error>(self, v: u64) -> Result<Self::Value, E> {
T::deserialize(de::value::U64Deserializer::new(v)).map(OneOrMany::One)
}

fn visit_f64<E: de::Error>(self, v: f64) -> Result<Self::Value, E> {
T::deserialize(de::value::F64Deserializer::new(v)).map(OneOrMany::One)
}

fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
T::deserialize(de::value::UnitDeserializer::new()).map(OneOrMany::One)
}
}

impl<'de, T: Clone + Deserialize<'de>> Deserialize<'de> for OneOrMany<T> {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_any(OneOrManyVisitor(PhantomData))
}
}

impl<T: Clone> OneOrMany<T> {
/// Convert to a vector, regardless of whether it was originally one item or many
pub fn into_vec(self) -> Vec<T> {
Expand Down Expand Up @@ -58,3 +116,121 @@ impl<T: Clone> OneOrMany<T> {
}
}
}

#[cfg(test)]
mod tests {
use super::*;

/// this type is used in OneOrMany<T> tests as the provided T type.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
struct Item {
name: String,
value: u32,
}

impl Item {
/// create an item.
fn new(name: &str, value: u32) -> Item {
Item {
name: name.to_string(),
value,
}
}
}

#[test]
fn test_deserialize_one() {
let json = r#"{"name": "foo", "value": 42}"#;
let result: OneOrMany<Item> = serde_json::from_str(json).unwrap();
match result {
OneOrMany::One(i) => assert_eq!(i, Item::new("foo", 42)),
OneOrMany::Many(_) => panic!("expected One"),
}
}

#[test]
fn test_deserialize_many() {
let json = r#"[{"name": "foo", "value": 1}, {"name": "bar", "value": 2}]"#;
let result: OneOrMany<Item> = serde_json::from_str(json).unwrap();
match result {
OneOrMany::Many(items) => {
assert_eq!(items.len(), 2);
assert_eq!(items[0], Item::new("foo", 1));
assert_eq!(items[1], Item::new("bar", 2));
}
OneOrMany::One(_) => panic!("expected Many"),
}
}

#[test]
fn test_deserialize_many_single_element() {
// A JSON array with one element should still produce Many
let json = r#"[{"name": "foo", "value": 1}]"#;
let result: OneOrMany<Item> = serde_json::from_str(json).unwrap();
match result {
OneOrMany::Many(items) => assert_eq!(items.len(), 1),
OneOrMany::One(_) => panic!("expected Many"),
}
}

#[test]
fn test_deserialize_empty_array() {
let json = r#"[]"#;
let result: OneOrMany<Item> = serde_json::from_str(json).unwrap();
match result {
OneOrMany::Many(items) => assert!(items.is_empty()),
OneOrMany::One(_) => panic!("expected Many"),
}
}

#[test]
fn test_error_missing_field_in_one() {
// "value" field is missing — error should name the missing field, not say "untagged enum"
let json = r#"{"name": "foo"}"#;
let err = serde_json::from_str::<OneOrMany<Item>>(json)
.unwrap_err()
.to_string();
assert!(
err.contains("value"),
"expected error to mention the missing field 'value', got: {err}"
);
}

#[test]
fn test_error_wrong_type_in_one() {
// "value" is a string instead of u32
let json = r#"{"name": "foo", "value": "not-a-number"}"#;
let err = serde_json::from_str::<OneOrMany<Item>>(json)
.unwrap_err()
.to_string();
assert!(
err.contains("expected u32"),
"inner error message not captured"
);
}

#[test]
fn test_error_missing_field_in_many_element() {
// Second element in array is missing "value" — error should propagate from element deserialization
let json = r#"[{"name": "foo", "value": 1}, {"name": "bar"}]"#;
let err = serde_json::from_str::<OneOrMany<Item>>(json)
.unwrap_err()
.to_string();
assert!(
err.contains("value"),
"expected error to mention the missing field 'value', got: {err}"
);
}

#[test]
fn test_into_vec_one() {
let one: OneOrMany<Item> = OneOrMany::One(Item::new("a", 1));
assert_eq!(one.into_vec(), vec![Item::new("a", 1)]);
}

#[test]
fn test_into_vec_many() {
let many: OneOrMany<Item> = OneOrMany::Many(vec![Item::new("a", 1), Item::new("b", 2)]);
assert_eq!(many.into_vec(), vec![Item::new("a", 1), Item::new("b", 2)]);
}
}
Loading