json_scanner 0.1.1 → 0.3.0

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.
@@ -3,6 +3,7 @@
3
3
 
4
4
  #include "ruby.h"
5
5
  #include "ruby/intern.h"
6
+ #include "ruby/version.h"
6
7
  #include <yajl/yajl_parse.h>
7
8
  #include <yajl/yajl_gen.h>
8
9
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JsonScanner
4
- VERSION = "0.1.1"
4
+ VERSION = "0.3.0"
5
5
  end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rake/clean"
4
+ require "rake/extensiontask"
5
+
6
+ module Rake
7
+ class ExtensionTestTask < ExtensionTask
8
+ #
9
+ # The C files to compile.
10
+ #
11
+ attr_accessor :c_spec_files
12
+
13
+ #
14
+ # The folders where includes for the test files are.
15
+ #
16
+ # Default: %w{/usr/include /usr/include/google}
17
+ #
18
+ attr_accessor :test_includes
19
+
20
+ #
21
+ # The libraries to link against.
22
+ #
23
+ # Default: %w{cmockery}
24
+ #
25
+ attr_accessor :test_libraries
26
+
27
+ #
28
+ # The folders where the libraries are
29
+ #
30
+ # Default: %w{/usr/lib}
31
+ #
32
+ attr_accessor :test_lib_folders
33
+
34
+ def initialize(*args, &block)
35
+ super
36
+ @c_spec_files = []
37
+ @test_includes = %w[/usr/include /usr/include/google]
38
+ @test_libraries = %w[cmockery]
39
+ @test_lib_folders = %w[/usr/lib]
40
+ init_test_tasks(
41
+ "#{@tmp_dir}/test", "compile:#{@name}:test",
42
+ "spec:c:#{@name}", "spec:valgrind:#{@name}", "spec:gdb:#{@name}",
43
+ )
44
+ end
45
+
46
+ private
47
+
48
+ def includes
49
+ @includes ||= (@test_includes + [
50
+ ".",
51
+ "../../#{@ext_dir}",
52
+ "/usr/include/ruby-#{RUBY_VERSION}",
53
+ "/usr/include/ruby-#{RUBY_VERSION}/#{RUBY_PLATFORM}",
54
+ ]).map { |l| "-I#{l}" }.join(" ")
55
+ end
56
+
57
+ def libraries
58
+ @libraries ||= (@test_libraries + %w[ruby pthread crypto]).map { |l| "-l#{l}" }.join(" ")
59
+ end
60
+
61
+ def lib_folders
62
+ @lib_folders ||= (@test_lib_folders + %w[/usr/lib .]).map { |l| "-L#{l}" }.join(" ")
63
+ end
64
+
65
+ def compile_tests
66
+ # compile the test sources
67
+ FileList["*.c"].each do |cfile|
68
+ sh "gcc -g #{includes} -c #{cfile}"
69
+ end
70
+
71
+ source_objects = FileList["../#{RUBY_PLATFORM}/#{@name}/#{RUBY_VERSION}/*.o"]
72
+ # link the executables
73
+ FileList["*.o"].each do |ofile|
74
+ sh "gcc -g #{lib_folders} #{libraries} #{source_objects} #{ofile} -o #{ofile.ext}"
75
+ end
76
+ end
77
+
78
+ def init_compile_task(compile_dir, compile_task)
79
+ directory compile_dir
80
+ desc "Compile #{@name} tests"
81
+ task compile_task => ["compile:#{@name}", compile_dir] do
82
+ # copy the test files into the compilation folder
83
+ @c_spec_files.each { |file| cp file, compile_dir }
84
+
85
+ # start compilation
86
+ chdir(compile_dir) { compile_tests }
87
+ end
88
+ end
89
+
90
+ def init_valgrind_task(compile_dir, compile_task, valgrind_task)
91
+ desc "Execute valgrind for a #{@name} test"
92
+ task valgrind_task => [compile_task] do |_t, args|
93
+ sh "valgrind --num-callers=50 --error-limit=no --partial-loads-ok=yes --undef-value-errors=no " \
94
+ "--leak-check=full #{compile_dir}/#{args.test}"
95
+ end
96
+ end
97
+
98
+ def init_gdb_task(compile_dir, compile_task, gdb_task)
99
+ desc "Execute gdb for a #{@name} test"
100
+ task gdb_task => [compile_task] do |_t, args|
101
+ sh "gdb #{compile_dir}/#{args.test}"
102
+ end
103
+ end
104
+
105
+ def init_test_task(compile_dir, compile_task, test_task)
106
+ desc "Test #{@name}"
107
+ task test_task => [compile_task] do |_t, args|
108
+ if args.test
109
+ sh "#{compile_dir}/#{args.test}"
110
+ else
111
+ FileList["#{compile_dir}/*.o"].each do |ofile|
112
+ sh ofile.ext.to_s
113
+ end
114
+ end
115
+ end
116
+ end
117
+
118
+ def init_test_tasks(compile_dir, compile_task, test_task, valgrind_task, gdb_task)
119
+ init_compile_task(compile_dir, compile_task)
120
+ init_valgrind_task(compile_dir, compile_task, valgrind_task)
121
+ init_gdb_task(compile_dir, compile_task, gdb_task)
122
+ init_test_task(compile_dir, compile_task, test_task)
123
+
124
+ desc "Test all C extensions"
125
+ task "spec:c" => [test_task]
126
+ end
127
+ end
128
+ end
File without changes
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "spec_helper"
4
+ require "json"
4
5
 
5
6
  RSpec.describe JsonScanner do
6
7
  it "has a version number" do
@@ -8,19 +9,63 @@ RSpec.describe JsonScanner do
8
9
  end
9
10
 
10
11
  it "scans json" do
11
- result = described_class.scan('["1", {"a": 2}]', [[0], [1, "a"], []], false)
12
+ result = described_class.scan('["1", {"a": 2}]', [[0], [1, "a"], []])
12
13
  expect(result).to eq([[[1, 4, :string]], [[12, 13, :number]], [[0, 15, :array]]])
13
- expect(described_class.scan('"2"', [[]], false)).to eq([[[0, 3, :string]]])
14
+ expect(described_class.scan('"2"', [[]])).to eq([[[0, 3, :string]]])
14
15
  expect(
15
- described_class.scan("[0,1,2,3,4,5,6,7]", [[(0..2)], [(4...6)]], false)
16
+ described_class.scan("[0,1,2,3,4,5,6,7]", [[(0..2)], [(4...6)]]),
16
17
  ).to eq(
17
- [[[1, 2, :number], [3, 4, :number], [5, 6, :number]], [[9, 10, :number], [11, 12, :number]]]
18
+ [[[1, 2, :number], [3, 4, :number], [5, 6, :number]], [[9, 10, :number], [11, 12, :number]]],
18
19
  )
19
- expect(described_class.scan('{"a": 1}', [["a"], []], false)).to eq(
20
- [[[6, 7, :number]], [[0, 8, :object]]]
20
+ expect(described_class.scan('{"a": 1}', [["a"], []])).to eq(
21
+ [[[6, 7, :number]], [[0, 8, :object]]],
21
22
  )
22
23
  end
23
24
 
25
+ it "supports 'symbolize_path_keys'" do
26
+ expect(
27
+ described_class.scan('{"a": {"b": 1}}', [[:a, "b"]], with_path: true),
28
+ ).to eq([[[%w[a b], [12, 13, :number]]]])
29
+ expect(
30
+ described_class.scan('{"a": {"b": 1}}', [[:a, "b"]], with_path: true, symbolize_path_keys: true),
31
+ ).to eq([[[%i[a b], [12, 13, :number]]]])
32
+ end
33
+
34
+ it "supports any key selector" do
35
+ expect(
36
+ described_class.scan(
37
+ '[{"a":1,"b":2},{"c":3,"d":4},[5]]',
38
+ [[described_class::ANY_INDEX, described_class::ANY_KEY]],
39
+ ),
40
+ ).to eq(
41
+ [[[6, 7, :number], [12, 13, :number], [20, 21, :number], [26, 27, :number]]],
42
+ )
43
+ expect(
44
+ described_class.scan(
45
+ '{"a":[1,2],"b":{"c":3}}',
46
+ [[described_class::ANY_KEY, described_class::ANY_INDEX]],
47
+ ),
48
+ ).to eq(
49
+ [[[6, 7, :number], [8, 9, :number]]],
50
+ )
51
+ end
52
+
53
+ it "works with max path len correctly" do
54
+ expect(
55
+ described_class.scan('{"a": [1]}', [[], ["a"]]),
56
+ ).to eq(
57
+ [[[0, 10, :object]], [[6, 9, :array]]],
58
+ )
59
+ expect(
60
+ described_class.scan('{"a": {"b": 1}}', [[], ["a"]]),
61
+ ).to eq(
62
+ [[[0, 15, :object]], [[6, 14, :object]]],
63
+ )
64
+ expect(described_class.scan('{"a": 1}', [[]])).to eq([[[0, 8, :object]]])
65
+ expect(described_class.scan("[[1]]", [[]])).to eq([[[0, 5, :array]]])
66
+ expect(described_class.scan("[[1]]", [[0]])).to eq([[[1, 4, :array]]])
67
+ end
68
+
24
69
  it "raises on invalid json" do
25
70
  expect do
26
71
  begin
@@ -28,10 +73,10 @@ RSpec.describe JsonScanner do
28
73
  # TODO: investigate
29
74
  # got "munmap_chunk(): invalid pointer" in in console once after
30
75
  # JsonScanner.scan '[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]', [[0,0,0,0,0,0,0]], true + Ctrl+D
31
- # (last arg wasn't handled at the time)
32
- # but I don't think it's a problem of tht extension or libyajl, it happened at exit and I free everything before
76
+ # (last arg wasn't handled at the time and was intended for with_path kwarg)
77
+ # but I don't think it's a problem of the extension or libyajl, it happened at exit and I free everything before
33
78
  # `JsonScanner.scan` returns
34
- described_class.scan "[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]", [[0, 0, 0, 0, 0, 0, 0]], false
79
+ described_class.scan "[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]", [[0, 0, 0, 0, 0, 0, 0]]
35
80
  ensure
36
81
  GC.stress = false
37
82
  end
@@ -40,26 +85,268 @@ RSpec.describe JsonScanner do
40
85
 
41
86
  it "allows to select ranges" do
42
87
  expect(
43
- described_class.scan("[[1,2],[3,4]]", [[described_class::ANY_INDEX, described_class::ANY_INDEX]], false)
88
+ described_class.scan("[[1,2],[3,4]]", [[described_class::ANY_INDEX, described_class::ANY_INDEX]]),
44
89
  ).to eq(
45
- [[[2, 3, :number], [4, 5, :number], [8, 9, :number], [10, 11, :number]]]
90
+ [[[2, 3, :number], [4, 5, :number], [8, 9, :number], [10, 11, :number]]],
46
91
  )
47
92
  expect(
48
- described_class.scan("[[1,2],[3,4]]", [[described_class::ANY_INDEX, (0...1)]], false)
93
+ described_class.scan("[[1,2],[3,4]]", [[described_class::ANY_INDEX, (0...1)]]),
49
94
  ).to eq(
50
- [[[2, 3, :number], [8, 9, :number]]]
95
+ [[[2, 3, :number], [8, 9, :number]]],
51
96
  )
52
97
  end
53
98
 
54
99
  it "allows only positive or -1 values" do
55
100
  expect do
56
- described_class.scan("[[1,2],[3,4]]", [[(0...-1)]], false)
101
+ described_class.scan("[[1,2],[3,4]]", [[(0...-1)]])
57
102
  end.to raise_error ArgumentError
58
103
  expect do
59
- described_class.scan("[[1,2],[3,4]]", [[(0..-2)]], false)
104
+ described_class.scan("[[1,2],[3,4]]", [[(0..-2)]])
60
105
  end.to raise_error ArgumentError
61
106
  expect do
62
- described_class.scan("[[1,2],[3,4]]", [[(-42..1)]], false)
107
+ described_class.scan("[[1,2],[3,4]]", [[(-42..1)]])
63
108
  end.to raise_error ArgumentError
64
109
  end
110
+
111
+ it "allows to configure error messages" do
112
+ expect do
113
+ described_class.scan "{1}", []
114
+ end.to raise_error described_class::ParseError, /invalid object key(?!.*\(right here\))/m
115
+ expect do
116
+ described_class.scan "{1}", [], verbose_error: false
117
+ end.to raise_error described_class::ParseError, /invalid object key(?!.*\(right here\))/m
118
+ expect do
119
+ described_class.scan "{1}", [], verbose_error: true
120
+ end.to raise_error described_class::ParseError, /invalid object key(?=.*\(right here\))/m
121
+ expect do
122
+ described_class.scan("[0, 42,", [[(1..-1)]], verbose_error: true)
123
+ end.to raise_error described_class::ParseError, /parse error: premature EOF.*\[0, 42,.*\(right here\) ------\^/m
124
+ end
125
+
126
+ it "includes bytes consumed in the exception" do
127
+ expect do
128
+ described_class.scan("[[1,2],,[3,4]]", [])
129
+ end.to(
130
+ raise_error(described_class::ParseError) do |exc|
131
+ expect(exc.bytes_consumed).to eq(8)
132
+ end,
133
+ )
134
+ end
135
+
136
+ it "allows to return an actual path to the element" do
137
+ with_path_expected_res = [
138
+ # result for first matcher, each element array of two items:
139
+ # array of path elements and 3-element array start,end,type
140
+ [[[0], [1, 6, :array]], [[1], [7, 12, :array]]],
141
+ [
142
+ [[0, 0], [2, 3, :number]], [[0, 1], [4, 5, :number]],
143
+ [[1, 0], [8, 9, :number]], [[1, 1], [10, 11, :number]],
144
+ ],
145
+ ]
146
+ params = [
147
+ "[[1,2],[3,4]]",
148
+ [
149
+ [described_class::ANY_INDEX],
150
+ [described_class::ANY_INDEX, described_class::ANY_INDEX],
151
+ ],
152
+ ]
153
+ expect(described_class.scan(*params, with_path: true)).to eq(with_path_expected_res)
154
+ expect(described_class.scan(*params, true)).to eq(with_path_expected_res)
155
+ expect(
156
+ described_class.scan(*params, false, with_path: true),
157
+ ).to eq(with_path_expected_res)
158
+ end
159
+
160
+ it "ignores reqular flag if kwarg is given" do
161
+ expect(
162
+ described_class.scan(
163
+ "[[1,2],[3,4]]",
164
+ [
165
+ [described_class::ANY_INDEX],
166
+ [described_class::ANY_INDEX, described_class::ANY_INDEX],
167
+ ],
168
+ true, with_path: false,
169
+ ),
170
+ ).to eq(
171
+ [
172
+ # result for first matcher, each element 3-element array start,end,type
173
+ [[1, 6, :array], [7, 12, :array]],
174
+ [
175
+ [2, 3, :number], [4, 5, :number],
176
+ [8, 9, :number], [10, 11, :number],
177
+ ],
178
+ ],
179
+ )
180
+ end
181
+
182
+ it "allows to pass config as a hash" do
183
+ expect(
184
+ described_class.scan("[1]", [[0]], { with_path: true }),
185
+ ).to eq(
186
+ [
187
+ [[[0], [1, 2, :number]]],
188
+ ],
189
+ )
190
+ end
191
+
192
+ it "allows to configure yajl" do
193
+ expect(
194
+ described_class.scan("[1]____________", [[0]], { allow_trailing_garbage: true }),
195
+ ).to eq([[[1, 2, :number]]])
196
+ expect(
197
+ described_class.scan(
198
+ '["1", {"a": /* comment */ 2}]____________', [[1, "a"]],
199
+ { allow_trailing_garbage: true, allow_comments: true },
200
+ ),
201
+ ).to eq([[[26, 27, :number]]])
202
+ expect(
203
+ described_class.scan(
204
+ '[{"a": /* comment */ 1}]_________', [[]],
205
+ { allow_comments: true, allow_trailing_garbage: true },
206
+ ),
207
+ ).to eq([[[0, 24, :array]]])
208
+ end
209
+
210
+ it "works with utf-8" do
211
+ json = '{"ルビー": ["Руби"]}'.encode(Encoding::UTF_8)
212
+ expect(described_class.scan(json, [[]])).to eq([[[0, json.bytesize, :object]]])
213
+ res = described_class.scan(json, [["ルビー", 0]])
214
+ expect(res).to eq([[[15, 25, :string]]])
215
+ elem = res.first.first
216
+ expect(JSON.parse(json.byteslice(elem[0]...elem[1]), quirks_mode: true)).to eq("Руби")
217
+ end
218
+
219
+ it "raises exceptions in utf-8" do
220
+ bad_json = '{"ルビー": ["Руби" 1]}'.encode(Encoding::UTF_8)
221
+ expect do
222
+ described_class.scan(bad_json, [[]], verbose_error: true)
223
+ # Checks encoding
224
+ end.to raise_error(described_class::ParseError, Regexp.new(Regexp.escape(bad_json)))
225
+ end
226
+
227
+ it "works with different encodings" do
228
+ # TODO: encoding validation
229
+ json = '{"a": 1}'.encode(Encoding::UTF_32LE)
230
+ expect do
231
+ described_class.scan(json, [[]])
232
+ end.to raise_error(described_class::ParseError)
233
+ end
234
+
235
+ context "with yajl params" do
236
+ it "supports 'allow_comments'" do
237
+ params = ["[0, /* answer */ 42, 0]", [[(1..-1)]]]
238
+ expect(described_class.scan(*params, allow_comments: true)).to eq(
239
+ [[[17, 19, :number], [21, 22, :number]]],
240
+ )
241
+ expect do
242
+ described_class.scan(*params)
243
+ end.to raise_error(described_class::ParseError)
244
+ end
245
+
246
+ it "supports 'dont_validate_strings'" do
247
+ params = ["\"\x81\x83\"", [[]]]
248
+ expect(described_class.scan(*params, dont_validate_strings: true)).to eq(
249
+ [[[0, 4, :string]]],
250
+ )
251
+ expect do
252
+ described_class.scan(*params)
253
+ end.to raise_error(described_class::ParseError)
254
+ params = ["{\"\x81\x83\": 42}", [[JsonScanner::ANY_KEY]]]
255
+ expect(described_class.scan(*params, dont_validate_strings: true, with_path: true)).to eq(
256
+ [[[["\x81\x83".dup.force_encoding(Encoding::BINARY)], [7, 9, :number]]]],
257
+ )
258
+ expect do
259
+ described_class.scan(*params, with_path: true)
260
+ end.to raise_error(described_class::ParseError)
261
+ end
262
+
263
+ it "supports 'allow_trailing_garbage'" do
264
+ params = ["[0, 42, 0]garbage", [[(1..-1)]]]
265
+ expect(described_class.scan(*params, allow_trailing_garbage: true)).to eq(
266
+ [[[4, 6, :number], [8, 9, :number]]],
267
+ )
268
+ expect do
269
+ described_class.scan(*params)
270
+ end.to raise_error(described_class::ParseError)
271
+ end
272
+
273
+ it "supports 'allow_multiple_values'" do
274
+ params = ["[0, 42, 0] [0, 34]", [[(1..-1)]]]
275
+ expect(described_class.scan(*params, allow_multiple_values: true)).to eq(
276
+ [[[4, 6, :number], [8, 9, :number], [16, 18, :number]]],
277
+ )
278
+ expect do
279
+ described_class.scan(*params)
280
+ end.to raise_error(described_class::ParseError)
281
+ end
282
+
283
+ it "handles multiple top-level values correctly with 'allow_multiple_values'" do
284
+ expect(described_class.scan("[0, 42, 0] [0, 34]", [[]], allow_multiple_values: true)).to eq(
285
+ [[[0, 10, :array], [12, 19, :array]]],
286
+ )
287
+ expect(described_class.scan('{"42": 34} [0, 34]', [[]], allow_multiple_values: true)).to eq(
288
+ [[[0, 10, :object], [12, 19, :array]]],
289
+ )
290
+ expect(described_class.scan('[0, 42, 0] {"42": 34}', [[]], allow_multiple_values: true)).to eq(
291
+ [[[0, 10, :array], [12, 22, :object]]],
292
+ )
293
+ expect(described_class.scan('{"42": 34} {"0": 34}', [[]], allow_multiple_values: true)).to eq(
294
+ [[[0, 10, :object], [12, 21, :object]]],
295
+ )
296
+ end
297
+
298
+ it "supports 'allow_partial_values'" do
299
+ params = ["[0, 42, 0,", [[(1..-1)]]]
300
+ expect(described_class.scan(*params, allow_partial_values: true)).to eq(
301
+ [[[4, 6, :number], [8, 9, :number]]],
302
+ )
303
+ expect do
304
+ described_class.scan(*params)
305
+ end.to raise_error(described_class::ParseError)
306
+ expect(described_class.scan("[0, 42, 0", [[(1..-1)]], allow_partial_values: true)).to eq(
307
+ [[[4, 6, :number], [8, 9, :number]]],
308
+ )
309
+ expect(described_class.scan("[0, 42, true", [[(1..-1)]], allow_partial_values: true)).to eq(
310
+ [[[4, 6, :number], [8, 12, :boolean]]],
311
+ )
312
+ end
313
+ end
314
+
315
+ describe described_class::Config do
316
+ it "saves state" do
317
+ key = "abracadabra".dup
318
+ conf = described_class.new [[], [key]]
319
+ key["cad"] = 0.chr
320
+ key = nil # rubocop:disable Lint/UselessAssignment
321
+ GC.start
322
+ expect(
323
+ 10.times.map do
324
+ JsonScanner.scan '{"abracadabra": 10}', conf, with_path: true
325
+ end.uniq,
326
+ ).to eq([[[[[], [0, 19, :object]]], [[["abracadabra"], [16, 18, :number]]]]])
327
+ expect(
328
+ 10.times.map do
329
+ JsonScanner.scan '{"abracadabra": 10}', conf
330
+ end.uniq,
331
+ ).to eq([[[[0, 19, :object]], [[16, 18, :number]]]])
332
+ end
333
+
334
+ it "re-raises exceptions" do
335
+ expect do
336
+ described_class.new [[(0...-1)]]
337
+ end.to raise_error ArgumentError
338
+ expect do
339
+ described_class.new [[(0..-2)]]
340
+ end.to raise_error ArgumentError
341
+ expect do
342
+ described_class.new [[(-42..1)]]
343
+ end.to raise_error ArgumentError
344
+ end
345
+
346
+ it "supports inspect" do
347
+ expect(
348
+ described_class.new([[], ["abracadabra", JsonScanner::ANY_INDEX], [42, JsonScanner::ANY_KEY]]).inspect,
349
+ ).to eq("#<JsonScanner::Config [[], ['abracadabra', (0..9223372036854775807)], [42, ('*'..'*')]]>")
350
+ end
351
+ end
65
352
  end
metadata CHANGED
@@ -1,16 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: json_scanner
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - uvlad7
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-12-16 00:00:00.000000000 Z
11
+ date: 2025-08-14 00:00:00.000000000 Z
12
12
  dependencies: []
13
- description: This gem uses yajl lib to scan a json string and allows you to parse
13
+ description: This gem uses the yajl lib to scan a JSON string and allows you to parse
14
14
  pieces of it
15
15
  email:
16
16
  - uvlad7@gmail.com
@@ -26,6 +26,8 @@ files:
26
26
  - lib/json_scanner.rb
27
27
  - lib/json_scanner/version.rb
28
28
  - sig/json_scanner.rbs
29
+ - spec/extensiontesttask.rb
30
+ - spec/json_scanner_spec.c
29
31
  - spec/json_scanner_spec.rb
30
32
  - spec/spec_helper.rb
31
33
  homepage: https://github.com/uvlad7/json_scanner