kdl 0.1.2 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +4 -3
- data/.gitignore +1 -0
- data/.gitmodules +3 -0
- data/README.md +16 -2
- data/Rakefile +11 -6
- data/bin/console +2 -0
- data/bin/kdl +8 -0
- data/bin/racc +29 -0
- data/bin/rake +29 -0
- data/bin/setup +1 -0
- data/kdl.gemspec +5 -5
- data/lib/kdl/document.rb +1 -2
- data/lib/kdl/kdl.tab.rb +254 -167
- data/lib/kdl/kdl.yy +44 -27
- data/lib/kdl/node.rb +30 -7
- data/lib/kdl/string_dumper.rb +45 -0
- data/lib/kdl/tokenizer.rb +52 -30
- data/lib/kdl/types/base64.rb +15 -0
- data/lib/kdl/types/country/iso3166_countries.rb +512 -0
- data/lib/kdl/types/country/iso3166_subdivisions.rb +3927 -0
- data/lib/kdl/types/country.rb +69 -0
- data/lib/kdl/types/currency/iso4217_currencies.rb +189 -0
- data/lib/kdl/types/currency.rb +26 -0
- data/lib/kdl/types/date_time.rb +41 -0
- data/lib/kdl/types/decimal.rb +13 -0
- data/lib/kdl/types/duration/iso8601_parser.rb +147 -0
- data/lib/kdl/types/duration.rb +28 -0
- data/lib/kdl/types/email/parser.rb +151 -0
- data/lib/kdl/types/email.rb +43 -0
- data/lib/kdl/types/hostname/validator.rb +51 -0
- data/lib/kdl/types/hostname.rb +32 -0
- data/lib/kdl/types/ip.rb +32 -0
- data/lib/kdl/types/irl/parser.rb +123 -0
- data/lib/kdl/types/irl.rb +46 -0
- data/lib/kdl/types/regex.rb +13 -0
- data/lib/kdl/types/url.rb +30 -0
- data/lib/kdl/types/url_template.rb +328 -0
- data/lib/kdl/types/uuid.rb +17 -0
- data/lib/kdl/types.rb +22 -0
- data/lib/kdl/value.rb +42 -10
- data/lib/kdl/version.rb +1 -1
- data/lib/kdl.rb +4 -2
- metadata +48 -7
data/lib/kdl/kdl.yy
CHANGED
@@ -4,59 +4,76 @@ class KDL::Parser
|
|
4
4
|
STRING RAWSTRING
|
5
5
|
INTEGER FLOAT TRUE FALSE NULL
|
6
6
|
WS NEWLINE
|
7
|
+
LBRACE RBRACE
|
7
8
|
LPAREN RPAREN
|
8
9
|
EQUALS
|
9
10
|
SEMICOLON
|
10
11
|
EOF
|
11
12
|
SLASHDASH
|
13
|
+
ESCLINE
|
12
14
|
rule
|
13
15
|
document : nodes { KDL::Document.new(val[0]) }
|
14
16
|
| linespaces { KDL::Document.new([])}
|
15
17
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
node
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
18
|
+
nodes : none { [] }
|
19
|
+
| linespaces node { [val[1]] }
|
20
|
+
| linespaces empty_node { [] }
|
21
|
+
| nodes node { [*val[0], val[1]] }
|
22
|
+
| nodes empty_node { val[0] }
|
23
|
+
node : untyped_node { val[0] }
|
24
|
+
| type untyped_node { val[1].as_type(val[0], @type_parsers.fetch(val[0], nil)) }
|
25
|
+
untyped_node : node_decl node_term { val[0].tap { |x| x.children = [] } }
|
26
|
+
| node_decl node_children node_term { val[0].tap { |x| x.children = val[1] } }
|
27
|
+
| node_decl empty_children node_term { val[0].tap { |x| x.children = [] } }
|
28
|
+
node_decl : identifier { KDL::Node.new(val[0]) }
|
29
|
+
| node_decl ws value { val[0].tap { |x| x.arguments << val[2] } }
|
30
|
+
| node_decl ws SLASHDASH ws_star value { val[0] }
|
31
|
+
| node_decl ws property { val[0].tap { |x| x.properties[val[2][0]] = val[2][1] } }
|
32
|
+
| node_decl ws SLASHDASH ws_star property { val[0] }
|
33
|
+
empty_node : SLASHDASH ws_star node
|
34
|
+
node_children : ws_star LBRACE nodes RBRACE { val[2] }
|
35
|
+
| ws_star LBRACE linespaces RBRACE { [] }
|
31
36
|
empty_children: SLASHDASH node_children
|
32
|
-
|
|
37
|
+
| ws empty_children
|
33
38
|
node_term: linespaces | semicolon_term
|
34
39
|
semicolon_term: SEMICOLON | SEMICOLON linespaces
|
35
40
|
|
36
|
-
|
37
|
-
|
41
|
+
type : LPAREN identifier RPAREN { val[1] }
|
42
|
+
|
43
|
+
identifier: IDENT { val[0].value }
|
44
|
+
| STRING { val[0].value }
|
45
|
+
| RAWSTRING { val[0].value }
|
38
46
|
|
39
47
|
property: identifier EQUALS value { [val[0], val[2]] }
|
40
48
|
|
41
|
-
value :
|
42
|
-
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
49
|
+
value : untyped_value
|
50
|
+
| type untyped_value { val[1].as_type(val[0], @type_parsers.fetch(val[0], nil)) }
|
51
|
+
|
52
|
+
untyped_value : STRING { KDL::Value::String.new(val[0].value) }
|
53
|
+
| RAWSTRING { KDL::Value::String.new(val[0].value) }
|
54
|
+
| INTEGER { KDL::Value::Int.new(val[0].value) }
|
55
|
+
| FLOAT { KDL::Value::Float.new(val[0].value, format: val[0].meta[:format]) }
|
56
|
+
| boolean { KDL::Value::Boolean.new(val[0]) }
|
57
|
+
| NULL { KDL::Value::Null }
|
47
58
|
|
48
59
|
boolean : TRUE { true }
|
49
60
|
| FALSE { false }
|
50
61
|
|
51
|
-
|
62
|
+
ws: WS | ESCLINE | WS ESCLINE | ESCLINE WS | WS ESCLINE WS
|
63
|
+
ws_star: none | ws
|
52
64
|
linespace: WS | NEWLINE | EOF
|
53
65
|
linespaces: linespace | linespaces linespace
|
54
|
-
linespace_star: none | linespaces
|
55
66
|
|
56
67
|
none: { nil }
|
57
68
|
|
58
69
|
---- inner
|
59
|
-
|
70
|
+
|
71
|
+
def parse(str, options = {})
|
72
|
+
if options.fetch(:parse_types, true)
|
73
|
+
@type_parsers = ::KDL::Types::MAPPING.merge(options.fetch(:type_parsers, {}))
|
74
|
+
else
|
75
|
+
@type_parsers = {}
|
76
|
+
end
|
60
77
|
@tokenizer = ::KDL::Tokenizer.new(str)
|
61
78
|
do_parse
|
62
79
|
end
|
data/lib/kdl/node.rb
CHANGED
@@ -1,31 +1,31 @@
|
|
1
1
|
module KDL
|
2
2
|
class Node
|
3
|
-
attr_accessor :name, :arguments, :properties, :children
|
3
|
+
attr_accessor :name, :arguments, :properties, :children, :type
|
4
4
|
|
5
|
-
def initialize(name, arguments = [], properties = {}, children = [])
|
5
|
+
def initialize(name, arguments = [], properties = {}, children = [], type: nil)
|
6
6
|
@name = name
|
7
7
|
@arguments = arguments
|
8
8
|
@properties = properties
|
9
9
|
@children = children
|
10
|
+
@type = type
|
10
11
|
end
|
11
12
|
|
12
13
|
def to_s(level = 0)
|
13
14
|
indent = ' ' * level
|
14
|
-
s = "#{indent}#{name}"
|
15
|
+
s = "#{indent}#{type ? "(#{id_to_s type})" : ''}#{id_to_s name}"
|
15
16
|
unless arguments.empty?
|
16
17
|
s += " #{arguments.map(&:to_s).join(' ')}"
|
17
18
|
end
|
18
19
|
unless properties.empty?
|
19
|
-
s += " #{properties.map { |k, v| "#{k}=#{v}" }.join(' ')}"
|
20
|
+
s += " #{properties.map { |k, v| "#{id_to_s k}=#{v}" }.join(' ')}"
|
20
21
|
end
|
21
22
|
unless children.empty?
|
22
23
|
s += " {\n"
|
23
|
-
s += children.map { |c| c.to_s(level + 1) }.join("\n")
|
24
|
-
s += "
|
24
|
+
s += children.map { |c| "#{c.to_s(level + 1)}\n" }.join("\n")
|
25
|
+
s += "#{indent}}"
|
25
26
|
end
|
26
27
|
s
|
27
28
|
end
|
28
|
-
alias inspect to_s
|
29
29
|
|
30
30
|
def ==(other)
|
31
31
|
return false unless other.is_a?(Node)
|
@@ -35,5 +35,28 @@ module KDL
|
|
35
35
|
properties == other.properties &&
|
36
36
|
children == other.children
|
37
37
|
end
|
38
|
+
|
39
|
+
def as_type(type, parser = nil)
|
40
|
+
if parser.nil?
|
41
|
+
@type = type
|
42
|
+
self
|
43
|
+
else
|
44
|
+
result = parser.call(self, type)
|
45
|
+
|
46
|
+
return self.as_type(type) if result.nil?
|
47
|
+
|
48
|
+
unless result.is_a?(::KDL::Node)
|
49
|
+
raise ArgumentError, "expected parser to return an instance of ::KDL::Node, got `#{result.class}'"
|
50
|
+
end
|
51
|
+
|
52
|
+
result
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def id_to_s(id)
|
59
|
+
StringDumper.stringify_identifier(id)
|
60
|
+
end
|
38
61
|
end
|
39
62
|
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module KDL
|
2
|
+
module StringDumper
|
3
|
+
class << self
|
4
|
+
def call(string)
|
5
|
+
%("#{string.each_char.map { |char| escape(char) }.join}")
|
6
|
+
end
|
7
|
+
|
8
|
+
def stringify_identifier(ident)
|
9
|
+
if bare_identifier?(ident)
|
10
|
+
ident
|
11
|
+
else
|
12
|
+
call(ident)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def print?(char)
|
19
|
+
' ' <= char && char <= '\x7e'
|
20
|
+
end
|
21
|
+
|
22
|
+
def escape(char)
|
23
|
+
case char
|
24
|
+
when "\n" then '\n'
|
25
|
+
when "\r" then '\r'
|
26
|
+
when "\t" then '\t'
|
27
|
+
when '\\' then '\\\\'
|
28
|
+
when '"' then '\"'
|
29
|
+
when "\b" then '\b'
|
30
|
+
when "\f" then '\f'
|
31
|
+
else char
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def unicode_escape(char)
|
36
|
+
"\\u{#{char.codepoints.first.to_s(16)}}"
|
37
|
+
end
|
38
|
+
|
39
|
+
def bare_identifier?(name)
|
40
|
+
escape_chars = '\\\/(){}<>;\[\]=,"'
|
41
|
+
name =~ /^([^0-9\-+\s#{escape_chars}][^\s#{escape_chars}]*|[\-+](?!true|false|null)[^0-9\s#{escape_chars}][^\s#{escape_chars}]*)$/
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/kdl/tokenizer.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'bigdecimal'
|
2
|
+
|
1
3
|
module KDL
|
2
4
|
class Tokenizer
|
3
5
|
class Error < StandardError
|
@@ -7,13 +9,14 @@ module KDL
|
|
7
9
|
end
|
8
10
|
|
9
11
|
class Token
|
10
|
-
attr_reader :type, :value, :line, :column
|
12
|
+
attr_reader :type, :value, :line, :column, :meta
|
11
13
|
|
12
|
-
def initialize(type, value, line, column)
|
14
|
+
def initialize(type, value, line, column, meta = {})
|
13
15
|
@type = type
|
14
16
|
@value = value
|
15
17
|
@line = line
|
16
18
|
@column = column
|
19
|
+
@meta = meta
|
17
20
|
end
|
18
21
|
|
19
22
|
def ==(other)
|
@@ -31,8 +34,8 @@ module KDL
|
|
31
34
|
attr_reader :index
|
32
35
|
|
33
36
|
SYMBOLS = {
|
34
|
-
'{' => :
|
35
|
-
'}' => :
|
37
|
+
'{' => :LBRACE,
|
38
|
+
'}' => :RBRACE,
|
36
39
|
'=' => :EQUALS,
|
37
40
|
'=' => :EQUALS,
|
38
41
|
';' => :SEMICOLON
|
@@ -46,10 +49,13 @@ module KDL
|
|
46
49
|
|
47
50
|
NEWLINES = ["\u000A", "\u0085", "\u000C", "\u2028", "\u2029"]
|
48
51
|
|
49
|
-
NON_IDENTIFIER_CHARS = Regexp.escape "#{SYMBOLS.keys.join('')}
|
52
|
+
NON_IDENTIFIER_CHARS = Regexp.escape "#{SYMBOLS.keys.join('')}()/\\<>[]\","
|
50
53
|
IDENTIFIER_CHARS = /[^#{NON_IDENTIFIER_CHARS}\x0-\x20]/
|
51
54
|
INITIAL_IDENTIFIER_CHARS = /[^#{NON_IDENTIFIER_CHARS}0-9\x0-\x20]/
|
52
55
|
|
56
|
+
ALLOWED_IN_TYPE = [:ident, :string, :rawstring]
|
57
|
+
NOT_ALLOWED_AFTER_TYPE = [:single_line_comment, :multi_line_comment]
|
58
|
+
|
53
59
|
def initialize(str, start = 0)
|
54
60
|
@str = str
|
55
61
|
@context = nil
|
@@ -60,6 +66,8 @@ module KDL
|
|
60
66
|
@previous_context = nil
|
61
67
|
@line = 1
|
62
68
|
@column = 1
|
69
|
+
@type_context = false
|
70
|
+
@last_token = nil
|
63
71
|
end
|
64
72
|
|
65
73
|
def next_token
|
@@ -117,15 +125,13 @@ module KDL
|
|
117
125
|
end
|
118
126
|
when '\\'
|
119
127
|
t = Tokenizer.new(@str, @index + 1)
|
120
|
-
la = t.next_token
|
121
|
-
if la == :NEWLINE
|
122
|
-
@index = t.index
|
123
|
-
new_line
|
124
|
-
elsif la == :WS && (lan = t.next_token[0]) == :NEWLINE
|
128
|
+
la = t.next_token
|
129
|
+
if la[0] == :NEWLINE || la[0] == :EOF || (la[0] == :WS && (lan = t.next_token[0]) == :NEWLINE || lan == :EOF)
|
125
130
|
@index = t.index
|
126
131
|
new_line
|
132
|
+
return token(:ESCLINE, "\\#{la[1].value}")
|
127
133
|
else
|
128
|
-
raise_error "Unexpected '\\'"
|
134
|
+
raise_error "Unexpected '\\' (#{la[0]})"
|
129
135
|
end
|
130
136
|
when *SYMBOLS.keys
|
131
137
|
return token(SYMBOLS[c], c).tap { traverse(1) }
|
@@ -174,6 +180,12 @@ module KDL
|
|
174
180
|
self.context = :ident
|
175
181
|
@buffer = c
|
176
182
|
traverse(1)
|
183
|
+
when '('
|
184
|
+
@type_context = true
|
185
|
+
return token(:LPAREN, c).tap { traverse(1) }
|
186
|
+
when ')'
|
187
|
+
@type_context = false
|
188
|
+
return token(:RPAREN, c).tap { traverse(1) }
|
177
189
|
else
|
178
190
|
raise_error "Unexpected character #{c.inspect}"
|
179
191
|
end
|
@@ -279,18 +291,6 @@ module KDL
|
|
279
291
|
if WHITEPACE.include?(c)
|
280
292
|
traverse(1)
|
281
293
|
@buffer += c
|
282
|
-
elsif c == "\\"
|
283
|
-
t = Tokenizer.new(@str, @index + 1)
|
284
|
-
la = t.next_token[0]
|
285
|
-
if la == :NEWLINE
|
286
|
-
@index = t.index
|
287
|
-
new_line
|
288
|
-
elsif (la == :WS && (lan = t.next_token[0]) == :NEWLINE)
|
289
|
-
@index = t.index
|
290
|
-
new_line
|
291
|
-
else
|
292
|
-
raise_error "Unexpected '\\'"
|
293
|
-
end
|
294
294
|
elsif c == "/" && @str[@index + 1] == '*'
|
295
295
|
self.context = :multi_line_comment
|
296
296
|
@comment_nesting = 1
|
@@ -304,8 +304,8 @@ module KDL
|
|
304
304
|
|
305
305
|
private
|
306
306
|
|
307
|
-
def token(type, value)
|
308
|
-
[type, Token.new(type, value, @line_at_start, @column_at_start)]
|
307
|
+
def token(type, value, **meta)
|
308
|
+
@last_token = [type, Token.new(type, value, @line_at_start, @column_at_start, meta)]
|
309
309
|
end
|
310
310
|
|
311
311
|
def traverse(n = 1)
|
@@ -323,6 +323,11 @@ module KDL
|
|
323
323
|
end
|
324
324
|
|
325
325
|
def context=(val)
|
326
|
+
if @type_context && !ALLOWED_IN_TYPE.include?(val)
|
327
|
+
raise_error "#{val} context not allowed in type declaration"
|
328
|
+
elsif @last_token && @last_token[0] == :RPAREN && NOT_ALLOWED_AFTER_TYPE.include?(val)
|
329
|
+
raise_error 'Comments are not allowed after a type declaration'
|
330
|
+
end
|
326
331
|
@previous_context = @context
|
327
332
|
@context = val
|
328
333
|
end
|
@@ -333,18 +338,35 @@ module KDL
|
|
333
338
|
end
|
334
339
|
|
335
340
|
def parse_decimal(s)
|
336
|
-
return
|
337
|
-
|
341
|
+
return parse_float(s) if s =~ /[.E]/i
|
342
|
+
|
343
|
+
token(:INTEGER, Integer(munch_underscores(s), 10), format: '%d')
|
344
|
+
end
|
345
|
+
|
346
|
+
def parse_float(s)
|
347
|
+
match, _, fraction, exponent = *s.match(/^([-+]?[\d_]+)(?:\.([\d_]+))?(?:[eE]([-+]?[\d_]+))?$/)
|
348
|
+
raise_error "Invalid floating point value #{s}" if match.nil?
|
349
|
+
|
350
|
+
s = munch_underscores(s)
|
351
|
+
|
352
|
+
decimals = fraction.nil? ? 0 : fraction.size
|
353
|
+
value = Float(s)
|
354
|
+
scientific = value.abs >= 100 || (exponent && exponent.to_i.abs >= 2)
|
355
|
+
if value.infinite? || (value.zero? && exponent.to_i < 0)
|
356
|
+
token(:FLOAT, BigDecimal(s))
|
357
|
+
else
|
358
|
+
token(:FLOAT, value, format: scientific ? "%.#{decimals}E" : nil)
|
359
|
+
end
|
338
360
|
end
|
339
|
-
|
361
|
+
|
340
362
|
def parse_hexadecimal(s)
|
341
363
|
token(:INTEGER, Integer(munch_underscores(s), 16))
|
342
364
|
end
|
343
|
-
|
365
|
+
|
344
366
|
def parse_octal(s)
|
345
367
|
token(:INTEGER, Integer(munch_underscores(s), 8))
|
346
368
|
end
|
347
|
-
|
369
|
+
|
348
370
|
def parse_binary(s)
|
349
371
|
token(:INTEGER, Integer(munch_underscores(s), 2))
|
350
372
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'base64'
|
2
|
+
|
3
|
+
module KDL
|
4
|
+
module Types
|
5
|
+
class Base64 < Value
|
6
|
+
def self.call(value, type = 'base64')
|
7
|
+
return nil unless value.is_a? ::KDL::Value::String
|
8
|
+
|
9
|
+
data = ::Base64.decode64(value.value)
|
10
|
+
new(data, type: type)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
MAPPING['base64'] = Base64
|
14
|
+
end
|
15
|
+
end
|