Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Newer
Older
100644 327 lines (297 sloc) 13.666 kb
af460ce @fjl add new json parser
fjl authored
1 % Copyright (c) 2011 by Travelping GmbH <info@travelping.com>
2 % Copyright (c) 2010 by Felix Lange <fjl@twurst.com>
3
4 % Permission is hereby granted, free of charge, to any person obtaining a copy
5 % of this software and associated documentation files (the "Software"), to deal
6 % in the Software without restriction, including without limitation the rights
7 % to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 % copies of the Software, and to permit persons to whom the Software is
9 % furnished to do so, subject to the following conditions:
10 %
11 % The above copyright notice and this permission notice shall be included in
12 % all copies or substantial portions of the Software.
13 %
14 % THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 % IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 % FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 % AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 % LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 % OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20 % THE SOFTWARE.
209dadc init
Felix Lange authored
21
1c0db49 @fjl move record-to-object conversion to the hello_json module
fjl authored
22 %% @doc
23 %% This module provides a JSON codec (UTF-8 only),
24 %% and some utitlies to make working with JSON data a bit easier.
25 %%
26 %% == Erlang JSON encoding ==
27 %% The JSON data encoding used by this module is summarized by the table below.
28 %%
29 %% <table border="1">
30 %% <thead><tr><td><b>JSON Type</b></td><td><b>Erlang Type</b></td></tr></thead>
31 %% <tbody>
32 %% <tr><td>string</td><td>binary()</td></tr>
33 %% <tr><td>number</td><td>integer() | float()</td></tr>
34 %% <tr><td>boolean</td><td>'true' | 'false'</td></tr>
35 %% <tr><td>array</td><td>list()</td></tr>
ab8485d edoc documentation for stateless servers and hello_simple_client
Felix Lange authored
36 %% <tr><td>object</td><td>{[{binary(), value()}]}</td></tr>
1c0db49 @fjl move record-to-object conversion to the hello_json module
fjl authored
37 %% </tbody>
38 %% </table>
39 %%
ab8485d edoc documentation for stateless servers and hello_simple_client
Felix Lange authored
40 %% When <em>encoding</em> JSON objects, the keys may be specified as a binary,
41 %% string or atom.
42 %%
1c0db49 @fjl move record-to-object conversion to the hello_json module
fjl authored
43 %% == Object-to-Record Conversion ==
44 %% One particularly useful facility is the automated conversion
45 %% of JSON objects to and from Erlang records. Because record definitions
46 %% exist only at compile time, the conversion routines are defined as
47 %% macros in the `hello.hrl' include file. They are documented below.
48 %%
ab8485d edoc documentation for stateless servers and hello_simple_client
Felix Lange authored
49 %% === ?record_to_json_obj(RecordName::atom(), Record::tuple()) -> value() ===
1c0db49 @fjl move record-to-object conversion to the hello_json module
fjl authored
50 %%
51 %% This macro converts a record to a JSON object. Some things to be aware of:
52 %% <ul>
53 %% <li>`undefined' is converted to `null'</li>
54 %% <li>The values contained in the record should adhere to the {@link value()} type specification.
55 %% Among other things, this means that all strings must be encoded as binaries and the only kind
56 %% of tuple allowed is `{proplist()}'.<br/>
57 %% If any value cannot be encoded, the conversion will exit with error `badjson'.
58 %% </li>
59 %% <li>The value passed in as `Record' must match the record definition of `RecordName'.
60 %% If it doesn't, the conversion will exit with reason `badarg'.
61 %% </li>
62 %% </ul>
63 %%
ab8485d edoc documentation for stateless servers and hello_simple_client
Felix Lange authored
64 %% === ?json_obj_to_record(RecordName::atom(), Obj::value()) -> tuple() ===
1c0db49 @fjl move record-to-object conversion to the hello_json module
fjl authored
65 %%
66 %% This macro converts a JSON object into an Erlang record. The conversion will ignore any keys in the object that
67 %% that do not have a corresponding field in the record. For missing keys the default value specified <i>in the record definition</i>
68 %% will be used. Furthermore,
69 %% <ul>
70 %% <li>`null' is converted to `undefined'.</li>
71 %% <li>The conversion will exit with error `badarg' if `Obj' is not a JSON object.</li>
72 %% </ul>
73 %%
ab8485d edoc documentation for stateless servers and hello_simple_client
Felix Lange authored
74 %% === ?json_obj_into_record(RecordName::atom(), Defaults::tuple(), Obj::value()) -> tuple() ===
1c0db49 @fjl move record-to-object conversion to the hello_json module
fjl authored
75 %%
76 %% This macro performs the same function as <b>`?json_obj_to_record/2'</b>, except that in case of missing keys the value
77 %% used is taken <i>from the `Defaults' record</i>. You might find this macro useful if you want to merge an object
78 %% <i>into</i> an existing instance of the record type in question.
79 %% @end
80
6f1657a @fjl rename the project to 'hello'
fjl authored
81 -module(hello_json).
af460ce @fjl add new json parser
fjl authored
82 -export([encode/1, decode/1]).
1c0db49 @fjl move record-to-object conversion to the hello_json module
fjl authored
83 -export([object_to_record/5, record_to_object/4]).
84 -export_type([value/0, json_string/0, json_number/0, json_boolean/0, json_array/0, json_object/0, json_null/0]).
c6ad73d json: decode escape sequences correctly
Felix Lange authored
85 -compile(bin_opt_info).
1c0db49 @fjl move record-to-object conversion to the hello_json module
fjl authored
86
87 -type value() :: json_string() | json_number() | json_boolean() | json_array() | json_object() | json_null().
88 -type json_string() :: binary().
89 -type json_number() :: integer() | float().
90 -type json_boolean() :: boolean().
91 -type json_array() :: list(value()).
92 -type json_object() :: {list({binary(), value()})}.
93 -type json_null() :: 'null'.
94
af460ce @fjl add new json parser
fjl authored
95 %% --------------------------------------------------------------------------------
96 %% -- Encoder
1c0db49 @fjl move record-to-object conversion to the hello_json module
fjl authored
97 -spec encode(value()) -> binary().
98
af460ce @fjl add new json parser
fjl authored
99 encode(Num) when is_integer(Num) -> list_to_binary(integer_to_list(Num));
100 encode(Num) when is_float(Num) -> list_to_binary(float_to_list(Num));
101 encode(true) -> <<"true">>;
102 encode(false) -> <<"false">>;
103 encode(null) -> <<"null">>;
7ef7b98 @fjl use ejson-like encoding for JSON objects
fjl authored
104 encode({Props}) -> enc_obj(Props, <<"{">>);
af460ce @fjl add new json parser
fjl authored
105 encode({obj, Props}) -> enc_obj(Props, <<"{">>);
106 encode(Lis) when is_list(Lis) -> enc_array(Lis, <<"[">>);
107 encode(AnythingElse) -> enc_string(AnythingElse).
108
109 enc_obj([], Result) ->
110 <<Result/binary, "}">>;
111 enc_obj([{K, V}], Result) ->
112 <<Result/binary, (enc_string(K))/binary, ":", (encode(V))/binary, "}">>;
113 enc_obj([{K, V}, Next | Rest], Result) ->
114 NR = <<Result/binary, (enc_string(K))/binary, ":", (encode(V))/binary, ",">>,
115 enc_obj([Next | Rest], NR).
116
117 enc_array([], Result) ->
118 <<Result/binary, "]">>;
119 enc_array([Elem], Result) ->
120 <<Result/binary, (encode(Elem))/binary, "]">>;
121 enc_array([Elem, Next | Rest], Result) ->
122 NR = <<Result/binary, (encode(Elem))/binary, ",">>,
123 enc_array([Next | Rest], NR).
124
125 enc_string(Bin) when is_binary(Bin) -> <<$", (escape(Bin))/binary, $">>;
126 enc_string(Lis) when is_list(Lis) -> <<$", (escape(list_to_binary(Lis)))/binary, $">>;
127 enc_string(Atm) when is_atom(Atm) -> <<$", (escape(atom_to_binary(Atm, utf8)))/binary, $">>;
128 enc_string(Int) when is_integer(Int) -> <<$", (list_to_binary(integer_to_list(Int)))/binary, $">>;
129 enc_string(Flt) when is_float(Flt) -> <<$", (list_to_binary(io_lib:write(Flt)))/binary, $">>.
130
131 -compile({inline, escape/1}).
132 escape(Bin) ->
133 << <<(esc_chr(Chr))/binary>> || <<Chr/utf8>> <= Bin >>.
134
135 -compile({inline, esc_chr/1}).
136 esc_chr($") -> <<"\\\"">>;
137 esc_chr($\\) -> <<"\\\\">>;
367ae2d fix encoding of some backslash escape sequences
Felix Lange authored
138 esc_chr($\n) -> <<"\\n">>;
139 esc_chr($\r) -> <<"\\r">>;
140 esc_chr($\t) -> <<"\\t">>;
141 esc_chr($\b) -> <<"\\b">>;
142 esc_chr($\f) -> <<"\\f">>;
af460ce @fjl add new json parser
fjl authored
143 esc_chr(Chr) when Chr > 31 -> <<Chr/utf8>>;
144 esc_chr(Chr) -> <<"\\u", (pad4(list_to_binary(integer_to_list(Chr, 16))))/binary>>.
145
146 -compile({inline, pad4/1}).
147 pad4(<<X>>) -> <<"000", X>>;
148 pad4(<<X, Y>>) -> <<"00", X, Y>>;
149 pad4(<<X, Y, Z>>) -> <<"0", X, Y, Z>>;
150 pad4(Bin) -> Bin.
151
152 %% --------------------------------------------------------------------------------
153 %% -- Decoder
1c0db49 @fjl move record-to-object conversion to the hello_json module
fjl authored
154 -spec decode(string() | binary()) -> {ok, value(), binary()} | {error, syntax_error}.
155
af460ce @fjl add new json parser
fjl authored
156 decode(JSON) when is_list(JSON) ->
157 decode1(iolist_to_binary(JSON));
158 decode(JSON) when is_binary(JSON) ->
159 decode1(JSON);
160 decode(_) ->
161 error(badarg).
162
c6ad73d json: decode escape sequences correctly
Felix Lange authored
163 -compile({inline, decode1/1}).
164 decode1(Bin) ->
af460ce @fjl add new json parser
fjl authored
165 try
166 {Dec, Rest} = decode2(Bin),
167 {ok, Dec, Rest}
168 catch
169 error:syntax_error ->
170 {error, syntax_error}
209dadc init
Felix Lange authored
171 end.
172
af460ce @fjl add new json parser
fjl authored
173 decode2(<<Bin/binary>>) ->
174 case skipspace(Bin) of
175 <<${, Text/binary>> -> dec_object(Text, []);
176 <<$[, Text/binary>> -> dec_array(Text, []);
177 <<$", Text/binary>> -> dec_stringb(Text);
178 <<D, Text/binary>> when (D =:= $-) orelse ((D >= $0) and (D =< $9)) -> dec_number(Text, D);
179 <<"true", R/binary>> -> {true, R};
180 <<"false", R/binary>> -> {false, R};
181 <<"null", R/binary>> -> {null, R};
182 <<>> -> error(syntax_error); % eof
183 <<_/binary>> -> error(syntax_error)
209dadc init
Felix Lange authored
184 end.
185
af460ce @fjl add new json parser
fjl authored
186 -compile({inline, dec_stringb/1}).
187 dec_stringb(Bin) ->
188 {StrLis, Rest} = dec_string(Bin, []),
189 {unicode:characters_to_binary(StrLis, utf8), Rest}.
190
191 dec_string(<<Bin/binary>>, Res) ->
192 case Bin of
193 <<$\\, R1/binary>> ->
194 case R1 of
c6ad73d json: decode escape sequences correctly
Felix Lange authored
195 <<C, R2/binary>> when (C =:= $"); (C =:= $\\) ->
af460ce @fjl add new json parser
fjl authored
196 dec_string(R2, [C | Res]);
c6ad73d json: decode escape sequences correctly
Felix Lange authored
197 <<$n, R2/binary>> ->
198 dec_string(R2, [$\n | Res]);
199 <<$r, R2/binary>> ->
200 dec_string(R2, [$\r | Res]);
201 <<$t, R2/binary>> ->
202 dec_string(R2, [$\t | Res]);
203 <<$b, R2/binary>> ->
204 dec_string(R2, [$\b | Res]);
205 <<$f, R2/binary>> ->
206 dec_string(R2, [$\f | Res]);
af460ce @fjl add new json parser
fjl authored
207 <<$u, A, B, C, D, R2/binary>> ->
c6ad73d json: decode escape sequences correctly
Felix Lange authored
208 dec_string(R2, [list_to_integer([A,B,C,D], 16) | Res]);
209 _ ->
210 error(syntax_error) % bad escape seq
af460ce @fjl add new json parser
fjl authored
211 end;
212 <<$", R/binary>> ->
213 {lists:reverse(Res), R};
214 <<C/utf8, R/binary>> ->
215 dec_string(R, [C | Res]);
216 <<>> ->
217 error(syntax_error) % eof
209dadc init
Felix Lange authored
218 end.
219
af460ce @fjl add new json parser
fjl authored
220 -compile({inline, dec_number/2}).
221 dec_number(Bin, C) ->
222 {NumLis, Rest} = numchars(Bin, [C]),
223 case (catch list_to_integer(NumLis)) of
224 {'EXIT', {badarg, _}} ->
225 {list_to_float(NumLis), Rest};
226 Int ->
227 {Int, Rest}
209dadc init
Felix Lange authored
228 end.
229
af460ce @fjl add new json parser
fjl authored
230 -compile({inline, numchars/2}).
231 numchars(Bin, Res) ->
232 case Bin of
233 <<C, R/binary>> when ((C >= $0) and (C =< $9)) or (C =:= $.)
234 or (C =:= $e) or (C =:= $E) or (C =:= $-) or (C =:= $+) ->
235 numchars(R, [C|Res]);
236 Rest ->
237 {lists:reverse(Res), Rest}
209dadc init
Felix Lange authored
238 end.
239
af460ce @fjl add new json parser
fjl authored
240 dec_array(<<Bin/binary>>, Res) ->
241 case skipspace(Bin) of
242 <<"]", R1/binary>> -> {[], R1};
243 NonEmpty ->
244 {Term, R2} = decode2(NonEmpty),
245 case skipspace(R2) of
246 <<",", R3/binary>> -> dec_array(R3, [Term | Res]);
247 <<"]", R3/binary>> -> {lists:reverse([Term | Res]), R3};
248 _ -> error(syntax_error)
249 end
209dadc init
Felix Lange authored
250 end.
251
af460ce @fjl add new json parser
fjl authored
252 dec_object(<<Bin/binary>>, Res) ->
253 case skipspace(Bin) of
254 <<"}", Rest/binary>> ->
7ef7b98 @fjl use ejson-like encoding for JSON objects
fjl authored
255 {{[]}, Rest};
af460ce @fjl add new json parser
fjl authored
256 <<$", R1/binary>> ->
ff5a8e5 @liveforeverx A better way to decode Keys from Object.
liveforeverx authored
257 {Key, R2} = dec_stringb(R1),
af460ce @fjl add new json parser
fjl authored
258 <<":", R3/binary>> = skipspace(R2),
259 {Value, R4} = decode2(R3),
260 case skipspace(R4) of
ff5a8e5 @liveforeverx A better way to decode Keys from Object.
liveforeverx authored
261 <<",", R5/binary>> -> dec_object(R5, [{Key, Value} | Res]);
262 <<"}", R5/binary>> -> {{[{Key, Value} | Res]}, R5};
af460ce @fjl add new json parser
fjl authored
263 _ -> error(syntax_error)
264 end;
265 _ ->
266 error(syntax_error)
209dadc init
Felix Lange authored
267 end.
268
af460ce @fjl add new json parser
fjl authored
269 skipspace(<<Bin/binary>>) ->
270 case Bin of
271 <<" ", R/binary>> -> skipspace(R);
272 <<"\t", R/binary>> -> skipspace(R);
273 <<"\n", R/binary>> -> skipspace(R);
274 <<"\r", R/binary>> -> skipspace(R);
275 <<"\f", R/binary>> -> skipspace(R);
276 Else -> Else
209dadc init
Felix Lange authored
277 end.
1c0db49 @fjl move record-to-object conversion to the hello_json module
fjl authored
278
279 %% --------------------------------------------------------------------------------
280 %% -- Object to Record
281 %% @hidden
282 -spec object_to_record(atom(), [atom()], integer(), tuple(), json_object()) -> tuple().
283
284 object_to_record(RecName, RecAttrs, RecSize, Templ, {Props}) when is_list(Props) ->
285 (not is_tuple(Templ)) andalso error(badarg),
286 (element(1, Templ) /= RecName) andalso error(badarg),
287 (tuple_size(Templ) /= RecSize) andalso error(badarg),
288
289 {_EndP, Rec} = lists:foldl(fun (Attr, {Posn, TheRec}) ->
70a3701 fix object to record for new key encoding (refs #480)
Felix Lange authored
290 case proplists:get_value(atom_to_binary(Attr, utf8), Props) of
1c0db49 @fjl move record-to-object conversion to the hello_json module
fjl authored
291 undefined -> {Posn + 1, TheRec};
292 null -> {Posn + 1, setelement(Posn, TheRec, undefined)};
293 Value -> {Posn + 1, setelement(Posn, TheRec, Value)}
294 end
295 end, {2, Templ}, RecAttrs),
296 Rec;
297 object_to_record(_RecName, _RecAttrs, _RecSize, _Defaults, _NonObject) ->
298 error(badarg).
299
300 %% @hidden
301 -spec record_to_object(atom(), [atom()], integer(), tuple()) -> json_object().
302
303 record_to_object(RecName, RecAttrs, RecSize, Tuple) when is_tuple(Tuple) ->
304 (element(1, Tuple) /= RecName) andalso error(badarg),
305 (tuple_size(Tuple) /= RecSize) andalso error(badarg),
306 {_EndP, ObjProps} =
307 lists:foldl(fun (Attr, {Posn, Proplis}) ->
308 {Posn + 1, [{atom_to_list(Attr), ensure_json_compat(element(Posn, Tuple))} | Proplis]}
309 end, {2, []}, RecAttrs),
310 {ObjProps};
311 record_to_object(_RecNam, _RecAttr, _RecSiz, _Tuple) ->
312 error(badarg).
313
314 ensure_json_compat(undefined) -> null;
315 ensure_json_compat(null) -> null;
316 ensure_json_compat(true) -> true;
317 ensure_json_compat(false) -> false;
318 ensure_json_compat(A) when is_atom(A) -> atom_to_binary(A, utf8);
319 ensure_json_compat(B) when is_binary(B) -> B;
320 ensure_json_compat(N) when is_number(N) -> N;
321 ensure_json_compat(L) when is_list(L) ->
322 lists:map(fun ensure_json_compat/1, L);
323 ensure_json_compat({Props}) when is_list(Props) ->
324 {lists:map(fun ({K, V}) -> {K, ensure_json_compat(V)} end, Props)};
325 ensure_json_compat(_Val) ->
326 error(badjson).
Something went wrong with that request. Please try again.