An Elixir toolkit for the AT Protocol. hexdocs.pm/atex
elixir bluesky atproto decentralization

feat(lexicons): add structs for queries and procedures

ovyerus.com 75981faa 6c7c464a

verified
+120 -68
+99 -24
lib/atex/lexicon.ex
··· 67 67 end 68 68 end) 69 69 70 - foo = 71 - quote do 72 - def id, do: unquote(lexicon_id) 73 - 74 - unquote_splicing(defs) 75 - end 70 + quote do 71 + def id, do: unquote(lexicon_id) 76 72 77 - if lexicon.id == "app.bsky.feed.post" do 78 - IO.puts("-----") 79 - foo |> Macro.expand(__ENV__) |> Macro.to_string() |> IO.puts() 73 + unquote_splicing(defs) 80 74 end 81 - 82 - foo 83 75 end 84 76 85 - # For records and objects: 86 - # - [x] `main` is in core module, otherwise nested with its name (should probably be handled above instead of in `def_to_schema`, like expanding typespecs) 87 - # - [x] Define all keys in the schema, `@enforce`ing non-nullable/required fields 88 - # - [x] `$type` field with the full NSID 89 - # - [x] Custom JSON encoder function that omits optional fields that are `nil`, due to different semantics 90 - # - [ ] Add `$type` to schema but make it optional - allowing unbranded types through, but mismatching brand will fail. 91 77 # - [ ] `t()` type should be the struct in it. (add to non-main structs too?) 92 78 93 79 @spec def_to_schema(nsid :: String.t(), def_name :: String.t(), lexicon_def :: map()) :: ··· 107 93 108 94 defp def_to_schema(nsid, def_name, %{type: "record", record: record}) do 109 95 # TODO: record rkey format validator 96 + type_name = Atex.NSID.canonical_name(nsid, to_string(def_name)) 97 + 98 + record = 99 + put_in(record, [:properties, :"$type"], %{ 100 + type: "string", 101 + const: type_name, 102 + default: type_name 103 + }) 104 + 110 105 def_to_schema(nsid, def_name, record) 111 106 end 112 107 113 - # TODO: need to spit out an extra 'branded' type with `$type` field, for use in union refs. 108 + # TODO: add struct to types 114 109 defp def_to_schema( 115 110 nsid, 116 111 def_name, ··· 158 153 end) 159 154 160 155 struct_keys = 161 - Enum.map(properties, fn 156 + properties 157 + |> Enum.filter(fn {key, _} -> key !== :"$type" end) 158 + |> Enum.map(fn 162 159 {key, %{default: default}} -> {key, default} 163 160 {key, _field} -> {key, nil} 164 - end) ++ [{:"$type", if(def_name == :main, do: nsid, else: "#{nsid}##{def_name}")}] 161 + end) 162 + |> then(&(&1 ++ [{:"$type", if(def_name == :main, do: nsid, else: "#{nsid}##{def_name}")}])) 165 163 166 - enforced_keys = properties |> Map.keys() |> Enum.filter(&(to_string(&1) in required)) 164 + enforced_keys = 165 + properties |> Map.keys() |> Enum.filter(&(to_string(&1) in required && &1 != :"$type")) 167 166 168 167 optional_if_nil_keys = 169 168 properties ··· 171 170 |> Enum.filter(fn key -> 172 171 key = to_string(key) 173 172 # TODO: what if it is nullable but not required? 174 - key not in required && key not in nullable 173 + key not in required && key not in nullable && key != "$type" 175 174 end) 176 175 177 176 quoted_struct = ··· 227 226 schema 228 227 end 229 228 230 - [params, output] 229 + # Root struct containing `params` 230 + main = 231 + if params do 232 + { 233 + :main, 234 + nil, 235 + quote do 236 + %__MODULE__{params: params()} 237 + end, 238 + quote do 239 + @enforce_keys [:params] 240 + defstruct params: nil 241 + end 242 + } 243 + else 244 + { 245 + :main, 246 + nil, 247 + quote do 248 + %__MODULE__{} 249 + end, 250 + quote do 251 + defstruct [] 252 + end 253 + } 254 + end 255 + 256 + [main, params, output] 231 257 |> Enum.reject(&is_nil/1) 232 258 end 233 259 ··· 257 283 schema 258 284 end 259 285 260 - [params, output, input] 286 + # Root struct containing `input`, `raw_input`, and `params` 287 + main = 288 + { 289 + :main, 290 + nil, 291 + cond do 292 + params && input -> 293 + quote do 294 + %__MODULE__{input: input(), params: params()} 295 + end 296 + 297 + input -> 298 + quote do 299 + %__MODULE__{input: input()} 300 + end 301 + 302 + params -> 303 + quote do 304 + %__MODULE__{raw_input: any(), params: params()} 305 + end 306 + 307 + true -> 308 + quote do 309 + %__MODULE__{raw_input: any()} 310 + end 311 + end, 312 + cond do 313 + params && input -> 314 + quote do 315 + defstruct input: nil, params: nil 316 + end 317 + 318 + input -> 319 + quote do 320 + defstruct input: nil 321 + end 322 + 323 + params -> 324 + quote do 325 + defstruct raw_input: nil, params: nil 326 + end 327 + 328 + true -> 329 + quote do 330 + defstruct raw_input: nil 331 + end 332 + end 333 + } 334 + 335 + [main, params, output, input] 261 336 |> Enum.reject(&is_nil/1) 262 337 end 263 338
+12 -44
lib/atex/lexicon/validators/string.ex
··· 1 1 defmodule Atex.Lexicon.Validators.String do 2 2 alias Atex.Lexicon.Validators 3 3 4 - @type format() :: 5 - :at_identifier 6 - | :at_uri 7 - | :cid 8 - | :datetime 9 - | :did 10 - | :handle 11 - | :nsid 12 - | :tid 13 - | :record_key 14 - | :uri 15 - | :language 16 - 17 4 @type option() :: 18 - {:format, format()} 5 + {:format, String.t()} 19 6 | {:min_length, non_neg_integer()} 20 7 | {:max_length, non_neg_integer()} 21 8 | {:min_graphemes, non_neg_integer()} ··· 31 18 32 19 @record_key_re ~r"^[a-zA-Z0-9.-_:~]$" 33 20 34 - # TODO: probably should go into a different module, one with general lexicon -> validator gen conversions 35 - @spec format_to_atom(String.t()) :: format() 36 - def format_to_atom(format) do 37 - case format do 38 - "at-identifier" -> :at_identifier 39 - "at-uri" -> :at_uri 40 - "cid" -> :cid 41 - "datetime" -> :datetime 42 - "did" -> :did 43 - "handle" -> :handle 44 - "nsid" -> :nsid 45 - "tid" -> :tid 46 - "record-key" -> :record_key 47 - "uri" -> :uri 48 - "language" -> :language 49 - _ -> raise "Unknown lexicon string format `#{format}`" 50 - end 51 - end 52 - 53 21 @spec validate(term(), list(option())) :: Peri.validation_result() 54 22 def validate(value, options) when is_binary(value) do 55 23 options ··· 74 42 75 43 defp validate_option(_value, {option, nil}) when option in @option_keys, do: :ok 76 44 77 - defp validate_option(value, {:format, :at_identifier}), 45 + defp validate_option(value, {:format, "at-identifier"}), 78 46 do: 79 47 Validators.boolean_validate( 80 48 Atex.DID.match?(value) or Atex.Handle.match?(value), 81 49 "should be a valid DID or handle" 82 50 ) 83 51 84 - defp validate_option(value, {:format, :at_uri}), 52 + defp validate_option(value, {:format, "at-uri"}), 85 53 do: Validators.boolean_validate(Atex.AtURI.match?(value), "should be a valid at:// URI") 86 54 87 - defp validate_option(value, {:format, :cid}) do 55 + defp validate_option(value, {:format, "cid"}) do 88 56 # TODO: is there a regex provided by the lexicon docs/somewhere? 89 57 try do 90 58 Multiformats.CID.decode(value) ··· 93 61 end 94 62 end 95 63 96 - defp validate_option(value, {:format, :datetime}) do 64 + defp validate_option(value, {:format, "datetime"}) do 97 65 # NaiveDateTime is used over DateTime because the result isn't actually 98 66 # being used, so we don't need to include a calendar library just for this. 99 67 case NaiveDateTime.from_iso8601(value) do ··· 102 70 end 103 71 end 104 72 105 - defp validate_option(value, {:format, :did}), 73 + defp validate_option(value, {:format, "did"}), 106 74 do: Validators.boolean_validate(Atex.DID.match?(value), "should be a valid DID") 107 75 108 - defp validate_option(value, {:format, :handle}), 76 + defp validate_option(value, {:format, "handle"}), 109 77 do: Validators.boolean_validate(Atex.Handle.match?(value), "should be a valid handle") 110 78 111 - defp validate_option(value, {:format, :nsid}), 79 + defp validate_option(value, {:format, "nsid"}), 112 80 do: Validators.boolean_validate(Atex.NSID.match?(value), "should be a valid NSID") 113 81 114 - defp validate_option(value, {:format, :tid}), 82 + defp validate_option(value, {:format, "tid"}), 115 83 do: Validators.boolean_validate(Atex.TID.match?(value), "should be a valid TID") 116 84 117 - defp validate_option(value, {:format, :record_key}), 85 + defp validate_option(value, {:format, "record-key"}), 118 86 do: 119 87 Validators.boolean_validate( 120 88 Regex.match?(@record_key_re, value), 121 89 "should be a valid record key" 122 90 ) 123 91 124 - defp validate_option(value, {:format, :uri}) do 92 + defp validate_option(value, {:format, "uri"}) do 125 93 case URI.new(value) do 126 94 {:ok, _} -> :ok 127 95 {:error, _} -> {:error, "should be a valid URI", []} 128 96 end 129 97 end 130 98 131 - defp validate_option(value, {:format, :language}) do 99 + defp validate_option(value, {:format, "language"}) do 132 100 case Cldr.LanguageTag.parse(value) do 133 101 {:ok, _} -> :ok 134 102 {:error, _} -> {:error, "should be a valid BCP 47 language tag", []}
+9
lib/atex/nsid.ex
··· 45 45 possible_fragment 46 46 end 47 47 end 48 + 49 + @spec canonical_name(String.t(), String.t()) :: String.t() 50 + def canonical_name(nsid, fragment) do 51 + if fragment == "main" do 52 + nsid 53 + else 54 + "#{nsid}##{fragment}" 55 + end 56 + end 48 57 end