// Record key -- the last path segment of an AT-URI, naming one record within
// one collection of one repository.
//
// https://atproto.com/specs/record-key
// Ported from @atproto/syntax packages/syntax/src/recordkey.ts.
//
// Most record keys in practice are TIDs (`Tid` below), which sort
// chronologically, or the literal `self` for singleton records like
// `app.bsky.actor.profile`. But the syntax is wider than either, and a client
// that assumed otherwise would fail on perfectly ordinary repositories.
//
// Record keys are CASE-SENSITIVE. Unlike handles, `Self` and `self` are two
// different records, so nothing here normalizes.
///|
const RECORD_KEY_MAX_LENGTH : Int = 512
///|
/// A syntactically valid record key.
pub struct RecordKey(String) derive(Eq, Debug)
///|
pub impl Show for RecordKey with fn output(self, logger) {
logger.write_string(self.0)
}
///|
pub fn RecordKey::to_string(self : Self) -> String {
self.0
}
///|
pub fn RecordKey::unchecked(rkey : String) -> RecordKey {
RecordKey(rkey)
}
///|
pub fn RecordKey::is_valid(rkey : String) -> Bool {
try {
RecordKey::parse(rkey) |> ignore
true
} catch {
_ => false
}
}
///|
/// The order matters for the message, and for one substantive reason: `.` and
/// `..` both satisfy the character set, so checking the set first would report
/// them as a syntax failure rather than as the reserved path components they
/// are. They are excluded because a record key is also a path segment in the
/// repository's Merkle tree.
pub fn RecordKey::parse(rkey : String) -> RecordKey raise SyntaxError {
fn bad(reason : String) -> SyntaxError {
SyntaxError(kind=RecordKey, input=rkey, reason~)
}
guard rkey.length() >= 1 && rkey.length() <= RECORD_KEY_MAX_LENGTH else {
raise bad("record key must be 1 to \{RECORD_KEY_MAX_LENGTH} characters")
}
guard rkey != "." && rkey != ".." else {
raise bad("record key can not be \".\" or \"..\"")
}
guard all_chars(rkey, is_record_key_char) else {
raise bad("record key syntax not valid (regex)")
}
RecordKey(rkey)
}
///|
/// `[a-zA-Z0-9_~.:-]`. Every one of these is safe both as an MST path component
/// and as an unescaped RFC 3986 path segment, which is the constraint the set
/// was chosen to satisfy.
fn is_record_key_char(c : Char) -> Bool {
is_ascii_alnum(c) || c == '_' || c == '~' || c == '.' || c == ':' || c == '-'
}