lkml 0.1.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.
- checksums.yaml +7 -0
- data/LICENSE.md +10 -0
- data/README.md +11 -0
- data/lib/lkml/keys.rb +125 -0
- data/lib/lkml/lexer.rb +161 -0
- data/lib/lkml/parser.rb +338 -0
- data/lib/lkml/simple.rb +297 -0
- data/lib/lkml/tokens.rb +211 -0
- data/lib/lkml/tree.rb +319 -0
- data/lib/lkml/version.rb +5 -0
- data/lib/lkml/visitors.rb +91 -0
- data/lib/lkml.rb +26 -0
- metadata +54 -0
data/lib/lkml/tree.rb
ADDED
@@ -0,0 +1,319 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Node and token classes that make up the parse tree.
|
4
|
+
|
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
|
15
|
+
|
16
|
+
def format_value
|
17
|
+
@value
|
18
|
+
end
|
19
|
+
|
20
|
+
def accept(visitor)
|
21
|
+
visitor.visit_token(self)
|
22
|
+
end
|
23
|
+
|
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
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_s
|
35
|
+
[@prefix, format_value, @suffix].join
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class LeftCurlyBrace < SyntaxToken
|
40
|
+
def initialize(*, **)
|
41
|
+
super('{', *, **)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class RightCurlyBrace < SyntaxToken
|
46
|
+
def initialize(*, **)
|
47
|
+
super('}', *, **)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class Colon < SyntaxToken
|
52
|
+
def initialize(*, **)
|
53
|
+
super(':', *, **)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class LeftBracket < SyntaxToken
|
58
|
+
def initialize(*, **)
|
59
|
+
super('[', *, **)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class RightBracket < SyntaxToken
|
64
|
+
def initialize(*, **)
|
65
|
+
super(']', *, **)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class DoubleSemicolon < SyntaxToken
|
70
|
+
def initialize(*, **)
|
71
|
+
super(';;', *, **)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
class Comma < SyntaxToken
|
76
|
+
def initialize(*, **)
|
77
|
+
super(',', *, **)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
class QuotedSyntaxToken < SyntaxToken
|
82
|
+
def format_value
|
83
|
+
"\"#{@value.gsub('\"', '"').gsub('"', '\"')}\""
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
class ExpressionSyntaxToken < SyntaxToken
|
88
|
+
attr_reader :expr_suffix
|
89
|
+
|
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
|
94
|
+
|
95
|
+
def to_s
|
96
|
+
[@prefix, format_value, @expr_suffix, ';;', @suffix].join
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
class SyntaxNode
|
101
|
+
def children
|
102
|
+
raise NotImplementedError, 'Subclasses must implement the children method'
|
103
|
+
end
|
104
|
+
|
105
|
+
def line_number
|
106
|
+
raise NotImplementedError, 'Subclasses must implement the line_number method'
|
107
|
+
end
|
108
|
+
|
109
|
+
def accept(visitor)
|
110
|
+
raise NotImplementedError, 'Subclasses must implement the accept method'
|
111
|
+
end
|
112
|
+
|
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
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
class PairNode < SyntaxNode
|
125
|
+
attr_reader :type, :value, :colon
|
126
|
+
|
127
|
+
def initialize(type, value, colon: Colon.new(suffix: ' '))
|
128
|
+
super()
|
129
|
+
@type = type
|
130
|
+
@value = value
|
131
|
+
@colon = colon
|
132
|
+
end
|
133
|
+
|
134
|
+
def to_s
|
135
|
+
[@type, @colon, @value].join
|
136
|
+
end
|
137
|
+
|
138
|
+
def children
|
139
|
+
nil
|
140
|
+
end
|
141
|
+
|
142
|
+
def line_number
|
143
|
+
@type.line_number
|
144
|
+
end
|
145
|
+
|
146
|
+
def accept(visitor)
|
147
|
+
visitor.visit_pair(self)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
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
|
164
|
+
|
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
|
176
|
+
|
177
|
+
def children
|
178
|
+
if @items.any? && @items.first.is_a?(PairNode)
|
179
|
+
@items
|
180
|
+
else
|
181
|
+
[]
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def line_number
|
186
|
+
@type.line_number
|
187
|
+
end
|
188
|
+
|
189
|
+
def accept(visitor)
|
190
|
+
visitor.visit_list(self)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
class ContainerNode < SyntaxNode
|
195
|
+
attr_reader :items, :top_level
|
196
|
+
|
197
|
+
def initialize(items, top_level: false)
|
198
|
+
super()
|
199
|
+
@items = items
|
200
|
+
@top_level = top_level
|
201
|
+
validate_keys
|
202
|
+
end
|
203
|
+
|
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."
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def children
|
214
|
+
@items
|
215
|
+
end
|
216
|
+
|
217
|
+
def line_number
|
218
|
+
@items.first&.line_number
|
219
|
+
end
|
220
|
+
|
221
|
+
def accept(visitor)
|
222
|
+
visitor.visit_container(self)
|
223
|
+
end
|
224
|
+
|
225
|
+
def to_s
|
226
|
+
@items.map(&:to_s).join
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
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
|
242
|
+
|
243
|
+
def to_s
|
244
|
+
[
|
245
|
+
@type,
|
246
|
+
@colon,
|
247
|
+
@name || '',
|
248
|
+
@left_brace,
|
249
|
+
@container || '',
|
250
|
+
@right_brace
|
251
|
+
].join
|
252
|
+
end
|
253
|
+
|
254
|
+
def children
|
255
|
+
@container.children
|
256
|
+
end
|
257
|
+
|
258
|
+
def line_number
|
259
|
+
@type.line_number
|
260
|
+
end
|
261
|
+
|
262
|
+
def accept(visitor)
|
263
|
+
visitor.visit_block(self)
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
class DocumentNode < SyntaxNode
|
268
|
+
attr_reader :container, :prefix, :suffix
|
269
|
+
|
270
|
+
def initialize(container, prefix: '', suffix: '')
|
271
|
+
super()
|
272
|
+
@container = container
|
273
|
+
@prefix = prefix
|
274
|
+
@suffix = suffix
|
275
|
+
end
|
276
|
+
|
277
|
+
def children
|
278
|
+
[@container]
|
279
|
+
end
|
280
|
+
|
281
|
+
def line_number
|
282
|
+
1
|
283
|
+
end
|
284
|
+
|
285
|
+
def accept(visitor)
|
286
|
+
visitor.visit(self)
|
287
|
+
end
|
288
|
+
|
289
|
+
def to_s
|
290
|
+
[@prefix, @container, @suffix].join
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
class Visitor
|
295
|
+
def visit(document)
|
296
|
+
raise NotImplementedError, 'Subclasses must implement the visit method'
|
297
|
+
end
|
298
|
+
|
299
|
+
def visit_container(node)
|
300
|
+
raise NotImplementedError, 'Subclasses must implement the visit_container method'
|
301
|
+
end
|
302
|
+
|
303
|
+
def visit_block(node)
|
304
|
+
raise NotImplementedError, 'Subclasses must implement the visit_block method'
|
305
|
+
end
|
306
|
+
|
307
|
+
def visit_list(node)
|
308
|
+
raise NotImplementedError, 'Subclasses must implement the visit_list method'
|
309
|
+
end
|
310
|
+
|
311
|
+
def visit_pair(node)
|
312
|
+
raise NotImplementedError, 'Subclasses must implement the visit_pair method'
|
313
|
+
end
|
314
|
+
|
315
|
+
def visit_token(token)
|
316
|
+
raise NotImplementedError, 'Subclasses must implement the visit_token method'
|
317
|
+
end
|
318
|
+
end
|
319
|
+
end
|
data/lib/lkml/version.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
require_relative 'tree'
|
6
|
+
|
7
|
+
module Lkml
|
8
|
+
class BasicVisitor < Visitor
|
9
|
+
def _visit(node)
|
10
|
+
if node.is_a?(SyntaxToken)
|
11
|
+
nil
|
12
|
+
elsif node.respond_to?(:children) && node.children
|
13
|
+
node.children.each { |child| child.accept(self) }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def visit(document)
|
18
|
+
_visit(document)
|
19
|
+
end
|
20
|
+
|
21
|
+
def visit_container(node)
|
22
|
+
_visit(node)
|
23
|
+
end
|
24
|
+
|
25
|
+
def visit_block(node)
|
26
|
+
_visit(node)
|
27
|
+
end
|
28
|
+
|
29
|
+
def visit_list(node)
|
30
|
+
_visit(node)
|
31
|
+
end
|
32
|
+
|
33
|
+
def visit_pair(node)
|
34
|
+
_visit(node)
|
35
|
+
end
|
36
|
+
|
37
|
+
def visit_token(token)
|
38
|
+
_visit(token)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class LookMlVisitor < BasicVisitor
|
43
|
+
def _visit(node)
|
44
|
+
node.to_s
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class BasicTransformer < Visitor
|
49
|
+
def _visit_items(node)
|
50
|
+
if node.respond_to?(:children) && node.children
|
51
|
+
new_children = node.children.map { |child| child.accept(self) }
|
52
|
+
node.class.new(items: new_children)
|
53
|
+
else
|
54
|
+
node
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def _visit_container(node)
|
59
|
+
if node.respond_to?(:container) && node.container
|
60
|
+
new_child = node.container.accept(self)
|
61
|
+
node.class.new(container: new_child)
|
62
|
+
else
|
63
|
+
node
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def visit(node)
|
68
|
+
_visit_container(node)
|
69
|
+
end
|
70
|
+
|
71
|
+
def visit_container(node)
|
72
|
+
_visit_items(node)
|
73
|
+
end
|
74
|
+
|
75
|
+
def visit_list(node)
|
76
|
+
_visit_items(node)
|
77
|
+
end
|
78
|
+
|
79
|
+
def visit_block(node)
|
80
|
+
_visit_container(node)
|
81
|
+
end
|
82
|
+
|
83
|
+
def visit_pair(node)
|
84
|
+
node
|
85
|
+
end
|
86
|
+
|
87
|
+
def visit_token(token)
|
88
|
+
token
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
data/lib/lkml.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lkml/keys'
|
4
|
+
require_relative 'lkml/lexer'
|
5
|
+
require_relative 'lkml/parser'
|
6
|
+
require_relative 'lkml/simple'
|
7
|
+
require_relative 'lkml/tokens'
|
8
|
+
require_relative 'lkml/tree'
|
9
|
+
require_relative 'lkml/visitors'
|
10
|
+
|
11
|
+
module Lkml
|
12
|
+
def self.parse(text)
|
13
|
+
lexer = Lexer.new(text)
|
14
|
+
tokens = lexer.scan
|
15
|
+
Parser.new(tokens).parse
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.dump(obj, io = nil)
|
19
|
+
parser = DictParser.new
|
20
|
+
result = parser.parse(obj).to_s
|
21
|
+
return result unless io
|
22
|
+
|
23
|
+
io.write(result)
|
24
|
+
nil
|
25
|
+
end
|
26
|
+
end
|
metadata
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: lkml
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Sylvain Utard
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-01-19 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: ''
|
14
|
+
email: sylvain.utard@gmail.com
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- LICENSE.md
|
20
|
+
- README.md
|
21
|
+
- lib/lkml.rb
|
22
|
+
- lib/lkml/keys.rb
|
23
|
+
- lib/lkml/lexer.rb
|
24
|
+
- lib/lkml/parser.rb
|
25
|
+
- lib/lkml/simple.rb
|
26
|
+
- lib/lkml/tokens.rb
|
27
|
+
- lib/lkml/tree.rb
|
28
|
+
- lib/lkml/version.rb
|
29
|
+
- lib/lkml/visitors.rb
|
30
|
+
homepage: ''
|
31
|
+
licenses:
|
32
|
+
- MIT
|
33
|
+
metadata:
|
34
|
+
rubygems_mfa_required: 'true'
|
35
|
+
post_install_message:
|
36
|
+
rdoc_options: []
|
37
|
+
require_paths:
|
38
|
+
- lib
|
39
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '3.2'
|
44
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
requirements: []
|
50
|
+
rubygems_version: 3.5.11
|
51
|
+
signing_key:
|
52
|
+
specification_version: 4
|
53
|
+
summary: LookML Ruby Parser
|
54
|
+
test_files: []
|