diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d7a6aa6 --- /dev/null +++ b/go.sum @@ -0,0 +1,23 @@ +code.icb4dc0.de/buildr/api v0.0.0-20230817151157-dbc0adad8f8b h1:pZDHPJCMRw3UM1Jd0y7XnPZaDOzZ5du17EvW09mgb4g= +code.icb4dc0.de/buildr/api v0.0.0-20230817151157-dbc0adad8f8b/go.mod h1:Bxt+fw/9hH7/WESz3asYBIWPr81UlUMEleXJGTqX6ys= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230815205213-6bfd019c3878 h1:lv6/DhyiFFGsmzxbsUUTOkN29II+zeWHxvT8Lpdxsv0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230815205213-6bfd019c3878/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= +google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw= +google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= diff --git a/hcl/tag.go b/hcl/tag.go new file mode 100644 index 0000000..fd7fa81 --- /dev/null +++ b/hcl/tag.go @@ -0,0 +1,71 @@ +package hcl + +import ( + "errors" + "fmt" + "reflect" + "strings" +) + +const ( + ElementTypeAttribute ElementType = "attr" + ElementTypeBlock ElementType = "block" + ElementTypeLabel ElementType = "label" + ElementTypeRemain ElementType = "remain" +) + +var ErrNoHclTag = errors.New("no hcl tag") + +type ElementType string + +type FieldMeta struct { + Name string + Type ElementType + Optional bool + Ignore bool +} + +func ExtractFieldMeta(t reflect.StructField, readTagName, writeTagName string) (FieldMeta, error) { + hclTag, present := t.Tag.Lookup(readTagName) + if !present { + return FieldMeta{}, ErrNoHclTag + } + + cfg := FieldMeta{ + Name: t.Name, + Type: ElementTypeAttribute, + } + + if hclWriteTag, present := t.Tag.Lookup(writeTagName); present && hclWriteTag == "-" { + cfg.Ignore = true + + return cfg, nil + } + + split := strings.Split(hclTag, ",") + + if len(split) > 0 && split[0] != "" { + cfg.Name = split[0] + } + + if len(split) > 1 { + switch split[1] { + case "": + fallthrough + case "attr": + cfg.Type = ElementTypeAttribute + case "optional": + cfg.Optional = true + case "label": + cfg.Type = ElementTypeLabel + case "block": + cfg.Type = ElementTypeBlock + case "remain": + cfg.Type = ElementTypeRemain + default: + return FieldMeta{}, fmt.Errorf("unknown element type %q", split[1]) + } + } + + return cfg, nil +} diff --git a/protocol/config.go b/protocol/config.go new file mode 100644 index 0000000..de33458 --- /dev/null +++ b/protocol/config.go @@ -0,0 +1,26 @@ +package protocol + +var ( + _ marshalConfigOption = WithTagName("hcl") + _ unmarshalConfigOption = WithTagName("hcl") +) + +type WithTagName string + +func (o WithTagName) applyToMarshalConfig(cfg *marshalConfig) { + cfg.TagName = string(o) +} + +func (o WithTagName) applyToUnmarshalConfig(cfg *unmarshalConfig) { + cfg.TagName = string(o) +} + +func defaultConfig() protocolConfig { + return protocolConfig{ + TagName: "hcl", + } +} + +type protocolConfig struct { + TagName string +} diff --git a/protocol/marshal.go b/protocol/marshal.go index cb94401..7cb099a 100644 --- a/protocol/marshal.go +++ b/protocol/marshal.go @@ -11,9 +11,17 @@ import ( var ErrExpectedStruct = errors.New("expected struct") -func Marshal(in any) (*rpcv1.ModuleSpec, error) { +type marshalConfigOption interface { + applyToMarshalConfig(cfg *marshalConfig) +} + +func Marshal(in any, opts ...marshalConfigOption) (*rpcv1.ModuleSpec, error) { cfg := marshalConfig{ - TagName: "hcl", + protocolConfig: defaultConfig(), + } + + for _, opt := range opts { + opt.applyToMarshalConfig(&cfg) } val := reflect.ValueOf(in) @@ -29,7 +37,7 @@ func Marshal(in any) (*rpcv1.ModuleSpec, error) { } type marshalConfig struct { - TagName string + protocolConfig } func (cfg marshalConfig) marshal(in reflect.Value) (*rpcv1.ModuleSpec, error) { @@ -39,8 +47,6 @@ func (cfg marshalConfig) marshal(in reflect.Value) (*rpcv1.ModuleSpec, error) { Values: make(map[string]*rpcv1.ModuleSpec_Value, numField), } - // TODO consider tag for field name - inputType := in.Type() for i := 0; i < numField; i++ { @@ -116,6 +122,7 @@ func (cfg marshalConfig) fieldName(field reflect.StructField) string { func (cfg marshalConfig) mapValue(in reflect.Value) *rpcv1.ModuleSpec_Value { result := &rpcv1.ModuleSpec_Value{ Type: rpcv1.ModuleSpec_ValueTypeMap, + Kind: rpcv1.ModuleSpec_ValueKindAttribute, ComplexValue: make(map[string]*rpcv1.ModuleSpec_Value, in.Len()), } @@ -132,6 +139,7 @@ func (cfg marshalConfig) structValue(in reflect.Value) *rpcv1.ModuleSpec_Value { numFields := inputType.NumField() result := &rpcv1.ModuleSpec_Value{ Type: rpcv1.ModuleSpec_ValueTypeObject, + Kind: rpcv1.ModuleSpec_ValueKindAttribute, ComplexValue: make(map[string]*rpcv1.ModuleSpec_Value, numFields), } @@ -150,6 +158,7 @@ func (cfg marshalConfig) structValue(in reflect.Value) *rpcv1.ModuleSpec_Value { func boolValue(in bool) *rpcv1.ModuleSpec_Value { return &rpcv1.ModuleSpec_Value{ Type: rpcv1.ModuleSpec_ValueTypeSingle, + Kind: rpcv1.ModuleSpec_ValueKindAttribute, SingleValue: &rpcv1.ModuleSpec_Value_BoolValue{ BoolValue: in, }, @@ -159,6 +168,7 @@ func boolValue(in bool) *rpcv1.ModuleSpec_Value { func boolSliceValue(val reflect.Value) *rpcv1.ModuleSpec_Value { result := &rpcv1.ModuleSpec_Value{ Type: rpcv1.ModuleSpec_ValueTypeBoolSlice, + Kind: rpcv1.ModuleSpec_ValueKindAttribute, BoolValues: make([]bool, 0, val.Len()), } @@ -172,6 +182,7 @@ func boolSliceValue(val reflect.Value) *rpcv1.ModuleSpec_Value { func intValue[T Integer](in T) *rpcv1.ModuleSpec_Value { return &rpcv1.ModuleSpec_Value{ Type: rpcv1.ModuleSpec_ValueTypeSingle, + Kind: rpcv1.ModuleSpec_ValueKindAttribute, SingleValue: &rpcv1.ModuleSpec_Value_IntValue{ IntValue: int64(in), }, @@ -181,6 +192,7 @@ func intValue[T Integer](in T) *rpcv1.ModuleSpec_Value { func intSliceValue(val reflect.Value, selector func(v reflect.Value) int64) *rpcv1.ModuleSpec_Value { result := &rpcv1.ModuleSpec_Value{ Type: rpcv1.ModuleSpec_ValueTypeIntSlice, + Kind: rpcv1.ModuleSpec_ValueKindAttribute, IntValues: make([]int64, 0, val.Len()), } @@ -194,6 +206,7 @@ func intSliceValue(val reflect.Value, selector func(v reflect.Value) int64) *rpc func floatValue[T Float](in T) *rpcv1.ModuleSpec_Value { return &rpcv1.ModuleSpec_Value{ Type: rpcv1.ModuleSpec_ValueTypeSingle, + Kind: rpcv1.ModuleSpec_ValueKindAttribute, SingleValue: &rpcv1.ModuleSpec_Value_DoubleValue{ DoubleValue: float64(in), }, @@ -203,6 +216,7 @@ func floatValue[T Float](in T) *rpcv1.ModuleSpec_Value { func floatSliceValue(val reflect.Value) *rpcv1.ModuleSpec_Value { result := &rpcv1.ModuleSpec_Value{ Type: rpcv1.ModuleSpec_ValueTypeDoubleSlice, + Kind: rpcv1.ModuleSpec_ValueKindAttribute, DoubleValues: make([]float64, 0, val.Len()), } @@ -216,6 +230,7 @@ func floatSliceValue(val reflect.Value) *rpcv1.ModuleSpec_Value { func stringValue(in string) *rpcv1.ModuleSpec_Value { return &rpcv1.ModuleSpec_Value{ Type: rpcv1.ModuleSpec_ValueTypeSingle, + Kind: rpcv1.ModuleSpec_ValueKindAttribute, SingleValue: &rpcv1.ModuleSpec_Value_StringValue{ StringValue: in, }, @@ -225,6 +240,7 @@ func stringValue(in string) *rpcv1.ModuleSpec_Value { func stringSliceValue(val reflect.Value) *rpcv1.ModuleSpec_Value { result := &rpcv1.ModuleSpec_Value{ Type: rpcv1.ModuleSpec_ValueTypeStringSlice, + Kind: rpcv1.ModuleSpec_ValueKindAttribute, StringValues: make([]string, 0, val.Len()), } diff --git a/protocol/unmarshal.go b/protocol/unmarshal.go index d1d5275..a1b4948 100644 --- a/protocol/unmarshal.go +++ b/protocol/unmarshal.go @@ -9,16 +9,25 @@ import ( rpcv1 "code.icb4dc0.de/buildr/api/generated/rpc/v1" ) -var ( - ErrUnmatchingType = errors.New("field type does not match wire value") -) +var ErrUnmatchingType = errors.New("field type does not match wire value") -func Unmarshal(input *rpcv1.ModuleSpec, into any) error { +type unmarshalConfigOption interface { + applyToUnmarshalConfig(cfg *unmarshalConfig) +} +func Unmarshal(input *rpcv1.ModuleSpec, into any, opts ...unmarshalConfigOption) error { if u, ok := into.(SpecUnmarshaler); ok { return u.UnmarshalModuleSpec(input) } + cfg := unmarshalConfig{ + protocolConfig: defaultConfig(), + } + + for _, opt := range opts { + opt.applyToUnmarshalConfig(&cfg) + } + val := reflect.ValueOf(into) switch val.Kind() { @@ -26,10 +35,14 @@ func Unmarshal(input *rpcv1.ModuleSpec, into any) error { default: return errors.New("into value must be a pointer") } - return unmarshal(input.Values, val.Elem(), reflect.TypeOf(into).Elem()) + return cfg.unmarshal(input.Values, val.Elem(), reflect.TypeOf(into).Elem()) } -func unmarshal(input map[string]*rpcv1.ModuleSpec_Value, into reflect.Value, intoType reflect.Type) error { +type unmarshalConfig struct { + protocolConfig +} + +func (cfg unmarshalConfig) unmarshal(input map[string]*rpcv1.ModuleSpec_Value, into reflect.Value, intoType reflect.Type) error { for i := 0; i < intoType.NumField(); i++ { tf := intoType.Field(i) if !tf.IsExported() { @@ -57,7 +70,7 @@ func unmarshal(input map[string]*rpcv1.ModuleSpec_Value, into reflect.Value, int } } - if mapped, err := mapSpecValueTo(val, tf.Type); err != nil { + if mapped, err := cfg.mapSpecValueTo(val, tf.Type); err != nil { return err } else if into.Type().Kind() == reflect.Pointer { into.Elem().Field(i).Set(mapped) @@ -69,21 +82,21 @@ func unmarshal(input map[string]*rpcv1.ModuleSpec_Value, into reflect.Value, int return nil } -func mapSpecValueTo(val *rpcv1.ModuleSpec_Value, targetType reflect.Type) (reflect.Value, error) { +func (cfg unmarshalConfig) mapSpecValueTo(val *rpcv1.ModuleSpec_Value, targetType reflect.Type) (reflect.Value, error) { switch val.Type { case rpcv1.ModuleSpec_ValueTypeUnknown: return reflect.Value{}, fmt.Errorf("%w: expected %s", ErrUnmatchingType, targetType.String()) case rpcv1.ModuleSpec_ValueTypeObject: if targetType.Kind() == reflect.Struct { structVal := reflect.New(targetType) - if err := unmarshal(val.ComplexValue, structVal, targetType); err != nil { + if err := cfg.unmarshal(val.ComplexValue, structVal, targetType); err != nil { return reflect.Value{}, err } else { return structVal.Elem(), nil } } else if targetType.Kind() == reflect.Pointer && targetType.Elem().Kind() == reflect.Struct { structVal := reflect.New(targetType.Elem()) - if err := unmarshal(val.ComplexValue, structVal, targetType.Elem()); err != nil { + if err := cfg.unmarshal(val.ComplexValue, structVal, targetType.Elem()); err != nil { return reflect.Value{}, err } else { return structVal, nil @@ -99,7 +112,7 @@ func mapSpecValueTo(val *rpcv1.ModuleSpec_Value, targetType reflect.Type) (refle mapVal := reflect.MakeMap(targetType) for k, v := range val.ComplexValue { - if mappedVal, err := mapSpecValueTo(v, targetType.Elem()); err != nil { + if mappedVal, err := cfg.mapSpecValueTo(v, targetType.Elem()); err != nil { return reflect.Value{}, err } else { mapVal.SetMapIndex(reflect.ValueOf(k), mappedVal)