groonga-command 1.0.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.
Files changed (40) hide show
  1. data/Gemfile +21 -0
  2. data/README.md +47 -0
  3. data/Rakefile +44 -0
  4. data/doc/text/lgpl-2.1.txt +502 -0
  5. data/doc/text/news.md +5 -0
  6. data/groonga-command.gemspec +64 -0
  7. data/lib/groonga/command.rb +20 -0
  8. data/lib/groonga/command/base.rb +138 -0
  9. data/lib/groonga/command/column-create.rb +39 -0
  10. data/lib/groonga/command/column-remove.rb +36 -0
  11. data/lib/groonga/command/column-rename.rb +37 -0
  12. data/lib/groonga/command/delete.rb +38 -0
  13. data/lib/groonga/command/error.rb +24 -0
  14. data/lib/groonga/command/get.rb +39 -0
  15. data/lib/groonga/command/load.rb +56 -0
  16. data/lib/groonga/command/parser.rb +424 -0
  17. data/lib/groonga/command/select.rb +69 -0
  18. data/lib/groonga/command/suggest.rb +47 -0
  19. data/lib/groonga/command/table-create.rb +39 -0
  20. data/lib/groonga/command/table-remove.rb +35 -0
  21. data/lib/groonga/command/table-rename.rb +36 -0
  22. data/lib/groonga/command/truncate.rb +35 -0
  23. data/lib/groonga/command/version.rb +23 -0
  24. data/test/command/test-base.rb +114 -0
  25. data/test/command/test-column-create.rb +47 -0
  26. data/test/command/test-column-remove.rb +41 -0
  27. data/test/command/test-column-rename.rb +43 -0
  28. data/test/command/test-delete.rb +45 -0
  29. data/test/command/test-get.rb +44 -0
  30. data/test/command/test-load.rb +49 -0
  31. data/test/command/test-select.rb +60 -0
  32. data/test/command/test-suggest.rb +68 -0
  33. data/test/command/test-table-create.rb +48 -0
  34. data/test/command/test-table-remove.rb +39 -0
  35. data/test/command/test-table-rename.rb +41 -0
  36. data/test/command/test-truncate.rb +39 -0
  37. data/test/groonga-command-test-utils.rb +102 -0
  38. data/test/run-test.rb +39 -0
  39. data/test/test-parser.rb +353 -0
  40. metadata +237 -0
@@ -0,0 +1,56 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright (C) 2012 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
+ require "groonga/command/base"
20
+
21
+ module Groonga
22
+ module Command
23
+ class Load < Base
24
+ Command.register("load", self)
25
+
26
+ class << self
27
+ def parameter_names
28
+ [
29
+ :values,
30
+ :table,
31
+ :columns,
32
+ :ifexists,
33
+ :input_type,
34
+ :each,
35
+ ]
36
+ end
37
+ end
38
+
39
+ attr_writer :columns
40
+ def initialize(*argumetns)
41
+ super
42
+ @columns = nil
43
+ end
44
+
45
+ def columns
46
+ @columns ||= parse_columns(self[:columns])
47
+ end
48
+
49
+ private
50
+ def parse_columns(columns)
51
+ return columns if columns.nil?
52
+ columns.split(/\s*,\s*/)
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,424 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright (C) 2011-2012 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
+ require "shellwords"
20
+ require "cgi"
21
+
22
+ require "yajl"
23
+
24
+ require "groonga/command/error"
25
+
26
+ require "groonga/command/base"
27
+ require "groonga/command/get"
28
+ require "groonga/command/select"
29
+ require "groonga/command/suggest"
30
+ require "groonga/command/load"
31
+ require "groonga/command/table-create"
32
+ require "groonga/command/table-remove"
33
+ require "groonga/command/table-rename"
34
+ require "groonga/command/column-create"
35
+ require "groonga/command/column-remove"
36
+ require "groonga/command/column-rename"
37
+ require "groonga/command/delete"
38
+ require "groonga/command/truncate"
39
+
40
+ module Groonga
41
+ module Command
42
+ class ParseError < Error
43
+ attr_reader :reason, :location
44
+ def initialize(reason, before, after)
45
+ @reason = reason
46
+ @location = compute_location(before, after)
47
+ super("#{@reason}:\n#{@location}")
48
+ end
49
+
50
+ private
51
+ def compute_location(before, after)
52
+ location = ""
53
+ if before[-1] == ?\n
54
+ location << before
55
+ location << after
56
+ location << "^"
57
+ elsif after[0] == ?\n
58
+ location << before
59
+ location << "\n"
60
+ location << " " * before.bytesize + "^"
61
+ location << after
62
+ else
63
+ location << before
64
+ location << after
65
+ location << " " * before.bytesize + "^"
66
+ end
67
+ location
68
+ end
69
+ end
70
+
71
+ class Parser
72
+ class << self
73
+ def parse(data, &block)
74
+ if block_given?
75
+ event_parse(data, &block)
76
+ else
77
+ stand_alone_parse(data)
78
+ end
79
+ end
80
+
81
+ private
82
+ def event_parse(data)
83
+ parser = new
84
+
85
+ parser.on_command do |command|
86
+ yield(:on_command, command)
87
+ end
88
+ parser.on_load_start do |command|
89
+ yield(:on_load_start, command)
90
+ end
91
+ parser.on_load_columns do |command, header|
92
+ yield(:on_load_columns, command, header)
93
+ end
94
+ parser.on_load_value do |command, value|
95
+ yield(:on_load_value, command, value)
96
+ end
97
+ parser.on_load_complete do |command|
98
+ yield(:on_load_complete, command)
99
+ end
100
+ parser.on_comment do |comment|
101
+ yield(:on_comment, comment)
102
+ end
103
+
104
+ consume_data(parser, data)
105
+ end
106
+
107
+ def stand_alone_parse(data)
108
+ parsed_command = nil
109
+
110
+ parser = new
111
+ parser.on_command do |command|
112
+ parsed_command = command
113
+ end
114
+ parser.on_load_complete do |command|
115
+ parsed_command = command
116
+ end
117
+
118
+ consume_data(parser, data)
119
+ if parsed_command.nil?
120
+ raise ParseError.new("not completed", data.lines.to_a.last, "")
121
+ end
122
+
123
+ parsed_command
124
+ end
125
+
126
+ def consume_data(parser, data)
127
+ if data.respond_to?(:each)
128
+ data.each do |chunk|
129
+ parser << chunk
130
+ end
131
+ else
132
+ parser << data
133
+ end
134
+ parser.finish
135
+ end
136
+ end
137
+
138
+ def initialize
139
+ reset
140
+ initialize_hooks
141
+ end
142
+
143
+ def <<(chunk)
144
+ @buffer << chunk
145
+ consume_buffer
146
+ end
147
+
148
+ def finish
149
+ if @loading
150
+ raise ParseError.new("not completed",
151
+ @command.original_source.lines.to_a.last,
152
+ "")
153
+ else
154
+ catch do |tag|
155
+ parse_line(@buffer)
156
+ end
157
+ end
158
+ end
159
+
160
+ # @overload on_command(command)
161
+ # @overload on_command {|command| }
162
+ def on_command(*arguments, &block)
163
+ if block_given?
164
+ @on_command_hook = block
165
+ else
166
+ @on_command_hook.call(*arguments) if @on_command_hook
167
+ end
168
+ end
169
+
170
+ # @overload on_load_start(command)
171
+ # @overload on_load_start {|command| }
172
+ def on_load_start(*arguments, &block)
173
+ if block_given?
174
+ @on_load_start_hook = block
175
+ else
176
+ @on_load_start_hook.call(*arguments) if @on_load_start_hook
177
+ end
178
+ end
179
+
180
+ # @overload on_load_columns(command)
181
+ # @overload on_load_columns {|command| }
182
+ def on_load_columns(*arguments, &block)
183
+ if block_given?
184
+ @on_load_columns_hook = block
185
+ else
186
+ @on_load_columns_hook.call(*arguments) if @on_load_columns_hook
187
+ end
188
+ end
189
+
190
+ # @overload on_load_value(command)
191
+ # @overload on_load_value {|command| }
192
+ def on_load_value(*arguments, &block)
193
+ if block_given?
194
+ @on_load_value_hook = block
195
+ else
196
+ @on_load_value_hook.call(*arguments) if @on_load_value_hook
197
+ end
198
+ end
199
+
200
+ # @overload on_load_complete(command)
201
+ # @overload on_load_complete(command) { }
202
+ def on_load_complete(*arguments, &block)
203
+ if block_given?
204
+ @on_load_complete_hook = block
205
+ else
206
+ @on_load_complete_hook.call(*arguments) if @on_load_complete_hook
207
+ end
208
+ end
209
+
210
+ # @overload on_comment(comment)
211
+ # @overload on_comment {|comment| }
212
+ def on_comment(*arguments, &block)
213
+ if block_given?
214
+ @on_comment_hook = block
215
+ else
216
+ @on_comment_hook.call(*arguments) if @on_comment_hook
217
+ end
218
+ end
219
+
220
+ private
221
+ def consume_buffer
222
+ catch do |tag|
223
+ loop do
224
+ if @loading
225
+ consume_load_values(tag)
226
+ else
227
+ parse_line(consume_line(tag))
228
+ end
229
+ end
230
+ end
231
+ end
232
+
233
+ def consume_load_values(tag)
234
+ if @in_load_values
235
+ json, separator, rest = @buffer.partition(/[\]},]/)
236
+ if @load_value_completed
237
+ throw(tag) if separator.empty?
238
+ if separator == ","
239
+ if /\A\s*\z/ =~ json
240
+ @command.original_source << json << separator
241
+ @buffer = rest
242
+ @load_value_completed = false
243
+ return
244
+ else
245
+ raise ParseError.new("record separate comma is missing",
246
+ @command.original_source.lines.to_a.last,
247
+ json)
248
+ end
249
+ elsif separator == "]"
250
+ if /\A\s*\z/ =~ json
251
+ @command.original_source << json << separator
252
+ @buffer = rest
253
+ on_load_complete(@command)
254
+ reset
255
+ return
256
+ end
257
+ end
258
+ end
259
+ @buffer = rest
260
+ parse_json(json)
261
+ if separator.empty?
262
+ throw(tag)
263
+ else
264
+ @load_value_completed = false
265
+ parse_json(separator)
266
+ end
267
+ else
268
+ spaces, start_json, rest = @buffer.partition("[")
269
+ unless /\A\s*\z/ =~ spaces
270
+ raise ParseError.new("there are garbages before JSON",
271
+ @command.original_source.lines.to_a.last,
272
+ spaces)
273
+ end
274
+ if start_json.empty?
275
+ @command.original_source << @buffer
276
+ @buffer.clear
277
+ throw(tag)
278
+ else
279
+ @command.original_source << spaces << start_json
280
+ @buffer = rest
281
+ @json_parser = Yajl::Parser.new
282
+ @json_parser.on_parse_complete = lambda do |object|
283
+ if object.is_a?(Array) and @command.columns.nil?
284
+ @command.columns = object
285
+ on_load_columns(@command, object)
286
+ else
287
+ on_load_value(@command, object)
288
+ end
289
+ @load_value_completed = true
290
+ end
291
+ @in_load_values = true
292
+ end
293
+ end
294
+ end
295
+
296
+ def parse_json(json)
297
+ @command.original_source << json
298
+ begin
299
+ @json_parser << json
300
+ rescue Yajl::ParseError
301
+ before_json = @command.original_source[0..(-json.bytesize)]
302
+ message = "invalid JSON: #{$!.message}: <#{json}>:\n"
303
+ message << before_json
304
+ raise ParseError.new(message, before_json, json)
305
+ end
306
+ end
307
+
308
+ def consume_line(tag)
309
+ current_line, separator, rest = @buffer.partition(/\r?\n/)
310
+ throw(tag) if separator.empty?
311
+
312
+ if current_line.end_with?("\\")
313
+ @buffer.sub!(/\\\r?\n/, "")
314
+ consume_line(tag)
315
+ else
316
+ @buffer = rest
317
+ current_line
318
+ end
319
+ end
320
+
321
+ def parse_line(line)
322
+ case line
323
+ when /\A\s*\z/
324
+ # ignore empty line
325
+ when /\A\#/
326
+ on_comment($POSTMATCH)
327
+ else
328
+ @command = parse_command(line)
329
+ @command.original_source = line
330
+ process_command
331
+ end
332
+ end
333
+
334
+ def process_command
335
+ if @command.name == "load"
336
+ on_load_start(@command)
337
+ if @command.columns
338
+ on_load_columns(@command, @command.columns)
339
+ end
340
+ if @command[:values]
341
+ values = Yajl::Parser.parse(@command[:values])
342
+ if @command.columns.nil? and values.first.is_a?(Array)
343
+ header = values.shift
344
+ @command.columns = header
345
+ on_load_columns(@command, header)
346
+ end
347
+ values.each do |value|
348
+ on_load_value(@command, value)
349
+ end
350
+ on_load_complete(@command)
351
+ reset
352
+ else
353
+ @command.original_source << "\n"
354
+ @loading = true
355
+ end
356
+ else
357
+ on_command(@command)
358
+ reset
359
+ end
360
+ end
361
+
362
+ def parse_command(input)
363
+ if input.start_with?("/d/")
364
+ parse_uri_path(input)
365
+ else
366
+ parse_command_line(input)
367
+ end
368
+ end
369
+
370
+ def parse_uri_path(path)
371
+ name, arguments_string = path.split(/\?/, 2)
372
+ arguments = {}
373
+ if arguments_string
374
+ arguments_string.split(/&/).each do |argument_string|
375
+ key, value = argument_string.split(/\=/, 2)
376
+ arguments[key] = CGI.unescape(value)
377
+ end
378
+ end
379
+ name = name.gsub(/\A\/d\//, '')
380
+ name, output_type = name.split(/\./, 2)
381
+ arguments["output_type"] = output_type if output_type
382
+ command_class = Command.find(name)
383
+ command = command_class.new(name, arguments)
384
+ command.original_format = :uri
385
+ command
386
+ end
387
+
388
+ def parse_command_line(command_line)
389
+ name, *arguments = Shellwords.shellwords(command_line)
390
+ pair_arguments = {}
391
+ ordered_arguments = []
392
+ until arguments.empty?
393
+ argument = arguments.shift
394
+ if argument.start_with?("--")
395
+ pair_arguments[argument.sub(/\A--/, "")] = arguments.shift
396
+ else
397
+ ordered_arguments << argument
398
+ end
399
+ end
400
+ command_class = Command.find(name)
401
+ command = command_class.new(name, pair_arguments, ordered_arguments)
402
+ command.original_format = :command
403
+ command
404
+ end
405
+
406
+ private
407
+ def reset
408
+ @command = nil
409
+ @loading = false
410
+ @in_load_values = false
411
+ @load_value_completed = false
412
+ @buffer = "".force_encoding("ASCII-8BIT")
413
+ end
414
+
415
+ def initialize_hooks
416
+ @on_command_hook = nil
417
+ @on_load_start_hook = nil
418
+ @on_load_columns_hook = nil
419
+ @on_load_value_hook = nil
420
+ @on_load_complete_hook = nil
421
+ end
422
+ end
423
+ end
424
+ end