smarter_json 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of smarter_json might be problematic. Click here for more details.
- checksums.yaml +7 -0
- data/.gitignore +46 -0
- data/CHANGELOG.md +75 -0
- data/LICENSE.txt +21 -0
- data/README.md +110 -0
- data/Rakefile +22 -0
- data/docs/_introduction.md +48 -0
- data/docs/basic_read_api.md +72 -0
- data/docs/basic_write_api.md +144 -0
- data/docs/examples.md +140 -0
- data/docs/options.md +69 -0
- data/ext/smarter_json/extconf.rb +30 -0
- data/ext/smarter_json/smarter_json.c +1424 -0
- data/ext/smarter_json/smarter_json.h +9 -0
- data/ext/smarter_json/vendor/ryu.h +819 -0
- data/ext/smarter_json/vendor/ryu.md +22 -0
- data/lib/smarter_json/errors.rb +28 -0
- data/lib/smarter_json/generator.rb +209 -0
- data/lib/smarter_json/parser.rb +926 -0
- data/lib/smarter_json/version.rb +5 -0
- data/lib/smarter_json.rb +24 -0
- metadata +86 -0
data/docs/examples.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
|
|
2
|
+
### Contents
|
|
3
|
+
|
|
4
|
+
* [Introduction](./_introduction.md)
|
|
5
|
+
* [The Basic Read API](./basic_read_api.md)
|
|
6
|
+
* [The Basic Write API](./basic_write_api.md)
|
|
7
|
+
* [Configuration Options](./options.md)
|
|
8
|
+
* [**Examples**](./examples.md)
|
|
9
|
+
|
|
10
|
+
--------------
|
|
11
|
+
|
|
12
|
+
# Examples
|
|
13
|
+
|
|
14
|
+
**Rescue from `SmarterJSON::Error` (recommended):** SmarterJSON raises only on genuinely unparseable input (an unterminated string, a mismatched bracket), with line and column in the message. Rescuing from `SmarterJSON::Error` lets your application handle bad input gracefully.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
1. [Read a JSON String](#example-1-read-a-json-string)
|
|
19
|
+
2. [Read a JSON File](#example-2-read-a-json-file)
|
|
20
|
+
3. [Implicit Root Object (config-style, no braces)](#example-3-implicit-root-object-config-style-no-braces)
|
|
21
|
+
4. [Multiple Documents (NDJSON) → Array](#example-4-multiple-documents-ndjson--array)
|
|
22
|
+
5. [Streaming a Large File with a Block](#example-5-streaming-a-large-file-with-a-block)
|
|
23
|
+
6. [Symbolize Keys](#example-6-symbolize-keys)
|
|
24
|
+
7. [Duplicate Keys](#example-7-duplicate-keys)
|
|
25
|
+
8. [High-Precision Numbers: BigDecimal vs Float](#example-8-high-precision-numbers-bigdecimal-vs-float)
|
|
26
|
+
9. [Lenient Input: Comments, Trailing Commas, Unquoted Keys](#example-9-lenient-input-comments-trailing-commas-unquoted-keys)
|
|
27
|
+
10. [Write JSON](#example-10-write-json)
|
|
28
|
+
11. [Write NDJSON](#example-11-write-ndjson)
|
|
29
|
+
12. [Round-Trip Read and Write](#example-12-round-trip-read-and-write)
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
### Example 1: Read a JSON String
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
require "smarter_json"
|
|
37
|
+
|
|
38
|
+
SmarterJSON.process('{"a": 1, "b": [2, 3]}') # => {"a"=>1, "b"=>[2, 3]}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Example 2: Read a JSON File
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
SmarterJSON.process_file("config.json") # => the parsed value
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`process_file` opens the file, reads it with the labeled [`encoding:`](./options.md) (default `"UTF-8"`), and parses it.
|
|
48
|
+
|
|
49
|
+
### Example 3: Implicit Root Object (config-style, no braces)
|
|
50
|
+
|
|
51
|
+
A config file that starts with `key: value` and has no outer `{}` is read as an object:
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
SmarterJSON.process("host: localhost\nport: 5432") # => {"host"=>"localhost", "port"=>5432}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Example 4: Multiple Documents (NDJSON) → Array
|
|
58
|
+
|
|
59
|
+
Plain `process` reads NDJSON / JSONL / concatenated documents with no block and no special method. Zero documents → `nil`, one → its value, two or more → an `Array`:
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
SmarterJSON.process(%({"id":1}\n{"id":2}\n{"id":3})) # => [{"id"=>1}, {"id"=>2}, {"id"=>3}]
|
|
63
|
+
SmarterJSON.process('{"id":1}') # => {"id"=>1}
|
|
64
|
+
SmarterJSON.process("") # => nil
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Example 5: Streaming a Large File with a Block
|
|
68
|
+
|
|
69
|
+
For input larger than memory, pass a block. Each document is yielded as it is read; the whole file is never loaded:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
SmarterJSON.process_file("events.ndjson") { |event| EventJob.perform_async(event) }
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Example 6: Symbolize Keys
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
SmarterJSON.process('{"a": 1, "b": 2}', symbolize_keys: true) # => {:a=>1, :b=>2}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Example 7: Duplicate Keys
|
|
82
|
+
|
|
83
|
+
By default the last value wins. Choose `:first_wins` or `:raise` instead:
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
SmarterJSON.process('{"a":1,"a":2}') # => {"a"=>2} (:last_wins, the default)
|
|
87
|
+
SmarterJSON.process('{"a":1,"a":2}', duplicate_key: :first_wins) # => {"a"=>1}
|
|
88
|
+
SmarterJSON.process('{"a":1,"a":2}', duplicate_key: :raise) # raises SmarterJSON::ParseError
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Example 8: High-Precision Numbers: BigDecimal vs Float
|
|
92
|
+
|
|
93
|
+
The default `:auto` keeps high-precision decimals as `BigDecimal` (matching Oj). Force `Float` for raw speed when you don't need the precision:
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
SmarterJSON.process("65.613616999999977") # => BigDecimal (:auto, the default)
|
|
97
|
+
SmarterJSON.process("65.613616999999977", bigdecimal_load: :float) # => 65.613616999999977 (a Float)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Example 9: Lenient Input: Comments, Trailing Commas, Unquoted Keys
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
SmarterJSON.process(<<~JSON)
|
|
104
|
+
{
|
|
105
|
+
host: localhost, # unquoted key, quoteless value, and a trailing comma
|
|
106
|
+
port: 5432,
|
|
107
|
+
/* block comment */
|
|
108
|
+
url: http://example.com
|
|
109
|
+
}
|
|
110
|
+
JSON
|
|
111
|
+
# => {"host"=>"localhost", "port"=>5432, "url"=>"http://example.com"}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
A `#`/`//` only starts a comment when preceded by whitespace, so `http://example.com` stays a string rather than being truncated.
|
|
115
|
+
|
|
116
|
+
### Example 10: Write JSON
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
SmarterJSON.generate({ "a" => 1, "b" => [2, 3] }) # => '{"a":1,"b":[2,3]}'
|
|
120
|
+
SmarterJSON.generate([1, 2, 3]) # => '[1,2,3]'
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Example 11: Write NDJSON
|
|
124
|
+
|
|
125
|
+
An Array writes one element per line:
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
SmarterJSON.generate([{ "id" => 1 }, { "id" => 2 }], format: :ndjson) # => "{\"id\":1}\n{\"id\":2}\n"
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Example 12: Round-Trip Read and Write
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
obj = { "a" => 1, "b" => [2, "three", nil, true] }
|
|
135
|
+
SmarterJSON.process(SmarterJSON.generate(obj)) == obj # => true
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---------------
|
|
139
|
+
|
|
140
|
+
PREVIOUS: [Configuration Options](./options.md) | UP: [README](../README.md)
|
data/docs/options.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
|
|
2
|
+
### Contents
|
|
3
|
+
|
|
4
|
+
* [Introduction](./_introduction.md)
|
|
5
|
+
* [The Basic Read API](./basic_read_api.md)
|
|
6
|
+
* [The Basic Write API](./basic_write_api.md)
|
|
7
|
+
* [**Configuration Options**](./options.md)
|
|
8
|
+
* [Examples](./examples.md)
|
|
9
|
+
|
|
10
|
+
--------------
|
|
11
|
+
|
|
12
|
+
# Configuration Options
|
|
13
|
+
|
|
14
|
+
## Reading
|
|
15
|
+
|
|
16
|
+
These options are passed to [`SmarterJSON.process`](./basic_read_api.md) and `SmarterJSON.process_file` as the second argument; anything you set overrides the defaults below.
|
|
17
|
+
|
|
18
|
+
| Option | Default | Explanation |
|
|
19
|
+
|-------------------|--------------|------------------------------------------------------------------------------------------------------------------------|
|
|
20
|
+
| `:symbolize_keys` | `false` | Return object keys as Symbols instead of Strings. |
|
|
21
|
+
| `:duplicate_key` | `:last_wins` | How to handle a key that repeats within one object: `:last_wins`, `:first_wins`, or `:raise`. |
|
|
22
|
+
| `:bigdecimal_load`| `:auto` | `:auto` keeps high-precision decimals as `BigDecimal` (matches Oj); `:float` forces every number to `Float`; `:bigdecimal` forces every decimal to `BigDecimal`. |
|
|
23
|
+
| `:acceleration` | `true` | Use the C extension when it is compiled and loadable; `false` forces the pure-Ruby parser. Both produce identical results. |
|
|
24
|
+
| `:encoding` | `nil` | Labels the input's encoding (e.g. `"UTF-8"`). It does **not** trigger a transcoding pass — see below. |
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
SmarterJSON.process('{"a": 1}', symbolize_keys: true) # => {:a=>1}
|
|
28
|
+
SmarterJSON.process('{"a":1,"a":2}', duplicate_key: :raise) # raises SmarterJSON::ParseError
|
|
29
|
+
SmarterJSON.process(big_decimal_json, bigdecimal_load: :float) # every number as Float (fastest)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### A note on `:encoding`
|
|
33
|
+
|
|
34
|
+
`:encoding` labels what the input *is* — it does not transcode. The parser works on the bytes in their native encoding and emits string values with the same encoding tag, the same way `smarter_csv` handles encodings. Bytes that are invalid for the claimed encoding raise `SmarterJSON::EncodingError` (a kind of `SmarterJSON::ParseError`). A UTF-8 BOM is handled automatically; UTF-16 / UTF-32 input is out of scope.
|
|
35
|
+
|
|
36
|
+
### A note on `:bigdecimal_load`
|
|
37
|
+
|
|
38
|
+
The default `:auto` preserves high-precision numbers as `BigDecimal`, matching Oj's default. That is intrinsically slower than producing `Float` on number-heavy files (e.g. `canada.json`). For raw speed when you don't need the extra precision, pass `bigdecimal_load: :float`.
|
|
39
|
+
|
|
40
|
+
## Writing
|
|
41
|
+
|
|
42
|
+
These options are passed to [`SmarterJSON.generate`](./basic_write_api.md) as the second argument.
|
|
43
|
+
|
|
44
|
+
| Option | Default | Explanation |
|
|
45
|
+
|------------|---------|-----------------------------------------------------------------------------------------------------------------------------|
|
|
46
|
+
| `:format` | `:json` | `:json` writes standard JSON (Hash → object, Array → array, scalar → scalar). `:ndjson` writes newline-delimited JSON: an Array becomes one element per line, any other value becomes a single line. |
|
|
47
|
+
| `:indent` | `0` | Spaces per nesting level for pretty-printing. `0` (the default) is compact output. Empty objects/arrays stay inline. Not allowed with `:ndjson` (a record must be a single line). |
|
|
48
|
+
| `:sort_keys` | `false` | Emit object keys in sorted order (Symbol keys sorted by their string form). Useful for canonical, diff-friendly output. |
|
|
49
|
+
| `:ascii_only` | `false` | Escape every non-ASCII character as `\uXXXX` (astral characters as a UTF-16 surrogate pair). The default emits raw UTF-8. |
|
|
50
|
+
| `:script_safe` | `false` | Escape the `/` in `</` and the JS line separators U+2028 / U+2029, so output is safe to embed in an HTML `<script>` tag. |
|
|
51
|
+
| `:coerce` | `false` | When `true`, a value that isn't natively supported is converted by its own `as_json` (the result is re-emitted, so the other options still apply) or, failing that, `to_json` (spliced verbatim). When `false` (the default), such a value raises `SmarterJSON::GenerateError`. |
|
|
52
|
+
|
|
53
|
+
Any other `:format` value, a negative/non-Integer `:indent`, or combining `:indent` with `:ndjson`, raises `ArgumentError`.
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
SmarterJSON.generate([1, 2, 3]) # => "[1,2,3]" (default :json — a single JSON array)
|
|
57
|
+
SmarterJSON.generate([1, 2, 3], format: :ndjson) # => "1\n2\n3\n" (one element per line)
|
|
58
|
+
SmarterJSON.generate({ "a" => 1 }, indent: 2) # => "{\n \"a\": 1\n}" (pretty-printed)
|
|
59
|
+
SmarterJSON.generate({ "b" => 2, "a" => 1 }, sort_keys: true) # => '{"a":1,"b":2}'
|
|
60
|
+
SmarterJSON.generate("café", ascii_only: true) # => '"caf\u00e9"'
|
|
61
|
+
SmarterJSON.generate("</script>", script_safe: true) # => '"<\/script>"'
|
|
62
|
+
SmarterJSON.generate(model, coerce: true) # => uses model.as_json (else model.to_json)
|
|
63
|
+
SmarterJSON.generate(model) # raises SmarterJSON::GenerateError (coerce off)
|
|
64
|
+
SmarterJSON.generate({}, format: :bogus) # raises ArgumentError
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---------------
|
|
68
|
+
|
|
69
|
+
PREVIOUS: [The Basic Write API](./basic_write_api.md) | NEXT: [Examples](./examples.md) | UP: [README](../README.md)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mkmf"
|
|
4
|
+
require "rbconfig"
|
|
5
|
+
|
|
6
|
+
# Ruby sometimes ships CFLAGS with "-g -O3"; drop the debug half so the
|
|
7
|
+
# extension is built optimized, not with debug info.
|
|
8
|
+
if RbConfig::MAKEFILE_CONFIG["CFLAGS"].include?("-g -O3")
|
|
9
|
+
RbConfig::MAKEFILE_CONFIG["CFLAGS"] = RbConfig::MAKEFILE_CONFIG["CFLAGS"].sub("-g -O3", "-O3 $(cflags)")
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
optflags = "-O3 -flto -fomit-frame-pointer -DNDEBUG".dup
|
|
13
|
+
# -march=native is skipped on arm64-darwin (Apple clang already targets the host).
|
|
14
|
+
optflags << " -march=native" unless RUBY_PLATFORM.start_with?("arm64-darwin")
|
|
15
|
+
# -fno-semantic-interposition: GCC/Clang only (not MSVC).
|
|
16
|
+
optflags << " -fno-semantic-interposition" unless RUBY_PLATFORM.include?("mswin")
|
|
17
|
+
|
|
18
|
+
CONFIG["optflags"] = optflags
|
|
19
|
+
CONFIG["debugflags"] = ""
|
|
20
|
+
|
|
21
|
+
# rb_enc_interned_str (Ruby 3.0+) lets us intern object keys straight from the
|
|
22
|
+
# input bytes; on older Rubies the C code falls back to a plain new string.
|
|
23
|
+
have_func("rb_enc_interned_str", "ruby.h")
|
|
24
|
+
|
|
25
|
+
# Pre-sized hashes (3.2+) and bulk insert (2.6+) for object building; the C code
|
|
26
|
+
# falls back to rb_hash_new + per-pair aset when these are unavailable.
|
|
27
|
+
have_func("rb_hash_new_capa", "ruby.h")
|
|
28
|
+
have_func("rb_hash_bulk_insert", "ruby.h")
|
|
29
|
+
|
|
30
|
+
create_makefile("smarter_json/smarter_json")
|