kdl 1.0.6 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +8 -1
  3. data/.gitignore +1 -0
  4. data/.gitmodules +4 -0
  5. data/Gemfile +6 -1
  6. data/README.md +67 -7
  7. data/Rakefile +6 -1
  8. data/bin/kdl +1 -1
  9. data/kdl.gemspec +2 -2
  10. data/lib/kdl/document.rb +60 -2
  11. data/lib/kdl/error.rb +24 -0
  12. data/lib/kdl/kdl.tab.rb +305 -231
  13. data/lib/kdl/kdl.yy +57 -49
  14. data/lib/kdl/node.rb +116 -13
  15. data/lib/kdl/parser_common.rb +28 -0
  16. data/lib/kdl/string_dumper.rb +32 -33
  17. data/lib/kdl/tokenizer.rb +387 -136
  18. data/lib/kdl/types/base64.rb +3 -1
  19. data/lib/kdl/types/country/iso3166_countries.rb +3 -1
  20. data/lib/kdl/types/country/iso3166_subdivisions.rb +3 -1
  21. data/lib/kdl/types/country.rb +4 -2
  22. data/lib/kdl/types/currency/iso4217_currencies.rb +3 -1
  23. data/lib/kdl/types/currency.rb +3 -1
  24. data/lib/kdl/types/date_time.rb +5 -3
  25. data/lib/kdl/types/decimal.rb +3 -1
  26. data/lib/kdl/types/duration/iso8601_parser.rb +3 -1
  27. data/lib/kdl/types/duration.rb +3 -1
  28. data/lib/kdl/types/email/parser.rb +10 -8
  29. data/lib/kdl/types/email.rb +3 -1
  30. data/lib/kdl/types/hostname/validator.rb +3 -1
  31. data/lib/kdl/types/hostname.rb +3 -1
  32. data/lib/kdl/types/ip.rb +3 -1
  33. data/lib/kdl/types/irl/parser.rb +10 -8
  34. data/lib/kdl/types/irl.rb +3 -1
  35. data/lib/kdl/types/regex.rb +3 -1
  36. data/lib/kdl/types/url.rb +3 -1
  37. data/lib/kdl/types/url_template.rb +6 -4
  38. data/lib/kdl/types/uuid.rb +3 -1
  39. data/lib/kdl/types.rb +2 -0
  40. data/lib/kdl/v1/document.rb +19 -0
  41. data/lib/kdl/v1/kdl.tab.rb +594 -0
  42. data/lib/kdl/v1/kdl.yy +89 -0
  43. data/lib/kdl/v1/node.rb +32 -0
  44. data/lib/kdl/v1/string_dumper.rb +30 -0
  45. data/lib/kdl/v1/tokenizer.rb +298 -0
  46. data/lib/kdl/v1/value.rb +91 -0
  47. data/lib/kdl/v1.rb +13 -0
  48. data/lib/kdl/value.rb +87 -15
  49. data/lib/kdl/version.rb +3 -1
  50. data/lib/kdl.rb +47 -1
  51. metadata +14 -7
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KDL
4
+ module V1
5
+ module StringDumper
6
+ include ::KDL::StringDumper
7
+
8
+ def call(string)
9
+ %("#{string.each_char.map { |char| escape(char) }.join}")
10
+ end
11
+
12
+ def stringify_identifier(ident)
13
+ if bare_identifier?(ident)
14
+ ident
15
+ else
16
+ call(ident)
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def bare_identifier?(name)
23
+ escape_chars = '\\\/(){}<>;\[\]=,"'
24
+ name =~ /^([^0-9\-+\s#{escape_chars}][^\s#{escape_chars}]*|[\-+](?!true|false|null)[^0-9\s#{escape_chars}][^\s#{escape_chars}]*)$/
25
+ end
26
+
27
+ extend self
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,298 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KDL
4
+ module V1
5
+ class Tokenizer < KDL::Tokenizer
6
+ NON_IDENTIFIER_CHARS = Regexp.escape "#{SYMBOLS.keys.join}()/\\<>[]\",#{WHITESPACE.join}#{OTHER_NON_IDENTIFIER_CHARS.join}"
7
+ IDENTIFIER_CHARS = /[^#{NON_IDENTIFIER_CHARS}]/
8
+ INITIAL_IDENTIFIER_CHARS = /[^#{NON_IDENTIFIER_CHARS}0-9]/
9
+
10
+ def next_token
11
+ @context = nil
12
+ @previous_context = nil
13
+ @line_at_start = @line
14
+ @column_at_start = @column
15
+ loop do
16
+ c = self[@index]
17
+ case @context
18
+ when nil
19
+ case c
20
+ when '"'
21
+ self.context = :string
22
+ @buffer = +''
23
+ traverse(1)
24
+ when 'r'
25
+ if @str[@index + 1] == '"'
26
+ self.context = :rawstring
27
+ traverse(2)
28
+ @rawstring_hashes = 0
29
+ @buffer = +''
30
+ next
31
+ elsif @str[@index + 1] == '#'
32
+ i = @index + 1
33
+ @rawstring_hashes = 0
34
+ while @str[i] == '#'
35
+ @rawstring_hashes += 1
36
+ i += 1
37
+ end
38
+ if @str[i] == '"'
39
+ self.context = :rawstring
40
+ @index = i + 1
41
+ @buffer = +''
42
+ next
43
+ end
44
+ end
45
+ self.context = :ident
46
+ @buffer = +c
47
+ traverse(1)
48
+ when '-'
49
+ n = self[@index + 1]
50
+ if n =~ /[0-9]/
51
+ n2 = self[@index + 2]
52
+ if n == '0' && n2 =~ /[box]/
53
+ self.context = integer_context(n2)
54
+ traverse(3)
55
+ else
56
+ self.context = :decimal
57
+ traverse(1)
58
+ end
59
+ else
60
+ self.context = :ident
61
+ traverse(1)
62
+ end
63
+ @buffer = +c
64
+ when /[0-9+]/
65
+ n = self[@index + 1]
66
+ if c == '0' && n =~ /[box]/
67
+ traverse(2)
68
+ @buffer = +''
69
+ self.context = integer_context(n)
70
+ else
71
+ self.context = :decimal
72
+ @buffer = +c
73
+ traverse(1)
74
+ end
75
+ when '\\'
76
+ t = Tokenizer.new(@str, @index + 1)
77
+ la = t.next_token
78
+ if la[0] == :NEWLINE || la[0] == :EOF || (la[0] == :WS && (lan = t.next_token[0]) == :NEWLINE || lan == :EOF)
79
+ traverse_to(t.index)
80
+ @buffer = "#{c}#{la[1].value}"
81
+ @buffer << "\n" if lan == :NEWLINE
82
+ self.context = :whitespace
83
+ else
84
+ raise_error "Unexpected '\\' (#{la[0]})"
85
+ end
86
+ when *SYMBOLS.keys
87
+ return token(SYMBOLS[c], -c).tap { traverse(1) }
88
+ when *NEWLINES, "\r"
89
+ nl = expect_newline
90
+ return token(:NEWLINE, -nl).tap do
91
+ traverse(nl.length)
92
+ end
93
+ when "/"
94
+ if self[@index + 1] == '/'
95
+ self.context = :single_line_comment
96
+ traverse(2)
97
+ elsif self[@index + 1] == '*'
98
+ self.context = :multi_line_comment
99
+ @comment_nesting = 1
100
+ traverse(2)
101
+ elsif self[@index + 1] == '-'
102
+ return token(:SLASHDASH, '/-').tap { traverse(2) }
103
+ else
104
+ self.context = :ident
105
+ @buffer = +c
106
+ traverse(1)
107
+ end
108
+ when *WHITESPACE
109
+ self.context = :whitespace
110
+ @buffer = +c
111
+ traverse(1)
112
+ when nil
113
+ return [false, token(:EOF, :EOF)[1]] if @done
114
+
115
+ @done = true
116
+ return token(:EOF, :EOF)
117
+ when INITIAL_IDENTIFIER_CHARS
118
+ self.context = :ident
119
+ @buffer = +c
120
+ traverse(1)
121
+ when '('
122
+ @type_context = true
123
+ return token(:LPAREN, -c).tap { traverse(1) }
124
+ when ')'
125
+ @type_context = false
126
+ return token(:RPAREN, -c).tap { traverse(1) }
127
+ else
128
+ raise_error "Unexpected character #{c.inspect}"
129
+ end
130
+ when :ident
131
+ case c
132
+ when IDENTIFIER_CHARS
133
+ traverse(1)
134
+ @buffer << c
135
+ else
136
+ case @buffer
137
+ when 'true' then return token(:TRUE, true)
138
+ when 'false' then return token(:FALSE, false)
139
+ when 'null' then return token(:NULL, nil)
140
+ else return token(:IDENT, -@buffer)
141
+ end
142
+ end
143
+ when :string
144
+ case c
145
+ when '\\'
146
+ c2 = self[@index + 1]
147
+ if c2.match?(NEWLINES_PATTERN)
148
+ i = 2
149
+ while self[@index + i].match?(NEWLINES_PATTERN)
150
+ i+=1
151
+ end
152
+ traverse(i)
153
+ else
154
+ @buffer << c
155
+ @buffer << c2
156
+ traverse(2)
157
+ end
158
+ when '"'
159
+ return token(:STRING, -unescape(@buffer)).tap { traverse(1) }
160
+ when nil
161
+ raise_error "Unterminated string literal"
162
+ else
163
+ @buffer << c
164
+ traverse(1)
165
+ end
166
+ when :rawstring
167
+ raise_error "Unterminated rawstring literal" if c.nil?
168
+
169
+ if c == '"'
170
+ h = 0
171
+ h += 1 while self[@index + 1 + h] == '#' && h < @rawstring_hashes
172
+ if h == @rawstring_hashes
173
+ return token(:RAWSTRING, -@buffer).tap { traverse(1 + h) }
174
+ end
175
+ end
176
+
177
+ @buffer << c
178
+ traverse(1)
179
+ when :decimal
180
+ case c
181
+ when /[0-9.\-+_eE]/
182
+ traverse(1)
183
+ @buffer << c
184
+ else
185
+ return parse_decimal(@buffer)
186
+ end
187
+ when :hexadecimal
188
+ case c
189
+ when /[0-9a-fA-F_]/
190
+ traverse(1)
191
+ @buffer << c
192
+ else
193
+ return parse_hexadecimal(@buffer)
194
+ end
195
+ when :octal
196
+ case c
197
+ when /[0-7_]/
198
+ traverse(1)
199
+ @buffer << c
200
+ else
201
+ return parse_octal(@buffer)
202
+ end
203
+ when :binary
204
+ case c
205
+ when /[01_]/
206
+ traverse(1)
207
+ @buffer << c
208
+ else
209
+ return parse_binary(@buffer)
210
+ end
211
+ when :single_line_comment
212
+ case c
213
+ when *NEWLINES, "\r"
214
+ self.context = nil
215
+ @column_at_start = @column
216
+ next
217
+ when nil
218
+ @done = true
219
+ return token(:EOF, :EOF)
220
+ else
221
+ traverse(1)
222
+ end
223
+ when :multi_line_comment
224
+ if c == '/' && self[@index + 1] == '*'
225
+ @comment_nesting += 1
226
+ traverse(2)
227
+ elsif c == '*' && self[@index + 1] == '/'
228
+ @comment_nesting -= 1
229
+ traverse(2)
230
+ if @comment_nesting == 0
231
+ revert_context
232
+ end
233
+ else
234
+ traverse(1)
235
+ end
236
+ when :whitespace
237
+ if WHITESPACE.include?(c)
238
+ traverse(1)
239
+ @buffer << c
240
+ elsif c == "/" && self[@index + 1] == '*'
241
+ self.context = :multi_line_comment
242
+ @comment_nesting = 1
243
+ traverse(2)
244
+ elsif c == "\\"
245
+ t = Tokenizer.new(@str, @index + 1)
246
+ la = t.next_token
247
+ if la[0] == :NEWLINE || la[0] == :EOF || (la[0] == :WS && (lan = t.next_token[0]) == :NEWLINE || lan == :EOF)
248
+ traverse_to(t.index)
249
+ @buffer << "#{c}#{la[1].value}"
250
+ @buffer << "\n" if lan == :NEWLINE
251
+ else
252
+ raise_error "Unexpected '\\' (#{la[0]})"
253
+ end
254
+ else
255
+ return token(:WS, -@buffer)
256
+ end
257
+ else
258
+ # :nocov:
259
+ raise_error "Unknown context `#{@context}'"
260
+ # :nocov:
261
+ end
262
+ end
263
+ end
264
+
265
+ private
266
+
267
+ def allowed_in_type?(val)
268
+ %i[ident string rawstring].include?(val)
269
+ end
270
+
271
+ def allowed_after_type?(val)
272
+ !%i[single_line_comment multi_line_comment].include?(val)
273
+ end
274
+
275
+ def unescape(string)
276
+ string.gsub(/\\[^u]/) do |m|
277
+ case m
278
+ when '\n' then "\n"
279
+ when '\r' then "\r"
280
+ when '\t' then "\t"
281
+ when '\\\\' then "\\"
282
+ when '\"' then "\""
283
+ when '\b' then "\b"
284
+ when '\f' then "\f"
285
+ when '\/' then "/"
286
+ else raise_error "Unexpected escape #{m.inspect}"
287
+ end
288
+ end.gsub(/\\u\{[0-9a-fA-F]{0,6}\}/) do |m|
289
+ i = Integer(m[3..-2], 16)
290
+ if i < 0 || i > 0x10FFFF
291
+ raise_error "Invalid code point #{u}"
292
+ end
293
+ i.chr(Encoding::UTF_8)
294
+ end
295
+ end
296
+ end
297
+ end
298
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KDL
4
+ module V1
5
+ class Value < ::KDL::Value
6
+ module Methods
7
+ def to_s
8
+ return stringify_value unless type
9
+
10
+ "(#{StringDumper.stringify_identifier type})#{stringify_value}"
11
+ end
12
+
13
+ def ==(other)
14
+ return self == other.value if other.is_a?(self.class.superclass)
15
+
16
+ value == other
17
+ end
18
+
19
+ def version
20
+ 1
21
+ end
22
+
23
+ def to_v1
24
+ self
25
+ end
26
+
27
+ def to_v2
28
+ self.class.superclass.new(value, format:, type:)
29
+ end
30
+ end
31
+
32
+ include Methods
33
+
34
+ class Int < ::KDL::Value::Int
35
+ include Methods
36
+ end
37
+
38
+ class Float < ::KDL::Value::Float
39
+ include Methods
40
+
41
+ def stringify_value
42
+ if value.nan? || value.infinite?
43
+ warn "[WARNING] Attempting to serialize non-finite Float using KDL v1"
44
+ return Null.stringify_value
45
+ end
46
+ super
47
+ end
48
+ end
49
+
50
+ class Boolean < ::KDL::Value::Boolean
51
+ include Methods
52
+
53
+ def stringify_value
54
+ value.to_s
55
+ end
56
+ end
57
+
58
+ class String < ::KDL::Value::String
59
+ include Methods
60
+
61
+ def stringify_value
62
+ StringDumper.call(value)
63
+ end
64
+ end
65
+
66
+ class NullImpl < ::KDL::Value::NullImpl
67
+ include Methods
68
+
69
+ def stringify_value
70
+ "null"
71
+ end
72
+
73
+ def to_v2
74
+ type ? ::KDL::Value::NullImpl.new(type:) : ::KDL::Value::Null
75
+ end
76
+ end
77
+ Null = NullImpl.new
78
+
79
+ def self.from(value)
80
+ case value
81
+ when ::String then String.new(value)
82
+ when Integer then Int.new(value)
83
+ when ::Float then Float.new(value)
84
+ when TrueClass, FalseClass then Boolean.new(value)
85
+ when NilClass then Null
86
+ else raise Error("Unsupported value type: #{value.class}")
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
data/lib/kdl/v1.rb ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "kdl/v1/tokenizer"
4
+ require "kdl/v1/document"
5
+ require "kdl/v1/value"
6
+ require "kdl/v1/node"
7
+ require "kdl/v1/string_dumper"
8
+ require "kdl/v1/kdl.tab"
9
+
10
+ module KDL
11
+ module V1
12
+ end
13
+ end
data/lib/kdl/value.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module KDL
2
4
  class Value
3
5
  attr_reader :value, :format, :type
@@ -15,18 +17,30 @@ module KDL
15
17
  result = parser.call(self, type)
16
18
  return self.as_type(type) if result.nil?
17
19
 
18
- unless result.is_a?(::KDL::Value)
19
- raise ArgumentError, "expected parser to return an instance of ::KDL::Value, got `#{result.class}'"
20
+ unless result.is_a?(::KDL::Value::Custom)
21
+ raise ArgumentError, "expected parser to return an instance of ::KDL::Value::Custom, got `#{result.class}'"
20
22
  end
21
23
 
22
24
  result
23
25
  end
24
26
  end
25
27
 
28
+ def ==(other)
29
+ return self == other.value if other.is_a?(self.class)
30
+
31
+ value == other
32
+ end
33
+
26
34
  def to_s
27
35
  return stringify_value unless type
28
36
 
29
- "(#{StringDumper.stringify_identifier type})#{stringify_value}"
37
+ "(#{StringDumper.call type})#{stringify_value}"
38
+ end
39
+
40
+ def inspect
41
+ return value.inspect unless type
42
+
43
+ "(#{type.inspect})#{value.inspect}"
30
44
  end
31
45
 
32
46
  def stringify_value
@@ -35,31 +49,65 @@ module KDL
35
49
  value.to_s
36
50
  end
37
51
 
52
+ def version
53
+ 2
54
+ end
55
+
56
+ def to_v2
57
+ self
58
+ end
59
+
60
+ def method_missing(name, *args, **kwargs, &block)
61
+ value.public_send(name, *args, **kwargs, &block)
62
+ end
63
+
64
+ def respond_to_missing?(name, include_all = false)
65
+ value.respond_to?(name, include_all)
66
+ end
67
+
38
68
  class Int < Value
39
- def ==(other)
40
- other.is_a?(Int) && value == other.value
69
+ def to_v1
70
+ V1::Value::Int.new(value, format:, type:)
41
71
  end
42
72
  end
43
73
 
44
74
  class Float < Value
45
75
  def ==(other)
46
- other.is_a?(Float) && value == other.value
76
+ return self == other.value if other.is_a?(Float)
77
+ return other.nan? if value.nan?
78
+
79
+ value == other
47
80
  end
48
81
 
49
82
  def stringify_value
83
+ return '#nan' if value.nan?
84
+ return '#inf' if value == ::Float::INFINITY
85
+ return '#-inf' if value == -::Float::INFINITY
50
86
  return super.upcase unless value.is_a?(BigDecimal)
51
87
 
52
88
  sign, digits, _, exponent = value.split
53
- s = sign.negative? ? '-' : ''
54
- s += "#{digits[0]}.#{digits[1..-1]}"
55
- s += "E#{exponent.negative? ? '' : '+'}#{exponent - 1}"
89
+ s = +''
90
+ s << '-' if sign.negative?
91
+ s << "#{digits[0]}.#{digits[1..-1]}"
92
+ s << "E#{exponent.negative? ? '' : '+'}#{exponent - 1}"
56
93
  s
57
94
  end
95
+
96
+ def to_v1
97
+ if value.nan? || value.infinite?
98
+ warn "[WARNING] Converting non-finite Float to KDL v1"
99
+ end
100
+ V1::Value::Float.new(value, format:, type:)
101
+ end
58
102
  end
59
103
 
60
104
  class Boolean < Value
61
- def ==(other)
62
- other.is_a?(Boolean) && value == other.value
105
+ def stringify_value
106
+ "##{value}"
107
+ end
108
+
109
+ def to_v1
110
+ V1::Value::Boolean.new(value, format:, type:)
63
111
  end
64
112
  end
65
113
 
@@ -68,8 +116,8 @@ module KDL
68
116
  StringDumper.call(value)
69
117
  end
70
118
 
71
- def ==(other)
72
- other.is_a?(String) && value == other.value
119
+ def to_v1
120
+ V1::Value::String.new(value, format:, type:)
73
121
  end
74
122
  end
75
123
 
@@ -79,15 +127,39 @@ module KDL
79
127
  end
80
128
 
81
129
  def stringify_value
82
- "null"
130
+ "#null"
83
131
  end
84
132
 
85
133
  def ==(other)
86
- other.is_a?(NullImpl)
134
+ other.is_a?(NullImpl) || other.nil?
135
+ end
136
+
137
+ def to_v1
138
+ type ? V1::Value::NullImpl.new(type:) : V1::Value::Null
87
139
  end
88
140
  end
89
141
  Null = NullImpl.new
90
142
 
143
+ class Custom < Value
144
+ attr_reader :oriinal_value
145
+
146
+ def self.call(value, type)
147
+ new(value, type:)
148
+ end
149
+
150
+ def version
151
+ nil
152
+ end
153
+
154
+ def to_v1
155
+ self
156
+ end
157
+
158
+ def to_v2
159
+ self
160
+ end
161
+ end
162
+
91
163
  def self.from(value)
92
164
  case value
93
165
  when ::String then String.new(value)
data/lib/kdl/version.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module KDL
2
- VERSION = "1.0.6"
4
+ VERSION = "2.0.1"
3
5
  end
data/lib/kdl.rb CHANGED
@@ -1,14 +1,60 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "kdl/version"
4
+ require "kdl/error"
2
5
  require "kdl/tokenizer"
3
6
  require "kdl/document"
4
7
  require "kdl/value"
5
8
  require "kdl/node"
6
9
  require "kdl/string_dumper"
7
10
  require "kdl/types"
11
+ require "kdl/parser_common"
8
12
  require "kdl/kdl.tab"
13
+ require "kdl/v1"
9
14
 
10
15
  module KDL
16
+ class << self
17
+ attr_accessor :default_version
18
+ attr_accessor :default_output_version
19
+ end
20
+
11
21
  def self.parse_document(input, options = {})
12
- Parser.new.parse(input, options)
22
+ warn "[DEPRECATION] `KDL.parse_document' is deprecated. Please use `KDL.parse' instead."
23
+ parse(input, **options)
24
+ end
25
+
26
+ def self.parse(input, version: default_version, output_version: default_output_version, **options)
27
+ case version
28
+ when 2
29
+ Parser.new(output_module: output_module(output_version || 2), **options).parse(input)
30
+ when 1
31
+ V1::Parser.new.parse(input, output_module: output_module(output_version || 1), **options)
32
+ when nil
33
+ auto_parse(input, output_version:, **options)
34
+ else
35
+ raise UnsupportedVersionError.new("Unsupported version '#{version}'", version)
36
+ end
37
+ end
38
+
39
+ def self.load_file(filespec, **options)
40
+ parse(File.read(filespec, encoding: Encoding::UTF_8), **options)
41
+ end
42
+
43
+ def self.auto_parse(input, output_version: default_output_version, **options)
44
+ parse(input, version: 2, output_version: output_version || 2, **options)
45
+ rescue VersionMismatchError => e
46
+ parse(input, version: e.version, output_version: output_version || e.version, **options)
47
+ rescue Tokenizer::Error, Racc::ParseError => e
48
+ parse(input, version: 1, output_version: output_version || 1, **options) rescue raise e
49
+ end
50
+
51
+ def self.output_module(version)
52
+ case version
53
+ when 1 then KDL::V1
54
+ when 2 then KDL
55
+ else
56
+ warn "Unknown output_version `#{version}', defaulting to v2"
57
+ KDL
58
+ end
13
59
  end
14
60
  end