kdl 1.0.6 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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