groonga-command-parser 1.0.3 → 1.0.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Gem Version](https://badge.fury.io/rb/groonga-command-parser.svg)](http://badge.fury.io/rb/groonga-command-parser)
|
4
|
+
[![Build Status](https://travis-ci.org/groonga/groonga-command-parser.svg?branch=master)](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:
|