mirror of
https://github.com/woutdp/live_svelte
synced 2026-05-24 09:28:21 +00:00
Use OTP json encoder by default. Add support for custom json library. (#199)
* add support for custom json library besides jason * use native erlang json encoder * fixed json parsing. added more json tests * Prepare for 0.17.0 release
This commit is contained in:
parent
d21239ec3e
commit
1f9ebc49d2
12 changed files with 567 additions and 43 deletions
17
CHANGELOG.md
17
CHANGELOG.md
|
|
@ -5,7 +5,22 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## UNRELEASED
|
||||
## 0.17.0 - 2026-01-22
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- **Minimum OTP version is now 27** - LiveSvelte now uses Erlang's native `:json` module by default
|
||||
- **Minimum Elixir version is now 1.17** - Required for OTP 27 support
|
||||
- **Jason is now optional** - Add `{:jason, "~> 1.2"}` to your deps if you want to use Jason instead of native JSON
|
||||
|
||||
### Changed
|
||||
|
||||
- Default JSON library changed from Jason to native Erlang `:json` module (`LiveSvelte.JSON`)
|
||||
- Structs are automatically converted to maps by the native JSON encoder (no `@derive` needed)
|
||||
|
||||
### Added
|
||||
|
||||
- New `LiveSvelte.JSON` module that wraps Erlang's native `:json` module with a Jason-compatible interface
|
||||
|
||||
## 0.16.0 - 2025-04-18
|
||||
|
||||
|
|
|
|||
54
README.md
54
README.md
|
|
@ -92,7 +92,7 @@ _If you're updating from an older version, make sure to check the `CHANGELOG.md`
|
|||
```elixir
|
||||
defp deps do
|
||||
[
|
||||
{:live_svelte, "~> 0.16.0"}
|
||||
{:live_svelte, "~> 0.17.0"}
|
||||
]
|
||||
end
|
||||
```
|
||||
|
|
@ -487,6 +487,43 @@ To disable SSR on a specific component, set the `ssr` property to false. Like so
|
|||
<.svelte name="Example" ssr={false} />
|
||||
```
|
||||
|
||||
### JSON Library
|
||||
|
||||
LiveSvelte uses Erlang/OTP 27's native `:json` module by default for JSON encoding.
|
||||
This provides excellent performance without requiring external dependencies.
|
||||
|
||||
**Note:** LiveSvelte requires Elixir 1.17+ and OTP 27+ for the native JSON module.
|
||||
|
||||
#### Using Jason or Poison
|
||||
|
||||
If you prefer to use Jason, Poison, or another JSON library, configure it in your `config.exs`:
|
||||
|
||||
```elixir
|
||||
# config/config.exs
|
||||
config :live_svelte, json_library: Jason
|
||||
```
|
||||
|
||||
Add the dependency to your `mix.exs`:
|
||||
|
||||
```elixir
|
||||
# mix.exs
|
||||
defp deps do
|
||||
[
|
||||
{:live_svelte, "~> 0.17"},
|
||||
{:jason, "~> 1.2"} # or {:poison, "~> 5.0"}
|
||||
]
|
||||
end
|
||||
```
|
||||
|
||||
The JSON library must implement `encode!/1` that accepts any Elixir term and returns a JSON string.
|
||||
|
||||
#### Struct Encoding
|
||||
|
||||
The native JSON encoder automatically converts structs to maps before encoding. This means
|
||||
you don't need `@derive Jason.Encoder` when using the default native JSON encoder.
|
||||
|
||||
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`.
|
||||
|
||||
### live_json
|
||||
|
||||
|
|
@ -527,12 +564,15 @@ You can find an example [here](https://github.com/woutdp/live_svelte/blob/master
|
|||
|
||||
### Structs and Ecto
|
||||
|
||||
We use [Jason](https://github.com/michalmuskala/jason) to serialize any data you pass in the props so it can be handled by Javascript.
|
||||
Jason doesn't know how to handle structs by default, so you need to define it yourself.
|
||||
LiveSvelte serializes data passed in props to JSON so it can be handled by JavaScript.
|
||||
|
||||
#### Structs
|
||||
**With native JSON (default):** Structs are automatically converted to maps. No additional configuration needed.
|
||||
|
||||
For example, if you have a regular struct like this:
|
||||
**With Jason:** Jason doesn't know how to handle structs by default, so you need to define it yourself using `@derive`.
|
||||
|
||||
#### Structs (Jason only)
|
||||
|
||||
If you're using Jason and have a struct like this:
|
||||
|
||||
```elixir
|
||||
defmodule User do
|
||||
|
|
@ -558,9 +598,9 @@ defmodule User do
|
|||
end
|
||||
```
|
||||
|
||||
#### Ecto
|
||||
#### Ecto (Jason only)
|
||||
|
||||
In ecto's case it's important to _also_ omit the `__meta__` field as it's not serializable.
|
||||
When using Jason with Ecto schemas, it's important to _also_ omit the `__meta__` field as it's not serializable.
|
||||
|
||||
Check out the following example:
|
||||
|
||||
|
|
|
|||
22
assets/package-lock.json
generated
22
assets/package-lock.json
generated
|
|
@ -31,7 +31,6 @@
|
|||
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
|
|
@ -454,7 +453,6 @@
|
|||
"integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/set-array": "^1.2.1",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10",
|
||||
|
|
@ -480,7 +478,6 @@
|
|||
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
|
|
@ -508,8 +505,7 @@
|
|||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
|
||||
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.14.0",
|
||||
|
|
@ -531,7 +527,6 @@
|
|||
"integrity": "sha512-xsc9Xv0xlVfwp2o7sQ+GCQ1PgbkdcpWdTzrwXxO3xDMTAywVS3oXVOcOHuRjAPkS4P9b+yc/qNF15460v+jp4Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"acorn": ">=8.9.0"
|
||||
}
|
||||
|
|
@ -542,7 +537,6 @@
|
|||
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
|
|
@ -553,7 +547,6 @@
|
|||
"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
|
|
@ -565,6 +558,7 @@
|
|||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
|
|
@ -620,8 +614,7 @@
|
|||
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.1.4.tgz",
|
||||
"integrity": "sha512-oO82nKPHKkzIj/hbtuDYy/JHqBHFlMIW36SDiPCVsj87ntDLcWN+sJ1erdVryd4NxODacFTsdrIE3b7IamqbOg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/esrap": {
|
||||
"version": "1.2.2",
|
||||
|
|
@ -629,7 +622,6 @@
|
|||
"integrity": "sha512-F2pSJklxx1BlQIQgooczXCPHmcWpn6EsP5oo73LQfonG9fIlIENQ8vMmfGXeojP9MrkzUNAfyU5vdFlR9shHAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.4.15",
|
||||
"@types/estree": "^1.0.1"
|
||||
|
|
@ -641,7 +633,6 @@
|
|||
"integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "*"
|
||||
}
|
||||
|
|
@ -651,8 +642,7 @@
|
|||
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
|
||||
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.12",
|
||||
|
|
@ -660,7 +650,6 @@
|
|||
"integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||
}
|
||||
|
|
@ -708,8 +697,7 @@
|
|||
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz",
|
||||
"integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import Config
|
|||
config :live_svelte,
|
||||
ssr_module: LiveSvelte.SSR.NodeJS,
|
||||
ssr: true
|
||||
# json_library defaults to LiveSvelte.JSON (native Erlang :json module)
|
||||
|
||||
# if Mix.env() == :dev do
|
||||
# esbuild = fn args ->
|
||||
|
|
|
|||
|
|
@ -94,19 +94,20 @@ defmodule LiveSvelte do
|
|||
<script>
|
||||
<%= raw(@ssr_render["head"]) %>
|
||||
</script>
|
||||
<div
|
||||
id={id(@name)}
|
||||
data-name={@name}
|
||||
data-props={json(@props)}
|
||||
data-ssr={@ssr_render != nil}
|
||||
data-live-json={
|
||||
if @init, do: json(@live_json_props), else: @live_json_props |> Map.keys() |> json()
|
||||
}
|
||||
data-slots={@slots |> Slots.base_encode_64() |> json}
|
||||
phx-update="ignore"
|
||||
phx-hook="SvelteHook"
|
||||
class={@class}
|
||||
>
|
||||
<% svelte_id = id(@name) %>
|
||||
<div
|
||||
id={svelte_id}
|
||||
data-name={@name}
|
||||
data-props={json(@props)}
|
||||
data-ssr={@ssr_render != nil}
|
||||
data-live-json={
|
||||
if @init, do: json(@live_json_props), else: @live_json_props |> Map.keys() |> json()
|
||||
}
|
||||
data-slots={@slots |> Slots.base_encode_64() |> json}
|
||||
phx-hook="SvelteHook"
|
||||
class={@class}
|
||||
>
|
||||
<div id={"#{svelte_id}-target"} data-svelte-target phx-update="ignore">
|
||||
<%= raw(@ssr_render["head"]) %>
|
||||
<style>
|
||||
<%= raw(@ssr_render["css"]["code"]) %>
|
||||
|
|
@ -114,6 +115,7 @@ defmodule LiveSvelte do
|
|||
<%= raw(@ssr_render["html"]) %>
|
||||
<%= render_slot(@loading) %>
|
||||
</div>
|
||||
</div>
|
||||
</.live_json>
|
||||
"""
|
||||
end
|
||||
|
|
@ -128,7 +130,8 @@ defmodule LiveSvelte do
|
|||
end
|
||||
|
||||
defp json(props) do
|
||||
Jason.encode!(props)
|
||||
json_library = Application.get_env(:live_svelte, :json_library, LiveSvelte.JSON)
|
||||
json_library.encode!(props)
|
||||
end
|
||||
|
||||
defp id(name), do: "#{name}-#{System.unique_integer([:positive])}"
|
||||
|
|
|
|||
102
lib/live_svelte/json.ex
Normal file
102
lib/live_svelte/json.ex
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
defmodule LiveSvelte.JSON do
|
||||
@moduledoc """
|
||||
JSON encoding using Erlang/OTP 27's native :json module.
|
||||
|
||||
This module provides a Jason-compatible interface (`encode!/1`)
|
||||
that wraps the native Erlang :json module for use with LiveSvelte.
|
||||
|
||||
## Features
|
||||
|
||||
- Uses Erlang's built-in `:json` module (OTP 27+)
|
||||
- Automatically converts structs to maps
|
||||
- Converts all map keys to strings (matching Jason behavior)
|
||||
- Handles nested data structures
|
||||
|
||||
## Usage
|
||||
|
||||
This module is the default JSON encoder for LiveSvelte. To use a different
|
||||
encoder like Jason, configure it in your `config.exs`:
|
||||
|
||||
config :live_svelte, json_library: Jason
|
||||
|
||||
## SSR Compatibility Note
|
||||
|
||||
When using server-side rendering, the NodeJS worker uses Jason internally
|
||||
to serialize data to the Node.js process. This module is designed to produce
|
||||
Jason-compatible output, ensuring consistency between SSR and client-side
|
||||
hydration.
|
||||
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Encodes an Elixir term to a JSON string.
|
||||
|
||||
Structs are automatically converted to maps before encoding.
|
||||
Returns a binary string.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> LiveSvelte.JSON.encode!(%{foo: "bar"})
|
||||
"{\"foo\":\"bar\"}"
|
||||
|
||||
iex> LiveSvelte.JSON.encode!([1, 2, 3])
|
||||
"[1,2,3]"
|
||||
|
||||
"""
|
||||
@spec encode!(term()) :: binary()
|
||||
def encode!(term) do
|
||||
term
|
||||
|> prepare_term()
|
||||
|> :json.encode()
|
||||
|> IO.iodata_to_binary()
|
||||
end
|
||||
|
||||
# Recursively prepare terms for JSON encoding.
|
||||
# Converts structs to maps, nil to null, and handles nested structures.
|
||||
|
||||
# nil becomes JSON null
|
||||
defp prepare_term(nil), do: :null
|
||||
|
||||
# Booleans pass through (Erlang :json handles them)
|
||||
defp prepare_term(true), do: true
|
||||
defp prepare_term(false), do: false
|
||||
|
||||
# Other atoms become strings (matches Jason behavior)
|
||||
defp prepare_term(atom) when is_atom(atom) do
|
||||
Atom.to_string(atom)
|
||||
end
|
||||
|
||||
# Structs become maps (strip __struct__ key)
|
||||
defp prepare_term(%_{} = struct) do
|
||||
struct
|
||||
|> Map.from_struct()
|
||||
|> prepare_term()
|
||||
end
|
||||
|
||||
# Maps: convert all keys to strings, recursively process values
|
||||
defp prepare_term(map) when is_map(map) do
|
||||
Map.new(map, fn {k, v} -> {prepare_key(k), prepare_term(v)} end)
|
||||
end
|
||||
|
||||
# Lists: recursively process elements
|
||||
defp prepare_term(list) when is_list(list) do
|
||||
Enum.map(list, &prepare_term/1)
|
||||
end
|
||||
|
||||
# Tuples become arrays
|
||||
defp prepare_term(tuple) when is_tuple(tuple) do
|
||||
tuple
|
||||
|> Tuple.to_list()
|
||||
|> prepare_term()
|
||||
end
|
||||
|
||||
# Numbers and binaries pass through
|
||||
defp prepare_term(term), do: term
|
||||
|
||||
# Key conversion helpers - ensure all keys become strings
|
||||
defp prepare_key(key) when is_atom(key), do: Atom.to_string(key)
|
||||
defp prepare_key(key) when is_integer(key), do: Integer.to_string(key)
|
||||
defp prepare_key(key) when is_float(key), do: Float.to_string(key)
|
||||
defp prepare_key(key) when is_binary(key), do: key
|
||||
defp prepare_key(key), do: to_string(key)
|
||||
end
|
||||
10
mix.exs
10
mix.exs
|
|
@ -1,14 +1,15 @@
|
|||
defmodule LiveSvelte.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
@version "0.16.0"
|
||||
@version "0.17.0"
|
||||
@repo_url "https://github.com/woutdp/live_svelte"
|
||||
|
||||
def project do
|
||||
[
|
||||
app: :live_svelte,
|
||||
version: @version,
|
||||
elixir: "~> 1.12",
|
||||
elixir: "~> 1.17",
|
||||
elixirc_paths: elixirc_paths(Mix.env()),
|
||||
start_permanent: Mix.env() == :prod,
|
||||
aliases: aliases(),
|
||||
deps: deps(),
|
||||
|
|
@ -53,10 +54,13 @@ defmodule LiveSvelte.MixProject do
|
|||
]
|
||||
end
|
||||
|
||||
defp elixirc_paths(:test), do: ["lib", "test/support"]
|
||||
defp elixirc_paths(_), do: ["lib"]
|
||||
|
||||
defp deps do
|
||||
[
|
||||
{:ex_doc, "~> 0.37.3", only: :dev, runtime: false},
|
||||
{:jason, "~> 1.2"},
|
||||
{:jason, "~> 1.2", optional: true},
|
||||
{:nodejs, "~> 3.1"},
|
||||
{:phoenix, ">= 1.7.0"},
|
||||
{:phoenix_html, ">= 3.3.1"},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "live_svelte",
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.0",
|
||||
"description": "",
|
||||
"license": "MIT",
|
||||
"module": "./priv/static/live_svelte.esm.js",
|
||||
|
|
|
|||
152
test/json_library_test.exs
Normal file
152
test/json_library_test.exs
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
defmodule LiveSvelte.JSONLibraryTest do
|
||||
# must be synchronous, tests are sensitive to config changes
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
describe "native JSON library (default)" do
|
||||
test "uses LiveSvelte.JSON by default when no config is provided" do
|
||||
Application.delete_env(:live_svelte, :json_library)
|
||||
json_library = Application.get_env(:live_svelte, :json_library, LiveSvelte.JSON)
|
||||
assert json_library == LiveSvelte.JSON
|
||||
end
|
||||
|
||||
test "encodes props correctly with native JSON" do
|
||||
Application.delete_env(:live_svelte, :json_library)
|
||||
|
||||
data = %{foo: "bar", baz: 123}
|
||||
|
||||
# Call the private json/1 function through the public API
|
||||
# by testing that the component renders with correct data attributes
|
||||
assigns = %{
|
||||
__changed__: nil,
|
||||
socket: nil,
|
||||
name: "Test",
|
||||
props: data,
|
||||
live_json_props: %{},
|
||||
ssr: false,
|
||||
class: nil,
|
||||
loading: [],
|
||||
inner_block: []
|
||||
}
|
||||
|
||||
result = LiveSvelte.svelte(assigns)
|
||||
html = Phoenix.HTML.Safe.to_iodata(result) |> IO.iodata_to_binary()
|
||||
|
||||
# HTML entities are escaped in the output
|
||||
# Native JSON encodes correctly
|
||||
assert html =~ "data-props="
|
||||
assert html =~ "foo"
|
||||
assert html =~ "bar"
|
||||
assert html =~ "baz"
|
||||
assert html =~ "123"
|
||||
end
|
||||
end
|
||||
|
||||
describe "custom JSON library" do
|
||||
test "uses custom library when configured" do
|
||||
load_config("test/json_library_test/custom_json_config.exs")
|
||||
json_library = Application.get_env(:live_svelte, :json_library)
|
||||
assert json_library == TestJSONLibrary
|
||||
end
|
||||
|
||||
test "calls custom library's encode!/1 function" do
|
||||
load_config("test/json_library_test/custom_json_config.exs")
|
||||
|
||||
data = %{test: "data"}
|
||||
|
||||
assigns = %{
|
||||
__changed__: nil,
|
||||
socket: nil,
|
||||
name: "Test",
|
||||
props: data,
|
||||
live_json_props: %{},
|
||||
ssr: false,
|
||||
class: nil,
|
||||
loading: [],
|
||||
inner_block: []
|
||||
}
|
||||
|
||||
result = LiveSvelte.svelte(assigns)
|
||||
html = Phoenix.HTML.Safe.to_iodata(result) |> IO.iodata_to_binary()
|
||||
|
||||
# Should contain the test library's output
|
||||
assert html =~ ~s(data-props="TEST_ENCODED:%{test: "data"}")
|
||||
end
|
||||
end
|
||||
|
||||
describe "backward compatibility with Jason" do
|
||||
test "works with Jason when explicitly configured" do
|
||||
Application.put_env(:live_svelte, :json_library, Jason)
|
||||
|
||||
json_library = Application.get_env(:live_svelte, :json_library)
|
||||
assert json_library == Jason
|
||||
|
||||
data = %{legacy: "test"}
|
||||
|
||||
assigns = %{
|
||||
__changed__: nil,
|
||||
socket: nil,
|
||||
name: "Legacy",
|
||||
props: data,
|
||||
live_json_props: %{},
|
||||
ssr: false,
|
||||
class: nil,
|
||||
loading: [],
|
||||
inner_block: []
|
||||
}
|
||||
|
||||
result = LiveSvelte.svelte(assigns)
|
||||
html = Phoenix.HTML.Safe.to_iodata(result) |> IO.iodata_to_binary()
|
||||
|
||||
# HTML entities are escaped in the output
|
||||
assert html =~ ~s(data-props="{"legacy":"test"}")
|
||||
|
||||
# Clean up
|
||||
Application.delete_env(:live_svelte, :json_library)
|
||||
end
|
||||
end
|
||||
|
||||
describe "struct encoding" do
|
||||
defmodule TestUser do
|
||||
defstruct name: "John", age: 27
|
||||
end
|
||||
|
||||
test "native JSON encodes structs as maps automatically" do
|
||||
Application.delete_env(:live_svelte, :json_library)
|
||||
|
||||
data = %{user: %TestUser{name: "Jane", age: 30}}
|
||||
|
||||
assigns = %{
|
||||
__changed__: nil,
|
||||
socket: nil,
|
||||
name: "StructTest",
|
||||
props: data,
|
||||
live_json_props: %{},
|
||||
ssr: false,
|
||||
class: nil,
|
||||
loading: [],
|
||||
inner_block: []
|
||||
}
|
||||
|
||||
result = LiveSvelte.svelte(assigns)
|
||||
html = Phoenix.HTML.Safe.to_iodata(result) |> IO.iodata_to_binary()
|
||||
|
||||
# Should encode struct fields
|
||||
assert html =~ "user"
|
||||
assert html =~ "name"
|
||||
assert html =~ "Jane"
|
||||
assert html =~ "age"
|
||||
assert html =~ "30"
|
||||
end
|
||||
end
|
||||
|
||||
# Helper function to reload configuration
|
||||
# Pattern copied from ssr_test.exs:27-34
|
||||
defp load_config(path) do
|
||||
Application.started_applications(:infinity)
|
||||
|> Enum.reduce([], fn {app, _, _}, acc ->
|
||||
[{app, Application.get_all_env(app)} | acc]
|
||||
end)
|
||||
|> Config.Reader.merge(Config.Reader.read!(path))
|
||||
|> Application.put_all_env()
|
||||
end
|
||||
end
|
||||
3
test/json_library_test/custom_json_config.exs
Normal file
3
test/json_library_test/custom_json_config.exs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import Config
|
||||
|
||||
config :live_svelte, json_library: TestJSONLibrary
|
||||
208
test/live_svelte/json_test.exs
Normal file
208
test/live_svelte/json_test.exs
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
defmodule LiveSvelte.JSONTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias LiveSvelte.JSON
|
||||
|
||||
describe "encode!/1" do
|
||||
test "encodes simple maps" do
|
||||
assert JSON.encode!(%{foo: "bar"}) == ~s({"foo":"bar"})
|
||||
end
|
||||
|
||||
test "encodes nested maps" do
|
||||
result = JSON.encode!(%{outer: %{inner: "value"}})
|
||||
assert result == ~s({"outer":{"inner":"value"}})
|
||||
end
|
||||
|
||||
test "encodes lists" do
|
||||
assert JSON.encode!([1, 2, 3]) == "[1,2,3]"
|
||||
end
|
||||
|
||||
test "encodes lists of maps" do
|
||||
result = JSON.encode!([%{a: 1}, %{b: 2}])
|
||||
assert result == ~s([{"a":1},{"b":2}])
|
||||
end
|
||||
|
||||
test "encodes integers" do
|
||||
assert JSON.encode!(42) == "42"
|
||||
end
|
||||
|
||||
test "encodes floats" do
|
||||
assert JSON.encode!(3.14) == "3.14"
|
||||
end
|
||||
|
||||
test "encodes booleans" do
|
||||
assert JSON.encode!(true) == "true"
|
||||
assert JSON.encode!(false) == "false"
|
||||
end
|
||||
|
||||
test "encodes nil as null" do
|
||||
assert JSON.encode!(nil) == "null"
|
||||
end
|
||||
|
||||
test "encodes strings" do
|
||||
assert JSON.encode!("hello") == ~s("hello")
|
||||
end
|
||||
|
||||
test "encodes atoms as strings" do
|
||||
assert JSON.encode!(:hello) == ~s("hello")
|
||||
end
|
||||
|
||||
test "encodes maps with atom keys" do
|
||||
result = JSON.encode!(%{foo: "bar", baz: 123})
|
||||
# Map key order may vary, so we parse and compare
|
||||
assert result =~ "foo"
|
||||
assert result =~ "bar"
|
||||
assert result =~ "baz"
|
||||
assert result =~ "123"
|
||||
end
|
||||
|
||||
test "encodes maps with string keys" do
|
||||
assert JSON.encode!(%{"foo" => "bar"}) == ~s({"foo":"bar"})
|
||||
end
|
||||
end
|
||||
|
||||
describe "struct encoding" do
|
||||
defmodule TestStruct do
|
||||
defstruct name: "test", value: 42
|
||||
end
|
||||
|
||||
defmodule NestedStruct do
|
||||
defstruct user: nil, data: %{}
|
||||
end
|
||||
|
||||
test "encodes structs as maps" do
|
||||
result = JSON.encode!(%TestStruct{})
|
||||
decoded = :json.decode(result)
|
||||
assert decoded["name"] == "test"
|
||||
assert decoded["value"] == 42
|
||||
end
|
||||
|
||||
test "encodes structs with custom values" do
|
||||
result = JSON.encode!(%TestStruct{name: "custom", value: 100})
|
||||
decoded = :json.decode(result)
|
||||
assert decoded["name"] == "custom"
|
||||
assert decoded["value"] == 100
|
||||
end
|
||||
|
||||
test "encodes nested structs" do
|
||||
nested = %NestedStruct{
|
||||
user: %TestStruct{name: "john", value: 30},
|
||||
data: %{key: "value"}
|
||||
}
|
||||
|
||||
result = JSON.encode!(nested)
|
||||
decoded = :json.decode(result)
|
||||
assert decoded["user"]["name"] == "john"
|
||||
assert decoded["user"]["value"] == 30
|
||||
assert decoded["data"]["key"] == "value"
|
||||
end
|
||||
|
||||
test "encodes structs in lists" do
|
||||
list = [%TestStruct{name: "a"}, %TestStruct{name: "b"}]
|
||||
result = JSON.encode!(list)
|
||||
decoded = :json.decode(result)
|
||||
assert length(decoded) == 2
|
||||
assert Enum.at(decoded, 0)["name"] == "a"
|
||||
assert Enum.at(decoded, 1)["name"] == "b"
|
||||
end
|
||||
end
|
||||
|
||||
describe "tuple encoding" do
|
||||
test "encodes tuples as arrays" do
|
||||
assert JSON.encode!({1, 2, 3}) == "[1,2,3]"
|
||||
end
|
||||
|
||||
test "encodes tuples with mixed types" do
|
||||
result = JSON.encode!({"hello", 42, true})
|
||||
assert result == ~s(["hello",42,true])
|
||||
end
|
||||
end
|
||||
|
||||
describe "edge cases" do
|
||||
test "encodes empty map" do
|
||||
assert JSON.encode!(%{}) == "{}"
|
||||
end
|
||||
|
||||
test "encodes empty list" do
|
||||
assert JSON.encode!([]) == "[]"
|
||||
end
|
||||
|
||||
test "encodes deeply nested structures" do
|
||||
deep = %{a: %{b: %{c: %{d: [1, 2, %{e: "f"}]}}}}
|
||||
result = JSON.encode!(deep)
|
||||
decoded = :json.decode(result)
|
||||
assert get_in(decoded, ["a", "b", "c", "d"]) |> Enum.at(2) |> Map.get("e") == "f"
|
||||
end
|
||||
end
|
||||
|
||||
describe "integer key handling" do
|
||||
test "encodes maps with integer keys as string keys" do
|
||||
result = JSON.encode!(%{1 => "a", 2 => "b"})
|
||||
decoded = :json.decode(result)
|
||||
assert Map.has_key?(decoded, "1")
|
||||
assert Map.has_key?(decoded, "2")
|
||||
assert decoded["1"] == "a"
|
||||
assert decoded["2"] == "b"
|
||||
end
|
||||
|
||||
test "encodes large maps with integer keys (LiveJson scenario)" do
|
||||
data = for i <- 1..100, into: %{}, do: {i, i * 2}
|
||||
result = JSON.encode!(data)
|
||||
decoded = :json.decode(result)
|
||||
assert decoded["1"] == 2
|
||||
assert decoded["50"] == 100
|
||||
assert decoded["100"] == 200
|
||||
end
|
||||
|
||||
test "encodes nested maps with integer keys" do
|
||||
data = %{1 => %{2 => "nested"}}
|
||||
result = JSON.encode!(data)
|
||||
decoded = :json.decode(result)
|
||||
assert decoded["1"]["2"] == "nested"
|
||||
end
|
||||
|
||||
test "encodes mixed key types" do
|
||||
data = %{1 => "int", :atom => "atom", "string" => "string"}
|
||||
result = JSON.encode!(data)
|
||||
decoded = :json.decode(result)
|
||||
assert decoded["1"] == "int"
|
||||
assert decoded["atom"] == "atom"
|
||||
assert decoded["string"] == "string"
|
||||
end
|
||||
end
|
||||
|
||||
describe "atom value handling" do
|
||||
test "encodes atom values as strings" do
|
||||
result = JSON.encode!(%{status: :active})
|
||||
decoded = :json.decode(result)
|
||||
assert decoded["status"] == "active"
|
||||
end
|
||||
|
||||
test "preserves boolean atoms" do
|
||||
assert JSON.encode!(true) == "true"
|
||||
assert JSON.encode!(false) == "false"
|
||||
end
|
||||
|
||||
test "encodes atom values in lists" do
|
||||
result = JSON.encode!([:one, :two, :three])
|
||||
assert result == ~s(["one","two","three"])
|
||||
end
|
||||
|
||||
test "encodes mixed atom and string values" do
|
||||
result = JSON.encode!(%{atom_val: :test, string_val: "test"})
|
||||
decoded = :json.decode(result)
|
||||
assert decoded["atom_val"] == "test"
|
||||
assert decoded["string_val"] == "test"
|
||||
end
|
||||
end
|
||||
|
||||
describe "float key handling" do
|
||||
test "encodes maps with float keys as string keys" do
|
||||
result = JSON.encode!(%{1.5 => "float"})
|
||||
decoded = :json.decode(result)
|
||||
# Float.to_string may produce different representations
|
||||
assert map_size(decoded) == 1
|
||||
assert Enum.at(Map.values(decoded), 0) == "float"
|
||||
end
|
||||
end
|
||||
end
|
||||
8
test/support/test_json_library.ex
Normal file
8
test/support/test_json_library.ex
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
defmodule TestJSONLibrary do
|
||||
@moduledoc false
|
||||
# Mock JSON library for testing custom configuration
|
||||
|
||||
def encode!(data) do
|
||||
"TEST_ENCODED:#{inspect(data)}"
|
||||
end
|
||||
end
|
||||
Loading…
Reference in a new issue