A set of utilities for working with the AT Protocol in Elixir.
at v0.2.0 5.1 kB view raw
1defmodule Atex.AtURI do 2 @moduledoc """ 3 Struct and helper functions for manipulating `at://` URIs, which identify 4 specific records within the AT Protocol. 5 6 ATProto spec: https://atproto.com/specs/at-uri-scheme 7 8 This module only supports the restricted URI syntax used for the Lexicon 9 `at-uri` type, with no support for query strings or fragments. If/when the 10 full syntax gets widespread use, this module will expand to accomodate them. 11 12 Both URIs using DIDs and handles ("example.com") are supported. 13 """ 14 15 use TypedStruct 16 17 @did ~S"did:(?:plc|web):[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]" 18 @handle ~S"(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?" 19 @nsid ~S"[a-zA-Z](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?:\.[a-zA-Z](?:[a-zA-Z0-9]{0,62})?)" 20 21 @authority "(?<authority>(?:#{@did})|(?:#{@handle}))" 22 @collection "(?<collection>#{@nsid})" 23 @rkey "(?<rkey>[a-zA-Z0-9.-_:~]{1,512})" 24 25 @re ~r"^at://#{@authority}(?:/#{@collection}(?:/#{@rkey})?)?$" 26 27 typedstruct do 28 field :authority, String.t(), enforce: true 29 field :collection, String.t() | nil 30 field :rkey, String.t() | nil 31 end 32 33 @doc """ 34 Create a new AtURI struct from a string by matching it against the regex. 35 36 Returns `{:ok, aturi}` if a valid `at://` URI is given, otherwise it will return `:error`. 37 38 ## Examples 39 40 iex> Atex.AtURI.new("at://did:plc:44ybard66vv44zksje25o7dz/app.bsky.feed.post/3jwdwj2ctlk26") 41 {:ok, %Atex.AtURI{ 42 rkey: "3jwdwj2ctlk26", 43 collection: "app.bsky.feed.post", 44 authority: "did:plc:44ybard66vv44zksje25o7dz" 45 }} 46 47 iex> Atex.AtURI.new("at:invalid/malformed") 48 :error 49 50 Partial URIs pointing to a collection without a record key, or even just a given authority, are also supported: 51 52 iex> Atex.AtURI.new("at://ovyerus.com/sh.comet.v0.feed.track") 53 {:ok, %Atex.AtURI{ 54 rkey: nil, 55 collection: "sh.comet.v0.feed.track", 56 authority: "ovyerus.com" 57 }} 58 59 iex> Atex.AtURI.new("at://did:web:comet.sh") 60 {:ok, %Atex.AtURI{ 61 rkey: nil, 62 collection: nil, 63 authority: "did:web:comet.sh" 64 }} 65 """ 66 @spec new(String.t()) :: {:ok, t()} | :error 67 def new(string) when is_binary(string) do 68 # TODO: test different ways to get a good error from regex on which part failed match? 69 case Regex.named_captures(@re, string) do 70 %{} = captures -> {:ok, from_named_captures(captures)} 71 nil -> :error 72 end 73 end 74 75 @doc """ 76 The same as `new/1` but raises an `ArgumentError` if an invalid string is given. 77 78 ## Examples 79 80 iex> Atex.AtURI.new!("at://did:plc:44ybard66vv44zksje25o7dz/app.bsky.feed.post/3jwdwj2ctlk26") 81 %Atex.AtURI{ 82 rkey: "3jwdwj2ctlk26", 83 collection: "app.bsky.feed.post", 84 authority: "did:plc:44ybard66vv44zksje25o7dz" 85 } 86 87 iex> Atex.AtURI.new!("at:invalid/malformed") 88 ** (ArgumentError) Malformed at:// URI 89 """ 90 @spec new!(String.t()) :: t() 91 def new!(string) when is_binary(string) do 92 case new(string) do 93 {:ok, uri} -> uri 94 :error -> raise ArgumentError, message: "Malformed at:// URI" 95 end 96 end 97 98 @doc """ 99 Check if a string is a valid `at://` URI. 100 101 ## Examples 102 103 iex> Atex.AtURI.match?("at://did:plc:44ybard66vv44zksje25o7dz/app.bsky.feed.post/3jwdwj2ctlk26") 104 true 105 106 iex> Atex.AtURI.match?("at://did:web:comet.sh") 107 true 108 109 iex> Atex.AtURI.match?("at://ovyerus.com/sh.comet.v0.feed.track") 110 true 111 112 iex> Atex.AtURI.match?("gobbledy gook") 113 false 114 """ 115 @spec match?(String.t()) :: boolean() 116 def match?(string), do: Regex.match?(@re, string) 117 118 @doc """ 119 Format an `Atex.AtURI` to the canonical string representation. 120 121 Also available via the `String.Chars` protocol. 122 123 ## Examples 124 125 iex> aturi = %Atex.AtURI{ 126 ...> rkey: "3jwdwj2ctlk26", 127 ...> collection: "app.bsky.feed.post", 128 ...> authority: "did:plc:44ybard66vv44zksje25o7dz" 129 ...> } 130 iex> Atex.AtURI.to_string(aturi) 131 "at://did:plc:44ybard66vv44zksje25o7dz/app.bsky.feed.post/3jwdwj2ctlk26" 132 133 iex> aturi = %Atex.AtURI{authority: "did:web:comet.sh"} 134 iex> to_string(aturi) 135 "at://did:web:comet.sh" 136 """ 137 @spec to_string(t()) :: String.t() 138 def to_string(%__MODULE__{} = uri) do 139 "at://#{uri.authority}/#{uri.collection}/#{uri.rkey}" 140 |> String.trim_trailing("/") 141 end 142 143 defp from_named_captures(%{"authority" => authority, "collection" => "", "rkey" => ""}), 144 do: %__MODULE__{authority: authority} 145 146 defp from_named_captures(%{"authority" => authority, "collection" => collection, "rkey" => ""}), 147 do: %__MODULE__{authority: authority, collection: collection} 148 149 defp from_named_captures(%{ 150 "authority" => authority, 151 "collection" => collection, 152 "rkey" => rkey 153 }), 154 do: %__MODULE__{authority: authority, collection: collection, rkey: rkey} 155end 156 157defimpl String.Chars, for: Atex.AtURI do 158 def to_string(uri), do: Atex.AtURI.to_string(uri) 159end