···6767 end
6868 end)
69697070- foo =
7171- quote do
7272- def id, do: unquote(lexicon_id)
7373-7474- unquote_splicing(defs)
7575- end
7070+ quote do
7171+ def id, do: unquote(lexicon_id)
76727777- if lexicon.id == "app.bsky.feed.post" do
7878- IO.puts("-----")
7979- foo |> Macro.expand(__ENV__) |> Macro.to_string() |> IO.puts()
7373+ unquote_splicing(defs)
8074 end
8181-8282- foo
8375 end
84768585- # For records and objects:
8686- # - [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)
8787- # - [x] Define all keys in the schema, `@enforce`ing non-nullable/required fields
8888- # - [x] `$type` field with the full NSID
8989- # - [x] Custom JSON encoder function that omits optional fields that are `nil`, due to different semantics
9090- # - [ ] Add `$type` to schema but make it optional - allowing unbranded types through, but mismatching brand will fail.
9177 # - [ ] `t()` type should be the struct in it. (add to non-main structs too?)
92789379 @spec def_to_schema(nsid :: String.t(), def_name :: String.t(), lexicon_def :: map()) ::
···1079310894 defp def_to_schema(nsid, def_name, %{type: "record", record: record}) do
10995 # TODO: record rkey format validator
9696+ type_name = Atex.NSID.canonical_name(nsid, to_string(def_name))
9797+9898+ record =
9999+ put_in(record, [:properties, :"$type"], %{
100100+ type: "string",
101101+ const: type_name,
102102+ default: type_name
103103+ })
104104+110105 def_to_schema(nsid, def_name, record)
111106 end
112107113113- # TODO: need to spit out an extra 'branded' type with `$type` field, for use in union refs.
108108+ # TODO: add struct to types
114109 defp def_to_schema(
115110 nsid,
116111 def_name,
···158153 end)
159154160155 struct_keys =
161161- Enum.map(properties, fn
156156+ properties
157157+ |> Enum.filter(fn {key, _} -> key !== :"$type" end)
158158+ |> Enum.map(fn
162159 {key, %{default: default}} -> {key, default}
163160 {key, _field} -> {key, nil}
164164- end) ++ [{:"$type", if(def_name == :main, do: nsid, else: "#{nsid}##{def_name}")}]
161161+ end)
162162+ |> then(&(&1 ++ [{:"$type", if(def_name == :main, do: nsid, else: "#{nsid}##{def_name}")}]))
165163166166- enforced_keys = properties |> Map.keys() |> Enum.filter(&(to_string(&1) in required))
164164+ enforced_keys =
165165+ properties |> Map.keys() |> Enum.filter(&(to_string(&1) in required && &1 != :"$type"))
167166168167 optional_if_nil_keys =
169168 properties
···171170 |> Enum.filter(fn key ->
172171 key = to_string(key)
173172 # TODO: what if it is nullable but not required?
174174- key not in required && key not in nullable
173173+ key not in required && key not in nullable && key != "$type"
175174 end)
176175177176 quoted_struct =
···227226 schema
228227 end
229228230230- [params, output]
229229+ # Root struct containing `params`
230230+ main =
231231+ if params do
232232+ {
233233+ :main,
234234+ nil,
235235+ quote do
236236+ %__MODULE__{params: params()}
237237+ end,
238238+ quote do
239239+ @enforce_keys [:params]
240240+ defstruct params: nil
241241+ end
242242+ }
243243+ else
244244+ {
245245+ :main,
246246+ nil,
247247+ quote do
248248+ %__MODULE__{}
249249+ end,
250250+ quote do
251251+ defstruct []
252252+ end
253253+ }
254254+ end
255255+256256+ [main, params, output]
231257 |> Enum.reject(&is_nil/1)
232258 end
233259···257283 schema
258284 end
259285260260- [params, output, input]
286286+ # Root struct containing `input`, `raw_input`, and `params`
287287+ main =
288288+ {
289289+ :main,
290290+ nil,
291291+ cond do
292292+ params && input ->
293293+ quote do
294294+ %__MODULE__{input: input(), params: params()}
295295+ end
296296+297297+ input ->
298298+ quote do
299299+ %__MODULE__{input: input()}
300300+ end
301301+302302+ params ->
303303+ quote do
304304+ %__MODULE__{raw_input: any(), params: params()}
305305+ end
306306+307307+ true ->
308308+ quote do
309309+ %__MODULE__{raw_input: any()}
310310+ end
311311+ end,
312312+ cond do
313313+ params && input ->
314314+ quote do
315315+ defstruct input: nil, params: nil
316316+ end
317317+318318+ input ->
319319+ quote do
320320+ defstruct input: nil
321321+ end
322322+323323+ params ->
324324+ quote do
325325+ defstruct raw_input: nil, params: nil
326326+ end
327327+328328+ true ->
329329+ quote do
330330+ defstruct raw_input: nil
331331+ end
332332+ end
333333+ }
334334+335335+ [main, params, output, input]
261336 |> Enum.reject(&is_nil/1)
262337 end
263338
+12-44
lib/atex/lexicon/validators/string.ex
···11defmodule Atex.Lexicon.Validators.String do
22 alias Atex.Lexicon.Validators
3344- @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-174 @type option() ::
1818- {:format, format()}
55+ {:format, String.t()}
196 | {:min_length, non_neg_integer()}
207 | {:max_length, non_neg_integer()}
218 | {:min_graphemes, non_neg_integer()}
···31183219 @record_key_re ~r"^[a-zA-Z0-9.-_:~]$"
33203434- # TODO: probably should go into a different module, one with general lexicon -> validator gen conversions
3535- @spec format_to_atom(String.t()) :: format()
3636- def format_to_atom(format) do
3737- case format do
3838- "at-identifier" -> :at_identifier
3939- "at-uri" -> :at_uri
4040- "cid" -> :cid
4141- "datetime" -> :datetime
4242- "did" -> :did
4343- "handle" -> :handle
4444- "nsid" -> :nsid
4545- "tid" -> :tid
4646- "record-key" -> :record_key
4747- "uri" -> :uri
4848- "language" -> :language
4949- _ -> raise "Unknown lexicon string format `#{format}`"
5050- end
5151- end
5252-5321 @spec validate(term(), list(option())) :: Peri.validation_result()
5422 def validate(value, options) when is_binary(value) do
5523 options
···74427543 defp validate_option(_value, {option, nil}) when option in @option_keys, do: :ok
76447777- defp validate_option(value, {:format, :at_identifier}),
4545+ defp validate_option(value, {:format, "at-identifier"}),
7846 do:
7947 Validators.boolean_validate(
8048 Atex.DID.match?(value) or Atex.Handle.match?(value),
8149 "should be a valid DID or handle"
8250 )
83518484- defp validate_option(value, {:format, :at_uri}),
5252+ defp validate_option(value, {:format, "at-uri"}),
8553 do: Validators.boolean_validate(Atex.AtURI.match?(value), "should be a valid at:// URI")
86548787- defp validate_option(value, {:format, :cid}) do
5555+ defp validate_option(value, {:format, "cid"}) do
8856 # TODO: is there a regex provided by the lexicon docs/somewhere?
8957 try do
9058 Multiformats.CID.decode(value)
···9361 end
9462 end
95639696- defp validate_option(value, {:format, :datetime}) do
6464+ defp validate_option(value, {:format, "datetime"}) do
9765 # NaiveDateTime is used over DateTime because the result isn't actually
9866 # being used, so we don't need to include a calendar library just for this.
9967 case NaiveDateTime.from_iso8601(value) do
···10270 end
10371 end
10472105105- defp validate_option(value, {:format, :did}),
7373+ defp validate_option(value, {:format, "did"}),
10674 do: Validators.boolean_validate(Atex.DID.match?(value), "should be a valid DID")
10775108108- defp validate_option(value, {:format, :handle}),
7676+ defp validate_option(value, {:format, "handle"}),
10977 do: Validators.boolean_validate(Atex.Handle.match?(value), "should be a valid handle")
11078111111- defp validate_option(value, {:format, :nsid}),
7979+ defp validate_option(value, {:format, "nsid"}),
11280 do: Validators.boolean_validate(Atex.NSID.match?(value), "should be a valid NSID")
11381114114- defp validate_option(value, {:format, :tid}),
8282+ defp validate_option(value, {:format, "tid"}),
11583 do: Validators.boolean_validate(Atex.TID.match?(value), "should be a valid TID")
11684117117- defp validate_option(value, {:format, :record_key}),
8585+ defp validate_option(value, {:format, "record-key"}),
11886 do:
11987 Validators.boolean_validate(
12088 Regex.match?(@record_key_re, value),
12189 "should be a valid record key"
12290 )
12391124124- defp validate_option(value, {:format, :uri}) do
9292+ defp validate_option(value, {:format, "uri"}) do
12593 case URI.new(value) do
12694 {:ok, _} -> :ok
12795 {:error, _} -> {:error, "should be a valid URI", []}
12896 end
12997 end
13098131131- defp validate_option(value, {:format, :language}) do
9999+ defp validate_option(value, {:format, "language"}) do
132100 case Cldr.LanguageTag.parse(value) do
133101 {:ok, _} -> :ok
134102 {:error, _} -> {:error, "should be a valid BCP 47 language tag", []}
+9
lib/atex/nsid.ex
···4545 possible_fragment
4646 end
4747 end
4848+4949+ @spec canonical_name(String.t(), String.t()) :: String.t()
5050+ def canonical_name(nsid, fragment) do
5151+ if fragment == "main" do
5252+ nsid
5353+ else
5454+ "#{nsid}##{fragment}"
5555+ end
5656+ end
4857end