From 5c7638853b8071cc213d83ce507d3fdbfa21da37 Mon Sep 17 00:00:00 2001 From: NoF0rte Date: Mon, 13 Dec 2021 10:04:14 -0700 Subject: [PATCH 1/2] impliedStructType will create an object with optional fields if there are fields that are pointers --- cty/gocty/type_implied.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cty/gocty/type_implied.go b/cty/gocty/type_implied.go index ce4c8f1e..9d5726c5 100644 --- a/cty/gocty/type_implied.go +++ b/cty/gocty/type_implied.go @@ -86,6 +86,7 @@ func impliedStructType(rt reflect.Type, path cty.Path) (cty.Type, error) { } atys := make(map[string]cty.Type, len(fieldIdxs)) + var optionals []string { // Temporary extension of path for attributes @@ -100,9 +101,13 @@ func impliedStructType(rt reflect.Type, path cty.Path) (cty.Type, error) { return cty.NilType, err } + if ft.Kind() == reflect.Ptr { + optionals = append(optionals, k) + } + atys[k] = aty } } - return cty.Object(atys), nil + return cty.ObjectWithOptionalAttrs(atys, optionals), nil } From d4f73a53601395a3fdadb9d4a4743940955ecdee Mon Sep 17 00:00:00 2001 From: NoF0rte Date: Mon, 13 Dec 2021 18:17:38 -0700 Subject: [PATCH 2/2] Implemented the 'optional' field tag option to allow for optional fields --- cty/gocty/helpers.go | 55 ++++++++++++++++++++++++++++++++++----- cty/gocty/in.go | 6 ++--- cty/gocty/out.go | 14 ++++++---- cty/gocty/type_implied.go | 12 ++++----- 4 files changed, 66 insertions(+), 21 deletions(-) diff --git a/cty/gocty/helpers.go b/cty/gocty/helpers.go index 94ffd2fb..771c6588 100644 --- a/cty/gocty/helpers.go +++ b/cty/gocty/helpers.go @@ -3,6 +3,7 @@ package gocty import ( "math/big" "reflect" + "strings" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/set" @@ -20,22 +21,62 @@ var emptyInterfaceType = reflect.TypeOf(interface{}(nil)) var stringType = reflect.TypeOf("") -// structTagIndices interrogates the fields of the given type (which must +type tagInfo struct { + index int + optional bool +} + +// tagOptions is the string following a comma in a struct field's "cty" +// tag, or the empty string. It does not include the leading comma. +type tagOptions string + +func parseTag(tag string) (string, tagOptions) { + if idx := strings.Index(tag, ","); idx != -1 { + return tag[:idx], tagOptions(tag[idx+1:]) + } + return tag, tagOptions("") +} + +// Contains reports whether a comma-separated list of options +// contains a particular substr flag. substr must be surrounded by a +// string boundary or commas. +func (o tagOptions) Contains(optionName string) bool { + if len(o) == 0 { + return false + } + s := string(o) + for s != "" { + var next string + i := strings.Index(s, ",") + if i >= 0 { + s, next = s[:i], s[i+1:] + } + if s == optionName { + return true + } + s = next + } + return false +} + +// structTagInfo interrogates the fields of the given type (which must // be a struct type, or we'll panic) and returns a map from the cty -// attribute names declared via struct tags to the indices of the -// fields holding those tags. +// attribute names declared via struct tags to the tagInfo // // This function will panic if two fields within the struct are tagged with // the same cty attribute name. -func structTagIndices(st reflect.Type) map[string]int { +func structTagInfo(st reflect.Type) map[string]tagInfo { ct := st.NumField() - ret := make(map[string]int, ct) + ret := make(map[string]tagInfo, ct) for i := 0; i < ct; i++ { field := st.Field(i) - attrName := field.Tag.Get("cty") + attrName, opt := parseTag(field.Tag.Get("cty")) if attrName != "" { - ret[attrName] = i + ret[attrName] = tagInfo{ + index: i, + optional: opt.Contains("optional"), + } } } diff --git a/cty/gocty/in.go b/cty/gocty/in.go index ca9de21d..1e0f7116 100644 --- a/cty/gocty/in.go +++ b/cty/gocty/in.go @@ -355,7 +355,7 @@ func toCtyObject(val reflect.Value, attrTypes map[string]cty.Type, path cty.Path // path to give us a place to put our GetAttr step. path = append(path, cty.PathStep(nil)) - attrFields := structTagIndices(val.Type()) + attrFields := structTagInfo(val.Type()) vals := make(map[string]cty.Value, len(attrTypes)) for k, at := range attrTypes { @@ -363,9 +363,9 @@ func toCtyObject(val reflect.Value, attrTypes map[string]cty.Type, path cty.Path Name: k, } - if fieldIdx, have := attrFields[k]; have { + if tagInfo, have := attrFields[k]; have { var err error - vals[k], err = toCtyValue(val.Field(fieldIdx), at, path) + vals[k], err = toCtyValue(val.Field(tagInfo.index), at, path) if err != nil { return cty.NilVal, err } diff --git a/cty/gocty/out.go b/cty/gocty/out.go index e9c2599e..b942b96d 100644 --- a/cty/gocty/out.go +++ b/cty/gocty/out.go @@ -451,15 +451,19 @@ func fromCtyObject(val cty.Value, target reflect.Value, path cty.Path) error { case reflect.Struct: attrTypes := val.Type().AttributeTypes() - targetFields := structTagIndices(target.Type()) + targetFields := structTagInfo(target.Type()) path = append(path, nil) - for k, i := range targetFields { + for k, info := range targetFields { if _, exists := attrTypes[k]; !exists { + if info.optional { + continue + } + // If the field in question isn't able to represent nil, // that's an error. - fk := target.Field(i).Kind() + fk := target.Field(info.index).Kind() switch fk { case reflect.Ptr, reflect.Slice, reflect.Map, reflect.Interface: // okay @@ -474,14 +478,14 @@ func fromCtyObject(val cty.Value, target reflect.Value, path cty.Path) error { Name: k, } - fieldIdx, exists := targetFields[k] + fieldInfo, exists := targetFields[k] if !exists { return path.NewErrorf("unsupported attribute %q", k) } ev := val.GetAttr(k) - targetField := target.Field(fieldIdx) + targetField := target.Field(fieldInfo.index) err := fromCtyValue(ev, targetField, path) if err != nil { return err diff --git a/cty/gocty/type_implied.go b/cty/gocty/type_implied.go index 9d5726c5..a2eaaf92 100644 --- a/cty/gocty/type_implied.go +++ b/cty/gocty/type_implied.go @@ -80,28 +80,28 @@ func impliedStructType(rt reflect.Type, path cty.Path) (cty.Type, error) { return cty.DynamicPseudoType, nil } - fieldIdxs := structTagIndices(rt) - if len(fieldIdxs) == 0 { + fieldInfos := structTagInfo(rt) + if len(fieldInfos) == 0 { return cty.NilType, path.NewErrorf("no cty.Type for %s (no cty field tags)", rt) } - atys := make(map[string]cty.Type, len(fieldIdxs)) + atys := make(map[string]cty.Type, len(fieldInfos)) var optionals []string { // Temporary extension of path for attributes path := append(path, nil) - for k, fi := range fieldIdxs { + for k, info := range fieldInfos { path[len(path)-1] = cty.GetAttrStep{Name: k} - ft := rt.Field(fi).Type + ft := rt.Field(info.index).Type aty, err := impliedType(ft, path) if err != nil { return cty.NilType, err } - if ft.Kind() == reflect.Ptr { + if info.optional { optionals = append(optionals, k) }