config/configschema: InternalValidate for blocks
This checks that a schema complies with the documented constraints on which values are valid. It is primarily intended for use in tests.
This commit is contained in:
parent
d712a04c32
commit
f117906bdb
|
@ -0,0 +1,92 @@
|
|||
package configschema
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
multierror "github.com/hashicorp/go-multierror"
|
||||
)
|
||||
|
||||
var validName = regexp.MustCompile(`^[a-z0-9_]+$`)
|
||||
|
||||
// InternalValidate returns an error if the receiving block and its child
|
||||
// schema definitions have any consistencies with the documented rules for
|
||||
// valid schema.
|
||||
//
|
||||
// This is intended to be used within unit tests to detect when a given
|
||||
// schema is invalid.
|
||||
func (b *Block) InternalValidate() error {
|
||||
if b == nil {
|
||||
return fmt.Errorf("top-level block schema is nil")
|
||||
}
|
||||
return b.internalValidate("", nil)
|
||||
|
||||
}
|
||||
|
||||
func (b *Block) internalValidate(prefix string, err error) error {
|
||||
for name, attrS := range b.Attributes {
|
||||
if attrS == nil {
|
||||
err = multierror.Append(err, fmt.Errorf("%s%s: attribute schema is nil", prefix, name))
|
||||
continue
|
||||
}
|
||||
if !validName.MatchString(name) {
|
||||
err = multierror.Append(err, fmt.Errorf("%s%s: name may contain only lowercase letters, digits and underscores", prefix, name))
|
||||
}
|
||||
if attrS.Optional == false && attrS.Required == false && attrS.Computed == false {
|
||||
err = multierror.Append(err, fmt.Errorf("%s%s: must set Optional, Required or Computed", prefix, name))
|
||||
}
|
||||
if attrS.Optional && attrS.Required {
|
||||
err = multierror.Append(err, fmt.Errorf("%s%s: cannot set both Optional and Required", prefix, name))
|
||||
}
|
||||
if attrS.Computed && attrS.Required {
|
||||
err = multierror.Append(err, fmt.Errorf("%s%s: cannot set both Computed and Required", prefix, name))
|
||||
}
|
||||
if attrS.Type == cty.NilType {
|
||||
err = multierror.Append(err, fmt.Errorf("%s%s: Type must be set to something other than cty.NilType", prefix, name))
|
||||
}
|
||||
}
|
||||
|
||||
for name, blockS := range b.BlockTypes {
|
||||
if blockS == nil {
|
||||
err = multierror.Append(err, fmt.Errorf("%s%s: block schema is nil", prefix, name))
|
||||
continue
|
||||
}
|
||||
|
||||
if _, isAttr := b.Attributes[name]; isAttr {
|
||||
err = multierror.Append(err, fmt.Errorf("%s%s: name defined as both attribute and child block type", prefix, name))
|
||||
} else if !validName.MatchString(name) {
|
||||
err = multierror.Append(err, fmt.Errorf("%s%s: name may contain only lowercase letters, digits and underscores", prefix, name))
|
||||
}
|
||||
|
||||
if blockS.MinItems < 0 || blockS.MaxItems < 0 {
|
||||
err = multierror.Append(err, fmt.Errorf("%s%s: MinItems and MaxItems must both be greater than zero", prefix, name))
|
||||
}
|
||||
|
||||
switch blockS.Nesting {
|
||||
case NestingSingle:
|
||||
switch {
|
||||
case blockS.MinItems != blockS.MaxItems:
|
||||
err = multierror.Append(err, fmt.Errorf("%s%s: MinItems and MaxItems must match in NestingSingle mode", prefix, name))
|
||||
case blockS.MinItems < 0 || blockS.MinItems > 1:
|
||||
err = multierror.Append(err, fmt.Errorf("%s%s: MinItems and MaxItems must be set to either 0 or 1 in NestingSingle mode", prefix, name))
|
||||
}
|
||||
case NestingList, NestingSet:
|
||||
if blockS.MinItems > blockS.MaxItems && blockS.MaxItems != 0 {
|
||||
err = multierror.Append(err, fmt.Errorf("%s%s: MinItems must be less than or equal to MaxItems in %s mode", prefix, name, blockS.Nesting))
|
||||
}
|
||||
case NestingMap:
|
||||
if blockS.MinItems != 0 || blockS.MaxItems != 0 {
|
||||
err = multierror.Append(err, fmt.Errorf("%s%s: MinItems and MaxItems must both be 0 in NestingMap mode", prefix, name))
|
||||
}
|
||||
default:
|
||||
err = multierror.Append(err, fmt.Errorf("%s%s: invalid nesting mode %s", prefix, name, blockS.Nesting))
|
||||
}
|
||||
|
||||
subPrefix := prefix + name + "."
|
||||
err = blockS.Block.internalValidate(subPrefix, err)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,238 @@
|
|||
package configschema
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
multierror "github.com/hashicorp/go-multierror"
|
||||
)
|
||||
|
||||
func TestBlockInternalValidate(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
Block *Block
|
||||
ErrCount int
|
||||
}{
|
||||
"empty": {
|
||||
&Block{},
|
||||
0,
|
||||
},
|
||||
"valid": {
|
||||
&Block{
|
||||
Attributes: map[string]*Attribute{
|
||||
"foo": &Attribute{
|
||||
Type: cty.String,
|
||||
Required: true,
|
||||
},
|
||||
"bar": &Attribute{
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
},
|
||||
"baz": &Attribute{
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
"baz_maybe": &Attribute{
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
BlockTypes: map[string]*NestedBlock{
|
||||
"single": &NestedBlock{
|
||||
Nesting: NestingSingle,
|
||||
Block: Block{},
|
||||
},
|
||||
"single_required": &NestedBlock{
|
||||
Nesting: NestingSingle,
|
||||
Block: Block{},
|
||||
MinItems: 1,
|
||||
MaxItems: 1,
|
||||
},
|
||||
"list": &NestedBlock{
|
||||
Nesting: NestingList,
|
||||
Block: Block{},
|
||||
},
|
||||
"list_required": &NestedBlock{
|
||||
Nesting: NestingList,
|
||||
Block: Block{},
|
||||
MinItems: 1,
|
||||
},
|
||||
"set": &NestedBlock{
|
||||
Nesting: NestingSet,
|
||||
Block: Block{},
|
||||
},
|
||||
"set_required": &NestedBlock{
|
||||
Nesting: NestingSet,
|
||||
Block: Block{},
|
||||
MinItems: 1,
|
||||
},
|
||||
"map": &NestedBlock{
|
||||
Nesting: NestingMap,
|
||||
Block: Block{},
|
||||
},
|
||||
},
|
||||
},
|
||||
0,
|
||||
},
|
||||
"attribute with no flags set": {
|
||||
&Block{
|
||||
Attributes: map[string]*Attribute{
|
||||
"foo": &Attribute{
|
||||
Type: cty.String,
|
||||
},
|
||||
},
|
||||
},
|
||||
1, // must set one of the flags
|
||||
},
|
||||
"attribute required and optional": {
|
||||
&Block{
|
||||
Attributes: map[string]*Attribute{
|
||||
"foo": &Attribute{
|
||||
Type: cty.String,
|
||||
Required: true,
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
1, // both required and optional
|
||||
},
|
||||
"attribute required and computed": {
|
||||
&Block{
|
||||
Attributes: map[string]*Attribute{
|
||||
"foo": &Attribute{
|
||||
Type: cty.String,
|
||||
Required: true,
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
1, // both required and computed
|
||||
},
|
||||
"attribute optional and computed": {
|
||||
&Block{
|
||||
Attributes: map[string]*Attribute{
|
||||
"foo": &Attribute{
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
0,
|
||||
},
|
||||
"attribute with missing type": {
|
||||
&Block{
|
||||
Attributes: map[string]*Attribute{
|
||||
"foo": &Attribute{
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
1, // Type must be set
|
||||
},
|
||||
"attribute with invalid name": {
|
||||
&Block{
|
||||
Attributes: map[string]*Attribute{
|
||||
"fooBar": &Attribute{
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
1, // name may not contain uppercase letters
|
||||
},
|
||||
"block type with invalid name": {
|
||||
&Block{
|
||||
BlockTypes: map[string]*NestedBlock{
|
||||
"fooBar": &NestedBlock{
|
||||
Nesting: NestingSingle,
|
||||
},
|
||||
},
|
||||
},
|
||||
1, // name may not contain uppercase letters
|
||||
},
|
||||
"colliding names": {
|
||||
&Block{
|
||||
Attributes: map[string]*Attribute{
|
||||
"foo": &Attribute{
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
BlockTypes: map[string]*NestedBlock{
|
||||
"foo": &NestedBlock{
|
||||
Nesting: NestingSingle,
|
||||
},
|
||||
},
|
||||
},
|
||||
1, // "foo" is defined as both attribute and block type
|
||||
},
|
||||
"nested block with badness": {
|
||||
&Block{
|
||||
BlockTypes: map[string]*NestedBlock{
|
||||
"bad": &NestedBlock{
|
||||
Nesting: NestingSingle,
|
||||
Block: Block{
|
||||
Attributes: map[string]*Attribute{
|
||||
"nested_bad": &Attribute{
|
||||
Type: cty.String,
|
||||
Required: true,
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
1, // nested_bad is both required and optional
|
||||
},
|
||||
"nil": {
|
||||
nil,
|
||||
1, // block is nil
|
||||
},
|
||||
"nil attr": {
|
||||
&Block{
|
||||
Attributes: map[string]*Attribute{
|
||||
"bad": nil,
|
||||
},
|
||||
},
|
||||
1, // attribute schema is nil
|
||||
},
|
||||
"nil block type": {
|
||||
&Block{
|
||||
BlockTypes: map[string]*NestedBlock{
|
||||
"bad": nil,
|
||||
},
|
||||
},
|
||||
1, // block schema is nil
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
errs := multierrorErrors(test.Block.InternalValidate())
|
||||
if got, want := len(errs), test.ErrCount; got != want {
|
||||
t.Errorf("wrong number of errors %d; want %d", got, want)
|
||||
for _, err := range errs {
|
||||
t.Logf("- %s", err.Error())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func multierrorErrors(err error) []error {
|
||||
// A function like this should really be part of the multierror package...
|
||||
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch terr := err.(type) {
|
||||
case *multierror.Error:
|
||||
return terr.Errors
|
||||
default:
|
||||
return []error{err}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue