-
Notifications
You must be signed in to change notification settings - Fork 0
/
json_parser.cr
215 lines (193 loc) · 5.27 KB
/
json_parser.cr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
require "../spec_helper"
require "benchmark"
require "json"
include Parsem
STRING_DELIMETER = '"'
STRING_ESCAPE_CHAR = '\\'
ARRAY_START_DELIMITER = '['
ARRAY_END_DELIMITER = ']'
OBJECT_START_DELIMITER = '{'
OBJECT_END_DELIMITER = '}'
ITEM_DELIMITER = ','
KEY_VALUE_DELIMITER = ':'
alias JSONValue = JSON::Any
value = Parser(Char, JSONValue).fail
null = (
string("null")
).map { JSONValue.new(nil) }
bool = (
string("true") | string("false")
).map { |s| JSONValue.new(s[0] == 't') }
integer = (
Parsem.digit.repeat(1..)
).join.map { |s| JSONValue.new(s.to_i64) }.name("integer")
string_content_char_simple = none_of([STRING_DELIMETER, STRING_ESCAPE_CHAR])
string_content_char_escape_seq = \
(token(STRING_ESCAPE_CHAR) >> alternatives(
[{'"', '"'},
{'\\', '\\'},
{'/', '/'},
{'b', '\b'},
{'f', '\f'},
{'n', '\n'},
{'r', '\r'},
{'t', '\t'},
]
.map { |char| token(char[0]).map { char[1] } }
))
string_content_char = string_content_char_simple | string_content_char_escape_seq
string_raw = (
token(STRING_DELIMETER) >>
string_content_char.repeat(0..) <<
token(STRING_DELIMETER)
).join
string = string_raw.map { |s| JSONValue.new(s) }.name("string")
array = (
token(ARRAY_START_DELIMITER) >> ws >>
lazy(
((value << ws << token(ITEM_DELIMITER) << ws).repeat(..).extend <=>
value).optional.flatten
) <<
ws << token(ARRAY_END_DELIMITER)
).map { |array| JSONValue.new(array) }.name("array")
record KeyValuePair, key : String, value : JSONValue
object_key_value_pair = infer(->KeyValuePair.new) ^
string_raw.name("key") << ws << token(KEY_VALUE_DELIMITER) << ws <=>
lazy(value)
object = (
token(OBJECT_START_DELIMITER) >> ws >>
(((object_key_value_pair << ws << token(ITEM_DELIMITER) << ws).repeat(..).extend <=>
object_key_value_pair)).optional.flatten <<
ws << token(OBJECT_END_DELIMITER)
).map { |key_value_pairs| JSONValue.new Hash.zip(
key_value_pairs.map &.key,
key_value_pairs.map &.value
) }.name("object")
value = null | bool | integer | string | array | object
json = ws >> (array | object) << ws
# TODO: Tests for arrays
describe "JSON parser" do
context "given valid input" do
it "parses empty objects" do
json.parse(
<<-'END'
{}
END
).should eq({} of JSONValue => JSONValue)
json.parse(
<<-'END'
{ }
END
).should eq({} of JSONValue => JSONValue)
end
it "parses objects with simple key-value pairs" do
json.parse(
<<-'END'
{"a": "b"}
END
).should eq({"a" => "b"})
json.parse(
<<-'END'
{
"abc": 123,
"defg": null,
"h": true,
"i": false,
"j": [],
"k": {}
}
END
).should eq({
"abc" => 123,
"defg" => nil,
"h" => true,
"i" => false,
"j" => [] of JSONValue,
"k" => {} of JSONValue => JSONValue,
})
end
it "parses objects with keys that contain escape sequences" do
json.parse(
<<-'END'
{"quote\"backslash\\tab\tlinebreak\n": "value"}
END
).should eq({"quote\"backslash\\tab\tlinebreak\n" => "value"})
end
end
context "given invalid input" do
it "rejects unclosed objects" do
json.parse(
<<-'END'
{
END
).to_s.should eq("ParseError: expected }, but found end of input")
json.parse(
<<-'END'
{
"abc": "defg"
END
).to_s.should eq("ParseError: expected }, but found end of input")
end
it "rejects objects with trailing commas" do
json.parse(
<<-'END'
{,}
END
).to_s.should eq("ParseError: expected }, but found ,")
json.parse(
<<-'END'
{"a": "b", }
END
).to_s.should eq("ParseError: expected key, but found }")
end
it "rejects objects without keys for values" do
json.parse(
<<-'END'
{ : "value" }
END
).to_s.should eq("ParseError: expected }, but found :")
end
it "rejects objects without values for keys" do
json.parse(
<<-'END'
{"key": }
END
).to_s.should eq("ParseError: expected null, true, false, integer, string, array or object, but found }")
json.parse(
<<-'END'
{
"a": "b",
"c": "d",
"e"
}
END
).to_s.should eq("ParseError: expected :, but found }")
end
end
{% if flag? :release %}
context "doesn't take forever" do
it "for large arrays of integers" do
times = Benchmark.measure do
json.parse(
"[#{(1..500_000).to_a.join(", ")}]"
)
end
print_times times
times.utime.should be < 1
end
it "for large arrays of strings" do
times = Benchmark.measure do
json.parse(
"[#{(1..500_000).map { |i| "\"#{i}\"" }.join(", ")}]"
)
end
print_times times
times.utime.should be < 1
end
end
{% end %}
end
private def print_times(times : Benchmark::BM::Tms)
puts "\n user system total real"
puts times
end