1// Package convert is taken from github.com/tmccombs/hcl2json added support to convert hcl.Body
2package convert
3
4import (
5	"encoding/json"
6	"fmt"
7	"strings"
8
9	hcl "github.com/hashicorp/hcl/v2"
10	"github.com/hashicorp/hcl/v2/hclsyntax"
11	"github.com/zclconf/go-cty/cty"
12	ctyconvert "github.com/zclconf/go-cty/cty/convert"
13	ctyjson "github.com/zclconf/go-cty/cty/json"
14)
15
16type Options struct {
17	Simplify  bool
18	Variables map[string]cty.Value
19}
20
21// Bytes takes the contents of an HCL file, as bytes, and converts
22// them into a JSON representation of the HCL file.
23func Bytes(bytes []byte, filename string, options Options) ([]byte, error) {
24	file, diags := hclsyntax.ParseConfig(bytes, filename, hcl.Pos{Line: 1, Column: 1})
25	if diags.HasErrors() {
26		return nil, fmt.Errorf("parse config: %v", diags.Errs())
27	}
28
29	hclBytes, err := File(file, options)
30	if err != nil {
31		return nil, fmt.Errorf("convert to HCL: %w", err)
32	}
33
34	return hclBytes, nil
35}
36
37// File takes an HCL file and converts it to its JSON representation.
38func File(file *hcl.File, options Options) ([]byte, error) {
39	convertedFile, err := ConvertFile(file, options)
40	if err != nil {
41		return nil, fmt.Errorf("convert file: %w", err)
42	}
43
44	jsonBytes, err := json.Marshal(convertedFile)
45	if err != nil {
46		return nil, fmt.Errorf("marshal json: %w", err)
47	}
48
49	return jsonBytes, nil
50}
51
52// Body takes the contents of an HCL Body, and converts
53// them into a JSON representation of the HCL file.
54func Body(hclBody hcl.Body, options Options) ([]byte, error) {
55	var out jsonObj
56	var err error
57
58	c := converter{
59		bytes:   nil,
60		options: options,
61		context: addVariables(GetEvalContext(""), options.Variables),
62	}
63
64	body, ok := hclBody.(*hclsyntax.Body)
65	if !ok {
66		// body is hcl.json and cannot converted to hclsyntax
67		attrs, diags := hclBody.JustAttributes()
68		if diags != nil {
69			return nil, fmt.Errorf("convert body: %w", diags)
70		}
71		out, err = c.convertAttributes(attrs)
72	} else {
73		out, err = c.convertBody(body)
74	}
75
76	if err != nil {
77		return nil, fmt.Errorf("convert body: %w", err)
78	}
79
80	jsonBytes, err := json.Marshal(out)
81	if err != nil {
82		return nil, fmt.Errorf("marshal json: %w", err)
83	}
84
85	return jsonBytes, nil
86}
87
88type jsonObj map[string]interface{}
89
90type converter struct {
91	bytes   []byte
92	options Options
93	context *hcl.EvalContext
94}
95
96// addVariables modifes given context in place adding supplied variables to it.
97func addVariables(ctx *hcl.EvalContext, vars map[string]cty.Value) *hcl.EvalContext {
98	if len(vars) == 0 {
99		return ctx
100	}
101	if ctx.Variables == nil {
102		ctx.Variables = make(map[string]cty.Value, len(vars))
103	}
104	for k, v := range vars {
105		ctx.Variables[k] = v
106	}
107	return ctx
108}
109
110func ConvertFile(file *hcl.File, options Options) (jsonObj, error) {
111	body, ok := file.Body.(*hclsyntax.Body)
112	if !ok {
113		return nil, fmt.Errorf("convert file body to body type")
114	}
115
116	c := converter{
117		bytes:   file.Bytes,
118		options: options,
119		context: addVariables(GetEvalContext(""), options.Variables),
120	}
121
122	out, err := c.convertBody(body)
123	if err != nil {
124		return nil, fmt.Errorf("convert body: %w", err)
125	}
126
127	return out, nil
128}
129
130func (c *converter) convertBody(body *hclsyntax.Body) (jsonObj, error) {
131	out := make(jsonObj)
132
133	for _, block := range body.Blocks {
134		if err := c.convertBlock(block, out); err != nil {
135			return nil, fmt.Errorf("convert block: %w", err)
136		}
137	}
138
139	var err error
140	for key, value := range body.Attributes {
141		out[key], err = c.convertExpression(value.Expr)
142		if err != nil {
143			return nil, fmt.Errorf("convert expression: %w", err)
144		}
145	}
146
147	return out, nil
148}
149
150func (c *converter) rangeSource(r hcl.Range) string {
151	// for some reason the range doesn't include the ending paren, so
152	// check if the next character is an ending paren, and include it if it is.
153	end := r.End.Byte
154	if end < len(c.bytes) && c.bytes[end] == ')' {
155		end++
156	}
157	return string(c.bytes[r.Start.Byte:end])
158}
159
160func (c *converter) convertBlock(block *hclsyntax.Block, out jsonObj) error {
161	key := block.Type
162	for _, label := range block.Labels {
163
164		// Labels represented in HCL are defined as quoted strings after the name of the block:
165		// block "label_one" "label_two"
166		//
167		// Labels represtend in JSON are nested one after the other:
168		// "label_one": {
169		//   "label_two": {}
170		// }
171		//
172		// To create the JSON representation, check to see if the label exists in the current output:
173		//
174		// When the label exists, move onto the next label reference.
175		// When a label does not exist, create the label in the output and set that as the next label reference
176		// in order to append (potential) labels to it.
177		if _, exists := out[key]; exists {
178			var ok bool
179			out, ok = out[key].(jsonObj)
180			if !ok {
181				return fmt.Errorf("unable to convert Block to JSON: %v.%v", block.Type, strings.Join(block.Labels, "."))
182			}
183		} else {
184			out[key] = make(jsonObj)
185			out = out[key].(jsonObj)
186		}
187
188		key = label
189	}
190
191	value, err := c.convertBody(block.Body)
192	if err != nil {
193		return fmt.Errorf("convert body: %w", err)
194	}
195
196	// Multiple blocks can exist with the same name, at the same
197	// level in the JSON document (e.g. locals).
198	//
199	// For consistency, always wrap the value in a collection.
200	// When multiple values are at the same key
201	if current, exists := out[key]; exists {
202		out[key] = append(current.([]interface{}), value)
203	} else {
204		out[key] = []interface{}{value}
205	}
206
207	return nil
208}
209
210func (c *converter) convertAttributes(attributes hcl.Attributes) (jsonObj, error) {
211	var out = make(jsonObj)
212
213	for _, attr := range attributes {
214		val, err := attr.Expr.Value(c.context)
215		if err != nil {
216			return nil, fmt.Errorf("convert expression: %w", err)
217		}
218		out[attr.Name] = ctyjson.SimpleJSONValue{Value: val}
219	}
220	return out, nil
221}
222
223func (c *converter) convertExpression(expr hclsyntax.Expression) (interface{}, error) {
224	if c.options.Simplify {
225		value, err := expr.Value(c.context)
226		if err == nil {
227			return ctyjson.SimpleJSONValue{Value: value}, nil
228		}
229	}
230
231	// assume it is hcl syntax (because, um, it is)
232	switch value := expr.(type) {
233	case *hclsyntax.LiteralValueExpr:
234		return ctyjson.SimpleJSONValue{Value: value.Val}, nil
235	case *hclsyntax.UnaryOpExpr:
236		return c.convertUnary(value)
237	case *hclsyntax.TemplateExpr:
238		return c.convertTemplate(value)
239	case *hclsyntax.TemplateWrapExpr:
240		return c.convertExpression(value.Wrapped)
241	case *hclsyntax.TupleConsExpr:
242		list := make([]interface{}, 0)
243		for _, ex := range value.Exprs {
244			elem, err := c.convertExpression(ex)
245			if err != nil {
246				return nil, err
247			}
248			list = append(list, elem)
249		}
250		return list, nil
251	case *hclsyntax.ObjectConsExpr:
252		m := make(jsonObj)
253		for _, item := range value.Items {
254			key, err := c.convertKey(item.KeyExpr)
255			if err != nil {
256				return nil, err
257			}
258			m[key], err = c.convertExpression(item.ValueExpr)
259			if err != nil {
260				return nil, err
261			}
262		}
263		return m, nil
264	default:
265		return c.wrapExpr(expr), nil
266	}
267}
268
269func (c *converter) convertUnary(v *hclsyntax.UnaryOpExpr) (interface{}, error) {
270	_, isLiteral := v.Val.(*hclsyntax.LiteralValueExpr)
271	if !isLiteral {
272		// If the expression after the operator isn't a literal, fall back to
273		// wrapping the expression with ${...}
274		return c.wrapExpr(v), nil
275	}
276	val, err := v.Value(nil)
277	if err != nil {
278		return nil, err
279	}
280	return ctyjson.SimpleJSONValue{Value: val}, nil
281}
282
283func (c *converter) convertTemplate(t *hclsyntax.TemplateExpr) (string, error) {
284	if t.IsStringLiteral() {
285		// safe because the value is just the string
286		v, err := t.Value(nil)
287		if err != nil {
288			return "", err
289		}
290		return v.AsString(), nil
291	}
292	var builder strings.Builder
293	for _, part := range t.Parts {
294		s, err := c.convertStringPart(part)
295		if err != nil {
296			return "", err
297		}
298		builder.WriteString(s)
299	}
300	return builder.String(), nil
301}
302
303func (c *converter) convertStringPart(expr hclsyntax.Expression) (string, error) {
304	switch v := expr.(type) {
305	case *hclsyntax.LiteralValueExpr:
306		s, err := ctyconvert.Convert(v.Val, cty.String)
307		if err != nil {
308			return "", err
309		}
310		return s.AsString(), nil
311	case *hclsyntax.TemplateExpr:
312		return c.convertTemplate(v)
313	case *hclsyntax.TemplateWrapExpr:
314		return c.convertStringPart(v.Wrapped)
315	case *hclsyntax.ConditionalExpr:
316		return c.convertTemplateConditional(v)
317	case *hclsyntax.TemplateJoinExpr:
318		return c.convertTemplateFor(v.Tuple.(*hclsyntax.ForExpr))
319	default:
320		// treating as an embedded expression
321		return c.wrapExpr(expr), nil
322	}
323}
324
325func (c *converter) convertKey(keyExpr hclsyntax.Expression) (string, error) {
326	// a key should never have dynamic input
327	if k, isKeyExpr := keyExpr.(*hclsyntax.ObjectConsKeyExpr); isKeyExpr {
328		keyExpr = k.Wrapped
329		if _, isTraversal := keyExpr.(*hclsyntax.ScopeTraversalExpr); isTraversal {
330			return c.rangeSource(keyExpr.Range()), nil
331		}
332	}
333	return c.convertStringPart(keyExpr)
334}
335
336func (c *converter) convertTemplateConditional(expr *hclsyntax.ConditionalExpr) (string, error) {
337	var builder strings.Builder
338	builder.WriteString("%{if ")
339	builder.WriteString(c.rangeSource(expr.Condition.Range()))
340	builder.WriteString("}")
341	trueResult, err := c.convertStringPart(expr.TrueResult)
342	if err != nil {
343		return "", nil
344	}
345	builder.WriteString(trueResult)
346	falseResult, _ := c.convertStringPart(expr.FalseResult)
347	if len(falseResult) > 0 {
348		builder.WriteString("%{else}")
349		builder.WriteString(falseResult)
350	}
351	builder.WriteString("%{endif}")
352
353	return builder.String(), nil
354}
355
356func (c *converter) convertTemplateFor(expr *hclsyntax.ForExpr) (string, error) {
357	var builder strings.Builder
358	builder.WriteString("%{for ")
359	if len(expr.KeyVar) > 0 {
360		builder.WriteString(expr.KeyVar)
361		builder.WriteString(", ")
362	}
363	builder.WriteString(expr.ValVar)
364	builder.WriteString(" in ")
365	builder.WriteString(c.rangeSource(expr.CollExpr.Range()))
366	builder.WriteString("}")
367	templ, err := c.convertStringPart(expr.ValExpr)
368	if err != nil {
369		return "", err
370	}
371	builder.WriteString(templ)
372	builder.WriteString("%{endfor}")
373
374	return builder.String(), nil
375}
376
377func (c *converter) wrapExpr(expr hclsyntax.Expression) string {
378	return "${" + c.rangeSource(expr.Range()) + "}"
379}
380