groonga-command-parser 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.
data/doc/text/news.md ADDED
@@ -0,0 +1,5 @@
1
+ # News
2
+
3
+ ## 1.0.0: 2013-09-29
4
+
5
+ The first release!!! It is extracted from groonga-command gem.
@@ -0,0 +1,66 @@
1
+ # -*- mode: ruby; coding: utf-8 -*-
2
+ #
3
+ # Copyright (C) 2012-2013 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
+ base_dir = File.dirname(__FILE__)
20
+ lib_dir = File.join(base_dir, "lib")
21
+
22
+ $LOAD_PATH.unshift(lib_dir)
23
+ require "groonga/command/parser/version"
24
+
25
+ clean_white_space = lambda do |entry|
26
+ entry.gsub(/(\A\n+|\n+\z)/, '') + "\n"
27
+ end
28
+
29
+ Gem::Specification.new do |spec|
30
+ spec.name = "groonga-command-parser"
31
+ spec.version = Groonga::Command::Parser::VERSION.dup
32
+
33
+ spec.authors = ["Kouhei Sutou"]
34
+ spec.email = ["kou@clear-code.com"]
35
+
36
+ readme = File.read("README.md")
37
+ readme.force_encoding("UTF-8") if readme.respond_to?(:force_encoding)
38
+ entries = readme.split(/^\#\#\s(.*)$/)
39
+ description = clean_white_space.call(entries[entries.index("Description") + 1])
40
+ spec.summary, spec.description, = description.split(/\n\n+/, 3)
41
+
42
+ spec.files = ["README.md", "Rakefile", "Gemfile", "#{spec.name}.gemspec"]
43
+ spec.files += [".yardopts"]
44
+ spec.files += Dir.glob("lib/**/*.rb")
45
+ spec.files += Dir.glob("doc/text/*")
46
+ spec.test_files += Dir.glob("test/**/*")
47
+ # Dir.chdir("bin") do
48
+ # spec.executables = Dir.glob("*")
49
+ # end
50
+
51
+ spec.homepage = "https://github.com/groonga/groonga-command-parser"
52
+ spec.licenses = ["LGPLv2.1+"]
53
+ spec.require_paths = ["lib"]
54
+
55
+ spec.add_runtime_dependency("groonga-command")
56
+ spec.add_runtime_dependency("yajl-ruby")
57
+
58
+ spec.add_development_dependency("test-unit")
59
+ spec.add_development_dependency("test-unit-notify")
60
+ spec.add_development_dependency("rake")
61
+ spec.add_development_dependency("bundler")
62
+ spec.add_development_dependency("packnga")
63
+ spec.add_development_dependency("yard")
64
+ spec.add_development_dependency("redcarpet")
65
+ end
66
+
@@ -0,0 +1,406 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright (C) 2011-2013 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"
25
+
26
+ require "groonga/command/parser/error"
27
+ require "groonga/command/parser/version"
28
+
29
+ module Groonga
30
+ module Command
31
+ class Parser
32
+ class << self
33
+
34
+ # parses groonga command or HTTP (starts with "/d/") command.
35
+ # @overload parse(data)
36
+ # @!macro [new] parser.parse.argument
37
+ # @param [String] data parsed command.
38
+ # @return [Groonga::Command] Returns
39
+ # {Groonga::Command} including parsed data.
40
+ # @!macro parser.parse.argument
41
+ # @overload parse(data, &block)
42
+ # @!macro parser.parse.argument
43
+ def parse(data, &block)
44
+ if block_given?
45
+ event_parse(data, &block)
46
+ else
47
+ stand_alone_parse(data)
48
+ end
49
+ end
50
+
51
+ private
52
+ def event_parse(data)
53
+ parser = new
54
+
55
+ parser.on_command do |command|
56
+ yield(:on_command, command)
57
+ end
58
+ parser.on_load_start do |command|
59
+ yield(:on_load_start, command)
60
+ end
61
+ parser.on_load_columns do |command, header|
62
+ yield(:on_load_columns, command, header)
63
+ end
64
+ parser.on_load_value do |command, value|
65
+ yield(:on_load_value, command, value)
66
+ end
67
+ parser.on_load_complete do |command|
68
+ yield(:on_load_complete, command)
69
+ end
70
+ parser.on_comment do |comment|
71
+ yield(:on_comment, comment)
72
+ end
73
+
74
+ consume_data(parser, data)
75
+ end
76
+
77
+ def stand_alone_parse(data)
78
+ parsed_command = nil
79
+
80
+ parser = new
81
+ parser.on_command do |command|
82
+ parsed_command = command
83
+ end
84
+ parser.on_load_columns do |command, columns|
85
+ command[:columns] ||= columns.join(",")
86
+ end
87
+ values = []
88
+ parser.on_load_value do |_, value|
89
+ values << value
90
+ end
91
+ parser.on_load_complete do |command|
92
+ parsed_command = command
93
+ parsed_command[:values] ||= Yajl::Encoder.encode(values)
94
+ end
95
+
96
+ consume_data(parser, data)
97
+ if parsed_command.nil?
98
+ raise Error.new("not completed", data.lines.to_a.last, "")
99
+ end
100
+
101
+ parsed_command
102
+ end
103
+
104
+ def consume_data(parser, data)
105
+ if data.respond_to?(:each)
106
+ data.each do |chunk|
107
+ parser << chunk
108
+ end
109
+ else
110
+ parser << data
111
+ end
112
+ parser.finish
113
+ end
114
+ end
115
+
116
+ def initialize
117
+ reset
118
+ initialize_hooks
119
+ end
120
+
121
+ # Streaming parsing command.
122
+ # @param [String] chunk parsed chunk of command.
123
+ def <<(chunk)
124
+ @buffer << chunk
125
+ consume_buffer
126
+ end
127
+
128
+ # Finishes parsing. If Parser is loading values specified "load"
129
+ # command, this method raises {Parser::Error}.
130
+ def finish
131
+ if @loading
132
+ raise Error.new("not completed",
133
+ @command.original_source.lines.to_a.last,
134
+ "")
135
+ else
136
+ catch do |tag|
137
+ parse_line(@buffer)
138
+ end
139
+ end
140
+ end
141
+
142
+ # @overload on_command(command)
143
+ # @overload on_command {|command| }
144
+ def on_command(*arguments, &block)
145
+ if block_given?
146
+ @on_command_hook = block
147
+ else
148
+ @on_command_hook.call(*arguments) if @on_command_hook
149
+ end
150
+ end
151
+
152
+ # @overload on_load_start(command)
153
+ # @overload on_load_start {|command| }
154
+ def on_load_start(*arguments, &block)
155
+ if block_given?
156
+ @on_load_start_hook = block
157
+ else
158
+ @on_load_start_hook.call(*arguments) if @on_load_start_hook
159
+ end
160
+ end
161
+
162
+ # @overload on_load_columns(command)
163
+ # @overload on_load_columns {|command| }
164
+ def on_load_columns(*arguments, &block)
165
+ if block_given?
166
+ @on_load_columns_hook = block
167
+ else
168
+ @on_load_columns_hook.call(*arguments) if @on_load_columns_hook
169
+ end
170
+ end
171
+
172
+ # @overload on_load_value(command)
173
+ # @overload on_load_value {|command| }
174
+ def on_load_value(*arguments, &block)
175
+ if block_given?
176
+ @on_load_value_hook = block
177
+ else
178
+ @on_load_value_hook.call(*arguments) if @on_load_value_hook
179
+ end
180
+ end
181
+
182
+ # @overload on_load_complete(command)
183
+ # @overload on_load_complete(command) { }
184
+ def on_load_complete(*arguments, &block)
185
+ if block_given?
186
+ @on_load_complete_hook = block
187
+ else
188
+ @on_load_complete_hook.call(*arguments) if @on_load_complete_hook
189
+ end
190
+ end
191
+
192
+ # @overload on_comment(comment)
193
+ # @overload on_comment {|comment| }
194
+ def on_comment(*arguments, &block)
195
+ if block_given?
196
+ @on_comment_hook = block
197
+ else
198
+ @on_comment_hook.call(*arguments) if @on_comment_hook
199
+ end
200
+ end
201
+
202
+ private
203
+ def consume_buffer
204
+ catch do |tag|
205
+ loop do
206
+ if @loading
207
+ consume_load_values(tag)
208
+ else
209
+ parse_line(consume_line(tag))
210
+ end
211
+ end
212
+ end
213
+ end
214
+
215
+ def consume_load_values(tag)
216
+ if @in_load_values
217
+ json, separator, rest = @buffer.partition(/[\]},]/)
218
+ if @load_value_completed
219
+ throw(tag) if separator.empty?
220
+ if separator == ","
221
+ if /\A\s*\z/ =~ json
222
+ @command.original_source << json << separator
223
+ @buffer = rest
224
+ @load_value_completed = false
225
+ return
226
+ else
227
+ raise Error.new("record separate comma is missing",
228
+ @command.original_source.lines.to_a.last,
229
+ json)
230
+ end
231
+ elsif separator == "]"
232
+ if /\A\s*\z/ =~ json
233
+ @command.original_source << json << separator
234
+ @buffer = rest
235
+ on_load_complete(@command)
236
+ reset
237
+ return
238
+ end
239
+ end
240
+ end
241
+ @buffer = rest
242
+ parse_json(json)
243
+ if separator.empty?
244
+ throw(tag)
245
+ else
246
+ @load_value_completed = false
247
+ parse_json(separator)
248
+ end
249
+ else
250
+ spaces, start_json, rest = @buffer.partition("[")
251
+ unless /\A\s*\z/ =~ spaces
252
+ raise Error.new("there are garbages before JSON",
253
+ @command.original_source.lines.to_a.last,
254
+ spaces)
255
+ end
256
+ if start_json.empty?
257
+ @command.original_source << @buffer
258
+ @buffer.clear
259
+ throw(tag)
260
+ else
261
+ @command.original_source << spaces << start_json
262
+ @buffer = rest
263
+ @json_parser = Yajl::Parser.new
264
+ @json_parser.on_parse_complete = lambda do |object|
265
+ if object.is_a?(::Array) and @command.columns.nil?
266
+ @command.columns = object
267
+ on_load_columns(@command, object)
268
+ else
269
+ on_load_value(@command, object)
270
+ end
271
+ @load_value_completed = true
272
+ end
273
+ @in_load_values = true
274
+ end
275
+ end
276
+ end
277
+
278
+ def parse_json(json)
279
+ @command.original_source << json
280
+ begin
281
+ @json_parser << json
282
+ rescue Yajl::ParseError
283
+ before_json = @command.original_source[0..(-json.bytesize)]
284
+ message = "invalid JSON: #{$!.message}: <#{json}>:\n"
285
+ message << before_json
286
+ raise Error.new(message, before_json, json)
287
+ end
288
+ end
289
+
290
+ def consume_line(tag)
291
+ current_line, separator, rest = @buffer.partition(/\r?\n/)
292
+ throw(tag) if separator.empty?
293
+
294
+ if current_line.end_with?("\\")
295
+ @buffer.sub!(/\\\r?\n/, "")
296
+ consume_line(tag)
297
+ else
298
+ @buffer = rest
299
+ current_line
300
+ end
301
+ end
302
+
303
+ def parse_line(line)
304
+ case line
305
+ when /\A\s*\z/
306
+ # ignore empty line
307
+ when /\A\#/
308
+ on_comment($POSTMATCH)
309
+ else
310
+ @command = parse_command(line)
311
+ @command.original_source = line
312
+ process_command
313
+ end
314
+ end
315
+
316
+ def process_command
317
+ if @command.name == "load"
318
+ on_load_start(@command)
319
+ if @command.columns
320
+ on_load_columns(@command, @command.columns)
321
+ end
322
+ if @command[:values]
323
+ values = Yajl::Parser.parse(@command[:values])
324
+ if @command.columns.nil? and values.first.is_a?(::Array)
325
+ header = values.shift
326
+ @command.columns = header
327
+ on_load_columns(@command, header)
328
+ end
329
+ values.each do |value|
330
+ on_load_value(@command, value)
331
+ end
332
+ on_load_complete(@command)
333
+ reset
334
+ else
335
+ @command.original_source << "\n"
336
+ @loading = true
337
+ end
338
+ else
339
+ on_command(@command)
340
+ @command = nil
341
+ end
342
+ end
343
+
344
+ def parse_command(input)
345
+ if input.start_with?("/d/")
346
+ parse_uri_path(input)
347
+ else
348
+ parse_command_line(input)
349
+ end
350
+ end
351
+
352
+ def parse_uri_path(path)
353
+ name, arguments_string = path.split(/\?/, 2)
354
+ arguments = {}
355
+ if arguments_string
356
+ arguments_string.split(/&/).each do |argument_string|
357
+ key, value = argument_string.split(/\=/, 2)
358
+ next if value.nil?
359
+ arguments[key] = CGI.unescape(value)
360
+ end
361
+ end
362
+ name = name.gsub(/\A\/d\//, '')
363
+ name, output_type = name.split(/\./, 2)
364
+ arguments["output_type"] = output_type if output_type
365
+ command_class = Command.find(name)
366
+ command = command_class.new(name, arguments)
367
+ command.original_format = :uri
368
+ command
369
+ end
370
+
371
+ def parse_command_line(command_line)
372
+ name, *arguments = Shellwords.shellwords(command_line)
373
+ pair_arguments = {}
374
+ ordered_arguments = []
375
+ until arguments.empty?
376
+ argument = arguments.shift
377
+ if argument.start_with?("--")
378
+ pair_arguments[argument.sub(/\A--/, "")] = arguments.shift
379
+ else
380
+ ordered_arguments << argument
381
+ end
382
+ end
383
+ command_class = Command.find(name)
384
+ command = command_class.new(name, pair_arguments, ordered_arguments)
385
+ command.original_format = :command
386
+ command
387
+ end
388
+
389
+ def reset
390
+ @command = nil
391
+ @loading = false
392
+ @in_load_values = false
393
+ @load_value_completed = false
394
+ @buffer = "".force_encoding("ASCII-8BIT")
395
+ end
396
+
397
+ def initialize_hooks
398
+ @on_command_hook = nil
399
+ @on_load_start_hook = nil
400
+ @on_load_columns_hook = nil
401
+ @on_load_value_hook = nil
402
+ @on_load_complete_hook = nil
403
+ end
404
+ end
405
+ end
406
+ end