Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

schema/attribute: introduce explicit IsOptional field #1

Merged
merged 1 commit into from
Nov 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion decoder/body_candidates.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ func sortedBlockTypes(blocks map[string]*schema.BlockSchema) []string {
}

func isAttributeDeclarable(body *hclsyntax.Body, name string, attr *schema.AttributeSchema) bool {
if attr.IsReadOnly {
if attr.IsComputed && !attr.IsOptional {
return false
}

Expand Down
71 changes: 71 additions & 0 deletions decoder/body_decoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,77 @@ func TestDecoder_CandidateAtPos_incompleteAttributes(t *testing.T) {
}
}

func TestDecoder_CandidateAtPos_computedAttributes(t *testing.T) {
bodySchema := &schema.BodySchema{
Blocks: map[string]*schema.BlockSchema{
"customblock": {
Labels: []*schema.LabelSchema{
{Name: "type"},
},
Body: &schema.BodySchema{
Attributes: map[string]*schema.AttributeSchema{
"attr1": {ValueType: cty.Number, IsComputed: true},
"attr2": {ValueType: cty.Number, IsComputed: true, IsOptional: true},
"some_other_attr": {ValueType: cty.Number},
"another_attr": {ValueType: cty.Number},
},
},
},
},
}

d := NewDecoder()
d.SetSchema(bodySchema)

f, _ := hclsyntax.ParseConfig([]byte(`customblock "label1" {
attr
}
`), "test.tf", hcl.InitialPos)
err := d.LoadFile("test.tf", f)
if err != nil {
t.Fatal(err)
}

candidates, err := d.CandidatesAtPos("test.tf", hcl.Pos{
Line: 2,
Column: 7,
Byte: 29,
})
if err != nil {
t.Fatal(err)
}
expectedCandidates := lang.Candidates{
List: []lang.Candidate{
{
Label: "attr2",
Detail: "Optional, number",
TextEdit: lang.TextEdit{
Range: hcl.Range{
Filename: "test.tf",
Start: hcl.Pos{
Line: 2,
Column: 3,
Byte: 25,
},
End: hcl.Pos{
Line: 2,
Column: 7,
Byte: 29,
},
},
NewText: "attr2",
Snippet: "attr2 = ${1:1}",
},
Kind: lang.AttributeCandidateKind,
},
},
IsComplete: true,
}
if diff := cmp.Diff(expectedCandidates, candidates); diff != "" {
t.Fatalf("unexpected candidates: %s", diff)
}
}

func TestDecoder_CandidateAtPos_incompleteBlocks(t *testing.T) {
bodySchema := &schema.BodySchema{
Blocks: map[string]*schema.BlockSchema{
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.14

require (
github.com/google/go-cmp v0.3.1
github.com/hashicorp/go-multierror v1.1.0
github.com/hashicorp/hcl/v2 v2.6.0
github.com/mitchellh/copystructure v1.0.0
github.com/zclconf/go-cty v1.6.1
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI=
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
github.com/hashicorp/hcl/v2 v2.6.0 h1:3krZOfGY6SziUXa6H9PJU6TyohHn7I+ARYnhbeNBz+o=
github.com/hashicorp/hcl/v2 v2.6.0/go.mod h1:bQTN5mpo+jewjJgh8jr0JUguIi7qPHUF6yIfAEN3jqY=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
Expand Down
28 changes: 27 additions & 1 deletion schema/attribute_schema.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package schema

import (
"errors"

"github.com/hashicorp/hcl-lang/lang"
"github.com/zclconf/go-cty/cty"
)
Expand All @@ -9,8 +11,9 @@ import (
type AttributeSchema struct {
Description lang.MarkupContent
IsRequired bool
IsOptional bool
IsDeprecated bool
IsReadOnly bool
IsComputed bool

ValueType cty.Type
ValueTypes ValueTypes
Expand All @@ -33,3 +36,26 @@ func (vt ValueTypes) FriendlyNames() []string {
func (*AttributeSchema) isSchemaImpl() schemaImplSigil {
return schemaImplSigil{}
}

func (as *AttributeSchema) Validate() error {
if len(as.ValueTypes) == 0 && as.ValueType == cty.NilType {
return errors.New("one of ValueType or ValueTypes must be specified")
}
if len(as.ValueTypes) > 0 && as.ValueType != cty.NilType {
return errors.New("ValueType or ValueTypes must be specified, not both")
}

if as.IsOptional && as.IsRequired {
return errors.New("IsOptional or IsRequired must be set, not both")
}

if as.IsRequired && as.IsComputed {
return errors.New("cannot be both IsRequired and IsComputed")
}

if !as.IsRequired && !as.IsOptional && !as.IsComputed {
return errors.New("one of IsRequired, IsOptional, or IsComputed must be set")
}

return nil
}
81 changes: 81 additions & 0 deletions schema/attribute_schema_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package schema

import (
"errors"
"fmt"
"testing"

"github.com/zclconf/go-cty/cty"
)

func TestAttributeSchema_Validate(t *testing.T) {
testCases := []struct {
schema *AttributeSchema
expectedErr error
}{
{
&AttributeSchema{},
errors.New("one of ValueType or ValueTypes must be specified"),
},
{
&AttributeSchema{
ValueType: cty.String,
},
errors.New("one of IsRequired, IsOptional, or IsComputed must be set"),
},
{
&AttributeSchema{
ValueTypes: []cty.Type{cty.String, cty.Number},
IsComputed: true,
},
nil,
},
{
&AttributeSchema{
ValueType: cty.String,
ValueTypes: []cty.Type{cty.String, cty.Number},
IsComputed: true,
},
errors.New("ValueType or ValueTypes must be specified, not both"),
},
{
&AttributeSchema{
ValueType: cty.String,
IsRequired: true,
IsOptional: true,
},
errors.New("IsOptional or IsRequired must be set, not both"),
},
{
&AttributeSchema{
ValueType: cty.String,
IsRequired: true,
IsComputed: true,
},
errors.New("cannot be both IsRequired and IsComputed"),
},
{
&AttributeSchema{
ValueType: cty.String,
IsOptional: true,
IsComputed: true,
},
nil,
},
}

for i, tc := range testCases {
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
err := tc.schema.Validate()
if tc.expectedErr == nil && err != nil {
t.Fatal(err)
}
if tc.expectedErr != nil && err == nil {
t.Fatalf("expected error: %q, none given", tc.expectedErr.Error())
}
if tc.expectedErr != nil && tc.expectedErr.Error() != err.Error() {
t.Fatalf("error mismatch,\nexpected: %q\ngiven: %q", tc.expectedErr.Error(), err.Error())
}
})
}
}
36 changes: 35 additions & 1 deletion schema/body_schema.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package schema

import (
"fmt"

"github.com/hashicorp/go-multierror"
"github.com/hashicorp/hcl-lang/lang"
)

Expand All @@ -14,7 +17,6 @@ type BodySchema struct {
Detail string
Description lang.MarkupContent

// TODO: validate conflict between Attributes and AnyAttribute
// TODO: Functions
}

Expand All @@ -29,3 +31,35 @@ func NewBodySchema() *BodySchema {
Attributes: make(map[string]*AttributeSchema, 0),
}
}

func (bs *BodySchema) Validate() error {
if len(bs.Attributes) > 0 && bs.AnyAttribute != nil {
return fmt.Errorf("one of Attributes or AnyAttribute must be set, not both")
}

var result *multierror.Error
for name, attr := range bs.Attributes {
err := attr.Validate()
if err != nil {
result = multierror.Append(result, fmt.Errorf("%s: %s", name, err))
}
}

for bType, block := range bs.Blocks {
if block.Body == nil {
continue
}
err := block.Body.Validate()
if err != nil {
if me, ok := err.(*multierror.Error); ok {
for _, err := range me.Errors {
result = multierror.Append(result, fmt.Errorf("%s: %s", bType, err))
}
} else {
result = multierror.Append(result, fmt.Errorf("%s: %s", bType, err))
}
}
}

return result.ErrorOrNil()
}