-
Notifications
You must be signed in to change notification settings - Fork 172
/
Copy pathencoder.ex
239 lines (188 loc) · 6.49 KB
/
encoder.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
defprotocol Jason.Encoder do
@moduledoc """
Protocol controlling how a value is encoded to JSON.
## Deriving
The protocol allows leveraging the Elixir's `@derive` feature
to simplify protocol implementation in trivial cases. Accepted
options are:
* `:only` - encodes only values of specified keys.
* `:except` - encodes all struct fields except specified keys.
By default all keys except the `:__struct__` key are encoded.
## Example
Let's assume a presence of the following struct:
defmodule Test do
defstruct [:foo, :bar, :baz]
end
If we were to call `@derive Jason.Encoder` just before `defstruct`,
an implementation similar to the following implementation would be generated:
defimpl Jason.Encoder, for: Test do
def encode(value, opts) do
Jason.Encode.map(Map.take(value, [:foo, :bar, :baz]), opts)
end
end
If we called `@derive {Jason.Encoder, only: [:foo]}`, an implementation
similar to the following implementation would be generated:
defimpl Jason.Encoder, for: Test do
def encode(value, opts) do
Jason.Encode.map(Map.take(value, [:foo]), opts)
end
end
If we called `@derive {Jason.Encoder, except: [:foo]}`, an implementation
similar to the following implementation would be generated:
defimpl Jason.Encoder, for: Test do
def encode(value, opts) do
Jason.Encode.map(Map.take(value, [:bar, :baz]), opts)
end
end
The actually generated implementations are more efficient computing some data
during compilation similar to the macros from the `Jason.Helpers` module.
## Explicit implementation
If you wish to implement the protocol fully yourself, it is advised to
use functions from the `Jason.Encode` module to do the actual iodata
generation - they are highly optimized and verified to always produce
valid JSON.
"""
@type t :: term
@type opts :: Jason.Encode.opts()
@fallback_to_any true
@doc """
Encodes `value` to JSON.
The argument `opts` is opaque - it can be passed to various functions in
`Jason.Encode` (or to the protocol function itself) for encoding values to JSON.
"""
@spec encode(t, opts) :: iodata
def encode(value, opts)
end
defimpl Jason.Encoder, for: Any do
defmacro __deriving__(module, struct, opts) do
fields = fields_to_encode(struct, opts)
kv = Enum.map(fields, &{&1, generated_var(&1)})
escape = quote(do: escape)
encode_map = quote(do: encode_map)
encode_args = [escape, encode_map]
kv_iodata = Jason.Codegen.build_kv_iodata(kv, encode_args)
quote do
defimpl Jason.Encoder, for: unquote(module) do
require Jason.Helpers
def encode(%{unquote_splicing(kv)}, {unquote(escape), unquote(encode_map)}) do
unquote(kv_iodata)
end
end
end
end
# The same as Macro.var/2 except it sets generated: true and handles _ key
defp generated_var(:_) do
{:__, [generated: true], __MODULE__.Underscore}
end
defp generated_var(name) do
{name, [generated: true], __MODULE__}
end
def encode(%_{} = struct, _opts) do
raise Protocol.UndefinedError,
protocol: @protocol,
value: struct,
description: """
Jason.Encoder protocol must always be explicitly implemented.
If you own the struct, you can derive the implementation specifying \
which fields should be encoded to JSON:
@derive {Jason.Encoder, only: [....]}
defstruct ...
It is also possible to encode all fields, although this should be \
used carefully to avoid accidentally leaking private information \
when new fields are added:
@derive Jason.Encoder
defstruct ...
Finally, if you don't own the struct you want to encode to JSON, \
you may use Protocol.derive/3 placed outside of any module:
Protocol.derive(Jason.Encoder, NameOfTheStruct, only: [...])
Protocol.derive(Jason.Encoder, NameOfTheStruct)
"""
end
def encode(value, _opts) do
raise Protocol.UndefinedError,
protocol: @protocol,
value: value,
description: "Jason.Encoder protocol must always be explicitly implemented"
end
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)}) that are not defined 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)}) that are not defined in defstruct: " <>
"#{inspect(fields -- [:__struct__])}"
end
true ->
fields -- [:__struct__]
end
end
end
# The following implementations are formality - they are already covered
# by the main encoding mechanism in Jason.Encode, but exist mostly for
# documentation purposes and if anybody had the idea to call the protocol directly.
defimpl Jason.Encoder, for: Atom do
def encode(atom, opts) do
Jason.Encode.atom(atom, opts)
end
end
defimpl Jason.Encoder, for: Integer do
def encode(integer, _opts) do
Jason.Encode.integer(integer)
end
end
defimpl Jason.Encoder, for: Float do
def encode(float, _opts) do
Jason.Encode.float(float)
end
end
defimpl Jason.Encoder, for: List do
def encode(list, opts) do
Jason.Encode.list(list, opts)
end
end
defimpl Jason.Encoder, for: Map do
def encode(map, opts) do
Jason.Encode.map(map, opts)
end
end
defimpl Jason.Encoder, for: BitString do
def encode(binary, opts) when is_binary(binary) do
Jason.Encode.string(binary, opts)
end
def encode(bitstring, _opts) do
raise Protocol.UndefinedError,
protocol: @protocol,
value: bitstring,
description: "cannot encode a bitstring to JSON"
end
end
defimpl Jason.Encoder, for: [Date, Time, NaiveDateTime, DateTime] do
def encode(value, _opts) do
[?", @for.to_iso8601(value), ?"]
end
end
if Code.ensure_loaded?(Decimal) do
defimpl Jason.Encoder, for: Decimal do
def encode(value, _opts) do
[?", Decimal.to_string(value), ?"]
end
end
end
defimpl Jason.Encoder, for: Jason.Fragment do
def encode(%{encode: encode}, opts) do
encode.(opts)
end
end