lines 0.2.0 → 0.9.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +6 -14
- data/.gitignore +1 -0
- data/.travis.yml +1 -1
- data/CHANGELOG.md +16 -0
- data/Gemfile +1 -8
- data/LICENSE.txt +1 -1
- data/README.md +21 -64
- data/examples/cli.rb +17 -0
- data/lib/lines.rb +111 -395
- data/lib/lines/common.rb +37 -0
- data/lib/lines/generator.rb +168 -0
- data/lib/lines/parser.rb +182 -0
- data/lib/lines/version.rb +1 -1
- data/lines.gemspec +10 -8
- data/spec/lines_generator_bench.rb +45 -0
- data/spec/lines_generator_spec.rb +65 -0
- data/spec/lines_parser_bench.rb +50 -0
- data/spec/{lines_loader_spec.rb → lines_parser_spec.rb} +28 -7
- data/spec/spec_helper.rb +2 -6
- metadata +57 -28
- data/lib/lines/active_record.rb +0 -70
- data/lib/lines/loader.rb +0 -229
- data/lib/lines/logger.rb +0 -61
- data/lib/lines/rack_logger.rb +0 -39
- data/spec/bench.rb +0 -46
- data/spec/lines_spec.rb +0 -190
- data/spec/parse-bench +0 -26
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'lines/generator'
|
3
|
+
|
4
|
+
describe Lines::Generator do
|
5
|
+
def expect_dump(obj, opts={})
|
6
|
+
expect(subject.generate obj, opts)
|
7
|
+
end
|
8
|
+
|
9
|
+
it "can dump stuff" do
|
10
|
+
expect_dump(foo: "bar", bar: 33).to eq('foo=bar bar=33')
|
11
|
+
end
|
12
|
+
|
13
|
+
it "handles max_depth items" do
|
14
|
+
expect_dump({x: [444]}, max_nesting: 1).to eq('x=[...]')
|
15
|
+
expect_dump({x: {y: 444}}, max_nesting: 1).to eq('x={...}')
|
16
|
+
end
|
17
|
+
|
18
|
+
it "handles max_size items" do
|
19
|
+
expect_dump({a: "aaa", b: "bbb", c: "cccc"}, max_bytesize: 18).to eq('a=aaa b=bbb c=cccc')
|
20
|
+
expect_dump({a: "aaa", b: "bbb", c: "cccc"}, max_bytesize: 17).to eq('a=aaa b=bbb ...')
|
21
|
+
expect_dump({a: "aaa", b: "bbb", c: "cccc"}, max_bytesize: 15).to eq('a=aaa b=bbb ...')
|
22
|
+
expect_dump({a: "aaa", b: "bbb", c: "cccc"}, max_bytesize: 14).to eq('a=aaa ...')
|
23
|
+
end
|
24
|
+
|
25
|
+
it "treats missing value in a pair as an empty string" do
|
26
|
+
expect_dump(x: '').to eq("x=")
|
27
|
+
end
|
28
|
+
|
29
|
+
it "escapes quotes in quoted strings" do
|
30
|
+
expect_dump(x: "foo'bar").to eq("x=\"foo'bar\"")
|
31
|
+
expect_dump(x: 'foo"bar').to eq("x='foo\"bar'")
|
32
|
+
end
|
33
|
+
|
34
|
+
it "doesn't parse literals when they are keys" do
|
35
|
+
expect_dump(3 => 4).to eq("3=4")
|
36
|
+
end
|
37
|
+
|
38
|
+
it "escapes ambiguous strings" do
|
39
|
+
expect_dump(x: '4', y: '-3.3').to eq("x='4' y='-3.3'")
|
40
|
+
expect_dump(a: '#t', b: '#f', c: 'nil').to eq("a='#t' b='#f' c='nil'")
|
41
|
+
end
|
42
|
+
|
43
|
+
it "handles some random stuff" do
|
44
|
+
expect_dump("" => "").to eq("=")
|
45
|
+
expect_dump('"' => 'zzz').to eq('\'"\'=zzz')
|
46
|
+
end
|
47
|
+
|
48
|
+
it "escapes various whitespace characters" do
|
49
|
+
expect_dump("\r\n\t" => "\r\n\t").to eq('\'\r\n\t\'=\'\r\n\t\'')
|
50
|
+
end
|
51
|
+
|
52
|
+
it "generates sample log lines" do
|
53
|
+
expect_dump("commit" => "716f337").to eq("commit=716f337")
|
54
|
+
|
55
|
+
line = <<LINE.rstrip
|
56
|
+
at=2013-07-12T21:33:47Z commit=716f337 sql="SELECT FROM_UNIXTIME(UNIX_TIMESTAMP(created_at) - UNIX_TIMESTAMP(created_at)%(300)) as timestamp FROM `job_queue_logs` WHERE `job_queue_logs`.`account_id` = 'effe376baf553c590c02090abe512278' AND (created_at >= '2013-06-28 16:56:12') GROUP BY timestamp" elapsed=[31.9 ms]
|
57
|
+
LINE
|
58
|
+
expect_dump(
|
59
|
+
"at" => Time.at(1373664827).utc,
|
60
|
+
"commit" => "716f337",
|
61
|
+
"sql" => "SELECT FROM_UNIXTIME(UNIX_TIMESTAMP(created_at) - UNIX_TIMESTAMP(created_at)%(300)) as timestamp FROM `job_queue_logs` WHERE `job_queue_logs`.`account_id` = 'effe376baf553c590c02090abe512278' AND (created_at >= '2013-06-28 16:56:12') GROUP BY timestamp",
|
62
|
+
"elapsed" => [31.9, "ms"],
|
63
|
+
).to eq(line)
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
$:.unshift File.expand_path('../../lib', __FILE__)
|
2
|
+
|
3
|
+
require 'benchmark/ips'
|
4
|
+
require 'time'
|
5
|
+
|
6
|
+
$message = {
|
7
|
+
"at" => Time.now.utc.iso8601,
|
8
|
+
"pid" => Process.pid,
|
9
|
+
"app" => File.basename($0),
|
10
|
+
"pri" => "info",
|
11
|
+
"msg" => "This is my message",
|
12
|
+
"user" => {"t" => true, "f" => false, "n" => nil},
|
13
|
+
"elapsed" => [55.67, 'ms'],
|
14
|
+
}
|
15
|
+
|
16
|
+
$data = {}
|
17
|
+
|
18
|
+
formatters = [
|
19
|
+
['lines', "Lines.dump($message)", "Lines.load($data[:lines])"],
|
20
|
+
|
21
|
+
['json/pure', "JSON.dump($message)", "JSON.load($data[:'json/pure'])"],
|
22
|
+
['oj', "Oj.dump($message)", "Oj.load($data[:oj])"],
|
23
|
+
['yajl', "Yajl.dump($message)", "Yajl.load($data[:yajl])"],
|
24
|
+
|
25
|
+
['msgpack', "MessagePack.dump($message)", "MessagePack.load($data[:msgpack])"],
|
26
|
+
['bson', "$message.to_bson", "???"],
|
27
|
+
['tnetstring', "TNetstring.dump($message)", "TNetstring.load($data[:tnetstring])"],
|
28
|
+
]
|
29
|
+
|
30
|
+
Benchmark.ips do |x|
|
31
|
+
x.compare!
|
32
|
+
formatters.each do |(feature, dumper, loader)|
|
33
|
+
begin
|
34
|
+
require feature
|
35
|
+
|
36
|
+
$data[feature.to_sym] = eval dumper
|
37
|
+
#puts "%-12s %-5d %s" % [feature, data.size, data]
|
38
|
+
|
39
|
+
msg = eval loader
|
40
|
+
|
41
|
+
if $message != msg
|
42
|
+
p [:invalid, $message, msg]
|
43
|
+
end
|
44
|
+
|
45
|
+
x.report feature, loader
|
46
|
+
rescue LoadError
|
47
|
+
puts "%-12s could not be loaded" % [feature]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -1,11 +1,11 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
-
require 'lines/
|
2
|
+
require 'lines/parser'
|
3
3
|
|
4
|
-
describe Lines::
|
5
|
-
subject
|
4
|
+
describe Lines::Parser do
|
5
|
+
subject{ Lines::Parser }
|
6
6
|
|
7
|
-
def expect_load(str)
|
8
|
-
expect(subject.parse str)
|
7
|
+
def expect_load(str, opts={})
|
8
|
+
expect(subject.parse str, opts)
|
9
9
|
end
|
10
10
|
|
11
11
|
it "can load stuff" do
|
@@ -17,12 +17,20 @@ describe Lines::Loader do
|
|
17
17
|
expect_load('x={...}').to eq("x" => {"..." => ""})
|
18
18
|
end
|
19
19
|
|
20
|
+
it "handles unfinished lines" do
|
21
|
+
expect_load('x=foo ').to eq("x" => "foo")
|
22
|
+
end
|
23
|
+
|
24
|
+
it "handles \\n at the end of line" do
|
25
|
+
expect_load("x=foo\n").to eq("x" => "foo")
|
26
|
+
end
|
27
|
+
|
20
28
|
it "treats missing value in a pair as an empty string" do
|
21
29
|
expect_load('x=').to eq("x" => "")
|
22
30
|
end
|
23
31
|
|
24
32
|
it "has non-greedy string parsing" do
|
25
|
-
expect_load('x="
|
33
|
+
expect_load('x="#t" bar="baz"').to eq("x" => "#t", "bar" => "baz")
|
26
34
|
end
|
27
35
|
|
28
36
|
it "unscapes quotes in quoted strings" do
|
@@ -39,11 +47,24 @@ describe Lines::Loader do
|
|
39
47
|
expect_load('"\""=zzz').to eq('"' => "zzz")
|
40
48
|
end
|
41
49
|
|
50
|
+
it "knows how to restore iso time" do
|
51
|
+
expect_load("at=2013-07-12T21:33:47Z").to eq("at" => Time.at(1373664827).utc)
|
52
|
+
end
|
53
|
+
|
54
|
+
it "can symbolize the names" do
|
55
|
+
expect_load("foo=bar", symbolize_names: true).to eq(foo: "bar")
|
56
|
+
end
|
57
|
+
|
58
|
+
it "restores escaped characters from a string" do
|
59
|
+
pending
|
60
|
+
expect_load('\'\r\n\t\'=\'\r\n\t\'').to eq("\r\n\t" => "\r\n\t")
|
61
|
+
end
|
62
|
+
|
42
63
|
it "parses sample log lines" do
|
43
64
|
expect_load("commit=716f337").to eq("commit" => "716f337")
|
44
65
|
|
45
66
|
line = <<LINE
|
46
|
-
at=2013-07-12T21:33:47Z commit=716f337 sql="SELECT FROM_UNIXTIME(UNIX_TIMESTAMP(created_at) - UNIX_TIMESTAMP(created_at)%(300)) as timestamp FROM `job_queue_logs` WHERE `job_queue_logs`.`account_id` = 'effe376baf553c590c02090abe512278' AND (created_at >= '2013-06-28 16:56:12') GROUP BY timestamp" elapsed=31.9
|
67
|
+
at=2013-07-12T21:33:47Z commit=716f337 sql="SELECT FROM_UNIXTIME(UNIX_TIMESTAMP(created_at) - UNIX_TIMESTAMP(created_at)%(300)) as timestamp FROM `job_queue_logs` WHERE `job_queue_logs`.`account_id` = 'effe376baf553c590c02090abe512278' AND (created_at >= '2013-06-28 16:56:12') GROUP BY timestamp" elapsed=[31.9 ms]
|
47
68
|
LINE
|
48
69
|
expect_load(line).to eq(
|
49
70
|
"at" => Time.at(1373664827).utc,
|
data/spec/spec_helper.rb
CHANGED
@@ -1,17 +1,13 @@
|
|
1
1
|
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
2
2
|
RSpec.configure do |config|
|
3
|
-
|
4
|
-
config.run_all_when_everything_filtered = true
|
5
|
-
config.filter_run :focus
|
6
|
-
|
3
|
+
# Only use the expect syntax
|
7
4
|
config.expect_with :rspec do |c|
|
8
5
|
c.syntax = :expect
|
9
6
|
end
|
10
7
|
|
11
|
-
|
12
8
|
# Run specs in random order to surface order dependencies. If you find an
|
13
9
|
# order dependency and want to debug it, you can fix the order by providing
|
14
10
|
# the seed, which is printed after each run.
|
15
11
|
# --seed 1234
|
16
12
|
config.order = 'random'
|
17
|
-
end
|
13
|
+
end
|
metadata
CHANGED
@@ -1,69 +1,98 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lines
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.9.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
|
-
-
|
7
|
+
- zimbatm
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2014-11-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: benchmark-ips
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
13
27
|
- !ruby/object:Gem::Dependency
|
14
28
|
name: bundler
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
16
30
|
requirements:
|
17
|
-
- - ~>
|
31
|
+
- - "~>"
|
18
32
|
- !ruby/object:Gem::Version
|
19
33
|
version: '1.3'
|
20
34
|
type: :development
|
21
35
|
prerelease: false
|
22
36
|
version_requirements: !ruby/object:Gem::Requirement
|
23
37
|
requirements:
|
24
|
-
- - ~>
|
38
|
+
- - "~>"
|
25
39
|
- !ruby/object:Gem::Version
|
26
40
|
version: '1.3'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
27
55
|
- !ruby/object:Gem::Dependency
|
28
56
|
name: rspec
|
29
57
|
requirement: !ruby/object:Gem::Requirement
|
30
58
|
requirements:
|
31
|
-
- -
|
59
|
+
- - ">="
|
32
60
|
- !ruby/object:Gem::Version
|
33
61
|
version: '0'
|
34
62
|
type: :development
|
35
63
|
prerelease: false
|
36
64
|
version_requirements: !ruby/object:Gem::Requirement
|
37
65
|
requirements:
|
38
|
-
- -
|
66
|
+
- - ">="
|
39
67
|
- !ruby/object:Gem::Version
|
40
68
|
version: '0'
|
41
|
-
description:
|
69
|
+
description: |
|
70
|
+
A log format that's readable by humans and easily parseable by computers.
|
42
71
|
email:
|
43
|
-
-
|
72
|
+
- zimbatm@zimbatm.com
|
44
73
|
executables: []
|
45
74
|
extensions: []
|
46
75
|
extra_rdoc_files: []
|
47
76
|
files:
|
48
|
-
- .gitignore
|
49
|
-
- .rspec
|
50
|
-
- .travis.yml
|
77
|
+
- ".gitignore"
|
78
|
+
- ".rspec"
|
79
|
+
- ".travis.yml"
|
51
80
|
- CHANGELOG.md
|
52
81
|
- Gemfile
|
53
82
|
- LICENSE.txt
|
54
83
|
- README.md
|
55
84
|
- Rakefile
|
85
|
+
- examples/cli.rb
|
56
86
|
- lib/lines.rb
|
57
|
-
- lib/lines/
|
58
|
-
- lib/lines/
|
59
|
-
- lib/lines/
|
60
|
-
- lib/lines/rack_logger.rb
|
87
|
+
- lib/lines/common.rb
|
88
|
+
- lib/lines/generator.rb
|
89
|
+
- lib/lines/parser.rb
|
61
90
|
- lib/lines/version.rb
|
62
91
|
- lines.gemspec
|
63
|
-
- spec/
|
64
|
-
- spec/
|
65
|
-
- spec/
|
66
|
-
- spec/
|
92
|
+
- spec/lines_generator_bench.rb
|
93
|
+
- spec/lines_generator_spec.rb
|
94
|
+
- spec/lines_parser_bench.rb
|
95
|
+
- spec/lines_parser_spec.rb
|
67
96
|
- spec/spec_helper.rb
|
68
97
|
homepage: https://github.com/zimbatm/lines-ruby
|
69
98
|
licenses:
|
@@ -75,23 +104,23 @@ require_paths:
|
|
75
104
|
- lib
|
76
105
|
required_ruby_version: !ruby/object:Gem::Requirement
|
77
106
|
requirements:
|
78
|
-
- -
|
107
|
+
- - ">="
|
79
108
|
- !ruby/object:Gem::Version
|
80
109
|
version: '0'
|
81
110
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
82
111
|
requirements:
|
83
|
-
- -
|
112
|
+
- - ">="
|
84
113
|
- !ruby/object:Gem::Version
|
85
114
|
version: '0'
|
86
115
|
requirements: []
|
87
116
|
rubyforge_project:
|
88
|
-
rubygems_version: 2.
|
117
|
+
rubygems_version: 2.4.2
|
89
118
|
signing_key:
|
90
119
|
specification_version: 4
|
91
|
-
summary: Lines is an opinionated structured
|
120
|
+
summary: Lines is an opinionated structured log format
|
92
121
|
test_files:
|
93
|
-
- spec/
|
94
|
-
- spec/
|
95
|
-
- spec/
|
96
|
-
- spec/
|
122
|
+
- spec/lines_generator_bench.rb
|
123
|
+
- spec/lines_generator_spec.rb
|
124
|
+
- spec/lines_parser_bench.rb
|
125
|
+
- spec/lines_parser_spec.rb
|
97
126
|
- spec/spec_helper.rb
|
data/lib/lines/active_record.rb
DELETED
@@ -1,70 +0,0 @@
|
|
1
|
-
require 'active_record'
|
2
|
-
require 'active_record/log_subscriber'
|
3
|
-
require 'lines'
|
4
|
-
|
5
|
-
module Lines
|
6
|
-
class ActiveRecordSubscriber < ActiveSupport::LogSubscriber
|
7
|
-
def render_bind(column, value)
|
8
|
-
if column
|
9
|
-
if column.binary?
|
10
|
-
value = "<#{value.bytesize} bytes of binary data>"
|
11
|
-
end
|
12
|
-
|
13
|
-
[column.name, value]
|
14
|
-
else
|
15
|
-
[nil, value]
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
def sql(event)
|
20
|
-
payload = event.payload
|
21
|
-
|
22
|
-
return if payload[:name] == 'SCHEMA' || payload[:name] == 'EXPLAIN'
|
23
|
-
|
24
|
-
args = {}
|
25
|
-
|
26
|
-
args[:name] = payload[:name] if payload[:name]
|
27
|
-
args[:sql] = payload[:sql].squeeze(' ')
|
28
|
-
|
29
|
-
if payload[:binds] && payload[:binds].any?
|
30
|
-
args[:binds] = payload[:binds].inject({}) do |hash,(col, v)|
|
31
|
-
k, v = render_bind(col, v)
|
32
|
-
hash[k] = v
|
33
|
-
hash
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
args[:elapsed] = [event.duration.round(1), 'ms']
|
38
|
-
|
39
|
-
Lines.log(args)
|
40
|
-
end
|
41
|
-
|
42
|
-
def identity(event)
|
43
|
-
Lines.log(name: event.payload[:name], line: event.payload[:line])
|
44
|
-
end
|
45
|
-
|
46
|
-
def logger; Lines.logger; end
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
# Subscribe lines to the AR events
|
51
|
-
Lines::ActiveRecordSubscriber.attach_to :active_record
|
52
|
-
|
53
|
-
# Remove the default ActiveRecord::LogSubscriber to avoid double outputs
|
54
|
-
ActiveSupport::LogSubscriber.log_subscribers.each do |subscriber|
|
55
|
-
if subscriber.is_a?(ActiveRecord::LogSubscriber)
|
56
|
-
component = :active_record
|
57
|
-
events = subscriber.public_methods(false).reject{ |method| method.to_s == 'call' }
|
58
|
-
events.each do |event|
|
59
|
-
ActiveSupport::Notifications.notifier.listeners_for("#{event}.#{component}").each do |listener|
|
60
|
-
if listener.instance_variable_get('@delegate') == subscriber
|
61
|
-
ActiveSupport::Notifications.unsubscribe listener
|
62
|
-
end
|
63
|
-
end
|
64
|
-
end
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
# Make sure it has a logger just in case
|
69
|
-
ActiveRecord::Base.logger = Lines.logger
|
70
|
-
|
data/lib/lines/loader.rb
DELETED
@@ -1,229 +0,0 @@
|
|
1
|
-
module Lines
|
2
|
-
module Error; end
|
3
|
-
|
4
|
-
class Loader
|
5
|
-
class ParseError < StandardError; include Error; end
|
6
|
-
|
7
|
-
DOT = '.'
|
8
|
-
EQUAL = '='
|
9
|
-
SPACE = ' '
|
10
|
-
OPEN_BRACKET = '['
|
11
|
-
SHUT_BRACKET = ']'
|
12
|
-
OPEN_BRACE = '{'
|
13
|
-
SHUT_BRACE = '}'
|
14
|
-
SINGLE_QUOTE = "'"
|
15
|
-
DOUBLE_QUOTE = '"'
|
16
|
-
BACKSLASH = '\\'
|
17
|
-
|
18
|
-
ESCAPED_SINGLE_QUOTE = "\\'"
|
19
|
-
ESCAPED_DOUBLE_QUOTE = '\"'
|
20
|
-
|
21
|
-
LITERAL_MATCH = /[^=\s}\]]+/
|
22
|
-
SINGLE_QUOTE_MATCH = /(?:\\.|[^'])*/
|
23
|
-
DOUBLE_QUOTE_MATCH = /(?:\\.|[^"])*/
|
24
|
-
|
25
|
-
NUM_MATCH = /-?(?:0|[1-9])\d*(?:\.\d+)?(?:[eE][+-]\d+)?/
|
26
|
-
ISO8601_ZULU_CAPTURE = /^(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)Z$/
|
27
|
-
NUM_CAPTURE = /^(#{NUM_MATCH})$/
|
28
|
-
UNIT_CAPTURE = /^(#{NUM_MATCH}):(.+)/
|
29
|
-
|
30
|
-
# Speeds parsing up a bit
|
31
|
-
constants.each(&:freeze)
|
32
|
-
|
33
|
-
EOF = nil
|
34
|
-
|
35
|
-
|
36
|
-
def self.load(string)
|
37
|
-
new.parse(string)
|
38
|
-
end
|
39
|
-
|
40
|
-
def parse(string)
|
41
|
-
init(string.rstrip)
|
42
|
-
inner_obj
|
43
|
-
end
|
44
|
-
|
45
|
-
protected
|
46
|
-
|
47
|
-
def init(string)
|
48
|
-
@string = string
|
49
|
-
@pos = 0
|
50
|
-
@c = @string[0]
|
51
|
-
end
|
52
|
-
|
53
|
-
def getc
|
54
|
-
@pos += 1
|
55
|
-
@c = @string[@pos]
|
56
|
-
end
|
57
|
-
|
58
|
-
def accept(char)
|
59
|
-
if @c == char
|
60
|
-
getc
|
61
|
-
return true
|
62
|
-
end
|
63
|
-
false
|
64
|
-
end
|
65
|
-
|
66
|
-
def peek(num)
|
67
|
-
@string[@pos+num]
|
68
|
-
end
|
69
|
-
|
70
|
-
def skip(num)
|
71
|
-
@pos += num
|
72
|
-
@c = @string[@pos]
|
73
|
-
end
|
74
|
-
|
75
|
-
def match(reg)
|
76
|
-
@string.match(reg, @pos)
|
77
|
-
end
|
78
|
-
|
79
|
-
def expect(char)
|
80
|
-
if !accept(char)
|
81
|
-
fail "Expected '#{char}' but got '#{@c}'"
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
def fail(msg)
|
86
|
-
raise ParseError, "At #{@pos}, #{msg}"
|
87
|
-
end
|
88
|
-
|
89
|
-
def dbg(*x)
|
90
|
-
#p [@pos, @c, @string[0..@pos]] + x
|
91
|
-
end
|
92
|
-
|
93
|
-
# Structures
|
94
|
-
|
95
|
-
|
96
|
-
def inner_obj
|
97
|
-
dbg :inner_obj
|
98
|
-
# Shortcut for the '...' max_depth notation
|
99
|
-
if @c == DOT && peek(1) == DOT && peek(2) == DOT
|
100
|
-
expect DOT
|
101
|
-
expect DOT
|
102
|
-
expect DOT
|
103
|
-
return {'...' => ''}
|
104
|
-
end
|
105
|
-
|
106
|
-
return {} if @c == EOF || @c == SHUT_BRACE
|
107
|
-
|
108
|
-
# First pair
|
109
|
-
k = key()
|
110
|
-
expect EQUAL
|
111
|
-
obj = {
|
112
|
-
k => value()
|
113
|
-
}
|
114
|
-
|
115
|
-
while accept(SPACE)
|
116
|
-
k = key()
|
117
|
-
expect EQUAL
|
118
|
-
obj[k] = value()
|
119
|
-
end
|
120
|
-
|
121
|
-
obj
|
122
|
-
end
|
123
|
-
|
124
|
-
def key
|
125
|
-
dbg :key
|
126
|
-
|
127
|
-
if @c == SINGLE_QUOTE
|
128
|
-
single_quoted_string
|
129
|
-
elsif @c == DOUBLE_QUOTE
|
130
|
-
double_quoted_string
|
131
|
-
else
|
132
|
-
literal(false)
|
133
|
-
end
|
134
|
-
end
|
135
|
-
|
136
|
-
def single_quoted_string
|
137
|
-
dbg :single_quoted_string
|
138
|
-
|
139
|
-
expect SINGLE_QUOTE
|
140
|
-
md = match SINGLE_QUOTE_MATCH
|
141
|
-
str = md[0].gsub ESCAPED_SINGLE_QUOTE, SINGLE_QUOTE
|
142
|
-
skip md[0].size
|
143
|
-
|
144
|
-
expect SINGLE_QUOTE
|
145
|
-
str
|
146
|
-
end
|
147
|
-
|
148
|
-
def double_quoted_string
|
149
|
-
dbg :double_quoted_string
|
150
|
-
|
151
|
-
expect DOUBLE_QUOTE
|
152
|
-
md = match DOUBLE_QUOTE_MATCH
|
153
|
-
str = md[0].gsub ESCAPED_DOUBLE_QUOTE, DOUBLE_QUOTE
|
154
|
-
skip md[0].size
|
155
|
-
|
156
|
-
expect DOUBLE_QUOTE
|
157
|
-
str
|
158
|
-
end
|
159
|
-
|
160
|
-
def literal(sub_parse)
|
161
|
-
dbg :literal, sub_parse
|
162
|
-
|
163
|
-
return "" unless ((md = match LITERAL_MATCH))
|
164
|
-
|
165
|
-
literal = md[0]
|
166
|
-
skip literal.size
|
167
|
-
|
168
|
-
return literal unless sub_parse
|
169
|
-
|
170
|
-
case literal
|
171
|
-
when 'nil'
|
172
|
-
nil
|
173
|
-
when '#t'
|
174
|
-
true
|
175
|
-
when '#f'
|
176
|
-
false
|
177
|
-
when ISO8601_ZULU_CAPTURE
|
178
|
-
Time.new($1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, '+00:00').utc
|
179
|
-
when NUM_CAPTURE
|
180
|
-
literal.index('.') ? Float(literal) : Integer(literal)
|
181
|
-
when UNIT_CAPTURE
|
182
|
-
num = $1.index('.') ? Float($1) : Integer($1)
|
183
|
-
unit = $2
|
184
|
-
[num, unit]
|
185
|
-
else
|
186
|
-
literal
|
187
|
-
end
|
188
|
-
end
|
189
|
-
|
190
|
-
def value
|
191
|
-
dbg :value
|
192
|
-
|
193
|
-
case @c
|
194
|
-
when OPEN_BRACKET
|
195
|
-
list
|
196
|
-
when OPEN_BRACE
|
197
|
-
object
|
198
|
-
when DOUBLE_QUOTE
|
199
|
-
double_quoted_string
|
200
|
-
when SINGLE_QUOTE
|
201
|
-
single_quoted_string
|
202
|
-
else
|
203
|
-
literal(:sub_parse)
|
204
|
-
end
|
205
|
-
end
|
206
|
-
|
207
|
-
def list
|
208
|
-
dbg :list
|
209
|
-
|
210
|
-
list = []
|
211
|
-
expect(OPEN_BRACKET)
|
212
|
-
list.push value
|
213
|
-
while accept(SPACE)
|
214
|
-
list.push value
|
215
|
-
end
|
216
|
-
expect(SHUT_BRACKET)
|
217
|
-
list
|
218
|
-
end
|
219
|
-
|
220
|
-
def object
|
221
|
-
dbg :object
|
222
|
-
|
223
|
-
expect(OPEN_BRACE)
|
224
|
-
obj = inner_obj
|
225
|
-
expect(SHUT_BRACE)
|
226
|
-
obj
|
227
|
-
end
|
228
|
-
end
|
229
|
-
end
|