mirror of
https://github.com/woutdp/live_svelte
synced 2026-05-24 09:28:21 +00:00
chore: added svelte encoder
This commit is contained in:
parent
5d526021dc
commit
2de7f6d733
8 changed files with 590 additions and 6 deletions
17
README.md
17
README.md
|
|
@ -561,6 +561,23 @@ you don't need `@derive Jason.Encoder` when using the default native JSON encode
|
|||
If you're using Jason and need custom struct encoding behavior, see the
|
||||
[Structs and Ecto](#structs-and-ecto) section for details on `@derive Jason.Encoder`.
|
||||
|
||||
#### LiveSvelte.Encoder protocol
|
||||
|
||||
All props pass through the `LiveSvelte.Encoder` protocol before JSON encoding. You can control which struct fields are sent to Svelte with `@derive`:
|
||||
|
||||
```elixir
|
||||
defmodule User do
|
||||
@derive {LiveSvelte.Encoder, except: [:password]}
|
||||
defstruct [:name, :email, :password]
|
||||
end
|
||||
```
|
||||
|
||||
- `@derive LiveSvelte.Encoder` — encode all fields except `__struct__`
|
||||
- `@derive {LiveSvelte.Encoder, only: [:name, :email]}` — encode only listed keys
|
||||
- `@derive {LiveSvelte.Encoder, except: [:password]}` — encode all except listed keys
|
||||
|
||||
Phoenix.HTML.Form, Ecto.Changeset, and Phoenix LiveView upload structs have built-in encoders. Date/Time types are encoded as ISO8601 strings.
|
||||
|
||||
### live_json
|
||||
|
||||
LiveSvelte has support for [live_json](https://github.com/Miserlou/live_json).
|
||||
|
|
|
|||
|
|
@ -13,8 +13,10 @@ defmodule ExampleWeb.LiveBreakingNewsTest do
|
|||
end
|
||||
|
||||
defp wait_for_headline_with_text(session, text, attempts \\ 50) do
|
||||
items = headline_items(session)
|
||||
found = Enum.any?(items, fn el -> Wallaby.Element.text(el) =~ text end)
|
||||
# Use container text to avoid stale refs when DOM updates (e.g. after Add/Remove).
|
||||
container = session |> find(Query.css("[data-testid='breaking-news-headlines']"))
|
||||
container_text = Wallaby.Element.text(container)
|
||||
found = String.contains?(container_text, text)
|
||||
cond do
|
||||
found -> session
|
||||
attempts == 0 -> raise "timeout waiting for headline containing #{inspect(text)}"
|
||||
|
|
@ -85,8 +87,10 @@ defmodule ExampleWeb.LiveBreakingNewsTest do
|
|||
end
|
||||
|
||||
defp wait_for_headline_removed(session, text, attempts) do
|
||||
items = headline_items(session)
|
||||
found = Enum.any?(items, fn el -> Wallaby.Element.text(el) =~ text end)
|
||||
# Use container text to avoid stale refs: after Remove, li elements are detached from DOM.
|
||||
container = session |> find(Query.css("[data-testid='breaking-news-headlines']"))
|
||||
container_text = Wallaby.Element.text(container)
|
||||
found = String.contains?(container_text, text)
|
||||
cond do
|
||||
not found -> session
|
||||
attempts == 0 -> raise "timeout waiting for headline #{inspect(text)} to be removed"
|
||||
|
|
|
|||
|
|
@ -151,7 +151,16 @@ defmodule LiveSvelte do
|
|||
|
||||
defp json(props) do
|
||||
json_library = Application.get_env(:live_svelte, :json_library, LiveSvelte.JSON)
|
||||
json_library.encode!(props)
|
||||
|
||||
# Ensure props pass through LiveSvelte.Encoder for all JSON libraries.
|
||||
# LiveSvelte.JSON already runs the encoder internally, so avoid double work.
|
||||
if json_library == LiveSvelte.JSON do
|
||||
json_library.encode!(props)
|
||||
else
|
||||
props
|
||||
|> LiveSvelte.Encoder.encode([])
|
||||
|> json_library.encode!()
|
||||
end
|
||||
end
|
||||
|
||||
# --- Deterministic ID generation ------------------------------------------------
|
||||
|
|
|
|||
378
lib/live_svelte/encoder.ex
Normal file
378
lib/live_svelte/encoder.ex
Normal file
|
|
@ -0,0 +1,378 @@
|
|||
defprotocol LiveSvelte.Encoder do
|
||||
@moduledoc """
|
||||
Protocol for encoding values for LiveSvelte JSON serialization.
|
||||
|
||||
Transforms structs and other terms into JSON-compatible data (maps, lists,
|
||||
primitives) before the configured JSON library encodes to a string. Supports
|
||||
`@derive` with `:only` and `:except` to control which struct fields are
|
||||
encoded. By default all keys except `:__struct__` are encoded.
|
||||
|
||||
## Deriving
|
||||
|
||||
* `@derive LiveSvelte.Encoder` — encode all struct fields except `:__struct__`
|
||||
* `@derive {LiveSvelte.Encoder, only: [:a, :b]}` — encode only listed keys
|
||||
* `@derive {LiveSvelte.Encoder, except: [:secret]}` — encode all except listed keys
|
||||
|
||||
## Example
|
||||
|
||||
defmodule User do
|
||||
@derive {LiveSvelte.Encoder, except: [:password]}
|
||||
defstruct [:name, :email, :password]
|
||||
end
|
||||
|
||||
For structs you don't own, use `Protocol.derive/3` outside the module.
|
||||
"""
|
||||
|
||||
@type t :: term()
|
||||
@type opts :: Keyword.t()
|
||||
@fallback_to_any true
|
||||
|
||||
@doc "Encodes a value to a JSON-compatible term (map, list, or primitive)."
|
||||
@spec encode(t(), opts()) :: any()
|
||||
def encode(value, opts \\ [])
|
||||
end
|
||||
|
||||
defimpl LiveSvelte.Encoder, for: Integer do
|
||||
def encode(value, _opts), do: value
|
||||
end
|
||||
|
||||
defimpl LiveSvelte.Encoder, for: Float do
|
||||
def encode(value, _opts), do: value
|
||||
end
|
||||
|
||||
defimpl LiveSvelte.Encoder, for: BitString do
|
||||
def encode(value, _opts), do: value
|
||||
end
|
||||
|
||||
defimpl LiveSvelte.Encoder, for: Atom do
|
||||
def encode(atom, _opts), do: atom
|
||||
end
|
||||
|
||||
defimpl LiveSvelte.Encoder, for: List do
|
||||
def encode(list, opts) do
|
||||
Enum.map(list, &LiveSvelte.Encoder.encode(&1, opts))
|
||||
end
|
||||
end
|
||||
|
||||
defimpl LiveSvelte.Encoder, for: Map do
|
||||
def encode(map, opts) do
|
||||
Map.new(map, fn {key, value} ->
|
||||
{key, LiveSvelte.Encoder.encode(value, opts)}
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl LiveSvelte.Encoder, for: [Date, Time, NaiveDateTime, DateTime] do
|
||||
def encode(value, _opts) do
|
||||
@for.to_iso8601(value)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl LiveSvelte.Encoder, for: Phoenix.HTML.Form do
|
||||
def encode(%Phoenix.HTML.Form{} = form, opts) do
|
||||
LiveSvelte.Encoder.encode(
|
||||
%{
|
||||
name: form.name,
|
||||
values: encode_form_values(form, opts),
|
||||
errors: encode_form_errors(form) || %{},
|
||||
valid: get_form_validity(form)
|
||||
},
|
||||
opts
|
||||
)
|
||||
rescue
|
||||
error in [Protocol.UndefinedError] ->
|
||||
reraise maybe_enhance_error(error), __STACKTRACE__
|
||||
end
|
||||
|
||||
defp get_form_validity(%{source: %{valid?: valid}}), do: valid
|
||||
defp get_form_validity(_), do: true
|
||||
|
||||
if Code.ensure_loaded?(Ecto.Association.NotLoaded) do
|
||||
defp maybe_enhance_error(%{value: %Ecto.Association.NotLoaded{}} = error) do
|
||||
Map.update!(error, :description, fn description ->
|
||||
[first | rest] = String.split(description, "\n\n")
|
||||
addition = "\n\nEncode form with LiveSvelte.Encoder.encode(form, nilify_not_loaded: true) to avoid."
|
||||
Enum.join([first | [addition | rest]], "\n\n")
|
||||
end)
|
||||
end
|
||||
|
||||
defp maybe_enhance_error(error), do: error
|
||||
else
|
||||
defp maybe_enhance_error(error), do: error
|
||||
end
|
||||
|
||||
if Code.ensure_loaded?(Ecto.Changeset) do
|
||||
@relations [:embed, :assoc]
|
||||
|
||||
defp collect_changeset_values(%Ecto.Changeset{} = source, opts) do
|
||||
data = Map.new(source.types, fn {field, type} -> {field, get_field_value(source, field, type, opts)} end)
|
||||
result = if is_struct(source.data), do: Map.merge(source.data, data), else: data
|
||||
Map.delete(result, :__meta__)
|
||||
end
|
||||
|
||||
defp get_field_value(source, field, {tag, %{cardinality: :one}}, opts) when tag in @relations do
|
||||
case Map.fetch(source.changes, field) do
|
||||
{:ok, nil} -> nil
|
||||
{:ok, %Ecto.Changeset{} = changeset} -> collect_changeset_values(changeset, opts)
|
||||
:error ->
|
||||
case Map.fetch!(source.data, field) do
|
||||
%Ecto.Association.NotLoaded{} = not_loaded ->
|
||||
if opts[:nilify_not_loaded], do: nil, else: not_loaded
|
||||
%{__meta__: _} = value -> Map.delete(value, :__meta__)
|
||||
value -> value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp get_field_value(source, field, {tag, %{cardinality: :many}}, opts) when tag in @relations do
|
||||
case Map.fetch(source.changes, field) do
|
||||
{:ok, changesets} ->
|
||||
changesets
|
||||
|> Enum.filter(&(&1.params != nil))
|
||||
|> Enum.map(&collect_changeset_values(&1, opts))
|
||||
:error ->
|
||||
case Map.fetch!(source.data, field) do
|
||||
%Ecto.Association.NotLoaded{} = not_loaded ->
|
||||
if opts[:nilify_not_loaded], do: nil, else: not_loaded
|
||||
[%{__meta__: _} | _] = value -> Enum.map(value, &Map.delete(&1, :__meta__))
|
||||
value -> value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp get_field_value(source, field, _type, _opts) do
|
||||
Phoenix.HTML.FormData.Ecto.Changeset.input_value(source, %{params: source.params}, field)
|
||||
end
|
||||
|
||||
if Code.ensure_loaded?(Phoenix.HTML.FormData.Ecto.Changeset) do
|
||||
def encode_form_values(%{impl: Phoenix.HTML.FormData.Ecto.Changeset, source: source}, opts) do
|
||||
source |> collect_changeset_values(opts) |> LiveSvelte.Encoder.encode(opts)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def encode_form_values(form, opts) do
|
||||
base_values =
|
||||
form.hidden
|
||||
|> Map.new()
|
||||
|> Map.merge(form.data)
|
||||
|> Map.merge(Map.new(form.params))
|
||||
|
||||
LiveSvelte.Encoder.encode(base_values, opts)
|
||||
end
|
||||
|
||||
if Code.ensure_loaded?(Ecto.Changeset) do
|
||||
defp collect_changeset_errors(%Ecto.Changeset{} = changeset) do
|
||||
errors = translate_errors(changeset.errors)
|
||||
Enum.reduce(changeset.changes, errors, fn {field, value}, acc ->
|
||||
case Map.get(changeset.types, field) do
|
||||
{tag, %{cardinality: :one}} when tag in @relations ->
|
||||
embed_errors = collect_changeset_errors(value)
|
||||
if embed_errors == %{}, do: acc, else: Map.put(acc, field, embed_errors)
|
||||
|
||||
{tag, %{cardinality: :many}} when tag in @relations ->
|
||||
list_errors =
|
||||
value
|
||||
|> Enum.filter(&(&1.params != nil))
|
||||
|> Enum.map(fn embed_changeset ->
|
||||
embed_errors = collect_changeset_errors(embed_changeset)
|
||||
if embed_errors == %{}, do: nil, else: embed_errors
|
||||
end)
|
||||
if Enum.all?(list_errors, &is_nil/1), do: acc, else: Map.put(acc, field, list_errors)
|
||||
|
||||
_ -> acc
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
if Code.ensure_loaded?(Phoenix.HTML.FormData.Ecto.Changeset) do
|
||||
def encode_form_errors(%{impl: Phoenix.HTML.FormData.Ecto.Changeset} = form) do
|
||||
collect_changeset_errors(form.source)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def encode_form_errors(form) do
|
||||
translate_errors(form.errors)
|
||||
end
|
||||
|
||||
defp translate_errors(errors) do
|
||||
Map.new(errors, fn {field, error} ->
|
||||
{field, error |> List.wrap() |> Enum.map(&translate_error/1)}
|
||||
end)
|
||||
end
|
||||
|
||||
defp translate_error({msg, opts}) do
|
||||
Enum.reduce(opts, msg, fn {key, value}, acc ->
|
||||
String.replace(acc, "%{#{key}}", value |> List.wrap() |> Enum.map_join(", ", &to_string/1))
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
if Code.ensure_loaded?(Ecto.Changeset) do
|
||||
defimpl LiveSvelte.Encoder, for: Ecto.Changeset do
|
||||
def encode(%Ecto.Changeset{} = cs, opts) do
|
||||
LiveSvelte.Encoder.encode(
|
||||
%{
|
||||
params: cs.params,
|
||||
changes: cs.changes,
|
||||
errors: changeset_errors_to_map(cs),
|
||||
valid?: cs.valid?
|
||||
},
|
||||
opts
|
||||
)
|
||||
end
|
||||
|
||||
defp changeset_errors_to_map(%Ecto.Changeset{} = changeset) do
|
||||
errors = changeset_errors_to_list(changeset.errors)
|
||||
|
||||
Enum.reduce(changeset.changes, errors, fn {field, value}, acc ->
|
||||
case Map.get(changeset.types, field) do
|
||||
{:embed, %{cardinality: :one}} when is_struct(value, Ecto.Changeset) ->
|
||||
embed_errors = changeset_errors_to_map(value)
|
||||
if embed_errors == %{}, do: acc, else: Map.put(acc, field, embed_errors)
|
||||
|
||||
{:embed, %{cardinality: :many}} when is_list(value) ->
|
||||
list_errors =
|
||||
value
|
||||
|> Enum.filter(&match?(%Ecto.Changeset{}, &1))
|
||||
|> Enum.map(fn embed_cs ->
|
||||
embed_errors = changeset_errors_to_map(embed_cs)
|
||||
if embed_errors == %{}, do: nil, else: embed_errors
|
||||
end)
|
||||
if Enum.all?(list_errors, &is_nil/1), do: acc, else: Map.put(acc, field, list_errors)
|
||||
|
||||
{:assoc, %{cardinality: :one}} when is_struct(value, Ecto.Changeset) ->
|
||||
embed_errors = changeset_errors_to_map(value)
|
||||
if embed_errors == %{}, do: acc, else: Map.put(acc, field, embed_errors)
|
||||
|
||||
{:assoc, %{cardinality: :many}} when is_list(value) ->
|
||||
list_errors =
|
||||
value
|
||||
|> Enum.filter(&match?(%Ecto.Changeset{}, &1))
|
||||
|> Enum.map(fn assoc_cs ->
|
||||
assoc_errors = changeset_errors_to_map(assoc_cs)
|
||||
if assoc_errors == %{}, do: nil, else: assoc_errors
|
||||
end)
|
||||
if Enum.all?(list_errors, &is_nil/1), do: acc, else: Map.put(acc, field, list_errors)
|
||||
|
||||
_ ->
|
||||
acc
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp changeset_errors_to_list(errors) do
|
||||
Map.new(errors, fn {field, error} ->
|
||||
{field, error |> List.wrap() |> Enum.map(&error_tuple_to_message/1)}
|
||||
end)
|
||||
end
|
||||
|
||||
defp error_tuple_to_message({msg, opts}) do
|
||||
Enum.reduce(opts, msg, fn {key, value}, acc ->
|
||||
String.replace(acc, "%{#{key}}", value |> List.wrap() |> Enum.map_join(", ", &to_string/1))
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if Code.ensure_loaded?(Phoenix.LiveView) do
|
||||
defimpl LiveSvelte.Encoder, for: Phoenix.LiveView.UploadConfig do
|
||||
def encode(%Phoenix.LiveView.UploadConfig{} = struct, opts) do
|
||||
errors =
|
||||
Enum.map(struct.errors, fn {key, value} ->
|
||||
%{ref: key, error: LiveSvelte.Encoder.encode(value, opts)}
|
||||
end)
|
||||
|
||||
entries =
|
||||
Enum.map(struct.entries, fn entry ->
|
||||
encoded = LiveSvelte.Encoder.encode(entry, opts)
|
||||
entry_errors = errors |> Enum.filter(&(&1.ref == entry.ref)) |> Enum.map(& &1.error)
|
||||
Map.put(encoded, :errors, entry_errors)
|
||||
end)
|
||||
|
||||
LiveSvelte.Encoder.encode(
|
||||
%{
|
||||
ref: struct.ref,
|
||||
name: struct.name,
|
||||
accept: struct.accept,
|
||||
max_entries: struct.max_entries,
|
||||
auto_upload: struct.auto_upload?,
|
||||
entries: entries,
|
||||
errors: errors
|
||||
},
|
||||
opts
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl LiveSvelte.Encoder, for: Phoenix.LiveView.UploadEntry do
|
||||
def encode(%Phoenix.LiveView.UploadEntry{} = struct, opts) do
|
||||
LiveSvelte.Encoder.encode(
|
||||
%{
|
||||
ref: struct.ref,
|
||||
client_name: struct.client_name,
|
||||
client_size: struct.client_size,
|
||||
client_type: struct.client_type,
|
||||
progress: struct.progress,
|
||||
done: struct.done?,
|
||||
valid: struct.valid?,
|
||||
preflighted: struct.preflighted?
|
||||
},
|
||||
opts
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defimpl LiveSvelte.Encoder, for: Any do
|
||||
defmacro __deriving__(module, struct, opts) do
|
||||
fields = fields_to_encode(struct, opts)
|
||||
|
||||
quote do
|
||||
defimpl LiveSvelte.Encoder, for: unquote(module) do
|
||||
def encode(struct, opts) do
|
||||
struct
|
||||
|> Map.take(unquote(fields))
|
||||
|> LiveSvelte.Encoder.encode(opts)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Default for structs without explicit impl: encode all keys except __struct__ and __meta__
|
||||
# (matches @derive LiveSvelte.Encoder default; __meta__ stripped for Ecto schemas).
|
||||
def encode(%{__struct__: _module} = struct, opts) do
|
||||
keys = Map.keys(struct) -- [:__struct__, :__meta__]
|
||||
struct
|
||||
|> Map.take(keys)
|
||||
|> LiveSvelte.Encoder.encode(opts)
|
||||
end
|
||||
|
||||
def encode(value, _opts), do: value
|
||||
|
||||
defp fields_to_encode(struct, opts) do
|
||||
fields = Map.keys(struct)
|
||||
|
||||
cond do
|
||||
only = Keyword.get(opts, :only) ->
|
||||
case only -- fields do
|
||||
[] -> only
|
||||
error_keys ->
|
||||
raise ArgumentError,
|
||||
":only specified keys (#{inspect(error_keys)}) not in defstruct: #{inspect(fields -- [:__struct__])}"
|
||||
end
|
||||
|
||||
except = Keyword.get(opts, :except) ->
|
||||
case except -- fields do
|
||||
[] -> fields -- [:__struct__ | except]
|
||||
error_keys ->
|
||||
raise ArgumentError,
|
||||
":except specified keys (#{inspect(error_keys)}) not in defstruct: #{inspect(fields -- [:__struct__])}"
|
||||
end
|
||||
|
||||
true ->
|
||||
fields -- [:__struct__]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -48,6 +48,7 @@ defmodule LiveSvelte.JSON do
|
|||
@spec encode!(term()) :: binary()
|
||||
def encode!(term) do
|
||||
term
|
||||
|> LiveSvelte.Encoder.encode([])
|
||||
|> prepare_term()
|
||||
|> :json.encode()
|
||||
|> IO.iodata_to_binary()
|
||||
|
|
@ -76,7 +77,11 @@ defmodule LiveSvelte.JSON do
|
|||
|
||||
"""
|
||||
@spec prepare(term()) :: term()
|
||||
def prepare(term), do: prepare_term(term)
|
||||
def prepare(term) do
|
||||
term
|
||||
|> LiveSvelte.Encoder.encode([])
|
||||
|> prepare_term()
|
||||
end
|
||||
|
||||
# Recursively prepare terms for JSON encoding.
|
||||
# Converts structs to maps, nil to null, and handles nested structures.
|
||||
|
|
|
|||
3
mix.exs
3
mix.exs
|
|
@ -11,6 +11,7 @@ defmodule LiveSvelte.MixProject do
|
|||
elixir: "~> 1.17",
|
||||
elixirc_paths: elixirc_paths(Mix.env()),
|
||||
start_permanent: Mix.env() == :prod,
|
||||
consolidate_protocols: Mix.env() != :test,
|
||||
aliases: aliases(),
|
||||
deps: deps(),
|
||||
|
||||
|
|
@ -60,6 +61,8 @@ defmodule LiveSvelte.MixProject do
|
|||
defp deps do
|
||||
[
|
||||
{:ex_doc, "~> 0.37.3", only: :dev, runtime: false},
|
||||
{:ecto, ">= 3.0.0", optional: true},
|
||||
{:phoenix_ecto, ">= 4.0.0", optional: true},
|
||||
{:jason, "~> 1.2", optional: true},
|
||||
{:lazy_html, ">= 0.1.0", only: :test},
|
||||
{:nodejs, "~> 3.1"},
|
||||
|
|
|
|||
3
mix.lock
3
mix.lock
|
|
@ -1,7 +1,9 @@
|
|||
%{
|
||||
"castore": {:hex, :castore, "1.0.0", "c25cd0794c054ebe6908a86820c8b92b5695814479ec95eeff35192720b71eec", [:mix], [], "hexpm", "577d0e855983a97ca1dfa33cbb8a3b6ece6767397ffb4861514343b078fc284b"},
|
||||
"cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"},
|
||||
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
|
||||
"earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"},
|
||||
"ecto": {:hex, :ecto, "3.13.5", "9d4a69700183f33bf97208294768e561f5c7f1ecf417e0fa1006e4a91713a834", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "df9efebf70cf94142739ba357499661ef5dbb559ef902b68ea1f3c1fabce36de"},
|
||||
"elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
|
||||
"esbuild": {:hex, :esbuild, "0.6.1", "a774bfa7b4512a1211bf15880b462be12a4c48ed753a170c68c63b2c95888150", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "569f7409fb5a932211573fc20e2a930a0d5cf3377c5b4f6506c651b1783a1678"},
|
||||
"ex_doc": {:hex, :ex_doc, "0.37.3", "f7816881a443cd77872b7d6118e8a55f547f49903aef8747dbcb345a75b462f9", [:mix], [{:earmark_parser, "~> 1.4.42", [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", "e6aebca7156e7c29b5da4daa17f6361205b2ae5f26e5c7d8ca0d3f7e18972233"},
|
||||
|
|
@ -15,6 +17,7 @@
|
|||
"nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
|
||||
"nodejs": {:hex, :nodejs, "3.1.0", "904c07b81a7b6077af35784df32ab36c62bd2b96edb91bfd04c157c21956cfa5", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5.1", [hex: :poolboy, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.7", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "a6b4480f3f266abb5927be8afacfc7809feefd7d1337fa3ce957d0b98eeeae52"},
|
||||
"phoenix": {:hex, :phoenix, "1.7.0", "cbed113bdc203e2ced75859011fe7e71eeebb6259cefa54de810d9c7048b5e22", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.4", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "8526139d4bd79ec97c5c3c8e69f6cd663597f782756cec874ba7da5429c93e34"},
|
||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.7.0", "75c4b9dfb3efdc42aec2bd5f8bccd978aca0651dbcbc7a3f362ea5d9d43153c6", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "1d75011e4254cb4ddf823e81823a9629559a1be93b4321a6a5f11a5306fbf4cc"},
|
||||
"phoenix_html": {:hex, :phoenix_html, "3.3.1", "4788757e804a30baac6b3fc9695bf5562465dd3f1da8eb8460ad5b404d9a2178", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "bed1906edd4906a15fd7b412b85b05e521e1f67c9a85418c55999277e553d0d3"},
|
||||
"phoenix_live_view": {:hex, :phoenix_live_view, "0.18.15", "58137e648fca9da56d6e931c9c3001f895ff090291052035f395bc958b82f1a5", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "888dd8ea986bebbda741acc65aef788c384d13db91fea416461b2e96aa06a193"},
|
||||
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"},
|
||||
|
|
|
|||
165
test/live_svelte/encoder_test.exs
Normal file
165
test/live_svelte/encoder_test.exs
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
defmodule LiveSvelte.EncoderTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
require Protocol
|
||||
alias LiveSvelte.Encoder
|
||||
|
||||
describe "encode/2 primitives" do
|
||||
test "Integer passes through" do
|
||||
assert Encoder.encode(42, []) == 42
|
||||
end
|
||||
|
||||
test "Float passes through" do
|
||||
assert Encoder.encode(3.14, []) == 3.14
|
||||
end
|
||||
|
||||
test "BitString passes through" do
|
||||
assert Encoder.encode("hello", []) == "hello"
|
||||
end
|
||||
|
||||
test "Atom passes through" do
|
||||
assert Encoder.encode(:ok, []) == :ok
|
||||
end
|
||||
|
||||
test "List recurses" do
|
||||
assert Encoder.encode([1, "a", :x], []) == [1, "a", :x]
|
||||
assert Encoder.encode([%{a: 1}], []) == [%{a: 1}]
|
||||
end
|
||||
|
||||
test "Map recurses" do
|
||||
assert Encoder.encode(%{a: 1, b: "two"}, []) == %{a: 1, b: "two"}
|
||||
end
|
||||
end
|
||||
|
||||
describe "encode/2 Date/Time ISO8601" do
|
||||
test "Date" do
|
||||
d = ~D[2026-01-31]
|
||||
assert Encoder.encode(d, []) == "2026-01-31"
|
||||
end
|
||||
|
||||
test "Time" do
|
||||
t = ~T[14:30:00]
|
||||
assert Encoder.encode(t, []) == "14:30:00"
|
||||
end
|
||||
|
||||
test "NaiveDateTime" do
|
||||
ndt = ~N[2026-01-31 14:30:00]
|
||||
assert Encoder.encode(ndt, []) == "2026-01-31T14:30:00"
|
||||
end
|
||||
|
||||
test "DateTime" do
|
||||
dt = DateTime.from_naive!(~N[2026-01-31 14:30:00], "Etc/UTC")
|
||||
assert Encoder.encode(dt, []) == "2026-01-31T14:30:00Z"
|
||||
end
|
||||
end
|
||||
|
||||
describe "@derive LiveSvelte.Encoder (default: all except __struct__)" do
|
||||
defmodule DeriveDefault do
|
||||
defstruct [:a, :b, :c]
|
||||
end
|
||||
|
||||
Protocol.derive(Encoder, DeriveDefault)
|
||||
|
||||
test "encodes all struct keys except __struct__" do
|
||||
s = %DeriveDefault{a: 1, b: "two", c: :three}
|
||||
assert Encoder.encode(s, []) == %{a: 1, b: "two", c: :three}
|
||||
end
|
||||
end
|
||||
|
||||
describe "@derive {LiveSvelte.Encoder, only: keys}" do
|
||||
defmodule DeriveOnly do
|
||||
defstruct [:a, :b, :c]
|
||||
end
|
||||
|
||||
Protocol.derive(Encoder, DeriveOnly, only: [:a, :c])
|
||||
|
||||
test "encodes only specified keys" do
|
||||
s = %DeriveOnly{a: 1, b: "secret", c: 3}
|
||||
assert Encoder.encode(s, []) == %{a: 1, c: 3}
|
||||
end
|
||||
end
|
||||
|
||||
describe "@derive {LiveSvelte.Encoder, except: keys}" do
|
||||
defmodule DeriveExcept do
|
||||
defstruct [:a, :b, :c]
|
||||
end
|
||||
|
||||
Protocol.derive(Encoder, DeriveExcept, except: [:b])
|
||||
|
||||
test "encodes all except specified keys" do
|
||||
s = %DeriveExcept{a: 1, b: "secret", c: 3}
|
||||
assert Encoder.encode(s, []) == %{a: 1, c: 3}
|
||||
end
|
||||
end
|
||||
|
||||
describe "Phoenix.HTML.Form" do
|
||||
test "encodes name, values, errors, valid" do
|
||||
form = %Phoenix.HTML.Form{
|
||||
name: "user",
|
||||
source: %{},
|
||||
params: %{"name" => "alice", "email" => "a@b.com"},
|
||||
data: %{},
|
||||
hidden: %{},
|
||||
impl: Phoenix.HTML.FormData.Map,
|
||||
id: "user-form",
|
||||
errors: [name: {"can't be blank", []}],
|
||||
options: []
|
||||
}
|
||||
|
||||
encoded = Encoder.encode(form, [])
|
||||
assert encoded.name == "user"
|
||||
assert encoded.valid == true
|
||||
assert encoded.values["name"] == "alice"
|
||||
assert encoded.values["email"] == "a@b.com"
|
||||
assert encoded.errors[:name] == ["can't be blank"]
|
||||
end
|
||||
end
|
||||
|
||||
describe "Ecto.Changeset (when Ecto loaded)" do
|
||||
if Code.ensure_loaded?(Ecto) do
|
||||
defmodule EncoderTestUser do
|
||||
use Ecto.Schema
|
||||
|
||||
schema "users" do
|
||||
field :name, :string
|
||||
field :email, :string
|
||||
end
|
||||
end
|
||||
|
||||
test "encodes params, changes, errors, valid?" do
|
||||
cs =
|
||||
%EncoderTestUser{}
|
||||
|> Ecto.Changeset.cast(%{name: "alice", email: "a@b.com"}, [:name, :email])
|
||||
|> Ecto.Changeset.add_error(:name, "can't be blank")
|
||||
|> Ecto.Changeset.apply_action(:validate)
|
||||
|
||||
case cs do
|
||||
{:ok, _} -> flunk("expected invalid changeset")
|
||||
{:error, changeset} ->
|
||||
encoded = Encoder.encode(changeset, [])
|
||||
assert encoded.valid? == false
|
||||
assert encoded.params["name"] == "alice"
|
||||
assert encoded.errors[:name] == ["can't be blank"]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "nested structs" do
|
||||
defmodule Inner do
|
||||
defstruct [:x]
|
||||
end
|
||||
|
||||
defmodule Outer do
|
||||
defstruct [:inner, :tag]
|
||||
end
|
||||
|
||||
Protocol.derive(Encoder, Inner)
|
||||
Protocol.derive(Encoder, Outer)
|
||||
|
||||
test "nested structs are encoded recursively" do
|
||||
s = %Outer{inner: %Inner{x: 42}, tag: "outer"}
|
||||
assert Encoder.encode(s, []) == %{inner: %{x: 42}, tag: "outer"}
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Reference in a new issue