groonga-command 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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