From de73e04bccacad1fbf22fd99879e9ff842195ff1 Mon Sep 17 00:00:00 2001 From: Devin Alexander Torres Date: Tue, 5 Mar 2024 06:52:19 -0600 Subject: [PATCH] Poison 6 Features: * Support Erlang 27 and Elixir 1.17 * Reintroduce `Poison.encode_to_iodata!/1` for Phoenix compatibility * Make `:html_safe` encode option follow OWASP recommended HTML escaping * Add `Date.Range` encoding * Allow `:as` decode option to be a function * Add a CHANGELOG Bug Fixes: * Stop double decoding structs * Fix various typespecs * Correctly encode some UTF-8 surrogate pairs Performance Improvements: * Significantly improve performance Breaking Changes: * Remove deprecated `HashSet` encoding * Minimum supported versions are now Erlang 24 and Elixir 1.12 Closes #105, #172, #191, #194, #199, #206, #207, #214, #217, #222. --- .credo.exs | 267 +++++++++++++++++++---------------- .dialyzer_ignore.exs | 2 - .formatter.exs | 4 +- .gitattributes | 1 + .github/workflows/ci.yml | 47 +++--- .gitignore | 4 +- .tool-versions | 4 +- .vscode/settings.json | 4 - CHANGELOG.md | 39 +++++ Dockerfile | 2 +- LICENSE | 4 +- README.md | 35 ++--- bench/run.exs | 51 ++++--- docker-compose.yml | 1 - lib/poison.ex | 111 +++++++++------ lib/poison/decoder.ex | 21 +-- lib/poison/encoder.ex | 70 ++++++--- lib/poison/parser.ex | 126 ++++++++--------- mix.exs | 64 +++++---- mix.lock | 54 ++++--- profile/profiler.ex | 1 + test/poison/encoder_test.exs | 195 +++++++++++-------------- test/poison/parser_test.exs | 42 +++--- test/test_helper.exs | 19 +-- vendor/JSONTestSuite | 2 +- 25 files changed, 635 insertions(+), 535 deletions(-) create mode 100644 .gitattributes create mode 100644 CHANGELOG.md diff --git a/.credo.exs b/.credo.exs index a9c6d0ca..6bb5d599 100644 --- a/.credo.exs +++ b/.credo.exs @@ -22,16 +22,14 @@ # In the latter case `**/*.{ex,exs}` will be used. # included: [ + "{.credo,.dialyzer_ignore,.formatter,mix}.exs", + "config/", + "bench/", "lib/", - "src/", - "test/", - "web/", - "apps/*/lib/", - "apps/*/src/", - "apps/*/test/", - "apps/*/web/" + "profile/", + "test/" ], - excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"] + excluded: [~r"/_build/", ~r"/deps/"] }, # # Load and configure plugins here: @@ -64,126 +62,151 @@ # # {Credo.Check.Design.DuplicatedCode, false} # - checks: [ - # - ## Consistency Checks - # - {Credo.Check.Consistency.ExceptionNames, []}, - {Credo.Check.Consistency.LineEndings, []}, - {Credo.Check.Consistency.ParameterPatternMatching, []}, - {Credo.Check.Consistency.SpaceAroundOperators, []}, - {Credo.Check.Consistency.SpaceInParentheses, []}, - {Credo.Check.Consistency.TabsOrSpaces, []}, + checks: %{ + enabled: [ + # + ## Consistency Checks + # + {Credo.Check.Consistency.ExceptionNames, []}, + {Credo.Check.Consistency.LineEndings, []}, + {Credo.Check.Consistency.MultiAliasImportRequireUse, []}, + {Credo.Check.Consistency.ParameterPatternMatching, []}, + {Credo.Check.Consistency.SpaceAroundOperators, []}, + {Credo.Check.Consistency.SpaceInParentheses, []}, + {Credo.Check.Consistency.TabsOrSpaces, []}, - # - ## Design Checks - # - # You can customize the priority of any check - # Priority values are: `low, normal, high, higher` - # - {Credo.Check.Design.AliasUsage, - [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]}, - # You can also customize the exit_status of each check. - # If you don't want TODO comments to cause `mix credo` to fail, just - # set this value to 0 (zero). - # - {Credo.Check.Design.TagTODO, [exit_status: 2]}, - {Credo.Check.Design.TagFIXME, []}, + # + ## Design Checks + # + # You can customize the priority of any check + # Priority values are: `low, normal, high, higher` + # + {Credo.Check.Design.AliasUsage, + [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]}, + {Credo.Check.Design.DuplicatedCode, []}, + {Credo.Check.Design.SkipTestWithoutComment, []}, + {Credo.Check.Design.TagFIXME, []}, + # You can also customize the exit_status of each check. + # If you don't want TODO comments to cause `mix credo` to fail, just + # set this value to 0 (zero). + # + {Credo.Check.Design.TagTODO, [exit_status: 2]}, - # - ## Readability Checks - # - {Credo.Check.Readability.AliasOrder, []}, - {Credo.Check.Readability.FunctionNames, []}, - {Credo.Check.Readability.LargeNumbers, []}, - {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, - {Credo.Check.Readability.ModuleAttributeNames, []}, - {Credo.Check.Readability.ModuleDoc, []}, - {Credo.Check.Readability.ModuleNames, []}, - {Credo.Check.Readability.ParenthesesInCondition, []}, - {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, - {Credo.Check.Readability.PredicateFunctionNames, []}, - {Credo.Check.Readability.PreferImplicitTry, []}, - {Credo.Check.Readability.RedundantBlankLines, []}, - {Credo.Check.Readability.Semicolons, []}, - {Credo.Check.Readability.SpaceAfterCommas, []}, - {Credo.Check.Readability.StringSigils, false}, - {Credo.Check.Readability.TrailingBlankLine, []}, - {Credo.Check.Readability.TrailingWhiteSpace, []}, - {Credo.Check.Readability.UnnecessaryAliasExpansion, []}, - {Credo.Check.Readability.VariableNames, []}, + # + ## Readability Checks + # + {Credo.Check.Readability.AliasAs, []}, + {Credo.Check.Readability.AliasOrder, []}, + {Credo.Check.Readability.BlockPipe, []}, + {Credo.Check.Readability.FunctionNames, []}, + {Credo.Check.Readability.ImplTrue, []}, + {Credo.Check.Readability.LargeNumbers, []}, + {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, + {Credo.Check.Readability.ModuleAttributeNames, []}, + {Credo.Check.Readability.ModuleDoc, []}, + {Credo.Check.Readability.ModuleNames, []}, + {Credo.Check.Readability.OneArityFunctionInPipe, []}, + {Credo.Check.Readability.ParenthesesInCondition, []}, + {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, + {Credo.Check.Readability.PipeIntoAnonymousFunctions, []}, + {Credo.Check.Readability.PredicateFunctionNames, []}, + {Credo.Check.Readability.PreferImplicitTry, []}, + {Credo.Check.Readability.RedundantBlankLines, []}, + {Credo.Check.Readability.Semicolons, []}, + {Credo.Check.Readability.SeparateAliasRequire, []}, + {Credo.Check.Readability.SingleFunctionToBlockPipe, []}, + {Credo.Check.Readability.SinglePipe, []}, + {Credo.Check.Readability.SpaceAfterCommas, []}, + {Credo.Check.Readability.StrictModuleLayout, []}, + {Credo.Check.Readability.StringSigils, []}, + {Credo.Check.Readability.TrailingBlankLine, []}, + {Credo.Check.Readability.TrailingWhiteSpace, []}, + {Credo.Check.Readability.UnnecessaryAliasExpansion, []}, + {Credo.Check.Readability.VariableNames, []}, + {Credo.Check.Readability.WithCustomTaggedTuple, []}, + {Credo.Check.Readability.WithSingleClause, []}, - # - ## Refactoring Opportunities - # - {Credo.Check.Refactor.CondStatements, []}, - {Credo.Check.Refactor.CyclomaticComplexity, []}, - {Credo.Check.Refactor.FunctionArity, []}, - {Credo.Check.Refactor.LongQuoteBlocks, []}, - {Credo.Check.Refactor.MapInto, false}, - {Credo.Check.Refactor.MatchInCondition, []}, - {Credo.Check.Refactor.NegatedConditionsInUnless, []}, - {Credo.Check.Refactor.NegatedConditionsWithElse, []}, - {Credo.Check.Refactor.Nesting, []}, - {Credo.Check.Refactor.UnlessWithElse, []}, - {Credo.Check.Refactor.WithClauses, []}, + # + ## Refactoring Opportunities + # + {Credo.Check.Refactor.ABCSize, []}, + {Credo.Check.Refactor.AppendSingleItem, []}, + {Credo.Check.Refactor.Apply, []}, + {Credo.Check.Refactor.CondStatements, []}, + {Credo.Check.Refactor.CyclomaticComplexity, []}, + {Credo.Check.Refactor.DoubleBooleanNegation, []}, + {Credo.Check.Refactor.FilterCount, []}, + {Credo.Check.Refactor.FilterFilter, []}, + {Credo.Check.Refactor.FilterReject, []}, + {Credo.Check.Refactor.FunctionArity, []}, + {Credo.Check.Refactor.IoPuts, []}, + {Credo.Check.Refactor.LongQuoteBlocks, []}, + {Credo.Check.Refactor.MapJoin, []}, + {Credo.Check.Refactor.MapMap, []}, + {Credo.Check.Refactor.MatchInCondition, []}, + {Credo.Check.Refactor.NegatedConditionsInUnless, []}, + {Credo.Check.Refactor.NegatedConditionsWithElse, []}, + {Credo.Check.Refactor.NegatedIsNil, []}, + {Credo.Check.Refactor.Nesting, []}, + {Credo.Check.Refactor.PassAsyncInTestCases, []}, + {Credo.Check.Refactor.PipeChainStart, []}, + {Credo.Check.Refactor.RedundantWithClauseResult, []}, + {Credo.Check.Refactor.RejectFilter, []}, + {Credo.Check.Refactor.RejectReject, []}, + {Credo.Check.Refactor.UnlessWithElse, []}, + {Credo.Check.Refactor.UtcNowTruncate, []}, + {Credo.Check.Refactor.WithClauses, []}, - # - ## Warnings - # - {Credo.Check.Warning.ApplicationConfigInModuleAttribute, []}, - {Credo.Check.Warning.BoolOperationOnSameValues, []}, - {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, - {Credo.Check.Warning.IExPry, []}, - {Credo.Check.Warning.IoInspect, []}, - {Credo.Check.Warning.LazyLogging, false}, - {Credo.Check.Warning.MixEnv, false}, - {Credo.Check.Warning.OperationOnSameValues, []}, - {Credo.Check.Warning.OperationWithConstantResult, []}, - {Credo.Check.Warning.RaiseInsideRescue, []}, - {Credo.Check.Warning.UnusedEnumOperation, []}, - {Credo.Check.Warning.UnusedFileOperation, []}, - {Credo.Check.Warning.UnusedKeywordOperation, []}, - {Credo.Check.Warning.UnusedListOperation, []}, - {Credo.Check.Warning.UnusedPathOperation, []}, - {Credo.Check.Warning.UnusedRegexOperation, []}, - {Credo.Check.Warning.UnusedStringOperation, []}, - {Credo.Check.Warning.UnusedTupleOperation, []}, - {Credo.Check.Warning.UnsafeExec, []}, + # + ## Warnings + # + {Credo.Check.Consistency.UnusedVariableNames, []}, + {Credo.Check.Warning.ApplicationConfigInModuleAttribute, []}, + {Credo.Check.Warning.BoolOperationOnSameValues, []}, + {Credo.Check.Warning.Dbg, []}, + {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, + {Credo.Check.Warning.IExPry, []}, + {Credo.Check.Warning.IoInspect, []}, + {Credo.Check.Warning.LazyLogging, false}, + {Credo.Check.Warning.LeakyEnvironment, []}, + {Credo.Check.Warning.MapGetUnsafePass, []}, + {Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, []}, + {Credo.Check.Warning.MixEnv, []}, + {Credo.Check.Warning.OperationOnSameValues, []}, + {Credo.Check.Warning.OperationWithConstantResult, []}, + {Credo.Check.Warning.RaiseInsideRescue, []}, + {Credo.Check.Warning.SpecWithStruct, []}, + {Credo.Check.Warning.UnsafeExec, []}, + {Credo.Check.Warning.UnsafeToAtom, []}, + {Credo.Check.Warning.UnusedEnumOperation, []}, + {Credo.Check.Warning.UnusedFileOperation, []}, + {Credo.Check.Warning.UnusedKeywordOperation, []}, + {Credo.Check.Warning.UnusedListOperation, []}, + {Credo.Check.Warning.UnusedPathOperation, []}, + {Credo.Check.Warning.UnusedRegexOperation, []}, + {Credo.Check.Warning.UnusedStringOperation, []}, + {Credo.Check.Warning.UnusedTupleOperation, []}, + {Credo.Check.Warning.WrongTestFileExtension, []} + ], + disabled: [ + # + # Controversial and experimental checks (opt-in, just move the check to `:enabled` + # and be sure to use `mix credo --strict` to see low priority checks) + # + {Credo.Check.Readability.MultiAlias, []}, + {Credo.Check.Readability.NestedFunctionCalls, []}, + {Credo.Check.Readability.OnePipePerLine, []}, + {Credo.Check.Readability.Specs, []}, + {Credo.Check.Refactor.ModuleDependencies, []}, + {Credo.Check.Refactor.VariableRebinding, []} - # - # Checks scheduled for next check update (opt-in for now, just replace `false` with `[]`) + # {Credo.Check.Refactor.MapInto, []}, - # - # Controversial and experimental checks (opt-in, just replace `false` with `[]`) - # - {Credo.Check.Consistency.MultiAliasImportRequireUse, []}, - {Credo.Check.Consistency.UnusedVariableNames, []}, - {Credo.Check.Design.DuplicatedCode, []}, - {Credo.Check.Readability.AliasAs, []}, - {Credo.Check.Readability.BlockPipe, []}, - {Credo.Check.Readability.ImplTrue, []}, - {Credo.Check.Readability.MultiAlias, false}, - {Credo.Check.Readability.SeparateAliasRequire, []}, - {Credo.Check.Readability.SinglePipe, []}, - {Credo.Check.Readability.Specs, false}, - {Credo.Check.Readability.StrictModuleLayout, []}, - {Credo.Check.Readability.WithCustomTaggedTuple, []}, - {Credo.Check.Refactor.ABCSize, []}, - {Credo.Check.Refactor.AppendSingleItem, []}, - {Credo.Check.Refactor.DoubleBooleanNegation, []}, - {Credo.Check.Refactor.ModuleDependencies, false}, - {Credo.Check.Refactor.NegatedIsNil, []}, - {Credo.Check.Refactor.PipeChainStart, []}, - {Credo.Check.Refactor.VariableRebinding, false}, - {Credo.Check.Warning.LeakyEnvironment, []}, - {Credo.Check.Warning.MapGetUnsafePass, []}, - {Credo.Check.Warning.UnsafeToAtom, []} - - # - # Custom checks can be created using `mix credo.gen.check`. - # - ] + # + # Custom checks can be created using `mix credo.gen.check`. + # + ] + } } ] } diff --git a/.dialyzer_ignore.exs b/.dialyzer_ignore.exs index 23fc3f6f..ca733537 100644 --- a/.dialyzer_ignore.exs +++ b/.dialyzer_ignore.exs @@ -1,5 +1,3 @@ [ - {"lib/poison/encoder.ex", :unknown_function, 80}, - {"lib/poison/decoder.ex", :unknown_function, 106}, {"lib/poison/parser.ex", :improper_list_constr} ] diff --git a/.formatter.exs b/.formatter.exs index 818a3c4c..3d57b788 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -2,7 +2,7 @@ [ import_deps: [:stream_data], inputs: [ - "{mix,.credo,.formatter}.exs", - "{config,lib,test,bench,profile}/**/*.{ex,exs}" + "{.credo,.dialyzer_ignore,.formatter,mix}.exs", + "{config,bench,lib,profile,test}/**/*.{ex,exs}" ] ] diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..6313b56c --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6b734e4c..66adb943 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,7 @@ on: push: branches: - master + - develop pull_request: branches: - master @@ -18,30 +19,32 @@ jobs: strategy: matrix: os: - - ubuntu-latest - - windows-latest + - ubuntu-22.04 - windows-2022 - - macos-11 otp: - - '24.1' - - '24.0' - - '23.3' - - '23.2' - - '23.1' - - '23.0' + - "26.2.2" + - "25.3.2" + - "24.3.4" elixir: - - '1.12.3' - - '1.12.2' - - '1.12.1' - - '1.12.0' - - '1.11.4' - - '1.11.3' - - '1.11.2' - - '1.11.1' - - '1.11.0' + - "1.16.3" + - "1.15.7" + - "1.14.5" + - "1.13.4" + - "1.12.3" + exclude: + - otp: "26.2.2" + elixir: "1.14.5" + - otp: "26.2.2" + elixir: "1.13.4" + - otp: "26.2.2" + elixir: "1.12.3" + - otp: "25.3.2" + elixir: "1.13.4" + - otp: "25.3.2" + elixir: "1.12.3" steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: fetch-depth: 0 submodules: true @@ -51,8 +54,8 @@ jobs: otp-version: ${{matrix.otp}} elixir-version: ${{matrix.elixir}} - - uses: actions/cache@v2 - id: mix-cache + - uses: actions/cache@v4 + id: cache with: path: | _build @@ -60,7 +63,7 @@ jobs: vendor key: ${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('mix.lock') }} - - if: steps.mix-cache.outputs.cache-hit != 'true' + - if: steps.cache.outputs.cache-hit != 'true' run: mix deps.get - run: mix coveralls.github diff --git a/.gitignore b/.gitignore index 9a1fb790..760631d5 100644 --- a/.gitignore +++ b/.gitignore @@ -22,8 +22,8 @@ erl_crash.dump # Ignore package tarball (built via "mix hex.build"). poison-*.tar -# Temporary files for e.g. tests -/tmp +# Temporary files, for example, from tests. +/tmp/ .elixir_ls *.benchee diff --git a/.tool-versions b/.tool-versions index 9a98889f..736ecce9 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ -erlang 24.1.7 -elixir 1.13.0-rc.1-otp-24 +erlang 27.0 +elixir 1.17.0-rc.1-otp-27 diff --git a/.vscode/settings.json b/.vscode/settings.json index 8dc16adf..9e5db875 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,5 @@ { "editor.formatOnSave": true, - "editor.formatOnType": true, - "files.watcherExclude": { - "**/target": true - }, "elixirLS.enableTestLenses": true, "elixirLS.dialyzerWarnOpts": ["no_improper_lists"] } diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..6f0fcdef --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,39 @@ +# Poison Changelog + +## v6.0.0 + +### Features + +* Support Erlang 27 and Elixir 1.17 + [#214](https://github.com/devinus/poison/issues/214) + [#222](https://github.com/devinus/poison/issues/222) +* Reintroduce `Poison.encode_to_iodata!/1` for Phoenix compatibility + [#172](https://github.com/devinus/poison/issues/172) + [#206](https://github.com/devinus/poison/pull/206) +* Make [`:html_safe`](`t:Poison.Encoder.escape/0`) encode option follow OWASP + recommended HTML escaping + [#194](https://github.com/devinus/poison/issues/194) +* Add `Date.Range` encoding +* Allow [`:as`](`t:Poison.Decoder.as/0`) decode option to be a function + [#207](https://github.com/devinus/poison/pull/207) +* Add a [CHANGELOG](CHANGELOG.md) + [#105](https://github.com/devinus/poison/issues/105) + +### Bug Fixes + +* Stop double decoding structs + [#191](https://github.com/devinus/poison/issues/191) +* Fix various typespecs + [#199](https://github.com/devinus/poison/issues/199) +* Correctly encode some UTF-8 surrogate pairs + [#217](https://github.com/devinus/poison/issues/217) + +### Performance Improvements + +* Significantly improve performance + ([2024-06-06](https://gist.github.com/devinus/afb351ae45194a6b93b6db9bf2d4c163)) + +### Breaking Changes + +* Remove deprecated `HashSet` encoding +* Minimum supported versions are now Erlang 24 and Elixir 1.12 diff --git a/Dockerfile b/Dockerfile index aaf480f6..6eaed46e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM elixir:1.12.3-alpine as base +FROM elixir:1.17.0-rc.0-otp-27-alpine AS base ARG MIX_ENV ENV MIX_ENV ${MIX_ENV:-test} RUN apk --no-cache add git build-base diff --git a/LICENSE b/LICENSE index 5f1897da..62b66301 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,6 @@ -Copyright (C) 2021 Devin Alexander Torres +BSD Zero Clause License + +Copyright (C) 2024 Devin Alexander Torres Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. diff --git a/README.md b/README.md index bf49a99c..f9fb9fd4 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ # Poison -[![Build Status](https://img.shields.io/github/workflow/status/devinus/poison/ci/master)](https://github.com/devinus/poison/actions/workflows/ci.yml) -[![Coverage Status](https://img.shields.io/coveralls/github/devinus/poison/master)](https://coveralls.io/github/devinus/poison?branch=master) -[![Hex.pm Version](https://img.shields.io/hexpm/v/poison.svg?style=flat-square)](https://hex.pm/packages/poison) -[![Hex.pm Download Total](https://img.shields.io/hexpm/dt/poison.svg?style=flat-square)](https://hex.pm/packages/poison) +[![Build Status](https://img.shields.io/github/actions/workflow/status/devinus/poison/ci.yml)](https://github.com/devinus/poison/actions/workflows/ci.yml) +[![Coverage Status](https://img.shields.io/coverallsCoverage/github/devinus/poison)](https://coveralls.io/github/devinus/poison?branch=master)) +[![Hex.pm Version](https://img.shields.io/hexpm/v/poison)](https://hex.pm/packages/poison) +[![Hex.pm Download Total](https://img.shields.io/hexpm/dt/poison)](https://hex.pm/packages/poison) +[![Hex.pm Dependents](https://img.shields.io/librariesio/dependents/hex/poison)](https://hex.pm/packages/poison) Poison is a new JSON library for Elixir focusing on wicked-fast **speed** without sacrificing **simplicity**, **completeness**, or **correctness**. @@ -17,7 +18,7 @@ several techniques that are [known to benefit BeamAsm][2] for JIT compilation, Poison benchmarks sometimes puts Poison's performance close to `jiffy` and usually faster than other Erlang/Elixir libraries. -Poison fully conforms to [RFC 7159][4], [ECMA 404][5], and fully passes the +Poison fully conforms to [RFC 8259][4], [ECMA 404][5], and fully passes the [JSONTestSuite][6]. ## Installation @@ -32,8 +33,8 @@ end Then, update your dependencies: -```sh-session -$ mix deps.get +```sh +mix deps.get ``` ## Usage @@ -133,7 +134,7 @@ ignored. ### Key Validation -According to [RFC 7159][4] keys in a JSON object should be unique. This is +According to [RFC 8259][4] keys in a JSON object should be unique. This is enforced and resolved in different ways in other libraries. In the Ruby JSON library for example, the output generated from encoding a hash with a duplicate key (say one is a string, the other an atom) will include both keys. When @@ -154,16 +155,16 @@ iex> Poison.encode!(%{:foo => "foo1", "foo" => "foo2"}, strict_keys: true) ## Benchmarking -```sh-session -$ MIX_ENV=bench mix run bench/run.exs +```sh +MIX_ENV=bench mix run bench/run.exs ``` ### Current Benchmarks -As of 2021-07-22: +As of 2024-06-06: -- Amazon EC2 c5.2xlarge instance running Ubuntu Server 20.04: - https://gist.github.com/devinus/f56cff9e5a0aa9de9215cf33212085f6 +- Amazon EC2 c6i.2xlarge instance running Ubuntu Server 22.04: + ## License @@ -171,10 +172,10 @@ Poison is released under the [public-domain-equivalent][8] [0BSD][9] license. [1]: https://erlang.org/euc/07/papers/1700Gustafsson.pdf [2]: https://erlang.org/documentation/doc-12.0-rc1/erts-12.0/doc/html/BeamAsm.html -[3]: http://jlouisramblings.blogspot.com/2013/07/problematic-traits-in-erlang.html -[4]: https://tools.ietf.org/html/rfc7159 -[5]: http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf +[3]: https://jlouisramblings.blogspot.com/2013/07/problematic-traits-in-erlang.html +[4]: https://datatracker.ietf.org/doc/html/rfc8259 +[5]: https://ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf [6]: https://github.com/nst/JSONTestSuite -[7]: http://prog21.dadgum.com/70.html +[7]: https://prog21.dadgum.com/70.html [8]: https://en.wikipedia.org/wiki/Public-domain-equivalent_license [9]: https://opensource.org/licenses/0BSD diff --git a/bench/run.exs b/bench/run.exs index 962c2386..ef918dd4 100644 --- a/bench/run.exs +++ b/bench/run.exs @@ -1,13 +1,16 @@ defmodule Bench do - alias Benchee.Formatters.{Console, HTML} + alias Benchee.Formatters.{Console, HTML, Markdown} def run_decode do Benchee.run(decode_jobs(), - parallel: 8, + title: "Decode", + parallel: 4, warmup: 1, time: 10, memory_time: 1, + reduction_time: 1, pre_check: true, + measure_function_call_overhead: true, load: Path.join(__DIR__, "decode.benchee"), save: [path: Path.join(__DIR__, "decode.benchee")], inputs: @@ -16,21 +19,24 @@ defmodule Bench do |> read_data() |> (&{name, &1}).() end, - before_each: fn input -> :binary.copy(input) end, formatters: [ {Console, extended_statistics: true}, - {HTML, extended_statistics: true, file: Path.expand("output/decode.html", __DIR__)} + {Markdown, file: Path.expand("output/decode.md", __DIR__)}, + {HTML, auto_open: false, file: Path.expand("output/decode.html", __DIR__)} ] ) end def run_encode do Benchee.run(encode_jobs(), - parallel: 8, + title: "Encode", + parallel: 4, warmup: 1, time: 10, memory_time: 1, + reduction_time: 1, pre_check: true, + measure_function_call_overhead: true, load: Path.join(__DIR__, "encode.benchee"), save: [path: Path.join(__DIR__, "encode.benchee")], inputs: @@ -42,7 +48,8 @@ defmodule Bench do end, formatters: [ {Console, extended_statistics: true}, - {HTML, extended_statistics: true, file: Path.expand("output/encode.html", __DIR__)} + {Markdown, file: Path.expand("output/encode.md", __DIR__)}, + {HTML, auto_open: false, file: Path.expand("output/encode.html", __DIR__)} ] ) end @@ -58,27 +65,39 @@ defmodule Bench do end defp decode_jobs do - %{ + jobs = %{ "Jason" => &Jason.decode!/1, "jiffy" => &:jiffy.decode(&1, [:return_maps, :use_nil]), - "JSON" => &JSON.decode!/1, - "jsone" => &:jsone.decode/1, + "jsone" => &:jsone.decode(&1, [:reject_invalid_utf8, duplicate_map_keys: :last]), "JSX" => &JSX.decode!(&1, [:strict]), "Poison" => &Poison.Parser.parse!/1, - "Tiny" => &Tiny.decode!/1 + "Tiny" => &Tiny.decode!/1, + "Thoas" => &:thoas.decode/1 } + + if Code.ensure_loaded?(:json) do + Map.put(jobs, "json", &:json.decode/1) + else + jobs + end end defp encode_jobs do - %{ - "Jason" => &Jason.encode!/1, + jobs = %{ + "Jason" => &Jason.encode_to_iodata!/1, "jiffy" => &:jiffy.encode(&1, [:use_nil]), - "JSON" => &JSON.encode!/1, "jsone" => &:jsone.encode/1, - "JSX" => &JSX.encode!/1, - "Poison" => &Poison.encode!/1, - "Tiny" => &Tiny.encode!/1 + "JSX" => &JSX.encode!(&1, [:strict]), + "Poison" => &Poison.encode_to_iodata!/1, + "Tiny" => &Tiny.encode_to_iodata!/1, + "Thoas" => &:thoas.encode_to_iodata/1 } + + if Code.ensure_loaded?(:json) do + Map.put(jobs, "json", &:json.encode/1) + else + jobs + end end defp decode_inputs do diff --git a/docker-compose.yml b/docker-compose.yml index e924c523..5740b989 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,3 @@ -version: "3.7" services: project: build: diff --git a/lib/poison.ex b/lib/poison.ex index bb07476a..381f29d2 100644 --- a/lib/poison.ex +++ b/lib/poison.ex @@ -9,39 +9,75 @@ defmodule Poison do alias Poison.{ParseError, Parser} @doc """ - Encode a value to JSON. + Encode a value to JSON IO data. - iex> Poison.encode([1, 2, 3]) - {:ok, "[1,2,3]"} + iex> Poison.encode_to_iodata([1, 2, 3]) + {:ok, [91, ["1", 44, "2", 44, "3"], 93]} + + iex> Poison.encode_to_iodata({}) + {:error, %Poison.EncodeError{message: nil, value: {}}} """ - @spec encode(Encoder.t(), Encoder.options()) :: - {:ok, iodata} - | {:error, Exception.t()} - def encode(value, options \\ %{}) do - {:ok, encode!(value, options)} + @spec encode_to_iodata(Encoder.t()) :: {:ok, iodata} | {:error, EncodeError.t()} + @spec encode_to_iodata(Encoder.t(), Encoder.options() | [Encoder.option()]) :: + {:ok, iodata} | {:error, EncodeError.t()} + def encode_to_iodata(value, options \\ %{}) do + {:ok, encode_to_iodata!(value, options)} rescue exception in [EncodeError] -> {:error, exception} end @doc """ - Encode a value to JSON as iodata. + Encode a value to JSON IO data. Raises an exception on error. - iex> Poison.encode_to_iodata([1, 2, 3]) - {:ok, [91, ["1", 44, "2", 44, "3"], 93]} + iex> Poison.encode_to_iodata!([1, 2, 3]) + [91, ["1", 44, "2", 44, "3"], 93] + + iex> Poison.encode_to_iodata!({}) + ** (Poison.EncodeError) unable to encode value: {} """ - @spec encode_to_iodata(Encoder.t, Keyword.t) :: {:ok, iodata} | {:error, {:invalid, any}} - def encode_to_iodata(value, options \\ []) do - encode(value, [iodata: true] ++ options) + @spec encode_to_iodata!(Encoder.t()) :: iodata + @spec encode_to_iodata!(Encoder.t(), Encoder.options() | [Encoder.option()]) :: iodata + def encode_to_iodata!(value, options \\ %{}) + + def encode_to_iodata!(value, options) when is_list(options) do + encode_to_iodata!(value, Map.new(options)) + end + + def encode_to_iodata!(value, options) do + Encoder.encode(value, options) end @doc """ - Encode a value to JSON, raises an exception on error. + Encode a value to JSON. + + iex> Poison.encode([1, 2, 3]) + {:ok, "[1,2,3]"} + + iex> Poison.encode({}) + {:error, %Poison.EncodeError{message: nil, value: {}}} + """ + @spec encode(Encoder.t()) :: {:ok, iodata} | {:error, EncodeError.t()} + @spec encode(Encoder.t(), Encoder.options() | [Encoder.option()]) :: + {:ok, iodata} | {:error, EncodeError.t()} + def encode(value, options \\ %{}) do + {:ok, encode!(value, options)} + rescue + exception in [EncodeError] -> + {:error, exception} + end + + @doc """ + Encode a value to JSON. Raises an exception on error. iex> Poison.encode!([1, 2, 3]) "[1,2,3]" + + iex> Poison.encode!({}) + ** (Poison.EncodeError) unable to encode value: {} """ - @spec encode!(Encoder.t(), Encoder.options()) :: iodata | no_return + @spec encode!(Encoder.t()) :: binary + @spec encode!(Encoder.t(), Encoder.options() | [Encoder.option()]) :: binary def encode!(value, options \\ %{}) def encode!(value, options) when is_list(options) do @@ -49,22 +85,7 @@ defmodule Poison do end def encode!(value, options) do - if options[:iodata] do - Encoder.encode(value, options) - else - value |> Encoder.encode(options) |> IO.iodata_to_binary() - end - end - - @doc """ - Encode a value to JSON as iodata, raises an exception on error. - - iex> Poison.encode_to_iodata!([1, 2, 3]) - [91, ["1", 44, "2", 44, "3"], 93] - """ - @spec encode_to_iodata!(Encoder.t, Keyword.t) :: iodata | no_return - def encode_to_iodata!(value, options \\ []) do - encode!(value, [iodata: true] ++ options) + value |> encode_to_iodata!(options) |> IO.iodata_to_binary() end @doc """ @@ -72,13 +93,13 @@ defmodule Poison do iex> Poison.decode("[1,2,3]") {:ok, [1, 2, 3]} + + iex> Poison.decode("[") + {:error, %Poison.ParseError{data: "[", skip: 1, value: nil}} """ - @spec decode(iodata) :: - {:ok, Parser.t()} - | {:error, Exception.t()} - @spec decode(iodata, Decoder.options()) :: - {:ok, Parser.t()} - | {:error, Exception.t()} + @spec decode(iodata) :: {:ok, Parser.t()} | {:error, ParseError.t() | DecodeError.t()} + @spec decode(iodata, Decoder.options() | [Decoder.option()]) :: + {:ok, Decoder.t()} | {:error, ParseError.t() | DecodeError.t()} def decode(iodata, options \\ %{}) do {:ok, decode!(iodata, options)} rescue @@ -87,17 +108,18 @@ defmodule Poison do end @doc """ - Decode JSON to a value, raises an exception on error. + Decode JSON to a value. Raises an exception on error. iex> Poison.decode!("[1,2,3]") [1, 2, 3] + + iex> Poison.decode!("[") + ** (Poison.ParseError) unexpected end of input at position 1 """ - @spec decode!(iodata) :: Parser.t() | no_return - def decode!(value) do - Parser.parse!(value, %{}) - end + @spec decode!(iodata) :: Parser.t() + @spec decode!(iodata, Decoder.options() | [Decoder.option()]) :: Decoder.t() + def decode!(value, options \\ %{}) - @spec decode!(iodata, Decoder.options()) :: Decoder.t() | no_return def decode!(value, options) when is_list(options) do decode!(value, Map.new(options)) end @@ -106,7 +128,6 @@ defmodule Poison do value |> Parser.parse!(options) |> Decode.transform(options) - |> Decoder.decode(options) end def decode!(value, options) do diff --git a/lib/poison/decoder.ex b/lib/poison/decoder.ex index 47055e90..9d607feb 100644 --- a/lib/poison/decoder.ex +++ b/lib/poison/decoder.ex @@ -17,6 +17,9 @@ defmodule Poison.Decode do alias Poison.Decoder + @compile :inline + @compile :inline_list_funcs + def transform(value, options) when is_map(value) or is_list(value) do case Map.get(options, :as) do nil -> value @@ -110,16 +113,16 @@ end defprotocol Poison.Decoder do @fallback_to_any true - @typep as :: map | struct | [as] | (t -> as | [as]) + @type keys :: :atoms | :atoms! + @type decimal :: boolean + @type as :: map | struct | [as] | (t -> as) - @typep option :: {:keys, :atoms | :atoms!} | {:decimal, boolean} | {:as, as} - @type options :: - %{ - optional(:keys) => :atoms | :atoms!, - optional(:decimal) => boolean, - optional(:as) => as - } - | [option] + @type option :: {:keys, keys} | {:decimal, decimal} | {:as, as} + @type options :: %{ + optional(:keys) => keys, + optional(:decimal) => decimal, + optional(:as) => as + } @spec decode(t, options) :: any def decode(value, options) diff --git a/lib/poison/encoder.ex b/lib/poison/encoder.ex index 30b49100..0c6bf3cb 100644 --- a/lib/poison/encoder.ex +++ b/lib/poison/encoder.ex @@ -80,11 +80,18 @@ end defprotocol Poison.Encoder do @fallback_to_any true - @typep escape :: :unicode | :javascript | :html_safe - @typep pretty :: boolean - @typep indent :: non_neg_integer - @typep offset :: non_neg_integer - @typep strict_keys :: boolean + @type escape :: :unicode | :javascript | :html_safe + @type pretty :: boolean + @type indent :: non_neg_integer + @type offset :: non_neg_integer + @type strict_keys :: boolean + + @type option :: + {:escape, escape} + | {:pretty, pretty} + | {:indent, indent} + | {:offset, offset} + | {:strict_keys, strict_keys} @type options :: %{ optional(:escape) => escape, @@ -130,7 +137,7 @@ defimpl Poison.Encoder, for: BitString do end end - # http://en.wikipedia.org/wiki/Unicode_control_characters + # https://en.wikipedia.org/wiki/Unicode_control_characters defp escape(<>, mode) when char <= 0x1F or char == 0x7F do [seq(char) | escape(rest, mode)] end @@ -147,8 +154,8 @@ defimpl Poison.Encoder, for: BitString do [seq(char) | escape(rest, :unicode)] end - # http://en.wikipedia.org/wiki/UTF-16#Example_UTF-16_encoding_procedure - # http://unicodebook.readthedocs.org/unicode_encodings.html + # https://en.wikipedia.org/wiki/UTF-16#U+D800_to_U+DFFF_(surrogates) + # https://unicodebook.readthedocs.io/unicode_encodings.html defp escape(<>, :unicode) when char > 0xFFFF do code = char - 0x10000 @@ -159,13 +166,28 @@ defimpl Poison.Encoder, for: BitString do ] end - defp escape(<>, mode) - when mode in [:html_safe, :javascript] and char in [0x2028, 0x2029] do - [seq(char) | escape(rest, mode)] + # https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html + # https://rubydoc.info/docs/rails/ActiveSupport%2FJSON%2FEncoding%2Eescape_html_entities_in_json= + defp escape(<>, :html_safe) do + ["\\u0026" | escape(rest, :html_safe)] + end + + defp escape(<>, :html_safe) do + ["\\u003C" | escape(rest, :html_safe)] + end + + defp escape(<::utf8, rest::bits>>, :html_safe) do + ["\\u003E" | escape(rest, :html_safe)] end - defp escape(<>, :html_safe) do - ["\\/" | escape(rest, :html_safe)] + defp escape(<<"\u2028"::utf8, rest::bits>>, mode) + when mode in [:html_safe, :javascript] do + ["\\u2028" | escape(rest, mode)] + end + + defp escape(<<"\u2029"::utf8, rest::bits>>, mode) + when mode in [:html_safe, :javascript] do + ["\\u2029" | escape(rest, mode)] end defp escape(string, mode) do @@ -177,11 +199,11 @@ defimpl Poison.Encoder, for: BitString do @compile {:inline, chunk_size: 3} defp chunk_size(<>, _mode, acc) - when char <= 0x1F or char in '"\\' do + when char <= 0x1F or char in ~c("\\) do acc end - defp chunk_size(<>, :html_safe, acc) do + defp chunk_size(<>, :html_safe, acc) when char in ~c(&<>) do acc end @@ -189,7 +211,7 @@ defimpl Poison.Encoder, for: BitString do chunk_size(rest, mode, acc + 1) end - defp chunk_size(<<_::utf8, _rest::bits>>, :unicode, acc) do + defp chunk_size(<<_codepoint::utf8, _rest::bits>>, :unicode, acc) do acc end @@ -199,7 +221,7 @@ defimpl Poison.Encoder, for: BitString do end defp chunk_size(<>, mode, acc) do - chunk_size(rest, mode, acc + byte_size(<>)) + chunk_size(rest, mode, acc + codepoint_size(codepoint)) end defp chunk_size(<<>>, _mode, acc), do: acc @@ -218,6 +240,16 @@ defimpl Poison.Encoder, for: BitString do s -> ["\\u" | s] end end + + @compile {:inline, codepoint_size: 1} + + defp codepoint_size(codepoint) do + cond do + codepoint <= 0x7FF -> 2 + codepoint <= 0xFFFF -> 3 + true -> 4 + end + end end defimpl Poison.Encoder, for: Integer do @@ -240,7 +272,7 @@ defimpl Poison.Encoder, for: Map do @compile :inline @compile :inline_list_funcs - def encode(map, _) when map_size(map) < 1, do: "{}" + def encode(map, _options) when map_size(map) < 1, do: "{}" def encode(map, options) do map @@ -347,7 +379,7 @@ defimpl Poison.Encoder, for: List do end end -defimpl Poison.Encoder, for: [Range, Stream, MapSet, HashSet, Date.Range] do +defimpl Poison.Encoder, for: [Range, Stream, MapSet, Date.Range] do alias Poison.{Encoder, Pretty} use Pretty diff --git a/lib/poison/parser.ex b/lib/poison/parser.ex index 5e5f2deb..cad8ba6f 100644 --- a/lib/poison/parser.ex +++ b/lib/poison/parser.ex @@ -30,7 +30,7 @@ defmodule Poison.ParseError do <<>> -> "unexpected end of input at position #{pos}" - <> -> + <> -> "unexpected token at position #{pos}: #{escape(token)}" _rest -> @@ -50,31 +50,28 @@ end defmodule Poison.Parser do @moduledoc """ - An RFC 7159 and ECMA 404 conforming JSON parser. + An RFC 8259 and ECMA 404 conforming JSON parser. - See: https://tools.ietf.org/html/rfc7159 - See: http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf + See: https://datatracker.ietf.org/doc/html/rfc8259 + See: https://ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf """ @compile :inline @compile :inline_list_funcs - @compile {:inline_effort, 2500} - @compile {:inline_size, 150} - @compile {:inline_unroll, 3} import Bitwise alias Poison.{Decoder, ParseError} - @typep value :: nil | true | false | map | list | float | integer | String.t() + @type scalar :: nil | true | false | float | integer | String.t() if Code.ensure_loaded?(Decimal) do - @type t :: value | Decimal.t() + @type t :: scalar | Decimal.t() | [t] | %{optional(String.t()) => t} else - @type t :: value + @type t :: scalar | [t] | %{optional(String.t()) => t} end - whitespace = '\s\t\n\r' + whitespace = ~c"\s\t\n\r" digits = ?0..?9 defmacrop syntax_error(skip) do @@ -83,14 +80,14 @@ defmodule Poison.Parser do end end - @spec parse!(iodata | binary, Decoder.options()) :: t | no_return + @spec parse!(iodata, Decoder.options()) :: t def parse!(value, options \\ %{}) def parse!(data, options) when is_bitstring(data) do [value | skip] = value(data, data, :maps.get(:keys, options, nil), :maps.get(:decimal, options, nil), 0) - <<_::binary-size(skip), rest::bits>> = data + <<_skip::binary-size(skip), rest::bits>> = data skip_whitespace(rest, skip, value) rescue exception in ParseError -> @@ -105,25 +102,16 @@ defmodule Poison.Parser do @compile {:inline, value: 5} - defp value(<>, _data, _keys, _decimal, skip) do - case rest do - <<"alse", _rest::bits>> -> [false | skip + 5] - _other -> syntax_error(skip) - end + defp value(<<"null", _rest::bits>>, _data, _keys, _decimal, skip) do + [nil | skip + 4] end - defp value(<>, _data, _keys, _decimal, skip) do - case rest do - <<"rue", _rest::bits>> -> [true | skip + 4] - _other -> syntax_error(skip) - end + defp value(<<"true", _rest::bits>>, _data, _keys, _decimal, skip) do + [true | skip + 4] end - defp value(<>, _data, _keys, _decimal, skip) do - case rest do - <<"ull", _rest::bits>> -> [nil | skip + 4] - _other -> syntax_error(skip) - end + defp value(<<"false", _rest::bits>>, _data, _keys, _decimal, skip) do + [false | skip + 5] end defp value(<>, _data, _keys, decimal, skip) do @@ -192,10 +180,10 @@ defmodule Poison.Parser do defp object_pairs(<>, data, keys, decimal, skip, acc) do start = skip + 1 [name | skip] = string_continue(rest, data, start) - <<_::binary-size(skip), rest::bits>> = data + <<_skip::binary-size(skip), rest::bits>> = data [value | skip] = object_value(rest, data, keys, decimal, skip) - <<_::binary-size(skip), rest::bits>> = data + <<_skip::binary-size(skip), rest::bits>> = data object_pairs_continue(rest, data, keys, decimal, skip, [ {object_name(keys, start, name), value} | acc @@ -256,8 +244,8 @@ defmodule Poison.Parser do @compile {:inline, array_values: 6} - defp array_values(<>, _data, _keys, _decimal, skip, _acc) do - [[] | skip + 1] + defp array_values(<>, _data, _keys, _decimal, skip, acc) do + [acc | skip + 1] end for char <- whitespace do @@ -268,7 +256,7 @@ defmodule Poison.Parser do defp array_values(rest, data, keys, decimal, skip, acc) do [value | skip] = value(rest, data, keys, decimal, skip) - <<_::binary-size(skip), rest::bits>> = data + <<_skip::binary-size(skip), rest::bits>> = data array_values_continue(rest, data, keys, decimal, skip, [value | acc]) end @@ -276,7 +264,7 @@ defmodule Poison.Parser do defp array_values_continue(<>, data, keys, decimal, skip, acc) do [value | skip] = value(rest, data, keys, decimal, skip + 1) - <<_::binary-size(skip), rest::bits>> = data + <<_skip::binary-size(skip), rest::bits>> = data array_values_continue(rest, data, keys, decimal, skip, [value | acc]) end @@ -352,7 +340,7 @@ defmodule Poison.Parser do @compile {:inline, number_exp: 6} - for e <- 'eE' do + for e <- ~c(eE) do defp number_exp(<>, decimal, skip, sign, coef, exp) do [value | skip] = number_exp_continue(rest, skip + 1) number_complete(decimal, skip, sign, coef, exp + value) @@ -424,7 +412,7 @@ defmodule Poison.Parser do # See: https://arxiv.org/pdf/2101.11408.pdf defp number_complete(_decimal, skip, sign, coef, exp) - when exp in -10..10 and coef <= unquote(max_sig) do + when exp in -22..22 and coef <= unquote(max_sig) do if exp < 0 do [coef / pow10(-exp) * sign | skip] else @@ -440,7 +428,7 @@ defmodule Poison.Parser do | skip ] rescue - ArithmeticError -> + ArgumentError -> reraise ParseError, [skip: skip, value: "#{coef * sign}e#{exp}"], __STACKTRACE__ end @@ -486,10 +474,13 @@ defmodule Poison.Parser do end unicode -> - [ - :unicode.characters_to_binary([acc | binary_part(data, skip, len)], :utf8) - | skip + len + 1 - ] + case :unicode.characters_to_binary([acc | binary_part(data, skip, len)], :utf8) do + string when is_binary(string) -> + [string | skip + len + 1] + + _other -> + syntax_error(skip + len) + end true -> [IO.iodata_to_binary([acc | binary_part(data, skip, len)]) | skip + len + 1] @@ -515,27 +506,21 @@ defmodule Poison.Parser do @compile {:inline, string_escape: 5} + defp string_escape(<>, data, skip, _unicode, acc) do + string_escape_unicode(rest, data, skip, acc) + end + for {seq, char} <- Enum.zip(~C("\ntr/fb), ~c("\\\n\t\r/\f\b)) do defp string_escape(<>, data, skip, unicode, acc) do - string_continue(rest, data, skip + 1, unicode, 0, [acc | unquote(<>)]) + string_continue(rest, data, skip + 1, unicode, 0, [acc | [unquote(char)]]) end end - defp string_escape( - <>, - data, - skip, - _unicode, - acc - ) do - string_escape_unicode(rest, data, skip, acc, seq1) - end - defp string_escape(_rest, _data, skip, _unicode, _acc), do: syntax_error(skip) - # http://www.ietf.org/rfc/rfc2781.txt - # http://perldoc.perl.org/Encode/Unicode.html#Surrogate-Pairs - # http://mathiasbynens.be/notes/javascript-encoding#surrogate-pairs + # https://www.ietf.org/rfc/rfc2781.txt + # https://perldoc.perl.org/Encode::Unicode#Surrogate-Pairs + # https://mathiasbynens.be/notes/javascript-encoding#surrogate-pairs defguardp is_hi_surrogate(cp) when cp in 0xD800..0xDBFF defguardp is_lo_surrogate(cp) when cp in 0xDC00..0xDFFF @@ -550,18 +535,23 @@ defmodule Poison.Parser do end end - @compile {:inline, string_escape_unicode: 5} + @compile {:inline, string_escape_unicode: 4} - defp string_escape_unicode(rest, data, skip, acc, seq1) do - cp1 = get_codepoint(seq1, skip) + defp string_escape_unicode(<>, data, skip, acc) do + case get_codepoint(seq1, skip) do + hi when is_hi_surrogate(hi) -> + string_escape_surrogate_pair(rest, data, skip, acc, seq1, hi) - cond do - is_hi_surrogate(cp1) -> string_escape_surrogate_pair(rest, data, skip, acc, seq1, cp1) - is_lo_surrogate(cp1) -> raise ParseError, skip: skip, value: "\\u#{seq1}" - true -> string_continue(rest, data, skip + 5, true, 0, [acc, cp1]) + lo when is_lo_surrogate(lo) -> + raise ParseError, skip: skip, value: "\\u#{seq1}" + + codepoint -> + string_continue(rest, data, skip + 5, true, 0, [acc | [codepoint]]) end end + defp string_escape_unicode(_rest, _data, skip, _acc), do: syntax_error(skip + 1) + @compile {:inline, string_escape_surrogate_pair: 6} defp string_escape_surrogate_pair( @@ -572,11 +562,13 @@ defmodule Poison.Parser do seq1, hi ) do - with lo when is_lo_surrogate(lo) <- get_codepoint(seq2, skip + 6) do - codepoint = 0x10000 + ((hi &&& 0x03FF) <<< 10) + (lo &&& 0x03FF) - string_continue(rest, data, skip + 11, true, 0, [acc, codepoint]) - else - _ -> raise ParseError, skip: skip, value: "\\u#{seq1}\\u#{seq2}" + case get_codepoint(seq2, skip + 6) do + lo when is_lo_surrogate(lo) -> + codepoint = 0x10000 + ((hi &&& 0x03FF) <<< 10) + (lo &&& 0x03FF) + string_continue(rest, data, skip + 11, true, 0, [acc | [codepoint]]) + + _other -> + raise ParseError, skip: skip, value: "\\u#{seq1}\\u#{seq2}" end end diff --git a/mix.exs b/mix.exs index 8b65ca6f..38d1550a 100644 --- a/mix.exs +++ b/mix.exs @@ -11,7 +11,7 @@ defmodule Poison.Mixfile do app: :poison, name: "Poison", version: @version, - elixir: "~> 1.11", + elixir: "~> 1.12", description: "An incredibly fast, pure Elixir JSON library", source_url: "https://github.com/devinus/poison", start_permanent: Mix.env() == :prod, @@ -23,13 +23,13 @@ defmodule Poison.Mixfile do aliases: aliases(), xref: [exclude: [Decimal]], dialyzer: [ - ignore_warnings: ".dialyzer_ignore.exs", plt_add_apps: [:decimal], flags: [ :error_handling, - :race_conditions, - :underspecs, - :unmatched_returns + :extra_return, + :missing_return, + :unmatched_returns, + :underspecs ] ], test_coverage: [tool: ExCoveralls], @@ -46,18 +46,16 @@ defmodule Poison.Mixfile do # Run "mix help compile.app" to learn about applications. def application do # Specify extra applications you'll use from Erlang/Elixir - spec = [extra_applications: []] - - if Mix.env() != :bench do - spec + if Mix.env() == :bench do + [extra_applications: [:eex]] else - Keyword.put_new(spec, :applications, [:logger]) + [] end end - defp elixirc_paths() do + defp elixirc_paths do if Mix.env() == :profile do - ["lib", "profile"] + ~w(lib profile) else ["lib"] end @@ -67,19 +65,22 @@ defmodule Poison.Mixfile do defp deps do [ {:benchee_html, "~> 1.0", only: :bench, runtime: false}, - {:benchee, "~> 1.0", only: :bench, runtime: false}, - {:credo, "~> 1.6", only: [:dev, :test], runtime: false}, - {:decimal, "~> 2.0", optional: true}, - {:dialyxir, "~> 1.1", only: [:dev, :test], runtime: false}, - {:ex_doc, "~> 0.26", only: [:dev, :test], runtime: false}, - {:excoveralls, "~> 0.14", only: :test, runtime: false}, + {:benchee_markdown, "~> 0.3", only: :bench, runtime: false}, + {:benchee, "~> 1.3", only: :bench, runtime: false}, + {:castore, "~> 1.0", only: :test, runtime: false}, + {:credo, "~> 1.7.7-rc", only: [:dev, :test], runtime: false}, + {:decimal, "~> 2.1", optional: true}, + {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}, + {:ex_doc, "~> 0.34", only: [:dev, :test], runtime: false}, + {:excoveralls, "~> 0.18", only: :test, runtime: false}, {:exjsx, "~> 4.0", only: [:bench, :profile], runtime: false}, - {:jason, "~> 1.2", only: [:dev, :test, :bench, :profile], runtime: false}, - {:jiffy, "~> 1.0", only: [:bench, :profile], runtime: false}, - {:json, "~> 1.4", only: [:bench, :profile], runtime: false}, - {:jsone, "~> 1.7", only: [:bench, :profile], runtime: false}, - {:junit_formatter, "~> 3.3", only: :test, runtime: false}, - {:stream_data, "~> 0.5", only: [:dev, :test], runtime: false}, + {:jason, "~> 1.5.0-alpha", only: [:dev, :test, :bench, :profile], runtime: false}, + {:jiffy, "~> 1.1", only: [:bench, :profile], runtime: false}, + {:jsone, "~> 1.8", only: [:bench, :profile], runtime: false}, + {:junit_formatter, "~> 3.4", only: :test, runtime: false}, + {:mix_audit, "~> 2.1", only: [:dev, :test], runtime: false}, + {:stream_data, "~> 1.1", only: [:dev, :test], runtime: false}, + {:thoas, "~> 1.2", only: [:bench, :profile], runtime: false}, {:tiny, "~> 1.0", only: [:bench, :profile], runtime: false} ] end @@ -88,7 +89,12 @@ defmodule Poison.Mixfile do [ main: "Poison", canonical: "https://hexdocs.pm/poison", - extras: ["README.md"] + extras: [ + "README.md", + "CHANGELOG.md": [title: "Changelog"], + LICENSE: [title: "License"] + ], + source_ref: "master" ] end @@ -104,8 +110,12 @@ defmodule Poison.Mixfile do defp aliases do [ "deps.get": [ - fn _ -> - System.cmd("git", ["submodule", "update", "--init"], cd: __DIR__, parallelism: true) + fn _args -> + System.cmd("git", ["submodule", "update", "--init"], + cd: __DIR__, + env: [], + parallelism: true + ) end, "deps.get" ] diff --git a/mix.lock b/mix.lock index f01dbfbf..11ea2739 100644 --- a/mix.lock +++ b/mix.lock @@ -1,36 +1,34 @@ %{ - "benchee": {:hex, :benchee, "1.0.1", "66b211f9bfd84bd97e6d1beaddf8fc2312aaabe192f776e8931cb0c16f53a521", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}], "hexpm", "3ad58ae787e9c7c94dd7ceda3b587ec2c64604563e049b2a0e8baafae832addb"}, - "benchee_html": {:hex, :benchee_html, "1.0.0", "5b4d24effebd060f466fb460ec06576e7b34a00fc26b234fe4f12c4f05c95947", [:mix], [{:benchee, ">= 0.99.0 and < 2.0.0", [hex: :benchee, repo: "hexpm", optional: false]}, {:benchee_json, "~> 1.0", [hex: :benchee_json, repo: "hexpm", optional: false]}], "hexpm", "5280af9aac432ff5ca4216d03e8a93f32209510e925b60e7f27c33796f69e699"}, + "benchee": {:hex, :benchee, "1.3.1", "c786e6a76321121a44229dde3988fc772bca73ea75170a73fd5f4ddf1af95ccf", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "76224c58ea1d0391c8309a8ecbfe27d71062878f59bd41a390266bf4ac1cc56d"}, + "benchee_html": {:hex, :benchee_html, "1.0.1", "1e247c0886c3fdb0d3f4b184b653a8d6fb96e4ad0d0389267fe4f36968772e24", [:mix], [{:benchee, ">= 0.99.0 and < 2.0.0", [hex: :benchee, repo: "hexpm", optional: false]}, {:benchee_json, "~> 1.0", [hex: :benchee_json, repo: "hexpm", optional: false]}], "hexpm", "b00a181af7152431901e08f3fc9f7197ed43ff50421a8347b0c80bf45d5b3fef"}, "benchee_json": {:hex, :benchee_json, "1.0.0", "cc661f4454d5995c08fe10dd1f2f72f229c8f0fb1c96f6b327a8c8fc96a91fe5", [:mix], [{:benchee, ">= 0.99.0 and < 2.0.0", [hex: :benchee, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "da05d813f9123505f870344d68fb7c86a4f0f9074df7d7b7e2bb011a63ec231c"}, - "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, - "certifi": {:hex, :certifi, "2.8.0", "d4fb0a6bb20b7c9c3643e22507e42f356ac090a1dcea9ab99e27e0376d695eba", [:rebar3], [], "hexpm", "6ac7efc1c6f8600b08d625292d4bbf584e14847ce1b6b5c44d983d273e1097ea"}, - "credo": {:hex, :credo, "1.6.1", "7dc76dcdb764a4316c1596804c48eada9fff44bd4b733a91ccbf0c0f368be61e", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "698607fb5993720c7e93d2d8e76f2175bba024de964e160e2f7151ef3ab82ac5"}, - "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"}, + "benchee_markdown": {:hex, :benchee_markdown, "0.3.3", "d48a1d9782693fae6c294fdb12f653bb90088172d467996bedb9887ff41cf4ef", [:mix], [{:benchee, ">= 1.1.0 and < 2.0.0", [hex: :benchee, repo: "hexpm", optional: false]}], "hexpm", "106dab9ae0b448747da89b9af7285b71841f5d8131f37c6612b7370a157860a4"}, + "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, + "castore": {:hex, :castore, "1.0.7", "b651241514e5f6956028147fe6637f7ac13802537e895a724f90bf3e36ddd1dd", [:mix], [], "hexpm", "da7785a4b0d2a021cd1292a60875a784b6caef71e76bf4917bdee1f390455cf5"}, + "credo": {:hex, :credo, "1.7.7-rc.0", "a7fb63b6e30e523355fb545cb089c5cba550b6d92e1a70db131e01a5dbb77368", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "c977f50ada33d6b053122945a0e4e74280585f7f8ac617aca46c913e88cc4de0"}, + "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, - "dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.17", "6f3c7e94170377ba45241d394389e800fb15adc5de51d0a3cd52ae766aafd63f", [:mix], [], "hexpm", "f93ac89c9feca61c165b264b5837bf82344d13bebc634cd575cb711e2e342023"}, + "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, - "ex_doc": {:hex, :ex_doc, "0.26.0", "1922164bac0b18b02f84d6f69cab1b93bc3e870e2ad18d5dacb50a9e06b542a3", [: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", "2775d66e494a9a48355db7867478ffd997864c61c65a47d31c4949459281c78d"}, - "excoveralls": {:hex, :excoveralls, "0.14.4", "295498f1ae47bdc6dce59af9a585c381e1aefc63298d48172efaaa90c3d251db", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e3ab02f2df4c1c7a519728a6f0a747e71d7d6e846020aae338173619217931c1"}, + "ex_doc": {:hex, :ex_doc, "0.34.0", "ab95e0775db3df71d30cf8d78728dd9261c355c81382bcd4cefdc74610bef13e", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "60734fb4c1353f270c3286df4a0d51e65a2c1d9fba66af3940847cc65a8066d7"}, + "excoveralls": {:hex, :excoveralls, "0.18.1", "a6f547570c6b24ec13f122a5634833a063aec49218f6fff27de9df693a15588c", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "d65f79db146bb20399f23046015974de0079668b9abb2f5aac074d078da60b8d"}, "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm", "32e95820a97cffea67830e91514a2ad53b888850442d6d395f53a1ac60c82e07"}, - "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, - "hackney": {:hex, :hackney, "1.18.0", "c4443d960bb9fba6d01161d01cd81173089686717d9490e5d3606644c48d121f", [:rebar3], [{:certifi, "~>2.8.0", [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", "9afcda620704d720db8c6a3123e9848d09c87586dc1c10479c42627b905b5c5e"}, - "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, - "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, - "jiffy": {:hex, :jiffy, "1.0.8", "60e36f00be35e5ac6e6cf2d4caf3bdf3103d4460aff385f543a8d7df2d6d9613", [:rebar3], [], "hexpm", "f9ae986ba5a0854eb48cf6a76192d9367086da86c20197da430630be7c087a4e"}, - "json": {:hex, :json, "1.4.1", "8648f04a9439765ad449bc56a3ff7d8b11dd44ff08ffcdefc4329f7c93843dfa", [:mix], [], "hexpm", "9abf218dbe4ea4fcb875e087d5f904ef263d012ee5ed21d46e9dbca63f053d16"}, - "jsone": {:hex, :jsone, "1.7.0", "1e3bd7d5dd44bb2eb0797dddea1cbf2ddab8d9f29e499a467ca171c23f5984ea", [:rebar3], [], "hexpm", "a3a33712ee6bc8be10cfa21c7c425a299de4c5a8533f9f931e577a6d0e8f5dbd"}, + "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, + "jason": {:hex, :jason, "1.5.0-alpha.2", "42bc9c545bcdf2d5096044449ddcf85235cdbfbaa19792f4a8be581c8f72608e", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:jason_native, ">= 0.0.0", [hex: :jason_native, repo: "hexpm", optional: true]}], "hexpm", "6dcaa0d9fdc22afe9b4d362f17f20844a85f121c50b6e9b9466ac04fe39f3665"}, + "jiffy": {:hex, :jiffy, "1.1.1", "aca10f47aa91697bf24ab9582c74e00e8e95474c7ef9f76d4f1a338d0f5de21b", [:rebar3], [], "hexpm", "62e1f0581c3c19c33a725c781dfa88410d8bff1bbafc3885a2552286b4785c4c"}, + "jsone": {:hex, :jsone, "1.8.1", "6bc74d3863d55d420077346da97c601711017a057f2fd1df65d6d65dd562fbab", [:rebar3], [], "hexpm", "c78918124148c51a7a84c678e39bbc6281f8cb582f1d88584628a98468e99738"}, "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm", "fc3499fed7a726995aa659143a248534adc754ebd16ccd437cd93b649a95091f"}, - "junit_formatter": {:hex, :junit_formatter, "3.3.0", "bd7914d92885f7cf949dbe1dc6bacf76badfb2c1f5f7b3f9433c20e5b6ec42c8", [:mix], [], "hexpm", "4d040410925324b155ae4c7d41e884a0cdebe53b917bee4f22adf152e987a666"}, - "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.15.2", "dc72dfe17eb240552857465cc00cce390960d9a0c055c4ccd38b70629227e97c", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "fd23ae48d09b32eff49d4ced2b43c9f086d402ee4fd4fcb2d7fad97fa8823e75"}, - "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, - "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, - "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, - "nimble_parsec": {:hex, :nimble_parsec, "1.2.0", "b44d75e2a6542dcb6acf5d71c32c74ca88960421b6874777f79153bbbbd7dccc", [:mix], [], "hexpm", "52b2871a7515a5ac49b00f214e4165a40724cf99798d8e4a65e4fd64ebd002c1"}, - "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, - "stream_data": {:hex, :stream_data, "0.5.0", "b27641e58941685c75b353577dc602c9d2c12292dd84babf506c2033cd97893e", [:mix], [], "hexpm", "012bd2eec069ada4db3411f9115ccafa38540a3c78c4c0349f151fc761b9e271"}, + "junit_formatter": {:hex, :junit_formatter, "3.4.0", "d0e8db6c34dab6d3c4154c3b46b21540db1109ae709d6cf99ba7e7a2ce4b1ac2", [:mix], [], "hexpm", "bb36e2ae83f1ced6ab931c4ce51dd3dbef1ef61bb4932412e173b0cfa259dacd"}, + "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, + "makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"}, + "mix_audit": {:hex, :mix_audit, "2.1.3", "c70983d5cab5dca923f9a6efe559abfb4ec3f8e87762f02bab00fa4106d17eda", [:make, :mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.9", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "8c3987100b23099aea2f2df0af4d296701efd031affb08d0746b2be9e35988ec"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, + "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, + "stream_data": {:hex, :stream_data, "1.1.0", "ef3a7cac0f200c43caf3e6caf9be63115851b4f1cde3f21afaab220adc40e3d7", [:mix], [], "hexpm", "cccc411d5facf1bab86e7c671382d164f05f8992574c95349d3c8b317e14d953"}, + "thoas": {:hex, :thoas, "1.2.1", "19a25f31177a17e74004d4840f66d791d4298c5738790fa2cc73731eb911f195", [:rebar3], [], "hexpm", "e38697edffd6e91bd12cea41b155115282630075c2a727e7a6b2947f5408b86a"}, "tiny": {:hex, :tiny, "1.0.1", "535ea7e600cb1c6ba17b53029266d9d7ec54ce29bfb05d906c433907acfa01ca", [:mix], [], "hexpm", "1278a457deb8d99135c378b71f66f21e283d8adea426252a3f8f5e003c536a7b"}, - "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, + "yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"}, + "yaml_elixir": {:hex, :yaml_elixir, "2.9.0", "9a256da867b37b8d2c1ffd5d9de373a4fda77a32a45b452f1708508ba7bbcb53", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "0cb0e7d4c56f5e99a6253ed1a670ed0e39c13fc45a6da054033928607ac08dfc"}, } diff --git a/profile/profiler.ex b/profile/profiler.ex index 2410979d..42a0a086 100644 --- a/profile/profiler.ex +++ b/profile/profiler.ex @@ -11,6 +11,7 @@ defmodule Poison.Profiler do path |> Path.basename(".json") |> String.replace(~r/-+/, "_") + # credo:disable-for-next-line Credo.Check.Warning.UnsafeToAtom |> String.to_atom() value = File.read!(path) diff --git a/test/poison/encoder_test.exs b/test/poison/encoder_test.exs index e136dbbc..74774313 100644 --- a/test/poison/encoder_test.exs +++ b/test/poison/encoder_test.exs @@ -4,13 +4,14 @@ defmodule Poison.EncoderTest do import Poison.TestGenerators + import Poison, only: [encode!: 1, encode!: 2] alias Poison.{EncodeError, Encoder} test "Atom" do - assert to_json(nil) == "null" - assert to_json(true) == "true" - assert to_json(false) == "false" - assert to_json(:poison) == ~s("poison") + assert encode!(nil) == "null" + assert encode!(true) == "true" + assert encode!(false) == "false" + assert encode!(:poison) == ~s("poison") end property "Atom" do @@ -19,57 +20,62 @@ defmodule Poison.EncoderTest do # credo:disable-for-next-line Credo.Check.Warning.UnsafeToAtom value = String.to_atom(str) ) do - assert to_json(value) == inspect(str) + assert encode!(value) == inspect(str) end end test "Integer" do - assert to_json(42) == "42" + assert encode!(42) == "42" + assert encode!(576_460_752_303_423_488) == "576460752303423488" end property "Integer" do check all(value <- integer()) do - assert to_json(value) == to_string(value) + assert encode!(value) == to_string(value) end end test "Float" do - assert to_json(99.99) == "99.99" - assert to_json(9.9e100) == "9.9e100" + assert encode!(99.99) == "99.99" + assert encode!(9.9e100) == "9.9e100" + assert encode!(9.9e-100) == "9.9e-100" end property "Float" do check all(value <- float()) do - assert to_json(value) == to_string(value) + assert encode!(value) == to_string(value) end end test "BitString" do - assert to_json("hello world") == ~s("hello world") - assert to_json("hello\nworld") == ~s("hello\\nworld") - assert to_json("\nhello\nworld\n") == ~s("\\nhello\\nworld\\n") - - assert to_json("\"") == ~s("\\"") - assert to_json("\0") == ~s("\\u0000") - assert to_json(<<31>>) == ~s("\\u001F") - assert to_json("☃", escape: :unicode) == ~s("\\u2603") - assert to_json("𝄞", escape: :unicode) == ~s("\\uD834\\uDD1E") - assert to_json("\u2028\u2029", escape: :javascript) == ~s("\\u2028\\u2029") - assert to_json("", escape: :html_safe) == ~s("<\\/script>") - - assert to_json("\uCCCC\uCCCC", escape: :html_safe) == - ~s("쳌<\\/script>쳌") - - assert to_json(~s(), - escape: :html_safe - ) == ~s("", escape: :html_safe) == ~s("\\u003C/script\\u003E") + + assert encode!("\uCCCC\uCCCC", escape: :html_safe) == + ~s("쳌\\u003C/script\\u003E쳌") + + assert encode!(~s(), escape: :html_safe) == + ~s("\\u003Cscript\\u003Evar s = \\\"\\u2028\\u2029\\\";\\u003C/script\\u003E") + + assert encode!("", escape: :html_safe) == ~s("\\u003C!-- comment --\\u003E") + assert encode!("one & two", escape: :html_safe) == ~s("one \\u0026 two") + + assert encode!("áéíóúàèìòùâêîôûãẽĩõũ") == ~s("áéíóúàèìòùâêîôûãẽĩõũ") end property "BitString" do - check all(value <- json_string()) do - assert to_json(value) == inspect(value) + check all(value <- json_string(min_length: 1)) do + assert encode!(value) == inspect(value) end check all( @@ -78,7 +84,7 @@ defmodule Poison.EncoderTest do <> = elem ) do seq = codepoint |> Integer.to_string(16) |> String.pad_leading(4, "0") - assert to_json(<>, escape: :unicode) == ~s("\\u#{seq}") + assert encode!(<>, escape: :unicode) == ~s("\\u#{seq}") end check all( @@ -89,111 +95,73 @@ defmodule Poison.EncoderTest do seq2 = lo |> Integer.to_string(16) |> String.pad_leading(4, "0") <> = <> value = :unicode.characters_to_binary([codepoint], :utf16, :utf8) - assert to_json(value, escape: :unicode) == ~s("\\u#{seq1}\\u#{seq2}") + assert encode!(value, escape: :unicode) == ~s("\\u#{seq1}\\u#{seq2}") end end property "List" do check all(value <- json_list(min_length: 1)) do - assert String.match?(to_json(value), ~r/^\[.*\]$/) + assert String.match?(encode!(value), ~r/^\[(([^,]+,)|[^\]]+){1,#{length(value)}}\]$/) end end test "Map" do - assert to_json(%{}) == "{}" - assert to_json(%{"foo" => "bar"}) == ~s({"foo":"bar"}) - assert to_json(%{foo: :bar}) == ~s({"foo":"bar"}) - assert to_json(%{42 => :bar}) == ~s({"42":"bar"}) - - assert to_json(%{foo: %{bar: %{baz: "baz"}}}, pretty: true) == """ - { - "foo": { - "bar": { - "baz": "baz" - } - } - }\ - """ + assert encode!(%{}) == "{}" + assert encode!(%{"foo" => "bar"}) == ~s({"foo":"bar"}) + assert encode!(%{foo: :bar}) == ~s({"foo":"bar"}) + assert encode!(%{42 => :bar}) == ~s({"42":"bar"}) + + assert encode!(%{foo: %{bar: %{baz: "baz"}}}, pretty: true) == + "{\n \"foo\": {\n \"bar\": {\n \"baz\": \"baz\"\n }\n }\n}" multi_key_map = %{"foo" => "foo1", :foo => "foo2"} - assert to_json(multi_key_map) == ~s({"foo":"foo1","foo":"foo2"}) + assert encode!(multi_key_map) == ~s({"foo":"foo1","foo":"foo2"}) error = %EncodeError{message: "duplicate key found: :foo", value: "foo"} assert Poison.encode(multi_key_map, strict_keys: true) == {:error, error} end property "Map" do check all(value <- json_map(min_length: 1)) do - assert String.match?(to_json(value), ~r/^{.*}$/) + assert String.match?(encode!(value), ~r/^\{([^:]+:[^,]+){1,#{map_size(value)}}\}$/) end end test "Range" do - assert to_json(1..3) == "[1,2,3]" - - assert to_json(1..3, pretty: true) == """ - [ - 1, - 2, - 3 - ]\ - """ + assert encode!(1..3) == "[1,2,3]" + assert encode!(1..3, pretty: true) == "[\n 1,\n 2,\n 3\n]" end test "Stream" do range = 1..10 - assert to_json(Stream.take(range, 0)) == "[]" - assert to_json(Stream.take(range, 3)) == "[1,2,3]" - - assert to_json(Stream.take(range, 3), pretty: true) == """ - [ - 1, - 2, - 3 - ]\ - """ + assert encode!(Stream.take(range, 0)) == "[]" + assert encode!(Stream.take(range, 3)) == "[1,2,3]" + assert encode!(Stream.take(range, 3), pretty: true) == "[\n 1,\n 2,\n 3\n]" end - # MapSet/HashSet have an unspecified order - - test "MapSet/HashSet" do - for type <- [MapSet, HashSet] do - set = type.new - assert to_json(set) == "[]" - - set = set |> type.put(1) |> type.put(2) - - assert to_json(set) in ~w([1,2] [2,1]) - - assert to_json(set, pretty: true) in [ - """ - [ - 1, - 2 - ]\ - """, - """ - [ - 2, - 1 - ]\ - """ - ] - end + # MapSet have an unspecified order + + test "MapSet" do + set = MapSet.new() + assert encode!(set) == "[]" + + set = set |> MapSet.put(1) |> MapSet.put(2) + assert encode!(set) in ~w([1,2] [2,1]) + assert encode!(set, pretty: true) in ["[\n 1,\n 2\n]", "[\n 2,\n 1\n]"] end test "Time" do {:ok, time} = Time.new(12, 13, 14) - assert to_json(time) == ~s("12:13:14") + assert encode!(time) == ~s("12:13:14") end test "Date" do {:ok, date} = Date.new(2000, 1, 1) - assert to_json(date) == ~s("2000-01-01") + assert encode!(date) == ~s("2000-01-01") end test "NaiveDateTime" do {:ok, datetime} = NaiveDateTime.new(2000, 1, 1, 12, 13, 14) - assert to_json(datetime) == ~s("2000-01-01T12:13:14") + assert encode!(datetime) == ~s("2000-01-01T12:13:14") end test "DateTime" do @@ -211,7 +179,7 @@ defmodule Poison.EncoderTest do utc_offset: 3600 } - assert to_json(datetime) == ~s("2000-01-01T12:13:14+00:30") + assert encode!(datetime) == ~s("2000-01-01T12:13:14+00:30") datetime = %DateTime{ year: 2000, @@ -227,22 +195,27 @@ defmodule Poison.EncoderTest do utc_offset: 0 } - assert to_json(datetime) == ~s("2000-01-01T12:13:14.050Z") + assert encode!(datetime) == ~s("2000-01-01T12:13:14.050Z") + end + + test "Date.Range" do + assert encode!(Date.range(~D[1969-08-15], ~D[1969-08-18])) == + ~s(["1969-08-15","1969-08-16","1969-08-17","1969-08-18"]) end test "URI" do uri = URI.parse("https://devinus.io") - assert to_json(uri) == ~s("https://devinus.io") + assert encode!(uri) == ~s("https://devinus.io") end test "Decimal" do decimal = Decimal.new("99.9") - assert to_json(decimal) == "99.9" + assert encode!(decimal) == "99.9" end property "Decimal" do check all(value <- map(float(), &Decimal.from_float/1)) do - assert to_json(value) == to_string(value) + assert encode!(value) == to_string(value) end end @@ -276,7 +249,7 @@ defmodule Poison.EncoderTest do size: 10 } - assert Poison.decode!(to_json(derived_using_only)) == %{ + assert Poison.decode!(encode!(derived_using_only)) == %{ "name" => "derived using :only" } @@ -285,12 +258,12 @@ defmodule Poison.EncoderTest do size: 10 } - assert Poison.decode!(to_json(derived_using_except)) == %{"size" => 10} + assert Poison.decode!(encode!(derived_using_except)) == %{"size" => 10} end test "EncodeError" do assert_raise EncodeError, fn -> - to_json(self()) + encode!(make_ref()) end end @@ -305,13 +278,7 @@ defmodule Poison.EncoderTest do offset: positive_integer() }) ) do - assert to_json(value, options) != "" + assert encode!(value, options) != "" end end - - defp to_json(value, options \\ []) do - value - |> Encoder.encode(Map.new(options)) - |> IO.iodata_to_binary() - end end diff --git a/test/poison/parser_test.exs b/test/poison/parser_test.exs index 8872b070..6acba411 100644 --- a/test/poison/parser_test.exs +++ b/test/poison/parser_test.exs @@ -34,25 +34,25 @@ defmodule Poison.ParserTest do parse!("1.0e+") end - assert parse!("0") == 0 - assert parse!("1") == 1 - assert parse!("-0") == 0 - assert parse!("-1") == -1 - assert parse!("0.1") == 0.1 - assert parse!("-0.1") == -0.1 - assert parse!("0e0") == 0 - assert parse!("0E0") == 0 - assert parse!("1e0") == 1 - assert parse!("1E0") == 1 - assert parse!("1.0e0") == 1.0 - assert parse!("1e+0") == 1 - assert parse!("1.0e+0") == 1.0 - assert parse!("0.1e1") == 0.1e1 - assert parse!("0.1e-1") == 0.1e-1 - assert parse!("99.99e99") == 99.99e99 + assert parse!("0") === 0 + assert parse!("1") === 1 + assert parse!("-0") === 0 + assert parse!("-1") === -1 + assert parse!("0.1") === 0.1 + assert parse!("-0.1") === -0.1 + assert parse!("0e0") === 0 + assert parse!("0E0") === 0 + assert parse!("1e0") === 1 + assert parse!("1E0") === 1 + assert parse!("1.0e0") === 1.0 + assert parse!("1e+0") === 1 + assert parse!("1.0e+0") === 1.0 + assert parse!("0.1e1") === 1 + assert parse!("0.1e-1") === 0.1e-1 + assert parse!("99.99e99") === 99.99e99 # credo:disable-for-next-line Credo.Check.Readability.LargeNumbers - assert parse!("123456789.123456789e123") == 1.234567891234568e131 + assert parse!("123456789.123456789e123") === 1.234567891234568e131 assert parse!("0", %{decimal: true}) == Decimal.new("0") assert parse!("-0", %{decimal: true}) == Decimal.new("-0") @@ -69,7 +69,7 @@ defmodule Poison.ParserTest do property "number" do check all(int <- integer()) do - assert parse!(Integer.to_string(int)) == int + assert parse!(Integer.to_string(int)) === int end check all(value <- float()) do @@ -244,12 +244,12 @@ defmodule Poison.ParserTest do end test "atom keys" do - hash = :erlang.phash2(:crypto.strong_rand_bytes(8)) + uint = :erlang.unique_integer([:positive]) assert_raise ParseError, - ~s(cannot parse value at position 2: "key#{hash}"), + ~s(cannot parse value at position 2: "key#{uint}"), fn -> - parse!(~s({"key#{hash}": null}), %{keys: :atoms!}) + parse!(~s({"key#{uint}": null}), %{keys: :atoms!}) end assert parse!(~s({"foo": "bar"}), %{keys: :atoms!}) == %{foo: "bar"} diff --git a/test/test_helper.exs b/test/test_helper.exs index bab4c540..a8c42ad7 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -23,22 +23,17 @@ defmodule Poison.TestGenerators do end def json_map(options \\ []) do - map_of(json_string(), json_value(), options) + map_of(json_string(min_length: 1), json_value(), options) end def json_complex_value do - one_of([ + tree( json_value(), - json_list(), - map_of( - json_string(), - one_of([ - json_value(), - json_list(), - json_map() - ]) - ) - ]) + &one_of([ + list_of(&1), + map_of(json_string(min_length: 1), &1) + ]) + ) end end diff --git a/vendor/JSONTestSuite b/vendor/JSONTestSuite index d64aefb5..984defc2 160000 --- a/vendor/JSONTestSuite +++ b/vendor/JSONTestSuite @@ -1 +1 @@ -Subproject commit d64aefb55228d9584d3e5b2433f720ea8fd00c82 +Subproject commit 984defc2deaa653cb73cd29f4144a720ec9efe7c