pub const KIT_KEY_VERSION: u32 = 1;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum KeyComponent {
Int(i64),
Text(String),
Null,
}
impl KeyComponent {
pub fn text(value: impl Into<String>) -> Self {
KeyComponent::Text(value.into())
}
}
fn escape_string(value: &str) -> String {
value.replace('\\', "\\\\").replace(':', "\\:")
}
fn unescape_string(value: &str) -> String {
value.replace("\\:", ":").replace("\\\\", "\\")
}
pub fn encode_component(value: &KeyComponent) -> String {
match value {
KeyComponent::Null => "n:null".to_string(),
KeyComponent::Int(i) => format!("i:{i}"),
KeyComponent::Text(s) => format!("s:{}", escape_string(s)),
}
}
fn decode_component(token: &str) -> KeyComponent {
if let Some(body) = token.strip_prefix("s:") {
KeyComponent::Text(unescape_string(body))
} else if let Some(body) = token.strip_prefix("i:") {
body.parse::<i64>()
.map(KeyComponent::Int)
.unwrap_or_else(|_| KeyComponent::Text(unescape_string(token)))
} else if token.strip_prefix("n:").is_some() {
KeyComponent::Null
} else {
KeyComponent::Text(unescape_string(token))
}
}
fn split_components(encoded: &str) -> Vec<String> {
let mut tokens: Vec<String> = Vec::new();
let mut current = String::new();
let mut escaped = false;
for ch in encoded.chars() {
if escaped {
current.push(ch);
escaped = false;
continue;
}
if ch == '\\' {
current.push(ch);
escaped = true;
continue;
}
if ch == ':' {
let starts_typed =
current.starts_with("s:") || current.starts_with("i:") || current.starts_with("n:");
if starts_typed {
tokens.push(std::mem::take(&mut current));
continue;
}
}
current.push(ch);
}
if current.len() >= 2 {
tokens.push(current);
}
tokens
}
pub fn decode_pk(encoded: &str) -> Vec<KeyComponent> {
split_components(encoded)
.iter()
.map(|t| decode_component(t))
.collect()
}
pub fn encode_pk(values: &[KeyComponent]) -> String {
values
.iter()
.map(encode_component)
.collect::<Vec<_>>()
.join(":")
}
pub fn encode_unique_key(
kit_version: u32,
constraint_name: &str,
values: &[KeyComponent],
) -> String {
let components = values
.iter()
.map(encode_component)
.collect::<Vec<_>>()
.join(":");
format!("uq:{kit_version}:{constraint_name}:{components}")
}
pub fn encode_row_guard_key(table_name: &str, encoded_pk: &str) -> String {
format!("rg:{table_name}:{encoded_pk}")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn int_and_text_do_not_collide() {
assert_eq!(encode_pk(&[KeyComponent::Int(1)]), "i:1");
assert_eq!(encode_pk(&[KeyComponent::text("1")]), "s:1");
assert_ne!(
encode_pk(&[KeyComponent::Int(1)]),
encode_pk(&[KeyComponent::text("1")])
);
}
#[test]
fn encode_pk_scalar_and_composite() {
assert_eq!(encode_pk(&[KeyComponent::text("hello")]), "s:hello");
assert_eq!(
encode_pk(&[KeyComponent::text("a"), KeyComponent::text("b")]),
"s:a:s:b"
);
assert_eq!(
encode_pk(&[KeyComponent::Int(42), KeyComponent::Int(7)]),
"i:42:i:7"
);
}
#[test]
fn encode_unique_key_matches_typescript_vectors() {
assert_eq!(
encode_unique_key(1, "users_email_uq", &[KeyComponent::text("a@example.com")]),
"uq:1:users_email_uq:s:a@example.com"
);
assert_eq!(
encode_unique_key(
1,
"shares_trip_user_uq",
&[KeyComponent::Int(42), KeyComponent::Int(7)]
),
"uq:1:shares_trip_user_uq:i:42:i:7"
);
assert_eq!(
encode_unique_key(1, "uq_esc", &[KeyComponent::text("a:b\\c")]),
"uq:1:uq_esc:s:a\\:b\\\\c"
);
assert_eq!(
encode_unique_key(1, "uq_null", &[KeyComponent::Null]),
"uq:1:uq_null:n:null"
);
}
#[test]
fn encode_row_guard_key_matches_typescript_vectors() {
assert_eq!(
encode_row_guard_key("trips", &encode_pk(&[KeyComponent::Int(5)])),
"rg:trips:i:5"
);
assert_eq!(
encode_row_guard_key("users", &encode_pk(&[KeyComponent::text("alpha")])),
"rg:users:s:alpha"
);
}
#[test]
fn pk_round_trips_through_decode() {
let cases = vec![
vec![KeyComponent::Int(1)],
vec![KeyComponent::Int(-99)],
vec![KeyComponent::text("alpha")],
vec![KeyComponent::text("a"), KeyComponent::text("b")],
vec![KeyComponent::Int(1), KeyComponent::text("x")],
];
for case in cases {
let encoded = encode_pk(&case);
assert_eq!(decode_pk(&encoded), case, "round-trip failed for {encoded}");
}
}
#[test]
fn decode_handles_typed_prefixes() {
assert_eq!(decode_pk("i:5"), vec![KeyComponent::Int(5)]);
assert_eq!(decode_pk("s:hi"), vec![KeyComponent::text("hi")]);
assert_eq!(decode_pk("n:null"), vec![KeyComponent::Null]);
}
#[test]
fn decode_does_not_panic_on_malformed_multibyte_input() {
let _ = decode_pk("中");
let _ = decode_pk("中:foo");
let _ = decode_pk("");
assert_eq!(decode_pk("中"), vec![KeyComponent::text("中")]);
assert_eq!(decode_pk("i:abc"), vec![KeyComponent::text("i:abc")]);
}
}