lkml 0.1.0 → 0.2.0

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.
data/lib/lkml/tree.rb CHANGED
@@ -1,319 +1,445 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Node and token classes that make up the parse tree.
3
+ require_relative "keys"
4
4
 
5
5
  module Lkml
6
- class SyntaxToken
7
- attr_reader :value, :line_number, :prefix, :suffix
8
-
9
- def initialize(value, line_number = nil, prefix: '', suffix: '')
10
- @value = value
11
- @line_number = line_number
12
- @prefix = prefix
13
- @suffix = suffix
14
- end
6
+ module Tree
7
+ module_function
15
8
 
16
- def format_value
17
- @value
9
+ def items_to_str(*items)
10
+ items.join
18
11
  end
19
12
 
20
- def accept(visitor)
21
- visitor.visit_token(self)
22
- end
13
+ module Visitor
14
+ def visit(_document)
15
+ raise NotImplementedError
16
+ end
17
+
18
+ def visit_container(_node)
19
+ raise NotImplementedError
20
+ end
21
+
22
+ def visit_block(_node)
23
+ raise NotImplementedError
24
+ end
25
+
26
+ def visit_list(_node)
27
+ raise NotImplementedError
28
+ end
23
29
 
24
- def ==(other)
25
- if other.is_a?(self.class)
26
- instance_variables.all? { |var| instance_variable_get(var) == other.instance_variable_get(var) }
27
- elsif other.is_a?(String)
28
- @value == other
29
- else
30
- false
30
+ def visit_pair(_node)
31
+ raise NotImplementedError
32
+ end
33
+
34
+ def visit_token(_token)
35
+ raise NotImplementedError
31
36
  end
32
37
  end
33
38
 
34
- def to_s
35
- [@prefix, format_value, @suffix].join
39
+ class SyntaxToken
40
+ attr_reader :value, :line_number, :prefix, :suffix
41
+
42
+ def initialize(value, line_number = nil, prefix = "", suffix = "", expr_suffix: nil)
43
+ @value = value
44
+ @line_number = line_number
45
+ @prefix = prefix
46
+ @suffix = suffix
47
+ @expr_suffix = expr_suffix unless expr_suffix.nil?
48
+ freeze
49
+ end
50
+
51
+ def format_value
52
+ @value
53
+ end
54
+
55
+ def accept(visitor)
56
+ visitor.visit_token(self)
57
+ end
58
+
59
+ def ==(other)
60
+ if instance_of?(other.class)
61
+ ivars_match?(other)
62
+ elsif other.is_a?(String)
63
+ @value == other
64
+ else
65
+ false
66
+ end
67
+ end
68
+
69
+ def eql?(other)
70
+ self == other
71
+ end
72
+
73
+ def hash
74
+ instance_variables.map { |v| instance_variable_get(v) }.hash
75
+ end
76
+
77
+ def to_s
78
+ Tree.items_to_str(@prefix, format_value, @suffix)
79
+ end
80
+
81
+ def with(**changes)
82
+ self.class.new(
83
+ changes.fetch(:value, @value),
84
+ changes.fetch(:line_number, @line_number),
85
+ changes.fetch(:prefix, @prefix),
86
+ changes.fetch(:suffix, @suffix)
87
+ )
88
+ end
89
+
90
+ protected
91
+
92
+ def ivars_match?(other)
93
+ instance_variables.all? { |v| instance_variable_get(v) == other.instance_variable_get(v) }
94
+ end
36
95
  end
37
- end
38
96
 
39
- class LeftCurlyBrace < SyntaxToken
40
- def initialize(*, **)
41
- super('{', *, **)
97
+ class LeftCurlyBrace < SyntaxToken
98
+ def initialize(value = "{", line_number = nil, prefix = "", suffix = "")
99
+ super
100
+ end
42
101
  end
43
- end
44
102
 
45
- class RightCurlyBrace < SyntaxToken
46
- def initialize(*, **)
47
- super('}', *, **)
103
+ class RightCurlyBrace < SyntaxToken
104
+ def initialize(value = "}", line_number = nil, prefix = "", suffix = "")
105
+ super
106
+ end
48
107
  end
49
- end
50
108
 
51
- class Colon < SyntaxToken
52
- def initialize(*, **)
53
- super(':', *, **)
109
+ class Colon < SyntaxToken
110
+ def initialize(value = ":", line_number = nil, prefix = "", suffix = "")
111
+ super
112
+ end
54
113
  end
55
- end
56
114
 
57
- class LeftBracket < SyntaxToken
58
- def initialize(*, **)
59
- super('[', *, **)
115
+ class LeftBracket < SyntaxToken
116
+ def initialize(value = "[", line_number = nil, prefix = "", suffix = "")
117
+ super
118
+ end
60
119
  end
61
- end
62
120
 
63
- class RightBracket < SyntaxToken
64
- def initialize(*, **)
65
- super(']', *, **)
121
+ class RightBracket < SyntaxToken
122
+ def initialize(value = "]", line_number = nil, prefix = "", suffix = "")
123
+ super
124
+ end
66
125
  end
67
- end
68
126
 
69
- class DoubleSemicolon < SyntaxToken
70
- def initialize(*, **)
71
- super(';;', *, **)
127
+ class DoubleSemicolon < SyntaxToken
128
+ def initialize(value = ";;", line_number = nil, prefix = "", suffix = "")
129
+ super
130
+ end
72
131
  end
73
- end
74
132
 
75
- class Comma < SyntaxToken
76
- def initialize(*, **)
77
- super(',', *, **)
133
+ class Comma < SyntaxToken
134
+ def initialize(value = ",", line_number = nil, prefix = "", suffix = "")
135
+ super
136
+ end
78
137
  end
79
- end
80
138
 
81
- class QuotedSyntaxToken < SyntaxToken
82
- def format_value
83
- "\"#{@value.gsub('\"', '"').gsub('"', '\"')}\""
139
+ class QuotedSyntaxToken < SyntaxToken
140
+ def format_value
141
+ "\"#{@value.gsub('\\"', '"').gsub('"', '\\"')}\""
142
+ end
84
143
  end
85
- end
86
144
 
87
- class ExpressionSyntaxToken < SyntaxToken
88
- attr_reader :expr_suffix
145
+ class ExpressionSyntaxToken < SyntaxToken
146
+ attr_reader :expr_suffix
89
147
 
90
- def initialize(value, line_number = nil, prefix: ' ', expr_suffix: ' ', suffix: '')
91
- super(value, line_number, prefix: prefix, suffix: suffix)
92
- @expr_suffix = expr_suffix
93
- end
148
+ def initialize(value, line_number = nil, prefix = " ", suffix = "", expr_suffix = " ")
149
+ super(value, line_number, prefix, suffix, expr_suffix: expr_suffix)
150
+ end
94
151
 
95
- def to_s
96
- [@prefix, format_value, @expr_suffix, ';;', @suffix].join
97
- end
98
- end
152
+ def with(**changes)
153
+ ExpressionSyntaxToken.new(
154
+ changes.fetch(:value, @value),
155
+ changes.fetch(:line_number, @line_number),
156
+ changes.fetch(:prefix, @prefix),
157
+ changes.fetch(:suffix, @suffix),
158
+ changes.fetch(:expr_suffix, @expr_suffix)
159
+ )
160
+ end
99
161
 
100
- class SyntaxNode
101
- def children
102
- raise NotImplementedError, 'Subclasses must implement the children method'
162
+ def to_s
163
+ Tree.items_to_str(@prefix, format_value, @expr_suffix, ";;", @suffix)
164
+ end
103
165
  end
104
166
 
105
- def line_number
106
- raise NotImplementedError, 'Subclasses must implement the line_number method'
107
- end
167
+ class SyntaxNode
168
+ def children
169
+ raise NotImplementedError
170
+ end
108
171
 
109
- def accept(visitor)
110
- raise NotImplementedError, 'Subclasses must implement the accept method'
111
- end
172
+ def line_number
173
+ raise NotImplementedError
174
+ end
112
175
 
113
- def ==(other)
114
- if other.is_a?(self.class)
115
- instance_variables.all? { |var| instance_variable_get(var) == other.instance_variable_get(var) }
116
- elsif other.is_a?(String)
117
- @value == other
118
- else
119
- false
176
+ def accept(_visitor)
177
+ raise NotImplementedError
120
178
  end
121
179
  end
122
- end
123
180
 
124
- class PairNode < SyntaxNode
125
- attr_reader :type, :value, :colon
181
+ class PairNode < SyntaxNode
182
+ attr_reader :type, :value, :colon
126
183
 
127
- def initialize(type, value, colon: Colon.new(suffix: ' '))
128
- super()
129
- @type = type
130
- @value = value
131
- @colon = colon
132
- end
184
+ def initialize(type:, value:, colon: nil)
185
+ super()
186
+ @type = type
187
+ @value = value
188
+ @colon = colon || Colon.new(":", nil, "", " ")
189
+ freeze
190
+ end
133
191
 
134
- def to_s
135
- [@type, @colon, @value].join
136
- end
192
+ def inspect
193
+ "#{self.class.name.split('::').last}(type='#{@type.value}', value='#{@value.value}')"
194
+ end
137
195
 
138
- def children
139
- nil
140
- end
196
+ def children
197
+ nil
198
+ end
141
199
 
142
- def line_number
143
- @type.line_number
144
- end
200
+ def line_number
201
+ @type.line_number
202
+ end
145
203
 
146
- def accept(visitor)
147
- visitor.visit_pair(self)
148
- end
149
- end
204
+ def accept(visitor)
205
+ visitor.visit_pair(self)
206
+ end
150
207
 
151
- class ListNode < SyntaxNode
152
- attr_reader :type, :items, :left_bracket, :right_bracket, :colon, :leading_comma, :trailing_comma
153
-
154
- def initialize(type, items:, left_bracket:, right_bracket:, colon: Colon.new(suffix: ' '), leading_comma: nil, trailing_comma: nil) # rubocop:disable Metrics/ParameterLists,Layout/LineLength
155
- super()
156
- @type = type
157
- @items = items
158
- @left_bracket = left_bracket
159
- @right_bracket = right_bracket
160
- @colon = colon
161
- @leading_comma = leading_comma
162
- @trailing_comma = trailing_comma
163
- end
208
+ def to_s
209
+ Tree.items_to_str(@type, @colon, @value)
210
+ end
164
211
 
165
- def to_s
166
- [
167
- @type,
168
- @colon,
169
- @left_bracket,
170
- @leading_comma && @items.any? ? @leading_comma : '',
171
- @items.map(&:to_s).join(','),
172
- @trailing_comma && @items.any? ? @trailing_comma : '',
173
- @right_bracket
174
- ].join
175
- end
212
+ def with(**changes)
213
+ PairNode.new(
214
+ type: changes.fetch(:type, @type),
215
+ value: changes.fetch(:value, @value),
216
+ colon: changes.fetch(:colon, @colon)
217
+ )
218
+ end
176
219
 
177
- def children
178
- if @items.any? && @items.first.is_a?(PairNode)
179
- @items
180
- else
181
- []
220
+ def ==(other)
221
+ instance_of?(other.class) &&
222
+ @type == other.type &&
223
+ @value == other.value &&
224
+ @colon == other.colon
182
225
  end
183
226
  end
184
227
 
185
- def line_number
186
- @type.line_number
187
- end
228
+ class ListNode < SyntaxNode
229
+ attr_reader :type, :items, :left_bracket, :right_bracket, :colon, :leading_comma, :trailing_comma
230
+
231
+ def initialize(type:, items:, left_bracket:, right_bracket:, colon: nil, leading_comma: nil,
232
+ trailing_comma: nil)
233
+ super()
234
+ @type = type
235
+ @items = items.freeze
236
+ @left_bracket = left_bracket
237
+ @right_bracket = right_bracket
238
+ @colon = colon || Colon.new(":", nil, "", " ")
239
+ @leading_comma = leading_comma
240
+ @trailing_comma = trailing_comma
241
+ freeze
242
+ end
188
243
 
189
- def accept(visitor)
190
- visitor.visit_list(self)
191
- end
192
- end
244
+ def inspect
245
+ "#{self.class.name.split('::').last}(type='#{@type.value}')"
246
+ end
247
+
248
+ def children
249
+ return [].freeze if @items.empty?
193
250
 
194
- class ContainerNode < SyntaxNode
195
- attr_reader :items, :top_level
251
+ @items.first.is_a?(PairNode) ? @items : []
252
+ end
253
+
254
+ def line_number
255
+ @type.line_number
256
+ end
257
+
258
+ def accept(visitor)
259
+ visitor.visit_list(self)
260
+ end
261
+
262
+ def to_s
263
+ mid = @items.join(",")
264
+ lc = @leading_comma && !@items.empty? ? @leading_comma.to_s : ""
265
+ tc = @trailing_comma && !@items.empty? ? @trailing_comma.to_s : ""
266
+ Tree.items_to_str(@type, @colon, @left_bracket, lc, mid, tc, @right_bracket)
267
+ end
196
268
 
197
- def initialize(items, top_level: false)
198
- super()
199
- @items = items
200
- @top_level = top_level
201
- validate_keys
269
+ def with(**changes)
270
+ ListNode.new(
271
+ type: changes.fetch(:type, @type),
272
+ items: changes.fetch(:items, @items),
273
+ left_bracket: changes.fetch(:left_bracket, @left_bracket),
274
+ right_bracket: changes.fetch(:right_bracket, @right_bracket),
275
+ colon: changes.fetch(:colon, @colon),
276
+ leading_comma: changes.fetch(:leading_comma, @leading_comma),
277
+ trailing_comma: changes.fetch(:trailing_comma, @trailing_comma)
278
+ )
279
+ end
280
+
281
+ def ==(other)
282
+ instance_of?(other.class) &&
283
+ @type == other.type &&
284
+ @items == other.items &&
285
+ @left_bracket == other.left_bracket &&
286
+ @right_bracket == other.right_bracket &&
287
+ @colon == other.colon &&
288
+ @leading_comma == other.leading_comma &&
289
+ @trailing_comma == other.trailing_comma
290
+ end
202
291
  end
203
292
 
204
- def validate_keys
205
- counter = @items.each_with_object(Hash.new(0)) { |item, hash| hash[item.type.value] += 1 }
206
- counter.each do |key, count|
207
- if !@top_level && count > 1 && !PLURAL_KEYS.include?(key)
208
- raise KeyError, "Key \"#{key}\" already exists in tree and would overwrite the existing value."
293
+ class ContainerNode < SyntaxNode
294
+ attr_reader :items, :top_level
295
+
296
+ def initialize(items:, top_level: false)
297
+ super()
298
+ @top_level = top_level
299
+ @items = items.freeze
300
+ validate_duplicate_keys!
301
+ freeze
302
+ end
303
+
304
+ def inspect
305
+ "#{self.class.name.split('::').last}()"
306
+ end
307
+
308
+ def validate_duplicate_keys!
309
+ counts = Hash.new(0)
310
+ @items.each { |item| counts[item.type.value] += 1 }
311
+ counts.each do |key, count|
312
+ next if @top_level || count <= 1 || Keys::PLURAL_KEYS.include?(key)
313
+
314
+ raise KeyError,
315
+ "Key \"#{key}\" already exists in tree and would overwrite the " \
316
+ "existing value."
209
317
  end
210
318
  end
211
- end
212
319
 
213
- def children
214
- @items
215
- end
320
+ def children
321
+ @items
322
+ end
216
323
 
217
- def line_number
218
- @items.first&.line_number
219
- end
324
+ def line_number
325
+ @items[0]&.line_number
326
+ end
220
327
 
221
- def accept(visitor)
222
- visitor.visit_container(self)
223
- end
328
+ def accept(visitor)
329
+ visitor.visit_container(self)
330
+ end
224
331
 
225
- def to_s
226
- @items.map(&:to_s).join
227
- end
228
- end
332
+ def to_s
333
+ Tree.items_to_str(*@items)
334
+ end
229
335
 
230
- class BlockNode < SyntaxNode
231
- attr_reader :type, :left_brace, :right_brace, :colon, :name, :container
232
-
233
- def initialize(type, left_brace: LeftCurlyBrace.new(suffix: "\n"), right_brace: RightCurlyBrace.new(prefix: "\n"), colon: Colon.new(suffix: ' '), name: nil, container: ContainerNode.new([])) # rubocop:disable Metrics/ParameterLists,Layout/LineLength
234
- super()
235
- @type = type
236
- @left_brace = left_brace
237
- @right_brace = right_brace
238
- @colon = colon
239
- @name = name
240
- @container = container
241
- end
336
+ def with(**changes)
337
+ ContainerNode.new(
338
+ items: changes.fetch(:items, @items),
339
+ top_level: changes.fetch(:top_level, @top_level)
340
+ )
341
+ end
242
342
 
243
- def to_s
244
- [
245
- @type,
246
- @colon,
247
- @name || '',
248
- @left_brace,
249
- @container || '',
250
- @right_brace
251
- ].join
343
+ def ==(other)
344
+ instance_of?(other.class) &&
345
+ @top_level == other.top_level &&
346
+ @items == other.items
347
+ end
252
348
  end
253
349
 
254
- def children
255
- @container.children
256
- end
350
+ class BlockNode < SyntaxNode
351
+ attr_reader :type, :left_brace, :right_brace, :colon, :name, :container
257
352
 
258
- def line_number
259
- @type.line_number
260
- end
353
+ def initialize(type:, left_brace: nil, right_brace: nil, colon: nil, name: nil, container: nil)
354
+ super()
355
+ @type = type
356
+ @left_brace = left_brace || LeftCurlyBrace.new("{", nil, "", "\n")
357
+ @right_brace = right_brace || RightCurlyBrace.new("}", nil, "\n", "")
358
+ @colon = colon || Colon.new(":", nil, "", " ")
359
+ @name = name
360
+ @container = container || ContainerNode.new(items: [])
361
+ freeze
362
+ end
261
363
 
262
- def accept(visitor)
263
- visitor.visit_block(self)
264
- end
265
- end
364
+ def inspect
365
+ nm = @name ? "name='#{@name.value}'" : "None"
366
+ "#{self.class.name.split('::').last}(type='#{@type.value}', #{nm})"
367
+ end
266
368
 
267
- class DocumentNode < SyntaxNode
268
- attr_reader :container, :prefix, :suffix
369
+ def children
370
+ @container.children
371
+ end
269
372
 
270
- def initialize(container, prefix: '', suffix: '')
271
- super()
272
- @container = container
273
- @prefix = prefix
274
- @suffix = suffix
275
- end
373
+ def line_number
374
+ @type.line_number
375
+ end
276
376
 
277
- def children
278
- [@container]
279
- end
377
+ def accept(visitor)
378
+ visitor.visit_block(self)
379
+ end
280
380
 
281
- def line_number
282
- 1
283
- end
381
+ def to_s
382
+ nm = @name || ""
383
+ ctr = @container || ""
384
+ Tree.items_to_str(@type, @colon, nm, @left_brace, ctr, @right_brace)
385
+ end
284
386
 
285
- def accept(visitor)
286
- visitor.visit(self)
287
- end
387
+ def with(**changes)
388
+ BlockNode.new(
389
+ type: changes.fetch(:type, @type),
390
+ left_brace: changes.fetch(:left_brace, @left_brace),
391
+ right_brace: changes.fetch(:right_brace, @right_brace),
392
+ colon: changes.fetch(:colon, @colon),
393
+ name: changes.fetch(:name, @name),
394
+ container: changes.fetch(:container, @container)
395
+ )
396
+ end
288
397
 
289
- def to_s
290
- [@prefix, @container, @suffix].join
398
+ def ==(other)
399
+ instance_of?(other.class) &&
400
+ @type == other.type &&
401
+ @left_brace == other.left_brace &&
402
+ @right_brace == other.right_brace &&
403
+ @colon == other.colon &&
404
+ @name == other.name &&
405
+ @container == other.container
406
+ end
291
407
  end
292
- end
293
408
 
294
- class Visitor
295
- def visit(document)
296
- raise NotImplementedError, 'Subclasses must implement the visit method'
297
- end
409
+ class DocumentNode < SyntaxNode
410
+ attr_reader :container, :prefix, :suffix
298
411
 
299
- def visit_container(node)
300
- raise NotImplementedError, 'Subclasses must implement the visit_container method'
301
- end
412
+ def initialize(container, prefix = "", suffix = "")
413
+ super()
414
+ @container = container
415
+ @prefix = prefix
416
+ @suffix = suffix
417
+ freeze
418
+ end
302
419
 
303
- def visit_block(node)
304
- raise NotImplementedError, 'Subclasses must implement the visit_block method'
305
- end
420
+ def children
421
+ [@container]
422
+ end
306
423
 
307
- def visit_list(node)
308
- raise NotImplementedError, 'Subclasses must implement the visit_list method'
309
- end
424
+ def line_number
425
+ 1
426
+ end
310
427
 
311
- def visit_pair(node)
312
- raise NotImplementedError, 'Subclasses must implement the visit_pair method'
313
- end
428
+ def accept(visitor)
429
+ visitor.visit(self)
430
+ end
431
+
432
+ def to_s
433
+ Tree.items_to_str(@prefix, @container, @suffix)
434
+ end
314
435
 
315
- def visit_token(token)
316
- raise NotImplementedError, 'Subclasses must implement the visit_token method'
436
+ def with(**changes)
437
+ DocumentNode.new(
438
+ changes.fetch(:container, @container),
439
+ changes.fetch(:prefix, @prefix),
440
+ changes.fetch(:suffix, @suffix)
441
+ )
442
+ end
317
443
  end
318
444
  end
319
445
  end