/
core.cr
340 lines (288 loc) · 9.15 KB
/
core.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
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
# Provides utility methods for the YAML 1.1 core schema
# with the additional independent types specified in http://yaml.org/type/
module YAML::Schema::Core
# Deserializes a YAML document.
#
# Same as `YAML.parse`.
def self.parse(data : String | IO)
Parser.new data, &.parse
end
# Deserializes multiple YAML documents.
#
# Same as `YAML.parse_all`.
def self.parse_all(data : String | IO)
Parser.new data, &.parse_all
end
# Assuming the *pull_parser* is positioned in a scalar,
# parses it according to the core schema, taking the
# scalar's style and tag into account, then advances
# the pull parser.
def self.parse_scalar(pull_parser : YAML::PullParser) : Nil | Bool | Int64 | Float64 | String | Time | Bytes
string = pull_parser.value
# Check for core schema tags
process_scalar_tag(pull_parser, pull_parser.tag) do |value|
return value
end
# Non-plain scalar is always a string
unless pull_parser.scalar_style.plain?
return string
end
parse_scalar(string)
end
# Parses a scalar value from the given *node*.
def self.parse_scalar(node : YAML::Nodes::Scalar) : Nil | Bool | Int64 | Float64 | String | Time | Bytes
string = node.value
# Check for core schema tags
process_scalar_tag(node) do |value|
return value
end
# Non-plain scalar is always a string
unless node.style.plain?
return string
end
parse_scalar(string)
end
# Parses a string according to the core schema, assuming
# the string had a plain style.
#
# ```
# YAML::Schema::Core.parse_scalar("hello") # => "hello"
# YAML::Schema::Core.parse_scalar("1.2") # => 1.2
# YAML::Schema::Core.parse_scalar("false") # => false
# ```
def self.parse_scalar(string : String) : Nil | Bool | Int64 | Float64 | String | Time | Bytes
if parse_null?(string)
return nil
end
value = parse_bool?(string)
return value unless value.nil?
value = parse_float_infinity_and_nan?(string)
return value if value
# Optimizations for prefixes that either parse to
# a number or are strings otherwise
case string
when .starts_with?("0x"),
.starts_with?("+0x"),
.starts_with?("-0x")
value = string.to_i64?(base: 16, prefix: true)
return value || string
when .starts_with?("0."),
.starts_with?('.')
value = parse_float?(string)
return value || string
when .starts_with?('0')
return 0_i64 if string.size == 1
value = string.to_i64?(base: 8, prefix: true)
return value || string
when .starts_with?('-'),
.starts_with?('+')
value = parse_number?(string)
return value || string
end
if string[0].ascii_number?
value = parse_number?(string)
return value if value
value = parse_time?(string)
return value if value
end
string
end
# Returns whether a string is reserved and must non be output
# with a plain style, according to the core schema.
#
# ```
# YAML::Schema::Core.reserved_string?("hello") # => false
# YAML::Schema::Core.reserved_string?("1.2") # => true
# YAML::Schema::Core.reserved_string?("false") # => true
# ```
def self.reserved_string?(string) : Bool
# There's simply no other way than parsing the string and
# checking what we got.
#
# The performance loss is minimal because `parse_scalar`
# doesn't allocate memory: it can only return primitive
# types, or `Time`, which is a struct.
!parse_scalar(string).is_a?(String)
end
# If `node` parses to a null value, returns `nil`, otherwise
# invokes the given block.
def self.parse_null_or(node : YAML::Nodes::Node)
if node.is_a?(YAML::Nodes::Scalar) && parse_null?(node.value)
nil
else
yield
end
end
# Invokes the block for each of the given *node*s keys and
# values, resolving merge keys (<<) when found (keys and
# values of the resolved merge mappings are yielded,
# recursively).
def self.each(node : YAML::Nodes::Mapping)
# We can't just traverse the nodes and invoke yield because
# yield can't recurse. So, we use a stack of {Mapping, index}.
# We pop from the stack and traverse the mapping values.
# When we find a merge, we stop (put back in the stack with
# that mapping and next index) and add solved mappings from
# the merge to the stack, and continue processing.
stack = [{node, 0}]
# Mappings that we already visited. In case of a recursion
# we want to stop. For example:
#
# foo: &foo
# <<: *foo
#
# When we traverse &foo we'll put it in visited,
# and when we find it in *foo we'll skip it.
#
# This has no use case, but we don't want to hang the program.
visited = Set(YAML::Nodes::Mapping).new
until stack.empty?
mapping, index = stack.pop
visited << mapping
while index < mapping.nodes.size
key = mapping.nodes[index]
index += 1
value = mapping.nodes[index]
index += 1
if key.is_a?(YAML::Nodes::Scalar) &&
key.value == "<<" &&
key.tag != "tag:yaml.org,2002:str" &&
solve_merge(stack, mapping, index, value, visited)
break
else
yield({key, value})
end
end
end
end
private def self.solve_merge(stack, mapping, index, value, visited)
value = value.value if value.is_a?(YAML::Nodes::Alias)
case value
when YAML::Nodes::Mapping
stack.push({mapping, index})
unless visited.includes?(value)
stack.push({value, 0})
end
true
when YAML::Nodes::Sequence
all_mappings = value.nodes.all? do |elem|
elem = elem.value if elem.is_a?(YAML::Nodes::Alias)
elem.is_a?(YAML::Nodes::Mapping)
end
if all_mappings
stack.push({mapping, index})
value.each do |elem|
elem = elem.value if elem.is_a?(YAML::Nodes::Alias)
mapping = elem.as(YAML::Nodes::Mapping)
unless visited.includes?(mapping)
stack.push({mapping, 0})
end
end
true
else
false
end
else
false
end
end
protected def self.parse_binary(string, location) : Bytes
Base64.decode(string)
rescue ex : Base64::Error
raise YAML::ParseException.new("Error decoding Base64: #{ex.message}", *location)
end
protected def self.parse_bool(string, location) : Bool
value = parse_bool?(string)
unless value.nil?
return value
end
raise YAML::ParseException.new("Invalid bool", *location)
end
protected def self.parse_int(string, location) : Int64
string.to_i64?(underscore: true, prefix: true) ||
raise(YAML::ParseException.new("Invalid int", *location))
end
protected def self.parse_float(string, location) : Float64
parse_float_infinity_and_nan?(string) ||
parse_float?(string) ||
raise(YAML::ParseException.new("Invalid float", *location))
end
protected def self.parse_null(string, location) : Nil
if parse_null?(string)
return nil
end
raise YAML::ParseException.new("Invalid null", *location)
end
protected def self.parse_time(string, location) : Time
parse_time?(string) ||
raise(YAML::ParseException.new("Invalid timestamp", *location))
end
protected def self.process_scalar_tag(scalar)
process_scalar_tag(scalar, scalar.tag) do |value|
yield value
end
end
protected def self.process_scalar_tag(source, tag)
case tag
when "tag:yaml.org,2002:binary"
yield parse_binary(source.value, source.location)
when "tag:yaml.org,2002:bool"
yield parse_bool(source.value, source.location)
when "tag:yaml.org,2002:float"
yield parse_float(source.value, source.location)
when "tag:yaml.org,2002:int"
yield parse_int(source.value, source.location)
when "tag:yaml.org,2002:null"
yield parse_null(source.value, source.location)
when "tag:yaml.org,2002:str"
yield source.value
when "tag:yaml.org,2002:timestamp"
yield parse_time(source.value, source.location)
end
end
private def self.parse_null?(string)
case string
when .empty?, "~", "null", "Null", "NULL"
true
else
false
end
end
private def self.parse_bool?(string)
case string
when "yes", "Yes", "YES", "true", "True", "TRUE", "on", "On", "ON"
true
when "no", "No", "NO", "false", "False", "FALSE", "off", "Off", "OFF"
false
else
nil
end
end
private def self.parse_number?(string)
parse_int?(string) || parse_float?(string)
end
private def self.parse_int?(string)
string.to_i64?(underscore: true)
end
private def self.parse_float?(string)
string = string.delete('_') if string.includes?('_')
string.to_f64?
end
private def self.parse_float_infinity_and_nan?(string)
case string
when ".inf", ".Inf", ".INF", "+.inf", "+.Inf", "+.INF"
Float64::INFINITY
when "-.inf", "-.Inf", "-.INF"
-Float64::INFINITY
when ".nan", ".NaN", ".NAN"
Float64::NAN
else
nil
end
end
private def self.parse_time?(string)
# Minimum length is that of YYYY-M-D
return nil if string.size < 8
Time::Format::YAML_DATE.parse?(string)
end
end