diff --git a/gozcl/schema.go b/gozcl/schema.go new file mode 100644 index 0000000..cbc487b --- /dev/null +++ b/gozcl/schema.go @@ -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 +} diff --git a/gozcl/schema_test.go b/gozcl/schema_test.go new file mode 100644 index 0000000..a5eb992 --- /dev/null +++ b/gozcl/schema_test.go @@ -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, + ) + } + }) + } +}