groonga-command-parser 1.0.0

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