graphddb_runtime 0.7.5

Rust runtime for GraphDDB — interprets the language-neutral IR (manifest.json + operations.json) and executes the validated access patterns against DynamoDB.
Documentation
//! Pagination cursor encode/decode — a faithful port of
//! `python/graphddb_runtime/cursor.py` and the TS `src/pagination/cursor.ts`.
//!
//! A cursor is the DynamoDB `LastEvaluatedKey` encoded as base64url-WITHOUT-pad
//! JSON: the JSON is the key object only (no wrapping envelope), so the byte shape
//! is identical for a given key across every runtime. The runtime works with the
//! DESERIALIZED (plain [`Value`]) key so the encoded JSON matches the un-marshalled
//! shape the other runtimes encode.

use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine;

use crate::errors::GraphDDBError;
use crate::value::{cursor_json, Value};

/// Encode a (deserialized) `LastEvaluatedKey` into a base64url cursor.
pub fn encode_cursor(last_evaluated_key: &Value) -> Result<String, GraphDDBError> {
    let payload = cursor_json(last_evaluated_key)?;
    Ok(URL_SAFE_NO_PAD.encode(payload.as_bytes()))
}

/// Decode a base64url cursor back into the plain JSON key (`serde_json::Value`),
/// used to rebuild an `ExclusiveStartKey`.
pub fn decode_cursor(cursor: &str) -> Result<serde_json::Value, GraphDDBError> {
    let bytes = URL_SAFE_NO_PAD
        .decode(cursor.as_bytes())
        .map_err(|e| GraphDDBError::new(format!("invalid base64url cursor: {e}")))?;
    let text = String::from_utf8(bytes)
        .map_err(|e| GraphDDBError::new(format!("cursor is not valid UTF-8: {e}")))?;
    serde_json::from_str(&text)
        .map_err(|e| GraphDDBError::new(format!("cursor did not decode to JSON: {e}")))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::value::indexmap_shim::IndexMap;

    #[test]
    fn round_trip_and_no_padding() {
        let mut m = IndexMap::new();
        m.insert("PK".to_string(), Value::S("GROUP#g1".to_string()));
        m.insert("SK".to_string(), Value::S("USER#u1".to_string()));
        let key = Value::M(m);
        let c = encode_cursor(&key).unwrap();
        assert!(!c.contains('='), "cursor must be padless: {c}");
        let decoded = decode_cursor(&c).unwrap();
        assert_eq!(decoded["PK"], "GROUP#g1");
        assert_eq!(decoded["SK"], "USER#u1");
    }

    #[test]
    fn golden_byte_shape() {
        // Pinned golden: base64url-nopad of {"PK":"P","SK":"S"} — the exact bytes
        // the Python/TS/PHP runtimes mint for this key.
        let mut m = IndexMap::new();
        m.insert("PK".to_string(), Value::S("P".to_string()));
        m.insert("SK".to_string(), Value::S("S".to_string()));
        let c = encode_cursor(&Value::M(m)).unwrap();
        // {"PK":"P","SK":"S"} -> base64url-nopad
        assert_eq!(c, "eyJQSyI6IlAiLCJTSyI6IlMifQ");
    }
}