README

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.