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/simple.rb CHANGED
@@ -1,296 +1,302 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Interface classes between the parse tree and a data structure of primitives.
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 'keys'
10
- require_relative 'tree'
5
+ require_relative "keys"
6
+ require_relative "tree"
11
7
 
12
8
  module Lkml
13
- def self.pluralize(key)
14
- # Converts a singular key like "explore" to a plural key, e.g. 'explores'.
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
- def self.singularize(key)
28
- # Converts a plural key like "explores" to a singular key, e.g. 'explore'.
29
- if key == 'queries'
30
- 'query'
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
- class DictVisitor
43
- attr_accessor :depth
18
+ class DictVisitor
19
+ include Tree::Visitor
44
20
 
45
- def initialize
46
- @depth = -1
47
- end
21
+ attr_accessor :depth
48
22
 
49
- def update_tree(target, update)
50
- keys = update.keys
51
- raise KeyError, 'Dictionary to update with cannot have multiple keys.' if keys.size > 1
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
- key = keys.first
32
+ key = keys.first
33
+ val = update[key]
54
34
 
55
- if PLURAL_KEYS.include?(key)
56
- plural_key = ::Lkml.pluralize(key)
57
- if target.key?(plural_key)
58
- target[plural_key] << update[key]
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[plural_key] = [update[key]]
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
- $stderr << "Multiple declarations of top-level key \"#{key}\" found. Using the last-declared value.\n"
68
- target[key] = update[key]
61
+ def visit(document)
62
+ visit_container(document.container)
63
+ end
69
64
 
70
- else
71
- target[key] = update[key]
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
- def visit(document)
76
- visit_container(document.container)
77
- end
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
- def visit_container(node)
80
- container = {}
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
- def visit_block(node)
92
- container_dict = node.container ? node.container.accept(self) : {}
93
- container_dict['name'] = node.name.accept(self) if node.name
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
- def visit_list(node)
98
- { node.type.accept(self) => node.items.map { |item| item.accept(self) } }
92
+ def visit_token(token)
93
+ token.value.to_s
94
+ end
99
95
  end
100
96
 
101
- def visit_pair(node)
102
- { node.type.accept(self) => node.value.accept(self) }
103
- end
97
+ class DictParser
98
+ attr_accessor :parent_key, :level, :base_indent, :latest_node
104
99
 
105
- def visit_token(token)
106
- token.value.to_s
107
- end
108
- end
100
+ def initialize
101
+ @parent_key = nil
102
+ @level = 0
103
+ @base_indent = " " * 2
104
+ @latest_node = Tree::DocumentNode
105
+ end
109
106
 
110
- class DictParser
111
- attr_accessor :parent_key, :level, :base_indent, :latest_node
107
+ def increase_level
108
+ @latest_node = nil
109
+ @level += 1
110
+ end
112
111
 
113
- def initialize
114
- @parent_key = nil
115
- @level = 0
116
- @base_indent = ' ' * 2
117
- @latest_node = DocumentNode
118
- end
112
+ def decrease_level
113
+ @level -= 1
114
+ end
119
115
 
120
- def increase_level
121
- @latest_node = nil
122
- @level += 1
123
- end
116
+ def indent
117
+ @level.positive? ? @base_indent * @level : ""
118
+ end
124
119
 
125
- def decrease_level
126
- @level -= 1
127
- end
120
+ def newline_indent
121
+ "\n#{indent}"
122
+ end
128
123
 
129
- def indent
130
- @level.positive? ? @base_indent * @level : ''
131
- end
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
- def newline_indent
134
- "\n#{indent}"
135
- end
134
+ def plural_key?(key)
135
+ singular_key = Keys.singularize(key)
136
+ return false unless Keys::PLURAL_KEYS.include?(singular_key)
136
137
 
137
- def prefix
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
- newline_indent
143
- end
140
+ return false if @parent_key == "query" && singular_key != "filters"
144
141
 
145
- def plural_key?(key)
146
- singular_key = ::Lkml.singularize(key)
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
- def resolve_filters(values)
153
- if values.first.key?('name')
154
- values.map do |value|
155
- name = value.delete('name')
156
- parse_block('filter', value, name:)
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
- def parse(obj)
166
- nodes = obj.map { |key, value| parse_any(key, value) }
167
- container = ContainerNode.new(nodes.flatten)
168
- DocumentNode.new(container)
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
- def parse_any(key, value)
181
- case value
182
- when String
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
- parse_list(key, value)
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
- def parse_block(key, items, name: nil)
204
- prev_parent_key = @parent_key
205
- @parent_key = key
206
- latest_node_at_this_level = @latest_node
207
- increase_level
208
- nodes = items.map { |k, v| parse_any(k, v) }
209
- decrease_level
210
- @latest_node = latest_node_at_this_level
211
- @parent_key = prev_parent_key
212
-
213
- container = ContainerNode.new(nodes.flatten)
214
-
215
- prefix = if @latest_node && @latest_node != DocumentNode
216
- "\n#{newline_indent}"
217
- else
218
- self.prefix
219
- end
220
-
221
- BlockNode.new(
222
- SyntaxToken.new(key, prefix: prefix),
223
- left_brace: LeftCurlyBrace.new(prefix: name ? ' ' : ''),
224
- right_brace: RightCurlyBrace.new(prefix: container.items.any? ? newline_indent : ''),
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
- def parse_list(key, values) # rubocop:disable Metrics/CyclomaticComplexity
231
- force_quote = key == 'suggestions'
232
- prev_parent_key = @parent_key
233
- @parent_key = key
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
- type_token = SyntaxToken.new(key, prefix: prefix)
236
- right_bracket = RightBracket.new
237
- items = []
238
- pair_mode = false
229
+ def parse_list(key, values)
230
+ force_quote = key == "suggestions"
231
+ prev_parent_key = @parent_key
232
+ @parent_key = key
239
233
 
240
- pair_mode = true if values.any? && !values.first.is_a?(String)
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
- if values.size >= 5 || pair_mode
243
- Comma.new
244
- increase_level
245
- values.each do |value|
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
- # Extract key and value from dictionary with only one key
248
- key, val = value.to_a.first
249
- items << parse_pair(key, val)
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
- items << parse_token(key, value, force_quote:, prefix: newline_indent)
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
- ListNode.new(
270
- type_token,
271
- left_bracket: LeftBracket.new,
272
- items: items,
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
- def parse_pair(key, value)
279
- force_quote = @parent_key == 'filters' && key != 'field'
280
- value_syntax_token = parse_token(key, value, force_quote:)
281
- PairNode.new(
282
- SyntaxToken.new(key, prefix: prefix),
283
- value_syntax_token
284
- ).tap { @latest_node = PairNode }
285
- end
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
- def parse_token(key, value, force_quote: false, prefix: '', suffix: '')
288
- if force_quote || QUOTED_LITERAL_KEYS.include?(key)
289
- QuotedSyntaxToken.new(value, prefix: prefix, suffix: suffix)
290
- elsif EXPR_BLOCK_KEYS.include?(key)
291
- ExpressionSyntaxToken.new(value.strip, prefix: prefix, suffix: suffix)
292
- else
293
- SyntaxToken.new(value, prefix: prefix, suffix: suffix)
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