use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine;
use serde_json::{Map, Value as Json};
use crate::errors::GraphDDBError;
use crate::value::write_json_string;
pub fn serialize_contract_key(key: &Json) -> String {
let obj = key.as_object().cloned().unwrap_or_default();
let mut keys: Vec<&String> = obj.keys().collect();
keys.sort();
let mut out = String::from("{");
for (i, k) in keys.iter().enumerate() {
if i > 0 {
out.push(',');
}
write_json_string(k, &mut out);
out.push(':');
write_json_value_compact(&obj[*k], &mut out);
}
out.push('}');
out
}
fn write_json_value_compact(v: &Json, out: &mut String) {
match v {
Json::String(s) => write_json_string(s, out),
Json::Null => out.push_str("null"),
Json::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
Json::Number(n) => out.push_str(&n.to_string()),
Json::Array(items) => {
out.push('[');
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push(',');
}
write_json_value_compact(item, out);
}
out.push(']');
}
Json::Object(map) => {
out.push('{');
for (i, (k, val)) in map.iter().enumerate() {
if i > 0 {
out.push(',');
}
write_json_string(k, out);
out.push(':');
write_json_value_compact(val, out);
}
out.push('}');
}
}
}
pub fn encode_per_key_cursor(key: &Json, inner: Option<&str>) -> Option<String> {
let inner = inner?;
let mut payload = String::from("{");
write_json_string("key", &mut payload);
payload.push(':');
write_json_string(&serialize_contract_key(key), &mut payload);
payload.push(',');
write_json_string("inner", &mut payload);
payload.push(':');
write_json_string(inner, &mut payload);
payload.push('}');
Some(URL_SAFE_NO_PAD.encode(payload.as_bytes()))
}
pub fn decode_per_key_cursor(cursor: &str, expected_key: &Json) -> Result<String, GraphDDBError> {
let bytes = URL_SAFE_NO_PAD.decode(cursor.as_bytes()).map_err(|_| {
GraphDDBError::new(
"Invalid per-key cursor: the value passed as `after` is not a cursor \
minted by this runtime (it failed to decode).",
)
})?;
let envelope: Json = serde_json::from_slice(&bytes).map_err(|_| {
GraphDDBError::new(
"Invalid per-key cursor: the value passed as `after` is not a cursor \
minted by this runtime (it failed to decode).",
)
})?;
let obj: &Map<String, Json> = envelope.as_object().ok_or_else(|| {
GraphDDBError::new(
"Invalid per-key cursor: the decoded envelope is missing its key / inner \
fields. A range cursor must be a per-key envelope minted by this runtime.",
)
})?;
let key = obj.get("key").and_then(Json::as_str);
let inner = obj.get("inner").and_then(Json::as_str);
let (key, inner) = match (key, inner) {
(Some(k), Some(i)) => (k, i),
_ => {
return Err(GraphDDBError::new(
"Invalid per-key cursor: the decoded envelope is missing its key / inner \
fields. A range cursor must be a per-key envelope minted by this runtime.",
))
}
};
let expected = serialize_contract_key(expected_key);
if key != expected {
return Err(GraphDDBError::new(format!(
"Per-key cursor mismatch: the supplied `after` cursor belongs to key {key}, \
but the method is being called for key {expected}. A range cursor may only \
resume pagination of the same key it was issued for."
)));
}
Ok(inner.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn serialize_sorts_keys() {
let key = json!({"b": "2", "a": "1"});
assert_eq!(serialize_contract_key(&key), "{\"a\":\"1\",\"b\":\"2\"}");
}
#[test]
fn round_trip_and_mismatch() {
let key = json!({"categoryId": "tech"});
let env = encode_per_key_cursor(&key, Some("INNER")).unwrap();
assert_eq!(decode_per_key_cursor(&env, &key).unwrap(), "INNER");
let other = json!({"categoryId": "sports"});
assert!(decode_per_key_cursor(&env, &other).is_err());
}
#[test]
fn none_inner_yields_none() {
let key = json!({"categoryId": "tech"});
assert_eq!(encode_per_key_cursor(&key, None), None);
}
}