lkml 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,297 @@
1
+ # frozen_string_literal: true
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.
8
+
9
+ require_relative 'keys'
10
+ require_relative 'tree'
11
+
12
+ 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
26
+
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
39
+ end
40
+ end
41
+
42
+ class DictVisitor
43
+ attr_accessor :depth
44
+
45
+ def initialize
46
+ @depth = -1
47
+ end
48
+
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
52
+
53
+ key = keys.first
54
+
55
+ if PLURAL_KEYS.include?(key)
56
+ plural_key = ::Lkml.pluralize(key)
57
+ if target.key?(plural_key)
58
+ target[plural_key] << update[key]
59
+ 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."
65
+ end
66
+
67
+ $stderr << "Multiple declarations of top-level key \"#{key}\" found. Using the last-declared value.\n"
68
+ target[key] = update[key]
69
+
70
+ else
71
+ target[key] = update[key]
72
+ end
73
+ end
74
+
75
+ def visit(document)
76
+ visit_container(document.container)
77
+ end
78
+
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
87
+ end
88
+ container
89
+ end
90
+
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
96
+
97
+ def visit_list(node)
98
+ { node.type.accept(self) => node.items.map { |item| item.accept(self) } }
99
+ end
100
+
101
+ def visit_pair(node)
102
+ { node.type.accept(self) => node.value.accept(self) }
103
+ end
104
+
105
+ def visit_token(token)
106
+ token.value.to_s
107
+ end
108
+ end
109
+
110
+ class DictParser
111
+ attr_accessor :parent_key, :level, :base_indent, :latest_node
112
+
113
+ def initialize
114
+ @parent_key = nil
115
+ @level = 0
116
+ @base_indent = ' ' * 2
117
+ @latest_node = DocumentNode
118
+ end
119
+
120
+ def increase_level
121
+ @latest_node = nil
122
+ @level += 1
123
+ end
124
+
125
+ def decrease_level
126
+ @level -= 1
127
+ end
128
+
129
+ def indent
130
+ @level.positive? ? @base_indent * @level : ''
131
+ end
132
+
133
+ def newline_indent
134
+ "\n#{indent}"
135
+ end
136
+
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
141
+
142
+ newline_indent
143
+ end
144
+
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
151
+
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:)
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
+ end
163
+ end
164
+
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
177
+ end
178
+ end
179
+
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)
187
+ else
188
+ parse_list(key, value)
189
+ 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
+ end
201
+ end
202
+
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
229
+
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
234
+
235
+ type_token = SyntaxToken.new(key, prefix: prefix)
236
+ right_bracket = RightBracket.new
237
+ items = []
238
+ pair_mode = false
239
+
240
+ pair_mode = true if values.any? && !values.first.is_a?(String)
241
+
242
+ if values.size >= 5 || pair_mode
243
+ Comma.new
244
+ increase_level
245
+ values.each do |value|
246
+ 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)
250
+ else
251
+ items << parse_token(key, value, force_quote:, prefix: newline_indent)
252
+ end
253
+ 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
+
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
277
+
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
286
+
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)
294
+ end
295
+ end
296
+ end
297
+ end
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Tokens used by the lexer to tokenize LookML.
4
+
5
+ module Lkml
6
+ module Tokens
7
+ class Token
8
+ # Base class for LookML tokens, lexed from LookML strings.
9
+
10
+ attr_reader :line_number
11
+
12
+ def initialize(line_number)
13
+ # Initializes a Token.
14
+ #
15
+ # Args:
16
+ # line_number: The corresponding line in the text where this token begins
17
+
18
+ @line_number = line_number
19
+ end
20
+
21
+ def ==(other)
22
+ # Compare one Token to another by their type.
23
+ self.class == other.class
24
+ end
25
+
26
+ def to_s
27
+ # Returns the token's string representation, truncated to 25 characters.
28
+ #
29
+ # If the token has a `value` attribute, include that in the output.
30
+
31
+ value = defined?(@value) ? @value.strip : ''
32
+ value = "#{value[0, 25].rstrip} ... " if value.length > 25
33
+ "#{self.class.name}(#{value})"
34
+ end
35
+ end
36
+
37
+ class ContentToken < Token
38
+ # Base class for LookML tokens that contain a string of content.
39
+
40
+ attr_accessor :value
41
+
42
+ def initialize(value, line_number)
43
+ # Initializes a ContentToken with string content.
44
+ #
45
+ # Args:
46
+ # value: A string value for the token's content
47
+ # line_number: The corresponding line in the text where this token begins
48
+
49
+ @value = value
50
+ super(line_number)
51
+ end
52
+
53
+ def ==(other)
54
+ # Compare one ContentToken to another by their values.
55
+ self.class == other.class && @value == other.value
56
+ end
57
+ end
58
+
59
+ class StreamStartToken < Token
60
+ # Represents the start of a stream of characters.
61
+
62
+ def initialize(line_number)
63
+ super
64
+ @id = '<stream start>'
65
+ end
66
+ end
67
+
68
+ class StreamEndToken < Token
69
+ # Represents the end of a stream of characters.
70
+
71
+ def initialize(line_number)
72
+ super
73
+ @id = '<stream end>'
74
+ end
75
+ end
76
+
77
+ class BlockStartToken < Token
78
+ # Represents the start of a block.
79
+
80
+ def initialize(line_number)
81
+ super
82
+ @id = '{'
83
+ end
84
+ end
85
+
86
+ class BlockEndToken < Token
87
+ # Represents the end of a block.
88
+
89
+ def initialize(line_number)
90
+ super
91
+ @id = '}'
92
+ end
93
+ end
94
+
95
+ class ValueToken < Token
96
+ # Separates a key from a value.
97
+
98
+ def initialize(line_number)
99
+ super
100
+ @id = ':'
101
+ end
102
+ end
103
+
104
+ class ExpressionBlockEndToken < Token
105
+ # Represents the end of an expression block.
106
+
107
+ def initialize(line_number)
108
+ super
109
+ @id = ';;'
110
+ end
111
+ end
112
+
113
+ class CommaToken < Token
114
+ # Separates elements in a list.
115
+
116
+ def initialize(line_number)
117
+ super
118
+ @id = ','
119
+ end
120
+ end
121
+
122
+ class ListStartToken < Token
123
+ # Represents the start of a list.
124
+
125
+ def initialize(line_number)
126
+ super
127
+ @id = '['
128
+ end
129
+ end
130
+
131
+ class ListEndToken < Token
132
+ # Represents the end of a list.
133
+
134
+ def initialize(line_number)
135
+ super
136
+ @id = ']'
137
+ end
138
+ end
139
+
140
+ class TriviaToken < ContentToken
141
+ # Represents a comment or whitespace.
142
+ end
143
+
144
+ class WhitespaceToken < TriviaToken
145
+ # Represents one or more whitespace characters.
146
+
147
+ def initialize(value, line_number)
148
+ super
149
+ @id = '<whitespace>'
150
+ end
151
+
152
+ def to_s
153
+ "#{self.class.name}(#{@value.inspect})"
154
+ end
155
+ end
156
+
157
+ class LinebreakToken < WhitespaceToken
158
+ # Represents a newline character.
159
+
160
+ def initialize(value, line_number)
161
+ super
162
+ @id = '<linebreak>'
163
+ end
164
+ end
165
+
166
+ class InlineWhitespaceToken < WhitespaceToken
167
+ # Represents one or more whitespace characters.
168
+
169
+ def initialize(value, line_number)
170
+ super
171
+ @id = '<inline whitespace>'
172
+ end
173
+ end
174
+
175
+ class CommentToken < TriviaToken
176
+ # Represents a comment.
177
+
178
+ def initialize(value, line_number)
179
+ super
180
+ @id = '<comment>'
181
+ end
182
+ end
183
+
184
+ class ExpressionBlockToken < ContentToken
185
+ # Contains the value of an expression block.
186
+
187
+ def initialize(value, line_number)
188
+ super
189
+ @id = '<expression block>'
190
+ end
191
+ end
192
+
193
+ class LiteralToken < ContentToken
194
+ # Contains the value of an unquoted literal.
195
+
196
+ def initialize(value, line_number)
197
+ super
198
+ @id = '<literal>'
199
+ end
200
+ end
201
+
202
+ class QuotedLiteralToken < ContentToken
203
+ # Contains the value of a quoted literal.
204
+
205
+ def initialize(value, line_number)
206
+ super
207
+ @id = '<quoted literal>'
208
+ end
209
+ end
210
+ end
211
+ end