From 4d62828fa6981c580d7060cb5fa91a72cdd4eebb Mon Sep 17 00:00:00 2001 From: Peter Kurfer Date: Fri, 18 Aug 2023 10:57:35 +0200 Subject: [PATCH] feat: initial code migration --- .editorconfig | 27 ++ go.mod | 17 ++ protocol/api.go | 11 + protocol/constraints.go | 21 ++ protocol/marshal.go | 236 +++++++++++++++++ protocol/marshal_test.go | 220 ++++++++++++++++ protocol/unmarshal.go | 193 ++++++++++++++ protocol/unmarshal_test.go | 515 +++++++++++++++++++++++++++++++++++++ renovate.json | 18 ++ 9 files changed, 1258 insertions(+) create mode 100644 .editorconfig create mode 100644 go.mod create mode 100644 protocol/api.go create mode 100644 protocol/constraints.go create mode 100644 protocol/marshal.go create mode 100644 protocol/marshal_test.go create mode 100644 protocol/unmarshal.go create mode 100644 protocol/unmarshal_test.go create mode 100644 renovate.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..2906418 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,27 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +tab_width = 4 +indent_style = space +insert_final_newline = false +max_line_length = 120 +trim_trailing_whitespace = true + +[*.go] +indent_style = tab +ij_smart_tabs = true +ij_go_GROUP_CURRENT_PROJECT_IMPORTS = true +ij_go_group_stdlib_imports = true +ij_go_import_sorting = goimports +ij_go_local_group_mode = project +ij_go_move_all_imports_in_one_declaration = true +ij_go_move_all_stdlib_imports_in_one_group = true +ij_go_remove_redundant_import_aliases = true + +[*.{yml,yaml}] +indent_size = 2 +tab_width = 2 +insert_final_newline = true \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..db9b789 --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module code.icb4dc0.de/buildr/common + +go 1.21 + +toolchain go1.21.0 + +require code.icb4dc0.de/buildr/api v0.0.0-20230817151157-dbc0adad8f8b + +require ( + github.com/golang/protobuf v1.5.3 // indirect + golang.org/x/net v0.14.0 // indirect + golang.org/x/sys v0.11.0 // indirect + golang.org/x/text v0.12.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230815205213-6bfd019c3878 // indirect + google.golang.org/grpc v1.57.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect +) diff --git a/protocol/api.go b/protocol/api.go new file mode 100644 index 0000000..8275dbf --- /dev/null +++ b/protocol/api.go @@ -0,0 +1,11 @@ +package protocol + +import rpcv1 "code.icb4dc0.de/buildr/api/generated/rpc/v1" + +type SpecUnmarshaler interface { + UnmarshalModuleSpec(val *rpcv1.ModuleSpec) error +} + +type SpecValueUnmarshaler interface { + UnmarshalSpecValue(val *rpcv1.ModuleSpec_Value) error +} diff --git a/protocol/constraints.go b/protocol/constraints.go new file mode 100644 index 0000000..b715a7c --- /dev/null +++ b/protocol/constraints.go @@ -0,0 +1,21 @@ +package protocol + +type Signed interface { + ~int | ~int8 | ~int16 | ~int32 | ~int64 +} + +type Unsigned interface { + ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr +} + +type Integer interface { + Signed | Unsigned +} + +type Float interface { + ~float32 | ~float64 +} + +type Numeric interface { + Integer | Float +} diff --git a/protocol/marshal.go b/protocol/marshal.go new file mode 100644 index 0000000..cb94401 --- /dev/null +++ b/protocol/marshal.go @@ -0,0 +1,236 @@ +package protocol + +import ( + "errors" + "fmt" + "reflect" + "strings" + + rpcv1 "code.icb4dc0.de/buildr/api/generated/rpc/v1" +) + +var ErrExpectedStruct = errors.New("expected struct") + +func Marshal(in any) (*rpcv1.ModuleSpec, error) { + cfg := marshalConfig{ + TagName: "hcl", + } + + val := reflect.ValueOf(in) + if val.Kind() == reflect.Ptr { + val = val.Elem() + } + + if val.Kind() != reflect.Struct { + return nil, fmt.Errorf("%w: got %T", ErrExpectedStruct, in) + } + + return cfg.marshal(val) +} + +type marshalConfig struct { + TagName string +} + +func (cfg marshalConfig) marshal(in reflect.Value) (*rpcv1.ModuleSpec, error) { + numField := in.NumField() + + out := &rpcv1.ModuleSpec{ + Values: make(map[string]*rpcv1.ModuleSpec_Value, numField), + } + + // TODO consider tag for field name + + inputType := in.Type() + + for i := 0; i < numField; i++ { + structField := inputType.Field(i) + if !structField.IsExported() { + continue + } + + fieldName := cfg.fieldName(structField) + + out.Values[fieldName] = cfg.mapReflectValueToSpecValue(in.Field(i)) + } + + return out, nil +} + +func (cfg marshalConfig) mapReflectValueToSpecValue(in reflect.Value) *rpcv1.ModuleSpec_Value { + switch in.Kind() { + case reflect.Map: + return cfg.mapValue(in) + case reflect.Bool: + return boolValue(in.Bool()) + case reflect.String: + return stringValue(in.String()) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return intValue(in.Int()) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return intValue(in.Uint()) + case reflect.Float32, reflect.Float64: + return floatValue(in.Float()) + case reflect.Struct: + return cfg.structValue(in) + case reflect.Slice, reflect.Array: + switch in.Type().Elem().Kind() { + case reflect.Bool: + return boolSliceValue(in) + case reflect.String: + return stringSliceValue(in) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return intSliceValue(in, reflect.Value.Int) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return intSliceValue(in, func(v reflect.Value) int64 { + return int64(v.Uint()) + }) + case reflect.Float32, reflect.Float64: + return floatSliceValue(in) + default: + return nil + } + default: + return nil + } +} + +func (cfg marshalConfig) fieldName(field reflect.StructField) string { + tagVal, ok := field.Tag.Lookup(cfg.TagName) + if !ok { + return field.Name + } + + split := strings.Split(tagVal, ",") + + switch { + case len(split) == 0: + fallthrough + case split[0] == "": + return strings.ToLower(field.Name) + default: + return strings.ToLower(split[0]) + } +} + +func (cfg marshalConfig) mapValue(in reflect.Value) *rpcv1.ModuleSpec_Value { + result := &rpcv1.ModuleSpec_Value{ + Type: rpcv1.ModuleSpec_ValueTypeMap, + ComplexValue: make(map[string]*rpcv1.ModuleSpec_Value, in.Len()), + } + + iter := in.MapRange() + for iter.Next() { + result.ComplexValue[iter.Key().String()] = cfg.mapReflectValueToSpecValue(iter.Value()) + } + + return result +} + +func (cfg marshalConfig) structValue(in reflect.Value) *rpcv1.ModuleSpec_Value { + inputType := in.Type() + numFields := inputType.NumField() + result := &rpcv1.ModuleSpec_Value{ + Type: rpcv1.ModuleSpec_ValueTypeObject, + ComplexValue: make(map[string]*rpcv1.ModuleSpec_Value, numFields), + } + + for i := 0; i < numFields; i++ { + structField := inputType.Field(i) + if !structField.IsExported() { + continue + } + + result.ComplexValue[structField.Name] = cfg.mapReflectValueToSpecValue(in.Field(i)) + } + + return result +} + +func boolValue(in bool) *rpcv1.ModuleSpec_Value { + return &rpcv1.ModuleSpec_Value{ + Type: rpcv1.ModuleSpec_ValueTypeSingle, + SingleValue: &rpcv1.ModuleSpec_Value_BoolValue{ + BoolValue: in, + }, + } +} + +func boolSliceValue(val reflect.Value) *rpcv1.ModuleSpec_Value { + result := &rpcv1.ModuleSpec_Value{ + Type: rpcv1.ModuleSpec_ValueTypeBoolSlice, + BoolValues: make([]bool, 0, val.Len()), + } + + for i := 0; i < val.Len(); i++ { + result.BoolValues = append(result.BoolValues, val.Index(i).Bool()) + } + + return result +} + +func intValue[T Integer](in T) *rpcv1.ModuleSpec_Value { + return &rpcv1.ModuleSpec_Value{ + Type: rpcv1.ModuleSpec_ValueTypeSingle, + SingleValue: &rpcv1.ModuleSpec_Value_IntValue{ + IntValue: int64(in), + }, + } +} + +func intSliceValue(val reflect.Value, selector func(v reflect.Value) int64) *rpcv1.ModuleSpec_Value { + result := &rpcv1.ModuleSpec_Value{ + Type: rpcv1.ModuleSpec_ValueTypeIntSlice, + IntValues: make([]int64, 0, val.Len()), + } + + for i := 0; i < val.Len(); i++ { + result.IntValues = append(result.IntValues, selector(val.Index(i))) + } + + return result +} + +func floatValue[T Float](in T) *rpcv1.ModuleSpec_Value { + return &rpcv1.ModuleSpec_Value{ + Type: rpcv1.ModuleSpec_ValueTypeSingle, + SingleValue: &rpcv1.ModuleSpec_Value_DoubleValue{ + DoubleValue: float64(in), + }, + } +} + +func floatSliceValue(val reflect.Value) *rpcv1.ModuleSpec_Value { + result := &rpcv1.ModuleSpec_Value{ + Type: rpcv1.ModuleSpec_ValueTypeDoubleSlice, + DoubleValues: make([]float64, 0, val.Len()), + } + + for i := 0; i < val.Len(); i++ { + result.DoubleValues = append(result.DoubleValues, val.Index(i).Float()) + } + + return result +} + +func stringValue(in string) *rpcv1.ModuleSpec_Value { + return &rpcv1.ModuleSpec_Value{ + Type: rpcv1.ModuleSpec_ValueTypeSingle, + SingleValue: &rpcv1.ModuleSpec_Value_StringValue{ + StringValue: in, + }, + } +} + +func stringSliceValue(val reflect.Value) *rpcv1.ModuleSpec_Value { + result := &rpcv1.ModuleSpec_Value{ + Type: rpcv1.ModuleSpec_ValueTypeStringSlice, + StringValues: make([]string, 0, val.Len()), + } + + for i := 0; i < val.Len(); i++ { + result.StringValues = append(result.StringValues, val.Index(i).String()) + } + + return result +} diff --git a/protocol/marshal_test.go b/protocol/marshal_test.go new file mode 100644 index 0000000..5bba671 --- /dev/null +++ b/protocol/marshal_test.go @@ -0,0 +1,220 @@ +package protocol_test + +import ( + "testing" + + rpcv1 "code.icb4dc0.de/buildr/api/generated/rpc/v1" + "code.icb4dc0.de/buildr/common/protocol" +) + +func TestMarshal_Bool_Success(t *testing.T) { + input := struct { + IsDeleted bool + }{ + IsDeleted: true, + } + + spec, err := protocol.Marshal(input) + if err != nil { + t.Fatal(err) + } + + nameVal, ok := spec.Values["IsDeleted"] + if !ok { + t.Fatal("IsDeleted not found") + } + + if nameVal.Type != rpcv1.ModuleSpec_ValueTypeSingle { + t.Fatalf("Expected single value type, got %v", nameVal.Type) + } + + if s, ok := nameVal.SingleValue.(*rpcv1.ModuleSpec_Value_BoolValue); !ok { + t.Fatalf("Expected string value, got %v", nameVal.SingleValue) + } else if !s.BoolValue { + t.Errorf("Expected bool value to be true, got %t", s.BoolValue) + } +} + +func TestMarshal_BoolSlice_Success(t *testing.T) { + input := struct { + IsDeleted []bool + }{ + IsDeleted: []bool{true}, + } + + spec, err := protocol.Marshal(input) + if err != nil { + t.Fatal(err) + } + + nameVal, ok := spec.Values["IsDeleted"] + if !ok { + t.Fatal("IsDeleted not found") + } + + if nameVal.Type != rpcv1.ModuleSpec_ValueTypeBoolSlice { + t.Fatalf("Expected bool slice value type, got %v", nameVal.Type) + } + + if len(nameVal.BoolValues) < 1 { + t.Fatalf("Expected at least one bool value, got %d", len(nameVal.BoolValues)) + } + + if !nameVal.BoolValues[0] { + t.Errorf("Expected bool value to be true, got %t", nameVal.BoolValues[0]) + } +} + +func TestMarshal_Int_Success(t *testing.T) { + input := struct { + Age int + }{ + Age: 42, + } + + spec, err := protocol.Marshal(input) + if err != nil { + t.Fatal(err) + } + + nameVal, ok := spec.Values["Age"] + if !ok { + t.Fatal("Age not found") + } + + if nameVal.Type != rpcv1.ModuleSpec_ValueTypeSingle { + t.Fatalf("Expected single value type, got %v", nameVal.Type) + } + + if s, ok := nameVal.SingleValue.(*rpcv1.ModuleSpec_Value_IntValue); !ok { + t.Fatalf("Expected string value, got %v", nameVal.SingleValue) + } else if s.IntValue != 42 { + t.Errorf("Expected int value to be 42, got %d", s.IntValue) + } +} + +func TestMarshal_IntSlice_Success(t *testing.T) { + input := struct { + Ages []int + }{ + Ages: []int{42}, + } + + spec, err := protocol.Marshal(input) + if err != nil { + t.Fatal(err) + } + + nameVal, ok := spec.Values["Ages"] + if !ok { + t.Fatal("Ages not found") + } + + if nameVal.Type != rpcv1.ModuleSpec_ValueTypeIntSlice { + t.Fatalf("Expected int slice value type, got %v", nameVal.Type) + } + + if len(nameVal.IntValues) < 1 { + t.Fatalf("Expected at least one bool value, got %d", len(nameVal.BoolValues)) + } + + if nameVal.IntValues[0] != 42 { + t.Errorf("Expected int value to be 52, got %d", nameVal.IntValues[0]) + } +} + +func TestMarshal_StringField_Success(t *testing.T) { + input := struct { + Name string + }{ + Name: "John Doe", + } + + spec, err := protocol.Marshal(input) + if err != nil { + t.Fatal(err) + } + + nameVal, ok := spec.Values["Name"] + if !ok { + t.Fatal("Name not found") + } + + if nameVal.Type != rpcv1.ModuleSpec_ValueTypeSingle { + t.Fatalf("Expected single value type, got %v", nameVal.Type) + } + + if s, ok := nameVal.SingleValue.(*rpcv1.ModuleSpec_Value_StringValue); !ok { + t.Fatalf("Expected string value, got %v", nameVal.SingleValue) + } else if s.StringValue != "John Doe" { + t.Errorf("Expected string value to be John Doe, got %s", s.StringValue) + } +} + +func TestMarshal_Float64_Success(t *testing.T) { + input := struct { + Pi float64 + }{ + Pi: 3.14, + } + + spec, err := protocol.Marshal(input) + if err != nil { + t.Fatal(err) + } + + nameVal, ok := spec.Values["Pi"] + if !ok { + t.Fatal("Pi not found") + } + + if nameVal.Type != rpcv1.ModuleSpec_ValueTypeSingle { + t.Fatalf("Expected single value type, got %v", nameVal.Type) + } + + if s, ok := nameVal.SingleValue.(*rpcv1.ModuleSpec_Value_DoubleValue); !ok { + t.Fatalf("Expected double value, got %v", nameVal.SingleValue) + } else if s.DoubleValue-3.14 > 0.000001 { + t.Errorf("Expected double value to be 3.14, got %f", s.DoubleValue) + } +} + +func TestMarshal_NestedStruct_Success(t *testing.T) { + type Address struct { + City string + } + input := struct { + Address Address + }{ + Address: Address{ + City: "New York", + }, + } + + spec, err := protocol.Marshal(input) + if err != nil { + t.Fatal(err) + } + + addressVal, ok := spec.Values["Address"] + if !ok { + t.Fatal("Address not found") + } + + if addressVal.Type != rpcv1.ModuleSpec_ValueTypeObject { + t.Fatalf("Expected object value type, got %v", addressVal.Type) + } + + cityVal, ok := addressVal.ComplexValue["City"] + if !ok { + t.Fatal("City not found") + } + + if cityVal.Type != rpcv1.ModuleSpec_ValueTypeSingle { + t.Fatalf("Expected single value type, got %v", cityVal.Type) + } + + if cityVal.SingleValue.(*rpcv1.ModuleSpec_Value_StringValue).StringValue != "New York" { + t.Errorf("Expected string value to be New York, got %s", cityVal.SingleValue.(*rpcv1.ModuleSpec_Value_StringValue).StringValue) + } +} diff --git a/protocol/unmarshal.go b/protocol/unmarshal.go new file mode 100644 index 0000000..d1d5275 --- /dev/null +++ b/protocol/unmarshal.go @@ -0,0 +1,193 @@ +package protocol + +import ( + "errors" + "fmt" + "reflect" + "strings" + + rpcv1 "code.icb4dc0.de/buildr/api/generated/rpc/v1" +) + +var ( + ErrUnmatchingType = errors.New("field type does not match wire value") +) + +func Unmarshal(input *rpcv1.ModuleSpec, into any) error { + + if u, ok := into.(SpecUnmarshaler); ok { + return u.UnmarshalModuleSpec(input) + } + + val := reflect.ValueOf(into) + + switch val.Kind() { + case reflect.Ptr: + default: + return errors.New("into value must be a pointer") + } + return unmarshal(input.Values, val.Elem(), reflect.TypeOf(into).Elem()) +} + +func 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() { + continue + } + + val, ok := input[strings.ToLower(tf.Name)] + if !ok { + continue + } + + if into.Type().Kind() == reflect.Pointer { + if u, ok := into.Elem().Field(i).Interface().(SpecValueUnmarshaler); ok { + if err := u.UnmarshalSpecValue(val); err != nil { + return err + } + continue + } + } else { + if u, ok := into.Field(i).Interface().(SpecValueUnmarshaler); ok { + if err := u.UnmarshalSpecValue(val); err != nil { + return err + } + continue + } + } + + if mapped, err := mapSpecValueTo(val, tf.Type); err != nil { + return err + } else if into.Type().Kind() == reflect.Pointer { + into.Elem().Field(i).Set(mapped) + } else { + into.Field(i).Set(mapped) + } + } + + return nil +} + +func 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 { + 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 { + return reflect.Value{}, err + } else { + return structVal, nil + } + } else { + return reflect.Value{}, fmt.Errorf("%w: expected struct, got %s", ErrUnmatchingType, val.Type) + } + case rpcv1.ModuleSpec_ValueTypeMap: + if targetType.Kind() != reflect.Map { + return reflect.Value{}, fmt.Errorf("%w: expected map, got %v", ErrUnmatchingType, targetType) + } + + mapVal := reflect.MakeMap(targetType) + + for k, v := range val.ComplexValue { + if mappedVal, err := mapSpecValueTo(v, targetType.Elem()); err != nil { + return reflect.Value{}, err + } else { + mapVal.SetMapIndex(reflect.ValueOf(k), mappedVal) + } + } + + return mapVal, nil + + case rpcv1.ModuleSpec_ValueTypeSingle: + switch sv := val.SingleValue.(type) { + case *rpcv1.ModuleSpec_Value_BoolValue: + if targetType.Kind() != reflect.Bool { + return reflect.Value{}, fmt.Errorf("%w: expected bool, got %v", ErrUnmatchingType, targetType) + } + return reflect.ValueOf(sv.BoolValue), nil + case *rpcv1.ModuleSpec_Value_StringValue: + if targetType.Kind() != reflect.String { + return reflect.Value{}, fmt.Errorf("%w: expected string, got %v", ErrUnmatchingType, targetType) + } + return reflect.ValueOf(sv.StringValue), nil + case *rpcv1.ModuleSpec_Value_IntValue: + if targetType.Kind() != reflect.Int { + return reflect.Value{}, fmt.Errorf("%w: expected int, got %v", ErrUnmatchingType, targetType) + } + return reflect.ValueOf(int(sv.IntValue)), nil + case *rpcv1.ModuleSpec_Value_DoubleValue: + if targetType.Kind() != reflect.Float64 { + return reflect.Value{}, fmt.Errorf("%w: expected float64, got %v", ErrUnmatchingType, targetType) + } + return reflect.ValueOf(sv.DoubleValue), nil + } + case rpcv1.ModuleSpec_ValueTypeBoolSlice: + if targetType.Kind() != reflect.Slice { + return reflect.Value{}, fmt.Errorf("%w: expected slice, got %v", ErrUnmatchingType, targetType) + } else if targetType.Elem().Kind() != reflect.Bool { + return reflect.Value{}, fmt.Errorf("%w: expected bool, got %v", ErrUnmatchingType, targetType.Elem()) + } + return reflect.ValueOf(val.BoolValues), nil + case rpcv1.ModuleSpec_ValueTypeStringSlice: + if targetType.Kind() != reflect.Slice { + return reflect.Value{}, fmt.Errorf("%w: expected slice, got %v", ErrUnmatchingType, targetType) + } else if targetType.Elem().Kind() != reflect.String { + return reflect.Value{}, fmt.Errorf("%w: expected string, got %v", ErrUnmatchingType, targetType.Elem()) + } + return reflect.ValueOf(val.StringValues), nil + case rpcv1.ModuleSpec_ValueTypeIntSlice: + if targetType.Kind() != reflect.Slice { + return reflect.Value{}, fmt.Errorf("%w: expected slice, got %v", ErrUnmatchingType, targetType) + } + switch targetType.Elem().Kind() { + case reflect.Int: + return reflect.ValueOf(convertNumericSlice[int64, int](val.IntValues)), nil + case reflect.Int8: + return reflect.ValueOf(convertNumericSlice[int64, int8](val.IntValues)), nil + case reflect.Int16: + return reflect.ValueOf(convertNumericSlice[int64, int16](val.IntValues)), nil + case reflect.Int32: + return reflect.ValueOf(convertNumericSlice[int64, int32](val.IntValues)), nil + case reflect.Int64: + return reflect.ValueOf(val.IntValues), nil + default: + return reflect.Value{}, fmt.Errorf("%w: expected int, got %v", ErrUnmatchingType, targetType.Elem()) + } + case rpcv1.ModuleSpec_ValueTypeDoubleSlice: + if targetType.Kind() != reflect.Slice { + return reflect.Value{}, fmt.Errorf("%w: expected slice, got %v", ErrUnmatchingType, targetType) + } + switch targetType.Elem().Kind() { + case reflect.Float32: + return reflect.ValueOf(convertNumericSlice[float64, float32](val.DoubleValues)), nil + case reflect.Float64: + return reflect.ValueOf(val.DoubleValues), nil + default: + return reflect.Value{}, fmt.Errorf("%w: expected int, got %v", ErrUnmatchingType, targetType.Elem()) + } + } + + return reflect.Value{}, nil +} + +type numeric interface { + Integer | Float +} + +func convertNumericSlice[TIn numeric, TOut numeric](input []TIn) []TOut { + output := make([]TOut, len(input)) + for i, v := range input { + output[i] = TOut(v) + } + return output +} diff --git a/protocol/unmarshal_test.go b/protocol/unmarshal_test.go new file mode 100644 index 0000000..1f1369c --- /dev/null +++ b/protocol/unmarshal_test.go @@ -0,0 +1,515 @@ +package protocol_test + +import ( + "testing" + + rpcv1 "code.icb4dc0.de/buildr/api/generated/rpc/v1" + "code.icb4dc0.de/buildr/common/protocol" +) + +func TestUnmarshal_Bool_Success(t *testing.T) { + target := struct { + Delete bool + }{} + + spec := &rpcv1.ModuleSpec{ + Values: map[string]*rpcv1.ModuleSpec_Value{ + "delete": { + Type: rpcv1.ModuleSpec_ValueTypeSingle, + SingleValue: &rpcv1.ModuleSpec_Value_BoolValue{ + BoolValue: true, + }, + }, + }, + } + + if err := protocol.Unmarshal(spec, &target); err != nil { + t.Errorf("Failed to unmarshal: %v", err) + } + + if !target.Delete { + t.Errorf("Expected Delete to be true") + } +} + +func TestUnmarshal_Bool_Err(t *testing.T) { + target := struct { + Delete string + }{} + + spec := &rpcv1.ModuleSpec{ + Values: map[string]*rpcv1.ModuleSpec_Value{ + "delete": { + Type: rpcv1.ModuleSpec_ValueTypeSingle, + SingleValue: &rpcv1.ModuleSpec_Value_BoolValue{ + BoolValue: true, + }, + }, + }, + } + + if err := protocol.Unmarshal(spec, &target); err == nil { + t.Errorf("Expected error") + } else { + t.Log(err.Error()) + } +} + +func TestUnmarshal_Bool_Slice_Success(t *testing.T) { + target := struct { + Delete []bool + }{} + + spec := &rpcv1.ModuleSpec{ + Values: map[string]*rpcv1.ModuleSpec_Value{ + "delete": { + Type: rpcv1.ModuleSpec_ValueTypeBoolSlice, + BoolValues: []bool{true}, + }, + }, + } + + if err := protocol.Unmarshal(spec, &target); err != nil { + t.Errorf("Failed to unmarshal: %v", err) + } + + if len(target.Delete) < 1 { + t.Errorf("Expected Delete to have at least one element") + } else if !target.Delete[0] { + t.Errorf("Expected Delete[0] to be true") + } +} + +func TestUnmarshal_Bool_Slice_Err(t *testing.T) { + target := struct { + Delete []string + }{} + + spec := &rpcv1.ModuleSpec{ + Values: map[string]*rpcv1.ModuleSpec_Value{ + "delete": { + Type: rpcv1.ModuleSpec_ValueTypeBoolSlice, + BoolValues: []bool{true}, + }, + }, + } + + if err := protocol.Unmarshal(spec, &target); err == nil { + t.Errorf("Expected error") + } else { + t.Log(err.Error()) + } +} + +func TestUnmarshal_String_Success(t *testing.T) { + target := struct { + Name string + }{} + + spec := &rpcv1.ModuleSpec{ + Values: map[string]*rpcv1.ModuleSpec_Value{ + "name": { + Type: rpcv1.ModuleSpec_ValueTypeSingle, + SingleValue: &rpcv1.ModuleSpec_Value_StringValue{ + StringValue: "Ted", + }, + }, + }, + } + + if err := protocol.Unmarshal(spec, &target); err != nil { + t.Errorf("Failed to unmarshal: %v", err) + } + + if target.Name != "Ted" { + t.Errorf("Expected Name to be 'Ted'") + } +} + +func TestUnmarshal_String_Err(t *testing.T) { + target := struct { + Name int + }{} + + spec := &rpcv1.ModuleSpec{ + Values: map[string]*rpcv1.ModuleSpec_Value{ + "name": { + Type: rpcv1.ModuleSpec_ValueTypeSingle, + SingleValue: &rpcv1.ModuleSpec_Value_StringValue{ + StringValue: "Ted", + }, + }, + }, + } + + if err := protocol.Unmarshal(spec, &target); err == nil { + t.Errorf("Expected error") + } else { + t.Log(err.Error()) + } +} + +func TestUnmarshal_String_Slice_Success(t *testing.T) { + target := struct { + Names []string + }{} + + spec := &rpcv1.ModuleSpec{ + Values: map[string]*rpcv1.ModuleSpec_Value{ + "names": { + Type: rpcv1.ModuleSpec_ValueTypeStringSlice, + StringValues: []string{"Ted"}, + }, + }, + } + + if err := protocol.Unmarshal(spec, &target); err != nil { + t.Errorf("Failed to unmarshal: %v", err) + } + + if len(target.Names) < 1 { + t.Errorf("Expected Names to have at least one element") + } else if target.Names[0] != "Ted" { + t.Errorf("Expected Names[0] to be 'Ted'") + } +} + +func TestUnmarshal_String_Slice_Err(t *testing.T) { + target := struct { + Names []int + }{} + + spec := &rpcv1.ModuleSpec{ + Values: map[string]*rpcv1.ModuleSpec_Value{ + "names": { + Type: rpcv1.ModuleSpec_ValueTypeStringSlice, + StringValues: []string{"Ted"}, + }, + }, + } + + if err := protocol.Unmarshal(spec, &target); err == nil { + t.Errorf("Expected error") + } else { + t.Log(err.Error()) + } +} + +func TestUnmarshal_Int_Success(t *testing.T) { + target := struct { + Age int + }{} + + spec := &rpcv1.ModuleSpec{ + Values: map[string]*rpcv1.ModuleSpec_Value{ + "age": { + Type: rpcv1.ModuleSpec_ValueTypeSingle, + SingleValue: &rpcv1.ModuleSpec_Value_IntValue{ + IntValue: 42, + }, + }, + }, + } + + if err := protocol.Unmarshal(spec, &target); err != nil { + t.Errorf("Failed to unmarshal: %v", err) + } + + if target.Age != 42 { + t.Errorf("Expected Age to be 42") + } +} + +func TestUnmarshal_Int_Err(t *testing.T) { + target := struct { + Age string + }{} + + spec := &rpcv1.ModuleSpec{ + Values: map[string]*rpcv1.ModuleSpec_Value{ + "age": { + Type: rpcv1.ModuleSpec_ValueTypeSingle, + SingleValue: &rpcv1.ModuleSpec_Value_IntValue{ + IntValue: 42, + }, + }, + }, + } + + if err := protocol.Unmarshal(spec, &target); err == nil { + t.Errorf("Expected error") + } else { + t.Log(err.Error()) + } +} + +func TestUnmarshal_Int_Slice_Success(t *testing.T) { + target := struct { + Ages []int + }{} + + spec := &rpcv1.ModuleSpec{ + Values: map[string]*rpcv1.ModuleSpec_Value{ + "ages": { + Type: rpcv1.ModuleSpec_ValueTypeIntSlice, + IntValues: []int64{42}, + }, + }, + } + + if err := protocol.Unmarshal(spec, &target); err != nil { + t.Errorf("Failed to unmarshal: %v", err) + } + + if len(target.Ages) < 1 { + t.Errorf("Expected Ages to have at least one element") + } else if target.Ages[0] != 42 { + t.Errorf("Expected Ages[0] to be 42") + } +} + +func TestUnmarshal_Int_Slice_Err(t *testing.T) { + target := struct { + Ages []string + }{} + + spec := &rpcv1.ModuleSpec{ + Values: map[string]*rpcv1.ModuleSpec_Value{ + "ages": { + Type: rpcv1.ModuleSpec_ValueTypeIntSlice, + IntValues: []int64{42}, + }, + }, + } + + if err := protocol.Unmarshal(spec, &target); err == nil { + t.Errorf("Expected error") + } else { + t.Log(err.Error()) + } +} + +func TestUnmarshal_Double_Success(t *testing.T) { + target := struct { + Pi float64 + }{} + + spec := &rpcv1.ModuleSpec{ + Values: map[string]*rpcv1.ModuleSpec_Value{ + "Pi": { + Type: rpcv1.ModuleSpec_ValueTypeSingle, + SingleValue: &rpcv1.ModuleSpec_Value_DoubleValue{ + DoubleValue: 3.14, + }, + }, + }, + } + + if err := protocol.Unmarshal(spec, &target); err != nil { + t.Errorf("Failed to unmarshal: %v", err) + } + + if target.Pi-3.14 > 0.0000001 { + t.Errorf("Expected Pi to be 3.14") + } +} + +func TestUnmarshal_Double_Err(t *testing.T) { + target := struct { + Pi string + }{} + + spec := &rpcv1.ModuleSpec{ + Values: map[string]*rpcv1.ModuleSpec_Value{ + "pi": { + Type: rpcv1.ModuleSpec_ValueTypeSingle, + SingleValue: &rpcv1.ModuleSpec_Value_DoubleValue{ + DoubleValue: 3.14, + }, + }, + }, + } + + if err := protocol.Unmarshal(spec, &target); err == nil { + t.Errorf("Expected error") + } else { + t.Log(err.Error()) + } +} + +func TestUnmarshal_Double_Slice_Success(t *testing.T) { + target := struct { + Pis []float64 + }{} + + spec := &rpcv1.ModuleSpec{ + Values: map[string]*rpcv1.ModuleSpec_Value{ + "pis": { + Type: rpcv1.ModuleSpec_ValueTypeDoubleSlice, + DoubleValues: []float64{3.14}, + }, + }, + } + + if err := protocol.Unmarshal(spec, &target); err != nil { + t.Errorf("Failed to unmarshal: %v", err) + } + + if len(target.Pis) < 1 { + t.Errorf("Expected Pis to have at least one element") + } else if target.Pis[0]-3.14 > 0.0000001 { + t.Errorf("Expected Pis[0] to be 3.14") + } +} + +func TestUnmarshal_Double_Slice_Err(t *testing.T) { + target := struct { + Pis []string + }{} + + spec := &rpcv1.ModuleSpec{ + Values: map[string]*rpcv1.ModuleSpec_Value{ + "pis": { + Type: rpcv1.ModuleSpec_ValueTypeDoubleSlice, + DoubleValues: []float64{3.14}, + }, + }, + } + + if err := protocol.Unmarshal(spec, &target); err == nil { + t.Errorf("Expected error") + } else { + t.Log(err.Error()) + } +} + +func TestUnmarshal_NestedStruct_Success(t *testing.T) { + target := struct { + Address struct { + City string + } + }{} + + spec := &rpcv1.ModuleSpec{ + Values: map[string]*rpcv1.ModuleSpec_Value{ + "address": { + Type: rpcv1.ModuleSpec_ValueTypeObject, + ComplexValue: map[string]*rpcv1.ModuleSpec_Value{ + "city": { + Type: rpcv1.ModuleSpec_ValueTypeSingle, + SingleValue: &rpcv1.ModuleSpec_Value_StringValue{ + StringValue: "New York", + }, + }, + }, + }, + }, + } + + if err := protocol.Unmarshal(spec, &target); err != nil { + t.Errorf("Failed to unmarshal: %v", err) + } + + if target.Address.City != "New York" { + t.Errorf("Expected City to be 'New York'") + } +} + +func TestUnmarshal_NestedStructPointer_Success(t *testing.T) { + target := struct { + Address *struct { + City string + } + }{} + + spec := &rpcv1.ModuleSpec{ + Values: map[string]*rpcv1.ModuleSpec_Value{ + "address": { + Type: rpcv1.ModuleSpec_ValueTypeObject, + ComplexValue: map[string]*rpcv1.ModuleSpec_Value{ + "city": { + Type: rpcv1.ModuleSpec_ValueTypeSingle, + SingleValue: &rpcv1.ModuleSpec_Value_StringValue{ + StringValue: "New York", + }, + }, + }, + }, + }, + } + + if err := protocol.Unmarshal(spec, &target); err != nil { + t.Errorf("Failed to unmarshal: %v", err) + } + + if target.Address.City != "New York" { + t.Errorf("Expected City to be 'New York'") + } +} + +func TestUnmarshal_Map_Success(t *testing.T) { + target := struct { + Values map[string]string + }{} + + spec := &rpcv1.ModuleSpec{ + Values: map[string]*rpcv1.ModuleSpec_Value{ + "values": { + Type: rpcv1.ModuleSpec_ValueTypeMap, + ComplexValue: map[string]*rpcv1.ModuleSpec_Value{ + "City": { + Type: rpcv1.ModuleSpec_ValueTypeSingle, + SingleValue: &rpcv1.ModuleSpec_Value_StringValue{ + StringValue: "New York", + }, + }, + }, + }, + }, + } + + if err := protocol.Unmarshal(spec, &target); err != nil { + t.Errorf("Failed to unmarshal: %v", err) + } + + if target.Values["City"] != "New York" { + t.Errorf("Expected City to be 'New York'") + } +} + +func TestUnmarshal_NestedMap_Success(t *testing.T) { + target := struct { + City struct { + Labels map[string]string + } + }{} + + spec := &rpcv1.ModuleSpec{ + Values: map[string]*rpcv1.ModuleSpec_Value{ + "city": { + Type: rpcv1.ModuleSpec_ValueTypeObject, + ComplexValue: map[string]*rpcv1.ModuleSpec_Value{ + "labels": { + Type: rpcv1.ModuleSpec_ValueTypeMap, + ComplexValue: map[string]*rpcv1.ModuleSpec_Value{ + "Region": { + Type: rpcv1.ModuleSpec_ValueTypeSingle, + SingleValue: &rpcv1.ModuleSpec_Value_StringValue{ + StringValue: "west", + }, + }, + }, + }, + }, + }, + }, + } + + if err := protocol.Unmarshal(spec, &target); err != nil { + t.Errorf("Failed to unmarshal: %v", err) + } + + if target.City.Labels["Region"] != "west" { + t.Errorf("Expected 'Region' to be 'west'") + } +} diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..89cc11e --- /dev/null +++ b/renovate.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + ":dependencyDashboard", + ":ignoreModulesAndTests", + ":semanticPrefixFixDepsChoreOthers", + ":autodetectPinVersions", + ":prHourlyLimit2", + ":prConcurrentLimit10", + "group:monorepos", + "group:recommended", + "workarounds:all" + ], + "postUpdateOptions": [ + "gomodTidy1.17", + "gomodUpdateImportPaths" + ] +}