use crate::error::Error;
use crate::value::Value;
use std::collections::BTreeMap;
pub trait Adapt<T>: Sized {
fn adapt(value: T) -> Result<Self, Error>;
}
type PipelineStep = Box<dyn Fn(Value) -> Result<Value, Error>>;
#[derive(Default)]
pub struct Pipeline {
steps: Vec<PipelineStep>,
}
impl Pipeline {
pub fn new() -> Self {
Self::default()
}
pub fn step(mut self, f: impl Fn(Value) -> Result<Value, Error> + 'static) -> Self {
self.steps.push(Box::new(f));
self
}
pub fn run(&self, input: Value) -> Result<Value, Error> {
let mut current = input;
for step in &self.steps {
current = step(current)?;
}
Ok(current)
}
}
#[derive(Default)]
pub struct FieldMapper {
mappings: BTreeMap<String, String>,
}
impl FieldMapper {
pub fn new() -> Self {
Self::default()
}
pub fn map(mut self, from: &str, to: &str) -> Self {
self.mappings.insert(from.to_string(), to.to_string());
self
}
pub fn apply(&self, value: &Value) -> Result<Value, Error> {
match value.as_object() {
Some(obj) => {
let mut out = BTreeMap::new();
for (k, v) in obj {
let new_key = self.mappings.get(k).cloned().unwrap_or_else(|| k.clone());
out.insert(new_key, v.clone());
}
Ok(Value::Object(out))
}
None => Err(crate::error::SerializationError::new(format!(
"FieldMapper expects an object, got {}",
value.type_name()
))
.into()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct Celsius(f64);
struct Fahrenheit(f64);
impl Adapt<Celsius> for Fahrenheit {
fn adapt(c: Celsius) -> Result<Self, Error> {
Ok(Fahrenheit(c.0 * 9.0 / 5.0 + 32.0))
}
}
#[test]
fn test_adapt_basic() {
let f = Fahrenheit::adapt(Celsius(0.0)).unwrap();
assert_eq!(f.0, 32.0);
let f2 = Fahrenheit::adapt(Celsius(100.0)).unwrap();
assert_eq!(f2.0, 212.0);
}
#[test]
fn test_pipeline_chain() {
let p = Pipeline::new()
.step(|v| match v {
Value::Int(n) => Ok(Value::Int(n * 2)),
other => Ok(other),
})
.step(|v| match v {
Value::Int(n) => Ok(Value::Int(n + 1)),
other => Ok(other),
});
assert_eq!(p.run(Value::Int(5)).unwrap(), Value::Int(11));
}
#[test]
fn test_pipeline_short_circuits_on_error() {
let p = Pipeline::new()
.step(|_| Err(crate::error::SerializationError::new("step1 failed").into()))
.step(Ok);
assert!(p.run(Value::Null).is_err());
}
#[test]
fn test_pipeline_empty() {
let p = Pipeline::new();
assert_eq!(p.run(Value::Int(7)).unwrap(), Value::Int(7));
}
fn make_obj(fields: &[(&str, Value)]) -> Value {
let mut m = BTreeMap::new();
for (k, v) in fields {
m.insert(k.to_string(), v.clone());
}
Value::Object(m)
}
#[test]
fn test_field_mapper_rename() {
let mapper = FieldMapper::new()
.map("first_name", "firstName")
.map("last_name", "lastName");
let input = make_obj(&[
("first_name", Value::String("Alice".into())),
("last_name", Value::String("Smith".into())),
("age", Value::Int(30)),
]);
let out = mapper.apply(&input).unwrap();
assert!(out.get("firstName").is_some());
assert!(out.get("lastName").is_some());
assert!(out.get("age").is_some());
assert!(out.get("first_name").is_none());
}
#[test]
fn test_field_mapper_non_object_fails() {
let mapper = FieldMapper::new();
assert!(mapper.apply(&Value::Int(1)).is_err());
}
#[test]
fn test_field_mapper_unmapped_keys_pass_through() {
let mapper = FieldMapper::new().map("a", "A");
let input = make_obj(&[("a", Value::Int(1)), ("b", Value::Int(2))]);
let out = mapper.apply(&input).unwrap();
assert!(out.get("A").is_some());
assert!(out.get("b").is_some());
}
}