Skip to content

Commit

Permalink
Improvements
Browse files Browse the repository at this point in the history
- Now passes the [JSONTestSuite][1]
- Adds static code analysis using [Credo][2]
- Moves the benchmarking suite to [Benchee][3]
- More consistent API

[1]: https://github.com/nst/JSONTestSuite
[2]: http://credo-ci.org
[3]: https://github.com/PragTob/benchee
  • Loading branch information
Devin Torres committed May 30, 2017
1 parent 4f2a543 commit df580bb
Show file tree
Hide file tree
Showing 33 changed files with 125,721 additions and 2,234 deletions.
138 changes: 138 additions & 0 deletions .credo.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# This file contains the configuration for Credo and you are probably reading
# this after creating it with `mix credo.gen.config`.
#
# If you find anything wrong or unclear in this file, please report an
# issue on GitHub: https://github.com/rrrene/credo/issues
#
%{
#
# You can have as many configs as you like in the `configs:` field.
configs: [
%{
#
# Run any exec using `mix credo -C <name>`. If no exec name is given
# "default" is used.
name: "default",
#
# These are the files included in the analysis:
files: %{
#
# You can give explicit globs or simply directories.
# In the latter case `**/*.{ex,exs}` will be used.
included: ["lib/", "src/", "web/", "apps/"],
excluded: [~r"/_build/", ~r"/deps/"]
},
#
# If you create your own checks, you must specify the source files for
# them here, so they can be loaded by Credo before running the analysis.
requires: [],
#
# Credo automatically checks for updates, like e.g. Hex does.
# You can disable this behaviour below:
check_for_updates: true,
#
# If you want to enforce a style guide and need a more traditional linting
# experience, you can change `strict` to `true` below:
strict: false,
#
# If you want to use uncolored output by default, you can change `color`
# to `false` below:
color: true,
#
# You can customize the parameters of any check by adding a second element
# to the tuple.
#
# To disable a check put `false` as second element:
#
# {Credo.Check.Design.DuplicatedCode, false}
#
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},

# For some checks, like AliasUsage, you can only customize the priority
# Priority values are: `low, normal, high, higher`
{Credo.Check.Design.AliasUsage, priority: :low},

# For others you can set parameters

# If you don't want the `setup` and `test` macro calls in ExUnit tests
# or the `schema` macro in Ecto schemas to trigger DuplicatedCode, just
# set the `excluded_macros` parameter to `[:schema, :setup, :test]`.
{Credo.Check.Design.DuplicatedCode, excluded_macros: []},

# 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},

{Credo.Check.Readability.FunctionNames},
{Credo.Check.Readability.LargeNumbers},
{Credo.Check.Readability.MaxLineLength, priority: :low, max_length: 100},
{Credo.Check.Readability.ModuleAttributeNames},
{Credo.Check.Readability.ModuleDoc, false},
{Credo.Check.Readability.ModuleNames},
{Credo.Check.Readability.ParenthesesOnZeroArityDefs},
{Credo.Check.Readability.ParenthesesInCondition},
{Credo.Check.Readability.PredicateFunctionNames},
{Credo.Check.Readability.PreferImplicitTry},
{Credo.Check.Readability.RedundantBlankLines},
{Credo.Check.Readability.StringSigils},
{Credo.Check.Readability.TrailingBlankLine},
{Credo.Check.Readability.TrailingWhiteSpace},
{Credo.Check.Readability.VariableNames},
{Credo.Check.Readability.Semicolons},
{Credo.Check.Readability.SpaceAfterCommas},

{Credo.Check.Refactor.DoubleBooleanNegation, false},
{Credo.Check.Refactor.CondStatements},
{Credo.Check.Refactor.CyclomaticComplexity},
{Credo.Check.Refactor.FunctionArity},
{Credo.Check.Refactor.MatchInCondition},
{Credo.Check.Refactor.NegatedConditionsInUnless},
{Credo.Check.Refactor.NegatedConditionsWithElse},
{Credo.Check.Refactor.Nesting},
{Credo.Check.Refactor.PipeChainStart},
{Credo.Check.Refactor.UnlessWithElse},

{Credo.Check.Warning.BoolOperationOnSameValues},
{Credo.Check.Warning.IExPry},
{Credo.Check.Warning.IoInspect},
{Credo.Check.Warning.LazyLogging},
{Credo.Check.Warning.OperationOnSameValues},
{Credo.Check.Warning.OperationWithConstantResult},
{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},

# Controversial and experimental checks (opt-in, just remove `, false`)
#
{Credo.Check.Refactor.ABCSize, false},
{Credo.Check.Refactor.AppendSingleItem, false},
{Credo.Check.Refactor.VariableRebinding, false},
{Credo.Check.Warning.MapGetUnsafePass, false},
{Credo.Check.Consistency.MultiAliasImportRequireUse, false},

# Deprecated checks (these will be deleted after a grace period)
{Credo.Check.Readability.Specs, false},
{Credo.Check.Warning.NameRedeclarationByAssignment, false},
{Credo.Check.Warning.NameRedeclarationByCase, false},
{Credo.Check.Warning.NameRedeclarationByDef, false},
{Credo.Check.Warning.NameRedeclarationByFn, false},

# Custom checks can be created using `mix credo.gen.check`.
#
]
}
]
}
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ end_of_line = lf
insert_final_newline = true

# 2 space indentation
[*.{ex,exs}]
[*.{ex,exs,json,yml}]
indent_style = space
indent_size = 2
18 changes: 16 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
# The directory Mix will write compiled artifacts to.
/_build
/bench/snapshots
/bench/graphs

# If you run "mix test --cover", coverage assets end up here.
/cover

# The directory Mix downloads your dependencies sources to.
/deps

# Where 3rd-party dependencies like ExDoc output generated docs.
/doc

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

/bench/output
17 changes: 5 additions & 12 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,19 +1,12 @@
language: elixir
elixir:
- 1.4.4
- 1.4.3
- 1.4.2
- 1.4.1
- 1.4.0
- 1.3.4
- 1.3.3
- 1.3.2
- 1.3.1
- 1.3.0
- 1.2.6
- 1.2.5
- 1.2.4
- 1.2.3
- 1.2.2
- 1.2.1
- 1.2.0
otp_release:
- 19.3
- 19.2
- 19.1
- 19.0
Expand Down
114 changes: 85 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,19 @@ Poison uses extensive [sub binary matching][1], a **hand-rolled parser** using
several techniques that are [known to benefit HiPE][2] for native compilation,
[IO list][3] encoding and **single-pass** decoding.

Preliminary benchmarking has sometimes put Poison's performance closer to
`jiffy`, and almost always faster than existing Elixir libraries.
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 the
[JSONTestSuite][6].

## Installation

First, add Poison to your `mix.exs` dependencies:

```elixir
def deps do
[{:poison, "~> 3.0"}]
[{:poison, "~> 4.0"}]
end
```

Expand Down Expand Up @@ -52,15 +55,11 @@ Poison.decode!(~s({"people": [{"name": "Devin Torres", "age": 27}]}),
#=> %{"people" => [%Person{age: 27, name: "Devin Torres"}]}
```

Every component of Poison -- the encoder, decoder, and parser -- are all usable
on their own without buying into other functionality. For example, if you were
Every component of Poison (encoder, decoder, and parser) are all usable on
their own without buying into other functionality. For example, if you were
interested purely in the speed of parsing JSON without a decoding step, you
could simply call `Poison.Parser.parse`.

If you use Poison 1.x, you have to set a module to `as` option in order to
decode into a struct. e.g. `as: Person` instead of `as: %Person{}`. The change
was introduced in 2.0.0.

## Parser

```iex
Expand Down Expand Up @@ -91,7 +90,7 @@ iex> IO.puts Poison.Encoder.encode([1, 2, 3], [])
```

Anything implementing the Encoder protocol is expected to return an
[IO list][4] to be embedded within any other Encoder's implementation and
[IO list][5] to be embedded within any other Encoder's implementation and
passable to any IO subsystem without conversion.

```elixir
Expand All @@ -102,14 +101,14 @@ defimpl Poison.Encoder, for: Person do
end
```

For maximum performance, make sure you `@derive [Poison.Encoder]` for any struct
you plan on encoding.
For maximum performance, make sure you `@derive [Poison.Encoder]` for any
struct you plan on encoding.

### Encoding only some attributes

When deriving structs for encoding, it is possible to select or exclude specific
attributes. This is achieved by deriving `Poison.Encoder` with the `:only` or
`:except` options set:
When deriving structs for encoding, it is possible to select or exclude
specific attributes. This is achieved by deriving `Poison.Encoder` with the
`:only` or `:except` options set:

```elixir
defmodule PersonOnlyName do
Expand All @@ -128,18 +127,19 @@ ignored.

### Key Validation

According to [the JSON spec](https://tools.ietf.org/html/rfc7159#section-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 parsing JSON of this type, Chromium
will override all previous values with the final one.
According to [RFC 7159][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
parsing JSON of this type, Chromium will override all previous values with the
final one.

Like Ruby, Poison will also generate JSON with duplicate keys. If you'd like to
Poison will generate JSON with duplicate keys if you attempt to encode a map
with atom and string keys whose encoded names would clash. If you'd like to
ensure that your generated JSON doesn't have this issue, you can pass the
`strict_keys: true` option when encoding. This will force the encoding to fail.

Note that validating keys can cause a small performance hit.
*Note:* Validating keys can cause a small performance hit.

```iex
iex> Poison.encode!(%{:foo => "foo1", "foo" => "foo2"}, strict_keys: true)
Expand All @@ -149,17 +149,73 @@ iex> Poison.encode!(%{:foo => "foo1", "foo" => "foo2"}, strict_keys: true)
## Benchmarking

```sh-session
$ mix deps.get
$ MIX_ENV=bench mix compile
$ MIX_ENV=bench mix bench
$ MIX_ENV=bench mix run bench/runs.exs
```

### Current Benchmarks

As of 2017-05-15 on a 2.8 GHz Intel Core i7:

```
## EncoderBench
benchmark name iterations average time
maps (jiffy) 500000 7.88 µs/op
structs (Poison) 200000 9.46 µs/op
structs (Jazz) 100000 15.43 µs/op
structs (JSX) 100000 18.45 µs/op
maps (Poison) 100000 19.45 µs/op
maps (Jazz) 100000 21.61 µs/op
maps (JSX) 50000 31.76 µs/op
maps (JSON) 50000 34.08 µs/op
structs (JSON) 50000 47.56 µs/op
strings (jiffy) 10000 107.68 µs/op
lists (Poison) 10000 120.79 µs/op
string escaping (jiffy) 10000 139.92 µs/op
lists (jiffy) 10000 229.18 µs/op
lists (Jazz) 10000 236.86 µs/op
strings (JSON) 10000 237.97 µs/op
strings (JSX) 10000 283.87 µs/op
lists (JSX) 5000 336.96 µs/op
jiffy 5000 429.92 µs/op
strings (Jazz) 5000 430.78 µs/op
jiffy (pretty) 5000 431.55 µs/op
lists (JSON) 5000 559.31 µs/op
strings (Poison) 5000 574.26 µs/op
string escaping (Jazz) 1000 1313.51 µs/op
string escaping (JSX) 1000 1474.66 µs/op
Poison 1000 1546.53 µs/op
string escaping (Poison) 1000 1728.66 µs/op
Poison (pretty) 1000 1784.37 µs/op
Jazz 1000 2060.77 µs/op
JSON 1000 2250.89 µs/op
JSX 1000 2252.77 µs/op
Jazz (pretty) 1000 2317.55 µs/op
JSX (pretty) 500 5577.33 µs/op
## ParserBench
benchmark name iterations average time
UTF-8 unescaping (jiffy) 50000 60.05 µs/op
UTF-8 unescaping (Poison) 10000 112.53 µs/op
UTF-8 unescaping (JSX) 10000 282.83 µs/op
UTF-8 unescaping (JSON) 5000 469.26 µs/op
jiffy 5000 479.07 µs/op
Poison 5000 730.85 µs/op
JSX 1000 1947.77 µs/op
JSON 500 5175.11 µs/op
Issue 90 (jiffy) 100 18864.70 µs/op
Issue 90 (Poison) 50 50091.16 µs/op
Issue 90 (JSX) 10 155975.20 µs/op
Issue 90 (JSON) 1 1964860.00 µs/op
```

## License

Poison is released under [CC0-1.0][5] (see `LICENSE`).
Poison is released under [CC0-1.0][6] (see [`LICENSE`](LICENSE)).

[1]: http://www.erlang.org/euc/07/papers/1700Gustafsson.pdf
[2]: http://www.erlang.org/workshop/2003/paper/p36-sagonas.pdf
[3]: http://jlouisramblings.blogspot.com/2013/07/problematic-traits-in-erlang.html
[4]: http://prog21.dadgum.com/70.html
[5]: https://creativecommons.org/publicdomain/zero/1.0/
[4]: https://tools.ietf.org/html/rfc7159
[5]: http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf
[6]: https://github.com/nst/JSONTestSuite
[7]: http://prog21.dadgum.com/70.html
[8]: https://creativecommons.org/publicdomain/zero/1.0/
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.1.0
4.0.0-dev
Loading

0 comments on commit df580bb

Please sign in to comment.