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

Add optional field tag option #120

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
55 changes: 48 additions & 7 deletions cty/gocty/helpers.go
Expand Up @@ -3,6 +3,7 @@ package gocty
import (
"math/big"
"reflect"
"strings"

"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/set"
Expand All @@ -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"),
}
}
}

Expand Down
6 changes: 3 additions & 3 deletions cty/gocty/in.go
Expand Up @@ -355,17 +355,17 @@ 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 {
path[len(path)-1] = cty.GetAttrStep{
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
}
Expand Down
14 changes: 9 additions & 5 deletions cty/gocty/out.go
Expand Up @@ -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
Expand All @@ -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
Expand Down
17 changes: 11 additions & 6 deletions cty/gocty/type_implied.go
Expand Up @@ -80,29 +80,34 @@ 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 info.optional {
optionals = append(optionals, k)
}

atys[k] = aty
}
}

return cty.Object(atys), nil
return cty.ObjectWithOptionalAttrs(atys, optionals), nil
}