hcl-rb 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|