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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +28 -0
- data/.gitignore +57 -0
- data/.rubocop.yml +25 -0
- data/.ruby-version +1 -0
- data/CONTRIBUTING.md +56 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +76 -0
- data/LICENSE.md +1 -2
- data/README.md +81 -7
- data/Rakefile +12 -0
- data/bin/lkml +6 -0
- data/lib/lkml/cli.rb +44 -0
- data/lib/lkml/keys.rb +64 -114
- data/lib/lkml/lexer.rb +45 -68
- data/lib/lkml/parser.rb +296 -191
- data/lib/lkml/simple.rb +244 -238
- data/lib/lkml/tokens.rb +44 -129
- data/lib/lkml/tree.rb +362 -236
- data/lib/lkml/utils.rb +32 -0
- data/lib/lkml/version.rb +1 -1
- data/lib/lkml/visitors.rb +69 -64
- data/lib/lkml.rb +49 -14
- data/lkml.gemspec +35 -0
- data/script/benchmark.rb +36 -0
- data/script/download_lookml.rb +70 -0
- metadata +88 -13
data/lib/lkml/simple.rb
CHANGED
|
@@ -1,296 +1,302 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
#
|
|
5
|
-
# These classes facilitate parsing and generation to and from simple data structures like
|
|
6
|
-
# lists and dictionaries, and allow users to parse and generate LookML without needing
|
|
7
|
-
# to interact with the parse tree.
|
|
3
|
+
require "logger"
|
|
8
4
|
|
|
9
|
-
require_relative
|
|
10
|
-
require_relative
|
|
5
|
+
require_relative "keys"
|
|
6
|
+
require_relative "tree"
|
|
11
7
|
|
|
12
8
|
module Lkml
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
case key
|
|
16
|
-
when 'filters', 'bind_filters', 'extends'
|
|
17
|
-
"#{key}__all"
|
|
18
|
-
when 'query'
|
|
19
|
-
'queries'
|
|
20
|
-
when 'remote_dependency'
|
|
21
|
-
'remote_dependencies'
|
|
22
|
-
else
|
|
23
|
-
"#{key}s"
|
|
24
|
-
end
|
|
25
|
-
end
|
|
9
|
+
module Simple
|
|
10
|
+
module_function
|
|
26
11
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
elsif key == 'remote_dependencies'
|
|
32
|
-
'remote_dependency'
|
|
33
|
-
elsif key.end_with?('__all')
|
|
34
|
-
key[0...-5] # Strip off __all
|
|
35
|
-
elsif key.end_with?('s')
|
|
36
|
-
key.chomp('s')
|
|
37
|
-
else
|
|
38
|
-
key
|
|
12
|
+
def flatten(sequence)
|
|
13
|
+
sequence.flat_map do |each|
|
|
14
|
+
each.is_a?(Array) ? each : [each]
|
|
15
|
+
end
|
|
39
16
|
end
|
|
40
|
-
end
|
|
41
17
|
|
|
42
|
-
|
|
43
|
-
|
|
18
|
+
class DictVisitor
|
|
19
|
+
include Tree::Visitor
|
|
44
20
|
|
|
45
|
-
|
|
46
|
-
@depth = -1
|
|
47
|
-
end
|
|
21
|
+
attr_accessor :depth
|
|
48
22
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
23
|
+
def initialize
|
|
24
|
+
@depth = -1
|
|
25
|
+
@logger = Logger.new($stderr)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def update_tree(target, update)
|
|
29
|
+
keys = update.keys
|
|
30
|
+
raise KeyError, "Dictionary to update with cannot have multiple keys." if keys.length != 1
|
|
52
31
|
|
|
53
|
-
|
|
32
|
+
key = keys.first
|
|
33
|
+
val = update[key]
|
|
54
34
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
35
|
+
if Keys::PLURAL_KEYS.include?(key)
|
|
36
|
+
plural_key = Keys.pluralize(key)
|
|
37
|
+
if target.key?(plural_key)
|
|
38
|
+
target[plural_key] << val
|
|
39
|
+
else
|
|
40
|
+
target[plural_key] = [val]
|
|
41
|
+
end
|
|
42
|
+
elsif target.key?(key)
|
|
43
|
+
if @depth.zero?
|
|
44
|
+
@logger.warn(
|
|
45
|
+
format(
|
|
46
|
+
'Multiple declarations of top-level key "%s" found. Using the last-declared value.',
|
|
47
|
+
key
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
target[key] = val
|
|
51
|
+
else
|
|
52
|
+
raise KeyError,
|
|
53
|
+
"Key \"#{key}\" already exists in tree " \
|
|
54
|
+
"and would overwrite the existing value."
|
|
55
|
+
end
|
|
59
56
|
else
|
|
60
|
-
target[
|
|
61
|
-
end
|
|
62
|
-
elsif target.key?(key)
|
|
63
|
-
unless @depth.zero?
|
|
64
|
-
raise KeyError, "Key \"#{key}\" already exists in tree and would overwrite the existing value."
|
|
57
|
+
target[key] = val
|
|
65
58
|
end
|
|
59
|
+
end
|
|
66
60
|
|
|
67
|
-
|
|
68
|
-
|
|
61
|
+
def visit(document)
|
|
62
|
+
visit_container(document.container)
|
|
63
|
+
end
|
|
69
64
|
|
|
70
|
-
|
|
71
|
-
|
|
65
|
+
def visit_container(node)
|
|
66
|
+
container = {}
|
|
67
|
+
unless node.items.empty?
|
|
68
|
+
@depth += 1
|
|
69
|
+
node.items.each do |item|
|
|
70
|
+
update_tree(container, item.accept(self))
|
|
71
|
+
end
|
|
72
|
+
@depth -= 1
|
|
73
|
+
end
|
|
74
|
+
container
|
|
72
75
|
end
|
|
73
|
-
end
|
|
74
76
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
def visit_block(node)
|
|
78
|
+
container_dict = node.container ? node.container.accept(self) : {}
|
|
79
|
+
container_dict = container_dict.dup
|
|
80
|
+
container_dict["name"] = node.name.accept(self) if node.name
|
|
81
|
+
{ node.type.accept(self) => container_dict }
|
|
82
|
+
end
|
|
78
83
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if node.items.any?
|
|
82
|
-
@depth += 1
|
|
83
|
-
node.items.each do |item|
|
|
84
|
-
update_tree(container, item.accept(self))
|
|
85
|
-
end
|
|
86
|
-
@depth -= 1
|
|
84
|
+
def visit_list(node)
|
|
85
|
+
{ node.type.accept(self) => node.items.map { |item| item.accept(self) } }
|
|
87
86
|
end
|
|
88
|
-
container
|
|
89
|
-
end
|
|
90
87
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
{ node.type.accept(self) => container_dict }
|
|
95
|
-
end
|
|
88
|
+
def visit_pair(node)
|
|
89
|
+
{ node.type.accept(self) => node.value.accept(self) }
|
|
90
|
+
end
|
|
96
91
|
|
|
97
|
-
|
|
98
|
-
|
|
92
|
+
def visit_token(token)
|
|
93
|
+
token.value.to_s
|
|
94
|
+
end
|
|
99
95
|
end
|
|
100
96
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
end
|
|
97
|
+
class DictParser
|
|
98
|
+
attr_accessor :parent_key, :level, :base_indent, :latest_node
|
|
104
99
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
100
|
+
def initialize
|
|
101
|
+
@parent_key = nil
|
|
102
|
+
@level = 0
|
|
103
|
+
@base_indent = " " * 2
|
|
104
|
+
@latest_node = Tree::DocumentNode
|
|
105
|
+
end
|
|
109
106
|
|
|
110
|
-
|
|
111
|
-
|
|
107
|
+
def increase_level
|
|
108
|
+
@latest_node = nil
|
|
109
|
+
@level += 1
|
|
110
|
+
end
|
|
112
111
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
@base_indent = ' ' * 2
|
|
117
|
-
@latest_node = DocumentNode
|
|
118
|
-
end
|
|
112
|
+
def decrease_level
|
|
113
|
+
@level -= 1
|
|
114
|
+
end
|
|
119
115
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
end
|
|
116
|
+
def indent
|
|
117
|
+
@level.positive? ? @base_indent * @level : ""
|
|
118
|
+
end
|
|
124
119
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
120
|
+
def newline_indent
|
|
121
|
+
"\n#{indent}"
|
|
122
|
+
end
|
|
128
123
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
124
|
+
def prefix
|
|
125
|
+
if @latest_node == Tree::DocumentNode
|
|
126
|
+
""
|
|
127
|
+
elsif @latest_node == Tree::BlockNode
|
|
128
|
+
"\n#{newline_indent}"
|
|
129
|
+
else
|
|
130
|
+
newline_indent
|
|
131
|
+
end
|
|
132
|
+
end
|
|
132
133
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
134
|
+
def plural_key?(key)
|
|
135
|
+
singular_key = Keys.singularize(key)
|
|
136
|
+
return false unless Keys::PLURAL_KEYS.include?(singular_key)
|
|
136
137
|
|
|
137
|
-
|
|
138
|
-
return '' if @latest_node == DocumentNode
|
|
139
|
-
return newline_indent if @latest_node.nil?
|
|
140
|
-
return "\n#{newline_indent}" if @latest_node == BlockNode
|
|
138
|
+
return false if singular_key == "allowed_value" && @parent_key&.sub(/s+\z/, "") == "access_grant"
|
|
141
139
|
|
|
142
|
-
|
|
143
|
-
end
|
|
140
|
+
return false if @parent_key == "query" && singular_key != "filters"
|
|
144
141
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
PLURAL_KEYS.include?(singular_key) &&
|
|
148
|
-
!(singular_key == 'allowed_value' && @parent_key.rstrip == 'access_grant') &&
|
|
149
|
-
!(@parent_key == 'query' && singular_key != 'filters')
|
|
150
|
-
end
|
|
142
|
+
true
|
|
143
|
+
end
|
|
151
144
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
145
|
+
def resolve_filters(values)
|
|
146
|
+
first = values[0]
|
|
147
|
+
if first.key?("name")
|
|
148
|
+
values.map do |value|
|
|
149
|
+
value = value.dup
|
|
150
|
+
name = value.delete("name")
|
|
151
|
+
parse_block("filter", value, name)
|
|
152
|
+
end
|
|
153
|
+
elsif first.key?("field") && first.key?("value")
|
|
154
|
+
values.map { |value| parse_block("filters", value.dup, nil) }
|
|
155
|
+
else
|
|
156
|
+
parse_list("filters", values)
|
|
157
157
|
end
|
|
158
|
-
elsif values.first.key?('field') && values.first.key?('value')
|
|
159
|
-
values.map { |value| parse_block('filters', value) }
|
|
160
|
-
else
|
|
161
|
-
parse_list('filters', values)
|
|
162
158
|
end
|
|
163
|
-
end
|
|
164
159
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
def expand_list(key, values)
|
|
172
|
-
if key == 'filters'
|
|
173
|
-
resolve_filters(values)
|
|
174
|
-
else
|
|
175
|
-
singular_key = ::Lkml.singularize(key)
|
|
176
|
-
values.map { |value| parse_any(singular_key, value) }.flatten
|
|
160
|
+
def parse(obj)
|
|
161
|
+
nodes = obj.map { |key, value| parse_any(key.to_s, value) }
|
|
162
|
+
container = Tree::ContainerNode.new(items: Simple.flatten(nodes).freeze)
|
|
163
|
+
Tree::DocumentNode.new(container)
|
|
177
164
|
end
|
|
178
|
-
end
|
|
179
165
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
parse_pair(key, value)
|
|
184
|
-
when Array
|
|
185
|
-
if plural_key?(key)
|
|
186
|
-
expand_list(key, value)
|
|
166
|
+
def expand_list(key, values)
|
|
167
|
+
if key == "filters"
|
|
168
|
+
Simple.flatten([resolve_filters(values)])
|
|
187
169
|
else
|
|
188
|
-
|
|
170
|
+
singular_key = Keys.singularize(key)
|
|
171
|
+
Simple.flatten(values.map { |v| parse_any(singular_key, v) })
|
|
189
172
|
end
|
|
190
|
-
when Hash
|
|
191
|
-
to_parse = value.dup
|
|
192
|
-
name = if KEYS_WITH_NAME_FIELDS.include?(key) || !value.key?('name')
|
|
193
|
-
nil
|
|
194
|
-
else
|
|
195
|
-
to_parse.delete('name')
|
|
196
|
-
end
|
|
197
|
-
parse_block(key, to_parse, name:)
|
|
198
|
-
else
|
|
199
|
-
raise TypeError, 'Value must be a string, list, tuple, or dict.'
|
|
200
173
|
end
|
|
201
|
-
end
|
|
202
174
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
name: name ? SyntaxToken.new(name) : nil,
|
|
226
|
-
container: container
|
|
227
|
-
).tap { @latest_node = BlockNode }
|
|
228
|
-
end
|
|
175
|
+
def parse_any(key, value)
|
|
176
|
+
case value
|
|
177
|
+
when String
|
|
178
|
+
parse_pair(key, value)
|
|
179
|
+
when Array
|
|
180
|
+
if plural_key?(key)
|
|
181
|
+
expand_list(key, value)
|
|
182
|
+
else
|
|
183
|
+
parse_list(key, value)
|
|
184
|
+
end
|
|
185
|
+
when Hash
|
|
186
|
+
h = value.transform_keys(&:to_s)
|
|
187
|
+
name = if Keys::KEYS_WITH_NAME_FIELDS.include?(key) || !h.key?("name")
|
|
188
|
+
nil
|
|
189
|
+
else
|
|
190
|
+
h.delete("name")
|
|
191
|
+
end
|
|
192
|
+
parse_block(key, h, name)
|
|
193
|
+
else
|
|
194
|
+
raise TypeError, "Value must be a string, list, tuple, or dict."
|
|
195
|
+
end
|
|
196
|
+
end
|
|
229
197
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
198
|
+
def parse_block(key, items, name = nil)
|
|
199
|
+
prev_parent_key = @parent_key
|
|
200
|
+
@parent_key = key
|
|
201
|
+
latest_node_at_level = @latest_node
|
|
202
|
+
increase_level
|
|
203
|
+
child_nodes = items.map { |k, v| parse_any(k.to_s, v) }
|
|
204
|
+
decrease_level
|
|
205
|
+
@latest_node = latest_node_at_level
|
|
206
|
+
@parent_key = prev_parent_key
|
|
207
|
+
|
|
208
|
+
container = Tree::ContainerNode.new(items: Simple.flatten(child_nodes).freeze)
|
|
209
|
+
|
|
210
|
+
block_prefix = if @latest_node && @latest_node != Tree::DocumentNode
|
|
211
|
+
"\n#{newline_indent}"
|
|
212
|
+
else
|
|
213
|
+
prefix
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
node = Tree::BlockNode.new(
|
|
217
|
+
type: Tree::SyntaxToken.new(key.to_s, nil, block_prefix),
|
|
218
|
+
left_brace: Tree::LeftCurlyBrace.new("{", nil, (name ? " " : ""), ""),
|
|
219
|
+
right_brace: Tree::RightCurlyBrace.new(
|
|
220
|
+
"}", nil, (container.items.empty? ? "" : newline_indent), ""
|
|
221
|
+
),
|
|
222
|
+
name: (name ? Tree::SyntaxToken.new(name.to_s) : nil),
|
|
223
|
+
container: container
|
|
224
|
+
)
|
|
225
|
+
@latest_node = Tree::BlockNode
|
|
226
|
+
node
|
|
227
|
+
end
|
|
234
228
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
229
|
+
def parse_list(key, values)
|
|
230
|
+
force_quote = key == "suggestions"
|
|
231
|
+
prev_parent_key = @parent_key
|
|
232
|
+
@parent_key = key
|
|
239
233
|
|
|
240
|
-
|
|
234
|
+
type_token = Tree::SyntaxToken.new(key.to_s, nil, prefix)
|
|
235
|
+
pair_mode = values[0] && !values[0].is_a?(String) && !values[0].is_a?(Integer)
|
|
241
236
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
237
|
+
items = []
|
|
238
|
+
if pair_mode || values.length >= 5
|
|
239
|
+
trailing_comma = Tree::Comma.new(",", nil, "", "")
|
|
240
|
+
increase_level
|
|
246
241
|
if pair_mode
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
242
|
+
values.each do |h|
|
|
243
|
+
h = h.transform_keys(&:to_s)
|
|
244
|
+
raise ArgumentError, "Expected single-key hash in list pair" unless h.size == 1
|
|
245
|
+
|
|
246
|
+
k, val = h.first
|
|
247
|
+
items << parse_pair(k, val)
|
|
248
|
+
end
|
|
250
249
|
else
|
|
251
|
-
|
|
250
|
+
values.each do |value|
|
|
251
|
+
items << DictParser.parse_token(key, value.to_s, force_quote, prefix: newline_indent)
|
|
252
|
+
end
|
|
252
253
|
end
|
|
254
|
+
decrease_level
|
|
255
|
+
right_bracket = Tree::RightBracket.new("]", nil, newline_indent, "")
|
|
256
|
+
node = Tree::ListNode.new(
|
|
257
|
+
type: type_token,
|
|
258
|
+
left_bracket: Tree::LeftBracket.new("[", nil, "", ""),
|
|
259
|
+
items: items.freeze,
|
|
260
|
+
right_bracket: right_bracket,
|
|
261
|
+
trailing_comma: trailing_comma
|
|
262
|
+
)
|
|
263
|
+
else
|
|
264
|
+
values.each_with_index do |value, i|
|
|
265
|
+
pref = i.zero? ? "" : " "
|
|
266
|
+
items << DictParser.parse_token(key, value.to_s, force_quote, prefix: pref)
|
|
267
|
+
end
|
|
268
|
+
node = Tree::ListNode.new(
|
|
269
|
+
type: type_token,
|
|
270
|
+
left_bracket: Tree::LeftBracket.new("[", nil, "", ""),
|
|
271
|
+
items: items.freeze,
|
|
272
|
+
right_bracket: Tree::RightBracket.new("]", nil, "", "")
|
|
273
|
+
)
|
|
253
274
|
end
|
|
254
|
-
decrease_level
|
|
255
|
-
right_bracket = RightBracket.new(prefix: newline_indent)
|
|
256
|
-
else
|
|
257
|
-
values.each_with_index do |value, i|
|
|
258
|
-
token = if i.zero?
|
|
259
|
-
parse_token(key, value, force_quote:)
|
|
260
|
-
else
|
|
261
|
-
parse_token(key, value, force_quote:, prefix: ' ')
|
|
262
|
-
end
|
|
263
|
-
items << token
|
|
264
|
-
end
|
|
265
|
-
end
|
|
266
|
-
|
|
267
|
-
@parent_key = prev_parent_key
|
|
268
275
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
right_bracket: right_bracket,
|
|
274
|
-
trailing_comma: pair_mode || values.size >= 5 ? Comma.new : nil
|
|
275
|
-
).tap { @latest_node = ListNode }
|
|
276
|
-
end
|
|
276
|
+
@parent_key = prev_parent_key
|
|
277
|
+
@latest_node = Tree::ListNode
|
|
278
|
+
node
|
|
279
|
+
end
|
|
277
280
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
281
|
+
def parse_pair(key, value)
|
|
282
|
+
force_quote = @parent_key == "filters" && key != "field"
|
|
283
|
+
value_syntax_token = DictParser.parse_token(key.to_s, value.to_s, force_quote)
|
|
284
|
+
node = Tree::PairNode.new(
|
|
285
|
+
type: Tree::SyntaxToken.new(key.to_s, nil, prefix),
|
|
286
|
+
value: value_syntax_token
|
|
287
|
+
)
|
|
288
|
+
@latest_node = Tree::PairNode
|
|
289
|
+
node
|
|
290
|
+
end
|
|
286
291
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
292
|
+
def self.parse_token(key, value, force_quote, prefix: "", suffix: "")
|
|
293
|
+
if force_quote || Keys::QUOTED_LITERAL_KEYS.include?(key)
|
|
294
|
+
Tree::QuotedSyntaxToken.new(value, nil, prefix, suffix)
|
|
295
|
+
elsif Keys::EXPR_BLOCK_KEYS.include?(key)
|
|
296
|
+
Tree::ExpressionSyntaxToken.new(value.strip, nil, prefix, suffix)
|
|
297
|
+
else
|
|
298
|
+
Tree::SyntaxToken.new(value, nil, prefix, suffix)
|
|
299
|
+
end
|
|
294
300
|
end
|
|
295
301
|
end
|
|
296
302
|
end
|