···11+defmodule Atex.Lexicon.Validators.Array do
22+ @type option() :: {:min_length, non_neg_integer()} | {:max_length, non_neg_integer()}
33+44+ @option_keys [:min_length, :max_length]
55+66+ # Needs type input
77+ @spec validate(Peri.schema_def(), term(), list(option())) :: Peri.validation_result()
88+ def validate(inner_type, value, options) when is_list(value) do
99+ # TODO: validate inner_type with Peri to make sure it's correct?
1010+1111+ options
1212+ |> Keyword.validate!(min_length: nil, max_length: nil)
1313+ |> Stream.map(&validate_option(value, &1))
1414+ |> Enum.find(:ok, fn x -> x != :ok end)
1515+ |> case do
1616+ :ok ->
1717+ value
1818+ |> Stream.map(&Peri.validate(inner_type, &1))
1919+ |> Enum.find({:ok, nil}, fn
2020+ {:ok, _} -> false
2121+ {:error, _} -> true
2222+ end)
2323+ |> case do
2424+ {:ok, _} -> :ok
2525+ e -> e
2626+ end
2727+2828+ e ->
2929+ e
3030+ end
3131+ end
3232+3333+ def validate(_inner_type, value, _options),
3434+ do: {:error, "expected type of `array`, received #{value}", [expected: :array, actual: value]}
3535+3636+ @spec validate_option(list(), option()) :: Peri.validation_result()
3737+ defp validate_option(value, option)
3838+3939+ defp validate_option(_value, {option, nil}) when option in @option_keys, do: :ok
4040+4141+ defp validate_option(value, {:min_length, expected}) when length(value) >= expected,
4242+ do: :ok
4343+4444+ defp validate_option(value, {:min_length, expected}) when length(value) < expected,
4545+ do: {:error, "should have a minimum length of #{expected}", [length: expected]}
4646+4747+ defp validate_option(value, {:max_length, expected}) when length(value) <= expected,
4848+ do: :ok
4949+5050+ defp validate_option(value, {:max_length, expected}) when length(value) > expected,
5151+ do: {:error, "should have a maximum length of #{expected}", [length: expected]}
5252+end
+55
lib/atex/lexicon/validators/integer.ex
···11+defmodule Atex.Lexicon.Validators.Integer do
22+ alias Atex.Lexicon.Validators
33+44+ @type option() ::
55+ {:minimum, integer()}
66+ | {:maximum, integer()}
77+ | {:enum, list(integer())}
88+ | {:const, integer()}
99+1010+ @option_keys [:minimum, :maximum, :enum, :const]
1111+1212+ @spec validate(term(), list(option())) :: Peri.validation_result()
1313+ def validate(value, options) when is_integer(value) do
1414+ options
1515+ |> Keyword.validate!(
1616+ minimum: nil,
1717+ maximum: nil,
1818+ enum: nil,
1919+ const: nil
2020+ )
2121+ |> Stream.map(&validate_option(value, &1))
2222+ |> Enum.find(:ok, fn x -> x != :ok end)
2323+ end
2424+2525+ def validate(value, _options),
2626+ do:
2727+ {:error, "expected type of `integer`, received #{value}",
2828+ [expected: :integer, actual: value]}
2929+3030+ @spec validate_option(integer(), option()) :: Peri.validation_result()
3131+ defp validate_option(value, option)
3232+3333+ defp validate_option(_value, {option, nil}) when option in @option_keys, do: :ok
3434+3535+ defp validate_option(value, {:minimum, expected}) when value >= expected, do: :ok
3636+3737+ defp validate_option(value, {:minimum, expected}) when value < expected,
3838+ do: {:error, "", [value: expected]}
3939+4040+ defp validate_option(value, {:maximum, expected}) when value <= expected, do: :ok
4141+4242+ defp validate_option(value, {:maximum, expected}) when value > expected,
4343+ do: {:error, "", [value: expected]}
4444+4545+ defp validate_option(value, {:enum, values}),
4646+ do:
4747+ Validators.boolean_validate(value in values, "should be one of the expected values",
4848+ enum: values
4949+ )
5050+5151+ defp validate_option(value, {:const, expected}) when value == expected, do: :ok
5252+5353+ defp validate_option(value, {:const, expected}),
5454+ do: {:error, "should match constant value", [actual: value, expected: expected]}
5555+end
+182
lib/atex/lexicon/validators/string.ex
···11+defmodule Atex.Lexicon.Validators.String do
22+ alias Atex.Lexicon.Validators
33+44+ @type format() ::
55+ :at_identifier
66+ | :at_uri
77+ | :cid
88+ | :datetime
99+ | :did
1010+ | :handle
1111+ | :nsid
1212+ | :tid
1313+ | :record_key
1414+ | :uri
1515+ | :language
1616+1717+ @type option() ::
1818+ {:format, format()}
1919+ | {:min_length, non_neg_integer()}
2020+ | {:max_length, non_neg_integer()}
2121+ | {:min_graphemes, non_neg_integer()}
2222+ | {:max_graphemes, non_neg_integer()}
2323+ | {:enum, list(String.t())}
2424+ | {:const, String.t()}
2525+2626+ @option_keys [
2727+ :format,
2828+ :min_length,
2929+ :max_length,
3030+ :min_graphemes,
3131+ :max_graphemes,
3232+ :enum,
3333+ :const
3434+ ]
3535+3636+ @record_key_re ~r"^[a-zA-Z0-9.-_:~]$"
3737+3838+ # TODO: probably should go into a different module, one with general lexicon -> validator gen conversions
3939+ @spec format_to_atom(String.t()) :: format()
4040+ def format_to_atom(format) do
4141+ case format do
4242+ "at-identifier" -> :at_identifier
4343+ "at-uri" -> :at_uri
4444+ "cid" -> :cid
4545+ "datetime" -> :datetime
4646+ "did" -> :did
4747+ "handle" -> :handle
4848+ "nsid" -> :nsid
4949+ "tid" -> :tid
5050+ "record-key" -> :record_key
5151+ "uri" -> :uri
5252+ "language" -> :language
5353+ _ -> raise "Unknown lexicon string format `#{format}`"
5454+ end
5555+ end
5656+5757+ @spec validate(term(), list(option())) :: Peri.validation_result()
5858+ def validate(value, options) when is_binary(value) do
5959+ options
6060+ |> Keyword.validate!(
6161+ format: nil,
6262+ min_length: nil,
6363+ max_length: nil,
6464+ min_graphemes: nil,
6565+ max_graphemes: nil,
6666+ enum: nil,
6767+ const: nil
6868+ )
6969+ # Stream so we early exit at the first error.
7070+ |> Stream.map(&validate_option(value, &1))
7171+ |> Enum.find(:ok, fn x -> x != :ok end)
7272+ end
7373+7474+ def validate(value, _options),
7575+ do:
7676+ {:error, "expected type of `string`, received #{value}", [expected: :string, actual: value]}
7777+7878+ @spec validate_option(String.t(), option()) :: Peri.validation_result()
7979+ defp validate_option(value, option)
8080+8181+ defp validate_option(_value, {option, nil}) when option in @option_keys, do: :ok
8282+8383+ defp validate_option(value, {:format, :at_identifier}),
8484+ do:
8585+ Validators.boolean_validate(
8686+ Atex.DID.match?(value) or Atex.Handle.match?(value),
8787+ "should be a valid DID or handle"
8888+ )
8989+9090+ defp validate_option(value, {:format, :at_uri}),
9191+ do: Validators.boolean_validate(Atex.AtURI.match?(value), "should be a valid at:// URI")
9292+9393+ defp validate_option(value, {:format, :cid}) do
9494+ # TODO: is there a regex provided by the lexicon docs/somewhere?
9595+ try do
9696+ Multiformats.CID.decode(value)
9797+ rescue
9898+ _ -> {:error, "should be a valid CID", []}
9999+ end
100100+ end
101101+102102+ defp validate_option(value, {:format, :datetime}) do
103103+ # NaiveDateTime is used over DateTime because the result isn't actually
104104+ # being used, so we don't need to include a calendar library just for this.
105105+ case NaiveDateTime.from_iso8601(value) do
106106+ {:ok, _} -> :ok
107107+ {:error, _} -> {:error, "should be a valid datetime", []}
108108+ end
109109+ end
110110+111111+ defp validate_option(value, {:format, :did}),
112112+ do: Validators.boolean_validate(Atex.DID.match?(value), "should be a valid DID")
113113+114114+ defp validate_option(value, {:format, :handle}),
115115+ do: Validators.boolean_validate(Atex.Handle.match?(value), "should be a valid handle")
116116+117117+ defp validate_option(value, {:format, :nsid}),
118118+ do: Validators.boolean_validate(Atex.NSID.match?(value), "should be a valid NSID")
119119+120120+ defp validate_option(value, {:format, :tid}),
121121+ do: Validators.boolean_validate(Atex.TID.match?(value), "should be a valid TID")
122122+123123+ defp validate_option(value, {:format, :record_key}),
124124+ do:
125125+ Validators.boolean_validate(
126126+ Regex.match?(@record_key_re, value),
127127+ "should be a valid record key"
128128+ )
129129+130130+ defp validate_option(value, {:format, :uri}) do
131131+ case URI.new(value) do
132132+ {:ok, _} -> :ok
133133+ {:error, _} -> {:error, "should be a valid URI", []}
134134+ end
135135+ end
136136+137137+ defp validate_option(value, {:format, :language}) do
138138+ case Cldr.LanguageTag.parse(value) do
139139+ {:ok, _} -> :ok
140140+ {:error, _} -> {:error, "should be a valid BCP 47 language tag", []}
141141+ end
142142+ end
143143+144144+ defp validate_option(value, {:min_length, expected}) when byte_size(value) >= expected,
145145+ do: :ok
146146+147147+ defp validate_option(value, {:min_length, expected}) when byte_size(value) < expected,
148148+ do: {:error, "should have a minimum byte length of #{expected}", [length: expected]}
149149+150150+ defp validate_option(value, {:max_length, expected}) when byte_size(value) <= expected,
151151+ do: :ok
152152+153153+ defp validate_option(value, {:max_length, expected}) when byte_size(value) > expected,
154154+ do: {:error, "should have a maximum byte length of #{expected}", [length: expected]}
155155+156156+ defp validate_option(value, {:min_graphemes, expected}),
157157+ do:
158158+ Validators.boolean_validate(
159159+ String.length(value) >= expected,
160160+ "should have a minimum length of #{expected}",
161161+ length: expected
162162+ )
163163+164164+ defp validate_option(value, {:max_graphemes, expected}),
165165+ do:
166166+ Validators.boolean_validate(
167167+ String.length(value) <= expected,
168168+ "should have a maximum length of #{expected}",
169169+ length: expected
170170+ )
171171+172172+ defp validate_option(value, {:enum, values}),
173173+ do:
174174+ Validators.boolean_validate(value in values, "should be one of the expected values",
175175+ enum: values
176176+ )
177177+178178+ defp validate_option(value, {:const, expected}) when value == expected, do: :ok
179179+180180+ defp validate_option(value, {:const, expected}),
181181+ do: {:error, "should match constant value", [actual: value, expected: expected]}
182182+end
+12
lib/atex/nsid.ex
···11+defmodule Atex.NSID do
22+ @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})?)$/
33+44+ @spec re() :: Regex.t()
55+ def re, do: @re
66+77+ @spec match?(String.t()) :: boolean()
88+ def match?(value), do: Regex.match?(@re, value)
99+1010+ # TODO: methods for fetching the authority and name from a nsid.
1111+ # maybe stuff for fetching the repo that belongs to an authority
1212+end
+21-1
lib/atex/tid.ex
···122122 """
123123 @spec decode(String.t()) :: {:ok, t()} | :error
124124 def decode(<<timestamp::binary-size(11), clock_id::binary-size(2)>> = tid) do
125125- if Regex.match?(@re, tid) do
125125+ if match?(tid) do
126126 timestamp = Base32Sortable.decode(timestamp)
127127 clock_id = Base32Sortable.decode(clock_id)
128128···162162 clock_id = (tid.clock_id &&& 1023) |> Base32Sortable.encode() |> String.pad_leading(2, "2")
163163 timestamp <> clock_id
164164 end
165165+166166+ @doc """
167167+ Check if a given string matches the format for a TID.
168168+169169+ ## Examples
170170+171171+ iex> Atex.TID.match?("3jzfcijpj2z2a")
172172+ true
173173+174174+ iex> Atex.TID.match?("2222222222222")
175175+ true
176176+177177+ iex> Atex.TID.match?("banana")
178178+ false
179179+180180+ iex> Atex.TID.match?("kjzfcijpj2z2a")
181181+ false
182182+ """
183183+ @spec match?(String.t()) :: boolean()
184184+ def match?(value), do: Regex.match?(@re, value)
165185end
166186167187defimpl String.Chars, for: Atex.TID do