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