graphddb_runtime 0.7.7

Rust runtime for GraphDDB — interprets the language-neutral IR (manifest.json + operations.json) and executes the validated access patterns against DynamoDB.
Documentation
//! Hydration of raw DynamoDB items into result values — a port of
//! `python/graphddb_runtime/hydration.py`.
//!
//! - only fields named in `select` are copied out;
//! - internal key attributes (`PK` / `SK` / `GSI*PK` / `GSI*SK`) are never part of
//!   the result (they are simply not selected);
//! - a string field whose manifest carries `format: "datetime"` is restored to the
//!   canonical ISO instant; `format: "date"` to midnight UTC.
//!
//! Relation keys in the select (objects, not `true`) are skipped here; the
//! single-operation core has no relations to assemble.

use serde_json::Value as Json;

use crate::errors::GraphDDBError;
use crate::iso_datetime::{normalize_date, normalize_datetime};
use crate::value::indexmap_shim::IndexMap;
use crate::value::Value;

/// True for an internal key attribute (`PK` / `SK` / `GSI*PK` / `GSI*SK`).
pub fn is_internal_key(name: &str) -> bool {
    if name == "PK" || name == "SK" {
        return true;
    }
    name.starts_with("GSI") && (name.ends_with("PK") || name.ends_with("SK"))
}

/// Hydrate a single deserialized item against a select list + the entity manifest
/// meta, returning an insertion-ordered map (select order preserved).
///
/// `select` is the ordered list of selected field names (value `true` in the
/// Python select); `entity_meta` is the entity's manifest object (carries
/// `fields.<name>.format`).
pub fn hydrate_item(
    raw: &IndexMap<Value>,
    select: &[String],
    entity_meta: &Json,
) -> Result<IndexMap<Value>, GraphDDBError> {
    let fields = entity_meta.get("fields").and_then(Json::as_object);
    let mut result = IndexMap::new();
    for field_name in select {
        if is_internal_key(field_name) {
            continue;
        }
        if let Some(v) = raw.get(field_name) {
            let field_meta = fields.and_then(|f| f.get(field_name));
            result.insert(field_name.clone(), deserialize_value(v, field_meta)?);
        }
    }
    Ok(result)
}

fn deserialize_value(value: &Value, field_meta: Option<&Json>) -> Result<Value, GraphDDBError> {
    let fmt = field_meta
        .and_then(|m| m.get("format"))
        .and_then(Json::as_str);
    match (fmt, value) {
        (Some("datetime"), Value::S(s)) => Ok(Value::S(normalize_datetime(s)?)),
        (Some("date"), Value::S(s)) => Ok(Value::S(normalize_date(s)?)),
        _ => Ok(value.clone()),
    }
}

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

    #[test]
    fn selects_only_named_and_drops_internal() {
        let mut raw = IndexMap::new();
        raw.insert("PK".to_string(), Value::S("x".to_string()));
        raw.insert("name".to_string(), Value::S("Alice".to_string()));
        raw.insert("secret".to_string(), Value::S("hidden".to_string()));
        let select = vec!["name".to_string(), "PK".to_string()];
        let out = hydrate_item(&raw, &select, &json!({"fields": {}})).unwrap();
        assert_eq!(out.len(), 1);
        assert_eq!(out.get("name").unwrap(), &Value::S("Alice".to_string()));
    }

    #[test]
    fn datetime_format_normalized() {
        let mut raw = IndexMap::new();
        raw.insert(
            "createdAt".to_string(),
            Value::S("2020-01-01T00:00:00Z".to_string()),
        );
        let meta = json!({"fields": {"createdAt": {"format": "datetime"}}});
        let out = hydrate_item(&raw, &["createdAt".to_string()], &meta).unwrap();
        assert_eq!(
            out.get("createdAt").unwrap(),
            &Value::S("2020-01-01T00:00:00.000Z".to_string())
        );
    }
}