hcl-rb 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/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/hcl/ast_visitor.rb +28 -0
- data/lib/hcl/decoder.rb +469 -0
- data/lib/hcl/generator.rb +26 -0
- data/lib/hcl/monkey_patch.rb +91 -0
- data/lib/hcl/parser.rb +17 -0
- data/lib/hcl/parslet.rb +162 -0
- data/lib/hcl/version.rb +3 -0
- data/lib/hcl.rb +16 -0
- metadata +98 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 5112fcbbb036a494cf40ca905f073e0390d49d9c2bc6a09631b6f565cc42b1a1
|
4
|
+
data.tar.gz: 04bca627bf636706076f49c19b6bfbf8a2146b690c2eed1e76f6ee8d8be6d917
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: bf9fd4abdf12d5467a38079d41b4c002bd01cc00d9f1f264d79d7237649f67179b58a352d98efe82037262dfbd4e4a17d463c62843dda0e90cc0f0434e9bbf1c
|
7
|
+
data.tar.gz: c4a391a9ef43ab2c9f18fbd1aaa5816162e2640e263b7e2418e7b5196b77f39fbf8eb71d49aaa833eea37a982d3f60da9d0f4853203cced9dd89e2f666a4a834
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "hcl"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
class HCL::ASTVisitor
|
2
|
+
@@types = [
|
3
|
+
:document,
|
4
|
+
:kv_key,
|
5
|
+
:string,
|
6
|
+
:key_string,
|
7
|
+
:object,
|
8
|
+
:comment,
|
9
|
+
:integer,
|
10
|
+
:boolean,
|
11
|
+
:float,
|
12
|
+
:heredoc,
|
13
|
+
:list,
|
14
|
+
:object
|
15
|
+
]
|
16
|
+
|
17
|
+
def visit(ast)
|
18
|
+
return nil unless ast
|
19
|
+
|
20
|
+
raise "AST object must be Hash" unless Hash === ast
|
21
|
+
|
22
|
+
type = @@types.find { |type| ast.key? type }
|
23
|
+
raise "Couldn't determine AST object type" unless type
|
24
|
+
|
25
|
+
method_name = "visit_#{type}"
|
26
|
+
send(method_name, ast)
|
27
|
+
end
|
28
|
+
end
|
data/lib/hcl/decoder.rb
ADDED
@@ -0,0 +1,469 @@
|
|
1
|
+
class HCL::Decoder < HCL::ASTVisitor
|
2
|
+
def initialize
|
3
|
+
end
|
4
|
+
|
5
|
+
def decode(ast)
|
6
|
+
visit(ast)
|
7
|
+
end
|
8
|
+
|
9
|
+
def visit_document(ast)
|
10
|
+
doc = ast[:document]
|
11
|
+
if Array === doc
|
12
|
+
visit({object: doc})
|
13
|
+
else
|
14
|
+
doc
|
15
|
+
end || {}
|
16
|
+
end
|
17
|
+
|
18
|
+
def conv_key(key)
|
19
|
+
if Hash === key
|
20
|
+
if key.key? :string
|
21
|
+
key[:string]
|
22
|
+
elsif Hash === key[:key]
|
23
|
+
key[:key][:string]
|
24
|
+
else
|
25
|
+
key[:key]
|
26
|
+
end
|
27
|
+
else
|
28
|
+
key
|
29
|
+
end.to_s
|
30
|
+
end
|
31
|
+
|
32
|
+
def visit_kv_key(ast)
|
33
|
+
key = conv_key(ast[:kv_key])
|
34
|
+
value = visit(ast[:value])
|
35
|
+
|
36
|
+
extra_keys = ast[:keys]
|
37
|
+
if extra_keys.nil? || extra_keys == []
|
38
|
+
{ key => value }
|
39
|
+
else
|
40
|
+
rest = extra_keys.reverse.inject(value) do |h, k|
|
41
|
+
{ conv_key(k) => h }
|
42
|
+
end
|
43
|
+
{ key => rest }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def newline?(b)
|
48
|
+
b == "\n".ord || b == "\r".ord
|
49
|
+
end
|
50
|
+
|
51
|
+
SIMPLE_ESCAPES = [
|
52
|
+
["a", "\a"],
|
53
|
+
["b", "\b"],
|
54
|
+
["f", "\f"],
|
55
|
+
["n", "\n"],
|
56
|
+
["r", "\r"],
|
57
|
+
["t", "\t"],
|
58
|
+
["v", "\v"],
|
59
|
+
["\\", "\\"],
|
60
|
+
["'", "'"],
|
61
|
+
["\"", "\""]
|
62
|
+
].map { |a, b| [a.ord, b] }.to_h
|
63
|
+
|
64
|
+
def undump(str)
|
65
|
+
chunks = nil
|
66
|
+
chunk_start = 0
|
67
|
+
braces = 0
|
68
|
+
dollar = false
|
69
|
+
hil = false
|
70
|
+
bytes = str.bytes
|
71
|
+
|
72
|
+
io = StringIO.new(str)
|
73
|
+
b = io.getbyte
|
74
|
+
|
75
|
+
until b.nil?
|
76
|
+
if braces == 0 and dollar and b == "{".ord
|
77
|
+
braces += 1
|
78
|
+
hil = true
|
79
|
+
elsif braces > 0 and b == "{".ord
|
80
|
+
braces += 1
|
81
|
+
end
|
82
|
+
|
83
|
+
if braces > 0 and b == "}".ord
|
84
|
+
braces -= 1
|
85
|
+
end
|
86
|
+
|
87
|
+
dollar = false
|
88
|
+
if braces == 0 and b == "$".ord
|
89
|
+
dollar = true
|
90
|
+
end
|
91
|
+
|
92
|
+
if b == "\\".ord
|
93
|
+
chunks = [] if chunks.nil?
|
94
|
+
|
95
|
+
if chunk_start != io.pos-1
|
96
|
+
chunks << str[chunk_start..io.pos-2]
|
97
|
+
end
|
98
|
+
|
99
|
+
s = nil
|
100
|
+
|
101
|
+
b = io.getbyte
|
102
|
+
|
103
|
+
escape = SIMPLE_ESCAPES[b]
|
104
|
+
|
105
|
+
if escape != nil
|
106
|
+
b = io.getbyte
|
107
|
+
s = escape
|
108
|
+
elsif newline? b
|
109
|
+
raise "string literal not terminated" if braces == 0
|
110
|
+
b = io.getbyte while newline? b
|
111
|
+
s = "\n"
|
112
|
+
elsif b == "x".ord
|
113
|
+
c1 = nil
|
114
|
+
c2 = nil
|
115
|
+
|
116
|
+
b = io.getbyte || 0
|
117
|
+
|
118
|
+
c1 = b.chr.hex
|
119
|
+
raise "invalid hexadecimal escape sequence" if c1.zero?
|
120
|
+
|
121
|
+
b = io.getbyte || 0
|
122
|
+
|
123
|
+
c2 = b.chr.hex
|
124
|
+
raise "invalid hexadecimal escape sequence" if c2.zero?
|
125
|
+
|
126
|
+
b = io.getbyte
|
127
|
+
s = (c1*16 + c2).to_s(16)
|
128
|
+
elsif b == "u".ord || b == "U".ord
|
129
|
+
size = if b == "U".ord then 8 else 4 end
|
130
|
+
|
131
|
+
b = io.getbyte # Skip "u".
|
132
|
+
|
133
|
+
codepoint = 0
|
134
|
+
hexdigits = 0
|
135
|
+
while hexdigits < size
|
136
|
+
hex = b && b.chr.hex
|
137
|
+
raise "UTF-8 escape sequence contained invalid character:(#{b.chr})" unless hex
|
138
|
+
|
139
|
+
hexdigits += 1
|
140
|
+
codepoint = codepoint * 16 + hex
|
141
|
+
|
142
|
+
raise "UTF-8 escape sequence too large" if codepoint > 0x10FFFF
|
143
|
+
|
144
|
+
b = io.getbyte
|
145
|
+
end
|
146
|
+
|
147
|
+
s = codepoint.chr(Encoding::UTF_8)
|
148
|
+
else
|
149
|
+
cb = b && b.chr.to_i
|
150
|
+
|
151
|
+
raise "invalid escape sequence" if cb.nil?
|
152
|
+
|
153
|
+
b = io.getbyte
|
154
|
+
|
155
|
+
if b != nil
|
156
|
+
c2 = b.chr.to_i
|
157
|
+
|
158
|
+
if b == "0".ord || c2 != 0
|
159
|
+
cb = 10 * cb + c2
|
160
|
+
b = io.getbyte
|
161
|
+
|
162
|
+
if b != nil
|
163
|
+
c3 = b.chr.to_i
|
164
|
+
|
165
|
+
if b == "0".ord || c3 != 0
|
166
|
+
cb = 10 * cb + c3
|
167
|
+
|
168
|
+
raise "invalid decimal escape sequence" if cb > 255
|
169
|
+
|
170
|
+
b = io.getbyte
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
s = cb.chr
|
177
|
+
end
|
178
|
+
|
179
|
+
chunks << s if s != nil
|
180
|
+
|
181
|
+
chunk_start = if b.nil? then io.pos else io.pos - 1 end
|
182
|
+
elsif b.nil? || (newline? b && braces == 0)
|
183
|
+
raise "unfinished string"
|
184
|
+
else
|
185
|
+
b = io.getbyte
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
raise "expected terminating brace" if b.nil? and braces != 0
|
190
|
+
|
191
|
+
if chunks != nil
|
192
|
+
# Put last chunk into buffer.
|
193
|
+
if chunk_start != io.pos
|
194
|
+
chunks << str[chunk_start..io.pos-1]
|
195
|
+
end
|
196
|
+
|
197
|
+
chunks.join
|
198
|
+
else
|
199
|
+
# There were no escape sequences.
|
200
|
+
str
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def visit_string(ast)
|
205
|
+
string = ast[:string]
|
206
|
+
if string == []
|
207
|
+
""
|
208
|
+
else
|
209
|
+
undump(string.to_s)
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def visit_key_string(ast)
|
214
|
+
ast[:key_string].to_s
|
215
|
+
end
|
216
|
+
|
217
|
+
def visit_comment(ast)
|
218
|
+
end
|
219
|
+
|
220
|
+
def visit_integer(ast)
|
221
|
+
ast[:integer].to_i
|
222
|
+
end
|
223
|
+
|
224
|
+
def visit_boolean(ast)
|
225
|
+
case ast[:boolean]
|
226
|
+
when "true" then
|
227
|
+
true
|
228
|
+
when "false"
|
229
|
+
false
|
230
|
+
else raise "unknown boolean #{ast}"
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
def visit_float(ast)
|
235
|
+
ast[:float].to_f
|
236
|
+
end
|
237
|
+
|
238
|
+
def visit_heredoc(ast)
|
239
|
+
doc = ast[:heredoc]
|
240
|
+
tag = doc[:tag]
|
241
|
+
content = doc[:doc].to_s.gsub(/#{tag}$/, "")[1..-1]
|
242
|
+
|
243
|
+
case doc[:backticks].to_s
|
244
|
+
when "<<" then
|
245
|
+
content
|
246
|
+
when "<<-" then
|
247
|
+
# We need to unindent each line based on the indentation level of the marker
|
248
|
+
lines = content.split("\n")
|
249
|
+
indent = lines.last
|
250
|
+
|
251
|
+
indented = true
|
252
|
+
lines.each do |line|
|
253
|
+
if indent.size > line.size
|
254
|
+
indented = false
|
255
|
+
break
|
256
|
+
end
|
257
|
+
|
258
|
+
prefix_found = line.delete_prefix(indent) != line
|
259
|
+
|
260
|
+
unless prefix_found
|
261
|
+
indented = false
|
262
|
+
break
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
# If all lines are not at least as indented as the terminating mark, return the
|
267
|
+
# heredoc as is, but trim the leading space from the marker on the final line.
|
268
|
+
unless indented
|
269
|
+
return content.sub(/\s+\Z/, "") + "\n"
|
270
|
+
end
|
271
|
+
|
272
|
+
unindented_lines = []
|
273
|
+
lines.each do |line|
|
274
|
+
unindented_lines << line.delete_prefix(indent)
|
275
|
+
end
|
276
|
+
unindented_lines.join("\n")
|
277
|
+
else raise "unknown backticks #{backticks}"
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
def visit_list(ast)
|
282
|
+
return [] if ast[:list].nil?
|
283
|
+
|
284
|
+
list = if Hash === ast[:list]
|
285
|
+
[ast[:list]]
|
286
|
+
else
|
287
|
+
ast[:list]
|
288
|
+
end
|
289
|
+
|
290
|
+
list.map do |a|
|
291
|
+
value = a[:value]
|
292
|
+
value = if Array === value
|
293
|
+
it = value.reject { |i| i.key? :comment }
|
294
|
+
raise "extra list value" unless it.size == 1
|
295
|
+
it.first
|
296
|
+
else
|
297
|
+
value
|
298
|
+
end
|
299
|
+
visit(value)
|
300
|
+
end.reject(&:nil?)
|
301
|
+
end
|
302
|
+
|
303
|
+
def recurse_objects(a, b, keys)
|
304
|
+
a_child = a.dup
|
305
|
+
b_child = b.dup
|
306
|
+
|
307
|
+
keys.each do |key|
|
308
|
+
a_child = a_child[key]
|
309
|
+
b_child = b_child[key]
|
310
|
+
end
|
311
|
+
|
312
|
+
return a_child, b_child
|
313
|
+
end
|
314
|
+
|
315
|
+
def hash_or_array(obj)
|
316
|
+
Hash === obj or Array === obj
|
317
|
+
end
|
318
|
+
|
319
|
+
def common_nested_keys(a, b)
|
320
|
+
a_child = a
|
321
|
+
b_child = b
|
322
|
+
|
323
|
+
keys = []
|
324
|
+
finished = false
|
325
|
+
|
326
|
+
until finished
|
327
|
+
break if Array === a_child or Array === b_child
|
328
|
+
break if a_child.nil? or b_child.nil?
|
329
|
+
break if a_child.length > 1 or b_child.length > 1
|
330
|
+
|
331
|
+
a_child.each do |k, v|
|
332
|
+
finished = true if b_child[k].nil?
|
333
|
+
finished = true if (not hash_or_array(a_child[k]) or not hash_or_array(b_child[k]))
|
334
|
+
break if finished
|
335
|
+
|
336
|
+
keys << k
|
337
|
+
a_child = a_child[k]
|
338
|
+
b_child = b_child[k]
|
339
|
+
break
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
return keys, a_child, b_child
|
344
|
+
end
|
345
|
+
|
346
|
+
def objects_share_keys?(a, b, keys)
|
347
|
+
b.each_key do |k|
|
348
|
+
return true if a.key? k
|
349
|
+
end
|
350
|
+
|
351
|
+
false
|
352
|
+
end
|
353
|
+
|
354
|
+
def set_object_nested(this, value, keys)
|
355
|
+
raise "no keys" unless keys.size > 0
|
356
|
+
|
357
|
+
if keys.size == 1
|
358
|
+
this[keys.last] = value
|
359
|
+
return
|
360
|
+
end
|
361
|
+
|
362
|
+
this_parent = this[keys.first]
|
363
|
+
|
364
|
+
keys.drop(1).each do |key|
|
365
|
+
this_parent = this_parent[key]
|
366
|
+
end
|
367
|
+
|
368
|
+
this_parent[keys.last] = value
|
369
|
+
end
|
370
|
+
|
371
|
+
def expand_objects(this, other, keys)
|
372
|
+
this_child, other_child = recurse_objects(this, other, keys)
|
373
|
+
|
374
|
+
set_object_nested(this, [this_child, other_child], keys)
|
375
|
+
end
|
376
|
+
|
377
|
+
def merge_object_lists(this, other, keys)
|
378
|
+
this_child, other_child = recurse_objects(this, other, keys)
|
379
|
+
|
380
|
+
if Hash === this_child && Array === other_child
|
381
|
+
object = this_child
|
382
|
+
list = other_child
|
383
|
+
elsif Array === this_child && Hash === other_child
|
384
|
+
object = other_child
|
385
|
+
list = this_child
|
386
|
+
else
|
387
|
+
raise "not both lists" unless Array === this_child && Array == other_child
|
388
|
+
end
|
389
|
+
|
390
|
+
if list.nil?
|
391
|
+
this_child.each_value do |v|
|
392
|
+
other_child << v
|
393
|
+
end
|
394
|
+
|
395
|
+
set_object_nested(this, other_child, keys)
|
396
|
+
else
|
397
|
+
list << object
|
398
|
+
set_object_nested(this, list, keys)
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
def merge_objects(this, other)
|
403
|
+
raise "merge_objects was called on the same object" if this == other
|
404
|
+
raise "merge_objects was called with non-objects" unless Hash === this && Hash === other
|
405
|
+
|
406
|
+
other.each do |k, v|
|
407
|
+
tmp = this[k]
|
408
|
+
if tmp != nil
|
409
|
+
if Hash === tmp and Hash === v
|
410
|
+
merge_objects(tmp, v)
|
411
|
+
else
|
412
|
+
this[k] = v
|
413
|
+
end
|
414
|
+
else
|
415
|
+
this[k] = v
|
416
|
+
end
|
417
|
+
end
|
418
|
+
end
|
419
|
+
|
420
|
+
def visit_object(ast)
|
421
|
+
object = ast[:object]
|
422
|
+
return {} if object == ""
|
423
|
+
|
424
|
+
pairs = ast[:object].map { |a| visit(a) }.reject(&:nil?)
|
425
|
+
|
426
|
+
pairs.inject({}) do |result, object|
|
427
|
+
first_key = object.sort.first[0]
|
428
|
+
existing = result[first_key]
|
429
|
+
value = object[first_key]
|
430
|
+
expand = false
|
431
|
+
|
432
|
+
if existing != nil
|
433
|
+
if Array === existing
|
434
|
+
existing << value
|
435
|
+
else
|
436
|
+
if Hash === existing
|
437
|
+
if not Hash === existing
|
438
|
+
expand = true
|
439
|
+
else
|
440
|
+
keys, a, b = common_nested_keys(existing, value)
|
441
|
+
if Array === a or Array === b
|
442
|
+
merge_object_lists(existing, value, keys)
|
443
|
+
elsif objects_share_keys?(a, b, keys)
|
444
|
+
if keys.size == 0
|
445
|
+
expand = true
|
446
|
+
else
|
447
|
+
expand_objects(existing, value, keys)
|
448
|
+
end
|
449
|
+
else
|
450
|
+
merge_objects(existing, value)
|
451
|
+
result[first_key] = existing
|
452
|
+
end
|
453
|
+
end
|
454
|
+
else
|
455
|
+
expand = true
|
456
|
+
end
|
457
|
+
|
458
|
+
if expand
|
459
|
+
result[first_key] = [existing, value]
|
460
|
+
end
|
461
|
+
end
|
462
|
+
else
|
463
|
+
result[first_key] = value
|
464
|
+
end
|
465
|
+
|
466
|
+
result
|
467
|
+
end
|
468
|
+
end
|
469
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class HCL::Generator
|
2
|
+
attr_reader :body, :doc
|
3
|
+
|
4
|
+
def initialize(doc)
|
5
|
+
# Ensure all the to_hcl methods are injected into the base Ruby classes
|
6
|
+
# used by HCL.
|
7
|
+
self.class.inject!
|
8
|
+
|
9
|
+
@doc = doc
|
10
|
+
@body = doc.to_hcl
|
11
|
+
|
12
|
+
return @body
|
13
|
+
end
|
14
|
+
|
15
|
+
# Whether or not the injections have already been done.
|
16
|
+
@@injected = false
|
17
|
+
# Inject to_hcl methods into the Ruby classes used by HCL (booleans,
|
18
|
+
# String, Numeric, Array). You can add to_hcl methods to your own classes
|
19
|
+
# to allow them to be easily serialized by the generator (and it will shout
|
20
|
+
# if something doesn't have a to_hcl method).
|
21
|
+
def self.inject!
|
22
|
+
return if @@injected
|
23
|
+
require 'hcl/monkey_patch'
|
24
|
+
@@injected = true
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module HCL
|
2
|
+
def self.escape_key(key)
|
3
|
+
str = key.to_s
|
4
|
+
pos = str =~ /[^a-zA-Z0-9_\-]/
|
5
|
+
|
6
|
+
return str if pos.nil?
|
7
|
+
|
8
|
+
str.dump
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class Object
|
13
|
+
def hcl_object?
|
14
|
+
self.kind_of?(Hash)
|
15
|
+
end
|
16
|
+
def hcl_list?
|
17
|
+
self.kind_of?(Array) && self.first.hcl_object?
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class Hash
|
22
|
+
def to_hcl(indent = 0)
|
23
|
+
return "" if self.empty?
|
24
|
+
hcl = ""
|
25
|
+
spaces = " " * indent
|
26
|
+
|
27
|
+
self.each do |k, v|
|
28
|
+
next if v.hcl_object?
|
29
|
+
next if v.hcl_list? and v.size > 0 and v.first.hcl_object?
|
30
|
+
|
31
|
+
hcl << spaces
|
32
|
+
hcl << HCL.escape_key(k) << " = "
|
33
|
+
hcl << v.to_hcl(indent + 4)
|
34
|
+
hcl << "\n"
|
35
|
+
end
|
36
|
+
|
37
|
+
self.each do |k, v|
|
38
|
+
if v.hcl_object?
|
39
|
+
key = HCL.escape_key(k)
|
40
|
+
hcl << spaces
|
41
|
+
hcl << key << " {\n"
|
42
|
+
hcl << v.to_hcl(indent + 4)
|
43
|
+
hcl << spaces << "}\n"
|
44
|
+
end
|
45
|
+
if v.hcl_list? and v.size > 0 and v.first.hcl_object?
|
46
|
+
key = HCL.escape_key(k)
|
47
|
+
hcl << spaces
|
48
|
+
hcl << key << " = ["
|
49
|
+
v.each do |i|
|
50
|
+
if i.hcl_object?
|
51
|
+
hcl << "\n" << spaces << "{\n"
|
52
|
+
else
|
53
|
+
hcl << spaces
|
54
|
+
end
|
55
|
+
hcl << i.to_hcl(indent + 4)
|
56
|
+
if i.hcl_object?
|
57
|
+
hcl << spaces << "},\n"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
hcl << spaces << "]\n"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
hcl
|
65
|
+
end
|
66
|
+
end
|
67
|
+
class Array
|
68
|
+
def to_hcl(indent = 0)
|
69
|
+
"[" + self.map {|v| v.to_hcl(indent) }.join(", ") + "]"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
class TrueClass
|
73
|
+
def to_hcl(indent = 0); "true"; end
|
74
|
+
end
|
75
|
+
class FalseClass
|
76
|
+
def to_hcl(indent = 0); "false"; end
|
77
|
+
end
|
78
|
+
class String
|
79
|
+
def to_hcl(indent = 0); self.inspect; end
|
80
|
+
end
|
81
|
+
class Numeric
|
82
|
+
def to_hcl(indent = 0); self.to_s; end
|
83
|
+
end
|
84
|
+
class Symbol
|
85
|
+
def to_hcl(indent = 0); HCL.escape_key(self.to_s); end
|
86
|
+
end
|
87
|
+
class DateTime
|
88
|
+
def to_hcl(indent = 0)
|
89
|
+
self.to_time.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
|
90
|
+
end
|
91
|
+
end
|
data/lib/hcl/parser.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
class HCL::Parser
|
2
|
+
def initialize(src)
|
3
|
+
@src = src
|
4
|
+
@parslet = HCL::Parslet.new
|
5
|
+
end
|
6
|
+
|
7
|
+
def parse
|
8
|
+
ast = begin
|
9
|
+
@parslet.parse(@src)
|
10
|
+
rescue Parslet::ParseFailed => error
|
11
|
+
puts error.parse_failure_cause.ascii_tree
|
12
|
+
raise
|
13
|
+
end
|
14
|
+
|
15
|
+
HCL::Decoder.new.decode(ast)
|
16
|
+
end
|
17
|
+
end
|
data/lib/hcl/parslet.rb
ADDED
@@ -0,0 +1,162 @@
|
|
1
|
+
require "parslet"
|
2
|
+
|
3
|
+
class HCL::Parslet < Parslet::Parser
|
4
|
+
rule(:document) {
|
5
|
+
all_space >>
|
6
|
+
((key_value >> all_space) | comment_line).repeat.as(:document) >>
|
7
|
+
all_space
|
8
|
+
}
|
9
|
+
root :document
|
10
|
+
|
11
|
+
rule(:value) {
|
12
|
+
boolean.as(:boolean) |
|
13
|
+
list |
|
14
|
+
object |
|
15
|
+
float.as(:float) |
|
16
|
+
scientific.as(:float) |
|
17
|
+
integer.as(:integer) |
|
18
|
+
string |
|
19
|
+
key.as(:key_string) |
|
20
|
+
heredoc.as(:heredoc)
|
21
|
+
}
|
22
|
+
|
23
|
+
rule(:trailing_comma?) {
|
24
|
+
(all_space >> str(",").maybe).maybe
|
25
|
+
}
|
26
|
+
|
27
|
+
rule (:dood) {
|
28
|
+
}
|
29
|
+
|
30
|
+
rule(:object) {
|
31
|
+
str("{") >> list_comments >> all_space >>
|
32
|
+
( ( key_value >> list_comments >> all_space ).repeat ).maybe.as(:object) >>
|
33
|
+
str("}") >> list_comments
|
34
|
+
}
|
35
|
+
|
36
|
+
rule(:sign) { str("-") }
|
37
|
+
rule(:sign?) { sign.maybe }
|
38
|
+
|
39
|
+
rule(:integer) {
|
40
|
+
str("0") | sign? >>
|
41
|
+
(match["1-9"] >> match["0-9"].repeat)
|
42
|
+
}
|
43
|
+
|
44
|
+
rule(:exponent) {
|
45
|
+
match["eE"] >> match["+\\-"].maybe >> match["0-9"].repeat
|
46
|
+
}
|
47
|
+
|
48
|
+
rule(:scientific) {
|
49
|
+
sign? >>
|
50
|
+
(match["0-9"] >> match["0-9"].repeat) >> exponent
|
51
|
+
}
|
52
|
+
|
53
|
+
rule(:float) {
|
54
|
+
sign? >>
|
55
|
+
(match["0-9"] >> match["0-9"].repeat).maybe >> str(".") >>
|
56
|
+
(match["0-9"] >> match["0-9"].repeat) >> exponent.maybe
|
57
|
+
}
|
58
|
+
|
59
|
+
rule(:key) {
|
60
|
+
string | (match["\\w_\\-"] >> match["\\w\\d_\\-.:"].repeat)
|
61
|
+
}
|
62
|
+
|
63
|
+
rule(:key_value) {
|
64
|
+
space >> key.as(:kv_key) >> space >>
|
65
|
+
((key.as(:key) >> space).repeat.as(:keys) >> object.as(:value) | (str("=") >> space >> value.as(:value))) >> trailing_comma?
|
66
|
+
}
|
67
|
+
|
68
|
+
rule (:sq_string) {
|
69
|
+
str("'") >> match["^'\\n"].repeat.maybe.as(:string) >> str("'")
|
70
|
+
}
|
71
|
+
|
72
|
+
rule (:string_inner) {
|
73
|
+
match["^\"\\\\"] | escape
|
74
|
+
}
|
75
|
+
|
76
|
+
rule(:dq_string) {
|
77
|
+
str('"') >> (hil | (str("${").absent? >> string_inner)).repeat.as(:string) >> str('"')
|
78
|
+
}
|
79
|
+
|
80
|
+
rule(:string) {
|
81
|
+
sq_string | dq_string
|
82
|
+
}
|
83
|
+
|
84
|
+
rule(:hil_inner) {
|
85
|
+
brace | match["^\\\\}"] | escape
|
86
|
+
}
|
87
|
+
|
88
|
+
rule(:hil) {
|
89
|
+
str("${") >> hil_inner.repeat.maybe >> str("}")
|
90
|
+
}
|
91
|
+
|
92
|
+
rule (:brace) {
|
93
|
+
str("{") >> hil_inner.repeat.maybe >> str("}")
|
94
|
+
}
|
95
|
+
|
96
|
+
rule(:heredoc) {
|
97
|
+
space >>
|
98
|
+
backticks.as(:backticks) >>
|
99
|
+
tag.capture(:tag).as(:tag) >> doc.as(:doc)
|
100
|
+
}
|
101
|
+
|
102
|
+
rule(:hex) {
|
103
|
+
match["0-9a-fA-F"]
|
104
|
+
}
|
105
|
+
|
106
|
+
rule(:escape) {
|
107
|
+
str("\\") >> (match["bfnrt\"\\\\"] |
|
108
|
+
(str("u") >> hex.repeat(4,4)) |
|
109
|
+
(str("U") >> hex.repeat(8,8)))
|
110
|
+
}
|
111
|
+
|
112
|
+
# the tag that delimits the heredoc
|
113
|
+
rule(:tag) { match['\\w\\d'].repeat(1) }
|
114
|
+
# the doc itself, ends when tag is found at start of line
|
115
|
+
rule(:doc) { gobble_eol >> doc_line }
|
116
|
+
# a doc_line is either the stop tag followed by nothing
|
117
|
+
# or just any kind of line.
|
118
|
+
rule(:doc_line) {
|
119
|
+
((space >> end_tag).absent? >> gobble_eol).repeat >> space >> end_tag
|
120
|
+
}
|
121
|
+
rule(:end_tag) { dynamic { |s,c| str(c.captures[:tag]) } }
|
122
|
+
# eats anything until an end of line is found
|
123
|
+
rule(:gobble_eol) { (newline.absent? >> any).repeat >> newline }
|
124
|
+
|
125
|
+
rule(:backticks) { str('<<') >> str("-").maybe }
|
126
|
+
|
127
|
+
rule(:boolean) { str("true") | str("false") }
|
128
|
+
|
129
|
+
rule(:space) { match[" \t"].repeat }
|
130
|
+
rule(:all_space) { match[" \t\r\n"].repeat }
|
131
|
+
rule(:newline) { str("\r").maybe >> str("\n") | str("\r") >> str("\n").maybe }
|
132
|
+
rule(:eof) { any.absent? }
|
133
|
+
rule(:newline_or_eof) { newline | eof }
|
134
|
+
|
135
|
+
rule(:comment_line) { comment >> all_space }
|
136
|
+
rule(:single_comment) { (str("#") | str("//")) >> match["^\n"].repeat >> newline_or_eof }
|
137
|
+
rule(:multiline_comment) { str("/*") >> (str("*/").absent? >> any).repeat >> str("*/") }
|
138
|
+
rule(:comment) { (single_comment | multiline_comment).as(:comment) }
|
139
|
+
|
140
|
+
# Finding comments in multiline lists requires accepting a bunch of
|
141
|
+
# possible newlines and stuff before the comment
|
142
|
+
rule(:list_comments) { (all_space >> comment_line).repeat }
|
143
|
+
|
144
|
+
rule(:list) {
|
145
|
+
str("[") >> all_space >> list_comments >>
|
146
|
+
( list_comments >> # Match any comments on first line
|
147
|
+
dood >>
|
148
|
+
(all_space >> str(",")).maybe >> # possible trailing comma
|
149
|
+
all_space >> list_comments # Grab any remaining comments just in case
|
150
|
+
).maybe.as(:list) >> str("]")
|
151
|
+
}
|
152
|
+
|
153
|
+
rule (:dood) {
|
154
|
+
all_space >> (value >> list_comments).as(:value) >>
|
155
|
+
(
|
156
|
+
# Separator followed by any comments
|
157
|
+
all_space >> str(",") >> (list_comments >>
|
158
|
+
# Value followed by any comments
|
159
|
+
all_space >> value).as(:value) >> list_comments >> all_space
|
160
|
+
).repeat
|
161
|
+
}
|
162
|
+
end
|
data/lib/hcl/version.rb
ADDED
data/lib/hcl.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require "hcl/version"
|
2
|
+
require "hcl/ast_visitor"
|
3
|
+
require "hcl/decoder"
|
4
|
+
require "hcl/generator"
|
5
|
+
require "hcl/parser"
|
6
|
+
require "hcl/parslet"
|
7
|
+
|
8
|
+
module HCL
|
9
|
+
def self.load(source)
|
10
|
+
HCL::Parser.new(source).parse
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.load_file(path)
|
14
|
+
HCL::Parser.new(File.read(path)).parse
|
15
|
+
end
|
16
|
+
end
|
metadata
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hcl-rb
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ruin0x11
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-05-08 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: parslet
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.8'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.8'
|
55
|
+
description:
|
56
|
+
email:
|
57
|
+
- ipickering2@gmail.com
|
58
|
+
executables:
|
59
|
+
- console
|
60
|
+
- setup
|
61
|
+
extensions: []
|
62
|
+
extra_rdoc_files: []
|
63
|
+
files:
|
64
|
+
- bin/console
|
65
|
+
- bin/setup
|
66
|
+
- lib/hcl.rb
|
67
|
+
- lib/hcl/ast_visitor.rb
|
68
|
+
- lib/hcl/decoder.rb
|
69
|
+
- lib/hcl/generator.rb
|
70
|
+
- lib/hcl/monkey_patch.rb
|
71
|
+
- lib/hcl/parser.rb
|
72
|
+
- lib/hcl/parslet.rb
|
73
|
+
- lib/hcl/version.rb
|
74
|
+
homepage: https://www.github.com/Ruin0x11/hcl-rb
|
75
|
+
licenses:
|
76
|
+
- MIT
|
77
|
+
metadata:
|
78
|
+
source_code_uri: https://www.github.com/Ruin0x11/hcl-rb
|
79
|
+
post_install_message:
|
80
|
+
rdoc_options: []
|
81
|
+
require_paths:
|
82
|
+
- lib
|
83
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - ">="
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
89
|
+
requirements:
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '0'
|
93
|
+
requirements: []
|
94
|
+
rubygems_version: 3.0.3
|
95
|
+
signing_key:
|
96
|
+
specification_version: 4
|
97
|
+
summary: A ruby parser for HCL (Hashicorp Configuration Language).
|
98
|
+
test_files: []
|