Current section
Files
Jump to
Current section
Files
lib/compat/jason.ex
defmodule RustyJson.Compat.Jason do
@moduledoc """
Jason compatibility layer for RustyJson.
This module provides automatic fallback to `Jason.Encoder` implementations
when encoding structs with `protocol: true`. It allows RustyJson to be a
true drop-in replacement for Jason without requiring code changes.
## Performance Warning
**This compatibility layer is only active when `protocol: true` is passed.**
```elixir
# Default - NO compatibility overhead, straight to Rust
RustyJson.encode!(data)
# With protocol - compatibility layer active
RustyJson.encode!(data, protocol: true)
```
When active, this layer:
1. Checks if a struct has a `Jason.Encoder` implementation
2. If found, calls `Jason.encode_to_iodata!/1` to encode it
3. Wraps the result in a `RustyJson.Fragment` for the Rust NIF
This adds Elixir-side overhead for each struct with a Jason.Encoder.
## How It Works
When you encode a struct with `protocol: true`:
```
%UserWithJasonEncoder{...}
│
▼
RustyJson.Encoder.Any.encode/1
│
▼
jason_encoder_available?/2 ◄── Checks for Jason.Encoder impl
│
▼ (if found)
Jason.encode_to_iodata!/1 ◄── Calls Jason to encode
│
▼
%RustyJson.Fragment{...} ◄── Wraps as Fragment
│
▼
Rust NIF ◄── Writes iodata directly
```
## Supported Scenarios
| Scenario | Supported | Notes |
|----------|-----------|-------|
| `@derive Jason.Encoder` | ✅ | Works with `:only`/`:except` options |
| `defimpl Jason.Encoder` | ✅ | Custom implementations work |
| `Jason.Fragment` | ✅ | Converted to `RustyJson.Fragment` |
| Nested Jason-encoded structs | ✅ | Recursively handled |
## Example
```elixir
# Existing code with Jason.Encoder
defmodule User do
@derive {Jason.Encoder, only: [:id, :name]}
defstruct [:id, :name, :password_hash]
end
# Works with RustyJson when protocol: true
user = %User{id: 1, name: "Alice", password_hash: "secret"}
RustyJson.encode!(user, protocol: true)
# => {"id":1,"name":"Alice"}
```
## When to Use
Use `protocol: true` when:
- Migrating from Jason and you have existing `Jason.Encoder` implementations
- You need custom struct encoding and don't want to implement `RustyJson.Encoder`
For maximum performance, implement `RustyJson.Encoder` directly instead.
"""
@doc """
Checks if a struct has a usable `Jason.Encoder` implementation.
Returns `true` if:
- `Jason.Encoder` module is loaded
- The struct has an implementation (not the fallback Any implementation)
- The implementation exports `encode/2`
## Examples
iex> RustyJson.Compat.Jason.encoder_available?(%UserWithJasonEncoder{})
true
iex> RustyJson.Compat.Jason.encoder_available?(%PlainStruct{})
false
"""
@spec encoder_available?(struct()) :: boolean()
def encoder_available?(struct) when is_struct(struct) do
if Code.ensure_loaded?(Jason.Encoder) and function_exported?(Jason.Encoder, :impl_for, 1) do
case Jason.Encoder.impl_for(struct) do
Jason.Encoder.Any -> false
impl -> function_exported?(impl, :encode, 2)
end
else
false
end
end
@doc """
Encodes a struct using its `Jason.Encoder` implementation.
Returns a `RustyJson.Fragment` containing the pre-encoded JSON as iodata.
The Rust NIF will write this directly to output without re-encoding.
## Examples
iex> user = %UserWithJasonEncoder{id: 1, name: "Alice"}
iex> fragment = RustyJson.Compat.Jason.encode_with_jason(user)
iex> fragment.encode
[123, [[34, "id", 34], 58, "1"], 44, [[34, "name", 34], 58, [34, "Alice", 34]], 125]
"""
@spec encode_with_jason(struct()) :: RustyJson.Fragment.t()
def encode_with_jason(struct) when is_struct(struct) do
%RustyJson.Fragment{encode: Jason.encode_to_iodata!(struct)}
end
@doc """
Converts a `Jason.Fragment` to a `RustyJson.Fragment`.
Jason.Fragment stores either:
- A function `(opts -> iodata)` in the `:encode` field
- Pre-computed iodata in the `:encode` field
This function normalizes both cases to a `RustyJson.Fragment` with iodata.
## Examples
iex> jason_frag = Jason.Fragment.new(~s({"pre":"encoded"}))
iex> rusty_frag = RustyJson.Compat.Jason.convert_fragment(jason_frag)
iex> rusty_frag.encode
"{\\"pre\\":\\"encoded\\"}"
"""
@spec convert_fragment(struct()) :: RustyJson.Fragment.t()
def convert_fragment(%{__struct__: Jason.Fragment, encode: encode})
when is_function(encode, 1) do
%RustyJson.Fragment{encode: encode.(nil)}
end
def convert_fragment(%{__struct__: Jason.Fragment, encode: encode}) do
%RustyJson.Fragment{encode: encode}
end
end