Current section

Files

Jump to
possum src possum did.gleam
Raw

src/possum/did.gleam

//// ## DID (Decentralized ID)
////
//// DIDs, or Decentralized IDentifiers, are universally-unique identifiers which
//// represent data repos. They are permanent and non-human-readable.
//// DIDs are a W3C specification. The AT Protocol currently supports
//// did:web and did:plc, two different DID methods.
////
//// DIDs resolve to documents which contain metadata about a repo,
//// including the address of the repo's PDS, the repo's handles,
//// and the public signing keys.
import gleam/dynamic/decode
import gleam/regexp
import gleam/result
import gleam/string
/// Available on: https://atproto.com/specs/did#did-identifier-syntax
const pattern = "^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$"
pub type ParseError {
/// Regex failed to compile
InvalidRegexPattern(pattern: String)
/// String does not have a valid DID format
InvalidSyntax(value: String)
/// Method can only be "plc" or "web"
InvalidDidMethod(value: String)
}
/// AT Protocol uses Decentralized Identifiers (DIDs) as persistent account
/// identifiers. An example DID is `did:plc:ewvi7nxzyoun6zhxrhs64oiz`.
///
/// Documentation: [Decentralized Identifiers](https://atproto.com/specs/did)
pub opaque type Did {
Did(method: Method, identifier: String)
}
/// Return the DID method
///
/// ## Examples
///
/// ```gleam
/// let assert Ok(did) = did.parse("did:plc:k5vecqzf4d5mdvkcu3mx6l5g")
/// let method = possum.method(did)
///
/// assert method == did.Plc
/// ```
pub fn method(self: Did) -> Method {
self.method
}
/// Return the DID identifier.
///
/// This function **does not** convert the value into a valid DID format,
/// for that use `to_string`.
///
/// ## Examples
///
/// ```gleam
/// import possum/did
///
/// let assert Ok(did) = did.parse("did:plc:k5vecqzf4d5mdvkcu3mx6l5g")
/// let identifier = did.identifier(did)
///
/// assert identifier == "k5vecqzf4d5mdvkcu3mx6l5g"
/// ```
pub fn indentifer(self: Did) -> String {
self.identifier
}
/// Convert a DID to a string.
///
/// ## Examples
///
/// ```gleam
/// import possum/did
///
/// let assert Ok(did) = did.parse("did:plc:k5vecqzf4d5mdvkcu3mx6l5g")
/// let string = did.to_string(did)
///
/// assert string == "did:plc:k5vecqzf4d5mdvkcu3mx6l5g"
/// ```
pub fn to_string(self: Did) -> String {
case self.method {
Plc -> "did:plc:" <> self.identifier
Web -> "did:web:" <> self.identifier
}
}
pub fn decoder() -> decode.Decoder(Did) {
use value <- decode.then(decode.string)
case parse(value) {
Ok(did) -> decode.success(did)
Error(_) -> decode.failure(Did(method: Plc, identifier: "possum"), "DID")
}
}
/// The core DID standard and describes a framework for compliant identifier
/// systems ("DID methods"), of which several exist. To ensure broad interoperation
/// across the ecosystem, atproto only supports a small number of "blessed" DID methods.
///
/// Documentation: [Blessed DID Methods](https://atproto.com/specs/did#blessed-did-methods)
pub type Method {
/// Self-authenticating DID method developed specifically for use with atproto.
/// See the DID PLC [website](https://web.plc.directory/) for more details.
Plc
/// [W3C community](https://w3c-ccg.github.io/did-method-web/) draft based
/// on HTTPS (and DNS). The identifier section is a hostname.
/// This method is supported in atproto to provide an independent
/// alternative to `did:plc`.
///
/// The special localhost hostname is allowed, but only in testing and development
/// environments. Port numbers (with separating colon hex-encoded) are only
/// allowed for localhost, and only in testing and development.
///
/// Documentation: [did:web](https://atproto.com/specs/did#did-web-in-at-protocol)
Web
}
/// Parse a string into a valid `Did` type
/// If the value is not a valid string then an error is returned.
///
/// ## Examples
///
/// ```gleam
/// import possum/did
///
/// let string = "did:plc:k5vecqzf4d5mdvkcu3mx6l5g"
/// let assert Ok(did) = did.parse(string)
/// ```
pub fn parse(content: String) -> Result(Did, ParseError) {
use with <- result.try(
regexp.from_string(pattern)
|> result.replace_error(InvalidRegexPattern(pattern:)),
)
use value <- result.try(case regexp.check(with:, content:) {
True -> Ok(content)
False -> Error(InvalidSyntax(content))
})
case string.trim(value) {
"did:plc:" <> identifier -> Ok(Did(method: Plc, identifier:))
"did:web:" <> identifier -> Ok(Did(method: Web, identifier:))
value -> Error(InvalidDidMethod(value:))
}
}