package json import ( "fmt" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/convert" ) // body is the implementation of "Body" used for files processed with the JSON // parser. type body struct { val node // If non-nil, the keys of this map cause the corresponding attributes to // be treated as non-existing. This is used when Body.PartialContent is // called, to produce the "remaining content" Body. hiddenAttrs map[string]struct{} } // expression is the implementation of "Expression" used for files processed // with the JSON parser. type expression struct { src node } func (b *body) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostics) { content, newBody, diags := b.PartialContent(schema) hiddenAttrs := newBody.(*body).hiddenAttrs var nameSuggestions []string for _, attrS := range schema.Attributes { if _, ok := hiddenAttrs[attrS.Name]; !ok { // Only suggest an attribute name if we didn't use it already. nameSuggestions = append(nameSuggestions, attrS.Name) } } for _, blockS := range schema.Blocks { // Blocks can appear multiple times, so we'll suggest their type // names regardless of whether they've already been used. nameSuggestions = append(nameSuggestions, blockS.Type) } jsonAttrs, attrDiags := b.collectDeepAttrs(b.val, nil) diags = append(diags, attrDiags...) for _, attr := range jsonAttrs { k := attr.Name if k == "//" { // Ignore "//" keys in objects representing bodies, to allow // their use as comments. continue } if _, ok := hiddenAttrs[k]; !ok { suggestion := nameSuggestion(k, nameSuggestions) if suggestion != "" { suggestion = fmt.Sprintf(" Did you mean %q?", suggestion) } diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Extraneous JSON object property", Detail: fmt.Sprintf("No argument or block type is named %q.%s", k, suggestion), Subject: &attr.NameRange, Context: attr.Range().Ptr(), }) } } return content, diags } func (b *body) PartialContent(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Body, hcl.Diagnostics) { var diags hcl.Diagnostics jsonAttrs, attrDiags := b.collectDeepAttrs(b.val, nil) diags = append(diags, attrDiags...) usedNames := map[string]struct{}{} if b.hiddenAttrs != nil { for k := range b.hiddenAttrs { usedNames[k] = struct{}{} } } content := &hcl.BodyContent{ Attributes: map[string]*hcl.Attribute{}, Blocks: nil, MissingItemRange: b.MissingItemRange(), } // Create some more convenient data structures for our work below. attrSchemas := map[string]hcl.AttributeSchema{} blockSchemas := map[string]hcl.BlockHeaderSchema{} for _, attrS := range schema.Attributes { attrSchemas[attrS.Name] = attrS } for _, blockS := range schema.Blocks { blockSchemas[blockS.Type] = blockS } for _, jsonAttr := range jsonAttrs { attrName := jsonAttr.Name if _, used := b.hiddenAttrs[attrName]; used { continue } if attrS, defined := attrSchemas[attrName]; defined { if existing, exists := content.Attributes[attrName]; exists { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Duplicate argument", Detail: fmt.Sprintf("The argument %q was already set at %s.", attrName, existing.Range), Subject: &jsonAttr.NameRange, Context: jsonAttr.Range().Ptr(), }) continue } content.Attributes[attrS.Name] = &hcl.Attribute{ Name: attrS.Name, Expr: &expression{src: jsonAttr.Value}, Range: hcl.RangeBetween(jsonAttr.NameRange, jsonAttr.Value.Range()), NameRange: jsonAttr.NameRange, } usedNames[attrName] = struct{}{} } else if blockS, defined := blockSchemas[attrName]; defined { bv := jsonAttr.Value blockDiags := b.unpackBlock(bv, blockS.Type, &jsonAttr.NameRange, blockS.LabelNames, nil, nil, &content.Blocks) diags = append(diags, blockDiags...) usedNames[attrName] = struct{}{} } // We ignore anything that isn't defined because that's the // PartialContent contract. The Content method will catch leftovers. } // Make sure we got all the required attributes. for _, attrS := range schema.Attributes { if !attrS.Required { continue } if _, defined := content.Attributes[attrS.Name]; !defined { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Missing required argument", Detail: fmt.Sprintf("The argument %q is required, but no definition was found.", attrS.Name), Subject: b.MissingItemRange().Ptr(), }) } } unusedBody := &body{ val: b.val, hiddenAttrs: usedNames, } return content, unusedBody, diags } // JustAttributes for JSON bodies interprets all properties of the wrapped // JSON object as attributes and returns them. func (b *body) JustAttributes() (hcl.Attributes, hcl.Diagnostics) { var diags hcl.Diagnostics attrs := make(map[string]*hcl.Attribute) obj, ok := b.val.(*objectVal) if !ok { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Incorrect JSON value type", Detail: "A JSON object is required here, setting the arguments for this block.", Subject: b.val.StartRange().Ptr(), }) return attrs, diags } for _, jsonAttr := range obj.Attrs { name := jsonAttr.Name if name == "//" { // Ignore "//" keys in objects representing bodies, to allow // their use as comments. continue } if _, hidden := b.hiddenAttrs[name]; hidden { continue } if existing, exists := attrs[name]; exists { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Duplicate attribute definition", Detail: fmt.Sprintf("The argument %q was already set at %s.", name, existing.Range), Subject: &jsonAttr.NameRange, }) continue } attrs[name] = &hcl.Attribute{ Name: name, Expr: &expression{src: jsonAttr.Value}, Range: hcl.RangeBetween(jsonAttr.NameRange, jsonAttr.Value.Range()), NameRange: jsonAttr.NameRange, } } // No diagnostics possible here, since the parser already took care of // finding duplicates and every JSON value can be a valid attribute value. return attrs, diags } func (b *body) MissingItemRange() hcl.Range { switch tv := b.val.(type) { case *objectVal: return tv.CloseRange case *arrayVal: return tv.OpenRange default: // Should not happen in correct operation, but might show up if the // input is invalid and we are producing partial results. return tv.StartRange() } } func (b *body) unpackBlock(v node, typeName string, typeRange *hcl.Range, labelsLeft []string, labelsUsed []string, labelRanges []hcl.Range, blocks *hcl.Blocks) (diags hcl.Diagnostics) { if len(labelsLeft) > 0 { labelName := labelsLeft[0] jsonAttrs, attrDiags := b.collectDeepAttrs(v, &labelName) diags = append(diags, attrDiags...) if len(jsonAttrs) == 0 { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Missing block label", Detail: fmt.Sprintf("At least one object property is required, whose name represents the %s block's %s.", typeName, labelName), Subject: v.StartRange().Ptr(), }) return } labelsUsed := append(labelsUsed, "") labelRanges := append(labelRanges, hcl.Range{}) for _, p := range jsonAttrs { pk := p.Name labelsUsed[len(labelsUsed)-1] = pk labelRanges[len(labelRanges)-1] = p.NameRange diags = append(diags, b.unpackBlock(p.Value, typeName, typeRange, labelsLeft[1:], labelsUsed, labelRanges, blocks)...) } return } // By the time we get here, we've peeled off all the labels and we're ready // to deal with the block's actual content. // need to copy the label slices because their underlying arrays will // continue to be mutated after we return. labels := make([]string, len(labelsUsed)) copy(labels, labelsUsed) labelR := make([]hcl.Range, len(labelRanges)) copy(labelR, labelRanges) switch tv := v.(type) { case *nullVal: // There is no block content, e.g the value is null. return case *objectVal: // Single instance of the block *blocks = append(*blocks, &hcl.Block{ Type: typeName, Labels: labels, Body: &body{ val: tv, }, DefRange: tv.OpenRange, TypeRange: *typeRange, LabelRanges: labelR, }) case *arrayVal: // Multiple instances of the block for _, av := range tv.Values { *blocks = append(*blocks, &hcl.Block{ Type: typeName, Labels: labels, Body: &body{ val: av, // might be mistyped; we'll find out when content is requested for this body }, DefRange: tv.OpenRange, TypeRange: *typeRange, LabelRanges: labelR, }) } default: diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Incorrect JSON value type", Detail: fmt.Sprintf("Either a JSON object or a JSON array is required, representing the contents of one or more %q blocks.", typeName), Subject: v.StartRange().Ptr(), }) } return } // collectDeepAttrs takes either a single object or an array of objects and // flattens it into a list of object attributes, collecting attributes from // all of the objects in a given array. // // Ordering is preserved, so a list of objects that each have one property // will result in those properties being returned in the same order as the // objects appeared in the array. // // This is appropriate for use only for objects representing bodies or labels // within a block. // // The labelName argument, if non-null, is used to tailor returned error // messages to refer to block labels rather than attributes and child blocks. // It has no other effect. func (b *body) collectDeepAttrs(v node, labelName *string) ([]*objectAttr, hcl.Diagnostics) { var diags hcl.Diagnostics var attrs []*objectAttr switch tv := v.(type) { case *nullVal: // If a value is null, then we don't return any attributes or return an error. case *objectVal: attrs = append(attrs, tv.Attrs...) case *arrayVal: for _, ev := range tv.Values { switch tev := ev.(type) { case *objectVal: attrs = append(attrs, tev.Attrs...) default: if labelName != nil { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Incorrect JSON value type", Detail: fmt.Sprintf("A JSON object is required here, to specify %s labels for this block.", *labelName), Subject: ev.StartRange().Ptr(), }) } else { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Incorrect JSON value type", Detail: "A JSON object is required here, to define arguments and child blocks.", Subject: ev.StartRange().Ptr(), }) } } } default: if labelName != nil { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Incorrect JSON value type", Detail: fmt.Sprintf("Either a JSON object or JSON array of objects is required here, to specify %s labels for this block.", *labelName), Subject: v.StartRange().Ptr(), }) } else { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Incorrect JSON value type", Detail: "Either a JSON object or JSON array of objects is required here, to define arguments and child blocks.", Subject: v.StartRange().Ptr(), }) } } return attrs, diags } func (e *expression) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { switch v := e.src.(type) { case *stringVal: if ctx != nil { // Parse string contents as a HCL native language expression. // We only do this if we have a context, so passing a nil context // is how the caller specifies that interpolations are not allowed // and that the string should just be returned verbatim. templateSrc := v.Value expr, diags := hclsyntax.ParseTemplate( []byte(templateSrc), v.SrcRange.Filename, // This won't produce _exactly_ the right result, since // the hclsyntax parser can't "see" any escapes we removed // while parsing JSON, but it's better than nothing. hcl.Pos{ Line: v.SrcRange.Start.Line, // skip over the opening quote mark Byte: v.SrcRange.Start.Byte + 1, Column: v.SrcRange.Start.Column + 1, }, ) if diags.HasErrors() { return cty.DynamicVal, diags } val, evalDiags := expr.Value(ctx) diags = append(diags, evalDiags...) return val, diags } return cty.StringVal(v.Value), nil case *numberVal: return cty.NumberVal(v.Value), nil case *booleanVal: return cty.BoolVal(v.Value), nil case *arrayVal: var diags hcl.Diagnostics vals := []cty.Value{} for _, jsonVal := range v.Values { val, valDiags := (&expression{src: jsonVal}).Value(ctx) vals = append(vals, val) diags = append(diags, valDiags...) } return cty.TupleVal(vals), diags case *objectVal: var diags hcl.Diagnostics attrs := map[string]cty.Value{} attrRanges := map[string]hcl.Range{} known := true for _, jsonAttr := range v.Attrs { // In this one context we allow keys to contain interpolation // expressions too, assuming we're evaluating in interpolation // mode. This achieves parity with the native syntax where // object expressions can have dynamic keys, while block contents // may not. name, nameDiags := (&expression{src: &stringVal{ Value: jsonAttr.Name, SrcRange: jsonAttr.NameRange, }}).Value(ctx) valExpr := &expression{src: jsonAttr.Value} val, valDiags := valExpr.Value(ctx) diags = append(diags, nameDiags...) diags = append(diags, valDiags...) var err error name, err = convert.Convert(name, cty.String) if err != nil { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid object key expression", Detail: fmt.Sprintf("Cannot use this expression as an object key: %s.", err), Subject: &jsonAttr.NameRange, Expression: valExpr, EvalContext: ctx, }) continue } if name.IsNull() { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid object key expression", Detail: "Cannot use null value as an object key.", Subject: &jsonAttr.NameRange, Expression: valExpr, EvalContext: ctx, }) continue } if !name.IsKnown() { // This is a bit of a weird case, since our usual rules require // us to tolerate unknowns and just represent the result as // best we can but if we don't know the key then we can't // know the type of our object at all, and thus we must turn // the whole thing into cty.DynamicVal. This is consistent with // how this situation is handled in the native syntax. // We'll keep iterating so we can collect other errors in // subsequent attributes. known = false continue } nameStr := name.AsString() if _, defined := attrs[nameStr]; defined { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Duplicate object attribute", Detail: fmt.Sprintf("An attribute named %q was already defined at %s.", nameStr, attrRanges[nameStr]), Subject: &jsonAttr.NameRange, Expression: e, EvalContext: ctx, }) continue } attrs[nameStr] = val attrRanges[nameStr] = jsonAttr.NameRange } if !known { // We encountered an unknown key somewhere along the way, so // we can't know what our type will eventually be. return cty.DynamicVal, diags } return cty.ObjectVal(attrs), diags case *nullVal: return cty.NullVal(cty.DynamicPseudoType), nil default: // Default to DynamicVal so that ASTs containing invalid nodes can // still be partially-evaluated. return cty.DynamicVal, nil } } func (e *expression) Variables() []hcl.Traversal { var vars []hcl.Traversal switch v := e.src.(type) { case *stringVal: templateSrc := v.Value expr, diags := hclsyntax.ParseTemplate( []byte(templateSrc), v.SrcRange.Filename, // This won't produce _exactly_ the right result, since // the hclsyntax parser can't "see" any escapes we removed // while parsing JSON, but it's better than nothing. hcl.Pos{ Line: v.SrcRange.Start.Line, // skip over the opening quote mark Byte: v.SrcRange.Start.Byte + 1, Column: v.SrcRange.Start.Column + 1, }, ) if diags.HasErrors() { return vars } return expr.Variables() case *arrayVal: for _, jsonVal := range v.Values { vars = append(vars, (&expression{src: jsonVal}).Variables()...) } case *objectVal: for _, jsonAttr := range v.Attrs { keyExpr := &stringVal{ // we're going to treat key as an expression in this context Value: jsonAttr.Name, SrcRange: jsonAttr.NameRange, } vars = append(vars, (&expression{src: keyExpr}).Variables()...) vars = append(vars, (&expression{src: jsonAttr.Value}).Variables()...) } } return vars } func (e *expression) Range() hcl.Range { return e.src.Range() } func (e *expression) StartRange() hcl.Range { return e.src.StartRange() } // Implementation for hcl.AbsTraversalForExpr. func (e *expression) AsTraversal() hcl.Traversal { // In JSON-based syntax a traversal is given as a string containing // traversal syntax as defined by hclsyntax.ParseTraversalAbs. switch v := e.src.(type) { case *stringVal: traversal, diags := hclsyntax.ParseTraversalAbs([]byte(v.Value), v.SrcRange.Filename, v.SrcRange.Start) if diags.HasErrors() { return nil } return traversal default: return nil } } // Implementation for hcl.ExprCall. func (e *expression) ExprCall() *hcl.StaticCall { // In JSON-based syntax a static call is given as a string containing // an expression in the native syntax that also supports ExprCall. switch v := e.src.(type) { case *stringVal: expr, diags := hclsyntax.ParseExpression([]byte(v.Value), v.SrcRange.Filename, v.SrcRange.Start) if diags.HasErrors() { return nil } call, diags := hcl.ExprCall(expr) if diags.HasErrors() { return nil } return call default: return nil } } // Implementation for hcl.ExprList. func (e *expression) ExprList() []hcl.Expression { switch v := e.src.(type) { case *arrayVal: ret := make([]hcl.Expression, len(v.Values)) for i, node := range v.Values { ret[i] = &expression{src: node} } return ret default: return nil } } // Implementation for hcl.ExprMap. func (e *expression) ExprMap() []hcl.KeyValuePair { switch v := e.src.(type) { case *objectVal: ret := make([]hcl.KeyValuePair, len(v.Attrs)) for i, jsonAttr := range v.Attrs { ret[i] = hcl.KeyValuePair{ Key: &expression{src: &stringVal{ Value: jsonAttr.Name, SrcRange: jsonAttr.NameRange, }}, Value: &expression{src: jsonAttr.Value}, } } return ret default: return nil } }