Merge pull request #3018 from apparentlymart/auto-set-hash
helper/schema: Default hashing function for sets
This commit is contained in:
commit
085dde5926
|
@ -38,15 +38,7 @@ func (r *FieldReadResult) ValueOrZero(s *Schema) interface{} {
|
||||||
return r.Value
|
return r.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
result := s.Type.Zero()
|
return s.ZeroValue()
|
||||||
|
|
||||||
// The zero value of a set is nil, but we want it
|
|
||||||
// to actually be an empty set object...
|
|
||||||
if set, ok := result.(*Set); ok && set.F == nil {
|
|
||||||
set.F = s.Set
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// addrToSchema finds the final element schema for the given address
|
// addrToSchema finds the final element schema for the given address
|
||||||
|
|
|
@ -201,7 +201,7 @@ func (r *ConfigFieldReader) readSet(
|
||||||
address []string, schema *Schema) (FieldReadResult, map[int]int, error) {
|
address []string, schema *Schema) (FieldReadResult, map[int]int, error) {
|
||||||
indexMap := make(map[int]int)
|
indexMap := make(map[int]int)
|
||||||
// Create the set that will be our result
|
// Create the set that will be our result
|
||||||
set := &Set{F: schema.Set}
|
set := schema.ZeroValue().(*Set)
|
||||||
|
|
||||||
raw, err := readListField(&nestedConfigFieldReader{r}, address, schema)
|
raw, err := readListField(&nestedConfigFieldReader{r}, address, schema)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -141,7 +141,7 @@ func (r *DiffFieldReader) readSet(
|
||||||
prefix := strings.Join(address, ".") + "."
|
prefix := strings.Join(address, ".") + "."
|
||||||
|
|
||||||
// Create the set that will be our result
|
// Create the set that will be our result
|
||||||
set := &Set{F: schema.Set}
|
set := schema.ZeroValue().(*Set)
|
||||||
|
|
||||||
// Go through the map and find all the set items
|
// Go through the map and find all the set items
|
||||||
for k, d := range r.Diff.Attributes {
|
for k, d := range r.Diff.Attributes {
|
||||||
|
|
|
@ -105,7 +105,7 @@ func (r *MapFieldReader) readSet(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the set that will be our result
|
// Create the set that will be our result
|
||||||
set := &Set{F: schema.Set}
|
set := schema.ZeroValue().(*Set)
|
||||||
|
|
||||||
// If we have an empty list, then return an empty list
|
// If we have an empty list, then return an empty list
|
||||||
if countRaw.Computed || countRaw.Value.(int) == 0 {
|
if countRaw.Computed || countRaw.Value.(int) == 0 {
|
||||||
|
|
|
@ -207,6 +207,30 @@ func (s *Schema) DefaultValue() (interface{}, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns a zero value for the schema.
|
||||||
|
func (s *Schema) ZeroValue() interface{} {
|
||||||
|
// If it's a set then we'll do a bit of extra work to provide the
|
||||||
|
// right hashing function in our empty value.
|
||||||
|
if s.Type == TypeSet {
|
||||||
|
setFunc := s.Set
|
||||||
|
if setFunc == nil {
|
||||||
|
// Default set function uses the schema to hash the whole value
|
||||||
|
elem := s.Elem
|
||||||
|
switch t := elem.(type) {
|
||||||
|
case *Schema:
|
||||||
|
setFunc = HashSchema(t)
|
||||||
|
case *Resource:
|
||||||
|
setFunc = HashResource(t)
|
||||||
|
default:
|
||||||
|
panic("invalid set element type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &Set{F: setFunc}
|
||||||
|
} else {
|
||||||
|
return s.Type.Zero()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Schema) finalizeDiff(
|
func (s *Schema) finalizeDiff(
|
||||||
d *terraform.ResourceAttrDiff) *terraform.ResourceAttrDiff {
|
d *terraform.ResourceAttrDiff) *terraform.ResourceAttrDiff {
|
||||||
if d == nil {
|
if d == nil {
|
||||||
|
@ -496,10 +520,8 @@ func (m schemaMap) InternalValidate(topSchemaMap schemaMap) error {
|
||||||
return fmt.Errorf("%s: Default is not valid for lists or sets", k)
|
return fmt.Errorf("%s: Default is not valid for lists or sets", k)
|
||||||
}
|
}
|
||||||
|
|
||||||
if v.Type == TypeList && v.Set != nil {
|
if v.Type != TypeSet && v.Set != nil {
|
||||||
return fmt.Errorf("%s: Set can only be set for TypeSet", k)
|
return fmt.Errorf("%s: Set can only be set for TypeSet", k)
|
||||||
} else if v.Type == TypeSet && v.Set == nil {
|
|
||||||
return fmt.Errorf("%s: Set must be set", k)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch t := v.Elem.(type) {
|
switch t := v.Elem.(type) {
|
||||||
|
@ -782,10 +804,10 @@ func (m schemaMap) diffSet(
|
||||||
}
|
}
|
||||||
|
|
||||||
if o == nil {
|
if o == nil {
|
||||||
o = &Set{F: schema.Set}
|
o = schema.ZeroValue().(*Set)
|
||||||
}
|
}
|
||||||
if n == nil {
|
if n == nil {
|
||||||
n = &Set{F: schema.Set}
|
n = schema.ZeroValue().(*Set)
|
||||||
}
|
}
|
||||||
os := o.(*Set)
|
os := o.(*Set)
|
||||||
ns := n.(*Set)
|
ns := n.(*Set)
|
||||||
|
|
|
@ -2789,7 +2789,7 @@ func TestSchemaMap_InternalValidate(t *testing.T) {
|
||||||
Optional: true,
|
Optional: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
true,
|
false,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Required but computed
|
// Required but computed
|
||||||
|
|
|
@ -0,0 +1,105 @@
|
||||||
|
package schema
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SerializeValueForHash(buf *bytes.Buffer, val interface{}, schema *Schema) {
|
||||||
|
if val == nil {
|
||||||
|
buf.WriteRune(';')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch schema.Type {
|
||||||
|
case TypeBool:
|
||||||
|
if val.(bool) {
|
||||||
|
buf.WriteRune('1')
|
||||||
|
} else {
|
||||||
|
buf.WriteRune('0')
|
||||||
|
}
|
||||||
|
case TypeInt:
|
||||||
|
buf.WriteString(strconv.Itoa(val.(int)))
|
||||||
|
case TypeFloat:
|
||||||
|
buf.WriteString(strconv.FormatFloat(val.(float64), 'g', -1, 64))
|
||||||
|
case TypeString:
|
||||||
|
buf.WriteString(val.(string))
|
||||||
|
case TypeList:
|
||||||
|
buf.WriteRune('(')
|
||||||
|
l := val.([]interface{})
|
||||||
|
for _, innerVal := range l {
|
||||||
|
serializeCollectionMemberForHash(buf, innerVal, schema.Elem)
|
||||||
|
}
|
||||||
|
buf.WriteRune(')')
|
||||||
|
case TypeMap:
|
||||||
|
m := val.(map[string]interface{})
|
||||||
|
var keys []string
|
||||||
|
for k := range m {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
buf.WriteRune('[')
|
||||||
|
for _, k := range keys {
|
||||||
|
innerVal := m[k]
|
||||||
|
buf.WriteString(k)
|
||||||
|
buf.WriteRune(':')
|
||||||
|
serializeCollectionMemberForHash(buf, innerVal, schema.Elem)
|
||||||
|
}
|
||||||
|
buf.WriteRune(']')
|
||||||
|
case TypeSet:
|
||||||
|
buf.WriteRune('{')
|
||||||
|
s := val.(*Set)
|
||||||
|
for _, innerVal := range s.List() {
|
||||||
|
serializeCollectionMemberForHash(buf, innerVal, schema.Elem)
|
||||||
|
}
|
||||||
|
buf.WriteRune('}')
|
||||||
|
default:
|
||||||
|
panic("unknown schema type to serialize")
|
||||||
|
}
|
||||||
|
buf.WriteRune(';')
|
||||||
|
}
|
||||||
|
|
||||||
|
// SerializeValueForHash appends a serialization of the given resource config
|
||||||
|
// to the given buffer, guaranteeing deterministic results given the same value
|
||||||
|
// and schema.
|
||||||
|
//
|
||||||
|
// Its primary purpose is as input into a hashing function in order
|
||||||
|
// to hash complex substructures when used in sets, and so the serialization
|
||||||
|
// is not reversible.
|
||||||
|
func SerializeResourceForHash(buf *bytes.Buffer, val interface{}, resource *Resource) {
|
||||||
|
sm := resource.Schema
|
||||||
|
m := val.(map[string]interface{})
|
||||||
|
var keys []string
|
||||||
|
for k := range sm {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
for _, k := range keys {
|
||||||
|
innerSchema := sm[k]
|
||||||
|
// Skip attributes that are not user-provided. Computed attributes
|
||||||
|
// do not contribute to the hash since their ultimate value cannot
|
||||||
|
// be known at plan/diff time.
|
||||||
|
if !(innerSchema.Required || innerSchema.Optional) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.WriteString(k)
|
||||||
|
buf.WriteRune(':')
|
||||||
|
innerVal := m[k]
|
||||||
|
SerializeValueForHash(buf, innerVal, innerSchema)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func serializeCollectionMemberForHash(buf *bytes.Buffer, val interface{}, elem interface{}) {
|
||||||
|
switch tElem := elem.(type) {
|
||||||
|
case *Schema:
|
||||||
|
SerializeValueForHash(buf, val, tElem)
|
||||||
|
case *Resource:
|
||||||
|
buf.WriteRune('<')
|
||||||
|
SerializeResourceForHash(buf, val, tElem)
|
||||||
|
buf.WriteString(">;")
|
||||||
|
default:
|
||||||
|
panic("invalid element type")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,214 @@
|
||||||
|
package schema
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSerializeForHash(t *testing.T) {
|
||||||
|
type testCase struct {
|
||||||
|
Schema interface{}
|
||||||
|
Value interface{}
|
||||||
|
Expected string
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []testCase{
|
||||||
|
|
||||||
|
testCase{
|
||||||
|
Schema: &Schema{
|
||||||
|
Type: TypeInt,
|
||||||
|
},
|
||||||
|
Value: 0,
|
||||||
|
Expected: "0;",
|
||||||
|
},
|
||||||
|
|
||||||
|
testCase{
|
||||||
|
Schema: &Schema{
|
||||||
|
Type: TypeInt,
|
||||||
|
},
|
||||||
|
Value: 200,
|
||||||
|
Expected: "200;",
|
||||||
|
},
|
||||||
|
|
||||||
|
testCase{
|
||||||
|
Schema: &Schema{
|
||||||
|
Type: TypeBool,
|
||||||
|
},
|
||||||
|
Value: true,
|
||||||
|
Expected: "1;",
|
||||||
|
},
|
||||||
|
|
||||||
|
testCase{
|
||||||
|
Schema: &Schema{
|
||||||
|
Type: TypeBool,
|
||||||
|
},
|
||||||
|
Value: false,
|
||||||
|
Expected: "0;",
|
||||||
|
},
|
||||||
|
|
||||||
|
testCase{
|
||||||
|
Schema: &Schema{
|
||||||
|
Type: TypeFloat,
|
||||||
|
},
|
||||||
|
Value: 1.0,
|
||||||
|
Expected: "1;",
|
||||||
|
},
|
||||||
|
|
||||||
|
testCase{
|
||||||
|
Schema: &Schema{
|
||||||
|
Type: TypeFloat,
|
||||||
|
},
|
||||||
|
Value: 1.54,
|
||||||
|
Expected: "1.54;",
|
||||||
|
},
|
||||||
|
|
||||||
|
testCase{
|
||||||
|
Schema: &Schema{
|
||||||
|
Type: TypeFloat,
|
||||||
|
},
|
||||||
|
Value: 0.1,
|
||||||
|
Expected: "0.1;",
|
||||||
|
},
|
||||||
|
|
||||||
|
testCase{
|
||||||
|
Schema: &Schema{
|
||||||
|
Type: TypeString,
|
||||||
|
},
|
||||||
|
Value: "hello",
|
||||||
|
Expected: "hello;",
|
||||||
|
},
|
||||||
|
|
||||||
|
testCase{
|
||||||
|
Schema: &Schema{
|
||||||
|
Type: TypeString,
|
||||||
|
},
|
||||||
|
Value: "1",
|
||||||
|
Expected: "1;",
|
||||||
|
},
|
||||||
|
|
||||||
|
testCase{
|
||||||
|
Schema: &Schema{
|
||||||
|
Type: TypeList,
|
||||||
|
Elem: &Schema{
|
||||||
|
Type: TypeString,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Value: []interface{}{},
|
||||||
|
Expected: "();",
|
||||||
|
},
|
||||||
|
|
||||||
|
testCase{
|
||||||
|
Schema: &Schema{
|
||||||
|
Type: TypeList,
|
||||||
|
Elem: &Schema{
|
||||||
|
Type: TypeString,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Value: []interface{}{"hello", "world"},
|
||||||
|
Expected: "(hello;world;);",
|
||||||
|
},
|
||||||
|
|
||||||
|
testCase{
|
||||||
|
Schema: &Schema{
|
||||||
|
Type: TypeList,
|
||||||
|
Elem: &Resource{
|
||||||
|
Schema: map[string]*Schema{
|
||||||
|
"fo": &Schema{
|
||||||
|
Type: TypeString,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"fum": &Schema{
|
||||||
|
Type: TypeString,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Value: []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"fo": "bar",
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"fo": "baz",
|
||||||
|
"fum": "boz",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Expected: "(<fo:bar;fum:;>;<fo:baz;fum:boz;>;);",
|
||||||
|
},
|
||||||
|
|
||||||
|
testCase{
|
||||||
|
Schema: &Schema{
|
||||||
|
Type: TypeSet,
|
||||||
|
Elem: &Schema{
|
||||||
|
Type: TypeString,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Value: NewSet(func(i interface{}) int { return len(i.(string)) }, []interface{}{
|
||||||
|
"hello",
|
||||||
|
"woo",
|
||||||
|
}),
|
||||||
|
Expected: "{woo;hello;};",
|
||||||
|
},
|
||||||
|
|
||||||
|
testCase{
|
||||||
|
Schema: &Schema{
|
||||||
|
Type: TypeMap,
|
||||||
|
Elem: &Schema{
|
||||||
|
Type: TypeString,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Value: map[string]interface{}{
|
||||||
|
"foo": "bar",
|
||||||
|
"baz": "foo",
|
||||||
|
},
|
||||||
|
Expected: "[baz:foo;foo:bar;];",
|
||||||
|
},
|
||||||
|
|
||||||
|
testCase{
|
||||||
|
Schema: &Resource{
|
||||||
|
Schema: map[string]*Schema{
|
||||||
|
"name": &Schema{
|
||||||
|
Type: TypeString,
|
||||||
|
Required: true,
|
||||||
|
},
|
||||||
|
"size": &Schema{
|
||||||
|
Type: TypeInt,
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
"green": &Schema{
|
||||||
|
Type: TypeBool,
|
||||||
|
Optional: true,
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
"upside_down": &Schema{
|
||||||
|
Type: TypeBool,
|
||||||
|
Computed: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Value: map[string]interface{}{
|
||||||
|
"name": "my-fun-database",
|
||||||
|
"size": 12,
|
||||||
|
"green": true,
|
||||||
|
},
|
||||||
|
Expected: "green:1;name:my-fun-database;size:12;",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
var gotBuf bytes.Buffer
|
||||||
|
schema := test.Schema
|
||||||
|
|
||||||
|
switch s := schema.(type) {
|
||||||
|
case *Schema:
|
||||||
|
SerializeValueForHash(&gotBuf, test.Value, s)
|
||||||
|
case *Resource:
|
||||||
|
SerializeResourceForHash(&gotBuf, test.Value, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
got := gotBuf.String()
|
||||||
|
if got != test.Expected {
|
||||||
|
t.Errorf("hash(%#v) got %#v, but want %#v", test.Value, got, test.Expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package schema
|
package schema
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
|
@ -15,6 +16,28 @@ func HashString(v interface{}) int {
|
||||||
return hashcode.String(v.(string))
|
return hashcode.String(v.(string))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HashResource hashes complex structures that are described using
|
||||||
|
// a *Resource. This is the default set implementation used when a set's
|
||||||
|
// element type is a full resource.
|
||||||
|
func HashResource(resource *Resource) SchemaSetFunc {
|
||||||
|
return func(v interface{}) int {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
SerializeResourceForHash(&buf, v, resource)
|
||||||
|
return hashcode.String(buf.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HashSchema hashes values that are described using a *Schema. This is the
|
||||||
|
// default set implementation used when a set's element type is a single
|
||||||
|
// schema.
|
||||||
|
func HashSchema(schema *Schema) SchemaSetFunc {
|
||||||
|
return func(v interface{}) int {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
SerializeValueForHash(&buf, v, schema)
|
||||||
|
return hashcode.String(buf.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Set is a set data structure that is returned for elements of type
|
// Set is a set data structure that is returned for elements of type
|
||||||
// TypeSet.
|
// TypeSet.
|
||||||
type Set struct {
|
type Set struct {
|
||||||
|
|
Loading…
Reference in New Issue