Skip to content

Commit 45581c8

Browse files
Chris Xuclaude
andcommitted
fix(types): accept term() for encoder input to support structs
The Toon.Encoder protocol normalizes input before encoding, so the public API should accept any term() - not just pre-normalized types. Changes: - Add Toon.Types.input() type as term() for encoder parameters - Update encode/1,2 and encode!/1,2 specs to use input() - Keep encodable() for normalized output (string-keyed maps) - Add tests for struct and atom-key encoding via public API This fixes Dialyzer warnings when calling Toon.encode!/1 with: - Ecto schemas (structs with atom keys) - Maps with atom keys - Any type implementing the Toon.Encoder protocol Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 039bd55 commit 45581c8

File tree

4 files changed

+55
-7
lines changed

4 files changed

+55
-7
lines changed

lib/toon.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ defmodule Toon do
7878
iex> Toon.encode(%{"data" => [1, 2, 3]}, delimiter: "\\t")
7979
{:ok, "data[3\\t]: 1\\t2\\t3"}
8080
"""
81-
@spec encode(Toon.Types.encodable(), keyword()) ::
81+
@spec encode(Toon.Types.input(), keyword()) ::
8282
{:ok, String.t()} | {:error, EncodeError.t()}
8383
defdelegate encode(data, opts \\ []), to: Encode
8484

@@ -96,7 +96,7 @@ defmodule Toon do
9696
iex> Toon.encode!(%{"count" => 42, "active" => true})
9797
"active: true\\ncount: 42"
9898
"""
99-
@spec encode!(Toon.Types.encodable(), keyword()) :: String.t()
99+
@spec encode!(Toon.Types.input(), keyword()) :: String.t()
100100
defdelegate encode!(data, opts \\ []), to: Encode
101101

102102
@doc """

lib/toon/encode/encode.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ defmodule Toon.Encode do
3232
iex> Toon.Encode.encode(%{"name" => "Alice"}, indent: 4)
3333
{:ok, "name: Alice"}
3434
"""
35-
@spec encode(Toon.Types.encodable(), keyword()) ::
35+
@spec encode(Toon.Types.input(), keyword()) ::
3636
{:ok, String.t()} | {:error, EncodeError.t()}
3737
def encode(data, opts \\ []) do
3838
start_time = System.monotonic_time()
@@ -99,7 +99,7 @@ defmodule Toon.Encode do
9999
iex> Toon.Encode.encode!(%{"tags" => ["a", "b"]})
100100
"tags[2]: a,b"
101101
"""
102-
@spec encode!(Toon.Types.encodable(), keyword()) :: String.t()
102+
@spec encode!(Toon.Types.input(), keyword()) :: String.t()
103103
def encode!(data, opts \\ []) do
104104
case encode(data, opts) do
105105
{:ok, result} -> result

lib/toon/shared/types.ex

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,23 @@ defmodule Toon.Types do
1212
@type primitive :: nil | boolean() | number() | String.t()
1313

1414
@typedoc """
15-
A JSON-compatible value that can be encoded to TOON format.
15+
Any value that can be passed to the encoder.
1616
17-
This is a recursive type that includes:
17+
The `Toon.Encoder` protocol normalizes input before encoding:
18+
- Structs via `@derive Toon.Encoder` or explicit implementations
19+
- Maps with atom keys are converted to string keys
20+
- Custom types implement the protocol to define their encoding
21+
22+
This is `term()` because any type with an Encoder implementation is valid.
23+
"""
24+
@type input :: term()
25+
26+
@typedoc """
27+
A normalized JSON-compatible value (output of encoding).
28+
29+
After the Encoder protocol processes input, the result is:
1830
- Primitives: `nil`, `boolean()`, `number()`, `String.t()`
19-
- Maps: `%{optional(String.t()) => encodable()}`
31+
- Maps with string keys: `%{optional(String.t()) => encodable()}`
2032
- Lists: `[encodable()]`
2133
"""
2234
@type encodable ::

test/toon/encoder_test.exs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,4 +161,40 @@ defmodule Toon.EncoderTest do
161161
assert Toon.Utils.normalize(user) == %{"name" => "Bob", "email" => "bob@test.com"}
162162
end
163163
end
164+
165+
# These tests verify the public API accepts structs and atom-keyed maps.
166+
# The Encoder protocol handles normalization, so the type specs must accept term().
167+
describe "Toon.encode!/1 accepts structs (Dialyzer compatibility)" do
168+
test "encodes struct with @derive Toon.Encoder" do
169+
person = %Person{name: "Alice", age: 30}
170+
result = Toon.encode!(person)
171+
172+
assert result =~ "name: Alice"
173+
assert result =~ "age: 30"
174+
end
175+
176+
test "encodes struct with explicit Encoder implementation" do
177+
date = %CustomDate{year: 2024, month: 6, day: 15}
178+
result = Toon.encode!(date)
179+
180+
assert result == "2024-06-15"
181+
end
182+
end
183+
184+
describe "Toon.encode!/1 accepts maps with atom keys (Dialyzer compatibility)" do
185+
test "encodes map with atom keys" do
186+
data = %{name: "Bob", active: true}
187+
result = Toon.encode!(data)
188+
189+
assert result =~ "name: Bob"
190+
assert result =~ "active: true"
191+
end
192+
193+
test "encodes nested map with atom keys" do
194+
data = %{user: %{name: "Charlie", age: 25}}
195+
result = Toon.encode!(data)
196+
197+
assert result =~ "name: Charlie"
198+
end
199+
end
164200
end

0 commit comments

Comments
 (0)