groonga-command-parser 1.0.3 → 1.0.4
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.
- checksums.yaml +4 -4
- data/.yardopts +2 -0
- data/README.md +3 -0
- data/doc/text/news.md +18 -0
- data/groonga-command-parser.gemspec +4 -3
- data/lib/groonga/command/parser.rb +35 -122
- data/lib/groonga/command/parser/command-line-splitter.rb +91 -0
- data/lib/groonga/command/parser/error.rb +17 -4
- data/lib/groonga/command/parser/load-values-parser.rb +209 -0
- data/lib/groonga/command/parser/version.rb +1 -1
- data/test/run-test.rb +7 -0
- data/test/test-command-line-splitter.rb +102 -0
- data/test/test-load-value-parser.rb +149 -0
- data/test/test-parser.rb +31 -14
- metadata +26 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 653da7d4f65f66a98a4fdc5300f1c40b768d99bf
|
4
|
+
data.tar.gz: 3be534841fc2a4e8ee175262107ce0336d81c287
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4c12a30d319639ac368a1610080cfef25c47198565245453e56265db91508b58664fe6886e1cb80bc6436beef2252ec61734337274740ed26c2ebe1e87201c14
|
7
|
+
data.tar.gz: 0a541136772e374816b60ceb5052b1848c9d7a0e1f05c5526a575f17e8d16ce570084a70fcc30ad6909311c9f70b6fad517605add4423c48468f1d409228ca47
|
data/.yardopts
CHANGED
data/README.md
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
# README
|
2
2
|
|
3
|
+
[](http://badge.fury.io/rb/groonga-command-parser)
|
4
|
+
[](https://travis-ci.org/groonga/groonga-command-parser)
|
5
|
+
|
3
6
|
## Name
|
4
7
|
|
5
8
|
groonga-command-parser
|
data/doc/text/news.md
CHANGED
@@ -1,5 +1,23 @@
|
|
1
1
|
# News
|
2
2
|
|
3
|
+
## 1.0.4: 2015-05-23
|
4
|
+
|
5
|
+
### Improvements
|
6
|
+
|
7
|
+
* Made quoted text handling Groonga compatible.
|
8
|
+
* Changed JSON parser to ffi-yajl from yajl-ruby.
|
9
|
+
* Supported JRuby by this change.
|
10
|
+
[GitHub#1] [Reported by Hiroyuki Sato]
|
11
|
+
* Improved performance for large load data.
|
12
|
+
|
13
|
+
### Fixes
|
14
|
+
|
15
|
+
* Fixed encoding related parse error on Windows.
|
16
|
+
|
17
|
+
### Thanks
|
18
|
+
|
19
|
+
* Hiroyuki Sato
|
20
|
+
|
3
21
|
## 1.0.3: 2014-12-12
|
4
22
|
|
5
23
|
### Improvements
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# -*- mode: ruby; coding: utf-8 -*-
|
2
2
|
#
|
3
|
-
# Copyright (C) 2012-
|
3
|
+
# Copyright (C) 2012-2015 Kouhei Sutou <kou@clear-code.com>
|
4
4
|
#
|
5
5
|
# This library is free software; you can redistribute it and/or
|
6
6
|
# modify it under the terms of the GNU Lesser General Public
|
@@ -53,7 +53,8 @@ Gem::Specification.new do |spec|
|
|
53
53
|
spec.require_paths = ["lib"]
|
54
54
|
|
55
55
|
spec.add_runtime_dependency("groonga-command", ">= 1.0.9")
|
56
|
-
spec.add_runtime_dependency("
|
56
|
+
spec.add_runtime_dependency("ffi")
|
57
|
+
spec.add_runtime_dependency("ffi-yajl")
|
57
58
|
|
58
59
|
spec.add_development_dependency("test-unit")
|
59
60
|
spec.add_development_dependency("test-unit-notify")
|
@@ -61,6 +62,6 @@ Gem::Specification.new do |spec|
|
|
61
62
|
spec.add_development_dependency("bundler")
|
62
63
|
spec.add_development_dependency("packnga")
|
63
64
|
spec.add_development_dependency("yard")
|
64
|
-
spec.add_development_dependency("
|
65
|
+
spec.add_development_dependency("kramdown")
|
65
66
|
end
|
66
67
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# -*- coding: utf-8 -*-
|
2
2
|
#
|
3
|
-
# Copyright (C) 2011-
|
3
|
+
# Copyright (C) 2011-2015 Kouhei Sutou <kou@clear-code.com>
|
4
4
|
#
|
5
5
|
# This library is free software; you can redistribute it and/or
|
6
6
|
# modify it under the terms of the GNU Lesser General Public
|
@@ -17,14 +17,14 @@
|
|
17
17
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
18
18
|
|
19
19
|
require "English"
|
20
|
-
require "shellwords"
|
21
20
|
require "cgi"
|
22
|
-
|
23
|
-
require "yajl"
|
21
|
+
require "json"
|
24
22
|
|
25
23
|
require "groonga/command"
|
26
24
|
|
27
25
|
require "groonga/command/parser/error"
|
26
|
+
require "groonga/command/parser/command-line-splitter"
|
27
|
+
require "groonga/command/parser/load-values-parser"
|
28
28
|
require "groonga/command/parser/version"
|
29
29
|
|
30
30
|
module Groonga
|
@@ -91,7 +91,7 @@ module Groonga
|
|
91
91
|
end
|
92
92
|
parser.on_load_complete do |command|
|
93
93
|
parsed_command = command
|
94
|
-
parsed_command[:values] ||=
|
94
|
+
parsed_command[:values] ||= JSON.generate(values)
|
95
95
|
end
|
96
96
|
|
97
97
|
consume_data(parser, data)
|
@@ -117,12 +117,14 @@ module Groonga
|
|
117
117
|
def initialize
|
118
118
|
reset
|
119
119
|
initialize_hooks
|
120
|
+
initialize_load_values_parser
|
120
121
|
end
|
121
122
|
|
122
123
|
# Streaming parsing command.
|
123
124
|
# @param [String] chunk parsed chunk of command.
|
124
125
|
def <<(chunk)
|
125
126
|
@buffer << chunk
|
127
|
+
@buffer.force_encoding("ASCII-8BIT")
|
126
128
|
consume_buffer
|
127
129
|
end
|
128
130
|
|
@@ -214,78 +216,10 @@ module Groonga
|
|
214
216
|
end
|
215
217
|
|
216
218
|
def consume_load_values(tag)
|
217
|
-
if @
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
if separator == ","
|
222
|
-
if /\A\s*\z/ =~ json
|
223
|
-
@command.original_source << json << separator
|
224
|
-
@buffer = rest
|
225
|
-
@load_value_completed = false
|
226
|
-
return
|
227
|
-
else
|
228
|
-
raise Error.new("record separate comma is missing",
|
229
|
-
@command.original_source.lines.to_a.last,
|
230
|
-
json)
|
231
|
-
end
|
232
|
-
elsif separator == "]"
|
233
|
-
if /\A\s*\z/ =~ json
|
234
|
-
@command.original_source << json << separator
|
235
|
-
@buffer = rest
|
236
|
-
on_load_complete(@command)
|
237
|
-
reset
|
238
|
-
return
|
239
|
-
end
|
240
|
-
end
|
241
|
-
end
|
242
|
-
@buffer = rest
|
243
|
-
parse_json(json)
|
244
|
-
if separator.empty?
|
245
|
-
throw(tag)
|
246
|
-
else
|
247
|
-
@load_value_completed = false
|
248
|
-
parse_json(separator)
|
249
|
-
end
|
250
|
-
else
|
251
|
-
spaces, start_json, rest = @buffer.partition("[")
|
252
|
-
unless /\A\s*\z/ =~ spaces
|
253
|
-
raise Error.new("there are garbages before JSON",
|
254
|
-
@command.original_source.lines.to_a.last,
|
255
|
-
spaces)
|
256
|
-
end
|
257
|
-
if start_json.empty?
|
258
|
-
@command.original_source << @buffer
|
259
|
-
@buffer.clear
|
260
|
-
throw(tag)
|
261
|
-
else
|
262
|
-
@command.original_source << spaces << start_json
|
263
|
-
@buffer = rest
|
264
|
-
@json_parser = Yajl::Parser.new
|
265
|
-
@json_parser.on_parse_complete = lambda do |object|
|
266
|
-
if object.is_a?(::Array) and @command.columns.nil?
|
267
|
-
@command.columns = object
|
268
|
-
on_load_columns(@command, object)
|
269
|
-
else
|
270
|
-
on_load_value(@command, object)
|
271
|
-
end
|
272
|
-
@load_value_completed = true
|
273
|
-
end
|
274
|
-
@in_load_values = true
|
275
|
-
end
|
276
|
-
end
|
277
|
-
end
|
278
|
-
|
279
|
-
def parse_json(json)
|
280
|
-
@command.original_source << json
|
281
|
-
begin
|
282
|
-
@json_parser << json
|
283
|
-
rescue Yajl::ParseError
|
284
|
-
before_json = @command.original_source[0..(-json.bytesize)]
|
285
|
-
message = "invalid JSON: #{$!.message}: <#{json}>:\n"
|
286
|
-
message << before_json
|
287
|
-
raise Error.new(message, before_json, json)
|
288
|
-
end
|
219
|
+
throw(tag) if @buffer.empty?
|
220
|
+
@command.original_source << @buffer
|
221
|
+
@load_values_parser << @buffer
|
222
|
+
@buffer.clear
|
289
223
|
end
|
290
224
|
|
291
225
|
def consume_line(tag)
|
@@ -321,16 +255,7 @@ module Groonga
|
|
321
255
|
on_load_columns(@command, @command.columns)
|
322
256
|
end
|
323
257
|
if @command[:values]
|
324
|
-
|
325
|
-
if @command.columns.nil? and values.first.is_a?(::Array)
|
326
|
-
header = values.shift
|
327
|
-
@command.columns = header
|
328
|
-
on_load_columns(@command, header)
|
329
|
-
end
|
330
|
-
values.each do |value|
|
331
|
-
on_load_value(@command, value)
|
332
|
-
end
|
333
|
-
on_load_complete(@command)
|
258
|
+
@load_values_parser << @command[:values]
|
334
259
|
reset
|
335
260
|
else
|
336
261
|
@command.original_source << "\n"
|
@@ -377,19 +302,8 @@ module Groonga
|
|
377
302
|
end
|
378
303
|
|
379
304
|
def parse_command_line(command_line)
|
380
|
-
|
381
|
-
arguments =
|
382
|
-
# TODO: Groonga supports backslash escape in both single
|
383
|
-
# quoted argument and double quoted argument ('...' and
|
384
|
-
# "..."). We should not unescape except single quoted
|
385
|
-
# argument ('...') because Shellwords.shellwords unescape it
|
386
|
-
# in double quoted argument. But we can't determine whether
|
387
|
-
# the argument is single quoted or not after
|
388
|
-
# Shellwords.shellwords... And groonga supports '\n' -> new line
|
389
|
-
# conversion. It isn't done by Shellwords.shellwords.
|
390
|
-
# Should we implement Shellwords.shellwords by ourselves?
|
391
|
-
unescape_argument_in_command_line(argument)
|
392
|
-
end
|
305
|
+
splitter = CommandLineSplitter.new(command_line)
|
306
|
+
name, *arguments = splitter.split
|
393
307
|
pair_arguments = {}
|
394
308
|
ordered_arguments = []
|
395
309
|
until arguments.empty?
|
@@ -406,31 +320,9 @@ module Groonga
|
|
406
320
|
command
|
407
321
|
end
|
408
322
|
|
409
|
-
def unescape_argument_in_command_line(argument)
|
410
|
-
argument.gsub(/\\(.)/) do
|
411
|
-
character = $1
|
412
|
-
case character
|
413
|
-
when "b"
|
414
|
-
"\b"
|
415
|
-
when "f"
|
416
|
-
"\f"
|
417
|
-
when "n"
|
418
|
-
"\n"
|
419
|
-
when "r"
|
420
|
-
"\r"
|
421
|
-
when "t"
|
422
|
-
"\t"
|
423
|
-
else
|
424
|
-
character
|
425
|
-
end
|
426
|
-
end
|
427
|
-
end
|
428
|
-
|
429
323
|
def reset
|
430
324
|
@command = nil
|
431
325
|
@loading = false
|
432
|
-
@in_load_values = false
|
433
|
-
@load_value_completed = false
|
434
326
|
@buffer = "".force_encoding("ASCII-8BIT")
|
435
327
|
end
|
436
328
|
|
@@ -441,6 +333,27 @@ module Groonga
|
|
441
333
|
@on_load_value_hook = nil
|
442
334
|
@on_load_complete_hook = nil
|
443
335
|
end
|
336
|
+
|
337
|
+
def initialize_load_values_parser
|
338
|
+
@load_values_parser = LoadValuesParser.new
|
339
|
+
@load_values_parser.on_value = lambda do |value|
|
340
|
+
if value.is_a?(::Array) and @command.columns.nil?
|
341
|
+
@command.columns = value
|
342
|
+
on_load_columns(@command, value)
|
343
|
+
else
|
344
|
+
on_load_value(@command, value)
|
345
|
+
end
|
346
|
+
end
|
347
|
+
@load_values_parser.on_end = lambda do |rest|
|
348
|
+
if rest
|
349
|
+
original_source_size = @command.original_source.size
|
350
|
+
@command.original_source.slice!(original_source_size - rest.size,
|
351
|
+
rest.size)
|
352
|
+
end
|
353
|
+
on_load_complete(@command)
|
354
|
+
reset
|
355
|
+
end
|
356
|
+
end
|
444
357
|
end
|
445
358
|
end
|
446
359
|
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# Copyright (C) 2015 Kouhei Sutou <kou@clear-code.com>
|
2
|
+
#
|
3
|
+
# This library is free software; you can redistribute it and/or
|
4
|
+
# modify it under the terms of the GNU Lesser General Public
|
5
|
+
# License as published by the Free Software Foundation; either
|
6
|
+
# version 2.1 of the License, or (at your option) any later version.
|
7
|
+
#
|
8
|
+
# This library is distributed in the hope that it will be useful,
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
11
|
+
# Lesser General Public License for more details.
|
12
|
+
#
|
13
|
+
# You should have received a copy of the GNU Lesser General Public
|
14
|
+
# License along with this library; if not, write to the Free Software
|
15
|
+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
16
|
+
|
17
|
+
require "English"
|
18
|
+
require "strscan"
|
19
|
+
|
20
|
+
module Groonga
|
21
|
+
module Command
|
22
|
+
class Parser
|
23
|
+
class CommandLineSplitter
|
24
|
+
def initialize(command_line)
|
25
|
+
@command_line = command_line
|
26
|
+
end
|
27
|
+
|
28
|
+
def split
|
29
|
+
tokens = []
|
30
|
+
scanner = StringScanner.new(@command_line)
|
31
|
+
skip_spaces(scanner)
|
32
|
+
start_quote = nil
|
33
|
+
until scanner.eos?
|
34
|
+
if start_quote
|
35
|
+
token = ""
|
36
|
+
loop do
|
37
|
+
chunk = scanner.scan_until(/#{Regexp.escape(start_quote)}/)
|
38
|
+
if chunk.nil?
|
39
|
+
token = start_quote + token + scanner.rest
|
40
|
+
scanner.terminate
|
41
|
+
break
|
42
|
+
end
|
43
|
+
if chunk[-2] == "\\"
|
44
|
+
token << chunk
|
45
|
+
else
|
46
|
+
token << chunk.chomp(start_quote)
|
47
|
+
break
|
48
|
+
end
|
49
|
+
end
|
50
|
+
tokens << unescape(token)
|
51
|
+
start_quote = nil
|
52
|
+
skip_spaces(scanner)
|
53
|
+
else
|
54
|
+
start_quote = scanner.scan(/['"]/)
|
55
|
+
if start_quote.nil?
|
56
|
+
tokens << scanner.scan_until(/\S+/)
|
57
|
+
skip_spaces(scanner)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
tokens
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
def skip_spaces(scanner)
|
66
|
+
scanner.scan(/\s+/)
|
67
|
+
end
|
68
|
+
|
69
|
+
def unescape(token)
|
70
|
+
token.gsub(/\\(.)/) do
|
71
|
+
character = $1
|
72
|
+
case character
|
73
|
+
when "b"
|
74
|
+
"\b"
|
75
|
+
when "f"
|
76
|
+
"\f"
|
77
|
+
when "n"
|
78
|
+
"\n"
|
79
|
+
when "r"
|
80
|
+
"\r"
|
81
|
+
when "t"
|
82
|
+
"\t"
|
83
|
+
else
|
84
|
+
character
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# -*- coding: utf-8 -*-
|
2
2
|
#
|
3
|
-
# Copyright (C) 2011-
|
3
|
+
# Copyright (C) 2011-2015 Kouhei Sutou <kou@clear-code.com>
|
4
4
|
#
|
5
5
|
# This library is free software; you can redistribute it and/or
|
6
6
|
# modify it under the terms of the GNU Lesser General Public
|
@@ -42,9 +42,22 @@ module Groonga
|
|
42
42
|
location << " " * before.bytesize + "^"
|
43
43
|
location << after
|
44
44
|
else
|
45
|
-
|
46
|
-
|
47
|
-
|
45
|
+
before_lines = before.lines.to_a
|
46
|
+
after_lines = after.lines.to_a
|
47
|
+
last_before_line = before_lines.last
|
48
|
+
if last_before_line
|
49
|
+
error_offset = last_before_line.bytesize
|
50
|
+
else
|
51
|
+
error_offset = 0
|
52
|
+
end
|
53
|
+
before_lines.each do |before_line|
|
54
|
+
location << before_line
|
55
|
+
end
|
56
|
+
location << after_lines.first
|
57
|
+
location << " " * error_offset + "^\n"
|
58
|
+
after_lines[1..-1].each do |after_line|
|
59
|
+
location << after_line
|
60
|
+
end
|
48
61
|
end
|
49
62
|
location
|
50
63
|
end
|
@@ -0,0 +1,209 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
#
|
3
|
+
# Copyright (C) 2015 Kouhei Sutou <kou@clear-code.com>
|
4
|
+
#
|
5
|
+
# This library is free software; you can redistribute it and/or
|
6
|
+
# modify it under the terms of the GNU Lesser General Public
|
7
|
+
# License as published by the Free Software Foundation; either
|
8
|
+
# version 2.1 of the License, or (at your option) any later version.
|
9
|
+
#
|
10
|
+
# This library is distributed in the hope that it will be useful,
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
13
|
+
# Lesser General Public License for more details.
|
14
|
+
#
|
15
|
+
# You should have received a copy of the GNU Lesser General Public
|
16
|
+
# License along with this library; if not, write to the Free Software
|
17
|
+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
18
|
+
|
19
|
+
# TODO: Remove me when https://github.com/chef/ffi-yajl/pull/62 is merged.
|
20
|
+
require "stringio"
|
21
|
+
ENV["FORCE_FFI_YAJL"] = "ffi"
|
22
|
+
require "ffi_yajl"
|
23
|
+
|
24
|
+
module FFI_Yajl
|
25
|
+
attach_function :yajl_get_bytes_consumed, [:yajl_handle], :size_t
|
26
|
+
end
|
27
|
+
|
28
|
+
module Groonga
|
29
|
+
module Command
|
30
|
+
class Parser
|
31
|
+
class LoadValuesParser
|
32
|
+
attr_writer :on_value
|
33
|
+
attr_writer :on_end
|
34
|
+
def initialize
|
35
|
+
initialize_callbacks
|
36
|
+
@handle = nil
|
37
|
+
@callbacks_memory = nil
|
38
|
+
@on_value = nil
|
39
|
+
@on_end = nil
|
40
|
+
@containers = []
|
41
|
+
@keys = []
|
42
|
+
end
|
43
|
+
|
44
|
+
def <<(data)
|
45
|
+
data_size = data.bytesize
|
46
|
+
return self if data_size.zero?
|
47
|
+
|
48
|
+
ensure_handle
|
49
|
+
|
50
|
+
status = FFI_Yajl.yajl_parse(@handle, data, data_size)
|
51
|
+
|
52
|
+
if status != :yajl_status_ok
|
53
|
+
consumed = FFI_Yajl.yajl_get_bytes_consumed(@handle)
|
54
|
+
if consumed > 0
|
55
|
+
consumed -= 1
|
56
|
+
end
|
57
|
+
if @containers.empty?
|
58
|
+
message = "there are garbages before JSON"
|
59
|
+
else
|
60
|
+
message = FFI_Yajl.yajl_get_error(@handle, 0, nil, 0).chomp
|
61
|
+
end
|
62
|
+
begin
|
63
|
+
raise Error.new(message,
|
64
|
+
data[0, consumed],
|
65
|
+
data[consumed..-1])
|
66
|
+
ensure
|
67
|
+
finalize_handle
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
if @containers.empty?
|
72
|
+
consumed = FFI_Yajl.yajl_get_bytes_consumed(@handle)
|
73
|
+
begin
|
74
|
+
if consumed < data_size
|
75
|
+
@on_end.call(data[consumed..-1])
|
76
|
+
else
|
77
|
+
@on_end.call(nil)
|
78
|
+
end
|
79
|
+
ensure
|
80
|
+
finalize_handle
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
self
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
def callback(*arguments)
|
89
|
+
FFI::Function.new(:int, [:pointer, *arguments]) do |_, *values|
|
90
|
+
yield(*values)
|
91
|
+
1
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def initialize_callbacks
|
96
|
+
@null_callback = callback do
|
97
|
+
push_value(nil)
|
98
|
+
end
|
99
|
+
@boolean_callback = callback(:int) do |c_boolean|
|
100
|
+
push_value(c_boolean != 0)
|
101
|
+
end
|
102
|
+
@number_callback = callback(:string, :size_t) do |data, size|
|
103
|
+
number_data = data.slice(0, size)
|
104
|
+
if /[\.eE]/ =~ number_data
|
105
|
+
number = number_data.to_f
|
106
|
+
else
|
107
|
+
number = number_data.to_i
|
108
|
+
end
|
109
|
+
push_value(number)
|
110
|
+
end
|
111
|
+
@string_callback = callback(:string, :size_t) do |data, size|
|
112
|
+
string = data.slice(0, size)
|
113
|
+
string.force_encoding(Encoding::UTF_8)
|
114
|
+
push_value(string)
|
115
|
+
end
|
116
|
+
@start_map_callback = callback do
|
117
|
+
push_container({})
|
118
|
+
end
|
119
|
+
@map_key_callback = callback(:string, :size_t) do |data, size|
|
120
|
+
key = data.slice(0, size)
|
121
|
+
key.force_encoding(Encoding::UTF_8)
|
122
|
+
@keys.push(key)
|
123
|
+
end
|
124
|
+
@end_map_callback = callback do
|
125
|
+
pop_container
|
126
|
+
end
|
127
|
+
@start_array_callback = callback do
|
128
|
+
push_container([])
|
129
|
+
end
|
130
|
+
@end_array_callback = callback do
|
131
|
+
pop_container
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def push_container(container)
|
136
|
+
@containers.push(container)
|
137
|
+
end
|
138
|
+
|
139
|
+
def pop_container
|
140
|
+
container = @containers.pop
|
141
|
+
if @containers.size == 1
|
142
|
+
@on_value.call(container)
|
143
|
+
else
|
144
|
+
push_value(container)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def push_value(value)
|
149
|
+
container = @containers.last
|
150
|
+
case container
|
151
|
+
when Hash
|
152
|
+
container[@keys.pop] = value
|
153
|
+
when Array
|
154
|
+
container.push(value)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def ensure_handle
|
159
|
+
return if @handle
|
160
|
+
initialize_handle
|
161
|
+
end
|
162
|
+
|
163
|
+
def initialize_handle
|
164
|
+
@callbacks_memory = FFI::MemoryPointer.new(FFI_Yajl::YajlCallbacks)
|
165
|
+
callbacks = FFI_Yajl::YajlCallbacks.new(@callbacks_memory)
|
166
|
+
callbacks[:yajl_null] = @null_callback
|
167
|
+
callbacks[:yajl_boolean] = @boolean_callback
|
168
|
+
callbacks[:yajl_integer] = nil
|
169
|
+
callbacks[:yajl_double] = nil
|
170
|
+
callbacks[:yajl_number] = @number_callback
|
171
|
+
callbacks[:yajl_string] = @string_callback
|
172
|
+
callbacks[:yajl_start_map] = @start_map_callback
|
173
|
+
callbacks[:yajl_map_key] = @map_key_callback
|
174
|
+
callbacks[:yajl_end_map] = @end_map_callback
|
175
|
+
callbacks[:yajl_start_array] = @start_array_callback
|
176
|
+
callbacks[:yajl_end_array] = @end_array_callback
|
177
|
+
|
178
|
+
@handle = FFI_Yajl.yajl_alloc(@callbacks_memory, nil, nil)
|
179
|
+
FFI_Yajl.yajl_config(@handle,
|
180
|
+
:yajl_allow_trailing_garbage,
|
181
|
+
:int,
|
182
|
+
1)
|
183
|
+
@finalizer = Finalizer.new(@handle)
|
184
|
+
ObjectSpace.define_finalizer(self, @finalizer)
|
185
|
+
end
|
186
|
+
|
187
|
+
def finalize_handle
|
188
|
+
@callbacks_memory = nil
|
189
|
+
@finalizer.call(object_id)
|
190
|
+
ObjectSpace.undefine_finalizer(self)
|
191
|
+
@finalizer = nil
|
192
|
+
@handle = nil
|
193
|
+
end
|
194
|
+
|
195
|
+
class Finalizer
|
196
|
+
def initialize(handle)
|
197
|
+
@handle = handle
|
198
|
+
end
|
199
|
+
|
200
|
+
def call(object_id)
|
201
|
+
return if @handle.nil?
|
202
|
+
FFI_Yajl.yajl_free(@handle)
|
203
|
+
@handle = nil
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
data/test/run-test.rb
CHANGED
@@ -36,6 +36,13 @@ end
|
|
36
36
|
$LOAD_PATH.unshift(lib_dir)
|
37
37
|
$LOAD_PATH.unshift(test_dir)
|
38
38
|
|
39
|
+
# TODO: Remove me when suppress warnings patches are merged int
|
40
|
+
# ffi_yajl.
|
41
|
+
require "stringio"
|
42
|
+
$VERBOSE = false
|
43
|
+
require "ffi_yajl/ffi"
|
44
|
+
$VERBOSE = true
|
45
|
+
|
39
46
|
require "groonga-command-parser-test-utils"
|
40
47
|
|
41
48
|
ENV["TEST_UNIT_MAX_DIFF_TARGET_STRING_SIZE"] ||= "5000"
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
#
|
3
|
+
# Copyright (C) 2015 Kouhei Sutou <kou@clear-code.com>
|
4
|
+
#
|
5
|
+
# This library is free software; you can redistribute it and/or
|
6
|
+
# modify it under the terms of the GNU Lesser General Public
|
7
|
+
# License as published by the Free Software Foundation; either
|
8
|
+
# version 2.1 of the License, or (at your option) any later version.
|
9
|
+
#
|
10
|
+
# This library is distributed in the hope that it will be useful,
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
13
|
+
# Lesser General Public License for more details.
|
14
|
+
#
|
15
|
+
# You should have received a copy of the GNU Lesser General Public
|
16
|
+
# License along with this library; if not, write to the Free Software
|
17
|
+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
18
|
+
|
19
|
+
class CommandLineSplitterTest < Test::Unit::TestCase
|
20
|
+
def split(command_line)
|
21
|
+
splitter = Groonga::Command::Parser::CommandLineSplitter.new(command_line)
|
22
|
+
splitter.split
|
23
|
+
end
|
24
|
+
|
25
|
+
test "name only" do
|
26
|
+
assert_equal(["status"],
|
27
|
+
split("status"))
|
28
|
+
end
|
29
|
+
|
30
|
+
sub_test_case "arguments" do
|
31
|
+
sub_test_case "no quote" do
|
32
|
+
test "value" do
|
33
|
+
assert_equal(["select", "Logs"],
|
34
|
+
split("select Logs"))
|
35
|
+
end
|
36
|
+
|
37
|
+
test "key and value" do
|
38
|
+
assert_equal(["select", "--table", "Logs"],
|
39
|
+
split("select --table Logs"))
|
40
|
+
end
|
41
|
+
|
42
|
+
test "multibyte character" do
|
43
|
+
assert_equal(["select", "テーブル"],
|
44
|
+
split("select テーブル"))
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
sub_test_case "single quote" do
|
49
|
+
test "value" do
|
50
|
+
assert_equal(["select", "Logs"],
|
51
|
+
split("select 'Logs'"))
|
52
|
+
end
|
53
|
+
|
54
|
+
test "key and value" do
|
55
|
+
assert_equal(["select", "--table", "Logs"],
|
56
|
+
split("select '--table' 'Logs'"))
|
57
|
+
end
|
58
|
+
|
59
|
+
test "space" do
|
60
|
+
assert_equal(["select", "Logs Table"],
|
61
|
+
split("select 'Logs Table'"))
|
62
|
+
end
|
63
|
+
|
64
|
+
test "escape quote" do
|
65
|
+
assert_equal(["select", "Logs \' Table"],
|
66
|
+
split("select 'Logs \\' Table'"))
|
67
|
+
end
|
68
|
+
|
69
|
+
test "new line" do
|
70
|
+
assert_equal(["select", "Logs \n Table"],
|
71
|
+
split("select 'Logs \\n Table'"))
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
sub_test_case "double quote" do
|
76
|
+
test "value" do
|
77
|
+
assert_equal(["select", "Logs"],
|
78
|
+
split("select \"Logs\""))
|
79
|
+
end
|
80
|
+
|
81
|
+
test "key and value" do
|
82
|
+
assert_equal(["select", "--table", "Logs"],
|
83
|
+
split("select \"--table\" \"Logs\""))
|
84
|
+
end
|
85
|
+
|
86
|
+
test "space" do
|
87
|
+
assert_equal(["select", "Logs Table"],
|
88
|
+
split("select \"Logs Table\""))
|
89
|
+
end
|
90
|
+
|
91
|
+
test "escape quote" do
|
92
|
+
assert_equal(["select", "Logs \" Table"],
|
93
|
+
split("select \"Logs \\\" Table\""))
|
94
|
+
end
|
95
|
+
|
96
|
+
test "new line" do
|
97
|
+
assert_equal(["select", "Logs \n Table"],
|
98
|
+
split("select \"Logs \\n Table\""))
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
# Copyright (C) 2015 Kouhei Sutou <kou@clear-code.com>
|
2
|
+
#
|
3
|
+
# This library is free software; you can redistribute it and/or
|
4
|
+
# modify it under the terms of the GNU Lesser General Public
|
5
|
+
# License as published by the Free Software Foundation; either
|
6
|
+
# version 2.1 of the License, or (at your option) any later version.
|
7
|
+
#
|
8
|
+
# This library is distributed in the hope that it will be useful,
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
11
|
+
# Lesser General Public License for more details.
|
12
|
+
#
|
13
|
+
# You should have received a copy of the GNU Lesser General Public
|
14
|
+
# License along with this library; if not, write to the Free Software
|
15
|
+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
16
|
+
|
17
|
+
class LoadValuesParserTest < Test::Unit::TestCase
|
18
|
+
def setup
|
19
|
+
@values = []
|
20
|
+
@parser = Groonga::Command::Parser::LoadValuesParser.new
|
21
|
+
@parser.on_value = lambda do |value|
|
22
|
+
@values << value
|
23
|
+
end
|
24
|
+
@parser.on_end = lambda do |rest|
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def parse(data)
|
29
|
+
data.each_line do |line|
|
30
|
+
@parser << line
|
31
|
+
end
|
32
|
+
@values
|
33
|
+
end
|
34
|
+
|
35
|
+
sub_test_case "array" do
|
36
|
+
test "empty" do
|
37
|
+
assert_equal([[]],
|
38
|
+
parse("[[]]"))
|
39
|
+
end
|
40
|
+
|
41
|
+
test "no container" do
|
42
|
+
assert_equal([[1, "abc", 2.9]],
|
43
|
+
parse(<<-JSON))
|
44
|
+
[
|
45
|
+
[1, "abc", 2.9]
|
46
|
+
]
|
47
|
+
JSON
|
48
|
+
end
|
49
|
+
|
50
|
+
test "array" do
|
51
|
+
assert_equal([[[1, "abc", 2.9]]],
|
52
|
+
parse(<<-JSON))
|
53
|
+
[
|
54
|
+
[[1, "abc", 2.9]]
|
55
|
+
]
|
56
|
+
JSON
|
57
|
+
end
|
58
|
+
|
59
|
+
test "object" do
|
60
|
+
assert_equal([
|
61
|
+
[
|
62
|
+
{
|
63
|
+
"number" => 1,
|
64
|
+
"string" => "abc",
|
65
|
+
"double" => 2.9,
|
66
|
+
},
|
67
|
+
],
|
68
|
+
],
|
69
|
+
parse(<<-JSON))
|
70
|
+
[
|
71
|
+
[
|
72
|
+
{
|
73
|
+
"number": 1,
|
74
|
+
"string": "abc",
|
75
|
+
"double": 2.9
|
76
|
+
}
|
77
|
+
]
|
78
|
+
]
|
79
|
+
JSON
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
sub_test_case "object" do
|
84
|
+
test "empty" do
|
85
|
+
assert_equal([{}],
|
86
|
+
parse("[{}]"))
|
87
|
+
end
|
88
|
+
|
89
|
+
test "no container" do
|
90
|
+
assert_equal([
|
91
|
+
{
|
92
|
+
"number" => 1,
|
93
|
+
"string" => "abc",
|
94
|
+
"double" => 2.9,
|
95
|
+
},
|
96
|
+
],
|
97
|
+
parse(<<-JSON))
|
98
|
+
[
|
99
|
+
{
|
100
|
+
"number": 1,
|
101
|
+
"string": "abc",
|
102
|
+
"double": 2.9
|
103
|
+
}
|
104
|
+
]
|
105
|
+
JSON
|
106
|
+
end
|
107
|
+
|
108
|
+
test "array" do
|
109
|
+
assert_equal([
|
110
|
+
{
|
111
|
+
"array" => [1, "abc", 2.9],
|
112
|
+
},
|
113
|
+
],
|
114
|
+
parse(<<-JSON))
|
115
|
+
[
|
116
|
+
{
|
117
|
+
"array": [1, "abc", 2.9]
|
118
|
+
}
|
119
|
+
]
|
120
|
+
JSON
|
121
|
+
end
|
122
|
+
|
123
|
+
test "object" do
|
124
|
+
assert_equal([
|
125
|
+
{
|
126
|
+
"object" => {
|
127
|
+
"number" => 1,
|
128
|
+
"string" => "abc",
|
129
|
+
"double" => 2.9,
|
130
|
+
},
|
131
|
+
},
|
132
|
+
],
|
133
|
+
parse(<<-JSON))
|
134
|
+
[
|
135
|
+
{
|
136
|
+
"object": {
|
137
|
+
"number": 1,
|
138
|
+
"string": "abc",
|
139
|
+
"double": 2.9
|
140
|
+
}
|
141
|
+
}
|
142
|
+
]
|
143
|
+
[
|
144
|
+
1
|
145
|
+
]
|
146
|
+
JSON
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
data/test/test-parser.rb
CHANGED
@@ -228,10 +228,11 @@ EOC
|
|
228
228
|
expected_events << [:load_columns, <<-EOC.chomp, ["_key", "name"]]
|
229
229
|
load --table Users --columns "_key, name"
|
230
230
|
EOC
|
231
|
-
expected_events << [:load_value, <<-EOC
|
231
|
+
expected_events << [:load_value, <<-EOC, ["alice", "Alice"]]
|
232
232
|
load --table Users --columns "_key, name"
|
233
233
|
[
|
234
234
|
["alice", "Alice"]
|
235
|
+
]
|
235
236
|
EOC
|
236
237
|
expected_events << [:load_complete, <<-EOC.chomp]
|
237
238
|
load --table Users --columns "_key, name"
|
@@ -254,16 +255,19 @@ EOC
|
|
254
255
|
expected_events << [:load_start, <<-EOC.chomp]
|
255
256
|
load --table Users
|
256
257
|
EOC
|
257
|
-
expected_events << [:load_columns, <<-EOC
|
258
|
+
expected_events << [:load_columns, <<-EOC, ["_key", "name"]]
|
258
259
|
load --table Users
|
259
260
|
[
|
260
|
-
["_key", "name"]
|
261
|
+
["_key", "name"],
|
262
|
+
["alice", "Alice"]
|
263
|
+
]
|
261
264
|
EOC
|
262
|
-
expected_events << [:load_value, <<-EOC
|
265
|
+
expected_events << [:load_value, <<-EOC, ["alice", "Alice"]]
|
263
266
|
load --table Users
|
264
267
|
[
|
265
268
|
["_key", "name"],
|
266
269
|
["alice", "Alice"]
|
270
|
+
]
|
267
271
|
EOC
|
268
272
|
expected_events << [:load_complete, <<-EOC.chomp]
|
269
273
|
load --table Users
|
@@ -289,17 +293,20 @@ EOC
|
|
289
293
|
load --table Users
|
290
294
|
EOC
|
291
295
|
value = {"_key" => "alice", "name" => "Alice"}
|
292
|
-
expected_events << [:load_value, <<-EOC
|
296
|
+
expected_events << [:load_value, <<-EOC, value]
|
293
297
|
load --table Users
|
294
298
|
[
|
295
|
-
{"_key": "alice", "name": "Alice"}
|
299
|
+
{"_key": "alice", "name": "Alice"},
|
300
|
+
{"_key": "bob", "name": "Bob"}
|
301
|
+
]
|
296
302
|
EOC
|
297
303
|
value = {"_key" => "bob", "name" => "Bob"}
|
298
|
-
expected_events << [:load_value, <<-EOC
|
304
|
+
expected_events << [:load_value, <<-EOC, value]
|
299
305
|
load --table Users
|
300
306
|
[
|
301
307
|
{"_key": "alice", "name": "Alice"},
|
302
308
|
{"_key": "bob", "name": "Bob"}
|
309
|
+
]
|
303
310
|
EOC
|
304
311
|
expected_events << [:load_complete, <<-EOC.chomp]
|
305
312
|
load --table Users
|
@@ -327,9 +334,14 @@ EOS
|
|
327
334
|
end
|
328
335
|
|
329
336
|
def test_no_record_separate_comma
|
330
|
-
message = "
|
331
|
-
before =
|
332
|
-
|
337
|
+
message = "parse error: after array element, I expect ',' or ']'"
|
338
|
+
before = <<-BEFORE
|
339
|
+
[
|
340
|
+
{"_key": "alice", "name": "Alice"}
|
341
|
+
BEFORE
|
342
|
+
after = <<-AFTER
|
343
|
+
{"_key": "bob", "name": "Bob"}
|
344
|
+
AFTER
|
333
345
|
error = Groonga::Command::Parser::Error.new(message, before, after)
|
334
346
|
assert_raise(error) do
|
335
347
|
@parser << <<-EOC
|
@@ -343,17 +355,22 @@ EOC
|
|
343
355
|
|
344
356
|
def test_garbage_before_json
|
345
357
|
message = "there are garbages before JSON"
|
346
|
-
before = "
|
347
|
-
after =
|
358
|
+
before = ""
|
359
|
+
after = <<-AFTER
|
360
|
+
XXX
|
361
|
+
[
|
362
|
+
{"_key": "alice", "name": "Alice"}
|
363
|
+
]
|
364
|
+
AFTER
|
348
365
|
error = Groonga::Command::Parser::Error.new(message, before, after)
|
349
366
|
assert_raise(error) do
|
350
|
-
@parser << <<-
|
367
|
+
@parser << <<-JSON
|
351
368
|
load --table Users
|
352
369
|
XXX
|
353
370
|
[
|
354
371
|
{"_key": "alice", "name": "Alice"}
|
355
372
|
]
|
356
|
-
|
373
|
+
JSON
|
357
374
|
end
|
358
375
|
end
|
359
376
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: groonga-command-parser
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kouhei Sutou
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2015-05-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: groonga-command
|
@@ -25,7 +25,21 @@ dependencies:
|
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: 1.0.9
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: ffi
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: ffi-yajl
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
30
44
|
requirements:
|
31
45
|
- - ">="
|
@@ -123,7 +137,7 @@ dependencies:
|
|
123
137
|
- !ruby/object:Gem::Version
|
124
138
|
version: '0'
|
125
139
|
- !ruby/object:Gem::Dependency
|
126
|
-
name:
|
140
|
+
name: kramdown
|
127
141
|
requirement: !ruby/object:Gem::Requirement
|
128
142
|
requirements:
|
129
143
|
- - ">="
|
@@ -153,11 +167,15 @@ files:
|
|
153
167
|
- doc/text/news.md
|
154
168
|
- groonga-command-parser.gemspec
|
155
169
|
- lib/groonga/command/parser.rb
|
170
|
+
- lib/groonga/command/parser/command-line-splitter.rb
|
156
171
|
- lib/groonga/command/parser/command/groonga-command-convert-format.rb
|
157
172
|
- lib/groonga/command/parser/error.rb
|
173
|
+
- lib/groonga/command/parser/load-values-parser.rb
|
158
174
|
- lib/groonga/command/parser/version.rb
|
159
175
|
- test/groonga-command-parser-test-utils.rb
|
160
176
|
- test/run-test.rb
|
177
|
+
- test/test-command-line-splitter.rb
|
178
|
+
- test/test-load-value-parser.rb
|
161
179
|
- test/test-parser.rb
|
162
180
|
homepage: https://github.com/groonga/groonga-command-parser
|
163
181
|
licenses:
|
@@ -185,7 +203,9 @@ specification_version: 4
|
|
185
203
|
summary: Groonga-command-parser is a Ruby library to parses [groonga](http://groonga.org/)'s
|
186
204
|
command syntax. You can write a program to process groonga's command by using groonga-command-parser.
|
187
205
|
test_files:
|
188
|
-
- test/test-parser.rb
|
189
|
-
- test/run-test.rb
|
190
206
|
- test/groonga-command-parser-test-utils.rb
|
207
|
+
- test/run-test.rb
|
208
|
+
- test/test-command-line-splitter.rb
|
209
|
+
- test/test-load-value-parser.rb
|
210
|
+
- test/test-parser.rb
|
191
211
|
has_rdoc:
|