A set of utilities for working with the AT Protocol in Elixir.

feat: peri validation for Lexicons

array validator

ovyerus.com 5f5c37c0 252a3252

verified
+2 -1
.gitignore
··· 24 24 25 25 .envrc 26 26 .direnv 27 - .vscode/ 27 + .vscode/ 28 + .elixir_ls
-3
.vscode/settings.json
··· 1 - { 2 - "git.enabled": false 3 - }
+16
lib/atex/did.ex
··· 1 + defmodule Atex.DID do 2 + @re ~r/^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$/ 3 + @blessed_re ~r/^did:(?:plc|web):[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$/ 4 + 5 + @spec re() :: Regex.t() 6 + def re, do: @re 7 + 8 + @spec match?(String.t()) :: boolean() 9 + def match?(value), do: Regex.match?(@re, value) 10 + 11 + @spec blessed_re() :: Regex.t() 12 + def blessed_re, do: @blessed_re 13 + 14 + @spec match_blessed?(String.t()) :: boolean() 15 + def match_blessed?(value), do: Regex.match?(@blessed_re, value) 16 + end
+9
lib/atex/handle.ex
··· 1 + defmodule Atex.Handle do 2 + @re ~r/^(?:[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])?$/ 3 + 4 + @spec re() :: Regex.t() 5 + def re, do: @re 6 + 7 + @spec match?(String.t()) :: boolean() 8 + def match?(value), do: Regex.match?(@re, value) 9 + end
+76
lib/atex/lexicon/validators.ex
··· 1 + defmodule Atex.Lexicon.Validators do 2 + alias Atex.Lexicon.Validators 3 + 4 + @type blob_option() :: {:accept, list(String.t())} | {:max_size, integer()} 5 + 6 + @type blob_t() :: 7 + %{ 8 + "$type": String.t(), 9 + req: %{"$link": String.t()}, 10 + mimeType: String.t(), 11 + size: integer() 12 + } 13 + | %{} 14 + 15 + @spec string(list(Validators.String.option())) :: Peri.custom_def() 16 + def string(options \\ []), do: {:custom, {Validators.String, :validate, [options]}} 17 + 18 + @spec integer(list(Validators.Integer.option())) :: Peri.custom_def() 19 + def integer(options \\ []), do: {:custom, {Validators.Integer, :validate, [options]}} 20 + 21 + @spec array(Peri.schema_def(), list(Validators.Array.option())) :: Peri.custom_def() 22 + def array(inner_type, options \\ []) do 23 + {:ok, ^inner_type} = Peri.validate_schema(inner_type) 24 + {:custom, {Validators.Array, :validate, [inner_type, options]}} 25 + end 26 + 27 + @spec blob(list(blob_option())) :: Peri.schema_def() 28 + def blob(options \\ []) do 29 + options = Keyword.validate!(options, accept: nil, max_size: nil) 30 + accept = Keyword.get(options, :accept) 31 + max_size = Keyword.get(options, :max_size) 32 + 33 + mime_type = 34 + {:required, 35 + if(accept, 36 + do: {:string, {:regex, strings_to_re(accept)}}, 37 + else: {:string, {:regex, ~r"^.+/.+$"}} 38 + )} 39 + 40 + { 41 + :either, 42 + { 43 + # Newer blobs 44 + %{ 45 + "$type": {:required, {:literal, "blob"}}, 46 + ref: {:required, %{"$link": {:required, :string}}}, 47 + mimeType: mime_type, 48 + size: {:required, if(max_size != nil, do: {:integer, {:lte, max_size}}, else: :integer)} 49 + }, 50 + # Old deprecated blobs 51 + %{ 52 + cid: {:reqiured, :string}, 53 + mimeType: mime_type 54 + } 55 + } 56 + } 57 + end 58 + 59 + @spec boolean_validate(boolean(), String.t(), keyword() | map()) :: 60 + Peri.validation_result() 61 + def boolean_validate(success?, error_message, context \\ []) do 62 + if success? do 63 + :ok 64 + else 65 + {:error, error_message, context} 66 + end 67 + end 68 + 69 + @spec strings_to_re(list(String.t())) :: Regex.t() 70 + defp strings_to_re(strings) do 71 + strings 72 + |> Enum.map(&String.replace(&1, "*", ".+")) 73 + |> Enum.join("|") 74 + |> then(&~r/^(#{&1})$/) 75 + end 76 + end
+52
lib/atex/lexicon/validators/array.ex
··· 1 + defmodule Atex.Lexicon.Validators.Array do 2 + @type option() :: {:min_length, non_neg_integer()} | {:max_length, non_neg_integer()} 3 + 4 + @option_keys [:min_length, :max_length] 5 + 6 + # Needs type input 7 + @spec validate(Peri.schema_def(), term(), list(option())) :: Peri.validation_result() 8 + def validate(inner_type, value, options) when is_list(value) do 9 + # TODO: validate inner_type with Peri to make sure it's correct? 10 + 11 + options 12 + |> Keyword.validate!(min_length: nil, max_length: nil) 13 + |> Stream.map(&validate_option(value, &1)) 14 + |> Enum.find(:ok, fn x -> x != :ok end) 15 + |> case do 16 + :ok -> 17 + value 18 + |> Stream.map(&Peri.validate(inner_type, &1)) 19 + |> Enum.find({:ok, nil}, fn 20 + {:ok, _} -> false 21 + {:error, _} -> true 22 + end) 23 + |> case do 24 + {:ok, _} -> :ok 25 + e -> e 26 + end 27 + 28 + e -> 29 + e 30 + end 31 + end 32 + 33 + def validate(_inner_type, value, _options), 34 + do: {:error, "expected type of `array`, received #{value}", [expected: :array, actual: value]} 35 + 36 + @spec validate_option(list(), option()) :: Peri.validation_result() 37 + defp validate_option(value, option) 38 + 39 + defp validate_option(_value, {option, nil}) when option in @option_keys, do: :ok 40 + 41 + defp validate_option(value, {:min_length, expected}) when length(value) >= expected, 42 + do: :ok 43 + 44 + defp validate_option(value, {:min_length, expected}) when length(value) < expected, 45 + do: {:error, "should have a minimum length of #{expected}", [length: expected]} 46 + 47 + defp validate_option(value, {:max_length, expected}) when length(value) <= expected, 48 + do: :ok 49 + 50 + defp validate_option(value, {:max_length, expected}) when length(value) > expected, 51 + do: {:error, "should have a maximum length of #{expected}", [length: expected]} 52 + end
+55
lib/atex/lexicon/validators/integer.ex
··· 1 + defmodule Atex.Lexicon.Validators.Integer do 2 + alias Atex.Lexicon.Validators 3 + 4 + @type option() :: 5 + {:minimum, integer()} 6 + | {:maximum, integer()} 7 + | {:enum, list(integer())} 8 + | {:const, integer()} 9 + 10 + @option_keys [:minimum, :maximum, :enum, :const] 11 + 12 + @spec validate(term(), list(option())) :: Peri.validation_result() 13 + def validate(value, options) when is_integer(value) do 14 + options 15 + |> Keyword.validate!( 16 + minimum: nil, 17 + maximum: nil, 18 + enum: nil, 19 + const: nil 20 + ) 21 + |> Stream.map(&validate_option(value, &1)) 22 + |> Enum.find(:ok, fn x -> x != :ok end) 23 + end 24 + 25 + def validate(value, _options), 26 + do: 27 + {:error, "expected type of `integer`, received #{value}", 28 + [expected: :integer, actual: value]} 29 + 30 + @spec validate_option(integer(), option()) :: Peri.validation_result() 31 + defp validate_option(value, option) 32 + 33 + defp validate_option(_value, {option, nil}) when option in @option_keys, do: :ok 34 + 35 + defp validate_option(value, {:minimum, expected}) when value >= expected, do: :ok 36 + 37 + defp validate_option(value, {:minimum, expected}) when value < expected, 38 + do: {:error, "", [value: expected]} 39 + 40 + defp validate_option(value, {:maximum, expected}) when value <= expected, do: :ok 41 + 42 + defp validate_option(value, {:maximum, expected}) when value > expected, 43 + do: {:error, "", [value: expected]} 44 + 45 + defp validate_option(value, {:enum, values}), 46 + do: 47 + Validators.boolean_validate(value in values, "should be one of the expected values", 48 + enum: values 49 + ) 50 + 51 + defp validate_option(value, {:const, expected}) when value == expected, do: :ok 52 + 53 + defp validate_option(value, {:const, expected}), 54 + do: {:error, "should match constant value", [actual: value, expected: expected]} 55 + end
+182
lib/atex/lexicon/validators/string.ex
··· 1 + defmodule Atex.Lexicon.Validators.String do 2 + alias Atex.Lexicon.Validators 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 + @type option() :: 18 + {:format, format()} 19 + | {:min_length, non_neg_integer()} 20 + | {:max_length, non_neg_integer()} 21 + | {:min_graphemes, non_neg_integer()} 22 + | {:max_graphemes, non_neg_integer()} 23 + | {:enum, list(String.t())} 24 + | {:const, String.t()} 25 + 26 + @option_keys [ 27 + :format, 28 + :min_length, 29 + :max_length, 30 + :min_graphemes, 31 + :max_graphemes, 32 + :enum, 33 + :const 34 + ] 35 + 36 + @record_key_re ~r"^[a-zA-Z0-9.-_:~]$" 37 + 38 + # TODO: probably should go into a different module, one with general lexicon -> validator gen conversions 39 + @spec format_to_atom(String.t()) :: format() 40 + def format_to_atom(format) do 41 + case format do 42 + "at-identifier" -> :at_identifier 43 + "at-uri" -> :at_uri 44 + "cid" -> :cid 45 + "datetime" -> :datetime 46 + "did" -> :did 47 + "handle" -> :handle 48 + "nsid" -> :nsid 49 + "tid" -> :tid 50 + "record-key" -> :record_key 51 + "uri" -> :uri 52 + "language" -> :language 53 + _ -> raise "Unknown lexicon string format `#{format}`" 54 + end 55 + end 56 + 57 + @spec validate(term(), list(option())) :: Peri.validation_result() 58 + def validate(value, options) when is_binary(value) do 59 + options 60 + |> Keyword.validate!( 61 + format: nil, 62 + min_length: nil, 63 + max_length: nil, 64 + min_graphemes: nil, 65 + max_graphemes: nil, 66 + enum: nil, 67 + const: nil 68 + ) 69 + # Stream so we early exit at the first error. 70 + |> Stream.map(&validate_option(value, &1)) 71 + |> Enum.find(:ok, fn x -> x != :ok end) 72 + end 73 + 74 + def validate(value, _options), 75 + do: 76 + {:error, "expected type of `string`, received #{value}", [expected: :string, actual: value]} 77 + 78 + @spec validate_option(String.t(), option()) :: Peri.validation_result() 79 + defp validate_option(value, option) 80 + 81 + defp validate_option(_value, {option, nil}) when option in @option_keys, do: :ok 82 + 83 + defp validate_option(value, {:format, :at_identifier}), 84 + do: 85 + Validators.boolean_validate( 86 + Atex.DID.match?(value) or Atex.Handle.match?(value), 87 + "should be a valid DID or handle" 88 + ) 89 + 90 + defp validate_option(value, {:format, :at_uri}), 91 + do: Validators.boolean_validate(Atex.AtURI.match?(value), "should be a valid at:// URI") 92 + 93 + defp validate_option(value, {:format, :cid}) do 94 + # TODO: is there a regex provided by the lexicon docs/somewhere? 95 + try do 96 + Multiformats.CID.decode(value) 97 + rescue 98 + _ -> {:error, "should be a valid CID", []} 99 + end 100 + end 101 + 102 + defp validate_option(value, {:format, :datetime}) do 103 + # NaiveDateTime is used over DateTime because the result isn't actually 104 + # being used, so we don't need to include a calendar library just for this. 105 + case NaiveDateTime.from_iso8601(value) do 106 + {:ok, _} -> :ok 107 + {:error, _} -> {:error, "should be a valid datetime", []} 108 + end 109 + end 110 + 111 + defp validate_option(value, {:format, :did}), 112 + do: Validators.boolean_validate(Atex.DID.match?(value), "should be a valid DID") 113 + 114 + defp validate_option(value, {:format, :handle}), 115 + do: Validators.boolean_validate(Atex.Handle.match?(value), "should be a valid handle") 116 + 117 + defp validate_option(value, {:format, :nsid}), 118 + do: Validators.boolean_validate(Atex.NSID.match?(value), "should be a valid NSID") 119 + 120 + defp validate_option(value, {:format, :tid}), 121 + do: Validators.boolean_validate(Atex.TID.match?(value), "should be a valid TID") 122 + 123 + defp validate_option(value, {:format, :record_key}), 124 + do: 125 + Validators.boolean_validate( 126 + Regex.match?(@record_key_re, value), 127 + "should be a valid record key" 128 + ) 129 + 130 + defp validate_option(value, {:format, :uri}) do 131 + case URI.new(value) do 132 + {:ok, _} -> :ok 133 + {:error, _} -> {:error, "should be a valid URI", []} 134 + end 135 + end 136 + 137 + defp validate_option(value, {:format, :language}) do 138 + case Cldr.LanguageTag.parse(value) do 139 + {:ok, _} -> :ok 140 + {:error, _} -> {:error, "should be a valid BCP 47 language tag", []} 141 + end 142 + end 143 + 144 + defp validate_option(value, {:min_length, expected}) when byte_size(value) >= expected, 145 + do: :ok 146 + 147 + defp validate_option(value, {:min_length, expected}) when byte_size(value) < expected, 148 + do: {:error, "should have a minimum byte length of #{expected}", [length: expected]} 149 + 150 + defp validate_option(value, {:max_length, expected}) when byte_size(value) <= expected, 151 + do: :ok 152 + 153 + defp validate_option(value, {:max_length, expected}) when byte_size(value) > expected, 154 + do: {:error, "should have a maximum byte length of #{expected}", [length: expected]} 155 + 156 + defp validate_option(value, {:min_graphemes, expected}), 157 + do: 158 + Validators.boolean_validate( 159 + String.length(value) >= expected, 160 + "should have a minimum length of #{expected}", 161 + length: expected 162 + ) 163 + 164 + defp validate_option(value, {:max_graphemes, expected}), 165 + do: 166 + Validators.boolean_validate( 167 + String.length(value) <= expected, 168 + "should have a maximum length of #{expected}", 169 + length: expected 170 + ) 171 + 172 + defp validate_option(value, {:enum, values}), 173 + do: 174 + Validators.boolean_validate(value in values, "should be one of the expected values", 175 + enum: values 176 + ) 177 + 178 + defp validate_option(value, {:const, expected}) when value == expected, do: :ok 179 + 180 + defp validate_option(value, {:const, expected}), 181 + do: {:error, "should match constant value", [actual: value, expected: expected]} 182 + end
+12
lib/atex/nsid.ex
··· 1 + defmodule Atex.NSID do 2 + @re ~r/^[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})?)$/ 3 + 4 + @spec re() :: Regex.t() 5 + def re, do: @re 6 + 7 + @spec match?(String.t()) :: boolean() 8 + def match?(value), do: Regex.match?(@re, value) 9 + 10 + # TODO: methods for fetching the authority and name from a nsid. 11 + # maybe stuff for fetching the repo that belongs to an authority 12 + end
+21 -1
lib/atex/tid.ex
··· 122 122 """ 123 123 @spec decode(String.t()) :: {:ok, t()} | :error 124 124 def decode(<<timestamp::binary-size(11), clock_id::binary-size(2)>> = tid) do 125 - if Regex.match?(@re, tid) do 125 + if match?(tid) do 126 126 timestamp = Base32Sortable.decode(timestamp) 127 127 clock_id = Base32Sortable.decode(clock_id) 128 128 ··· 162 162 clock_id = (tid.clock_id &&& 1023) |> Base32Sortable.encode() |> String.pad_leading(2, "2") 163 163 timestamp <> clock_id 164 164 end 165 + 166 + @doc """ 167 + Check if a given string matches the format for a TID. 168 + 169 + ## Examples 170 + 171 + iex> Atex.TID.match?("3jzfcijpj2z2a") 172 + true 173 + 174 + iex> Atex.TID.match?("2222222222222") 175 + true 176 + 177 + iex> Atex.TID.match?("banana") 178 + false 179 + 180 + iex> Atex.TID.match?("kjzfcijpj2z2a") 181 + false 182 + """ 183 + @spec match?(String.t()) :: boolean() 184 + def match?(value), do: Regex.match?(@re, value) 165 185 end 166 186 167 187 defimpl String.Chars, for: Atex.TID do
+1
mix.exs
··· 32 32 {:recase, "~> 0.5"}, 33 33 {:req, "~> 0.5"}, 34 34 {:typedstruct, "~> 0.5"}, 35 + {:ex_cldr, "~> 2.42"}, 35 36 {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, 36 37 {:ex_doc, "~> 0.34", only: :dev, runtime: false, warn_if_outdated: true} 37 38 ]
+5 -2
mix.lock
··· 1 1 %{ 2 2 "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, 3 + "cldr_utils": {:hex, :cldr_utils, "2.28.3", "d0ac5ed25913349dfaca8b7fe14722d588d8ccfa3e335b0510c7cc3f3c54d4e6", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "40083cd9a5d187f12d675cfeeb39285f0d43e7b7f2143765161b72205d57ffb5"}, 3 4 "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"}, 5 + "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, 4 6 "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, 7 + "ex_cldr": {:hex, :ex_cldr, "2.42.0", "17ea930e88b8802b330e1c1e288cdbaba52cbfafcccf371ed34b299a47101ffb", [:mix], [{:cldr_utils, "~> 2.28", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.19", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: true]}], "hexpm", "07264a7225810ecae6bdd6715d8800c037a1248dc0063923cddc4ca3c4888df6"}, 5 8 "ex_doc": {:hex, :ex_doc, "0.38.2", "504d25eef296b4dec3b8e33e810bc8b5344d565998cd83914ffe1b8503737c02", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "732f2d972e42c116a70802f9898c51b54916e542cc50968ac6980512ec90f42b"}, 6 9 "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, 7 10 "finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"}, ··· 16 19 "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, 17 20 "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, 18 21 "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, 19 - "peri": {:hex, :peri, "0.5.0", "c71e57d1c9abd26ae05f82cefb3a3f19ec2cf19602385a329843679af15a3082", [:mix], [{:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:stream_data, "~> 1.1", [hex: :stream_data, repo: "hexpm", optional: true]}], "hexpm", "526a93bfae9ba567f7cb0e87694de68b9e708e038a2cec7a3001851bcd4bfe71"}, 22 + "peri": {:hex, :peri, "0.5.1", "2140fd94095282aea1435c98307f25dde42005d319abb9927179301c310619c1", [:mix], [{:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:stream_data, "~> 1.1", [hex: :stream_data, repo: "hexpm", optional: true]}], "hexpm", "c214590d3bdf9d0e5f6d36df1cc87d956b7625c9ba32ca786983ba6df1936be3"}, 20 23 "recase": {:hex, :recase, "0.8.1", "ab98cd35857a86fa5ca99036f575241d71d77d9c2ab0c39aacf1c9b61f6f7d1d", [:mix], [], "hexpm", "9fd8d63e7e43bd9ea385b12364e305778b2bbd92537e95c4b2e26fc507d5e4c2"}, 21 - "req": {:hex, :req, "0.5.10", "a3a063eab8b7510785a467f03d30a8d95f66f5c3d9495be3474b61459c54376c", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "8a604815743f8a2d3b5de0659fa3137fa4b1cffd636ecb69b30b2b9b2c2559be"}, 24 + "req": {:hex, :req, "0.5.12", "7ce85835867a114c28b6cfc2d8a412f86660290907315ceb173a00e587b853d2", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "d65f3d0e7032eb245706554cb5240dbe7a07493154e2dd34e7bb65001aa6ef32"}, 22 25 "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, 23 26 "typedstruct": {:hex, :typedstruct, "0.5.3", "d68ae424251a41b81a8d0c485328ab48edbd3858f3565bbdac21b43c056fc9b4", [:make, :mix], [], "hexpm", "b53b8186701417c0b2782bf02a2db5524f879b8488f91d1d83b97d84c2943432"}, 24 27 "varint": {:hex, :varint, "1.5.1", "17160c70d0428c3f8a7585e182468cac10bbf165c2360cf2328aaa39d3fb1795", [:mix], [], "hexpm", "24f3deb61e91cb988056de79d06f01161dd01be5e0acae61d8d936a552f1be73"},