├── .github └── workflows │ └── ci.yml ├── .gitignore ├── COPYING ├── Makefile ├── README.md ├── benchmark ├── .gitignore ├── README.md ├── poison.patch └── run.sh ├── doc ├── README.md └── jsone.md ├── rebar.config ├── rebar.lock ├── src ├── jsone.app.src ├── jsone.erl ├── jsone_decode.erl ├── jsone_encode.erl └── jsone_inet.erl └── test ├── jsone_decode_tests.erl ├── jsone_encode_tests.erl ├── jsone_inet_tests.erl └── test_time_module.erl /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: build 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | jobs: 11 | ci: 12 | name: Run checks and tests over ${{matrix.otp}} and ${{matrix.os}} 13 | runs-on: ${{matrix.os}} 14 | strategy: 15 | matrix: 16 | otp: ['25.0', '26.0', '27.0'] 17 | os: [ubuntu-latest] 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: erlef/setup-beam@v1 21 | with: 22 | otp-version: ${{matrix.otp}} 23 | rebar3-version: '3.23.0' 24 | - run: rebar3 compile 25 | - run: rebar3 xref 26 | - run: rebar3 efmt -c 27 | - run: rebar3 eunit 28 | - run: rebar3 dialyzer 29 | - run: rebar3 edoc 30 | cov: 31 | needs: ci 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v4 35 | - uses: erlef/setup-beam@v1 36 | with: 37 | otp-version: '26.0' 38 | rebar3-version: '3.23.0' 39 | - run: rebar3 eunit 40 | - run: rebar3 cover 41 | - run: rebar3 covertool generate 42 | - run: cp _build/test/covertool/jsone.covertool.xml ./cobertura.xml 43 | - run: sudo pip install codecov 44 | - run: codecov 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .eunit 2 | deps 3 | *.o 4 | *.beam 5 | *.plt 6 | *.dump 7 | *.app 8 | *~ 9 | doc/* 10 | !doc/overview.edoc 11 | !doc/*.md 12 | .rebar 13 | _build 14 | .*.sw? 15 | rebar3.crashdump 16 | .efmt/ 17 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2013-2016 Takeru Ohta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: compile xref eunit dialyze edoc 2 | 3 | rebar ?= rebar3 4 | rebar_cmd = $(rebar) $(profile:%=as %) 5 | 6 | compile: 7 | @$(rebar_cmd) compile 8 | 9 | xref: 10 | @$(rebar_cmd) xref 11 | 12 | clean: 13 | @$(rebar_cmd) clean 14 | 15 | eunit: 16 | @$(rebar_cmd) do eunit,cover 17 | 18 | edoc: profile=edown 19 | edoc: 20 | @$(rebar_cmd) edoc 21 | 22 | start: compile 23 | -@$(rebar_cmd) shell 24 | 25 | dialyze: compile 26 | @$(rebar_cmd) dialyzer 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | jsone 2 | ===== 3 | 4 | [![hex.pm version](https://img.shields.io/hexpm/v/jsone.svg)](https://hex.pm/packages/jsone) 5 | [![Build Status](https://github.com/sile/jsone/workflows/build/badge.svg)](https://github.com/sile/jsone) 6 | [![Code Coverage](https://codecov.io/gh/sile/jsone/branch/master/graph/badge.svg)](https://codecov.io/gh/sile/jsone/branch/master) 7 | [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) 8 | 9 | An Erlang library for encoding, decoding [JSON](http://json.org/index.html) data. 10 | 11 | 12 | Features 13 | -------- 14 | - Provides simple encode/decode function only 15 | - [RFC7159](http://www.ietf.org/rfc/rfc7159.txt)-compliant 16 | - Supports UTF-8 encoded binary 17 | - Pure Erlang 18 | - Highly Efficient 19 | - Maybe one of the fastest JSON library (except those which are implemented in NIF) 20 | - See [Benchmark](benchmark/README.md) 21 | - Decode function is written in continuation-passing style(CPS) 22 | - CPS facilitates application of 'creation of sub binary delayed' optimization 23 | - See also [Erlang Efficiency Guide](http://www.erlang.org/doc/efficiency_guide/binaryhandling.html) 24 | 25 | 26 | QuickStart 27 | ---------- 28 | 29 | ```sh 30 | # clone 31 | $ git clone git://github.com/sile/jsone.git 32 | $ cd jsone 33 | 34 | # compile 35 | $ make compile 36 | 37 | # run tests 38 | $ make eunit 39 | 40 | # dialyze 41 | $ make dialyze 42 | 43 | # Erlang shell 44 | $ make start 45 | 1> jsone:decode(<<"[1,2,3]">>). 46 | [1,2,3] 47 | ``` 48 | 49 | Usage Example 50 | ------------- 51 | 52 | ```erlang 53 | %% Decode 54 | > jsone:decode(<<"[1,2,3]">>). 55 | [1,2,3] 56 | 57 | > jsone:decode(<<"{\"1\":2}">>). 58 | #{<<"1">> => 2} 59 | 60 | > jsone:decode(<<"{\"1\":2}">>, [{object_format, tuple}]). % tuple format 61 | {[{<<"1">>, 2}]} 62 | 63 | > jsone:decode(<<"{\"1\":2}">>, [{object_format, proplist}]). % proplist format 64 | [{<<"1">>, 2}] 65 | 66 | > jsone:try_decode(<<"[1,2,3] \"next value\"">>). % try_decode/1 returns remaining (unconsumed binary) 67 | {ok,[1,2,3],<<" \"next value\"">>} 68 | 69 | % error: raises exception 70 | > jsone:decode(<<"1.x">>). 71 | ** exception error: bad argument 72 | in function jsone_decode:number_fraction_part_rest/6 73 | called as jsone_decode:number_fraction_part_rest(<<"x">>,1,1,0,[],<<>>) 74 | in call from jsone:decode/1 (src/jsone.erl, line 71) 75 | 76 | % error: returns {error, Reason} 77 | > jsone:try_decode(<<"1.x">>). 78 | {error,{badarg,[{jsone_decode,number_fraction_part_rest, 79 | [<<"x">>,1,1,0,[],<<>>], 80 | [{line,228}]}]}} 81 | 82 | 83 | %% Encode 84 | > jsone:encode([1,2,3]). 85 | <<"[1,2,3]">> 86 | 87 | > jsone:encode(#{<<"key">> => <<"value">>}). % map format 88 | > jsone:encode({[{<<"key">>, <<"value">>}]}). % tuple format 89 | > jsone:encode([{<<"key">>, <<"value">>}]). % proplist format 90 | <<"{\"key\":\"value\"}">> 91 | 92 | > jsone:encode(#{key => <<"value">>}). % atom key is allowed 93 | <<"{\"key\":\"value\"}">> 94 | 95 | % error: raises exception 96 | > jsone:encode(#{123 => <<"value">>}). % non binary|atom key is not allowed 97 | ** exception error: bad argument 98 | in function jsone_encode:object_members/3 99 | called as jsone_encode:object_members([{123,<<"value">>}],[],<<"{">>) 100 | in call from jsone:encode/1 (src/jsone.erl, line 97) 101 | 102 | % error: returns {error, Reason} 103 | > jsone:try_encode({[{123, <<"value">>}]}). 104 | {error,{badarg,[{jsone_encode,object_members, 105 | [[{123,<<"value">>}],[],<<"{">>], 106 | [{line,138}]}]}} 107 | 108 | % 'object_key_type' option allows non-string object key 109 | > jsone:encode({[{123, <<"value">>}]}, [{object_key_type, scalar}]). 110 | <<"{\"123\":\"value\"}">> 111 | 112 | % 'undefined_as_null' option allows encoding atom undefined as null 113 | > jsone:encode(undefined,[undefined_as_null]). 114 | <<"null">> 115 | 116 | %% Pretty Print 117 | > Data = [true, #{<<"1">> => 2, <<"array">> => [[[[1]]], #{<<"ab">> => <<"cd">>}, [], #{}, false]}, null]. 118 | > io:format("~s\n", [jsone:encode(Data, [{indent, 2}, {space, 1}])]). 119 | [ 120 | true, 121 | { 122 | "1": 2, 123 | "array": [ 124 | [ 125 | [ 126 | [ 127 | 1 128 | ] 129 | ] 130 | ], 131 | { 132 | "ab": "cd" 133 | }, 134 | [], 135 | {}, 136 | false 137 | ] 138 | }, 139 | null 140 | ] 141 | ok 142 | 143 | %% Number Format 144 | > jsone:encode(1). % integer 145 | <<"1">> 146 | 147 | > jsone:encode(1.23). % float 148 | <<"1.22999999999999998224e+00">> % default: scientific notation 149 | 150 | > jsone:encode(1.23, [{float_format, [{decimals, 4}]}]). % decimal notation 151 | <<"1.2300">> 152 | 153 | > jsone:encode(1.23, [{float_format, [{decimals, 4}, compact]}]). % compact decimal notation 154 | <<"1.23">> 155 | 156 | %% If you want to safely cast object keys to atoms, the `attempt_atom' option will help. 157 | > jsone:decode(<<"{\"hello\": \"world\"}">>, [{keys, attempt_atom}]). 158 | #{<<"hello">> => <<"world">>} % There is no atom named "hello", so the key is decoded as binary. 159 | 160 | > hello. % Create "hello" atom. 161 | hello 162 | 163 | > jsone:decode(<<"{\"hello\": \"world\"}">>, [{keys, attempt_atom}]). 164 | #{hello => <<"world">>} % Now, the key is decoded as atom. 165 | ``` 166 | 167 | 168 | Data Mapping (Erlang <=> JSON) 169 | ------------------------------- 170 | 171 | ``` 172 | Erlang JSON Erlang 173 | ================================================================================================= 174 | 175 | null -> null -> null 176 | undefined -> null -> undefined % undefined_as_null 177 | true -> true -> true 178 | false -> false -> false 179 | <<"abc">> -> "abc" -> <<"abc">> 180 | abc -> "abc" -> <<"abc">> % non-special atom is regarded as a binary 181 | {{2010,1,1},{0,0,0}} -> "2010-01-01T00:00:00Z" -> <<"2010-01-01T00:00:00Z">> % datetime* 182 | {{2010,1,1},{0,0,0.0}} -> "2010-01-01T00:00:00.000Z" -> <<"2010-01-01T00:00:00.000Z">> % datetime* 183 | 123 -> 123 -> 123 184 | 123.4 -> 123.4 -> 123.4 185 | [1,2,3] -> [1,2,3] -> [1,2,3] 186 | {[]} -> {} -> {[]} % object_format=tuple 187 | {[{key, <<"val">>}]} -> {"key":"val"} -> {[{<<"key">>, <<"val">>}]} % object_format=tuple 188 | [{}] -> {} -> [{}] % object_format=proplist 189 | [{<<"key">>, val}] -> {"key":"val"} -> [{<<"key">>, <<"val">>}] % object_format=proplist 190 | #{} -> {} -> #{} % object_format=map 191 | #{key => val} -> {"key":"val"} -> #{<<"key">> => <<"val">>} % object_format=map 192 | {{json, IOList}} -> Value -> ~~~ % UTF-8 encoded term** 193 | {{json_utf8, Chars}} -> Value -> ~~~ % Unicode code points** 194 | ``` 195 | 196 | \* see [jsone:datetime_encode_format()](doc/jsone.md#type-datetime_encode_format) 197 | 198 | \** `{json, IOList}` and `{json_utf8, Chars}` allows inline already encoded JSON 199 | values. For example, you obtain JSON encoded data from database so you don't 200 | have to decode it first and encode again. See [jsone:json_term()](doc/jsone.md#type-json_term). 201 | 202 | API 203 | --- 204 | 205 | See [EDoc Document](doc/jsone.md) 206 | 207 | 208 | Benchmark 209 | --------- 210 | 211 | The results of [poison](https://github.com/devinus/poison) benchmarking. 212 | 213 | See the [benchmark/README.md](benchmark/README.md) file for more information. 214 | 215 | ### Encoding (Unit: IPS=inputs per second) 216 | 217 | | Input data \ Library | Jason | jiffy | JSON* | jsone | JSX | Poison | Tiny | 218 | |----------------------|-----------:|------------:|-------:|-----------:|-------:|-------:|-------:| 219 | | [Blockchain] | 2.77 K | **4.55 K** | 0.45 K | 1.44 K (3) | 0.60 K | 1.30 K | 0.99 K | 220 | | [Giphy] | 230.65 | **487.67** | 47.73 | 114.57 (4) | 44.97 | 114.57 | 113.59 | 221 | | [GitHub] | 880.03 | **1566.67** | 139.79 | 300.26 (5) | 99.68 | 424.75 | 455.07 | 222 | | [GovTrack] | 6.57 | **24.92** | 2.33 | 5.35 (5) | 2.65 | 7.06 | 7.86 | 223 | | [Issue 90] | **22.80** | 21.92 | 0.77 | 14.30 (3) | 5.33 | 12.60 | 12.95 | 224 | | [JSON Generateor] | 200.40 | **606.81** | 42.45 | 147.12 (4) | 68.73 | 187.95 | 123.93 | 225 | | [Pokedex] | 209.51 | **776.67** | 62.60 | 161.45 (4) | 69.87 | 190.93 | 125.16 | 226 | | [UTF-8 unescaped] | 626.25 | **6644.53** |1167.89 | 582.41 (4) | 273.48 | 401.44 | 220.14 | 227 | 228 | \* Only `JSON` didn't escape non-ASCII unicode characters on the encoding 229 | 230 | [Blockchain]: https://github.com/devinus/poison/blob/4.0.1/bench/data/blockchain.json 231 | [Giphy]: https://github.com/devinus/poison/blob/4.0.1/bench/data/giphy.json 232 | [GitHub]: https://github.com/devinus/poison/blob/4.0.1/bench/data/github.json 233 | [GovTrack]: https://github.com/devinus/poison/blob/4.0.1/bench/data/govtrack.json 234 | [Issue 90]: https://github.com/devinus/poison/blob/4.0.1/bench/data/issue-90.json 235 | [JSON Generateor]: https://github.com/devinus/poison/blob/4.0.1/bench/data/json-generator.json 236 | [JSON Generateor (Pretty)]: https://github.com/devinus/poison/blob/4.0.1/bench/data/json-generator-pretty.json 237 | [Pokedex]: https://github.com/devinus/poison/blob/4.0.1/bench/data/pokedex.json 238 | [UTF-8 escaped]: https://github.com/devinus/poison/blob/4.0.1/bench/data/utf-8-escaped.json 239 | [UTF-8 unescaped]: https://github.com/devinus/poison/blob/4.0.1/bench/data/utf-8-unescaped.json 240 | 241 | ### Decoding (Unit: IPS=inputs per second) 242 | 243 | | Input data \ Library | Jason | jiffy | JSON | jsone | JSX | Poison | Tiny | 244 | |----------------------------|-----------:|------------:|-------:|-----------:|-------:|-------:|-------:| 245 | | [Blockchain] | **2.75 K** | 2.62 K | 0.35 K | 2.21 K (3) | 0.89 K | 1.32 K | 1.49 K | 246 | | [Giphy] | 212.18 | **243.45** | 35.67 | 109.11 (5) | 64.32 | 110.76 | 114.54 | 247 | | [GitHub] | 973.41 | **1052.94** | 137.02 | 662.39 (3) | 271.97 | 438.79 | 542.67 | 248 | | [GovTrack] | **10.77** | 8.32 | 0.80 | 5.08 (3) | 2.81 | 3.58 | 3.65 | 249 | | [Issue 90] | 17.85 | **41.16** | 0.88 | 10.79 (5) | 6.02 | 13.63 | 14.03 | 250 | | [JSON Generateor] | **320.79** | 243.93 | 25.16 | 184.23 (3) | 111.24 | 135.47 | 139.78 | 251 | | [JSON Generateor (Pretty)] | **273.57** | 205.09 | 25.04 | 158.82 (3) | 97.93 | 123.31 | 136.65 | 252 | | [Pokedex] | **527.63** | 285.43 | 33.70 | 245.36 (3) | 140.90 | 172.45 | 152.59 | 253 | | [UTF-8 escaped] | 1224.48 | **7923.08** | 326.43 | 573.70 (4) | 550.36 | 918.21 | 520.31 | 254 | | [UTF-8 unescaped] | 5.56 K | **12.54 K** | 1.35 K | 5.09 K (3) | 3.30 K | 4.39 K | 1.46 K | 255 | 256 | 257 | License 258 | ------- 259 | 260 | This library is released under the MIT License. 261 | 262 | See the [COPYING](COPYING) file for full license information. 263 | -------------------------------------------------------------------------------- /benchmark/.gitignore: -------------------------------------------------------------------------------- 1 | poison/ -------------------------------------------------------------------------------- /benchmark/README.md: -------------------------------------------------------------------------------- 1 | Benchmark 2 | ========= 3 | 4 | Using [poison](https://github.com/devinus/poison)'s benchmark. 5 | Please execute the `./run.sh` script in this directory to run the benchmark. 6 | 7 | Benchmark Result 8 | ---------------- 9 | 10 | Version: 11 | - OTP-24 12 | - jsone: [v1.6.0](https://github.com/sile/jsone/releases/tag/1.6.0) 13 | 14 | Summary 15 | ------- 16 | 17 | ### Encoding (Unit: IPS=inputs per second) 18 | 19 | | Input data \ Library | Jason | jiffy | JSON* | jsone | JSX | Poison | Tiny | 20 | |----------------------|-----------:|------------:|-------:|-----------:|-------:|-------:|-------:| 21 | | [Blockchain] | 2.77 K | **4.55 K** | 0.45 K | 1.44 K (3) | 0.60 K | 1.30 K | 0.99 K | 22 | | [Giphy] | 230.65 | **487.67** | 47.73 | 114.57 (4) | 44.97 | 114.57 | 113.59 | 23 | | [GitHub] | 880.03 | **1566.67** | 139.79 | 300.26 (5) | 99.68 | 424.75 | 455.07 | 24 | | [GovTrack] | 6.57 | **24.92** | 2.33 | 5.35 (5) | 2.65 | 7.06 | 7.86 | 25 | | [Issue 90] | **22.80** | 21.92 | 0.77 | 14.30 (3) | 5.33 | 12.60 | 12.95 | 26 | | [JSON Generateor] | 200.40 | **606.81** | 42.45 | 147.12 (4) | 68.73 | 187.95 | 123.93 | 27 | | [Pokedex] | 209.51 | **776.67** | 62.60 | 161.45 (4) | 69.87 | 190.93 | 125.16 | 28 | | [UTF-8 unescaped] | 626.25 | **6644.53** |1167.89 | 582.41 (4) | 273.48 | 401.44 | 220.14 | 29 | 30 | \* Only `JSON` didn't escape non-ASCII unicode characters on the encoding 31 | 32 | [Blockchain]: https://github.com/devinus/poison/blob/4.0.1/bench/data/blockchain.json 33 | [Giphy]: https://github.com/devinus/poison/blob/4.0.1/bench/data/giphy.json 34 | [GitHub]: https://github.com/devinus/poison/blob/4.0.1/bench/data/github.json 35 | [GovTrack]: https://github.com/devinus/poison/blob/4.0.1/bench/data/govtrack.json 36 | [Issue 90]: https://github.com/devinus/poison/blob/4.0.1/bench/data/issue-90.json 37 | [JSON Generateor]: https://github.com/devinus/poison/blob/4.0.1/bench/data/json-generator.json 38 | [JSON Generateor (Pretty)]: https://github.com/devinus/poison/blob/4.0.1/bench/data/json-generator-pretty.json 39 | [Pokedex]: https://github.com/devinus/poison/blob/4.0.1/bench/data/pokedex.json 40 | [UTF-8 escaped]: https://github.com/devinus/poison/blob/4.0.1/bench/data/utf-8-escaped.json 41 | [UTF-8 unescaped]: https://github.com/devinus/poison/blob/4.0.1/bench/data/utf-8-unescaped.json 42 | 43 | ### Decoding (Unit: IPS=inputs per second) 44 | 45 | | Input data \ Library | Jason | jiffy | JSON | jsone | JSX | Poison | Tiny | 46 | |----------------------------|-----------:|------------:|-------:|-----------:|-------:|-------:|-------:| 47 | | [Blockchain] | **2.75 K** | 2.62 K | 0.35 K | 2.21 K (3) | 0.89 K | 1.32 K | 1.49 K | 48 | | [Giphy] | 212.18 | **243.45** | 35.67 | 109.11 (5) | 64.32 | 110.76 | 114.54 | 49 | | [GitHub] | 973.41 | **1052.94** | 137.02 | 662.39 (3) | 271.97 | 438.79 | 542.67 | 50 | | [GovTrack] | **10.77** | 8.32 | 0.80 | 5.08 (3) | 2.81 | 3.58 | 3.65 | 51 | | [Issue 90] | 17.85 | **41.16** | 0.88 | 10.79 (5) | 6.02 | 13.63 | 14.03 | 52 | | [JSON Generateor] | **320.79** | 243.93 | 25.16 | 184.23 (3) | 111.24 | 135.47 | 139.78 | 53 | | [JSON Generateor (Pretty)] | **273.57** | 205.09 | 25.04 | 158.82 (3) | 97.93 | 123.31 | 136.65 | 54 | | [Pokedex] | **527.63** | 285.43 | 33.70 | 245.36 (3) | 140.90 | 172.45 | 152.59 | 55 | | [UTF-8 escaped] | 1224.48 | **7923.08** | 326.43 | 573.70 (4) | 550.36 | 918.21 | 520.31 | 56 | | [UTF-8 unescaped] | 5.56 K | **12.54 K** | 1.35 K | 5.09 K (3) | 3.30 K | 4.39 K | 1.46 K | 57 | 58 | 59 | Details 60 | ------- 61 | 62 | ``` 63 | $ ./run.sh 64 | 65 | ## 66 | ## Encoding 67 | ## 68 | Operating System: Linux 69 | CPU Information: Intel(R) Core(TM) i7-8850H CPU @ 2.60GHz 70 | Number of Available Cores: 12 71 | Available memory: 24.68 GB 72 | Elixir 1.7.4 73 | Erlang 24.0.2 74 | 75 | Benchmark suite executing with the following configuration: 76 | warmup: 2 s 77 | time: 5 s 78 | memory time: 0 ns 79 | parallel: 4 80 | inputs: Blockchain, Giphy, GitHub, GovTrack, Issue 90, JSON Generator, Pokedex, UTF-8 unescaped 81 | Estimated total run time: 6.53 min 82 | 83 | ##### With input Blockchain ##### 84 | Name ips average deviation median 99th % 85 | jiffy 4.55 K 219.79 μs ±227.93% 159.30 μs 600.33 μs 86 | Jason 2.77 K 361.51 μs ±280.89% 164.50 μs 6000.18 μs 87 | jsone 1.44 K 692.75 μs ±202.11% 303.80 μs 6609.40 μs 88 | Poison 1.30 K 769.30 μs ±208.79% 324.50 μs 7034.96 μs 89 | Tiny 0.99 K 1009.25 μs ±187.05% 312.70 μs 7021.59 μs 90 | JSX 0.60 K 1677.61 μs ±125.78% 818 μs 8187.88 μs 91 | JSON (w/o unicode escape) 0.45 K 2198.45 μs ±116.54% 1010.80 μs 9246.82 μs 92 | 93 | Comparison: 94 | jiffy 4.55 K 95 | Jason 2.77 K - 1.64x slower +141.72 μs 96 | jsone 1.44 K - 3.15x slower +472.95 μs 97 | Poison 1.30 K - 3.50x slower +549.50 μs 98 | Tiny 0.99 K - 4.59x slower +789.45 μs 99 | JSX 0.60 K - 7.63x slower +1457.82 μs 100 | JSON (w/o unicode escape) 0.45 K - 10.00x slower +1978.65 μs 101 | 102 | ##### With input Giphy ##### 103 | Name ips average deviation median 99th % 104 | jiffy 487.67 2.05 ms ±78.70% 1.54 ms 8.92 ms 105 | Jason 230.65 4.34 ms ±70.26% 2.06 ms 10.43 ms 106 | Poison 141.49 7.07 ms ±47.75% 7.49 ms 13.94 ms 107 | jsone 114.57 8.73 ms ±29.15% 9.66 ms 12.39 ms 108 | Tiny 113.59 8.80 ms ±33.13% 9.65 ms 14.60 ms 109 | JSON (w/o unicode escape) 47.73 20.95 ms ±25.23% 21.45 ms 45.18 ms 110 | JSX 44.97 22.24 ms ±5.54% 22.17 ms 25.09 ms 111 | 112 | Comparison: 113 | jiffy 487.67 114 | Jason 230.65 - 2.11x slower +2.29 ms 115 | Poison 141.49 - 3.45x slower +5.02 ms 116 | jsone 114.57 - 4.26x slower +6.68 ms 117 | Tiny 113.59 - 4.29x slower +6.75 ms 118 | JSON (w/o unicode escape) 47.73 - 10.22x slower +18.90 ms 119 | JSX 44.97 - 10.85x slower +20.19 ms 120 | 121 | ##### With input GitHub ##### 122 | Name ips average deviation median 99th % 123 | jiffy 1566.67 0.64 ms ±125.06% 0.49 ms 6.57 ms 124 | Jason 880.03 1.14 ms ±157.00% 0.53 ms 7.18 ms 125 | Tiny 455.07 2.20 ms ±111.76% 0.98 ms 8.56 ms 126 | Poison 424.75 2.35 ms ±104.47% 1.11 ms 8.72 ms 127 | jsone 300.26 3.33 ms ±83.98% 1.52 ms 9.15 ms 128 | JSON (w/o unicode escape) 139.79 7.15 ms ±46.69% 8.63 ms 13.20 ms 129 | JSX 99.68 10.03 ms ±17.20% 9.80 ms 19.04 ms 130 | 131 | Comparison: 132 | jiffy 1566.67 133 | Jason 880.03 - 1.78x slower +0.50 ms 134 | Tiny 455.07 - 3.44x slower +1.56 ms 135 | Poison 424.75 - 3.69x slower +1.72 ms 136 | jsone 300.26 - 5.22x slower +2.69 ms 137 | JSON (w/o unicode escape) 139.79 - 11.21x slower +6.52 ms 138 | JSX 99.68 - 15.72x slower +9.39 ms 139 | 140 | ##### With input GovTrack ##### 141 | Name ips average deviation median 99th % 142 | jiffy 24.92 40.13 ms ±17.28% 37.63 ms 65.87 ms 143 | Tiny 7.86 127.26 ms ±16.34% 120.95 ms 202.01 ms 144 | Poison 7.06 141.71 ms ±17.46% 141.24 ms 230.78 ms 145 | Jason 6.57 152.32 ms ±29.45% 166.96 ms 221.99 ms 146 | jsone 5.35 186.87 ms ±14.14% 193.19 ms 210.20 ms 147 | JSX 2.65 376.97 ms ±9.89% 372.41 ms 458.19 ms 148 | JSON (w/o unicode escape) 2.33 429.24 ms ±10.80% 425.02 ms 535.64 ms 149 | 150 | Comparison: 151 | jiffy 24.92 152 | Tiny 7.86 - 3.17x slower +87.13 ms 153 | Poison 7.06 - 3.53x slower +101.58 ms 154 | Jason 6.57 - 3.80x slower +112.20 ms 155 | jsone 5.35 - 4.66x slower +146.75 ms 156 | JSX 2.65 - 9.39x slower +336.85 ms 157 | JSON (w/o unicode escape) 2.33 - 10.70x slower +389.12 ms 158 | 159 | ##### With input Issue 90 ##### 160 | Name ips average deviation median 99th % 161 | Jason 22.80 43.87 ms ±24.63% 40.02 ms 75.01 ms 162 | jiffy 21.92 45.62 ms ±19.38% 41.89 ms 77.27 ms 163 | jsone 14.30 69.94 ms ±15.61% 67.54 ms 133.58 ms 164 | Tiny 12.95 77.24 ms ±20.56% 72.77 ms 124.05 ms 165 | Poison 12.60 79.39 ms ±21.64% 75.25 ms 136.51 ms 166 | JSX 5.33 187.61 ms ±12.43% 180.47 ms 285.20 ms 167 | JSON (w/o unicode escape) 0.77 1297.79 ms ±11.08% 1252.49 ms 1612.29 ms 168 | 169 | Comparison: 170 | Jason 22.80 171 | jiffy 21.92 - 1.04x slower +1.75 ms 172 | jsone 14.30 - 1.59x slower +26.07 ms 173 | Tiny 12.95 - 1.76x slower +33.37 ms 174 | Poison 12.60 - 1.81x slower +35.52 ms 175 | JSX 5.33 - 4.28x slower +143.74 ms 176 | JSON (w/o unicode escape) 0.77 - 29.58x slower +1253.92 ms 177 | 178 | ##### With input JSON Generator ##### 179 | Name ips average deviation median 99th % 180 | jiffy 606.81 1.65 ms ±67.68% 1.34 ms 7.17 ms 181 | Jason 200.40 4.99 ms ±67.53% 2.64 ms 10.38 ms 182 | Poison 187.95 5.32 ms ±57.09% 3.26 ms 12.65 ms 183 | jsone 147.12 6.80 ms ±44.42% 8.28 ms 11.48 ms 184 | Tiny 123.93 8.07 ms ±42.50% 9.39 ms 15.41 ms 185 | JSX 68.73 14.55 ms ±20.50% 13.50 ms 20.39 ms 186 | JSON (w/o unicode escape) 42.45 23.56 ms ±19.94% 24.33 ms 32.94 ms 187 | 188 | Comparison: 189 | jiffy 606.81 190 | Jason 200.40 - 3.03x slower +3.34 ms 191 | Poison 187.95 - 3.23x slower +3.67 ms 192 | jsone 147.12 - 4.12x slower +5.15 ms 193 | Tiny 123.93 - 4.90x slower +6.42 ms 194 | JSX 68.73 - 8.83x slower +12.90 ms 195 | JSON (w/o unicode escape) 42.45 - 14.29x slower +21.91 ms 196 | 197 | ##### With input Pokedex ##### 198 | Name ips average deviation median 99th % 199 | jiffy 776.67 1.29 ms ±87.38% 0.99 ms 6.88 ms 200 | Jason 209.51 4.77 ms ±65.87% 2.54 ms 11.37 ms 201 | Poison 190.93 5.24 ms ±53.42% 3.18 ms 10.66 ms 202 | jsone 161.45 6.19 ms ±54.22% 7.61 ms 13.84 ms 203 | Tiny 125.16 7.99 ms ±39.98% 9.51 ms 12.67 ms 204 | JSX 69.87 14.31 ms ±26.83% 13.24 ms 26.74 ms 205 | JSON (w/o unicode escape) 62.60 15.97 ms ±24.85% 15.47 ms 28.53 ms 206 | 207 | Comparison: 208 | jiffy 776.67 209 | Jason 209.51 - 3.71x slower +3.49 ms 210 | Poison 190.93 - 4.07x slower +3.95 ms 211 | jsone 161.45 - 4.81x slower +4.91 ms 212 | Tiny 125.16 - 6.21x slower +6.70 ms 213 | JSX 69.87 - 11.12x slower +13.02 ms 214 | JSON (w/o unicode escape) 62.60 - 12.41x slower +14.69 ms 215 | 216 | ##### With input UTF-8 unescaped ##### 217 | Name ips average deviation median 99th % 218 | jiffy 6644.53 0.150 ms ±71.33% 0.111 ms 0.52 ms 219 | JSON (w/o unicode escape) 1167.89 0.86 ms ±197.53% 0.33 ms 7.07 ms 220 | Jason 626.25 1.60 ms ±126.38% 0.79 ms 7.84 ms 221 | jsone 582.41 1.72 ms ±120.31% 0.87 ms 8.40 ms 222 | Poison 401.44 2.49 ms ±101.83% 1.05 ms 9.21 ms 223 | JSX 273.48 3.66 ms ±72.06% 1.66 ms 8.39 ms 224 | Tiny 220.14 4.54 ms ±73.89% 2.99 ms 10.39 ms 225 | 226 | Comparison: 227 | jiffy 6644.53 228 | JSON (w/o unicode escape) 1167.89 - 5.69x slower +0.71 ms 229 | Jason 626.25 - 10.61x slower +1.45 ms 230 | jsone 582.41 - 11.41x slower +1.57 ms 231 | Poison 401.44 - 16.55x slower +2.34 ms 232 | JSX 273.48 - 24.30x slower +3.51 ms 233 | Tiny 220.14 - 30.18x slower +4.39 ms 234 | 235 | 236 | ## 237 | ## Decoding 238 | ## 239 | Operating System: Linux 240 | CPU Information: Intel(R) Core(TM) i7-8850H CPU @ 2.60GHz 241 | Number of Available Cores: 12 242 | Available memory: 24.68 GB 243 | Elixir 1.7.4 244 | Erlang 24.0.2 245 | 246 | Benchmark suite executing with the following configuration: 247 | warmup: 2 s 248 | time: 5 s 249 | memory time: 0 ns 250 | parallel: 4 251 | inputs: Blockchain, Giphy, GitHub, GovTrack, Issue 90, JSON Generator, JSON Generator (Pretty), Pokedex, UTF-8 escaped, UTF-8 unescaped 252 | Estimated total run time: 8.17 min 253 | 254 | ##### With input Blockchain ##### 255 | Name ips average deviation median 99th % 256 | Jason 2.75 K 364.27 μs ±42.74% 318.40 μs 824.75 μs 257 | jiffy 2.62 K 380.96 μs ±26.26% 334.30 μs 697.00 μs 258 | jsone 2.21 K 452.58 μs ±33.19% 401.70 μs 916.63 μs 259 | Tiny 1.49 K 672.69 μs ±25.01% 622.10 μs 1144.22 μs 260 | Poison 1.32 K 759.77 μs ±36.93% 682 μs 1385.84 μs 261 | JSX 0.89 K 1129.63 μs ±22.84% 1093.05 μs 1827.75 μs 262 | JSON 0.35 K 2853.02 μs ±20.10% 2656.30 μs 4258.25 μs 263 | 264 | Comparison: 265 | Jason 2.75 K 266 | jiffy 2.62 K - 1.05x slower +16.69 μs 267 | jsone 2.21 K - 1.24x slower +88.31 μs 268 | Tiny 1.49 K - 1.85x slower +308.42 μs 269 | Poison 1.32 K - 2.09x slower +395.51 μs 270 | JSX 0.89 K - 3.10x slower +765.36 μs 271 | JSON 0.35 K - 7.83x slower +2488.75 μs 272 | 273 | ##### With input Giphy ##### 274 | Name ips average deviation median 99th % 275 | jiffy 243.45 4.11 ms ±24.50% 3.79 ms 7.30 ms 276 | Jason 212.18 4.71 ms ±12.22% 4.65 ms 6.38 ms 277 | Tiny 114.54 8.73 ms ±18.56% 8.49 ms 14.61 ms 278 | Poison 110.76 9.03 ms ±14.05% 8.77 ms 13.39 ms 279 | jsone 109.11 9.17 ms ±10.48% 8.99 ms 12.37 ms 280 | JSX 64.32 15.55 ms ±11.93% 15.05 ms 21.78 ms 281 | JSON 35.67 28.03 ms ±16.36% 27.43 ms 42.75 ms 282 | 283 | Comparison: 284 | jiffy 243.45 285 | Jason 212.18 - 1.15x slower +0.61 ms 286 | Tiny 114.54 - 2.13x slower +4.62 ms 287 | Poison 110.76 - 2.20x slower +4.92 ms 288 | jsone 109.11 - 2.23x slower +5.06 ms 289 | JSX 64.32 - 3.78x slower +11.44 ms 290 | JSON 35.67 - 6.82x slower +23.92 ms 291 | 292 | ##### With input GitHub ##### 293 | Name ips average deviation median 99th % 294 | jiffy 1052.94 0.95 ms ±22.77% 0.93 ms 1.61 ms 295 | Jason 973.41 1.03 ms ±25.72% 0.91 ms 1.63 ms 296 | jsone 662.39 1.51 ms ±29.01% 1.33 ms 2.44 ms 297 | Tiny 542.67 1.84 ms ±24.63% 1.65 ms 2.97 ms 298 | Poison 438.79 2.28 ms ±27.41% 2.00 ms 3.77 ms 299 | JSX 271.97 3.68 ms ±25.09% 3.37 ms 6.39 ms 300 | JSON 137.02 7.30 ms ±24.43% 6.51 ms 14.15 ms 301 | 302 | Comparison: 303 | jiffy 1052.94 304 | Jason 973.41 - 1.08x slower +0.0776 ms 305 | jsone 662.39 - 1.59x slower +0.56 ms 306 | Tiny 542.67 - 1.94x slower +0.89 ms 307 | Poison 438.79 - 2.40x slower +1.33 ms 308 | JSX 271.97 - 3.87x slower +2.73 ms 309 | JSON 137.02 - 7.68x slower +6.35 ms 310 | 311 | ##### With input GovTrack ##### 312 | Name ips average deviation median 99th % 313 | Jason 10.77 92.89 ms ±3.60% 92.38 ms 104.04 ms 314 | jiffy 8.32 120.21 ms ±13.66% 116.74 ms 209.22 ms 315 | jsone 5.08 196.89 ms ±2.84% 196.93 ms 213.51 ms 316 | Tiny 3.65 273.94 ms ±9.00% 266.38 ms 355.47 ms 317 | Poison 3.58 279.42 ms ±5.59% 275.79 ms 328.55 ms 318 | JSX 2.81 356.16 ms ±8.26% 348.67 ms 462.50 ms 319 | JSON 0.80 1246.50 ms ±8.67% 1200.07 ms 1461.19 ms 320 | 321 | Comparison: 322 | Jason 10.77 323 | jiffy 8.32 - 1.29x slower +27.31 ms 324 | jsone 5.08 - 2.12x slower +103.99 ms 325 | Tiny 3.65 - 2.95x slower +181.04 ms 326 | Poison 3.58 - 3.01x slower +186.53 ms 327 | JSX 2.81 - 3.83x slower +263.27 ms 328 | JSON 0.80 - 13.42x slower +1153.61 ms 329 | 330 | ##### With input Issue 90 ##### 331 | Name ips average deviation median 99th % 332 | jiffy 41.16 24.30 ms ±24.21% 21.18 ms 39.99 ms 333 | Jason 17.85 56.03 ms ±18.81% 52.29 ms 115.16 ms 334 | Tiny 14.03 71.30 ms ±7.87% 69.13 ms 91.71 ms 335 | Poison 13.63 73.38 ms ±16.26% 69.75 ms 110.67 ms 336 | jsone 10.79 92.71 ms ±16.86% 87.96 ms 176.42 ms 337 | JSX 6.02 166.15 ms ±4.41% 166.21 ms 188.03 ms 338 | JSON 0.88 1132.61 ms ±11.59% 1162.26 ms 1283.75 ms 339 | 340 | Comparison: 341 | jiffy 41.16 342 | Jason 17.85 - 2.31x slower +31.73 ms 343 | Tiny 14.03 - 2.93x slower +47.00 ms 344 | Poison 13.63 - 3.02x slower +49.08 ms 345 | jsone 10.79 - 3.82x slower +68.41 ms 346 | JSX 6.02 - 6.84x slower +141.85 ms 347 | JSON 0.88 - 46.61x slower +1108.31 ms 348 | 349 | ##### With input JSON Generator ##### 350 | Name ips average deviation median 99th % 351 | Jason 320.79 3.12 ms ±18.37% 3.01 ms 4.84 ms 352 | jiffy 243.93 4.10 ms ±32.94% 4.04 ms 8.01 ms 353 | jsone 184.23 5.43 ms ±12.12% 5.29 ms 7.40 ms 354 | Tiny 139.78 7.15 ms ±23.25% 6.53 ms 12.31 ms 355 | Poison 135.47 7.38 ms ±24.91% 6.74 ms 12.84 ms 356 | JSX 111.24 8.99 ms ±17.87% 8.30 ms 12.68 ms 357 | JSON 25.16 39.75 ms ±14.21% 38.92 ms 70.68 ms 358 | 359 | Comparison: 360 | Jason 320.79 361 | jiffy 243.93 - 1.32x slower +0.98 ms 362 | jsone 184.23 - 1.74x slower +2.31 ms 363 | Tiny 139.78 - 2.29x slower +4.04 ms 364 | Poison 135.47 - 2.37x slower +4.26 ms 365 | JSX 111.24 - 2.88x slower +5.87 ms 366 | JSON 25.16 - 12.75x slower +36.63 ms 367 | 368 | ##### With input JSON Generator (Pretty) ##### 369 | Name ips average deviation median 99th % 370 | Jason 273.57 3.66 ms ±22.33% 3.48 ms 6.78 ms 371 | jiffy 205.09 4.88 ms ±24.77% 4.60 ms 8.14 ms 372 | jsone 158.82 6.30 ms ±19.76% 5.99 ms 11.16 ms 373 | Tiny 136.65 7.32 ms ±20.28% 6.77 ms 10.96 ms 374 | Poison 123.31 8.11 ms ±25.48% 7.26 ms 14.10 ms 375 | JSX 97.93 10.21 ms ±19.97% 9.45 ms 17.34 ms 376 | JSON 25.04 39.94 ms ±7.43% 39.61 ms 49.30 ms 377 | 378 | Comparison: 379 | Jason 273.57 380 | jiffy 205.09 - 1.33x slower +1.22 ms 381 | jsone 158.82 - 1.72x slower +2.64 ms 382 | Tiny 136.65 - 2.00x slower +3.66 ms 383 | Poison 123.31 - 2.22x slower +4.45 ms 384 | JSX 97.93 - 2.79x slower +6.56 ms 385 | JSON 25.04 - 10.93x slower +36.28 ms 386 | 387 | ##### With input Pokedex ##### 388 | Name ips average deviation median 99th % 389 | Jason 527.63 1.90 ms ±14.20% 1.84 ms 2.81 ms 390 | jiffy 285.43 3.50 ms ±28.47% 3.33 ms 5.95 ms 391 | jsone 245.36 4.08 ms ±15.65% 3.97 ms 6.13 ms 392 | Poison 172.45 5.80 ms ±21.78% 5.28 ms 8.66 ms 393 | Tiny 152.59 6.55 ms ±23.87% 5.94 ms 11.96 ms 394 | JSX 140.90 7.10 ms ±17.08% 6.63 ms 10.11 ms 395 | JSON 33.70 29.67 ms ±19.74% 28.52 ms 64.83 ms 396 | 397 | Comparison: 398 | Jason 527.63 399 | jiffy 285.43 - 1.85x slower +1.61 ms 400 | jsone 245.36 - 2.15x slower +2.18 ms 401 | Poison 172.45 - 3.06x slower +3.90 ms 402 | Tiny 152.59 - 3.46x slower +4.66 ms 403 | JSX 140.90 - 3.74x slower +5.20 ms 404 | JSON 33.70 - 15.66x slower +27.78 ms 405 | 406 | ##### With input UTF-8 escaped ##### 407 | Name ips average deviation median 99th % 408 | jiffy 7923.08 0.126 ms ±60.63% 0.106 ms 0.23 ms 409 | Jason 1224.48 0.82 ms ±35.91% 0.78 ms 1.65 ms 410 | Poison 918.21 1.09 ms ±31.37% 0.93 ms 2.34 ms 411 | jsone 573.70 1.74 ms ±29.56% 1.58 ms 3.20 ms 412 | JSX 550.36 1.82 ms ±26.99% 1.66 ms 3.62 ms 413 | Tiny 520.31 1.92 ms ±28.48% 1.75 ms 3.73 ms 414 | JSON 326.43 3.06 ms ±16.08% 3.00 ms 4.47 ms 415 | 416 | Comparison: 417 | jiffy 7923.08 418 | Jason 1224.48 - 6.47x slower +0.69 ms 419 | Poison 918.21 - 8.63x slower +0.96 ms 420 | jsone 573.70 - 13.81x slower +1.62 ms 421 | JSX 550.36 - 14.40x slower +1.69 ms 422 | Tiny 520.31 - 15.23x slower +1.80 ms 423 | JSON 326.43 - 24.27x slower +2.94 ms 424 | 425 | ##### With input UTF-8 unescaped ##### 426 | Name ips average deviation median 99th % 427 | jiffy 12.54 K 79.75 μs ±93.83% 67.80 μs 147.80 μs 428 | Jason 5.56 K 179.73 μs ±26.63% 159.30 μs 369.25 μs 429 | jsone 5.09 K 196.57 μs ±34.98% 167.20 μs 406 μs 430 | Poison 4.39 K 227.69 μs ±40.06% 194.20 μs 432.10 μs 431 | JSX 3.30 K 303.42 μs ±39.01% 284.30 μs 522.46 μs 432 | Tiny 1.46 K 686.25 μs ±30.36% 639.30 μs 1144.77 μs 433 | JSON 1.35 K 739.34 μs ±64.82% 561.25 μs 2226.09 μs 434 | 435 | Comparison: 436 | jiffy 12.54 K 437 | Jason 5.56 K - 2.25x slower +99.98 μs 438 | jsone 5.09 K - 2.46x slower +116.82 μs 439 | Poison 4.39 K - 2.85x slower +147.94 μs 440 | JSX 3.30 K - 3.80x slower +223.67 μs 441 | Tiny 1.46 K - 8.60x slower +606.50 μs 442 | JSON 1.35 K - 9.27x slower +659.59 μs 443 | ``` 444 | -------------------------------------------------------------------------------- /benchmark/poison.patch: -------------------------------------------------------------------------------- 1 | diff --git a/bench/run.exs b/bench/run.exs 2 | index 5e61983..1f54339 100644 3 | --- a/bench/run.exs 4 | +++ b/bench/run.exs 5 | @@ -1,11 +1,11 @@ 6 | encode_jobs = %{ 7 | - "Poison" => &Poison.encode!/1, 8 | - "Jason" => &Jason.encode!/1, 9 | - "JSX" => &JSX.encode!/1, 10 | + "Poison" => &Poison.encode!(&1, escape: :unicode), 11 | + "Jason" => &Jason.encode!(&1, escape: :unicode_safe), 12 | + "JSX" => &JSX.encode!(&1, [:uescape]), 13 | "Tiny" => &Tiny.encode!/1, 14 | "jsone" => &:jsone.encode/1, 15 | - "jiffy" => &:jiffy.encode/1, 16 | - "JSON" => &JSON.encode!/1 17 | + "jiffy" => &:jiffy.encode(&1, [:uescape]), 18 | + "JSON (w/o unicode escape)" => &JSON.encode!/1 19 | } 20 | 21 | encode_inputs = [ 22 | @@ -64,13 +64,7 @@ Benchee.run(encode_jobs, 23 | |> (&{name, &1}).() 24 | end, 25 | formatters: [ 26 | - &Benchee.Formatters.HTML.output/1, 27 | - &Benchee.Formatters.Console.output/1 28 | - ], 29 | - formatter_options: [ 30 | - html: [ 31 | - file: Path.expand("output/encode.html", __DIR__) 32 | - ] 33 | + Benchee.Formatters.Console 34 | ] 35 | ) 36 | 37 | @@ -85,12 +79,6 @@ Benchee.run(decode_jobs, 38 | |> (&{name, &1}).() 39 | end, 40 | formatters: [ 41 | - &Benchee.Formatters.HTML.output/1, 42 | - &Benchee.Formatters.Console.output/1 43 | - ], 44 | - formatter_options: [ 45 | - html: [ 46 | - file: Path.expand("output/decode.html", __DIR__) 47 | - ] 48 | + Benchee.Formatters.Console 49 | ] 50 | ) 51 | diff --git a/mix.exs b/mix.exs 52 | index c07cf0d..8426e22 100644 53 | --- a/mix.exs 54 | +++ b/mix.exs 55 | @@ -54,15 +54,13 @@ defmodule Poison.Mixfile do 56 | {:dialyxir, "~> 0.5", only: :dev, runtime: false}, 57 | {:ex_doc, "~> 0.19", only: :dev, runtime: false}, 58 | {:excoveralls, "~> 0.9", only: :test}, 59 | - {:benchee, "~> 0.13", only: :bench}, 60 | - {:benchee_json, "~> 0.5", only: :bench}, 61 | - {:benchee_html, "~> 0.5", only: :bench}, 62 | + {:benchee, "~> 1.0", only: :bench}, 63 | {:jason, "~> 1.1", only: [:test, :bench]}, 64 | {:exjsx, "~> 4.0", only: :bench}, 65 | {:tiny, "~> 1.0", only: :bench}, 66 | {:jsone, "~> 1.4", only: :bench}, 67 | - {:jiffy, "~> 0.15", only: :bench}, 68 | - {:json, "~> 1.2", only: :bench} 69 | + {:jiffy, "~> 1.0", only: :bench}, 70 | + {:json, "~> 1.4", only: :bench} 71 | ] 72 | end 73 | 74 | diff --git a/mix.lock b/mix.lock 75 | index 1151d4d..ff9b1bd 100644 76 | --- a/mix.lock 77 | +++ b/mix.lock 78 | @@ -1,28 +1,27 @@ 79 | %{ 80 | - "benchee": {:hex, :benchee, "0.13.1", "bd93ca05be78bcb6159c7176230efeda2f724f7ffd485515175ca411dff4893e", [:mix], [{:deep_merge, "~> 0.1", [hex: :deep_merge, repo: "hexpm", optional: false]}], "hexpm"}, 81 | - "benchee_html": {:hex, :benchee_html, "0.5.0", "1354ba8005f979c177561fe08b83cafbae43f3046e8a6cf209132b8967b6db17", [:mix], [{:benchee, "~> 0.12", [hex: :benchee, repo: "hexpm", optional: false]}, {:benchee_json, "~> 0.5", [hex: :benchee_json, repo: "hexpm", optional: false]}], "hexpm"}, 82 | - "benchee_json": {:hex, :benchee_json, "0.5.0", "75878fd9944093ced03e9c214e6e1429da147de43e2f4a4cbc4377294ed5d029", [:mix], [{:benchee, "~> 0.12", [hex: :benchee, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, 83 | - "certifi": {:hex, :certifi, "2.3.1", "d0f424232390bf47d82da8478022301c561cf6445b5b5fb6a84d49a9e76d2639", [:rebar3], [{:parse_trans, "3.2.0", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, 84 | - "deep_merge": {:hex, :deep_merge, "0.1.1", "c27866a7524a337b6a039eeb8dd4f17d458fd40fbbcb8c54661b71a22fffe846", [:mix], [], "hexpm"}, 85 | - "dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], [], "hexpm"}, 86 | - "earmark": {:hex, :earmark, "1.2.5", "4d21980d5d2862a2e13ec3c49ad9ad783ffc7ca5769cf6ff891a4553fbaae761", [:mix], [], "hexpm"}, 87 | - "ex_doc": {:hex, :ex_doc, "0.19.0", "e22b6434373b4870ea77b24df069dbac7002c1f483615e9ebfc0c37497e1c75c", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.7", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, 88 | - "excoveralls": {:hex, :excoveralls, "0.9.1", "14fd20fac51ab98d8e79615814cc9811888d2d7b28e85aa90ff2e30dcf3191d6", [:mix], [{:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, 89 | - "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"}, 90 | - "hackney": {:hex, :hackney, "1.13.0", "24edc8cd2b28e1c652593833862435c80661834f6c9344e84b6a2255e7aeef03", [:rebar3], [{:certifi, "2.3.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.2", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, 91 | - "idna": {:hex, :idna, "5.1.2", "e21cb58a09f0228a9e0b95eaa1217f1bcfc31a1aaa6e1fdf2f53a33f7dbd9494", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, 92 | - "jason": {:hex, :jason, "1.1.1", "d3ccb840dfb06f2f90a6d335b536dd074db748b3e7f5b11ab61d239506585eb2", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, 93 | - "jiffy": {:hex, :jiffy, "0.15.2", "de266c390111fd4ea28b9302f0bc3d7472468f3b8e0aceabfbefa26d08cd73b7", [:rebar3], [], "hexpm"}, 94 | - "json": {:hex, :json, "1.2.5", "3682c18c6b07480df2122d0daf5c05457b42c1990f197ce3de53884e8ba834c4", [:mix], [{:benchee, "~> 0.8", [hex: :benchee, repo: "hexpm", optional: true]}, {:benchee_html, "~> 0.1", [hex: :benchee_html, repo: "hexpm", optional: true]}, {:exjsx, "~> 4.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:jsone, "~> 1.4", [hex: :jsone, repo: "hexpm", optional: true]}, {:poison, "~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:tiny, "~> 1.0", [hex: :tiny, repo: "hexpm", optional: true]}], "hexpm"}, 95 | - "jsone": {:hex, :jsone, "1.4.7", "a970c23d9700ae7842b526c57677e6e3f10894b429524696ead547e9302391c0", [:rebar3], [], "hexpm"}, 96 | - "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm"}, 97 | - "makeup": {:hex, :makeup, "0.5.1", "966c5c2296da272d42f1de178c1d135e432662eca795d6dc12e5e8787514edf7", [:mix], [{:nimble_parsec, "~> 0.2.2", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, 98 | - "makeup_elixir": {:hex, :makeup_elixir, "0.8.0", "1204a2f5b4f181775a0e456154830524cf2207cf4f9112215c05e0b76e4eca8b", [:mix], [{:makeup, "~> 0.5.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 0.2.2", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, 99 | - "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, 100 | - "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, 101 | - "nimble_parsec": {:hex, :nimble_parsec, "0.2.2", "d526b23bdceb04c7ad15b33c57c4526bf5f50aaa70c7c141b4b4624555c68259", [:mix], [], "hexpm"}, 102 | - "parse_trans": {:hex, :parse_trans, "3.2.0", "2adfa4daf80c14dc36f522cf190eb5c4ee3e28008fc6394397c16f62a26258c2", [:rebar3], [], "hexpm"}, 103 | - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"}, 104 | - "tiny": {:hex, :tiny, "1.0.1", "535ea7e600cb1c6ba17b53029266d9d7ec54ce29bfb05d906c433907acfa01ca", [:mix], [], "hexpm"}, 105 | - "unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], [], "hexpm"}, 106 | + "benchee": {:hex, :benchee, "1.0.1", "66b211f9bfd84bd97e6d1beaddf8fc2312aaabe192f776e8931cb0c16f53a521", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}], "hexpm", "3ad58ae787e9c7c94dd7ceda3b587ec2c64604563e049b2a0e8baafae832addb"}, 107 | + "certifi": {:hex, :certifi, "2.6.1", "dbab8e5e155a0763eea978c913ca280a6b544bfa115633fa20249c3d396d9493", [:rebar3], [], "hexpm", "524c97b4991b3849dd5c17a631223896272c6b0af446778ba4675a1dff53bb7e"}, 108 | + "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, 109 | + "dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], [], "hexpm", "6c32a70ed5d452c6650916555b1f96c79af5fc4bf286997f8b15f213de786f73"}, 110 | + "earmark_parser": {:hex, :earmark_parser, "1.4.13", "0c98163e7d04a15feb62000e1a891489feb29f3d10cb57d4f845c405852bbef8", [:mix], [], "hexpm", "d602c26af3a0af43d2f2645613f65841657ad6efc9f0e361c3b6c06b578214ba"}, 111 | + "ex_doc": {:hex, :ex_doc, "0.24.2", "e4c26603830c1a2286dae45f4412a4d1980e1e89dc779fcd0181ed1d5a05c8d9", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "e134e1d9e821b8d9e4244687fb2ace58d479b67b282de5158333b0d57c6fb7da"}, 112 | + "excoveralls": {:hex, :excoveralls, "0.14.1", "14140e4ef343f2af2de33d35268c77bc7983d7824cb945e6c2af54235bc2e61f", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "4a588f9f8cf9dc140cc1f3d0ea4d849b2f76d5d8bee66b73c304bb3d3689c8b0"}, 113 | + "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm", "32e95820a97cffea67830e91514a2ad53b888850442d6d395f53a1ac60c82e07"}, 114 | + "hackney": {:hex, :hackney, "1.17.4", "99da4674592504d3fb0cfef0db84c3ba02b4508bae2dff8c0108baa0d6e0977c", [:rebar3], [{:certifi, "~>2.6.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "de16ff4996556c8548d512f4dbe22dd58a587bf3332e7fd362430a7ef3986b16"}, 115 | + "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, 116 | + "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, 117 | + "jiffy": {:hex, :jiffy, "1.0.8", "60e36f00be35e5ac6e6cf2d4caf3bdf3103d4460aff385f543a8d7df2d6d9613", [:rebar3], [], "hexpm", "f9ae986ba5a0854eb48cf6a76192d9367086da86c20197da430630be7c087a4e"}, 118 | + "json": {:hex, :json, "1.4.1", "8648f04a9439765ad449bc56a3ff7d8b11dd44ff08ffcdefc4329f7c93843dfa", [:mix], [], "hexpm", "9abf218dbe4ea4fcb875e087d5f904ef263d012ee5ed21d46e9dbca63f053d16"}, 119 | + "jsone": {:hex, :jsone, "1.6.0", "4ed7e456cff24ff153c2eac7e1855797ea1b1416ef0bbc368525ed8cbeb57518", [:rebar3], [], "hexpm", "9e5623ac927a278086a3e758537c68f312f6b16a2d0582a0d1c8a58bd4a939db"}, 120 | + "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm", "fc3499fed7a726995aa659143a248534adc754ebd16ccd437cd93b649a95091f"}, 121 | + "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, 122 | + "makeup_elixir": {:hex, :makeup_elixir, "0.15.1", "b5888c880d17d1cc3e598f05cdb5b5a91b7b17ac4eaf5f297cb697663a1094dd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "db68c173234b07ab2a07f645a5acdc117b9f99d69ebf521821d89690ae6c6ec8"}, 123 | + "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, 124 | + "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 125 | + "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, 126 | + "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"}, 127 | + "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, 128 | + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, 129 | + "tiny": {:hex, :tiny, "1.0.1", "535ea7e600cb1c6ba17b53029266d9d7ec54ce29bfb05d906c433907acfa01ca", [:mix], [], "hexpm", "1278a457deb8d99135c378b71f66f21e283d8adea426252a3f8f5e003c536a7b"}, 130 | + "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, 131 | } 132 | -------------------------------------------------------------------------------- /benchmark/run.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | set -eux 4 | 5 | git clone https://github.com/devinus/poison && cd poison 6 | git checkout 4.0.1 7 | patch -p1 < ../poison.patch 8 | mix deps.get 9 | MIX_ENV=bench mix run bench/run.exs 10 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # The jsone application # 4 | 5 | 6 | ## Modules ## 7 | 8 | 9 | 10 |
jsone
11 | 12 | -------------------------------------------------------------------------------- /doc/jsone.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Module jsone # 4 | * [Description](#description) 5 | * [Data Types](#types) 6 | * [Function Index](#index) 7 | * [Function Details](#functions) 8 | 9 | JSON decoding/encoding module. 10 | 11 | 12 | 13 | ## Data Types ## 14 | 15 | 16 | 17 | 18 | ### common_option() ### 19 | 20 | 21 |

 22 | common_option() = undefined_as_null
 23 | 
24 | 25 | `undefined_as_null`:
26 | - Treats `undefined` in Erlang as the conversion target for `null` in JSON. This means that `undefined` will be encoded to `null` and `null` will be decoded to `undefined`
27 | 28 | 29 | 30 | ### datetime_encode_format() ### 31 | 32 | 33 |

 34 | datetime_encode_format() = datetime_format() | {Format::datetime_format(), TimeZone::timezone()}
 35 | 
36 | 37 | Datetime encoding format. 38 | 39 | The default value of `TimeZone` is `utc`. 40 | 41 | ``` 42 | % 43 | % Universal Time 44 | % 45 | > jsone:encode({{2000, 3, 10}, {10, 3, 58}}, [{datetime_format, iso8601}]). 46 | <<"\"2000-03-10T10:03:58Z\"">> 47 | % 48 | % Local Time (JST) 49 | % 50 | > jsone:encode({{2000, 3, 10}, {10, 3, 58}}, [{datetime_format, {iso8601, local}}]). 51 | <<"\"2000-03-10T10:03:58+09:00\"">> 52 | % 53 | % Explicit TimeZone Offset 54 | % 55 | > jsone:encode({{2000, 3, 10}, {10, 3, 58}}, [{datetime_format, {iso8601, -2*60*60}}]). 56 | <<"\"2000-03-10T10:03:58-02:00\"">> 57 | ``` 58 | 59 | 60 | 61 | ### datetime_format() ### 62 | 63 | 64 |

 65 | datetime_format() = iso8601
 66 | 
67 | 68 | 69 | 70 | 71 | ### decode_option() ### 72 | 73 | 74 |

 75 | decode_option() = {object_format, tuple | proplist | map} | {allow_ctrl_chars, boolean()} | reject_invalid_utf8 | {keys, binary | atom | existing_atom | attempt_atom} | {duplicate_map_keys, first | last} | common_option()
 76 | 
77 | 78 | `object_format`:
79 | - Decoded JSON object format
80 | - `tuple`: An object is decoded as `{[]}` if it is empty, otherwise `{[{Key, Value}]}`.
81 | - `proplist`: An object is decoded as `[{}]` if it is empty, otherwise `[{Key, Value}]`.
82 | - `map`: An object is decoded as `#{}` if it is empty, otherwise `#{Key => Value}`.
83 | - default: `map` if OTP version is OTP-17 or more, `tuple` otherwise
84 | 85 | `allow_ctrl_chars`:
86 | - If the value is `true`, strings which contain unescaped control characters will be regarded as a legal JSON string
87 | - default: `false`
88 | 89 | `reject_invalid_utf8`:
90 | - Rejects JSON strings which contain invalid UTF-8 byte sequences
91 | 92 | `keys`:
93 | Defines way how object keys are decoded. The default value is `binary`. 94 | The option is compatible with `labels` option in `jsx`.
95 | - `binary`: The key is left as a string which is encoded as binary. It's default 96 | and backward compatible behaviour.
97 | - `atom`: The key is converted to an atom. Results in `badarg` if Key value 98 | regarded as UTF-8 is not a valid atom.
99 | - `existing_atom`: Returns existing atom. Any key value which is not 100 | existing atom raises `badarg` exception.
101 | - `attempt_atom`: Returns existing atom as `existing_atom` but returns a 102 | binary string if fails find one. 103 | 104 | `duplicate_map_keys`:
105 | https://www.ietf.org/rfc/rfc4627.txt says that keys SHOULD be 106 | unique, but they don't have to be. Most JSON parsers will either 107 | give you the value of the first, or last duplicate property 108 | encountered. When `object_format` is `tuple` or `proplist` all 109 | duplicates are returned. When `object_format` is `map` by default 110 | the first instance of a duplicate is returned. Setting 111 | `duplicate_map_keys` to `last` will change this behaviour to return 112 | the last such instance. 113 | - If the value is `first` then the first duplicate key/value is returned.
114 | - If the value is `last` then the last duplicate key/value is returned. 115 | - default: `first`
116 | 117 | 118 | 119 | ### encode_option() ### 120 | 121 | 122 |

123 | encode_option() = native_utf8 | native_forward_slash | canonical_form | {float_format, [float_format_option()]} | {datetime_format, datetime_encode_format()} | {object_key_type, string | scalar | value} | {space, non_neg_integer()} | {indent, non_neg_integer()} | {map_unknown_value, fun((term()) -> {ok, json_value()} | error)} | common_option()
124 | 
125 | 126 | `native_utf8`:
127 | - Encodes non ASCII UTF-8 characters as a human-readable(non-escaped) string
128 | 129 | `native_forward_slash`:
130 | - Prevents forward slashes in a JSON string from being escaped
131 | 132 | `canonical_form`:
133 | - produce a canonical form of a JSON document
134 | 135 | `{float_format, Options}`: 136 | - Encodes a `float()` value in the format which specified by `Options`
137 | - default: `[{scientific, 20}]`
138 | 139 | `{datetime_format, Format}`: 140 | - Encodes a `calendar:datetime()` value in the format which specified by `Format`
141 | - default: `{iso8601, utc}`
142 | 143 | `object_key_type`: 144 | - Allowable object key type
145 | - `string`: Only string values are allowed (i.e. `json_string()` type)
146 | - `scalar`: In addition to `string`, following values are allowed: nulls, booleans, numerics (i.e. `json_scalar()` type)
147 | - `value`: Any json compatible values are allowed (i.e. `json_value()` type)
148 | - default: `string`
149 | - NOTE: If `scalar` or `value` option is specified, non `json_string()` key will be automatically converted to a `binary()` value (e.g. `1` => `<<"1">>`, `#{}` => `<<"{}">>`)
150 | 151 | `{space, N}`:
152 | - Inserts `N` spaces after every comma and colon
153 | - default: `0`
154 | 155 | `{indent, N}`:
156 | - Inserts a newline and `N` spaces for each level of indentation
157 | - default: `0`
158 | 159 | `{map_unknown_value, Fun}`:
160 | - If specified, unknown values encountered during an encoding process are converted to `json_value()` by applying `Fun`. 161 | 162 | 163 | 164 | ### float_format_option() ### 165 | 166 | 167 |

168 | float_format_option() = {scientific, Decimals::0..249} | {decimals, Decimals::0..253} | compact
169 | 
170 | 171 | `scientific`:
172 | - The float will be formatted using scientific notation with `Decimals` digits of precision.
173 | 174 | `decimals`:
175 | - The encoded string will contain at most `Decimals` number of digits past the decimal point.
176 | - If `compact` is provided the trailing zeros at the end of the string are truncated.
177 | 178 | For more details, see [erlang:float_to_list/2](http://erlang.org/doc/man/erlang.html#float_to_list-2). 179 | 180 | ``` 181 | > jsone:encode(1.23). 182 | <<"1.22999999999999998224e+00">> 183 | > jsone:encode(1.23, [{float_format, [{scientific, 4}]}]). 184 | <"1.2300e+00">> 185 | > jsone:encode(1.23, [{float_format, [{scientific, 1}]}]). 186 | <<"1.2e+00">> 187 | > jsone:encode(1.23, [{float_format, [{decimals, 4}]}]). 188 | <<"1.2300">> 189 | > jsone:encode(1.23, [{float_format, [{decimals, 4}, compact]}]). 190 | <<"1.23">> 191 | ``` 192 | 193 | 194 | 195 | ### json_array() ### 196 | 197 | 198 |

199 | json_array() = [json_value()]
200 | 
201 | 202 | 203 | 204 | 205 | ### json_boolean() ### 206 | 207 | 208 |

209 | json_boolean() = boolean()
210 | 
211 | 212 | 213 | 214 | 215 | ### json_number() ### 216 | 217 | 218 |

219 | json_number() = number()
220 | 
221 | 222 | 223 | 224 | 225 | ### json_object() ### 226 | 227 | 228 |

229 | json_object() = json_object_format_tuple() | json_object_format_proplist() | json_object_format_map()
230 | 
231 | 232 | 233 | 234 | 235 | ### json_object_format_map() ### 236 | 237 | 238 |

239 | json_object_format_map() = #{}
240 | 
241 | 242 | 243 | 244 | 245 | ### json_object_format_proplist() ### 246 | 247 | 248 |

249 | json_object_format_proplist() = [{}] | json_object_members()
250 | 
251 | 252 | 253 | 254 | 255 | ### json_object_format_tuple() ### 256 | 257 | 258 |

259 | json_object_format_tuple() = {json_object_members()}
260 | 
261 | 262 | 263 | 264 | 265 | ### json_object_members() ### 266 | 267 | 268 |

269 | json_object_members() = [{json_string(), json_value()}]
270 | 
271 | 272 | 273 | 274 | 275 | ### json_scalar() ### 276 | 277 | 278 |

279 | json_scalar() = json_boolean() | json_number() | json_string()
280 | 
281 | 282 | 283 | 284 | 285 | ### json_string() ### 286 | 287 | 288 |

289 | json_string() = binary() | atom() | calendar:datetime()
290 | 
291 | 292 | NOTE: `decode/1` always returns `binary()` value 293 | 294 | 295 | 296 | ### json_term() ### 297 | 298 | 299 |

300 | json_term() = {{json, iolist()}} | {{json_utf8, unicode:chardata()}}
301 | 
302 | 303 | `json_term()` allows inline already encoded JSON value. `json` variant 304 | expects byte encoded utf8 data values as list members. `json_utf8` expect 305 | Unicode code points as list members. Binaries are copied "as is" in both 306 | variants except `json_utf8` will check if binary contain valid `UTF-8` 307 | encoded data. In short, `json` uses `erlang:iolist_to_binary/1` and 308 | `json_utf8` uses `unicode:chardata_to_binary/1` for encoding. 309 | 310 | A simple example is worth a thousand words. 311 | 312 | ``` 313 | 1> S = "hélo". 314 | "hélo" 315 | 2> shell:strings(false). 316 | true 317 | 3> S. 318 | [104,233,108,111] 319 | 4> B = jsone:encode({{json, S}}). % invalid UTF-8 320 | <<104,233,108,111>> 321 | 5> B2 = jsone:encode({{json_utf8, S}}). % valid UTF-8 322 | <<104,195,169,108,111>> 323 | 6> jsone:encode({{json, B}}). 324 | <<104,233,108,111>> 325 | 7> jsone:encode({{json_utf8, B}}). 326 | ** exception error: {invalid_json_utf8,<<104>>,<<233,108,111>>} 327 | in function jsone_encode:value/4 328 | called as jsone_encode:value({json_utf8,<<104,233,108,111>>}, 329 | [],<<>>, 330 | {encode_opt_v2,false, 331 | [{scientific,20}], 332 | {iso8601,0}, 333 | string,0,0}) 334 | in call from jsone:encode/2 (/home/hynek/work/altworx/jsone/_build/default/lib/jsone/src/jsone.erl, line 302) 335 | 8> jsone:encode({{json_utf8, B2}}). 336 | <<104,195,169,108,111>> 337 | 9> shell:strings(true). 338 | false 339 | 10> jsone:encode({{json_utf8, B2}}). 340 | <<"hélo"/utf8>> 341 | 11> jsone:encode({{json, binary_to_list(B2)}}). % UTF-8 encoded list leads to valid UTF-8 342 | <<"hélo"/utf8>> 343 | ``` 344 | 345 | 346 | 347 | 348 | ### json_value() ### 349 | 350 | 351 |

352 | json_value() = json_number() | json_string() | json_array() | json_object() | json_boolean() | null | undefined | json_term()
353 | 
354 | 355 | 356 | 357 | 358 | ### stack_item() ### 359 | 360 | 361 |

362 | stack_item() = {Module::module(), Function::atom(), Arity::arity() | (Args::[term()]), Location::[{file, Filename::string()} | {line, Line::pos_integer()}]}
363 | 
364 | 365 | An item in a stack back-trace. 366 | 367 | Note that the `erlang` module already defines the same `stack_item/0` type, 368 | but it is not exported from the module. 369 | So, maybe as a temporary measure, we redefine this type for passing full dialyzer analysis. 370 | 371 | 372 | 373 | ### timezone() ### 374 | 375 | 376 |

377 | timezone() = utc | local | utc_offset_seconds()
378 | 
379 | 380 | 381 | 382 | 383 | ### utc_offset_seconds() ### 384 | 385 | 386 |

387 | utc_offset_seconds() = -86399..86399
388 | 
389 | 390 | 391 | 392 | ## Function Index ## 393 | 394 | 395 |
decode/1Equivalent to decode(Json, []).
decode/2Decodes an erlang term from json text (a utf8 encoded binary).
encode/1Equivalent to encode(JsonValue, []).
encode/2Encodes an erlang term into json text (a utf8 encoded binary).
try_decode/1Equivalent to try_decode(Json, []).
try_decode/2Decodes an erlang term from json text (a utf8 encoded binary).
try_encode/1Equivalent to try_encode(JsonValue, []).
try_encode/2Encodes an erlang term into json text (a utf8 encoded binary).
396 | 397 | 398 | 399 | 400 | ## Function Details ## 401 | 402 | 403 | 404 | ### decode/1 ### 405 | 406 |

407 | decode(Json::binary()) -> json_value()
408 | 
409 |
410 | 411 | Equivalent to [`decode(Json, [])`](#decode-2). 412 | 413 | 414 | 415 | ### decode/2 ### 416 | 417 |

418 | decode(Json::binary(), Options::[decode_option()]) -> json_value()
419 | 
420 |
421 | 422 | Decodes an erlang term from json text (a utf8 encoded binary) 423 | 424 | Raises an error exception if input is not valid json 425 | 426 | ``` 427 | > jsone:decode(<<"1">>, []). 428 | 1 429 | > jsone:decode(<<"wrong json">>, []). 430 | ** exception error: bad argument 431 | in function jsone_decode:number_integer_part/4 432 | called as jsone_decode:number_integer_part(<<"wrong json">>,1,[],<<>>) 433 | in call from jsone:decode/1 (src/jsone.erl, line 71) 434 | ``` 435 | 436 | 437 | 438 | ### encode/1 ### 439 | 440 |

441 | encode(JsonValue::json_value()) -> binary()
442 | 
443 |
444 | 445 | Equivalent to [`encode(JsonValue, [])`](#encode-2). 446 | 447 | 448 | 449 | ### encode/2 ### 450 | 451 |

452 | encode(JsonValue::json_value(), Options::[encode_option()]) -> binary()
453 | 
454 |
455 | 456 | Encodes an erlang term into json text (a utf8 encoded binary) 457 | 458 | Raises an error exception if input is not an instance of type `json_value()` 459 | 460 | ``` 461 | > jsone:encode([1, null, 2]). 462 | <<"[1,null,2]">> 463 | > jsone:encode([1, self(), 2]). % A pid is not a json value 464 | ** exception error: bad argument 465 | in function jsone_encode:value/3 466 | called as jsone_encode:value(<0,34,0>,[{array_values,[2]}],<<"[1,">>) 467 | in call from jsone:encode/1 (src/jsone.erl, line 97) 468 | ``` 469 | 470 | 471 | 472 | ### try_decode/1 ### 473 | 474 |

475 | try_decode(Json::binary()) -> {ok, json_value(), Remainings::binary()} | {error, {Reason::term(), [stack_item()]}}
476 | 
477 |
478 | 479 | Equivalent to [`try_decode(Json, [])`](#try_decode-2). 480 | 481 | 482 | 483 | ### try_decode/2 ### 484 | 485 |

486 | try_decode(Json::binary(), Options::[decode_option()]) -> {ok, json_value(), Remainings::binary()} | {error, {Reason::term(), [stack_item()]}}
487 | 
488 |
489 | 490 | Decodes an erlang term from json text (a utf8 encoded binary) 491 | 492 | ``` 493 | > jsone:try_decode(<<"[1,2,3] \"next value\"">>, []). 494 | {ok,[1,2,3],<<" \"next value\"">>} 495 | > jsone:try_decode(<<"wrong json">>, []). 496 | {error,{badarg,[{jsone_decode,number_integer_part, 497 | [<<"wrong json">>,1,[],<<>>], 498 | [{line,208}]}]}} 499 | ``` 500 | 501 | 502 | 503 | ### try_encode/1 ### 504 | 505 |

506 | try_encode(JsonValue::json_value()) -> {ok, binary()} | {error, {Reason::term(), [stack_item()]}}
507 | 
508 |
509 | 510 | Equivalent to [`try_encode(JsonValue, [])`](#try_encode-2). 511 | 512 | 513 | 514 | ### try_encode/2 ### 515 | 516 |

517 | try_encode(JsonValue::json_value(), Options::[encode_option()]) -> {ok, binary()} | {error, {Reason::term(), [stack_item()]}}
518 | 
519 |
520 | 521 | Encodes an erlang term into json text (a utf8 encoded binary) 522 | 523 | ``` 524 | > jsone:try_encode([1, null, 2]). 525 | {ok,<<"[1,null,2]">>} 526 | > jsone:try_encode([1, hoge, 2]). % 'hoge' atom is not a json value 527 | {error,{badarg,[{jsone_encode,value, 528 | [hoge,[{array_values,[2]}],<<"[1,">>], 529 | [{line,86}]}]}} 530 | ``` 531 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | %% -*- erlang -*- 2 | {erl_opts, [warnings_as_errors, 3 | warn_export_all, 4 | warn_untyped_record, 5 | inline, 6 | {platform_define, "^R[01][0-9]", 'NO_MAP_TYPE'}, 7 | {platform_define, "^(R|17)", 'NO_DIALYZER_SPEC'}]}. 8 | 9 | {xref_checks, [fail_on_warning, undefined_function_calls]}. 10 | 11 | {clean_files, [".eunit/*", "ebin/*.beam"]}. 12 | 13 | {cover_enabled, true}. 14 | 15 | {edoc_opts, [{dialyzer_specs, all}, 16 | {report_missing_type, true}, 17 | {report_type_mismatch, true}, 18 | {pretty_print, erl_pp}, 19 | {preprocess, true}]}. 20 | {validate_app_modules, true}. 21 | 22 | {shell, [{apps, [jsone]}]}. 23 | 24 | {dialyzer, [{warnings, [error_handling, unmatched_returns, unknown, no_improper_lists]}]}. 25 | 26 | {profiles, [{native, [{erl_opts, [{d, 'ENABLE_HIPE'}]}]}, 27 | {test, [{erl_opts, [warnings_as_errors, 28 | warn_export_all, 29 | warn_untyped_record, 30 | inline, 31 | {platform_define, "^R[01][0-9]", 'NO_MAP_TYPE'}, 32 | {platform_define, "^(R|17)", 'NO_DIALYZER_SPEC'}, 33 | {d, 'MAP_ITER_ORDERED'}, 34 | {d, 'TIME_MODULE', test_time_module}]}]}, 35 | {edown, [{edoc_opts, [{doclet, edown_doclet}]}, {deps, [edown]}]}]}. 36 | 37 | {project_plugins, [covertool, rebar3_efmt]}. 38 | {cover_export_enabled, true}. 39 | {covertool, [{coverdata_files, ["ct.coverdata", "eunit.coverdata"]}]}. 40 | -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | []. 2 | -------------------------------------------------------------------------------- /src/jsone.app.src: -------------------------------------------------------------------------------- 1 | {application, jsone, 2 | [{description, "Erlang JSON Library"}, 3 | {vsn, "1.9.0"}, 4 | {registered, []}, 5 | {applications, [kernel, stdlib]}, 6 | {licenses, ["MIT"]}, 7 | {links, [{"GitHub", "https://github.com/sile/jsone"}]}, 8 | {env, []}]}. 9 | -------------------------------------------------------------------------------- /src/jsone.erl: -------------------------------------------------------------------------------- 1 | %%% @doc JSON decoding/encoding module 2 | %%% @end 3 | %%% 4 | %%% Copyright (c) 2013-2015, Takeru Ohta 5 | %%% 6 | %%% The MIT License 7 | %%% 8 | %%% Permission is hereby granted, free of charge, to any person obtaining a copy 9 | %%% of this software and associated documentation files (the "Software"), to deal 10 | %%% in the Software without restriction, including without limitation the rights 11 | %%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | %%% copies of the Software, and to permit persons to whom the Software is 13 | %%% furnished to do so, subject to the following conditions: 14 | %%% 15 | %%% The above copyright notice and this permission notice shall be included in 16 | %%% all copies or substantial portions of the Software. 17 | %%% 18 | %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | %%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | %%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | %%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | %%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | %%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | %%% THE SOFTWARE. 25 | %%% 26 | %%%--------------------------------------------------------------------------------------- 27 | -module(jsone). 28 | 29 | %%-------------------------------------------------------------------------------- 30 | %% Exported API 31 | %%-------------------------------------------------------------------------------- 32 | -export([decode/1, decode/2, 33 | try_decode/1, try_decode/2, 34 | encode/1, encode/2, 35 | try_encode/1, try_encode/2, 36 | term_to_json_string/1, 37 | ip_address_to_json_string/1]). 38 | 39 | -export_type([json_value/0, 40 | json_boolean/0, 41 | json_number/0, 42 | json_string/0, 43 | json_array/0, 44 | json_object/0, 45 | json_object_members/0, 46 | json_term/0, 47 | json_object_format_tuple/0, 48 | json_object_format_proplist/0, 49 | json_object_format_map/0, 50 | json_scalar/0, 51 | 52 | encode_option/0, 53 | decode_option/0, 54 | float_format_option/0, 55 | datetime_encode_format/0, 56 | datetime_format/0, 57 | timezone/0, 58 | utc_offset_seconds/0, 59 | stack_item/0]). 60 | 61 | %%-------------------------------------------------------------------------------- 62 | %% Types & Macros 63 | %%-------------------------------------------------------------------------------- 64 | -type json_value() :: json_number() | 65 | json_string() | 66 | json_array() | 67 | json_object() | 68 | json_boolean() | 69 | null | 70 | undefined | 71 | json_term(). 72 | -type json_boolean() :: boolean(). 73 | -type json_number() :: number(). 74 | -type json_string() :: binary() | atom() | calendar:datetime(). % NOTE: `decode/1' always returns `binary()' value 75 | -type json_array() :: [json_value()]. 76 | -type json_object() :: json_object_format_tuple() | json_object_format_proplist() | json_object_format_map(). 77 | -type json_object_members() :: [{json_string(), json_value()}]. 78 | -type json_term() :: {{json, iolist()}} | {{json_utf8, unicode:chardata()}}. 79 | %% `json_term()' allows inline already encoded JSON value. `json' variant 80 | %% expects byte encoded utf8 data values as list members. `json_utf8' expect 81 | %% Unicode code points as list members. Binaries are copied "as is" in both 82 | %% variants except `json_utf8' will check if binary contain valid `UTF-8' 83 | %% encoded data. In short, `json' uses `erlang:iolist_to_binary/1' and 84 | %% `json_utf8' uses `unicode:chardata_to_binary/1' for encoding. 85 | %% 86 | %% A simple example is worth a thousand words. 87 | %% 88 | %% ``` 89 | %% 1> S = "hélo". 90 | %% "hélo" 91 | %% 2> shell:strings(false). 92 | %% true 93 | %% 3> S. 94 | %% [104,233,108,111] 95 | %% 4> B = jsone:encode({{json, S}}). % invalid UTF-8 96 | %% <<104,233,108,111>> 97 | %% 5> B2 = jsone:encode({{json_utf8, S}}). % valid UTF-8 98 | %% <<104,195,169,108,111>> 99 | %% 6> jsone:encode({{json, B}}). 100 | %% <<104,233,108,111>> 101 | %% 7> jsone:encode({{json_utf8, B}}). 102 | %% ** exception error: {invalid_json_utf8,<<104>>,<<233,108,111>>} 103 | %% in function jsone_encode:value/4 104 | %% called as jsone_encode:value({json_utf8,<<104,233,108,111>>}, 105 | %% [],<<>>, 106 | %% {encode_opt_v2,false, 107 | %% [{scientific,20}], 108 | %% {iso8601,0}, 109 | %% string,0,0}) 110 | %% in call from jsone:encode/2 (/home/hynek/work/altworx/jsone/_build/default/lib/jsone/src/jsone.erl, line 302) 111 | %% 8> jsone:encode({{json_utf8, B2}}). 112 | %% <<104,195,169,108,111>> 113 | %% 9> shell:strings(true). 114 | %% false 115 | %% 10> jsone:encode({{json_utf8, B2}}). 116 | %% <<"hélo"/utf8>> 117 | %% 11> jsone:encode({{json, binary_to_list(B2)}}). % UTF-8 encoded list leads to valid UTF-8 118 | %% <<"hélo"/utf8>> 119 | %% ''' 120 | %% 121 | -type json_object_format_tuple() :: {json_object_members()}. 122 | -type json_object_format_proplist() :: [{}] | json_object_members(). 123 | 124 | -ifdef('NO_MAP_TYPE'). 125 | -opaque json_object_format_map() :: json_object_format_proplist(). 126 | %% `maps' is not supported in this erts version 127 | -else. 128 | -type json_object_format_map() :: map(). 129 | -endif. 130 | 131 | -type json_scalar() :: json_boolean() | json_number() | json_string(). 132 | 133 | -type float_format_option() :: {scientific, Decimals :: 0..249} | {decimals, Decimals :: 0..253} | compact | short. 134 | %% `scientific':
135 | %% - The float will be formatted using scientific notation with `Decimals' digits of precision.
136 | %% 137 | %% `decimals':
138 | %% - The encoded string will contain at most `Decimals' number of digits past the decimal point.
139 | %% - If `compact' is provided the trailing zeros at the end of the string are truncated.
140 | %% - If `short' is provided the float is formatted with the smallest number of digits.
141 | %% 142 | %% For more details, see erlang:float_to_list/2. 143 | %% 144 | %% ``` 145 | %% > jsone:encode(1.23). 146 | %% <<"1.22999999999999998224e+00">> 147 | %% 148 | %% > jsone:encode(1.23, [{float_format, [{scientific, 4}]}]). 149 | %% <"1.2300e+00">> 150 | %% 151 | %% > jsone:encode(1.23, [{float_format, [{scientific, 1}]}]). 152 | %% <<"1.2e+00">> 153 | %% 154 | %% > jsone:encode(1.23, [{float_format, [{decimals, 4}]}]). 155 | %% <<"1.2300">> 156 | %% 157 | %% > jsone:encode(1.23, [{float_format, [{decimals, 4}, compact]}]). 158 | %% <<"1.23">> 159 | %% ''' 160 | 161 | -type datetime_encode_format() :: Format :: datetime_format() | {Format :: datetime_format(), TimeZone :: timezone()}. 162 | %% Datetime encoding format. 163 | %% 164 | %% The default value of `TimeZone' is `utc'. 165 | %% 166 | %% ``` 167 | %% % 168 | %% % Universal Time 169 | %% % 170 | %% > jsone:encode({{2000, 3, 10}, {10, 3, 58}}, [{datetime_format, iso8601}]). 171 | %% <<"\"2000-03-10T10:03:58Z\"">> 172 | %% 173 | %% % 174 | %% % Local Time (JST) 175 | %% % 176 | %% > jsone:encode({{2000, 3, 10}, {10, 3, 58}}, [{datetime_format, {iso8601, local}}]). 177 | %% <<"\"2000-03-10T10:03:58+09:00\"">> 178 | %% 179 | %% % Also you can use {iso8601, local_dst} to properly calculate the timezone according to the daylight saving procedure. Consider using it, if the executing computer is located in a country that implements this procedure 180 | %% 181 | %% % 182 | %% % Explicit TimeZone Offset 183 | %% % 184 | %% > jsone:encode({{2000, 3, 10}, {10, 3, 58}}, [{datetime_format, {iso8601, -2*60*60}}]). 185 | %% <<"\"2000-03-10T10:03:58-02:00\"">> 186 | %% ''' 187 | 188 | -type datetime_format() :: iso8601. 189 | -type timezone() :: utc | local | local_dst | utc_offset_seconds(). 190 | -type utc_offset_seconds() :: -86399..86399. 191 | 192 | -type common_option() :: undefined_as_null. 193 | %% 194 | %% `undefined_as_null':
195 | %% - Treats `undefined' in Erlang as the conversion target for `null' in JSON. This means that `undefined' will be encoded to `null' and `null' will be decoded to `undefined'
196 | 197 | -type encode_option() :: native_utf8 | 198 | native_forward_slash | 199 | canonical_form | 200 | {float_format, [float_format_option()]} | 201 | {datetime_format, datetime_encode_format()} | 202 | {object_key_type, string | scalar | value} | 203 | {space, non_neg_integer()} | 204 | {indent, non_neg_integer()} | 205 | {map_unknown_value, undefined | fun((term()) -> {ok, json_value()} | error)} | 206 | skip_undefined | 207 | common_option(). 208 | %% `native_utf8':
209 | %% - Encodes non ASCII UTF-8 characters as a human-readable(non-escaped) string
210 | %% 211 | %% `native_forward_slash':
212 | %% - Prevents forward slashes in a JSON string from being escaped
213 | %% 214 | %% `canonical_form':
215 | %% - produce a canonical form of a JSON document
216 | %% 217 | %% `{float_format, Options}': 218 | %% - Encodes a `float()` value in the format which specified by `Options'
219 | %% - default: `[{scientific, 20}]'
220 | %% 221 | %% `{datetime_format, Format}`: 222 | %% - Encodes a `calendar:datetime()` value in the format which specified by `Format'
223 | %% - default: `{iso8601, utc}'
224 | %% 225 | %% `object_key_type': 226 | %% - Allowable object key type
227 | %% - `string': Only string values are allowed (i.e. `json_string()' type)
228 | %% - `scalar': In addition to `string', following values are allowed: nulls, booleans, numerics (i.e. `json_scalar()' type)
229 | %% - `value': Any json compatible values are allowed (i.e. `json_value()' type)
230 | %% - default: `string'
231 | %% - NOTE: If `scalar' or `value' option is specified, non `json_string()' key will be automatically converted to a `binary()' value (e.g. `1' => `<<"1">>', `#{}' => `<<"{}">>')
232 | %% 233 | %% `{space, N}':
234 | %% - Inserts `N' spaces after every comma and colon
235 | %% - default: `0'
236 | %% 237 | %% `{indent, N}':
238 | %% - Inserts a newline and `N' spaces for each level of indentation
239 | %% - default: `0'
240 | %% 241 | %% `skip_undefined':
242 | %% - If specified, each entry having `undefined' value in a object isn't included in the result JSON
243 | %% 244 | %% `{map_unknown_value, Fun}`:
245 | %% - If `Fun' is a function, unknown values encountered during an encoding process are converted to `json_value()` by applying `Fun'.
246 | %% - If `Fun' is `undefined', the encoding results in an error if there are unknown values.
247 | %% - default: `term_to_json_string/1'
248 | 249 | -type decode_option() :: {object_format, tuple | proplist | map} | 250 | {allow_ctrl_chars, boolean()} | 251 | reject_invalid_utf8 | 252 | {'keys', 'binary' | 'atom' | 'existing_atom' | 'attempt_atom'} | 253 | {duplicate_map_keys, first | last} | 254 | common_option(). 255 | %% `object_format':
256 | %% - Decoded JSON object format
257 | %% - `tuple': An object is decoded as `{[]}' if it is empty, otherwise `{[{Key, Value}]}'.
258 | %% - `proplist': An object is decoded as `[{}]' if it is empty, otherwise `[{Key, Value}]'.
259 | %% - `map': An object is decoded as `#{}' if it is empty, otherwise `#{Key => Value}'.
260 | %% - default: `map' if OTP version is OTP-17 or more, `tuple' otherwise
261 | %% 262 | %% `allow_ctrl_chars':
263 | %% - If the value is `true', strings which contain unescaped control characters will be regarded as a legal JSON string
264 | %% - default: `false'
265 | %% 266 | %% `reject_invalid_utf8':
267 | %% - Rejects JSON strings which contain invalid UTF-8 byte sequences
268 | %% 269 | %% `keys':
270 | %% Defines way how object keys are decoded. The default value is `binary'. 271 | %% The option is compatible with `labels' option in `jsx'.
272 | %% - `binary': The key is left as a string which is encoded as binary. It's default 273 | %% and backward compatible behaviour.
274 | %% - `atom': The key is converted to an atom. Results in `badarg' if Key value 275 | %% regarded as UTF-8 is not a valid atom.
276 | %% - `existing_atom': Returns existing atom. Any key value which is not 277 | %% existing atom raises `badarg' exception.
278 | %% - `attempt_atom': Returns existing atom as `existing_atom' but returns a 279 | %% binary string if fails find one. 280 | %% 281 | %% `duplicate_map_keys':
282 | %% https://www.ietf.org/rfc/rfc4627.txt says that keys SHOULD be 283 | %% unique, but they don't have to be. Most JSON parsers will either 284 | %% give you the value of the first, or last duplicate property 285 | %% encountered. When `object_format' is `tuple' or `proplist' all 286 | %% duplicates are returned. When `object_format' is `map' by default 287 | %% the first instance of a duplicate is returned. Setting 288 | %% `duplicate_map_keys' to `last' will change this behaviour to return 289 | %% the last such instance. 290 | %% - If the value is `first' then the first duplicate key/value is returned.
291 | %% - If the value is `last' then the last duplicate key/value is returned. 292 | %% - default: `first'
293 | %% 294 | 295 | -type stack_item() :: {Module :: module(), 296 | Function :: atom(), 297 | Arity :: arity() | (Args :: [term()]), 298 | Location :: [{file, Filename :: string()} | {line, Line :: pos_integer()}]}. 299 | %% An item in a stack back-trace. 300 | %% 301 | %% Note that the `erlang' module already defines the same `stack_item/0' type, 302 | %% but it is not exported from the module. 303 | %% So, maybe as a temporary measure, we redefine this type for passing full dialyzer analysis. 304 | 305 | -ifdef('OTP_RELEASE'). 306 | %% The 'OTP_RELEASE' macro introduced at OTP-21, 307 | %% so we can use it for detecting whether the Erlang compiler supports new try/catch syntax or not. 308 | -define(CAPTURE_STACKTRACE, :__StackTrace). 309 | -define(GET_STACKTRACE, __StackTrace). 310 | -else. 311 | -define(CAPTURE_STACKTRACE, ). 312 | -define(GET_STACKTRACE, erlang:get_stacktrace()). 313 | -endif. 314 | 315 | 316 | %%-------------------------------------------------------------------------------- 317 | %% Exported Functions 318 | %%-------------------------------------------------------------------------------- 319 | %% @equiv decode(Json, []) 320 | -spec decode(binary()) -> json_value(). 321 | decode(Json) -> 322 | decode(Json, []). 323 | 324 | 325 | %% @doc Decodes an erlang term from json text (a utf8 encoded binary) 326 | %% 327 | %% Raises an error exception if input is not valid json 328 | %% 329 | %% ``` 330 | %% > jsone:decode(<<"1">>, []). 331 | %% 1 332 | %% 333 | %% > jsone:decode(<<"wrong json">>, []). 334 | %% ** exception error: bad argument 335 | %% in function jsone_decode:number_integer_part/4 336 | %% called as jsone_decode:number_integer_part(<<"wrong json">>,1,[],<<>>) 337 | %% in call from jsone:decode/1 (src/jsone.erl, line 71) 338 | %% ''' 339 | -spec decode(binary(), [decode_option()]) -> json_value(). 340 | decode(Json, Options) -> 341 | try 342 | {ok, Value, Remainings} = try_decode(Json, Options), 343 | check_decode_remainings(Remainings), 344 | Value 345 | catch 346 | error:{badmatch, {error, {Reason, [StackItem]}}} ?CAPTURE_STACKTRACE-> 347 | erlang:raise(error, Reason, [StackItem | ?GET_STACKTRACE]) 348 | end. 349 | 350 | 351 | %% @equiv try_decode(Json, []) 352 | -spec try_decode(binary()) -> {ok, json_value(), Remainings :: binary()} | {error, {Reason :: term(), [stack_item()]}}. 353 | try_decode(Json) -> 354 | try_decode(Json, []). 355 | 356 | 357 | %% @doc Decodes an erlang term from json text (a utf8 encoded binary) 358 | %% 359 | %% ``` 360 | %% > jsone:try_decode(<<"[1,2,3] \"next value\"">>, []). 361 | %% {ok,[1,2,3],<<" \"next value\"">>} 362 | %% 363 | %% > jsone:try_decode(<<"wrong json">>, []). 364 | %% {error,{badarg,[{jsone_decode,number_integer_part, 365 | %% [<<"wrong json">>,1,[],<<>>], 366 | %% [{line,208}]}]}} 367 | %% ''' 368 | -spec try_decode(binary(), [decode_option()]) -> 369 | {ok, json_value(), Remainings :: binary()} | {error, {Reason :: term(), [stack_item()]}}. 370 | try_decode(Json, Options) -> 371 | jsone_decode:decode(Json, Options). 372 | 373 | 374 | %% @equiv encode(JsonValue, []) 375 | -spec encode(json_value()) -> binary(). 376 | encode(JsonValue) -> 377 | encode(JsonValue, []). 378 | 379 | 380 | %% @doc Encodes an erlang term into json text (a utf8 encoded binary) 381 | %% 382 | %% Raises an error exception if input is not a valid `json_value()' and the 383 | %% `Options` includes `[{map_unknown_value, undefined}]`. 384 | %% 385 | %% Note: Terms that are not `json_value()' will be handled by the function 386 | %% specified in the `map_unknown_value` option. By default, this function 387 | %% is `term_to_json_string/1'. 388 | %% 389 | %% ``` 390 | %% > jsone:encode([1, null, 2]). 391 | %% <<"[1,null,2]">> 392 | %% 393 | %% > jsone:encode([1, self(), 2], [{map_unknown_value, undefined}]). % PID is not a json value 394 | %% ** exception error: bad argument 395 | %% in function jsone_encode:value/3 396 | %% called as jsone_encode:value(<0,34,0>,[{array_values,[2]}],<<"[1,">>) 397 | %% in call from jsone:encode/1 (src/jsone.erl, line 97) 398 | %% ''' 399 | -spec encode(json_value() | term(), [encode_option()]) -> binary(). 400 | encode(JsonValue, Options) -> 401 | try 402 | {ok, Binary} = try_encode(JsonValue, Options), 403 | Binary 404 | catch 405 | error:{badmatch, {error, {Reason, [StackItem]}}} ?CAPTURE_STACKTRACE-> 406 | erlang:raise(error, Reason, [StackItem | ?GET_STACKTRACE]) 407 | end. 408 | 409 | 410 | %% @equiv try_encode(JsonValue, []) 411 | -spec try_encode(json_value() | term()) -> {ok, binary()} | {error, {Reason :: term(), [stack_item()]}}. 412 | try_encode(JsonValue) -> 413 | try_encode(JsonValue, []). 414 | 415 | 416 | %% @doc Encodes an erlang term into json text (a utf8 encoded binary) 417 | %% 418 | %% ``` 419 | %% > jsone:try_encode([1, null, 2]). 420 | %% {ok,<<"[1,null,2]">>} 421 | %% 422 | %% > jsone:try_encode([1, self(), 2], [{map_unknown_value, undefined}]). % PID is not a json value 423 | %% {error,{badarg,[{jsone_encode,value, 424 | %% [hoge,[{array_values,[2]}],<<"[1,">>], 425 | %% [{line,86}]}]}} 426 | %% ''' 427 | -spec try_encode(json_value() | term(), [encode_option()]) -> {ok, binary()} | {error, {Reason :: term(), [stack_item()]}}. 428 | try_encode(JsonValue, Options) -> 429 | jsone_encode:encode(JsonValue, Options). 430 | 431 | 432 | %% @doc Converts the given term `X' to its string representation (i.e., the result of `io_lib:format("~p", [X])'). 433 | -spec term_to_json_string(term()) -> {ok, json_string()} | error. 434 | term_to_json_string(X) -> 435 | {ok, list_to_binary(io_lib:format("~0p", [X]))}. 436 | 437 | 438 | %% @doc Convert an IP address into a text representation. 439 | %% 440 | %% This function can be specified as the value of the `map_unknown_value' encoding option. 441 | %% 442 | %% This function formats IPv6 addresses by following the recommendation defined in RFC 5952. 443 | %% Note that the trailing 32 bytes of special IPv6 addresses such as IPv4-Compatible (::X.X.X.X), 444 | %% IPv4-Mapped (::ffff:X.X.X.X), IPv4-Translated (::ffff:0:X.X.X.X) and IPv4/IPv6 translation 445 | %% (64:ff9b::X.X.X.X and 64:ff9b:1::X.X.X.X ~ 64:ff9b:1:ffff:ffff:ffff:X.X.X.X) are formatted 446 | %% using the IPv4 format. 447 | %% 448 | %% ``` 449 | %% > EncodeOpt = [{map_unknown_value, fun jsone:ip_address_to_json_string/1}]. 450 | %% 451 | %% > jsone:encode(#{ip => {127, 0, 0, 1}}, EncodeOpt). 452 | %% <<"{\"ip\":\"127.0.0.1\"}">> 453 | %% 454 | %% > {ok, Addr} = inet:parse_address("2001:DB8:0000:0000:0001:0000:0000:0001"). 455 | %% > jsone:encode(Addr, EncodeOpt). 456 | %% <<"\"2001:db8::1:0:0:1\"">> 457 | %% 458 | %% > jsone:encode([foo, {0, 0, 0, 0, 0, 16#FFFF, 16#7F00, 16#0001}], EncodeOpt). 459 | %% <<"[\"foo\",\"::ffff:127.0.0.1\"]">> 460 | %% ''' 461 | -spec ip_address_to_json_string(inet:ip_address() | any()) -> {ok, json_string()} | error. 462 | ip_address_to_json_string(X) -> 463 | jsone_inet:ip_address_to_json_string(X). 464 | 465 | 466 | %%-------------------------------------------------------------------------------- 467 | %% Internal Functions 468 | %%-------------------------------------------------------------------------------- 469 | -spec check_decode_remainings(binary()) -> ok. 470 | check_decode_remainings(<<>>) -> 471 | ok; 472 | check_decode_remainings(<<$ , Bin/binary>>) -> 473 | check_decode_remainings(Bin); 474 | check_decode_remainings(<<$\t, Bin/binary>>) -> 475 | check_decode_remainings(Bin); 476 | check_decode_remainings(<<$\r, Bin/binary>>) -> 477 | check_decode_remainings(Bin); 478 | check_decode_remainings(<<$\n, Bin/binary>>) -> 479 | check_decode_remainings(Bin); 480 | check_decode_remainings(<>) -> 481 | erlang:error(badarg, [Bin]). 482 | -------------------------------------------------------------------------------- /src/jsone_decode.erl: -------------------------------------------------------------------------------- 1 | %%% @doc JSON decoding module 2 | %%% @private 3 | %%% @end 4 | %%% 5 | %%% Copyright (c) 2013-2016, Takeru Ohta 6 | %%% 7 | %%% The MIT License 8 | %%% 9 | %%% Permission is hereby granted, free of charge, to any person obtaining a copy 10 | %%% of this software and associated documentation files (the "Software"), to deal 11 | %%% in the Software without restriction, including without limitation the rights 12 | %%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | %%% copies of the Software, and to permit persons to whom the Software is 14 | %%% furnished to do so, subject to the following conditions: 15 | %%% 16 | %%% The above copyright notice and this permission notice shall be included in 17 | %%% all copies or substantial portions of the Software. 18 | %%% 19 | %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | %%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | %%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | %%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | %%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | %%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | %%% THE SOFTWARE. 26 | %%% 27 | %%%--------------------------------------------------------------------------------------- 28 | -module(jsone_decode). 29 | 30 | -ifdef(ENABLE_HIPE). 31 | -compile([native, {hipe, [o3]}]). 32 | -endif. 33 | 34 | %%-------------------------------------------------------------------------------- 35 | %% Exported API 36 | %%-------------------------------------------------------------------------------- 37 | -export([decode/1, decode/2]). 38 | 39 | %%-------------------------------------------------------------------------------- 40 | %% Macros & Records & Types 41 | %%-------------------------------------------------------------------------------- 42 | -define(ERROR(Function, Args), {error, {badarg, [{?MODULE, Function, Args, [{line, ?LINE}]}]}}). 43 | 44 | -ifdef('NO_MAP_TYPE'). 45 | -define(DEFAULT_OBJECT_FORMAT, tuple). 46 | -define(LIST_TO_MAP(X), error({this_erts_does_not_support_maps, X})). 47 | -else. 48 | -define(DEFAULT_OBJECT_FORMAT, map). 49 | -define(LIST_TO_MAP(X), maps:from_list(X)). 50 | -endif. 51 | 52 | -type next() :: {array_next, [jsone:json_value()]} | 53 | {object_value, jsone:json_object_members()} | 54 | {object_next, jsone:json_string(), jsone:json_object_members()}. 55 | 56 | -type whitespace_next() :: value | 57 | array | 58 | object | 59 | {array_next, [jsone:json_value()]} | 60 | {object_key, jsone:json_object_members()} | 61 | {object_value, jsone:json_string(), jsone:json_object_members()} | 62 | {object_next, jsone:json_object_members()}. 63 | 64 | -type decode_result() :: {ok, jsone:json_value(), Rest :: binary()} | {error, {Reason :: term(), [jsone:stack_item()]}}. 65 | 66 | -record(decode_opt_v2, { 67 | object_format = ?DEFAULT_OBJECT_FORMAT :: tuple | proplist | map, 68 | allow_ctrl_chars = false :: boolean(), 69 | reject_invalid_utf8 = false :: boolean(), 70 | keys = binary :: 'binary' | 'atom' | 'existing_atom' | 'attempt_atom', 71 | undefined_as_null = false :: boolean(), 72 | duplicate_map_keys = first :: first | last 73 | }). 74 | -define(OPT, #decode_opt_v2). 75 | -type opt() :: #decode_opt_v2{}. 76 | 77 | 78 | %%-------------------------------------------------------------------------------- 79 | %% Exported Functions 80 | %%-------------------------------------------------------------------------------- 81 | -spec decode(binary()) -> decode_result(). 82 | decode(Json) -> 83 | decode(Json, []). 84 | 85 | 86 | -spec decode(binary(), [jsone:decode_option()]) -> decode_result(). 87 | decode(<>, Options) -> 88 | Opt = parse_options(Options), 89 | whitespace(Json, value, [], <<"">>, Opt). 90 | 91 | 92 | %%-------------------------------------------------------------------------------- 93 | %% Internal Functions 94 | %%-------------------------------------------------------------------------------- 95 | -spec next(binary(), jsone:json_value(), [next()], binary(), opt()) -> decode_result(). 96 | next(<>, Value, [], _Buf, _Opt) -> 97 | {ok, Value, Bin}; 98 | next(<>, Value, [Next | Nexts], Buf, Opt) -> 99 | case Next of 100 | {array_next, Values} -> 101 | whitespace(Bin, {array_next, [Value | Values]}, Nexts, Buf, Opt); 102 | {object_value, Members} -> 103 | whitespace(Bin, {object_value, Value, Members}, Nexts, Buf, Opt); 104 | {object_next, Key, Members} -> 105 | whitespace(Bin, {object_next, [{Key, Value} | Members]}, Nexts, Buf, Opt) 106 | end. 107 | 108 | 109 | -spec whitespace(binary(), whitespace_next(), [next()], binary(), opt()) -> decode_result(). 110 | whitespace(<<$ , Bin/binary>>, Next, Nexts, Buf, Opt) -> 111 | whitespace(Bin, Next, Nexts, Buf, Opt); 112 | whitespace(<<$\t, Bin/binary>>, Next, Nexts, Buf, Opt) -> 113 | whitespace(Bin, Next, Nexts, Buf, Opt); 114 | whitespace(<<$\r, Bin/binary>>, Next, Nexts, Buf, Opt) -> 115 | whitespace(Bin, Next, Nexts, Buf, Opt); 116 | whitespace(<<$\n, Bin/binary>>, Next, Nexts, Buf, Opt) -> 117 | whitespace(Bin, Next, Nexts, Buf, Opt); 118 | whitespace(<>, Next, Nexts, Buf, Opt) -> 119 | case Next of 120 | value -> 121 | value(Bin, Nexts, Buf, Opt); 122 | array -> 123 | array(Bin, Nexts, Buf, Opt); 124 | object -> 125 | object(Bin, Nexts, Buf, Opt); 126 | {object_key, Members} -> 127 | object_key(Bin, Members, Nexts, Buf, Opt); 128 | {array_next, Values} -> 129 | array_next(Bin, Values, Nexts, Buf, Opt); 130 | {object_value, Key, Members} -> 131 | object_value(Bin, Key, Members, Nexts, Buf, Opt); 132 | {object_next, Members} -> 133 | object_next(Bin, Members, Nexts, Buf, Opt) 134 | end. 135 | 136 | 137 | -spec value(binary(), [next()], binary(), opt()) -> decode_result(). 138 | value(<<"false", Bin/binary>>, Nexts, Buf, Opt) -> 139 | next(Bin, false, Nexts, Buf, Opt); 140 | value(<<"true", Bin/binary>>, Nexts, Buf, Opt) -> 141 | next(Bin, true, Nexts, Buf, Opt); 142 | value(<<"null", Bin/binary>>, Nexts, Buf, Opt = ?OPT{undefined_as_null = true}) -> 143 | next(Bin, undefined, Nexts, Buf, Opt); 144 | value(<<"null", Bin/binary>>, Nexts, Buf, Opt) -> 145 | next(Bin, null, Nexts, Buf, Opt); 146 | value(<<$[, Bin/binary>>, Nexts, Buf, Opt) -> 147 | whitespace(Bin, array, Nexts, Buf, Opt); 148 | value(<<${, Bin/binary>>, Nexts, Buf, Opt) -> 149 | whitespace(Bin, object, Nexts, Buf, Opt); 150 | value(<<$", Bin/binary>>, Nexts, Buf, Opt) -> 151 | string(Bin, byte_size(Buf), Nexts, Buf, Opt); 152 | value(<>, Nexts, Buf, Opt) -> 153 | number(Bin, Nexts, Buf, Opt). 154 | 155 | 156 | -spec array(binary(), [next()], binary(), opt()) -> decode_result(). 157 | array(<<$], Bin/binary>>, Nexts, Buf, Opt) -> 158 | next(Bin, [], Nexts, Buf, Opt); 159 | array(<>, Nexts, Buf, Opt) -> 160 | value(Bin, [{array_next, []} | Nexts], Buf, Opt). 161 | 162 | 163 | -spec array_next(binary(), [jsone:json_value()], [next()], binary(), opt()) -> decode_result(). 164 | array_next(<<$], Bin/binary>>, Values, Nexts, Buf, Opt) -> 165 | next(Bin, lists:reverse(Values), Nexts, Buf, Opt); 166 | array_next(<<$,, Bin/binary>>, Values, Nexts, Buf, Opt) -> 167 | whitespace(Bin, value, [{array_next, Values} | Nexts], Buf, Opt); 168 | array_next(Bin, Values, Nexts, Buf, Opt) -> 169 | ?ERROR(array_next, [Bin, Values, Nexts, Buf, Opt]). 170 | 171 | 172 | -spec object(binary(), [next()], binary(), opt()) -> decode_result(). 173 | object(<<$}, Bin/binary>>, Nexts, Buf, Opt) -> 174 | next(Bin, make_object([], Opt), Nexts, Buf, Opt); 175 | object(<>, Nexts, Buf, Opt) -> 176 | object_key(Bin, [], Nexts, Buf, Opt). 177 | 178 | 179 | -spec object_key(binary(), jsone:json_object_members(), [next()], binary(), opt()) -> decode_result(). 180 | object_key(<<$", Bin/binary>>, Members, Nexts, Buf, Opt) -> 181 | string(Bin, byte_size(Buf), [{object_value, Members} | Nexts], Buf, Opt); 182 | object_key(<>, Members, Nexts, Buf, Opt) -> 183 | ?ERROR(object_key, [Bin, Members, Nexts, Buf, Opt]). 184 | 185 | 186 | -spec object_value(binary(), jsone:json_string(), jsone:json_object_members(), [next()], binary(), opt()) -> 187 | decode_result(). 188 | object_value(<<$:, Bin/binary>>, Key, Members, Nexts, Buf, Opt) -> 189 | whitespace(Bin, value, [{object_next, object_key(Key, Opt), Members} | Nexts], Buf, Opt); 190 | object_value(Bin, Key, Members, Nexts, Buf, Opt) -> 191 | ?ERROR(object_value, [Bin, Key, Members, Nexts, Buf, Opt]). 192 | 193 | 194 | -compile({inline, [object_key/2]}). 195 | 196 | 197 | object_key(Key, ?OPT{keys = binary}) -> 198 | Key; 199 | object_key(Key, ?OPT{keys = atom}) -> 200 | binary_to_atom(Key, utf8); 201 | object_key(Key, ?OPT{keys = existing_atom}) -> 202 | binary_to_existing_atom(Key, utf8); 203 | object_key(Key, ?OPT{keys = attempt_atom}) -> 204 | try 205 | binary_to_existing_atom(Key, utf8) 206 | catch 207 | error:badarg -> 208 | Key 209 | end. 210 | 211 | 212 | -spec object_next(binary(), jsone:json_object_members(), [next()], binary(), opt()) -> decode_result(). 213 | object_next(<<$}, Bin/binary>>, Members, Nexts, Buf, Opt) -> 214 | next(Bin, make_object(Members, Opt), Nexts, Buf, Opt); 215 | object_next(<<$,, Bin/binary>>, Members, Nexts, Buf, Opt) -> 216 | whitespace(Bin, {object_key, Members}, Nexts, Buf, Opt); 217 | object_next(Bin, Members, Nexts, Buf, Opt) -> 218 | ?ERROR(object_next, [Bin, Members, Nexts, Buf, Opt]). 219 | 220 | 221 | -spec string(binary(), non_neg_integer(), [next()], binary(), opt()) -> decode_result(). 222 | string(<>, Start, Nexts, Buf, Opt) -> 223 | string(Bin, Bin, Start, Nexts, Buf, Opt). 224 | 225 | 226 | -spec string(binary(), binary(), non_neg_integer(), [next()], binary(), opt()) -> decode_result(). 227 | string(<<$", Bin/binary>>, Base, Start, Nexts, Buf, Opt) -> 228 | Prefix = binary:part(Base, 0, byte_size(Base) - byte_size(Bin) - 1), 229 | case Start =:= byte_size(Buf) of 230 | true -> 231 | next(Bin, Prefix, Nexts, Buf, Opt); 232 | false -> 233 | Buf2 = <>, 234 | next(Bin, binary:part(Buf2, Start, byte_size(Buf2) - Start), Nexts, Buf2, Opt) 235 | end; 236 | string(<<$\\, B/binary>>, Base, Start, Nexts, Buf, Opt) -> 237 | Prefix = binary:part(Base, 0, byte_size(Base) - byte_size(B) - 1), 238 | case B of 239 | <<$", Bin/binary>> -> 240 | string(Bin, Start, Nexts, <>, Opt); 241 | <<$/, Bin/binary>> -> 242 | string(Bin, Start, Nexts, <>, Opt); 243 | <<$\\, Bin/binary>> -> 244 | string(Bin, Start, Nexts, <>, Opt); 245 | <<$b, Bin/binary>> -> 246 | string(Bin, Start, Nexts, <>, Opt); 247 | <<$f, Bin/binary>> -> 248 | string(Bin, Start, Nexts, <>, Opt); 249 | <<$n, Bin/binary>> -> 250 | string(Bin, Start, Nexts, <>, Opt); 251 | <<$r, Bin/binary>> -> 252 | string(Bin, Start, Nexts, <>, Opt); 253 | <<$t, Bin/binary>> -> 254 | string(Bin, Start, Nexts, <>, Opt); 255 | <<$u, Bin/binary>> -> 256 | unicode_string(Bin, Start, Nexts, <>, Opt); 257 | _ -> 258 | ?ERROR(string, [<<$\\, B/binary>>, Base, Start, Nexts, Buf, Opt]) 259 | end; 260 | string(<<_, Bin/binary>>, Base, Start, Nexts, Buf, Opt) 261 | when Opt?OPT.allow_ctrl_chars, not Opt?OPT.reject_invalid_utf8 -> 262 | string(Bin, Base, Start, Nexts, Buf, Opt); 263 | string(<>, Base, Start, Nexts, Buf, Opt) when 16#20 =< C, not Opt?OPT.reject_invalid_utf8 -> 264 | string(Bin, Base, Start, Nexts, Buf, Opt); 265 | string(<<_/utf8, Bin/binary>>, Base, Start, Nexts, Buf, Opt) when Opt?OPT.allow_ctrl_chars -> 266 | string(Bin, Base, Start, Nexts, Buf, Opt); 267 | string(<>, Base, Start, Nexts, Buf, Opt) when 16#20 =< C -> 268 | string(Bin, Base, Start, Nexts, Buf, Opt); 269 | string(Bin, Base, Start, Nexts, Buf, Opt) -> 270 | ?ERROR(string, [Bin, Base, Start, Nexts, Buf, Opt]). 271 | 272 | 273 | -spec unicode_string(binary(), non_neg_integer(), [next()], binary(), opt()) -> decode_result(). 274 | unicode_string(<>, Start, Nexts, Buf, Opt) -> 275 | try binary_to_integer(N, 16) of 276 | High when 16#D800 =< High, High =< 16#DBFF -> 277 | %% surrogate pair 278 | case Bin of 279 | <<$\\, $u, N2:4/binary, Bin2/binary>> -> 280 | try binary_to_integer(N2, 16) of 281 | Low when 16#DC00 =< Low, Low =< 16#DFFF -> 282 | <> = <>, 283 | string(Bin2, Start, Nexts, <>, Opt); 284 | _ -> 285 | ?ERROR(unicode_string, [<>, Start, Nexts, Buf, Opt]) 286 | catch 287 | error:badarg -> 288 | ?ERROR(unicode_string, [<>, Start, Nexts, Buf, Opt]) 289 | end; 290 | _ -> 291 | ?ERROR(unicode_string, [<>, Start, Nexts, Buf, Opt]) 292 | end; 293 | Unicode 294 | when 16#DC00 =< Unicode, 295 | Unicode =< 16#DFFF; % second part of surrogate pair (without first part) 296 | 0 > Unicode -> 297 | ?ERROR(unicode_string, [<>, Start, Nexts, Buf, Opt]); 298 | Unicode -> 299 | string(Bin, Start, Nexts, <>, Opt) 300 | catch 301 | error:badarg -> 302 | ?ERROR(unicode_string, [<>, Start, Nexts, Buf, Opt]) 303 | end; 304 | unicode_string(Bin, Start, Nexts, Buf, Opt) -> 305 | ?ERROR(unicode_string, [Bin, Start, Nexts, Buf, Opt]). 306 | 307 | 308 | -spec number(binary(), [next()], binary(), opt()) -> decode_result(). 309 | number(<<$-, Bin/binary>>, Nexts, Buf, Opt) -> 310 | number_integer_part(Bin, -1, Nexts, Buf, Opt); 311 | number(<>, Nexts, Buf, Opt) -> 312 | number_integer_part(Bin, 1, Nexts, Buf, Opt). 313 | 314 | 315 | -spec number_integer_part(binary(), 1 | -1, [next()], binary(), opt()) -> decode_result(). 316 | number_integer_part(<<$0, Bin/binary>>, Sign, Nexts, Buf, Opt) -> 317 | number_fraction_part(Bin, Sign, 0, Nexts, Buf, Opt); 318 | number_integer_part(<>, Sign, Nexts, Buf, Opt) when $1 =< C, C =< $9 -> 319 | number_integer_part_rest(Bin, C - $0, Sign, Nexts, Buf, Opt); 320 | number_integer_part(Bin, Sign, Nexts, Buf, Opt) -> 321 | ?ERROR(number_integer_part, [Bin, Sign, Nexts, Buf, Opt]). 322 | 323 | 324 | -spec number_integer_part_rest(binary(), non_neg_integer(), 1 | -1, [next()], binary(), opt()) -> decode_result(). 325 | number_integer_part_rest(<>, N, Sign, Nexts, Buf, Opt) when $0 =< C, C =< $9 -> 326 | number_integer_part_rest(Bin, N * 10 + C - $0, Sign, Nexts, Buf, Opt); 327 | number_integer_part_rest(<>, N, Sign, Nexts, Buf, Opt) -> 328 | number_fraction_part(Bin, Sign, N, Nexts, Buf, Opt). 329 | 330 | 331 | -spec number_fraction_part(binary(), 1 | -1, non_neg_integer(), [next()], binary(), opt()) -> decode_result(). 332 | number_fraction_part(<<$., Bin/binary>>, Sign, Int, Nexts, Buf, Opt) -> 333 | number_fraction_part_rest(Bin, Sign, Int, 0, Nexts, Buf, Opt); 334 | number_fraction_part(<>, Sign, Int, Nexts, Buf, Opt) -> 335 | number_exponation_part(Bin, Sign * Int, 0, Nexts, Buf, Opt). 336 | 337 | 338 | -spec number_fraction_part_rest(binary(), 1 | -1, non_neg_integer(), non_neg_integer(), [next()], binary(), opt()) -> 339 | decode_result(). 340 | number_fraction_part_rest(<>, Sign, N, DecimalOffset, Nexts, Buf, Opt) when $0 =< C, C =< $9 -> 341 | number_fraction_part_rest(Bin, Sign, N * 10 + C - $0, DecimalOffset + 1, Nexts, Buf, Opt); 342 | number_fraction_part_rest(<>, Sign, N, DecimalOffset, Nexts, Buf, Opt) when DecimalOffset > 0 -> 343 | number_exponation_part(Bin, Sign * N, DecimalOffset, Nexts, Buf, Opt); 344 | number_fraction_part_rest(Bin, Sign, N, DecimalOffset, Nexts, Buf, Opt) -> 345 | ?ERROR(number_fraction_part_rest, [Bin, Sign, N, DecimalOffset, Nexts, Buf, Opt]). 346 | 347 | 348 | -spec number_exponation_part(binary(), integer(), non_neg_integer(), [next()], binary(), opt()) -> decode_result(). 349 | number_exponation_part(<<$e, $+, Bin/binary>>, N, DecimalOffset, Nexts, Buf, Opt) -> 350 | number_exponation_part(Bin, N, DecimalOffset, 1, 0, true, Nexts, Buf, Opt); 351 | number_exponation_part(<<$E, $+, Bin/binary>>, N, DecimalOffset, Nexts, Buf, Opt) -> 352 | number_exponation_part(Bin, N, DecimalOffset, 1, 0, true, Nexts, Buf, Opt); 353 | number_exponation_part(<<$e, $-, Bin/binary>>, N, DecimalOffset, Nexts, Buf, Opt) -> 354 | number_exponation_part(Bin, N, DecimalOffset, -1, 0, true, Nexts, Buf, Opt); 355 | number_exponation_part(<<$E, $-, Bin/binary>>, N, DecimalOffset, Nexts, Buf, Opt) -> 356 | number_exponation_part(Bin, N, DecimalOffset, -1, 0, true, Nexts, Buf, Opt); 357 | number_exponation_part(<<$e, Bin/binary>>, N, DecimalOffset, Nexts, Buf, Opt) -> 358 | number_exponation_part(Bin, N, DecimalOffset, 1, 0, true, Nexts, Buf, Opt); 359 | number_exponation_part(<<$E, Bin/binary>>, N, DecimalOffset, Nexts, Buf, Opt) -> 360 | number_exponation_part(Bin, N, DecimalOffset, 1, 0, true, Nexts, Buf, Opt); 361 | number_exponation_part(<>, N, DecimalOffset, Nexts, Buf, Opt) -> 362 | case DecimalOffset of 363 | 0 -> 364 | next(Bin, N, Nexts, Buf, Opt); 365 | _ -> 366 | next(Bin, N / math:pow(10, DecimalOffset), Nexts, Buf, Opt) 367 | end. 368 | 369 | 370 | -spec number_exponation_part(binary(), 371 | integer(), 372 | non_neg_integer(), 373 | 1 | -1, 374 | non_neg_integer(), 375 | boolean(), 376 | [next()], 377 | binary(), 378 | opt()) -> decode_result(). 379 | number_exponation_part(<>, N, DecimalOffset, ExpSign, Exp, _, Nexts, Buf, Opt) when $0 =< C, C =< $9 -> 380 | number_exponation_part(Bin, N, DecimalOffset, ExpSign, Exp * 10 + C - $0, false, Nexts, Buf, Opt); 381 | number_exponation_part(<>, N, DecimalOffset, ExpSign, Exp, false, Nexts, Buf, Opt) -> 382 | Pos = ExpSign * Exp - DecimalOffset, 383 | try case Pos of 384 | Pos when Pos >= 0 -> 385 | N * math:pow(10, Pos); 386 | _ -> 387 | N / math:pow(10, -Pos) % multiplying by decimal makes float errors larger. 388 | end of 389 | Res -> 390 | next(Bin, Res, Nexts, Buf, Opt) 391 | catch 392 | error:badarith -> 393 | ?ERROR(number_exponation_part, [Bin, N, DecimalOffset, ExpSign, Exp, false, Nexts, Buf, Opt]) 394 | end; 395 | number_exponation_part(Bin, N, DecimalOffset, ExpSign, Exp, IsFirst, Nexts, Buf, Opt) -> 396 | ?ERROR(number_exponation_part, [Bin, N, DecimalOffset, ExpSign, Exp, IsFirst, Nexts, Buf, Opt]). 397 | 398 | 399 | -spec make_object(jsone:json_object_members(), opt()) -> jsone:json_object(). 400 | make_object(Members, ?OPT{object_format = tuple}) -> 401 | {lists:reverse(Members)}; 402 | make_object(Members, ?OPT{object_format = map, duplicate_map_keys = last}) -> 403 | ?LIST_TO_MAP(lists:reverse(Members)); 404 | make_object(Members, ?OPT{object_format = map}) -> 405 | ?LIST_TO_MAP(Members); 406 | make_object([], _) -> 407 | [{}]; 408 | make_object(Members, _) -> 409 | lists:reverse(Members). 410 | 411 | 412 | -spec parse_options([jsone:decode_option()]) -> opt(). 413 | parse_options(Options) -> 414 | parse_option(Options, ?OPT{}). 415 | 416 | 417 | -spec parse_option([jsone:decode_option()], opt()) -> opt(). 418 | parse_option([], Opt) -> 419 | Opt; 420 | parse_option([{object_format, F} | T], Opt) when F =:= tuple; F =:= proplist; F =:= map -> 421 | parse_option(T, Opt?OPT{object_format = F}); 422 | parse_option([{allow_ctrl_chars, B} | T], Opt) when is_boolean(B) -> 423 | parse_option(T, Opt?OPT{allow_ctrl_chars = B}); 424 | parse_option([reject_invalid_utf8 | T], Opt) -> 425 | parse_option(T, Opt?OPT{reject_invalid_utf8 = true}); 426 | parse_option([{keys, K} | T], Opt) when K =:= binary; K =:= atom; K =:= existing_atom; K =:= attempt_atom -> 427 | parse_option(T, Opt?OPT{keys = K}); 428 | parse_option([undefined_as_null | T], Opt) -> 429 | parse_option(T, Opt?OPT{undefined_as_null = true}); 430 | parse_option([{duplicate_map_keys, V} | T], Opt) when V =:= first; V =:= last -> 431 | parse_option(T, Opt?OPT{duplicate_map_keys = V}); 432 | parse_option(List, Opt) -> 433 | error(badarg, [List, Opt]). 434 | -------------------------------------------------------------------------------- /src/jsone_encode.erl: -------------------------------------------------------------------------------- 1 | %%% @doc JSON encoding module 2 | %%% @private 3 | %%% @end 4 | %%% 5 | %%% Copyright (c) 2013-2016, Takeru Ohta 6 | %%% 7 | %%% The MIT License 8 | %%% 9 | %%% Permission is hereby granted, free of charge, to any person obtaining a copy 10 | %%% of this software and associated documentation files (the "Software"), to deal 11 | %%% in the Software without restriction, including without limitation the rights 12 | %%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | %%% copies of the Software, and to permit persons to whom the Software is 14 | %%% furnished to do so, subject to the following conditions: 15 | %%% 16 | %%% The above copyright notice and this permission notice shall be included in 17 | %%% all copies or substantial portions of the Software. 18 | %%% 19 | %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | %%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | %%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | %%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | %%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | %%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | %%% THE SOFTWARE. 26 | %%% 27 | %%%--------------------------------------------------------------------------------------- 28 | -module(jsone_encode). 29 | 30 | -ifdef(ENABLE_HIPE). 31 | -compile([native, {hipe, [o3]}]). 32 | -endif. 33 | 34 | %%-------------------------------------------------------------------------------- 35 | %% Exported API 36 | %%-------------------------------------------------------------------------------- 37 | -export([encode/1, encode/2]). 38 | 39 | %%-------------------------------------------------------------------------------- 40 | %% Macros & Records & Types 41 | %%-------------------------------------------------------------------------------- 42 | -define(ERROR(Function, Args), {error, {badarg, [{?MODULE, Function, Args, [{line, ?LINE}]}]}}). 43 | -define(IS_STR(X), (is_binary(X) orelse is_atom(X))). 44 | -define(IS_UINT(X), (is_integer(X) andalso X >= 0)). 45 | -define(IS_PNUM(X), (is_number(X) andalso X >= 0)). 46 | -define(IS_DATETIME(Y, M, D, H, Mi, S), 47 | (?IS_UINT(Y) andalso ?IS_UINT(M) andalso ?IS_UINT(D) andalso ?IS_UINT(H) andalso ?IS_UINT(Mi) andalso 48 | ?IS_PNUM(S))). 49 | 50 | -ifdef('NO_MAP_TYPE'). 51 | -define(IS_MAP(X), is_tuple(X)). 52 | -define(ENCODE_MAP(Value, Nexts, Buf, Opt), ?ERROR(value, [Value, Nexts, Buf, Opt])). 53 | -else. 54 | -define(IS_MAP(X), is_map(X)). 55 | -ifdef('MAP_ITER_ORDERED'). 56 | -if(?OTP_RELEASE >= 27). 57 | -define(ENCODE_MAP(Value, Nexts, Buf, Opt), 58 | object(maps:to_list(maps:iterator(Value, ordered)), Nexts, Buf, Opt)). 59 | -else. 60 | -define(ENCODE_MAP(Value, Nexts, Buf, Opt), object(maps:to_list(Value), Nexts, Buf, Opt)). 61 | -endif. 62 | -else. 63 | -define(ENCODE_MAP(Value, Nexts, Buf, Opt), object(maps:to_list(Value), Nexts, Buf, Opt)). 64 | -endif. 65 | -endif. 66 | 67 | -type encode_result() :: {ok, binary()} | {error, {Reason :: term(), [jsone:stack_item()]}}. 68 | -type next() :: {array_values, [jsone:json_value()]} | 69 | {object_value, jsone:json_value(), jsone:json_object_members()} | 70 | {object_members, jsone:json_object_members()} | 71 | {char, binary()}. 72 | 73 | -record(encode_opt_v2, { 74 | native_utf8 = false :: boolean(), 75 | native_forward_slash = false :: boolean(), 76 | canonical_form = false :: boolean(), 77 | float_format = [{scientific, 20}] :: [jsone:float_format_option()], 78 | datetime_format = {iso8601, 0} :: {jsone:datetime_format(), jsone:utc_offset_seconds()}, 79 | object_key_type = string :: string | scalar | value, 80 | space = 0 :: non_neg_integer(), 81 | indent = 0 :: non_neg_integer(), 82 | undefined_as_null = false :: boolean(), 83 | skip_undefined = false :: boolean(), 84 | map_unknown_value = fun jsone:term_to_json_string/1 :: undefined | 85 | fun((term()) -> {ok, jsone:json_value()} | error) 86 | }). 87 | -define(OPT, #encode_opt_v2). 88 | -type opt() :: #encode_opt_v2{}. 89 | 90 | 91 | %%-------------------------------------------------------------------------------- 92 | %% Exported Functions 93 | %%-------------------------------------------------------------------------------- 94 | -spec encode(jsone:json_value()) -> encode_result(). 95 | encode(Value) -> 96 | encode(Value, []). 97 | 98 | 99 | -spec encode(jsone:json_value() | term(), [jsone:encode_option()]) -> encode_result(). 100 | encode(Value, Options) -> 101 | Opt = parse_options(Options), 102 | value(Value, [], <<"">>, Opt). 103 | 104 | 105 | %%-------------------------------------------------------------------------------- 106 | %% Internal Functions 107 | %%-------------------------------------------------------------------------------- 108 | -spec next([next()], binary(), opt()) -> encode_result(). 109 | next([], Buf, _) -> 110 | {ok, Buf}; 111 | next(Level = [Next | Nexts], Buf, Opt) -> 112 | case Next of 113 | {array_values, Values} -> 114 | case Values of 115 | [] -> 116 | array_values(Values, Nexts, Buf, Opt); 117 | _ -> 118 | array_values(Values, Nexts, pp_newline_or_space(<>, Level, Opt), Opt) 119 | end; 120 | {object_value, Value, Members} -> 121 | object_value(Value, Members, Nexts, pp_space(<>, Opt), Opt); 122 | {object_members, Members} -> 123 | case Members of 124 | [] -> 125 | object_members(Members, Nexts, Buf, Opt); 126 | _ -> 127 | object_members(Members, Nexts, pp_newline_or_space(<>, Level, Opt), Opt) 128 | end; 129 | {char, C} -> 130 | next(Nexts, <>, Opt) 131 | end. 132 | 133 | 134 | -spec value(jsone:json_value(), [next()], binary(), opt()) -> encode_result(). 135 | value(null, Nexts, Buf, Opt) -> 136 | next(Nexts, <>, Opt); 137 | value(undefined, Nexts, Buf, Opt = ?OPT{undefined_as_null = true}) -> 138 | next(Nexts, <>, Opt); 139 | value(false, Nexts, Buf, Opt) -> 140 | next(Nexts, <>, Opt); 141 | value(true, Nexts, Buf, Opt) -> 142 | next(Nexts, <>, Opt); 143 | value({{json, T}}, Nexts, Buf, Opt) -> 144 | try 145 | next(Nexts, <>, Opt) 146 | catch 147 | error:badarg -> 148 | ?ERROR(value, [{json, T}, Nexts, Buf, Opt]) 149 | end; 150 | value({{json_utf8, T}}, Nexts, Buf, Opt) -> 151 | try unicode:characters_to_binary(T) of 152 | {error, OK, Invalid} -> 153 | {error, {{invalid_json_utf8, OK, Invalid}, 154 | [{?MODULE, value, [{json_utf8, T}, Nexts, Buf, Opt], [{line, ?LINE}]}]}}; 155 | {incomplete, OK, Incomplete} -> 156 | {error, {{invalid_json_utf8, OK, Incomplete}, 157 | [{?MODULE, value, [{json_utf8, T}, Nexts, Buf, Opt], [{line, ?LINE}]}]}}; 158 | B when is_binary(B) -> 159 | next(Nexts, <>, Opt) 160 | catch 161 | error:badarg -> 162 | ?ERROR(value, [{json_utf8, T}, Nexts, Buf, Opt]) 163 | end; 164 | value(Value, Nexts, Buf, Opt) when is_integer(Value) -> 165 | next(Nexts, <>, Opt); 166 | value(Value, Nexts, Buf, Opt) when is_float(Value) -> 167 | next(Nexts, <>, Opt); 168 | value(Value, Nexts, Buf, Opt) when ?IS_STR(Value) -> 169 | string(Value, Nexts, Buf, Opt); 170 | value({{_, _, _}, {_, _, _}} = Value, Nexts, Buf, Opt) -> 171 | datetime(Value, Nexts, Buf, Opt); 172 | value({Value}, Nexts, Buf, Opt) when is_list(Value) -> 173 | object(Value, Nexts, Buf, Opt); 174 | value([{}], Nexts, Buf, Opt) -> 175 | object([], Nexts, Buf, Opt); 176 | value([{{_, _, _}, {_, _, _}} | _] = Value, Nexts, Buf, Opt) -> 177 | array(Value, Nexts, Buf, Opt); 178 | value([{_, _} | _] = Value, Nexts, Buf, Opt) -> 179 | object(Value, Nexts, Buf, Opt); 180 | value(Value, Nexts, Buf, Opt) when ?IS_MAP(Value) -> 181 | ?ENCODE_MAP(Value, Nexts, Buf, Opt); 182 | value(Value, Nexts, Buf, Opt) when is_list(Value) -> 183 | array(Value, Nexts, Buf, Opt); 184 | value(Value, Nexts, Buf, Opt) -> 185 | case Opt?OPT.map_unknown_value of 186 | undefined -> 187 | ?ERROR(value, [Value, Nexts, Buf, Opt]); 188 | Fun -> 189 | case Fun(Value) of 190 | error -> 191 | ?ERROR(value, [Value, Nexts, Buf, Opt]); 192 | {ok, NewValue} -> 193 | value(NewValue, Nexts, Buf, Opt) 194 | end 195 | end. 196 | 197 | 198 | -spec string(jsone:json_string(), [next()], binary(), opt()) -> encode_result(). 199 | string(<>, Nexts, Buf, Opt) -> 200 | Len = unescaped_string_length(Str, Opt), 201 | <> = Str, 202 | escape_string(Rest, Nexts, <>, Opt); 203 | string(Str, Nexts, Buf, Opt) -> 204 | string(atom_to_binary(Str, utf8), Nexts, Buf, Opt). 205 | 206 | 207 | -spec datetime(calendar:datetime(), [next()], binary(), opt()) -> encode_result(). 208 | datetime({{Y, M, D}, {H, Mi, S}}, Nexts, Buf, Opt) when ?IS_DATETIME(Y, M, D, H, Mi, S) -> 209 | {iso8601, Tz} = Opt?OPT.datetime_format, 210 | Str = [format_year(Y), 211 | $-, 212 | format2digit(M), 213 | $-, 214 | format2digit(D), 215 | $T, 216 | format2digit(H), 217 | $:, 218 | format2digit(Mi), 219 | $:, 220 | format_seconds(S), 221 | format_tz(Tz)], 222 | next(Nexts, <>, Opt); 223 | datetime(Datetime, Nexts, Buf, Opt) -> 224 | ?ERROR(datetime, [Datetime, Nexts, Buf, Opt]). 225 | 226 | 227 | -ifndef(NO_DIALYZER_SPEC). 228 | -dialyzer({no_improper_lists, [format_year/1]}). 229 | -endif. 230 | 231 | 232 | -spec format_year(non_neg_integer()) -> iodata(). 233 | format_year(Y) when Y > 999 -> 234 | integer_to_binary(Y); 235 | format_year(Y) -> 236 | B = integer_to_binary(Y), 237 | [lists:duplicate(4 - byte_size(B), $0) | B]. 238 | 239 | 240 | -spec format2digit(non_neg_integer()) -> iolist(). 241 | format2digit(0) -> 242 | "00"; 243 | format2digit(1) -> 244 | "01"; 245 | format2digit(2) -> 246 | "02"; 247 | format2digit(3) -> 248 | "03"; 249 | format2digit(4) -> 250 | "04"; 251 | format2digit(5) -> 252 | "05"; 253 | format2digit(6) -> 254 | "06"; 255 | format2digit(7) -> 256 | "07"; 257 | format2digit(8) -> 258 | "08"; 259 | format2digit(9) -> 260 | "09"; 261 | format2digit(X) -> 262 | integer_to_list(X). 263 | 264 | 265 | -spec format_seconds(non_neg_integer() | float()) -> iolist(). 266 | format_seconds(S) when is_integer(S) -> 267 | format2digit(S); 268 | format_seconds(S) when is_float(S) -> 269 | io_lib:format("~6.3.0f", [S]). 270 | 271 | 272 | -spec format_tz(integer()) -> byte() | iolist(). 273 | format_tz(0) -> 274 | $Z; 275 | format_tz(Tz) when Tz > 0 -> 276 | [$+ | format_tz_(Tz)]; 277 | format_tz(Tz) -> 278 | [$- | format_tz_(-Tz)]. 279 | 280 | 281 | -define(SECONDS_PER_MINUTE, 60). 282 | -define(SECONDS_PER_HOUR, 3600). 283 | 284 | 285 | -spec format_tz_(integer()) -> iolist(). 286 | format_tz_(S) -> 287 | H = S div ?SECONDS_PER_HOUR, 288 | S1 = S rem ?SECONDS_PER_HOUR, 289 | M = S1 div ?SECONDS_PER_MINUTE, 290 | [format2digit(H), $:, format2digit(M)]. 291 | 292 | 293 | -spec object_key(jsone:json_value(), [next()], binary(), opt()) -> encode_result(). 294 | object_key(Key, Nexts, Buf, Opt) when ?IS_STR(Key) -> 295 | string(Key, Nexts, Buf, Opt); 296 | object_key(Key, Nexts, Buf, Opt = ?OPT{object_key_type = scalar}) when is_number(Key) -> 297 | value(Key, [{char, $"} | Nexts], <>, Opt); 298 | object_key(Key = {{Y, M, D}, {H, Mi, S}}, Nexts, Buf, Opt = ?OPT{object_key_type = Type}) 299 | when ?IS_DATETIME(Y, M, D, H, Mi, S), Type =/= string -> 300 | value(Key, Nexts, Buf, Opt); 301 | object_key(Key, Nexts, Buf, Opt = ?OPT{object_key_type = value}) -> 302 | case value(Key, [], <<>>, Opt) of 303 | {error, Reason} -> 304 | {error, Reason}; 305 | {ok, BinaryKey} -> 306 | string(BinaryKey, Nexts, Buf, Opt) 307 | end; 308 | object_key(Key, Nexts, Buf, Opt) -> 309 | ?ERROR(object_key, [Key, Nexts, Buf, Opt]). 310 | 311 | 312 | -define(H8(X), (hex(X)):16). 313 | -define(H16(X), ?H8(X bsr 8), ?H8(X band 16#FF)). 314 | 315 | -ifdef(ENABLE_HIPE). 316 | -define(COPY_UTF8, 317 | escape_string(<<2#110:3, C1:5, C2, Str/binary>>, Nexts, Buf, Opt) -> 318 | escape_string(Str, Nexts, <>, Opt); 319 | escape_string(<<2#1110:4, C1:4, C2:16, Str/binary>>, Nexts, Buf, Opt) -> 320 | escape_string(Str, Nexts, <>, Opt); 321 | escape_string(<<2#11110:5, C1:3, C2:24, Str/binary>>, Nexts, Buf, Opt) -> 322 | escape_string(Str, Nexts, <>, Opt)). 323 | -else. 324 | -define(COPY_UTF8, 325 | escape_string(<>, Nexts, Buf, Opt) -> 326 | escape_string(Str, Nexts, <>, Opt)). 327 | -endif. 328 | 329 | 330 | -spec escape_string(binary(), [next()], binary(), opt()) -> encode_result(). 331 | escape_string(<<"">>, Nexts, Buf, Opt) -> 332 | next(Nexts, <>, Opt); 333 | escape_string(<<$", Str/binary>>, Nexts, Buf, Opt) -> 334 | escape_string(Str, Nexts, <>, Opt); 335 | escape_string(<<$\\, Str/binary>>, Nexts, Buf, Opt) -> 336 | escape_string(Str, Nexts, <>, Opt); 337 | escape_string(<<$\b, Str/binary>>, Nexts, Buf, Opt) -> 338 | escape_string(Str, Nexts, <>, Opt); 339 | escape_string(<<$\f, Str/binary>>, Nexts, Buf, Opt) -> 340 | escape_string(Str, Nexts, <>, Opt); 341 | escape_string(<<$\n, Str/binary>>, Nexts, Buf, Opt) -> 342 | escape_string(Str, Nexts, <>, Opt); 343 | escape_string(<<$\r, Str/binary>>, Nexts, Buf, Opt) -> 344 | escape_string(Str, Nexts, <>, Opt); 345 | escape_string(<<$\t, Str/binary>>, Nexts, Buf, Opt) -> 346 | escape_string(Str, Nexts, <>, Opt); 347 | escape_string(<<$\/, Str/binary>>, Nexts, Buf, Opt) when not Opt?OPT.native_forward_slash -> 348 | escape_string(Str, Nexts, <>, Opt); 349 | escape_string(<<0:1, C:7, Str/binary>>, Nexts, Buf, Opt) -> 350 | case C < 16#20 of 351 | true -> 352 | escape_string(Str, Nexts, <>, Opt); 353 | false -> 354 | Len = unescaped_string_length(Str, Opt), 355 | <> = Str, 356 | escape_string(Rest, Nexts, <>, Opt) 357 | 358 | end; 359 | escape_string(<>, Nexts, Buf, Opt = ?OPT{native_utf8 = false}) -> 360 | if 361 | Ch =< 16#FFFF -> 362 | escape_string(Str, Nexts, <>, Opt); 363 | true -> 364 | <> = <>, 365 | escape_string(Str, Nexts, <>, Opt) 366 | end; 367 | ?COPY_UTF8; 368 | escape_string(Str, Nexts, Buf, Opt) -> 369 | ?ERROR(escape_string, [Str, Nexts, Buf, Opt]). 370 | 371 | 372 | -define(UNESCAPED_CHARS_WITH_SLASH, 373 | {false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, 374 | false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, 375 | false, true, true, false, true, true, true, true, true, true, true, true, true, true, true, true, true, 376 | true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, 377 | true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, 378 | true, true, true, true, true, true, true, true, true, true, false, true, true, true, true, true, true, 379 | true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, 380 | true, true, true, true, true, true, true, true, true, true, true, false, false, false, false, false, 381 | false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, 382 | false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, 383 | false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, 384 | false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, 385 | false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, 386 | false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, 387 | false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, 388 | false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, 389 | false, false, false, false}). 390 | 391 | -define(UNESCAPED_CHARS_WITHOUT_SLASH, 392 | {false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, 393 | false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, 394 | false, true, true, false, true, true, true, true, true, true, true, true, true, true, true, true, false, 395 | true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, 396 | true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, 397 | true, true, true, true, true, true, true, true, true, true, false, true, true, true, true, true, true, 398 | true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, 399 | true, true, true, true, true, true, true, true, true, true, true, false, false, false, false, false, 400 | false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, 401 | false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, 402 | false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, 403 | false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, 404 | false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, 405 | false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, 406 | false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, 407 | false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, 408 | false, false, false, false}). 409 | 410 | -compile({inline, [unescaped_string_length/2]}). 411 | 412 | 413 | -spec unescaped_string_length(binary(), opt()) -> non_neg_integer(). 414 | unescaped_string_length(Str, ?OPT{native_forward_slash = true}) -> 415 | unescaped_string_length(Str, 0, ?UNESCAPED_CHARS_WITH_SLASH); 416 | unescaped_string_length(Str, _) -> 417 | unescaped_string_length(Str, 0, ?UNESCAPED_CHARS_WITHOUT_SLASH). 418 | 419 | 420 | -spec unescaped_string_length(binary(), non_neg_integer(), tuple()) -> non_neg_integer(). 421 | unescaped_string_length(<>, N, Table) -> 422 | case element(C + 1, Table) of 423 | true -> 424 | unescaped_string_length(Str, N + 1, Table); 425 | false -> 426 | N 427 | end; 428 | unescaped_string_length(_, N, _) -> 429 | N. 430 | 431 | 432 | -compile({inline, [hex/1]}). 433 | 434 | 435 | -spec hex(byte()) -> 0..16#FFFF. 436 | hex(X) -> 437 | element(X + 1, 438 | {16#3030, 16#3031, 16#3032, 16#3033, 16#3034, 16#3035, 16#3036, 16#3037, 16#3038, 16#3039, 16#3061, 16#3062, 439 | 16#3063, 16#3064, 16#3065, 16#3066, 16#3130, 16#3131, 16#3132, 16#3133, 16#3134, 16#3135, 16#3136, 16#3137, 440 | 16#3138, 16#3139, 16#3161, 16#3162, 16#3163, 16#3164, 16#3165, 16#3166, 16#3230, 16#3231, 16#3232, 16#3233, 441 | 16#3234, 16#3235, 16#3236, 16#3237, 16#3238, 16#3239, 16#3261, 16#3262, 16#3263, 16#3264, 16#3265, 16#3266, 442 | 16#3330, 16#3331, 16#3332, 16#3333, 16#3334, 16#3335, 16#3336, 16#3337, 16#3338, 16#3339, 16#3361, 16#3362, 443 | 16#3363, 16#3364, 16#3365, 16#3366, 16#3430, 16#3431, 16#3432, 16#3433, 16#3434, 16#3435, 16#3436, 16#3437, 444 | 16#3438, 16#3439, 16#3461, 16#3462, 16#3463, 16#3464, 16#3465, 16#3466, 16#3530, 16#3531, 16#3532, 16#3533, 445 | 16#3534, 16#3535, 16#3536, 16#3537, 16#3538, 16#3539, 16#3561, 16#3562, 16#3563, 16#3564, 16#3565, 16#3566, 446 | 16#3630, 16#3631, 16#3632, 16#3633, 16#3634, 16#3635, 16#3636, 16#3637, 16#3638, 16#3639, 16#3661, 16#3662, 447 | 16#3663, 16#3664, 16#3665, 16#3666, 16#3730, 16#3731, 16#3732, 16#3733, 16#3734, 16#3735, 16#3736, 16#3737, 448 | 16#3738, 16#3739, 16#3761, 16#3762, 16#3763, 16#3764, 16#3765, 16#3766, 16#3830, 16#3831, 16#3832, 16#3833, 449 | 16#3834, 16#3835, 16#3836, 16#3837, 16#3838, 16#3839, 16#3861, 16#3862, 16#3863, 16#3864, 16#3865, 16#3866, 450 | 16#3930, 16#3931, 16#3932, 16#3933, 16#3934, 16#3935, 16#3936, 16#3937, 16#3938, 16#3939, 16#3961, 16#3962, 451 | 16#3963, 16#3964, 16#3965, 16#3966, 16#6130, 16#6131, 16#6132, 16#6133, 16#6134, 16#6135, 16#6136, 16#6137, 452 | 16#6138, 16#6139, 16#6161, 16#6162, 16#6163, 16#6164, 16#6165, 16#6166, 16#6230, 16#6231, 16#6232, 16#6233, 453 | 16#6234, 16#6235, 16#6236, 16#6237, 16#6238, 16#6239, 16#6261, 16#6262, 16#6263, 16#6264, 16#6265, 16#6266, 454 | 16#6330, 16#6331, 16#6332, 16#6333, 16#6334, 16#6335, 16#6336, 16#6337, 16#6338, 16#6339, 16#6361, 16#6362, 455 | 16#6363, 16#6364, 16#6365, 16#6366, 16#6430, 16#6431, 16#6432, 16#6433, 16#6434, 16#6435, 16#6436, 16#6437, 456 | 16#6438, 16#6439, 16#6461, 16#6462, 16#6463, 16#6464, 16#6465, 16#6466, 16#6530, 16#6531, 16#6532, 16#6533, 457 | 16#6534, 16#6535, 16#6536, 16#6537, 16#6538, 16#6539, 16#6561, 16#6562, 16#6563, 16#6564, 16#6565, 16#6566, 458 | 16#6630, 16#6631, 16#6632, 16#6633, 16#6634, 16#6635, 16#6636, 16#6637, 16#6638, 16#6639, 16#6661, 16#6662, 459 | 16#6663, 16#6664, 16#6665, 16#6666}). 460 | 461 | 462 | -spec array(jsone:json_array(), [next()], binary(), opt()) -> encode_result(). 463 | array([], Nexts, Buf, Opt) -> 464 | next(Nexts, <>, Opt); 465 | array(List, Nexts, Buf, Opt) -> 466 | array_values(List, Nexts, pp_newline(<>, Nexts, 1, Opt), Opt). 467 | 468 | 469 | -spec array_values(jsone:json_array(), [next()], binary(), opt()) -> encode_result(). 470 | array_values([], Nexts, Buf, Opt) -> 471 | next(Nexts, <<(pp_newline(Buf, Nexts, Opt))/binary, $]>>, Opt); 472 | array_values([X | Xs], Nexts, Buf, Opt) -> 473 | value(X, [{array_values, Xs} | Nexts], Buf, Opt). 474 | 475 | 476 | -spec object(jsone:json_object_members(), [next()], binary(), opt()) -> encode_result(). 477 | object(Members, Nexts, Buf, ?OPT{skip_undefined = true} = Opt) -> 478 | object1(lists:filter(fun({_, V}) -> V =/= undefined end, Members), Nexts, Buf, Opt); 479 | object(Members, Nexts, Buf, Opt) -> 480 | object1(Members, Nexts, Buf, Opt). 481 | 482 | 483 | -spec object1(jsone:json_object_members(), [next()], binary(), opt()) -> encode_result(). 484 | object1([], Nexts, Buf, Opt) -> 485 | next(Nexts, <>, Opt); 486 | object1(Members, Nexts, Buf, ?OPT{canonical_form = true} = Opt) -> 487 | object_members(lists:sort(Members), Nexts, pp_newline(<>, Nexts, 1, Opt), Opt); 488 | object1(Members, Nexts, Buf, Opt) -> 489 | object_members(Members, Nexts, pp_newline(<>, Nexts, 1, Opt), Opt). 490 | 491 | 492 | -spec object_members(jsone:json_object_members(), [next()], binary(), opt()) -> encode_result(). 493 | object_members([], Nexts, Buf, Opt) -> 494 | next(Nexts, <<(pp_newline(Buf, Nexts, Opt))/binary, $}>>, Opt); 495 | object_members([{Key, Value} | Xs], Nexts, Buf, Opt) -> 496 | object_key(Key, [{object_value, Value, Xs} | Nexts], Buf, Opt); 497 | object_members(Arg, Nexts, Buf, Opt) -> 498 | ?ERROR(object_members, [Arg, Nexts, Buf, Opt]). 499 | 500 | 501 | -spec object_value(jsone:json_value(), jsone:json_object_members(), [next()], binary(), opt()) -> encode_result(). 502 | object_value(Value, Members, Nexts, Buf, Opt) -> 503 | value(Value, [{object_members, Members} | Nexts], Buf, Opt). 504 | 505 | 506 | -spec pp_space(binary(), opt()) -> binary(). 507 | pp_space(Buf, Opt) -> 508 | padding(Buf, Opt?OPT.space). 509 | 510 | 511 | -spec pp_newline(binary(), list(), opt()) -> binary(). 512 | pp_newline(Buf, Level, Opt) -> 513 | pp_newline(Buf, Level, 0, Opt). 514 | 515 | 516 | -spec pp_newline(binary(), list(), non_neg_integer(), opt()) -> binary(). 517 | pp_newline(Buf, _, _, ?OPT{indent = 0}) -> 518 | Buf; 519 | pp_newline(Buf, L, Extra, ?OPT{indent = N}) -> 520 | padding(<>, Extra * N + length(L) * N). 521 | 522 | 523 | -spec pp_newline_or_space(binary(), list(), opt()) -> binary(). 524 | pp_newline_or_space(Buf, _, Opt = ?OPT{indent = 0}) -> 525 | pp_space(Buf, Opt); 526 | pp_newline_or_space(Buf, L, Opt) -> 527 | pp_newline(Buf, L, Opt). 528 | 529 | 530 | -spec padding(binary(), non_neg_integer()) -> binary(). 531 | padding(Buf, 0) -> 532 | Buf; 533 | padding(Buf, 1) -> 534 | <>; 535 | padding(Buf, 2) -> 536 | <>; 537 | padding(Buf, 3) -> 538 | <>; 539 | padding(Buf, 4) -> 540 | <>; 541 | padding(Buf, 5) -> 542 | <>; 543 | padding(Buf, 6) -> 544 | <>; 545 | padding(Buf, 7) -> 546 | <>; 547 | padding(Buf, 8) -> 548 | <>; 549 | padding(Buf, N) -> 550 | padding(<>, N - 9). 551 | 552 | 553 | -spec parse_options([jsone:encode_option()]) -> opt(). 554 | parse_options(Options) -> 555 | parse_option(Options, ?OPT{}). 556 | 557 | 558 | -spec parse_option([jsone:encode_option()], opt()) -> opt(). 559 | parse_option([], Opt) -> 560 | Opt; 561 | parse_option([native_utf8 | T], Opt) -> 562 | parse_option(T, Opt?OPT{native_utf8 = true}); 563 | parse_option([native_forward_slash | T], Opt) -> 564 | parse_option(T, Opt?OPT{native_forward_slash = true}); 565 | parse_option([canonical_form | T], Opt) -> 566 | parse_option(T, Opt?OPT{canonical_form = true}); 567 | parse_option([{float_format, F} | T], Opt) when is_list(F) -> 568 | parse_option(T, Opt?OPT{float_format = F}); 569 | parse_option([{space, N} | T], Opt) when is_integer(N), N >= 0 -> 570 | parse_option(T, Opt?OPT{space = N}); 571 | parse_option([{indent, N} | T], Opt) when is_integer(N), N >= 0 -> 572 | parse_option(T, Opt?OPT{indent = N}); 573 | parse_option([{object_key_type, Type} | T], Opt) when Type =:= string; Type =:= scalar; Type =:= value -> 574 | parse_option(T, Opt?OPT{object_key_type = Type}); 575 | parse_option([{datetime_format, Fmt} | T], Opt) -> 576 | case Fmt of 577 | iso8601 -> 578 | parse_option(T, Opt?OPT{datetime_format = {iso8601, 0}}); 579 | {iso8601, utc} -> 580 | parse_option(T, Opt?OPT{datetime_format = {iso8601, 0}}); 581 | {iso8601, local} -> 582 | parse_option(T, Opt?OPT{datetime_format = {iso8601, local_offset()}}); 583 | {iso8601, local_dst} -> 584 | parse_option(T, Opt?OPT{datetime_format = {iso8601, local_offset_dst()}}); 585 | {iso8601, N} when -86400 < N, N < 86400 -> 586 | parse_option(T, Opt?OPT{datetime_format = {iso8601, N}}); 587 | _ -> 588 | error(badarg, [[{datetime_format, Fmt} | T], Opt]) 589 | end; 590 | parse_option([undefined_as_null | T], Opt) -> 591 | parse_option(T, Opt?OPT{undefined_as_null = true}); 592 | parse_option([skip_undefined | T], Opt) -> 593 | parse_option(T, Opt?OPT{skip_undefined = true}); 594 | parse_option([{map_unknown_value, F} | T], Opt) when is_function(F, 1); F =:= undefined -> 595 | parse_option(T, Opt?OPT{map_unknown_value = F}); 596 | parse_option(List, Opt) -> 597 | error(badarg, [List, Opt]). 598 | 599 | 600 | -spec local_offset() -> jsone:utc_offset_seconds(). 601 | local_offset() -> 602 | UTC = {{1970, 1, 2}, {0, 0, 0}}, 603 | Local = calendar:universal_time_to_local_time({{1970, 1, 2}, {0, 0, 0}}), 604 | calendar:datetime_to_gregorian_seconds(Local) - calendar:datetime_to_gregorian_seconds(UTC). 605 | 606 | 607 | -ifndef(TIME_MODULE). 608 | 609 | -define(TIME_MODULE, erlang). 610 | 611 | -endif. 612 | 613 | 614 | -spec local_offset_dst() -> jsone:utc_offset_seconds(). 615 | local_offset_dst() -> 616 | LocalDateTime = ?TIME_MODULE:localtime(), 617 | calendar:datetime_to_gregorian_seconds(LocalDateTime) - 618 | calendar:datetime_to_gregorian_seconds(?TIME_MODULE:localtime_to_universaltime(LocalDateTime)). 619 | -------------------------------------------------------------------------------- /src/jsone_inet.erl: -------------------------------------------------------------------------------- 1 | %%% @doc Utility functions for `inet' module 2 | %%% @private 3 | %%% @end 4 | %%% 5 | %%% Copyright (c) 2013-2021, Takeru Ohta 6 | %%% 7 | %%% The MIT License 8 | %%% 9 | %%% Permission is hereby granted, free of charge, to any person obtaining a copy 10 | %%% of this software and associated documentation files (the "Software"), to deal 11 | %%% in the Software without restriction, including without limitation the rights 12 | %%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | %%% copies of the Software, and to permit persons to whom the Software is 14 | %%% furnished to do so, subject to the following conditions: 15 | %%% 16 | %%% The above copyright notice and this permission notice shall be included in 17 | %%% all copies or substantial portions of the Software. 18 | %%% 19 | %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | %%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | %%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | %%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | %%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | %%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | %%% THE SOFTWARE. 26 | %%% 27 | %%%--------------------------------------------------------------------------------------- 28 | -module(jsone_inet). 29 | 30 | %%-------------------------------------------------------------------------------- 31 | %% Exported API 32 | %%-------------------------------------------------------------------------------- 33 | -export([ip_address_to_json_string/1]). 34 | 35 | %%-------------------------------------------------------------------------------- 36 | %% Macros 37 | %%-------------------------------------------------------------------------------- 38 | -define(IS_IPV4_RANGE(X), is_integer(X) andalso 0 =< X andalso X =< 255). 39 | -define(IS_IPV4(A, B, C, D), 40 | ?IS_IPV4_RANGE(A) andalso ?IS_IPV4_RANGE(B) andalso ?IS_IPV4_RANGE(C) andalso ?IS_IPV4_RANGE(D)). 41 | 42 | -define(IS_IPV6_RANGE(X), is_integer(X) andalso 0 =< X andalso X =< 65535). 43 | -define(IS_IPV6(A, B, C, D, E, F, G, H), 44 | ?IS_IPV6_RANGE(A) andalso ?IS_IPV6_RANGE(B) andalso ?IS_IPV6_RANGE(C) andalso ?IS_IPV6_RANGE(D) andalso 45 | ?IS_IPV6_RANGE(E) andalso ?IS_IPV6_RANGE(F) andalso ?IS_IPV6_RANGE(G) andalso ?IS_IPV6_RANGE(H)). 46 | 47 | %%-------------------------------------------------------------------------------- 48 | %% Exported Functions 49 | %%-------------------------------------------------------------------------------- 50 | 51 | 52 | %% @doc Convert an IP address into a text representation. 53 | %% 54 | %% Please refer to the doc of `jsone:ip_address_to_json_string/1' for the detail. 55 | -spec ip_address_to_json_string(inet:ip_address() | any()) -> {ok, jsone:json_string()} | error. 56 | ip_address_to_json_string({A, B, C, D}) when ?IS_IPV4(A, B, C, D) -> 57 | {ok, iolist_to_binary(io_lib:format("~p.~p.~p.~p", [A, B, C, D]))}; 58 | ip_address_to_json_string({A, B, C, D, E, F, G, H}) when ?IS_IPV6(A, B, C, D, E, F, G, H) -> 59 | Text = 60 | case {A, B, C, D, E, F} of 61 | {0, 0, 0, 0, 0, 0} -> 62 | %% IPv4-compatible address 63 | io_lib:format("::~p.~p.~p.~p", [G bsr 8, G band 16#ff, H bsr 8, H band 16#ff]); 64 | {0, 0, 0, 0, 0, 16#ffff} -> 65 | %% IPv4-mapped address 66 | io_lib:format("::ffff:~p.~p.~p.~p", [G bsr 8, G band 16#ff, H bsr 8, H band 16#ff]); 67 | {0, 0, 0, 0, 16#ffff, 0} -> 68 | %% IPv4-translated address 69 | io_lib:format("::ffff:0:~p.~p.~p.~p", [G bsr 8, G band 16#ff, H bsr 8, H band 16#ff]); 70 | {16#64, 16#ff9b, 0, 0, 0, 0} -> 71 | %% IPv4/IPv6 translation 72 | io_lib:format("64:ff9b::~p.~p.~p.~p", [G bsr 8, G band 16#ff, H bsr 8, H band 16#ff]); 73 | {16#64, 16#ff9b, 1, _, _, _} -> 74 | %% IPv4/IPv6 translation 75 | {Prefix, _} = format_ipv6([A, B, C, D, E, F], 0, 0), 76 | Last = lists:flatten(io_lib:format("~p.~p.~p.~p", [G bsr 8, G band 16#ff, H bsr 8, H band 16#ff])), 77 | string:join(Prefix ++ [Last], ":"); 78 | _ -> 79 | format_ipv6([A, B, C, D, E, F, G, H]) 80 | end, 81 | {ok, iolist_to_binary(Text)}; 82 | ip_address_to_json_string(_) -> 83 | error. 84 | 85 | 86 | %%-------------------------------------------------------------------------------- 87 | %% Internal Functions 88 | %%-------------------------------------------------------------------------------- 89 | -spec format_ipv6([0..65535]) -> string(). 90 | format_ipv6(Xs) -> 91 | case format_ipv6(Xs, 0, 0) of 92 | {Ys, shortening} -> 93 | [$: | string:join(Ys, ":")]; 94 | {Ys, _} -> 95 | Text = string:join(Ys, ":"), 96 | case lists:last(Text) of 97 | $: -> 98 | [Text | ":"]; 99 | _ -> 100 | Text 101 | end 102 | end. 103 | 104 | 105 | -spec format_ipv6([0..65535], non_neg_integer(), non_neg_integer()) -> 106 | {[string()], not_shortened | shortening | shortened}. 107 | format_ipv6([], _Zeros, _MaxZeros) -> 108 | {[], not_shortened}; 109 | format_ipv6([X | Xs], Zeros0, MaxZeros) -> 110 | Zeros1 = 111 | case X of 112 | 0 -> 113 | Zeros0 + 1; 114 | _ -> 115 | 0 116 | end, 117 | Shorten = Zeros1 > MaxZeros andalso Zeros1 > 1, 118 | case format_ipv6(Xs, Zeros1, max(Zeros1, MaxZeros)) of 119 | {Ys, not_shortened} -> 120 | case Shorten of 121 | true -> 122 | {["" | Ys], shortening}; 123 | false -> 124 | {[to_hex(X) | Ys], not_shortened} 125 | end; 126 | {Ys, shortening} when X =:= 0 -> 127 | {Ys, shortening}; 128 | {Ys, _} -> 129 | {[to_hex(X) | Ys], shortened} 130 | end. 131 | 132 | 133 | -spec to_hex(0..65535) -> string(). 134 | to_hex(N) -> 135 | string:lowercase(integer_to_list(N, 16)). 136 | -------------------------------------------------------------------------------- /test/jsone_decode_tests.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2013-2015, Takeru Ohta 2 | %% coding: latin-1 3 | -module(jsone_decode_tests). 4 | 5 | -include_lib("eunit/include/eunit.hrl"). 6 | 7 | -ifdef('NO_MAP_TYPE'). 8 | -define(MAP_OBJECT_TYPE, tuple). 9 | -define(OBJ0, {[]}). 10 | -define(OBJ1(K, V), {[{K, V}]}). 11 | -define(OBJ2(K1, V1, K2, V2), {[{K1, V1}, {K2, V2}]}). 12 | -define(OBJ2_DUP_KEY(K1, V1, K2, V2), ?OBJ2(K1, V1, K2, V2)). 13 | -else. 14 | -define(MAP_OBJECT_TYPE, map). 15 | -define(OBJ0, #{}). 16 | -define(OBJ1(K, V), #{K => V}). 17 | -define(OBJ2(K1, V1, K2, V2), #{K1 => V1, K2 => V2}). 18 | -define(OBJ2_DUP_KEY(K1, V1, _K2, _V2), #{K1 => V1}). % the first (leftmost) value is used 19 | -define(OBJ2_DUP_KEY_LAST(_K1, _V1, K2, V2), #{K2 => V2}). % the last value is used 20 | -endif. 21 | 22 | 23 | decode_test_() -> 24 | [ 25 | %% Symbols 26 | {"false", fun() -> ?assertEqual({ok, false, <<"">>}, jsone_decode:decode(<<"false">>)) end}, 27 | {"true", fun() -> ?assertEqual({ok, true, <<"">>}, jsone_decode:decode(<<"true">>)) end}, 28 | {"null", fun() -> ?assertEqual({ok, null, <<"">>}, jsone_decode:decode(<<"null">>)) end}, 29 | 30 | %% Numbers: Integer 31 | {"positive integer", fun() -> ?assertEqual({ok, 1, <<"">>}, jsone_decode:decode(<<"1">>)) end}, 32 | {"zero", fun() -> ?assertEqual({ok, 0, <<"">>}, jsone_decode:decode(<<"0">>)) end}, 33 | {"negative integer", fun() -> ?assertEqual({ok, -1, <<"">>}, jsone_decode:decode(<<"-1">>)) end}, 34 | {"large integer (no limit on size)", 35 | fun() -> 36 | ?assertEqual({ok, 111111111111111111111111111111111111111111111111111111111111111111111111111111, <<"">>}, 37 | jsone_decode:decode(<<"111111111111111111111111111111111111111111111111111111111111111111111111111111">>)) 38 | end}, 39 | {"integer with leading zero (interpreted as zero and remaining binary)", 40 | fun() -> 41 | ?assertEqual({ok, 0, <<"0">>}, jsone_decode:decode(<<"00">>)), 42 | ?assertEqual({ok, 0, <<"1">>}, jsone_decode:decode(<<"01">>)), 43 | ?assertEqual({ok, 0, <<"1">>}, jsone_decode:decode(<<"-01">>)) 44 | end}, 45 | {"integer can't begin with an explicit plus sign", 46 | fun() -> ?assertMatch({error, {badarg, _}}, jsone_decode:decode(<<"+1">>)) 47 | end}, 48 | 49 | %% Numbers: Floats 50 | {"float: decimal notation", fun() -> ?assertEqual({ok, 1.23, <<"">>}, jsone_decode:decode(<<"1.23">>)) end}, 51 | {"float: exponential notation", 52 | fun() -> 53 | ?assertEqual({ok, 12.345, <<"">>}, jsone_decode:decode(<<"12345e-3">>)), % lower case 'e' 54 | ?assertEqual({ok, 12.345, <<"">>}, jsone_decode:decode(<<"12345E-3">>)), % upper case 'E' 55 | ?assertEqual({ok, 12.345, <<"">>}, jsone_decode:decode(<<"12345.0e-3">>)), 56 | ?assertEqual({ok, 12.345, <<"">>}, jsone_decode:decode(<<"0.12345E2">>)), 57 | ?assertEqual({ok, 12.345, <<"">>}, jsone_decode:decode(<<"0.12345e+2">>)), % exponent part can begin with plus sign 58 | ?assertEqual({ok, 12.345, <<"">>}, jsone_decode:decode(<<"0.12345E+2">>)), 59 | ?assertEqual({ok, -12.345, <<"">>}, jsone_decode:decode(<<"-0.012345e3">>)), 60 | ?assertEqual({ok, 123.0, <<"">>}, jsone_decode:decode(<<"1.23000000000000000000e+02">>)) 61 | end}, 62 | {"float: invalid format", 63 | fun() -> 64 | ?assertMatch({error, {badarg, _}}, jsone_decode:decode(<<".123">>)), % omitted integer part 65 | ?assertMatch({error, {badarg, _}}, jsone_decode:decode(<<"0.">>)), % omitted fraction part: EOS 66 | ?assertMatch({error, {badarg, _}}, jsone_decode:decode(<<"0.e+3">>)), % omitted fraction part: with exponent part 67 | ?assertMatch({error, {badarg, _}}, jsone_decode:decode(<<"0.1e">>)), % incomplete fraction part 68 | ?assertMatch({error, {badarg, _}}, jsone_decode:decode(<<"0.1e-">>)), % incomplete fraction part 69 | ?assertMatch({error, {badarg, _}}, jsone_decode:decode(<<"0.1ee-1">>)), % duplicated 'e' 70 | ?assertMatch({error, {badarg, _}}, jsone_decode:decode(<<"0.1e--1">>)), % duplicated sign 71 | ?assertMatch({error, {badarg, _}}, jsone_decode:decode(<<"1e999">>)), % exponent out of range 72 | ?assertMatch({error, {badarg, _}}, jsone_decode:decode(<<"0.1e999">>)), % exponent out of range 73 | ?assertMatch({error, {badarg, _}}, jsone_decode:decode(<<"100000000000000000000000000000000000e300">>)), % product out of range 74 | ?assertEqual({ok, 0.1, <<".2">>}, jsone_decode:decode(<<"0.1.2">>)) % duplicated '.': interpreted as individual tokens 75 | end}, 76 | 77 | %% Strings 78 | {"simple string", fun() -> ?assertEqual({ok, <<"abc">>, <<"">>}, jsone_decode:decode(<<"\"abc\"">>)) end}, 79 | {"string: escaped characters", 80 | fun() -> 81 | Input = list_to_binary([$", [ [$\\, C] || C <- [$", $/, $\\, $b, $f, $n, $r, $t] ], $"]), 82 | Expected = <<"\"\/\\\b\f\n\r\t">>, 83 | ?assertEqual({ok, Expected, <<"">>}, jsone_decode:decode(Input)) 84 | end}, 85 | {"string: escaped Unicode characters", 86 | fun() -> 87 | %% japanese 88 | Input1 = <<"\"\\u3042\\u3044\\u3046\\u3048\\u304A\"">>, 89 | Expected1 = <<"あいうえお">>, % assumed that the encoding of this file is UTF-8 90 | ?assertEqual({ok, Expected1, <<"">>}, jsone_decode:decode(Input1)), 91 | 92 | %% ascii 93 | Input2 = <<"\"\\u0061\\u0062\\u0063\"">>, 94 | Expected2 = <<"abc">>, 95 | ?assertEqual({ok, Expected2, <<"">>}, jsone_decode:decode(Input2)), 96 | 97 | %% other multi-byte characters 98 | Input3 = <<"\"\\u06DD\\u06DE\\u10AE\\u10AF\"">>, 99 | Expected3 = <<"۝۞ႮႯ">>, 100 | ?assertEqual({ok, Expected3, <<"">>}, jsone_decode:decode(Input3)), 101 | 102 | %% mixture of ascii and japanese characters 103 | Input4 = <<"\"a\\u30421\\u3044bb\\u304622\\u3048ccc\\u304A333\"">>, 104 | Expected4 = <<"aあ1いbbう22えcccお333">>, % assumed that the encoding of this file is UTF-8 105 | ?assertEqual({ok, Expected4, <<"">>}, jsone_decode:decode(Input4)) 106 | end}, 107 | {"string: surrogate pairs", 108 | fun() -> 109 | Input = <<"\"\\ud848\\udc49\\ud848\\udc9a\\ud848\\udcfc\"">>, 110 | Expected = <<"𢁉𢂚𢃼">>, 111 | ?assertEqual({ok, Expected, <<"">>}, jsone_decode:decode(Input)) 112 | end}, 113 | {"string: control characters", 114 | fun() -> 115 | Ctrls = lists:seq(0, 16#1f), 116 | lists:foreach(fun(C) -> 117 | %% Control characters are unacceptable 118 | ?assertMatch({error, {badarg, _}}, jsone_decode:decode(<<$", C, $">>)) 119 | end, 120 | Ctrls), 121 | lists:foreach(fun(C) -> 122 | %% `allow_ctrl_chars' option allows strings which contain unescaped control characters 123 | ?assertEqual({ok, <>, <<"">>}, 124 | jsone_decode:decode(<<$", C, $">>, [{allow_ctrl_chars, true}])) 125 | end, 126 | Ctrls) 127 | end}, 128 | {"string: invalid escape characters", 129 | fun() -> 130 | ?assertMatch({error, {badarg, _}}, jsone_decode:decode(<<"\"\\z\"">>)), % '\z' is undefined 131 | ?assertMatch({error, {badarg, _}}, jsone_decode:decode(<<"\"\\uab\"">>)), % too few hex characters 132 | ?assertMatch({error, {badarg, _}}, jsone_decode:decode(<<"\"\\ud848\"">>)), % high(first) surrogate only 133 | ?assertMatch({error, {badarg, _}}, jsone_decode:decode(<<"\"\\udc49\"">>)), % low(second) surrogate only 134 | ?assertMatch({error, {badarg, _}}, jsone_decode:decode(<<"\"\\ud848\\u0061\"">>)), % missing low(second) surrogate 135 | ?assertMatch({error, {badarg, _}}, jsone_decode:decode(<<"\"\\udf0u\"">>)), 136 | ?assertMatch({error, {badarg, _}}, jsone_decode:decode(<<"\"\\ud848\\udf0u\"">>)), 137 | ?assertMatch({error, {badarg, _}}, jsone_decode:decode(<<"\"\\u-3351\"">>)) 138 | end}, 139 | 140 | %% Arrays 141 | {"simple array", 142 | fun() -> 143 | Input = <<"[1,2,\"abc\",null]">>, 144 | Expected = [1, 2, <<"abc">>, null], 145 | ?assertEqual({ok, Expected, <<"">>}, jsone_decode:decode(Input)) 146 | end}, 147 | {"array: contains whitespaces", 148 | fun() -> 149 | Input = <<"[ 1,\t2, \n \"abc\",\r null]">>, 150 | Expected = [1, 2, <<"abc">>, null], 151 | ?assertEqual({ok, Expected, <<"">>}, jsone_decode:decode(Input)) 152 | end}, 153 | {"empty array", 154 | fun() -> 155 | ?assertEqual({ok, [], <<"">>}, jsone_decode:decode(<<"[]">>)), 156 | ?assertEqual({ok, [], <<"">>}, jsone_decode:decode(<<"[ \t\r\n]">>)) 157 | end}, 158 | {"array: trailing comma is disallowed", 159 | fun() -> 160 | Input = <<"[1, 2, \"abc\", null, ]">>, 161 | ?assertMatch({error, {badarg, _}}, jsone_decode:decode(Input)) 162 | end}, 163 | {"array: missing comma", 164 | fun() -> 165 | Input = <<"[1 2, \"abc\", null]">>, % a missing comma between '1' and '2' 166 | ?assertMatch({error, {badarg, _}}, jsone_decode:decode(Input)) 167 | end}, 168 | {"array: missing closing bracket", 169 | fun() -> 170 | Input = <<"[1, 2, \"abc\", null">>, 171 | ?assertMatch({error, {badarg, _}}, jsone_decode:decode(Input)) 172 | end}, 173 | 174 | %% Objects 175 | {"simple object", 176 | fun() -> 177 | Input = <<"{\"1\":2,\"key\":\"value\"}">>, 178 | Expected = ?OBJ2(<<"1">>, 2, <<"key">>, <<"value">>), 179 | ?assertEqual({ok, Expected, <<"">>}, jsone_decode:decode(Input)), % `map' is the default format 180 | ?assertEqual({ok, Expected, <<"">>}, jsone_decode:decode(Input, [{object_format, ?MAP_OBJECT_TYPE}])) 181 | end}, 182 | {"simple object: tuple or proplist", 183 | fun() -> 184 | Input = <<"{\"1\":2,\"key\":\"value\"}">>, 185 | Expected = {[{<<"1">>, 2}, {<<"key">>, <<"value">>}]}, 186 | ?assertEqual({ok, Expected, <<"">>}, jsone_decode:decode(Input, [{object_format, tuple}])), 187 | ?assertEqual({ok, element(1, Expected), <<"">>}, jsone_decode:decode(Input, [{object_format, proplist}])) 188 | end}, 189 | {"object: contains whitespaces", 190 | fun() -> 191 | Input = <<"{ \"1\" :\t 2,\n\r\"key\" : \n \"value\"}">>, 192 | Expected = ?OBJ2(<<"1">>, 2, <<"key">>, <<"value">>), 193 | ?assertEqual({ok, Expected, <<"">>}, jsone_decode:decode(Input)) 194 | end}, 195 | {"empty object", 196 | fun() -> 197 | ?assertEqual({ok, ?OBJ0, <<"">>}, jsone_decode:decode(<<"{}">>)), 198 | ?assertEqual({ok, ?OBJ0, <<"">>}, jsone_decode:decode(<<"{ \t\r\n}">>)), 199 | ?assertEqual({ok, {[]}, <<"">>}, jsone_decode:decode(<<"{}">>, [{object_format, tuple}])), 200 | ?assertEqual({ok, [{}], <<"">>}, jsone_decode:decode(<<"{}">>, [{object_format, proplist}])) 201 | end}, 202 | {"empty object: map", 203 | fun() -> 204 | ?assertEqual({ok, ?OBJ0, <<"">>}, jsone_decode:decode(<<"{}">>, [{object_format, ?MAP_OBJECT_TYPE}])) 205 | end}, 206 | {"duplicated members: map", 207 | fun() -> 208 | Input = <<"{\"1\":\"first\",\"1\":\"second\"}">>, 209 | Expected = ?OBJ2_DUP_KEY(<<"1">>, <<"first">>, <<"1">>, <<"second">>), 210 | ?assertEqual({ok, Expected, <<"">>}, jsone_decode:decode(Input, [{object_format, ?MAP_OBJECT_TYPE}])) 211 | end}, 212 | {"duplicated members last: map", 213 | fun() -> 214 | Input = <<"{\"1\":\"first\",\"1\":\"second\"}">>, 215 | Expected = ?OBJ2_DUP_KEY_LAST(<<"1">>, <<"first">>, <<"1">>, <<"second">>), 216 | ?assertEqual({ok, Expected, <<"">>}, 217 | jsone_decode:decode(Input, [{object_format, ?MAP_OBJECT_TYPE}, {duplicate_map_keys, last}])) 218 | end}, 219 | {"object: trailing comma is disallowed", 220 | fun() -> 221 | Input = <<"{\"1\":2, \"key\":\"value\", }">>, 222 | io:format("~p\n", [catch jsone_decode:decode(Input)]), 223 | ?assertMatch({error, {badarg, _}}, jsone_decode:decode(Input, [{object_format, tuple}])) 224 | end}, 225 | {"object: missing comma", 226 | fun() -> 227 | Input = <<"{\"1\":2 \"key\":\"value\"}">>, 228 | ?assertMatch({error, {badarg, _}}, jsone_decode:decode(Input)) 229 | end}, 230 | {"object: missing field key", 231 | fun() -> 232 | Input = <<"{:2, \"key\":\"value\"}">>, 233 | ?assertMatch({error, {badarg, _}}, jsone_decode:decode(Input)) 234 | end}, 235 | {"object: non string key", 236 | fun() -> 237 | Input = <<"{1:2, \"key\":\"value\"}">>, 238 | ?assertMatch({error, {badarg, _}}, jsone_decode:decode(Input)) 239 | end}, 240 | {"object: missing field value", 241 | fun() -> 242 | Input = <<"{\"1\", \"key\":\"value\"}">>, 243 | ?assertMatch({error, {badarg, _}}, jsone_decode:decode(Input)) 244 | end}, 245 | {"object: missing closing brace", 246 | fun() -> 247 | Input = <<"{\"1\":2 \"key\":\"value\"">>, 248 | ?assertMatch({error, {badarg, _}}, jsone_decode:decode(Input)) 249 | end}, 250 | {"atom keys", 251 | fun() -> 252 | KeyOpt = fun(Keys) -> [{keys, Keys}, {object_format, proplist}] end, 253 | Input = <<"{\"foo\":\"ok\"}">>, 254 | ?assertEqual([{<<"foo">>, <<"ok">>}], jsone:decode(Input, KeyOpt(binary))), 255 | ?assertEqual([{foo, <<"ok">>}], jsone:decode(Input, KeyOpt(atom))), 256 | ?assertEqual([{foo, <<"ok">>}], jsone:decode(Input, KeyOpt(existing_atom))), 257 | ?assertError(badarg, jsone:decode(<<"{\"@#$%^!\":\"ok\"}">>, KeyOpt(existing_atom))), 258 | ?assertEqual([{foo, <<"ok">>}], jsone:decode(Input, KeyOpt(attempt_atom))), 259 | ?assertEqual([{<<"@#$%^!">>, <<"ok">>}], jsone:decode(<<"{\"@#$%^!\":\"ok\"}">>, KeyOpt(attempt_atom))), 260 | Value = integer_to_binary(1234), 261 | % do not make atom in test code 262 | [{Atom, <<"ok">>}] = jsone:decode(<<"{\"", Value/binary, "\":\"ok\"}">>, KeyOpt(atom)), 263 | ?assertEqual(Value, atom_to_binary(Atom, latin1)) 264 | end}, 265 | {"garbage remainings chars", 266 | fun() -> 267 | ?assertError(badarg, jsone:decode(<<"1@">>)), 268 | ?assertEqual(1, jsone:decode(<<"1 \n\t\r ">>)) % Whitespaces are OK 269 | end}, 270 | 271 | %% Others 272 | {"compound data", 273 | fun() -> 274 | Input = <<" [true, {\"1\" : 2, \"array\":[[[[1]]], {\"ab\":\"cd\"}, false]}, null] ">>, 275 | Expected = [true, ?OBJ2(<<"1">>, 2, <<"array">>, [[[[1]]], ?OBJ1(<<"ab">>, <<"cd">>), false]), null], 276 | ?assertEqual({ok, Expected, <<" ">>}, jsone_decode:decode(Input)) 277 | end}, 278 | {"undefined_as_null option", 279 | fun() -> 280 | ?assertEqual({ok, undefined, <<>>}, jsone_decode:decode(<<"null">>, [undefined_as_null])), % OK 281 | ?assertEqual({ok, null, <<>>}, jsone_decode:decode(<<"null">>, [])) % OK 282 | end}, 283 | {"Invalid UTF-8 characters", 284 | fun() -> 285 | Input = <<123, 34, 105, 100, 34, 58, 34, 190, 72, 94, 90, 253, 121, 94, 71, 73, 68, 91, 122, 211, 253, 32, 286 | 94, 86, 67, 163, 253, 230, 34, 125>>, 287 | ?assertMatch({ok, _, _}, jsone:try_decode(Input)), 288 | ?assertMatch({error, {badarg, _}}, jsone:try_decode(Input, [reject_invalid_utf8])) 289 | end}]. 290 | -------------------------------------------------------------------------------- /test/jsone_encode_tests.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2013-2014, Takeru Ohta 2 | %% coding: latin-1 3 | -module(jsone_encode_tests). 4 | 5 | -include_lib("eunit/include/eunit.hrl"). 6 | 7 | -ifdef('NO_MAP_TYPE'). 8 | -define(OBJ0, {[]}). 9 | -define(OBJ1(K, V), {[{K, V}]}). 10 | -define(OBJ2(K1, V1, K2, V2), {[{K1, V1}, {K2, V2}]}). 11 | -define(OBJECT_FROM_LIST(List), List). 12 | -else. 13 | -define(OBJ0, #{}). 14 | -define(OBJ1(K, V), #{K => V}). 15 | -define(OBJ2(K1, V1, K2, V2), #{K1 => V1, K2 => V2}). 16 | -define(OBJECT_FROM_LIST(List), maps:from_list(List)). 17 | -endif. 18 | 19 | 20 | encode_test_() -> 21 | [ 22 | %% Symbols 23 | {"false", fun() -> ?assertEqual({ok, <<"false">>}, jsone_encode:encode(false)) end}, 24 | {"true", fun() -> ?assertEqual({ok, <<"true">>}, jsone_encode:encode(true)) end}, 25 | {"null", fun() -> ?assertEqual({ok, <<"null">>}, jsone_encode:encode(null)) end}, 26 | 27 | %% Numbers: Inline json term 28 | {"json", 29 | fun() -> 30 | ?assertEqual({ok, <<"{\"foo\":[1,2,3],\"bar\":\"", 195, 169, "ok\"}">>}, 31 | jsone_encode:encode(?OBJ2(foo, 32 | {{json, ["[" | [$1, ",2", <<",3]">>]]}}, 33 | <<"bar">>, 34 | {{json_utf8, [$", 233, "ok", $"]}}))), 35 | ?assertEqual({ok, <<"{\"foo\":[1,2,3],\"bar\":\"", 233, "ok\"}">>}, 36 | jsone_encode:encode(?OBJ2(foo, 37 | {{json, ["[" | [$1, ",2", <<",3]">>]]}}, 38 | <<"bar">>, 39 | {{json, [$", 233, "ok", $"]}}))), 40 | ?assertEqual({ok, <<"{\"json\":\"[1,2,3]\"}">>}, jsone_encode:encode([{json, <<"[1,2,3]">>}])), 41 | ?assertEqual({ok, <<"[[1,2,3]]">>}, jsone_encode:encode([{{json, <<"[1,2,3]">>}}])), 42 | 43 | %% Errors 44 | ?assertMatch({error, {{invalid_json_utf8, _, _}, _}}, jsone_encode:encode({{json_utf8, <<200, 83, 1>>}})), 45 | ?assertMatch({error, {{invalid_json_utf8, _, _}, _}}, jsone_encode:encode({{json_utf8, <<"abc", 192>>}})) 46 | end}, 47 | %% Numbers: Integer 48 | {"zero", fun() -> ?assertEqual({ok, <<"0">>}, jsone_encode:encode(0)) end}, 49 | {"positive integer", fun() -> ?assertEqual({ok, <<"1">>}, jsone_encode:encode(1)) end}, 50 | {"negative integer", fun() -> ?assertEqual({ok, <<"-1">>}, jsone_encode:encode(-1)) end}, 51 | {"large number", 52 | fun() -> 53 | ?assertEqual({ok, <<"11111111111111111111111111111111111111111111111111111111111111111111111">>}, 54 | jsone_encode:encode(11111111111111111111111111111111111111111111111111111111111111111111111)) 55 | end}, 56 | 57 | %% Numbers: Float", 58 | {"float", 59 | fun() -> 60 | Input = 1.234, 61 | ?assertMatch({ok, _}, jsone_encode:encode(Input)), 62 | ?assertEqual(Input, binary_to_float(element(2, jsone_encode:encode(Input)))) 63 | end}, 64 | {"float_format option", 65 | fun() -> 66 | Input = 1.23, 67 | ?assertEqual({ok, <<"1.22999999999999998224e+00">>}, jsone_encode:encode(Input)), 68 | ?assertEqual({ok, <<"1.2300e+00">>}, jsone_encode:encode(Input, [{float_format, [{scientific, 4}]}])), 69 | ?assertEqual({ok, <<"1.2e+00">>}, jsone_encode:encode(Input, [{float_format, [{scientific, 1}]}])), 70 | ?assertEqual({ok, <<"1.2300">>}, jsone_encode:encode(Input, [{float_format, [{decimals, 4}]}])), 71 | ?assertEqual({ok, <<"1.23">>}, jsone_encode:encode(Input, [{float_format, [{decimals, 4}, compact]}])) 72 | end}, 73 | 74 | %% Strings 75 | {"simple string", fun() -> ?assertEqual({ok, <<"\"abc\"">>}, jsone_encode:encode(<<"abc">>)) end}, 76 | {"atom is regarded as string", fun() -> ?assertEqual({ok, <<"\"abc\"">>}, jsone_encode:encode(abc)) end}, 77 | {"string: contains escaped characters", 78 | fun() -> 79 | Input = <<"\"\/\\\b\f\n\r\t">>, 80 | Expected = list_to_binary([$", [ [$\\, C] || C <- [$", $/, $\\, $b, $f, $n, $r, $t] ], $"]), 81 | ?assertEqual({ok, Expected}, jsone_encode:encode(Input)), 82 | ?assertEqual({ok, Expected}, jsone_encode:encode(Input, [native_utf8])) 83 | end}, 84 | {"string: contains forward slashes", 85 | fun() -> 86 | Input = <<"1/2">>, 87 | ?assertEqual({ok, <<"\"1\\/2\"">>}, jsone_encode:encode(Input)), 88 | ?assertEqual({ok, <<"\"1/2\"">>}, jsone_encode:encode(Input, [native_forward_slash])) 89 | end}, 90 | {"string: contains control characters", 91 | fun() -> 92 | Ctrls = lists:seq(16#00, 16#1F) -- [$\b, $\f, $\n, $\r, $\t], 93 | Input = list_to_binary(Ctrls), 94 | Expected = list_to_binary([$", [ io_lib:format("\\u00~2.16.0b", [C]) || C <- Ctrls ], $"]), 95 | ?assertEqual({ok, Expected}, jsone_encode:encode(Input)), 96 | ?assertEqual({ok, Expected}, jsone_encode:encode(Input, [native_utf8])) 97 | end}, 98 | {"string: contains multi-byte (UTF-8 encoded) characters", 99 | fun() -> 100 | %% japanese 101 | Input1 = <<"あいうえお">>, % assumed that the encoding of this file is UTF-8 102 | Expected1 = <<"\"\\u3042\\u3044\\u3046\\u3048\\u304a\"">>, 103 | ?assertEqual({ok, Expected1}, jsone_encode:encode(Input1)), 104 | Expected12 = <<$", Input1/binary, $">>, 105 | ?assertEqual({ok, Expected12}, jsone_encode:encode(Input1, [native_utf8])), 106 | 107 | %% other multi-byte characters 108 | Input2 = <<"۝۞ႮႯ">>, 109 | Expected2 = <<"\"\\u06dd\\u06de\\u10ae\\u10af\"">>, 110 | ?assertEqual({ok, Expected2}, jsone_encode:encode(Input2)), 111 | Expected22 = <<$", Input2/binary, $">>, 112 | ?assertEqual({ok, Expected22}, jsone_encode:encode(Input2, [native_utf8])) 113 | end}, 114 | {"string: contains surrogate pairs", 115 | fun() -> 116 | Input = <<"𢁉𢂚𢃼">>, 117 | Expected = <<"\"\\ud848\\udc49\\ud848\\udc9a\\ud848\\udcfc\"">>, 118 | ?assertEqual({ok, Expected}, jsone_encode:encode(Input)) 119 | end}, 120 | 121 | %% Strings variant: Datetimes 122 | {"datetime: iso8601: utc", 123 | fun() -> 124 | ?assertEqual({ok, <<"\"2015-06-25T14:57:25Z\"">>}, jsone_encode:encode({{2015, 6, 25}, {14, 57, 25}})), 125 | ?assertEqual({ok, <<"\"2015-06-25T14:57:25Z\"">>}, 126 | jsone_encode:encode({{2015, 6, 25}, {14, 57, 25}}, [{datetime_format, iso8601}])), 127 | ?assertEqual({ok, <<"\"2015-06-25T14:57:25Z\"">>}, 128 | jsone_encode:encode({{2015, 6, 25}, {14, 57, 25}}, [{datetime_format, {iso8601, utc}}])) 129 | end}, 130 | {"datetime: iso8601: local", 131 | fun() -> 132 | {ok, Json} = jsone_encode:encode({{2015, 6, 25}, {14, 57, 25}}, [{datetime_format, {iso8601, local}}]), 133 | 134 | UTC = {{1970, 1, 2}, {0, 0, 0}}, 135 | Local = calendar:universal_time_to_local_time({{1970, 1, 2}, {0, 0, 0}}), 136 | case UTC =:= Local of 137 | false -> 138 | ?assertMatch(<<"\"2015-06-25T14:57:25", _:6/binary, "\"">>, Json); 139 | true -> 140 | ?assertMatch(<<"\"2015-06-25T14:57:25Z\"">>, Json) 141 | end 142 | end}, 143 | {"datetime: iso8601: local with daylight saving variable zone - summer time (2h offset)", 144 | fun() -> 145 | test_time_module:set_localtime({{2024, 9, 15}, {11, 00, 00}}), 146 | test_time_module:mock_localtime_to_universaltime(fun(_) -> {{2024, 9, 15}, {9, 00, 00}} end), 147 | 148 | {ok, Json} = jsone_encode:encode({{2015, 6, 25}, {14, 57, 25}}, [{datetime_format, {iso8601, local_dst}}]), 149 | ?assertMatch(<<"\"2015-06-25T14:57:25+02:00\"">>, Json) 150 | end}, 151 | {"datetime: iso8601: local with daylight saving variable zone - winter time (1h offset)", 152 | fun() -> 153 | test_time_module:set_localtime({{2024, 12, 15}, {11, 00, 00}}), 154 | test_time_module:mock_localtime_to_universaltime(fun(_) -> {{2024, 12, 15}, {10, 00, 00}} end), 155 | 156 | {ok, Json} = jsone_encode:encode({{2015, 6, 25}, {14, 57, 25}}, [{datetime_format, {iso8601, local_dst}}]), 157 | ?assertMatch(<<"\"2015-06-25T14:57:25+01:00\"">>, Json) 158 | end}, 159 | {"datetime: iso8601: timezone", 160 | fun() -> 161 | ?assertEqual({ok, <<"\"2015-06-25T14:57:25Z\"">>}, 162 | jsone_encode:encode({{2015, 6, 25}, {14, 57, 25}}, [{datetime_format, {iso8601, 0}}])), 163 | ?assertEqual({ok, <<"\"2015-06-25T14:57:25+00:01\"">>}, 164 | jsone_encode:encode({{2015, 6, 25}, {14, 57, 25}}, [{datetime_format, {iso8601, 60}}])), 165 | ?assertEqual({ok, <<"\"2015-06-25T14:57:25-00:01\"">>}, 166 | jsone_encode:encode({{2015, 6, 25}, {14, 57, 25}}, [{datetime_format, {iso8601, -60}}])) 167 | end}, 168 | {"datetime as head of array", 169 | ?_assertEqual({ok, <<"[\"2015-06-25T14:57:25Z\"]">>}, jsone_encode:encode([{{2015, 6, 25}, {14, 57, 25}}]))}, 170 | 171 | {"datetime: iso8601: with fractions of seconds", 172 | fun() -> 173 | ?assertEqual({ok, <<"\"2015-06-25T14:57:25.325Z\"">>}, 174 | jsone_encode:encode({{2015, 6, 25}, {14, 57, 25.3245}})), 175 | ?assertEqual({ok, <<"\"2015-06-25T14:57:05.320Z\"">>}, 176 | jsone_encode:encode({{2015, 6, 25}, {14, 57, 5.32}})) 177 | end}, 178 | 179 | %% Arrays 180 | {"simple array", 181 | fun() -> 182 | Input = [1, 2, 3], 183 | Expected = <<"[1,2,3]">>, 184 | ?assertEqual({ok, Expected}, jsone_encode:encode(Input)) 185 | end}, 186 | {"empty array", 187 | fun() -> 188 | Input = [], 189 | Expected = <<"[]">>, 190 | ?assertEqual({ok, Expected}, jsone_encode:encode(Input)) 191 | end}, 192 | 193 | %% Objects 194 | {"simple object", 195 | fun() -> 196 | Input1 = {[{<<"key">>, <<"value">>}, {<<"1">>, 2}]}, 197 | Input2 = [{<<"key">>, <<"value">>}, {<<"1">>, 2}], 198 | Expected = <<"{\"key\":\"value\",\"1\":2}">>, 199 | ?assertEqual({ok, Expected}, jsone_encode:encode(Input1)), 200 | ?assertEqual({ok, Expected}, jsone_encode:encode(Input2)) 201 | end}, 202 | {"empty object", 203 | fun() -> 204 | Input1 = {[]}, 205 | Input2 = [{}], 206 | Expected = <<"{}">>, 207 | ?assertEqual({ok, Expected}, jsone_encode:encode(Input1)), 208 | ?assertEqual({ok, Expected}, jsone_encode:encode(Input2)) 209 | end}, 210 | {"simple object: map", 211 | fun() -> 212 | Input = ?OBJ2(<<"1">>, 2, <<"key">>, <<"value">>), 213 | Expected = <<"{\"1\":2,\"key\":\"value\"}">>, 214 | ?assertEqual({ok, Expected}, jsone_encode:encode(Input)) 215 | end}, 216 | {"empty object: map", 217 | fun() -> 218 | Input = ?OBJ0, 219 | Expected = <<"{}">>, 220 | ?assertEqual({ok, Expected}, jsone_encode:encode(Input)) 221 | end}, 222 | {"atom key is allowed", 223 | fun() -> 224 | Expected = <<"{\"key\":2}">>, 225 | ?assertEqual({ok, Expected}, jsone_encode:encode({[{key, 2}]})) 226 | end}, 227 | {"object_key_type option", 228 | fun() -> 229 | %% key: atom 230 | ?assertEqual({ok, <<"{\"a\":2}">>}, jsone_encode:encode(?OBJ1(a, 2), [{object_key_type, string}])), % OK 231 | ?assertEqual({ok, <<"{\"a\":2}">>}, jsone_encode:encode(?OBJ1(a, 2), [{object_key_type, scalar}])), % OK 232 | ?assertEqual({ok, <<"{\"a\":2}">>}, jsone_encode:encode(?OBJ1(a, 2), [{object_key_type, value}])), % OK 233 | 234 | %% key: number 235 | ?assertMatch({error, {badarg, _}}, jsone_encode:encode(?OBJ1(1, 2), [{object_key_type, string}])), % NG 236 | ?assertEqual({ok, <<"{\"1\":2}">>}, jsone_encode:encode(?OBJ1(1, 2), [{object_key_type, scalar}])), % OK 237 | ?assertEqual({ok, <<"{\"1\":2}">>}, jsone_encode:encode(?OBJ1(1, 2), [{object_key_type, value}])), % OK 238 | 239 | %% key: datetime 240 | ?assertMatch({error, {badarg, _}}, 241 | jsone_encode:encode(?OBJ1({{2000, 1, 1}, {0, 0, 0}}, 2), [{object_key_type, string}])), % NG 242 | ?assertEqual({ok, <<"{\"2000-01-01T00:00:00Z\":2}">>}, 243 | jsone_encode:encode(?OBJ1({{2000, 1, 1}, {0, 0, 0}}, 2), [{object_key_type, scalar}])), % OK 244 | ?assertEqual({ok, <<"{\"2000-01-01T00:00:00Z\":2}">>}, 245 | jsone_encode:encode(?OBJ1({{2000, 1, 1}, {0, 0, 0}}, 2), [{object_key_type, value}])), % OK 246 | 247 | %% key: array 248 | ?assertMatch({error, {badarg, _}}, jsone_encode:encode(?OBJ1([1], 2), [{object_key_type, string}])), % NG 249 | ?assertMatch({error, {badarg, _}}, jsone_encode:encode(?OBJ1([1], 2), [{object_key_type, scalar}])), % NG 250 | ?assertEqual({ok, <<"{\"[1]\":2}">>}, jsone_encode:encode(?OBJ1([1], 2), [{object_key_type, value}])), % OK 251 | 252 | %% key: object 253 | ?assertMatch({error, {badarg, _}}, jsone_encode:encode(?OBJ1(?OBJ0, 2), [{object_key_type, string}])), % NG 254 | ?assertMatch({error, {badarg, _}}, jsone_encode:encode(?OBJ1(?OBJ0, 2), [{object_key_type, scalar}])), % NG 255 | ?assertEqual({ok, <<"{\"{}\":2}">>}, jsone_encode:encode(?OBJ1(?OBJ0, 2), [{object_key_type, value}])) % OK 256 | end}, 257 | {"non binary object member key is disallowed", 258 | fun() -> 259 | ?assertMatch({error, {badarg, _}}, jsone_encode:encode({[{1, 2}]})), 260 | ?assertMatch({error, {badarg, _}}, jsone_encode:encode({[{"1", 2}]})) 261 | end}, 262 | {"undefined_as_null option", 263 | fun() -> 264 | ?assertEqual({ok, <<"null">>}, jsone_encode:encode(undefined, [undefined_as_null])), % OK 265 | ?assertEqual({ok, <<"\"undefined\"">>}, jsone_encode:encode(undefined, [])) % OK 266 | end}, 267 | {"skip_undefined option", 268 | fun() -> 269 | Object = #{ 270 | <<"1">> => undefined, 271 | <<"2">> => 3, 272 | <<"3">> => undefined 273 | }, 274 | ?assertEqual({ok, <<"{\"1\":null,\"2\":3,\"3\":null}">>}, 275 | jsone_encode:encode(Object, [undefined_as_null])), 276 | ?assertEqual({ok, <<"{\"2\":3}">>}, jsone_encode:encode(Object, [skip_undefined])) 277 | end}, 278 | 279 | %% Pretty Print 280 | {"space", 281 | fun() -> 282 | ?assertEqual({ok, <<"[]">>}, jsone_encode:encode([], [{space, 1}])), 283 | ?assertEqual({ok, <<"[1, 2, 3]">>}, jsone_encode:encode([1, 2, 3], [{space, 1}])), 284 | ?assertEqual({ok, <<"[1, 2, 3]">>}, jsone_encode:encode([1, 2, 3], [{space, 2}])), 285 | ?assertEqual({ok, <<"{}">>}, jsone_encode:encode(?OBJ0, [{space, 1}])), 286 | ?assertEqual({ok, <<"{\"a\": 1, \"b\": 2}">>}, jsone_encode:encode(?OBJ2(a, 1, b, 2), [{space, 1}])), 287 | ?assertEqual({ok, <<"{\"a\": 1, \"b\": 2}">>}, jsone_encode:encode(?OBJ2(a, 1, b, 2), [{space, 2}])) 288 | end}, 289 | {"indent", 290 | fun() -> 291 | ?assertEqual({ok, <<"[]">>}, jsone_encode:encode([], [{indent, 1}])), 292 | ?assertEqual({ok, <<"[\n 1,\n 2,\n 3\n]">>}, jsone_encode:encode([1, 2, 3], [{indent, 1}])), 293 | ?assertEqual({ok, <<"[\n 1,\n 2,\n 3\n]">>}, jsone_encode:encode([1, 2, 3], [{indent, 2}])), 294 | ?assertEqual({ok, <<"{}">>}, jsone_encode:encode(?OBJ0, [{indent, 1}])), 295 | ?assertEqual({ok, <<"{\n \"a\":1,\n \"b\":2\n}">>}, 296 | jsone_encode:encode(?OBJ2(a, 1, b, 2), [{indent, 1}])), 297 | ?assertEqual({ok, <<"{\n \"a\":1,\n \"b\":2\n}">>}, 298 | jsone_encode:encode(?OBJ2(a, 1, b, 2), [{indent, 2}])) 299 | end}, 300 | {"indent+space", 301 | fun() -> 302 | ?assertEqual({ok, <<"[]">>}, jsone_encode:encode([], [{indent, 1}, {space, 1}])), 303 | ?assertEqual({ok, <<"[\n 1,\n 2,\n 3\n]">>}, jsone_encode:encode([1, 2, 3], [{indent, 1}, {space, 1}])), 304 | ?assertEqual({ok, <<"[\n 1,\n 2,\n 3\n]">>}, 305 | jsone_encode:encode([1, 2, 3], [{indent, 2}, {space, 2}])), 306 | ?assertEqual({ok, <<"{}">>}, jsone_encode:encode(?OBJ0, [{indent, 1}, {space, 1}])), 307 | ?assertEqual({ok, <<"{\n \"a\": 1,\n \"b\": 2\n}">>}, 308 | jsone_encode:encode(?OBJ2(a, 1, b, 2), [{indent, 1}, {space, 1}])), 309 | ?assertEqual({ok, <<"{\n \"a\": 1,\n \"b\": 2\n}">>}, 310 | jsone_encode:encode(?OBJ2(a, 1, b, 2), [{indent, 2}, {space, 2}])) 311 | end}, 312 | 313 | %% `map_unknown_value` option 314 | {"`map_unknown_value` option", 315 | fun() -> 316 | Input = [{1, 2, 3, 4}], 317 | MapFun = fun({_, _, _, _} = Ip4) -> 318 | {ok, list_to_binary(inet:ntoa(Ip4))}; 319 | (_) -> 320 | error 321 | end, 322 | Expected = <<"[\"1.2.3.4\"]">>, 323 | ?assertEqual(Expected, jsone:encode(Input, [{map_unknown_value, MapFun}])) 324 | end}, 325 | {"`map_unknown_value` option with singleton tuple", 326 | fun() -> 327 | Input = [{foo}], 328 | MapFun = fun(Value) -> {ok, unicode:characters_to_binary(io_lib:format("~p~n", [Value]))} end, 329 | Expected = <<"[\"{foo}\\n\"]">>, 330 | ?assertEqual(Expected, jsone:encode(Input, [{map_unknown_value, MapFun}])) 331 | end}, 332 | {"IP address", 333 | fun() -> 334 | Input = #{ip => {127, 0, 0, 1}}, 335 | Expected = <<"{\"ip\":\"127.0.0.1\"}">>, 336 | ?assertEqual(Expected, jsone:encode(Input, [{map_unknown_value, fun jsone:ip_address_to_json_string/1}])), 337 | 338 | %% Without `map_unknown_value' option. 339 | ?assertMatch({error, _}, jsone:try_encode(Input, [{map_unknown_value, undefined}])) 340 | end}, 341 | 342 | %% Others 343 | {"compound data", 344 | fun() -> 345 | Input = [true, 346 | {[{<<"1">>, 2}, {<<"array">>, [[[[1]]], {[{<<"ab">>, <<"cd">>}]}, [], ?OBJ0, false]}]}, 347 | null], 348 | Expected = <<"[true,{\"1\":2,\"array\":[[[[1]]],{\"ab\":\"cd\"},[],{},false]},null]">>, 349 | ?assertEqual({ok, Expected}, jsone_encode:encode(Input)), 350 | 351 | PpExpected = 352 | <<"[\n true,\n {\n \"1\": 2,\n \"array\": [\n [\n [\n [\n 1\n ]\n ]\n ],\n {\n \"ab\": \"cd\"\n },\n [],\n {},\n false\n ]\n },\n null\n]">>, 353 | ?assertEqual({ok, PpExpected}, jsone_encode:encode(Input, [{indent, 1}, {space, 1}])) 354 | end}, 355 | {"invalid value", 356 | fun() -> 357 | Pid = self(), 358 | PidString = list_to_binary(io_lib:format("~p", [Pid])), 359 | ?assertEqual({ok, <<$", PidString/binary, $">>}, jsone_encode:encode(Pid)), 360 | ?assertMatch({error, {badarg, _}}, jsone_encode:encode(Pid, [{map_unknown_value, undefined}])) 361 | end}, 362 | {"wrong option", fun() -> ?assertError(badarg, jsone_encode:encode(1, [{no_such_option, hoge}])) end}, 363 | {"canonical_form", 364 | fun() -> 365 | Obj1 = ?OBJECT_FROM_LIST([ {<<"key", (integer_to_binary(I))/binary>>, I} || I <- lists:seq(1000, 0, -1) ]), 366 | Obj2 = ?OBJECT_FROM_LIST([ {<<"key", (integer_to_binary(I))/binary>>, I} || I <- lists:seq(0, 1000, 1) ]), 367 | ?assertEqual(jsone_encode:encode(Obj1, [canonical_form]), jsone_encode:encode(Obj2, [canonical_form])) 368 | end}]. 369 | -------------------------------------------------------------------------------- /test/jsone_inet_tests.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2013-2021, Takeru Ohta 2 | -module(jsone_inet_tests). 3 | 4 | -include_lib("eunit/include/eunit.hrl"). 5 | 6 | 7 | format_ipv4_test() -> 8 | Expected = <<"127.0.0.1">>, 9 | {ok, Addr} = inet:parse_ipv4_address(binary_to_list(Expected)), 10 | {ok, Actual} = jsone_inet:ip_address_to_json_string(Addr), 11 | ?assertEqual(Actual, Expected). 12 | 13 | 14 | format_ipv6_test() -> 15 | Addresses = [<<"::127.0.0.1">>, 16 | <<"::ffff:192.0.2.1">>, 17 | <<"::ffff:0:255.255.255.255">>, 18 | <<"64:ff9b::0.0.0.0">>, 19 | <<"64:ff9b:1::192.168.1.1">>, 20 | <<"64:ff9b:1::1:192.168.1.1">>, 21 | <<"::1:2:3:2001:db8">>, 22 | <<"2001:db8::">>, 23 | <<"2001:db8::1">>, 24 | <<"2001:db8::1:0:0:1">>, 25 | <<"2001:db8:0:1:1:1:1:1">>, 26 | <<"2001:0:0:1::1">>, 27 | <<"2001:db8:85a3::8a2e:370:7334">>], 28 | lists:foreach(fun(Expected) -> 29 | {ok, Addr} = inet:parse_ipv6_address(binary_to_list(Expected)), 30 | {ok, Bin} = jsone_inet:ip_address_to_json_string(Addr), 31 | ?assertEqual(Expected, Bin) 32 | end, 33 | Addresses). 34 | 35 | 36 | invalid_ip_addr_test() -> 37 | ?assertEqual(jsone_inet:ip_address_to_json_string(foo), error), 38 | ?assertEqual(jsone_inet:ip_address_to_json_string({1, 2, 3}), error), 39 | ?assertEqual(jsone_inet:ip_address_to_json_string({0, 10000, 0, 0}), error), 40 | ?assertEqual(jsone_inet:ip_address_to_json_string({-1, 0, 0, 0, 0, 0, 0, 0}), error). 41 | -------------------------------------------------------------------------------- /test/test_time_module.erl: -------------------------------------------------------------------------------- 1 | -module(test_time_module). 2 | -export([localtime/0, set_localtime/1, localtime_to_universaltime/1, mock_localtime_to_universaltime/1]). 3 | 4 | 5 | set_localtime({{_, _, _}, {_, _, _}} = LocalTime) -> 6 | erlang:put('__test_time_module__localtime__', LocalTime). 7 | 8 | 9 | localtime() -> 10 | erlang:get('__test_time_module__localtime__'). 11 | 12 | 13 | localtime_to_universaltime({{_, _, _}, {_, _, _}} = LocalTime) -> 14 | LocalTimeToUniversalTimeFun = erlang:get('__test_time_module_localtime_to_universaltime__'), 15 | LocalTimeToUniversalTimeFun(LocalTime). 16 | 17 | 18 | mock_localtime_to_universaltime(Fun) when is_function(Fun) -> 19 | erlang:put('__test_time_module_localtime_to_universaltime__', Fun). 20 | --------------------------------------------------------------------------------