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