api-surface #1

Merged
luna merged 51 commits from api-surface into mistress 2026-07-05 14:43:13 +00:00
4 changed files with 27 additions and 4 deletions
Showing only changes of commit a00e455c58 - Show all commits

fix(struct): address code review feedback

- Formatting: add field/hydrate/discord_struct to locals_without_parens
  in .formatter.exs so the no-paren data-DSL style is the formatted form;
  mix format then wrapped the long codec encode_value/2 clause. Both
  previously-unformatted files now satisfy mix format --check-formatted.
- Empty-struct guard: __before_compile__ now raises ArgumentError when a
  module does 'use Dexcord.Struct' with no field/hydrate declarations,
  instead of silently generating a bare defstruct []. Added a compile-time
  test mirroring the existing Code.compile_string validation tests.
Luna 2026-07-05 03:37:48 -03:00

View file

@ -1,4 +1,5 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
locals_without_parens: [field: 2, field: 3, hydrate: 2, discord_struct: 1]
]

View file

@ -66,13 +66,22 @@ defmodule Dexcord.Struct do
defmacro __before_compile__(env) do
module = env.module
raw_fields = Module.get_attribute(module, :dexcord_fields) || []
raw_hydrates = Module.get_attribute(module, :dexcord_hydrates) || []
if raw_fields == [] and raw_hydrates == [] do
raise ArgumentError,
"#{inspect(module)}: `use Dexcord.Struct` requires a `discord_struct do ... end` " <>
"block declaring at least one `field` or `hydrate`"
end
fields =
(Module.get_attribute(module, :dexcord_fields) || [])
raw_fields
|> Enum.reverse()
|> Enum.map(fn {name, type_ast, opts} -> build_field(name, type_ast, opts, env) end)
hydrates =
(Module.get_attribute(module, :dexcord_hydrates) || [])
raw_hydrates
|> Enum.reverse()
|> Enum.map(fn {name, opts} -> build_hydrate(name, opts, env) end)

View file

@ -88,7 +88,10 @@ defmodule Dexcord.Struct.Codec do
# structs delegate straight back here, while hand-written fallback structs
# (UnknownChannel, Component.Unknown — no __fields__/0) provide their own.
defp encode_value({:struct, _mod}, %_{} = v), do: v.__struct__.to_map(v)
defp encode_value({:list, inner}, v) when is_list(v), do: Enum.map(v, &encode_element(inner, &1))
defp encode_value({:list, inner}, v) when is_list(v),
do: Enum.map(v, &encode_element(inner, &1))
defp encode_value({:enum, mod}, v) when is_atom(v) and not is_nil(v), do: mod.encode(v)
defp encode_value({:enum, _mod}, {:unknown, n}) when is_integer(n), do: n
# raw passthrough: junk scalars that decode kept raw re-encode raw; a raw

View file

@ -126,6 +126,16 @@ defmodule Dexcord.StructTest do
""")
end
end
test "use Dexcord.Struct without any field/hydrate raises at compile time" do
assert_raise ArgumentError, ~r/requires a `discord_struct`? do/, fn ->
Code.compile_string("""
defmodule Dexcord.Test.EmptyStruct do
use Dexcord.Struct
end
""")
end
end
end
describe "from_map decode (Task 5)" do