marianoguerra/atproto/syntax does not have a README file

    SyntaxError

    pub(all) suberror SyntaxError {
    SyntaxError(kind~ : SyntaxKind, input~ : String, reason~ : String)
    } derive(Eq,
    Debug
    )

    impl Show for SyntaxError

    SyntaxError::describe_error

    fn SyntaxError::describe_error(self : SyntaxError) -> String

    The input is truncated: a DID may be 2048 characters and an AT-URI 8192, and a validation failure that floods a log is its own bug.

    SyntaxError::input

    fn SyntaxError::input(self : SyntaxError) -> String

    SyntaxError::kind

    fn SyntaxError::kind(self : SyntaxError) -> SyntaxKind

    SyntaxError::reason

    fn SyntaxError::reason(self : SyntaxError) -> String

    The upstream message, on its own. Callers rendering their own text want this rather than describe_error, which adds the offending input.

    AtIdentifier

    pub(all) enum AtIdentifier {
    Did(Did)
    Handle(Handle)
    } derive(Eq,
    Debug
    )

    AtIdentifier::as_did

    fn AtIdentifier::as_did(self : AtIdentifier) -> Did?

    The DID, if this is one. None for a handle -- resolving a handle to a DID needs the network and is a client call, not a syntax operation.

    AtIdentifier::as_handle

    fn AtIdentifier::as_handle(self : AtIdentifier) -> Handle?

    AtIdentifier::is_valid

    fn AtIdentifier::is_valid(input : String) -> Bool

    AtIdentifier::parse

    fn AtIdentifier::parse(input : String) -> AtIdentifier raise SyntaxError

    The did: prefix decides which validator runs, so a malformed DID is reported as a bad DID rather than as a bad handle -- did:thing is not a handle that happens to contain a colon.

    AtIdentifier::to_string

    fn AtIdentifier::to_string(self : AtIdentifier) -> String

    AtUri

    pub struct AtUri {
    authority : AtIdentifier
    path : AtUriPath
    fragment : String?
    } derive(Eq,
    Debug
    )

    A syntactically valid AT-URI.
    impl Show for AtUri

    AtUri::authority

    fn AtUri::authority(self : AtUri) -> AtIdentifier

    AtUri::collection

    fn AtUri::collection(self : AtUri) -> Nsid?

    AtUri::fragment

    fn AtUri::fragment(self : AtUri) -> String?

    AtUri::is_valid

    fn AtUri::is_valid(uri : String) -> Bool

    AtUri::make

    fn AtUri::make(authority : AtIdentifier, path : AtUriPath, fragment? : String) -> AtUri

    Builds an AT-URI from parts that are already valid, so it cannot fail. This is the constructor to reach for when you have a DID and a record key in hand rather than a string to interpret.

    AtUri::parse

    fn AtUri::parse(uri : String) -> AtUri raise SyntaxError

    Note what this does NOT accept, all of which the older AtUri class did: a missing at:// prefix, a trailing slash, a query string, and a record key that is not a valid record key. Every one of those is in the protocol's interop corpus as invalid.

    AtUri::path

    fn AtUri::path(self : AtUri) -> AtUriPath

    AtUri::rkey

    fn AtUri::rkey(self : AtUri) -> RecordKey?

    AtUri::to_string

    fn AtUri::to_string(self : AtUri) -> String

    AtUriPath

    pub(all) enum AtUriPath {
    Repo
    Collection(Nsid)
    Record(collection~ : Nsid, rkey~ : RecordKey)
    } derive(Eq,
    Debug
    )

    What an AT-URI points at. A record key without a collection is not a state this type can hold.

    Cid

    pub struct Cid {
    text : String
    version : Int
    codec : CidCodec
    hash_code : Int
    hash_length : Int
    } derive(Eq,
    Debug
    )

    A parsed CID, holding the string it came from.

    As everywhere else in this package the original bytes are what re-serializes, which for a CID matters twice over: it is a content hash, so a re-encoding that differs is a reference to nothing.
    impl Show for Cid

    Cid::codec

    fn Cid::codec(self : Cid) -> CidCodec

    Cid::hash_code

    fn Cid::hash_code(self : Cid) -> Int

    Cid::is_valid

    fn Cid::is_valid(text : String) -> Bool

    Cid::parse

    fn Cid::parse(text : String) -> Cid raise SyntaxError

    Cid::to_string

    fn Cid::to_string(self : Cid) -> String

    Cid::unchecked

    fn Cid::unchecked(text : String) -> Cid

    Trusts the caller. The header fields are reported as a CIDv1 SHA-256 DAG-CBOR CID, which is what atproto writes -- so do not read them off a value you built this way.

    Cid::version

    fn Cid::version(self : Cid) -> Int

    CidCodec

    pub(all) enum CidCodec {
    Raw
    DagCbor
    DagPb
    Other(Int)
    } derive(Eq,
    Debug
    )

    Multicodec content types, the ones atproto uses.

    CidCodec::code

    fn CidCodec::code(self : CidCodec) -> Int

    Datetime

    pub struct Datetime(String) derive(Eq,
    Debug
    )

    A syntactically and semantically valid RFC 3339 datetime, holding the exact bytes it was parsed from.
    impl Show for Datetime

    Datetime::from_epoch_millis

    fn Datetime::from_epoch_millis(millis : Int64) -> Datetime

    Builds a UTC datetime with millisecond precision -- 1985-04-12T23:20:50.123Z, the spelling the spec calls "preferred" and the one to write when creating a record.

    The clock is an argument, as everywhere else in this package.

    Datetime::is_valid

    fn Datetime::is_valid(value : String) -> Bool

    Datetime::parse

    fn Datetime::parse(value : String) -> Datetime raise SyntaxError

    Datetime::parts

    fn Datetime::parts(self : Datetime) -> DatetimeParts

    Datetime::to_epoch_seconds

    fn Datetime::to_epoch_seconds(self : Datetime) -> Int64

    Seconds since the Unix epoch, with the offset applied. Sub-second precision is dropped; parts().fraction still has it.

    Datetime::to_string

    fn Datetime::to_string(self : Datetime) -> String

    Datetime::unchecked

    fn Datetime::unchecked(value : String) -> Datetime

    DatetimeParts

    pub struct DatetimeParts {
    year : Int
    month : Int
    day : Int
    hour : Int
    minute : Int
    second : Int
    fraction : String
    offset_minutes : Int
    } derive(Eq,
    Debug
    )

    The parsed pieces. Kept alongside the string rather than replacing it, so callers can do arithmetic without the value losing its original spelling.

    Did

    pub struct Did(String) derive(Eq,
    Debug
    )

    A syntactically valid DID.

    Opaque: the only ways in are parse, which checks, and unchecked, which says in its name that it does not.
    impl Show for Did

    Did::is_valid

    fn Did::is_valid(did : String) -> Bool

    Did::method_name

    fn Did::method_name(self : Did) -> String

    The method: plc in did:plc:7iza6de2dwap2sbkpav7c6c6.

    Spelled method_name because method is a reserved word.

    Total, because a Did cannot exist without one.

    Did::parse

    fn Did::parse(did : String) -> Did raise SyntaxError

    The order of these checks is upstream's, and it is observable: a caller sees the message for the FIRST rule an input breaks, so reordering them changes the reported reason for inputs that break several. DID:method:val must report a missing prefix, not an uppercase method.

    Did::to_string

    fn Did::to_string(self : Did) -> String

    Did::unchecked

    fn Did::unchecked(did : String) -> Did

    Trusts the caller. For values that have already been validated -- read back out of a database this library wrote, or decoded from a response that was checked at the boundary -- and for tests.

    Handle

    pub struct Handle(String) derive(Eq,
    Debug
    )

    A syntactically valid handle. Always lower-case: parse normalizes, because handles are domain names and DNS is case-insensitive, so treating Alice.BSky.social and alice.bsky.social as different values would be a bug waiting to be written.
    impl Show for Handle

    Handle::is_invalid_sentinel

    fn Handle::is_invalid_sentinel(self : Handle) -> Bool

    True for the handle.invalid sentinel a PDS returns when it could not verify a handle against its DID document. Such a value is well-formed, so nothing else in this package will flag it.

    Handle::is_valid

    fn Handle::is_valid(handle : String) -> Bool

    Handle::is_valid_tld

    fn Handle::is_valid_tld(self : Handle) -> Bool

    Whether the handle's TLD is one atproto permits. Separate from parse because it is policy: the list has changed and will change again, and a handle that stops being allowed does not retroactively stop being well-formed.

    Handle::parse

    fn Handle::parse(handle : String) -> Handle raise SyntaxError

    Lower-casing is safe to do before validating rather than after because the character-set gate rejects everything outside ASCII, so no locale-dependent case mapping can apply.

    Handle::to_string

    fn Handle::to_string(self : Handle) -> String

    Handle::unchecked

    fn Handle::unchecked(handle : String) -> Handle

    Trusts the caller, and does NOT normalize -- unchecked means unchecked.

    Language

    pub struct Language(String) derive(Eq,
    Debug
    )

    A well-formed BCP 47 language tag, holding the bytes it was parsed from. Case is meaningful to readers by convention but not to equality in the protocol; nothing here normalizes, because a record must round-trip.
    impl Show for Language

    Language::is_valid

    fn Language::is_valid(tag : String) -> Bool

    Language::is_well_formed

    fn Language::is_well_formed(tag : String) -> Bool

    RFC 5646 §2.1 well-formedness, and nothing more. This is the level the Lexicon language format applies, so it is the one a decoder should use on a value arriving from a server -- being stricter than the protocol would mean rejecting records other clients happily wrote.

    Language::parse

    fn Language::parse(tag : String) -> Language raise SyntaxError

    Well-formed and valid: a lowercase 2-3 letter primary subtag, no repeated variant, no repeated extension singleton.

    Language::parse_lenient

    fn Language::parse_lenient(tag : String) -> Language raise SyntaxError

    Well-formedness only. See parse for the stricter reading.

    Language::primary

    fn Language::primary(self : Language) -> String?

    The primary language subtag -- en in en-GB-boont. None for a private-use-only tag, which names no language.

    Language::to_string

    fn Language::to_string(self : Language) -> String

    Language::unchecked

    fn Language::unchecked(tag : String) -> Language

    Nsid

    pub struct Nsid(String) derive(Eq,
    Debug
    )

    A syntactically valid NSID.
    impl Show for Nsid

    Nsid::authority

    fn Nsid::authority(self : Nsid) -> String

    The publishing authority, as a domain name -- so app.bsky.feed.post has authority bsky.app. The segments are reversed back into DNS order, which is the form you would look up or check a certificate against.

    Nsid::is_valid

    fn Nsid::is_valid(nsid : String) -> Bool

    Nsid::name

    fn Nsid::name(self : Nsid) -> String

    The name part -- post in app.bsky.feed.post.

    Nsid::parse

    fn Nsid::parse(nsid : String) -> Nsid raise SyntaxError

    Nsid::to_string

    fn Nsid::to_string(self : Nsid) -> String

    Nsid::unchecked

    fn Nsid::unchecked(nsid : String) -> Nsid

    RecordKey

    pub struct RecordKey(String) derive(Eq,
    Debug
    )

    A syntactically valid record key.
    impl Show for RecordKey

    RecordKey::is_valid

    fn RecordKey::is_valid(rkey : String) -> Bool

    RecordKey::parse

    fn RecordKey::parse(rkey : String) -> RecordKey raise SyntaxError

    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.

    RecordKey::to_string

    fn RecordKey::to_string(self : RecordKey) -> String

    RecordKey::unchecked

    fn RecordKey::unchecked(rkey : String) -> RecordKey

    SyntaxKind

    pub(all) enum SyntaxKind {
    Did
    Handle
    AtIdentifier
    Nsid
    RecordKey
    Tid
    AtUri
    Datetime
    Language
    Uri
    Cid
    } derive(Eq,
    Debug
    )

    Which syntax a value failed to satisfy.

    SyntaxKind::label

    fn SyntaxKind::label(self : SyntaxKind) -> String

    The name used in error messages, matching the reference implementation's spelling -- "ATURI" and "NSID" are shouted upstream, so they are here.

    Ticker

    pub struct Ticker {
    clock_id : Int
    last : Int64
    }

    Mints TIDs that increase even when the clock does not.

    Two calls in the same microsecond -- or across a clock that has gone backwards -- must not produce the same key or a decreasing one, because a repository's records are ordered by it. So the ticker keeps the last value it issued and steps past it, which costs a microsecond of drift and buys monotonicity.

    Ticker::new

    fn Ticker::new(clock_id : Int) -> Ticker

    clock_id should be random per process; this package will not draw it, because a random number needs a source and a source needs a platform.

    Ticker::next

    fn Ticker::next(self : Ticker, now_micros : Int64) -> Tid

    Tid

    pub struct Tid(String) derive(Compare, Eq,
    Debug
    )

    A syntactically valid TID.

    Compare is derived from the string, which is correct precisely because of the sortable alphabet -- see the note above before changing it.
    impl Show for Tid

    Tid::clock_id

    fn Tid::clock_id(self : Tid) -> Int

    The writer's random per-process identifier. Carries no meaning beyond breaking ties between two TIDs minted in the same microsecond.

    Tid::from_time

    fn Tid::from_time(micros : Int64, clock_id : Int) -> Tid

    Builds a TID from an explicit time. The clock is an argument rather than a call to now() so that this package needs no platform clock and stays buildable on every backend -- the same reason @ratectl in the sibling slack library takes now : Int64.

    micros is truncated to 53 bits and clock_id to 10, which is what makes this total: there is no input for which it produces an invalid TID.

    Tid::is_valid

    fn Tid::is_valid(tid : String) -> Bool

    Tid::parse

    fn Tid::parse(tid : String) -> Tid raise SyntaxError

    Tid::timestamp

    fn Tid::timestamp(self : Tid) -> Int64

    Microseconds since the Unix epoch.

    Tid::to_string

    fn Tid::to_string(self : Tid) -> String

    Tid::unchecked

    fn Tid::unchecked(tid : String) -> Tid

    Uri

    pub struct Uri(String) derive(Eq,
    Debug
    )

    A string shaped like a URI.
    impl Show for Uri

    Uri::is_valid

    fn Uri::is_valid(uri : String) -> Bool

    Uri::parse

    fn Uri::parse(uri : String) -> Uri raise SyntaxError

    Uri::scheme

    fn Uri::scheme(self : Uri) -> String

    The scheme, lower-cased -- https in https://example.com. This is the part worth branching on, and the reason to have a type here at all.

    Uri::to_string

    fn Uri::to_string(self : Uri) -> String

    Uri::unchecked

    fn Uri::unchecked(uri : String) -> Uri

    INVALID_HANDLE

    let INVALID_HANDLE : String

    What a PDS returns in place of a handle when it has one on file but could not bidirectionally verify it against the DID document. It is deliberately itself a syntactically valid handle, so it flows through code that does not check -- which is precisely why code that displays a handle should.

    disallowed_tlds

    let disallowed_tlds : Array[String]

    TLDs a handle may not use. Policy, not syntax: these strings parse fine and are refused for what they mean, so parse does not consult this list and is_valid_tld is a separate question.

    .test is deliberately absent -- it is allowed, for development.