datasources 0.1.39

Async datasource interfaces, middleware, registry, and test doubles
Documentation
use crate::converters::{is_column, peek_value, reflect, set_value, struct_fields};
use crate::datasource_error::DatasourceError;
use crate::result::Result;
use crate::value::Value;
use chrono::{DateTime, Utc};
use facet::{Def, Facet, HasFields, Partial, Peek, Shape};
use serde_json::{Map, Number, Value as Json};

pub struct JsonObjectSerializer;

impl JsonObjectSerializer {
    pub fn serialize<T: Facet<'static>>(value: &T) -> Result<Json> {
        peek_to_json(Peek::new(value))
    }

    pub fn deserialize<T: Facet<'static>>(value: &Json) -> Result<T> {
        let partial = write_into(Partial::alloc::<T>().map_err(reflect)?, value, T::SHAPE)?;
        partial.build().map_err(reflect)?.materialize().map_err(reflect)
    }

    pub fn to_string<T: Facet<'static>>(value: &T) -> Result<String> {
        Ok(Self::serialize(value)?.to_string())
    }

    pub fn from_str<T: Facet<'static> + Default>(text: &str) -> Result<T> {
        if text.trim().is_empty() {
            return Ok(T::default());
        }
        let json = serde_json::from_str(text).map_err(|error| DatasourceError::message(error.to_string()))?;
        Self::deserialize(&json)
    }
}

fn peek_to_json(value: Peek<'_, '_>) -> Result<Json> {
    if let Ok(option) = value.into_option() {
        return match option.value() {
            Some(inner) => peek_to_json(inner),
            None => Ok(Json::Null),
        };
    }
    if is_column(value.shape()) {
        return Ok(value_to_json(peek_value(value).unwrap_or(Value::Null)));
    }
    if let Ok(list) = value.into_list() {
        return list.iter().map(peek_to_json).collect::<Result<Vec<_>>>().map(Json::Array);
    }
    if let Ok(map) = value.into_map() {
        let mut object = Map::new();
        for (key, cell) in map.iter() {
            object.insert(map_key(key), peek_to_json(cell)?);
        }
        return Ok(Json::Object(object));
    }
    let st = value.into_struct().map_err(reflect)?;
    let mut object = Map::new();
    for (field, cell) in st.fields() {
        object.insert(field.name.to_string(), peek_to_json(cell)?);
    }
    Ok(Json::Object(object))
}

fn write_into<'a>(partial: Partial<'a>, json: &Json, shape: &'static Shape) -> Result<Partial<'a>> {
    if is_option(shape) {
        return write_option(partial, json, shape);
    }
    if is_column(shape) {
        return set_value(partial, &cell_from_json(json, shape)?, shape, true);
    }
    match shape.def {
        Def::List(list) => write_list(partial, json, list.t),
        Def::Map(map) => write_map(partial, json, map.v()),
        _ => write_struct(partial, json, shape),
    }
}

fn write_option<'a>(partial: Partial<'a>, json: &Json, shape: &'static Shape) -> Result<Partial<'a>> {
    if matches!(json, Json::Null) {
        return partial.set_default().map_err(reflect);
    }
    let inner = option_inner(shape)?;
    let partial = write_into(partial.begin_some().map_err(reflect)?, json, inner)?;
    partial.end().map_err(reflect)
}

fn write_list<'a>(
    partial: Partial<'a>,
    json: &Json,
    child: &'static Shape,
) -> Result<Partial<'a>> {
    let items = json
        .as_array()
        .ok_or_else(|| DatasourceError::message("expected a json array"))?;
    let mut partial = partial.init_list_with_capacity(items.len()).map_err(reflect)?;
    for item in items {
        partial = write_into(partial.begin_list_item().map_err(reflect)?, item, child)?;
        partial = partial.end().map_err(reflect)?;
    }
    Ok(partial)
}

fn write_map<'a>(
    partial: Partial<'a>,
    json: &Json,
    value: &'static Shape,
) -> Result<Partial<'a>> {
    let object = json
        .as_object()
        .ok_or_else(|| DatasourceError::message("expected a json object"))?;
    let mut partial = partial.init_map().map_err(reflect)?;
    for (key, cell) in object {
        partial = partial.set_key(key.clone()).map_err(reflect)?;
        partial = write_into(partial.begin_value().map_err(reflect)?, cell, value)?;
        partial = partial.end().map_err(reflect)?;
    }
    Ok(partial)
}

fn write_struct<'a>(partial: Partial<'a>, json: &Json, shape: &'static Shape) -> Result<Partial<'a>> {
    let object = json
        .as_object()
        .ok_or_else(|| DatasourceError::message("expected a json object"))?;
    let mut partial = partial;
    for field in struct_fields(shape)? {
        partial = partial.begin_field(field.name).map_err(reflect)?;
        partial = match field_json(object, field.name) {
            Some(cell) => write_into(partial, cell, field.shape())?,
            None => partial.set_default().map_err(reflect)?,
        };
        partial = partial.end().map_err(reflect)?;
    }
    Ok(partial)
}

fn field_json<'a>(object: &'a Map<String, Json>, name: &str) -> Option<&'a Json> {
    object.get(name).or_else(|| object.get(&camel(name)))
}

fn camel(name: &str) -> String {
    let mut parts = name.split('_');
    let first = parts.next().unwrap_or_default();
    parts.fold(first.to_string(), |mut name, part| {
        let mut chars = part.chars();
        if let Some(head) = chars.next() {
            name.push(head.to_ascii_uppercase());
            name.extend(chars);
        }
        name
    })
}

fn map_key(value: Peek<'_, '_>) -> String {
    peek_value(value).map(|cell| cell.wire_text()).unwrap_or_default()
}

fn value_to_json(value: Value) -> Json {
    match value {
        Value::Null => Json::Null,
        Value::Bool(flag) => Json::Bool(flag),
        Value::Int(number) => Json::Number(Number::from(number)),
        Value::Float(number) => Number::from_f64(number).map(Json::Number).unwrap_or(Json::Null),
        Value::Text(text) => Json::String(text),
        Value::DateTime(date) => Json::String(Value::iso_datetime(date)),
        Value::Uuid(id) => Json::String(id.to_string()),
        Value::Decimal(number) => Json::String(number.to_string()),
        Value::Binary(bytes) => Json::String(Value::from(bytes).wire_text()),
    }
}

fn cell_from_json(json: &Json, shape: &'static Shape) -> Result<Value> {
    match json {
        Json::Null => Ok(Value::Null),
        Json::Bool(flag) => Ok(Value::Bool(*flag)),
        Json::Number(number) => number
            .as_i64()
            .map(Value::Int)
            .or_else(|| number.as_f64().map(Value::Float))
            .ok_or_else(|| DatasourceError::message("cannot read json number")),
        Json::String(text) => Ok(text_cell(text, shape)),
        Json::Array(_) | Json::Object(_) => Err(DatasourceError::message(format!(
            "cannot assign json to {}",
            shape.type_identifier
        ))),
    }
}

fn text_cell(text: &str, shape: &'static Shape) -> Value {
    if is_datetime(shape) {
        if let Ok(date) = DateTime::parse_from_rfc3339(text) {
            return Value::DateTime(date.with_timezone(&Utc));
        }
    }
    if shape.type_identifier.contains("Uuid") {
        if let Ok(id) = text.parse() {
            return Value::Uuid(id);
        }
    }
    if is_binary(shape) {
        return Value::from(base64::Engine::decode(
            &base64::engine::general_purpose::STANDARD,
            text,
        ).unwrap_or_default());
    }
    Value::from(text.to_string())
}

fn is_datetime(shape: &'static Shape) -> bool {
    shape.type_identifier.contains("DateTime")
}

fn is_binary(shape: &'static Shape) -> bool {
    matches!(shape.def, Def::List(list) if list.t.type_identifier == "u8")
}

fn is_option(shape: &'static Shape) -> bool {
    matches!(shape.def, Def::Option(_))
}

fn option_inner(shape: &'static Shape) -> Result<&'static Shape> {
    match shape.def {
        Def::Option(option) => Ok(option.t()),
        _ => Err(DatasourceError::message(format!("{} is not an option", shape.type_identifier))),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use facet::Facet;
    use std::collections::HashMap;

    #[derive(Clone, Debug, Default, PartialEq, Facet)]
    struct Kid {
        id: i64,
        label: String,
    }

    #[derive(Clone, Debug, Default, PartialEq, Facet)]
    struct Pref {
        locale: String,
        timezone: String,
    }

    #[derive(Clone, Debug, Default, PartialEq, Facet)]
    struct House {
        id: i32,
        name: String,
        floor: Option<u32>,
        created: Option<DateTime<Utc>>,
        stamp: Option<Vec<u8>>,
        kids: Vec<Kid>,
        settings: HashMap<String, String>,
        prefs: HashMap<String, Pref>,
    }

    #[test]
    fn object_round_trips_nested_fields() {
        let house = House {
            id: 7,
            name: "ada".into(),
            floor: Some(2),
            created: Some(
                DateTime::parse_from_rfc3339("2024-01-02T03:04:05.000Z")
                    .unwrap()
                    .with_timezone(&Utc),
            ),
            stamp: Some(vec![1]),
            kids: vec![Kid {
                id: 2,
                label: "a".into(),
            }],
            settings: HashMap::from([("theme".into(), "dark".into())]),
            prefs: HashMap::from([(
                "home".into(),
                Pref {
                    locale: "en-GB".into(),
                    timezone: "Europe/London".into(),
                },
            )]),
        };
        let json = JsonObjectSerializer::serialize(&house).unwrap();
        assert_eq!(json["id"], 7);
        assert_eq!(json["name"], "ada");
        assert_eq!(json["floor"], 2);
        assert_eq!(json["kids"][0]["label"], "a");
        assert_eq!(json["settings"]["theme"], "dark");
        assert_eq!(json["prefs"]["home"]["locale"], "en-GB");
        assert_eq!(json["stamp"], "AQ==");
        let copied = JsonObjectSerializer::deserialize::<House>(&json).unwrap();
        assert_eq!(copied, house);
    }

    #[test]
    fn reads_camel_case_keys() {
        let house = JsonObjectSerializer::from_str::<House>(
            r#"{"id":3,"firstName":"skip","name":"ada","floor":0}"#,
        )
        .unwrap();
        assert_eq!(house.id, 3);
        assert_eq!(house.name, "ada");
        assert_eq!(house.floor, Some(0));
    }
}