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
···
.envrc
.direnv
-
.vscode/
+
.vscode/
+
.elixir_ls
-3
.vscode/settings.json
···
-
{
-
"git.enabled": false
-
}
+16
lib/atex/did.ex
···
+
defmodule Atex.DID do
+
@re ~r/^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$/
+
@blessed_re ~r/^did:(?:plc|web):[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$/
+
+
@spec re() :: Regex.t()
+
def re, do: @re
+
+
@spec match?(String.t()) :: boolean()
+
def match?(value), do: Regex.match?(@re, value)
+
+
@spec blessed_re() :: Regex.t()
+
def blessed_re, do: @blessed_re
+
+
@spec match_blessed?(String.t()) :: boolean()
+
def match_blessed?(value), do: Regex.match?(@blessed_re, value)
+
end
+9
lib/atex/handle.ex
···
+
defmodule Atex.Handle do
+
@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])?$/
+
+
@spec re() :: Regex.t()
+
def re, do: @re
+
+
@spec match?(String.t()) :: boolean()
+
def match?(value), do: Regex.match?(@re, value)
+
end
+76
lib/atex/lexicon/validators.ex
···
+
defmodule Atex.Lexicon.Validators do
+
alias Atex.Lexicon.Validators
+
+
@type blob_option() :: {:accept, list(String.t())} | {:max_size, integer()}
+
+
@type blob_t() ::
+
%{
+
"$type": String.t(),
+
req: %{"$link": String.t()},
+
mimeType: String.t(),
+
size: integer()
+
}
+
| %{}
+
+
@spec string(list(Validators.String.option())) :: Peri.custom_def()
+
def string(options \\ []), do: {:custom, {Validators.String, :validate, [options]}}
+
+
@spec integer(list(Validators.Integer.option())) :: Peri.custom_def()
+
def integer(options \\ []), do: {:custom, {Validators.Integer, :validate, [options]}}
+
+
@spec array(Peri.schema_def(), list(Validators.Array.option())) :: Peri.custom_def()
+
def array(inner_type, options \\ []) do
+
{:ok, ^inner_type} = Peri.validate_schema(inner_type)
+
{:custom, {Validators.Array, :validate, [inner_type, options]}}
+
end
+
+
@spec blob(list(blob_option())) :: Peri.schema_def()
+
def blob(options \\ []) do
+
options = Keyword.validate!(options, accept: nil, max_size: nil)
+
accept = Keyword.get(options, :accept)
+
max_size = Keyword.get(options, :max_size)
+
+
mime_type =
+
{:required,
+
if(accept,
+
do: {:string, {:regex, strings_to_re(accept)}},
+
else: {:string, {:regex, ~r"^.+/.+$"}}
+
)}
+
+
{
+
:either,
+
{
+
# Newer blobs
+
%{
+
"$type": {:required, {:literal, "blob"}},
+
ref: {:required, %{"$link": {:required, :string}}},
+
mimeType: mime_type,
+
size: {:required, if(max_size != nil, do: {:integer, {:lte, max_size}}, else: :integer)}
+
},
+
# Old deprecated blobs
+
%{
+
cid: {:reqiured, :string},
+
mimeType: mime_type
+
}
+
}
+
}
+
end
+
+
@spec boolean_validate(boolean(), String.t(), keyword() | map()) ::
+
Peri.validation_result()
+
def boolean_validate(success?, error_message, context \\ []) do
+
if success? do
+
:ok
+
else
+
{:error, error_message, context}
+
end
+
end
+
+
@spec strings_to_re(list(String.t())) :: Regex.t()
+
defp strings_to_re(strings) do
+
strings
+
|> Enum.map(&String.replace(&1, "*", ".+"))
+
|> Enum.join("|")
+
|> then(&~r/^(#{&1})$/)
+
end
+
end
+52
lib/atex/lexicon/validators/array.ex
···
+
defmodule Atex.Lexicon.Validators.Array do
+
@type option() :: {:min_length, non_neg_integer()} | {:max_length, non_neg_integer()}
+
+
@option_keys [:min_length, :max_length]
+
+
# Needs type input
+
@spec validate(Peri.schema_def(), term(), list(option())) :: Peri.validation_result()
+
def validate(inner_type, value, options) when is_list(value) do
+
# TODO: validate inner_type with Peri to make sure it's correct?
+
+
options
+
|> Keyword.validate!(min_length: nil, max_length: nil)
+
|> Stream.map(&validate_option(value, &1))
+
|> Enum.find(:ok, fn x -> x != :ok end)
+
|> case do
+
:ok ->
+
value
+
|> Stream.map(&Peri.validate(inner_type, &1))
+
|> Enum.find({:ok, nil}, fn
+
{:ok, _} -> false
+
{:error, _} -> true
+
end)
+
|> case do
+
{:ok, _} -> :ok
+
e -> e
+
end
+
+
e ->
+
e
+
end
+
end
+
+
def validate(_inner_type, value, _options),
+
do: {:error, "expected type of `array`, received #{value}", [expected: :array, actual: value]}
+
+
@spec validate_option(list(), option()) :: Peri.validation_result()
+
defp validate_option(value, option)
+
+
defp validate_option(_value, {option, nil}) when option in @option_keys, do: :ok
+
+
defp validate_option(value, {:min_length, expected}) when length(value) >= expected,
+
do: :ok
+
+
defp validate_option(value, {:min_length, expected}) when length(value) < expected,
+
do: {:error, "should have a minimum length of #{expected}", [length: expected]}
+
+
defp validate_option(value, {:max_length, expected}) when length(value) <= expected,
+
do: :ok
+
+
defp validate_option(value, {:max_length, expected}) when length(value) > expected,
+
do: {:error, "should have a maximum length of #{expected}", [length: expected]}
+
end
+55
lib/atex/lexicon/validators/integer.ex
···
+
defmodule Atex.Lexicon.Validators.Integer do
+
alias Atex.Lexicon.Validators
+
+
@type option() ::
+
{:minimum, integer()}
+
| {:maximum, integer()}
+
| {:enum, list(integer())}
+
| {:const, integer()}
+
+
@option_keys [:minimum, :maximum, :enum, :const]
+
+
@spec validate(term(), list(option())) :: Peri.validation_result()
+
def validate(value, options) when is_integer(value) do
+
options
+
|> Keyword.validate!(
+
minimum: nil,
+
maximum: nil,
+
enum: nil,
+
const: nil
+
)
+
|> Stream.map(&validate_option(value, &1))
+
|> Enum.find(:ok, fn x -> x != :ok end)
+
end
+
+
def validate(value, _options),
+
do:
+
{:error, "expected type of `integer`, received #{value}",
+
[expected: :integer, actual: value]}
+
+
@spec validate_option(integer(), option()) :: Peri.validation_result()
+
defp validate_option(value, option)
+
+
defp validate_option(_value, {option, nil}) when option in @option_keys, do: :ok
+
+
defp validate_option(value, {:minimum, expected}) when value >= expected, do: :ok
+
+
defp validate_option(value, {:minimum, expected}) when value < expected,
+
do: {:error, "", [value: expected]}
+
+
defp validate_option(value, {:maximum, expected}) when value <= expected, do: :ok
+
+
defp validate_option(value, {:maximum, expected}) when value > expected,
+
do: {:error, "", [value: expected]}
+
+
defp validate_option(value, {:enum, values}),
+
do:
+
Validators.boolean_validate(value in values, "should be one of the expected values",
+
enum: values
+
)
+
+
defp validate_option(value, {:const, expected}) when value == expected, do: :ok
+
+
defp validate_option(value, {:const, expected}),
+
do: {:error, "should match constant value", [actual: value, expected: expected]}
+
end
+182
lib/atex/lexicon/validators/string.ex
···
+
defmodule Atex.Lexicon.Validators.String do
+
alias Atex.Lexicon.Validators
+
+
@type format() ::
+
:at_identifier
+
| :at_uri
+
| :cid
+
| :datetime
+
| :did
+
| :handle
+
| :nsid
+
| :tid
+
| :record_key
+
| :uri
+
| :language
+
+
@type option() ::
+
{:format, format()}
+
| {:min_length, non_neg_integer()}
+
| {:max_length, non_neg_integer()}
+
| {:min_graphemes, non_neg_integer()}
+
| {:max_graphemes, non_neg_integer()}
+
| {:enum, list(String.t())}
+
| {:const, String.t()}
+
+
@option_keys [
+
:format,
+
:min_length,
+
:max_length,
+
:min_graphemes,
+
:max_graphemes,
+
:enum,
+
:const
+
]
+
+
@record_key_re ~r"^[a-zA-Z0-9.-_:~]$"
+
+
# TODO: probably should go into a different module, one with general lexicon -> validator gen conversions
+
@spec format_to_atom(String.t()) :: format()
+
def format_to_atom(format) do
+
case format do
+
"at-identifier" -> :at_identifier
+
"at-uri" -> :at_uri
+
"cid" -> :cid
+
"datetime" -> :datetime
+
"did" -> :did
+
"handle" -> :handle
+
"nsid" -> :nsid
+
"tid" -> :tid
+
"record-key" -> :record_key
+
"uri" -> :uri
+
"language" -> :language
+
_ -> raise "Unknown lexicon string format `#{format}`"
+
end
+
end
+
+
@spec validate(term(), list(option())) :: Peri.validation_result()
+
def validate(value, options) when is_binary(value) do
+
options
+
|> Keyword.validate!(
+
format: nil,
+
min_length: nil,
+
max_length: nil,
+
min_graphemes: nil,
+
max_graphemes: nil,
+
enum: nil,
+
const: nil
+
)
+
# Stream so we early exit at the first error.
+
|> Stream.map(&validate_option(value, &1))
+
|> Enum.find(:ok, fn x -> x != :ok end)
+
end
+
+
def validate(value, _options),
+
do:
+
{:error, "expected type of `string`, received #{value}", [expected: :string, actual: value]}
+
+
@spec validate_option(String.t(), option()) :: Peri.validation_result()
+
defp validate_option(value, option)
+
+
defp validate_option(_value, {option, nil}) when option in @option_keys, do: :ok
+
+
defp validate_option(value, {:format, :at_identifier}),
+
do:
+
Validators.boolean_validate(
+
Atex.DID.match?(value) or Atex.Handle.match?(value),
+
"should be a valid DID or handle"
+
)
+
+
defp validate_option(value, {:format, :at_uri}),
+
do: Validators.boolean_validate(Atex.AtURI.match?(value), "should be a valid at:// URI")
+
+
defp validate_option(value, {:format, :cid}) do
+
# TODO: is there a regex provided by the lexicon docs/somewhere?
+
try do
+
Multiformats.CID.decode(value)
+
rescue
+
_ -> {:error, "should be a valid CID", []}
+
end
+
end
+
+
defp validate_option(value, {:format, :datetime}) do
+
# NaiveDateTime is used over DateTime because the result isn't actually
+
# being used, so we don't need to include a calendar library just for this.
+
case NaiveDateTime.from_iso8601(value) do
+
{:ok, _} -> :ok
+
{:error, _} -> {:error, "should be a valid datetime", []}
+
end
+
end
+
+
defp validate_option(value, {:format, :did}),
+
do: Validators.boolean_validate(Atex.DID.match?(value), "should be a valid DID")
+
+
defp validate_option(value, {:format, :handle}),
+
do: Validators.boolean_validate(Atex.Handle.match?(value), "should be a valid handle")
+
+
defp validate_option(value, {:format, :nsid}),
+
do: Validators.boolean_validate(Atex.NSID.match?(value), "should be a valid NSID")
+
+
defp validate_option(value, {:format, :tid}),
+
do: Validators.boolean_validate(Atex.TID.match?(value), "should be a valid TID")
+
+
defp validate_option(value, {:format, :record_key}),
+
do:
+
Validators.boolean_validate(
+
Regex.match?(@record_key_re, value),
+
"should be a valid record key"
+
)
+
+
defp validate_option(value, {:format, :uri}) do
+
case URI.new(value) do
+
{:ok, _} -> :ok
+
{:error, _} -> {:error, "should be a valid URI", []}
+
end
+
end
+
+
defp validate_option(value, {:format, :language}) do
+
case Cldr.LanguageTag.parse(value) do
+
{:ok, _} -> :ok
+
{:error, _} -> {:error, "should be a valid BCP 47 language tag", []}
+
end
+
end
+
+
defp validate_option(value, {:min_length, expected}) when byte_size(value) >= expected,
+
do: :ok
+
+
defp validate_option(value, {:min_length, expected}) when byte_size(value) < expected,
+
do: {:error, "should have a minimum byte length of #{expected}", [length: expected]}
+
+
defp validate_option(value, {:max_length, expected}) when byte_size(value) <= expected,
+
do: :ok
+
+
defp validate_option(value, {:max_length, expected}) when byte_size(value) > expected,
+
do: {:error, "should have a maximum byte length of #{expected}", [length: expected]}
+
+
defp validate_option(value, {:min_graphemes, expected}),
+
do:
+
Validators.boolean_validate(
+
String.length(value) >= expected,
+
"should have a minimum length of #{expected}",
+
length: expected
+
)
+
+
defp validate_option(value, {:max_graphemes, expected}),
+
do:
+
Validators.boolean_validate(
+
String.length(value) <= expected,
+
"should have a maximum length of #{expected}",
+
length: expected
+
)
+
+
defp validate_option(value, {:enum, values}),
+
do:
+
Validators.boolean_validate(value in values, "should be one of the expected values",
+
enum: values
+
)
+
+
defp validate_option(value, {:const, expected}) when value == expected, do: :ok
+
+
defp validate_option(value, {:const, expected}),
+
do: {:error, "should match constant value", [actual: value, expected: expected]}
+
end
+12
lib/atex/nsid.ex
···
+
defmodule Atex.NSID do
+
@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})?)$/
+
+
@spec re() :: Regex.t()
+
def re, do: @re
+
+
@spec match?(String.t()) :: boolean()
+
def match?(value), do: Regex.match?(@re, value)
+
+
# TODO: methods for fetching the authority and name from a nsid.
+
# maybe stuff for fetching the repo that belongs to an authority
+
end
+21 -1
lib/atex/tid.ex
···
"""
@spec decode(String.t()) :: {:ok, t()} | :error
def decode(<<timestamp::binary-size(11), clock_id::binary-size(2)>> = tid) do
-
if Regex.match?(@re, tid) do
+
if match?(tid) do
timestamp = Base32Sortable.decode(timestamp)
clock_id = Base32Sortable.decode(clock_id)
···
clock_id = (tid.clock_id &&& 1023) |> Base32Sortable.encode() |> String.pad_leading(2, "2")
timestamp <> clock_id
end
+
+
@doc """
+
Check if a given string matches the format for a TID.
+
+
## Examples
+
+
iex> Atex.TID.match?("3jzfcijpj2z2a")
+
true
+
+
iex> Atex.TID.match?("2222222222222")
+
true
+
+
iex> Atex.TID.match?("banana")
+
false
+
+
iex> Atex.TID.match?("kjzfcijpj2z2a")
+
false
+
"""
+
@spec match?(String.t()) :: boolean()
+
def match?(value), do: Regex.match?(@re, value)
end
defimpl String.Chars, for: Atex.TID do
+1
mix.exs
···
{:recase, "~> 0.5"},
{:req, "~> 0.5"},
{:typedstruct, "~> 0.5"},
+
{:ex_cldr, "~> 2.42"},
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
{:ex_doc, "~> 0.34", only: :dev, runtime: false, warn_if_outdated: true}
]
+5 -2
mix.lock
···
%{
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
+
"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"},
"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"},
+
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
"earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"},
+
"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"},
"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"},
"file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"},
"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"},
···
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
-
"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"},
+
"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"},
"recase": {:hex, :recase, "0.8.1", "ab98cd35857a86fa5ca99036f575241d71d77d9c2ab0c39aacf1c9b61f6f7d1d", [:mix], [], "hexpm", "9fd8d63e7e43bd9ea385b12364e305778b2bbd92537e95c4b2e26fc507d5e4c2"},
-
"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"},
+
"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"},
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
"typedstruct": {:hex, :typedstruct, "0.5.3", "d68ae424251a41b81a8d0c485328ab48edbd3858f3565bbdac21b43c056fc9b4", [:make, :mix], [], "hexpm", "b53b8186701417c0b2782bf02a2db5524f879b8488f91d1d83b97d84c2943432"},
"varint": {:hex, :varint, "1.5.1", "17160c70d0428c3f8a7585e182468cac10bbf165c2360cf2328aaa39d3fb1795", [:mix], [], "hexpm", "24f3deb61e91cb988056de79d06f01161dd01be5e0acae61d8d936a552f1be73"},