gozcl: ImpliedBodySchema derives a body schema from a Go struct

This commit is contained in:
Martin Atkins 2017-05-20 16:47:14 -07:00
parent 0612bb9843
commit 17cf497b6e
2 changed files with 355 additions and 0 deletions

153
gozcl/schema.go Normal file
View File

@ -0,0 +1,153 @@
package gozcl
import (
"fmt"
"reflect"
"sort"
"strings"
"github.com/apparentlymart/go-zcl/zcl"
)
// ImpliedBodySchema produces a zcl.BodySchema derived from the type of the
// given value, which must be a struct value or a pointer to one. If an
// inappropriate value is passed, this function will panic.
//
// The second return argument indicates whether the given struct includes
// a "remain" field, and thus the returned schema is non-exhaustive.
//
// This uses the tags on the fields of the struct to discover how each
// field's value should be expressed within configuration. If an invalid
// mapping is attempted, this function will panic.
func ImpliedBodySchema(val interface{}) (schema *zcl.BodySchema, partial bool) {
ty := reflect.TypeOf(val)
if ty.Kind() == reflect.Ptr {
ty = ty.Elem()
}
if ty.Kind() != reflect.Struct {
panic(fmt.Sprintf("given value must be struct, not %T", val))
}
var attrSchemas []zcl.AttributeSchema
var blockSchemas []zcl.BlockHeaderSchema
tags := getFieldTags(ty)
attrNames := make([]string, 0, len(tags.Attributes))
for n := range tags.Attributes {
attrNames = append(attrNames, n)
}
sort.Strings(attrNames)
for _, n := range attrNames {
idx := tags.Attributes[n]
field := ty.Field(idx)
attrSchemas = append(attrSchemas, zcl.AttributeSchema{
Name: n,
Required: field.Type.Kind() != reflect.Ptr,
})
}
blockNames := make([]string, 0, len(tags.Blocks))
for n := range tags.Blocks {
blockNames = append(blockNames, n)
}
sort.Strings(blockNames)
for _, n := range blockNames {
idx := tags.Blocks[n]
field := ty.Field(idx)
fty := field.Type
if fty.Kind() == reflect.Slice {
fty = fty.Elem()
}
if fty.Kind() == reflect.Ptr {
fty = fty.Elem()
}
if fty.Kind() != reflect.Struct {
panic(fmt.Sprintf(
"zcl 'block' tag kind cannot be applied to %s field %s: struct required", field.Type.String(), field.Name,
))
}
ftags := getFieldTags(fty)
var labelNames []string
if len(ftags.Labels) > 0 {
labelNames = make([]string, len(ftags.Labels))
for i, l := range ftags.Labels {
labelNames[i] = l.Name
}
}
blockSchemas = append(blockSchemas, zcl.BlockHeaderSchema{
Type: n,
LabelNames: labelNames,
})
}
partial = tags.Remain != nil
schema = &zcl.BodySchema{
Attributes: attrSchemas,
Blocks: blockSchemas,
}
return schema, partial
}
type fieldTags struct {
Attributes map[string]int
Blocks map[string]int
Labels []labelField
Remain *int
}
type labelField struct {
FieldIndex int
Name string
}
func getFieldTags(ty reflect.Type) *fieldTags {
ret := &fieldTags{
Attributes: map[string]int{},
Blocks: map[string]int{},
}
ct := ty.NumField()
for i := 0; i < ct; i++ {
field := ty.Field(i)
tag := field.Tag.Get("zcl")
if tag == "" {
continue
}
comma := strings.Index(tag, ",")
var name, kind string
if comma != -1 {
name = tag[:comma]
kind = tag[comma+1:]
} else {
name = tag
kind = "attr"
}
switch kind {
case "attr":
ret.Attributes[name] = i
case "block":
ret.Blocks[name] = i
case "label":
ret.Labels = append(ret.Labels, labelField{
FieldIndex: i,
Name: name,
})
case "remain":
if ret.Remain != nil {
panic("only one 'remain' tag is permitted")
}
idx := i // copy, because this loop will continue assigning to i
ret.Remain = &idx
default:
panic(fmt.Sprintf("invalid zcl field tag kind %q on %s %q", kind, field.Type.String(), field.Name))
}
}
return ret
}

202
gozcl/schema_test.go Normal file
View File

@ -0,0 +1,202 @@
package gozcl
import (
"fmt"
"reflect"
"testing"
"github.com/apparentlymart/go-zcl/zcl"
"github.com/davecgh/go-spew/spew"
)
func TestImpliedBodySchema(t *testing.T) {
tests := []struct {
val interface{}
wantSchema *zcl.BodySchema
wantPartial bool
}{
{
struct{}{},
&zcl.BodySchema{},
false,
},
{
struct {
Ignored bool
}{},
&zcl.BodySchema{},
false,
},
{
struct {
Attr1 bool `zcl:"attr1"`
Attr2 bool `zcl:"attr2"`
}{},
&zcl.BodySchema{
Attributes: []zcl.AttributeSchema{
{
Name: "attr1",
Required: true,
},
{
Name: "attr2",
Required: true,
},
},
},
false,
},
{
struct {
Attr *bool `zcl:"attr,attr"`
}{},
&zcl.BodySchema{
Attributes: []zcl.AttributeSchema{
{
Name: "attr",
Required: false,
},
},
},
false,
},
{
struct {
Thing struct{} `zcl:"thing,block"`
}{},
&zcl.BodySchema{
Blocks: []zcl.BlockHeaderSchema{
{
Type: "thing",
},
},
},
false,
},
{
struct {
Thing struct {
Type string `zcl:"type,label"`
Name string `zcl:"name,label"`
} `zcl:"thing,block"`
}{},
&zcl.BodySchema{
Blocks: []zcl.BlockHeaderSchema{
{
Type: "thing",
LabelNames: []string{"type", "name"},
},
},
},
false,
},
{
struct {
Thing []struct {
Type string `zcl:"type,label"`
Name string `zcl:"name,label"`
} `zcl:"thing,block"`
}{},
&zcl.BodySchema{
Blocks: []zcl.BlockHeaderSchema{
{
Type: "thing",
LabelNames: []string{"type", "name"},
},
},
},
false,
},
{
struct {
Thing *struct {
Type string `zcl:"type,label"`
Name string `zcl:"name,label"`
} `zcl:"thing,block"`
}{},
&zcl.BodySchema{
Blocks: []zcl.BlockHeaderSchema{
{
Type: "thing",
LabelNames: []string{"type", "name"},
},
},
},
false,
},
{
struct {
Thing struct {
Name string `zcl:"name,label"`
Something string `zcl:"something"`
} `zcl:"thing,block"`
}{},
&zcl.BodySchema{
Blocks: []zcl.BlockHeaderSchema{
{
Type: "thing",
LabelNames: []string{"name"},
},
},
},
false,
},
{
struct {
Doodad string `zcl:"doodad"`
Thing struct {
Name string `zcl:"name,label"`
} `zcl:"thing,block"`
}{},
&zcl.BodySchema{
Attributes: []zcl.AttributeSchema{
{
Name: "doodad",
Required: true,
},
},
Blocks: []zcl.BlockHeaderSchema{
{
Type: "thing",
LabelNames: []string{"name"},
},
},
},
false,
},
{
struct {
Doodad string `zcl:"doodad"`
Config string `zcl:",remain"`
}{},
&zcl.BodySchema{
Attributes: []zcl.AttributeSchema{
{
Name: "doodad",
Required: true,
},
},
},
true,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%#v", test.val), func(t *testing.T) {
schema, partial := ImpliedBodySchema(test.val)
if !reflect.DeepEqual(schema, test.wantSchema) {
t.Errorf(
"wrong schema\ngot: %s\nwant: %s",
spew.Sdump(schema), spew.Sdump(test.wantSchema),
)
}
if partial != test.wantPartial {
t.Errorf(
"wrong partial flag\ngot: %#v\nwant: %#v",
partial, test.wantPartial,
)
}
})
}
}