diff --git a/README.md b/README.md index d583a6e..4ce6264 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,8 @@ # golymorph [![Pipeline Tests Status](https://github.com/SoulKa/golymorph/actions/workflows/go-test.yaml/badge.svg)](https://github.com/SoulKa/golymorph/actions/workflows/go-test.yaml) [![Godoc](https://godoc.org/github.com/SoulKa/golymorph?status.svg)](https://godoc.org/github.com/SoulKa/golymorph) The golymorph module enables resolving polymorphic typing at runtime. It's usually used in -conjunction with -JSON parsing and the `mapstructure` module. In fact, this module takes the use case -of `mapstructure` and takes it -a step further by allowing the user to define a custom type resolver function. +conjunction with JSON parsing and the `mapstructure` module. In fact, this module takes the use case +of `mapstructure` a step further by allowing the user to define a custom type resolver. ## Installation @@ -14,9 +12,42 @@ Standard `go get`: go get github.com/SoulKa/golymorph ``` -## Usage & Example +## Docs -For usage and examples see the [Godoc](http://godoc.org/github.com/SoulKa/golymorph). +The docs are hosted on [Godoc](http://godoc.org/github.com/SoulKa/golymorph). + +## Use Case + +Use this module to resolve polymorphic types at runtime. An example would be a struct that contains +a payload field which can be of different struct types not known at compile time: + +```go +// the parent type that contains the polymorphic payload +type Event struct { + Timestamp string + Payload any // <-- AlertPayload or PingPayload? +} + +// the polymorphic child types +type AlertPayload struct { + Type string + Message string +} +type PingPayload struct { + Type string + Ip string +} +``` + +If, for example, you have a JSON that you decode into the `Event` struct, it is cumbersome to parse the JSON into a map, look into the `type` field of the `payload` and after that select and parse the map into the correct type at the `payload` field of the `event` struct. +golymorph does exactly this: + +1. Look at the value of a defined field anywhere in the given `map` or JSON +2. Find the correct type using the given pairs of `value ==> reflect.Type` +3. Assign the correct type at the given position anywhere in the "parent" struct +4. Fully decode the given JSON or `map`, now with a concrete struct type as `payload` + +## Example ```go package main @@ -73,4 +104,4 @@ func main() { fmt.Printf("event: %+v\n", event) fmt.Printf("event payload: %T %+v\n", event.Payload, event.Payload.(AlertPayload)) } -``` \ No newline at end of file +``` diff --git a/polymorphism.go b/polymorphism.go index 4aa2c72..d48ef0b 100644 --- a/polymorphism.go +++ b/polymorphism.go @@ -1,47 +1,11 @@ package golymorph import ( - "encoding/json" "github.com/SoulKa/golymorph/objectpath" - "github.com/mitchellh/mapstructure" ) +// Polymorphism is the base struct for all polymorphism mappers. It contains the target path to assign the new type to. type Polymorphism struct { - // targetPath is the path to the object to assign the new type to - targetPath objectpath.ObjectPath -} - -// UnmarshalJSON unmarshals the given JSON data into the given output object using the given TypeResolver. -func UnmarshalJSON(resolver TypeResolver, data []byte, output any) error { - - // parse JSON - var jsonMap map[string]any - if err := json.Unmarshal(data, &jsonMap); err != nil { - return err - } - - // resolve polymorphism - if err := Decode(resolver, jsonMap, output); err != nil { - return err - } - - // success - return nil -} - -// Decode decodes the given source map into the given output object using the given TypeResolver and mapstructure. -func Decode(resolver TypeResolver, source map[string]any, output any) error { - - // create a new event - if err := resolver.AssignTargetType(&source, output); err != nil { - return err - } - - // use mapstructure to unmarshal the payload into the event - if err := mapstructure.Decode(source, output); err != nil { - return err - } - - // success - return nil + // TargetPath is the path to the object to assign the new type to + TargetPath objectpath.ObjectPath } diff --git a/polymorphism_builder.go b/polymorphism_builder.go index 5eed5c5..50d0b3b 100644 --- a/polymorphism_builder.go +++ b/polymorphism_builder.go @@ -1,54 +1,58 @@ package golymorph import ( - "errors" "github.com/SoulKa/golymorph/objectpath" "strings" ) -type PolymorphismBuilderBase struct { +type polymorphismBuilderBase struct { targetPath objectpath.ObjectPath errors []error } -type PolymorphismTypeMapBuilder struct { - PolymorphismBuilderBase - typeMap TypeMap - discriminatorPath objectpath.ObjectPath +type polymorphismBuilderEmpty interface { + // DefineTypeAt defines the target path of the polymorphism. This is the path where the polymorphism + // will be applied, i.e. where the new type is set. For valid paths see objectpath.NewObjectPathFromString. + DefineTypeAt(targetPath string) polymorphismBuilderStrategySelector } -type PolymorphismRuleBuilder struct { - PolymorphismBuilderBase - rules []Rule -} +type polymorphismBuilderStrategySelector interface { + // UsingRule defines a rule that is used to determine the new type. The rules are applied in the + // order they are defined. The first rule that matches is used to determine the new type. + UsingRule(rule Rule) polymorphismBuilderRuleAdder -type PolymorphismBuilderEmpty interface { - DefineTypeAt(targetPath string) PolymorphismBuilderStrategySelector + // UsingTypeMap defines a type map that is used to determine the new type. The type map is applied + UsingTypeMap(typeMap TypeMap) polymorphismBuilderDiscriminatorKeyDefiner } -type PolymorphismBuilderStrategySelector interface { - UsingRule(rule Rule) PolymorphismBuilderRuleAdder - UsingTypeMap(typeMap TypeMap) PolymorphismBuilderDiscriminatorKeyDefiner -} +type polymorphismBuilderRuleAdder interface { + // UsingRule defines a rule that is used to determine the new type. The rules are applied in the + // order they are defined. The first rule that matches is used to determine the new type. + UsingRule(rule Rule) polymorphismBuilderRuleAdder -type PolymorphismBuilderRuleAdder interface { - UsingRule(rule Rule) PolymorphismBuilderRuleAdder + // Build creates a new TypeResolver that can be used to resolve a polymorphic type. Build() (error, TypeResolver) } -type PolymorphismBuilderDiscriminatorKeyDefiner interface { - WithDiscriminatorAt(discriminatorKey string) PolymorphismBuilderFinalizer +type polymorphismBuilderDiscriminatorKeyDefiner interface { + // WithDiscriminatorAt defines the path to the discriminator key. The discriminator key is used to + // determine the new type. The value of the discriminator key is used to lookup the new type in the + // type map. + WithDiscriminatorAt(discriminatorKey string) polymorphismBuilderFinalizer } -type PolymorphismBuilderFinalizer interface { +type polymorphismBuilderFinalizer interface { + // Build creates a new TypeResolver that can be used to resolve a polymorphic type. Build() (error, TypeResolver) } -func NewPolymorphismBuilder() PolymorphismBuilderEmpty { - return &PolymorphismBuilderBase{*objectpath.NewSelfReferencePath(), []error{}} +// NewPolymorphismBuilder creates a new polymorphism builder that is used in a human readable way to create a polymorphism. +// It only allows a valid combination of rules and type maps. +func NewPolymorphismBuilder() polymorphismBuilderEmpty { + return &polymorphismBuilderBase{*objectpath.NewSelfReferencePath(), []error{}} } -func (b *PolymorphismBuilderBase) DefineTypeAt(targetPath string) PolymorphismBuilderStrategySelector { +func (b *polymorphismBuilderBase) DefineTypeAt(targetPath string) polymorphismBuilderStrategySelector { // make target path absolute if !strings.HasPrefix(targetPath, "/") { targetPath = "/" + targetPath @@ -63,46 +67,16 @@ func (b *PolymorphismBuilderBase) DefineTypeAt(targetPath string) PolymorphismBu return b } -func (b *PolymorphismBuilderBase) UsingRule(rule Rule) PolymorphismBuilderRuleAdder { - return &PolymorphismRuleBuilder{ - PolymorphismBuilderBase: *b, +func (b *polymorphismBuilderBase) UsingRule(rule Rule) polymorphismBuilderRuleAdder { + return &polymorphismRuleBuilder{ + polymorphismBuilderBase: *b, rules: []Rule{rule}, } } -func (b *PolymorphismRuleBuilder) UsingRule(rule Rule) PolymorphismBuilderRuleAdder { - b.rules = append(b.rules, rule) - return b -} - -func (b *PolymorphismBuilderBase) UsingTypeMap(typeMap TypeMap) PolymorphismBuilderDiscriminatorKeyDefiner { - return &PolymorphismTypeMapBuilder{ - PolymorphismBuilderBase: *b, +func (b *polymorphismBuilderBase) UsingTypeMap(typeMap TypeMap) polymorphismBuilderDiscriminatorKeyDefiner { + return &polymorphismTypeMapBuilder{ + polymorphismBuilderBase: *b, typeMap: typeMap, } } - -func (b *PolymorphismTypeMapBuilder) WithDiscriminatorAt(discriminatorKey string) PolymorphismBuilderFinalizer { - if err, path := objectpath.NewObjectPathFromString(discriminatorKey); err != nil { - b.errors = append(b.errors, err) - } else if err := path.ToAbsolutePath(&b.targetPath); err != nil { - b.errors = append(b.errors, err) - } else { - b.discriminatorPath = *path - } - return b -} - -func (b *PolymorphismRuleBuilder) Build() (error, TypeResolver) { - if len(b.errors) > 0 { - return errors.Join(b.errors...), nil - } - return nil, &RulePolymorphism{Polymorphism{b.targetPath}, b.rules} -} - -func (b *PolymorphismTypeMapBuilder) Build() (error, TypeResolver) { - if len(b.errors) > 0 { - return errors.Join(b.errors...), nil - } - return nil, &TypeMapPolymorphism{Polymorphism{b.targetPath}, b.discriminatorPath, b.typeMap} -} diff --git a/polymorphism_builder_test.go b/polymorphism_builder_test.go index 7ce6bfe..f6078e7 100644 --- a/polymorphism_builder_test.go +++ b/polymorphism_builder_test.go @@ -9,7 +9,7 @@ func TestPolymorphismBuilder_UsingRule(t *testing.T) { // Arrange errors, rule1 := NewRuleBuilder(). - WhenValueAtPathString("foo/bar"). + WhenValueAt("foo/bar"). IsEqualTo("test"). ThenAssignType(reflect.TypeOf(int64(0))). Build() @@ -19,7 +19,7 @@ func TestPolymorphismBuilder_UsingRule(t *testing.T) { // Arrange errors, rule2 := NewRuleBuilder(). - WhenValueAtPathString("foo/bar"). + WhenValueAt("foo/bar"). IsEqualTo("test"). ThenAssignType(reflect.TypeOf(int64(0))). Build() diff --git a/polymorphism_rule_builder.go b/polymorphism_rule_builder.go new file mode 100644 index 0000000..bf9543a --- /dev/null +++ b/polymorphism_rule_builder.go @@ -0,0 +1,25 @@ +package golymorph + +import ( + "errors" +) + +type polymorphismRuleBuilder struct { + polymorphismBuilderBase + rules []Rule +} + +func (b *polymorphismRuleBuilder) UsingRule(rule Rule) polymorphismBuilderRuleAdder { + b.rules = append(b.rules, rule) + return b +} + +func (b *polymorphismRuleBuilder) Build() (error, TypeResolver) { + if len(b.errors) > 0 { + return errors.Join(b.errors...), nil + } + return nil, &RulePolymorphism{ + Polymorphism{ + TargetPath: b.targetPath}, + b.rules} +} diff --git a/polymorphism_type_map_builder.go b/polymorphism_type_map_builder.go new file mode 100644 index 0000000..5b44d6e --- /dev/null +++ b/polymorphism_type_map_builder.go @@ -0,0 +1,34 @@ +package golymorph + +import ( + "errors" + "github.com/SoulKa/golymorph/objectpath" +) + +type polymorphismTypeMapBuilder struct { + polymorphismBuilderBase + typeMap TypeMap + discriminatorPath objectpath.ObjectPath +} + +func (b *polymorphismTypeMapBuilder) WithDiscriminatorAt(discriminatorKey string) polymorphismBuilderFinalizer { + if err, path := objectpath.NewObjectPathFromString(discriminatorKey); err != nil { + b.errors = append(b.errors, err) + } else if err := path.ToAbsolutePath(&b.targetPath); err != nil { + b.errors = append(b.errors, err) + } else { + b.discriminatorPath = *path + } + return b +} + +func (b *polymorphismTypeMapBuilder) Build() (error, TypeResolver) { + if len(b.errors) > 0 { + return errors.Join(b.errors...), nil + } + return nil, &TypeMapPolymorphism{ + Polymorphism: Polymorphism{ + TargetPath: b.targetPath}, + DiscriminatorPath: b.discriminatorPath, + TypeMap: b.typeMap} +} diff --git a/rule_builder.go b/rule_builder.go index 77134a5..6045d75 100644 --- a/rule_builder.go +++ b/rule_builder.go @@ -5,45 +5,42 @@ import ( "reflect" ) -// RuleBuilder is a builder for a polymorphism rule. -type RuleBuilder struct { +type ruleBuilder struct { errors []error valuePath objectpath.ObjectPath comparatorFunc func(any) bool newType reflect.Type } -type RuleBuilderBase interface { - WhenValueAt(valuePath objectpath.ObjectPath) RuleBuilderConditionSetter - WhenValueAtPathString(valuePath string) RuleBuilderConditionSetter +type ruleBuilderBase interface { + // WhenValueAt sets the path to the value in the source to compare. + WhenValueAt(valuePath string) ruleBuilderConditionSetter } -type RuleBuilderConditionSetter interface { - IsEqualTo(value any) RuleBuilderTypeAssigner - Matches(comparator func(any) bool) RuleBuilderTypeAssigner -} +type ruleBuilderConditionSetter interface { + // IsEqualTo sets the value to compare to. + IsEqualTo(value any) ruleBuilderTypeAssigner -type RuleBuilderTypeAssigner interface { - ThenAssignType(newType reflect.Type) RuleBuilderFinalizer + // Matches sets the function to use to compare the value at ValuePath to. + Matches(comparator func(any) bool) ruleBuilderTypeAssigner } -type RuleBuilderFinalizer interface { - Build() ([]error, Rule) +type ruleBuilderTypeAssigner interface { + // ThenAssignType sets the type to assign to the target if the rule matches. + ThenAssignType(newType reflect.Type) ruleBuilderFinalizer } -// NewRuleBuilder creates a new RuleBuilder. It enables a fluent interface for building a Rule. -func NewRuleBuilder() RuleBuilderBase { - return &RuleBuilder{} +type ruleBuilderFinalizer interface { + // Build builds the Rule and returns the errors encountered while building. + Build() ([]error, Rule) } -// WhenValueAt sets the path to the value in the source to compare. -func (b *RuleBuilder) WhenValueAt(valuePath objectpath.ObjectPath) RuleBuilderConditionSetter { - b.valuePath = valuePath - return b +// NewRuleBuilder creates a new ruleBuilder. It enables a fluent interface for building a Rule. +func NewRuleBuilder() ruleBuilderBase { + return &ruleBuilder{} } -// WhenValueAtPathString sets the path to the value in the source to compare. -func (b *RuleBuilder) WhenValueAtPathString(valuePath string) RuleBuilderConditionSetter { +func (b *ruleBuilder) WhenValueAt(valuePath string) ruleBuilderConditionSetter { if err, path := objectpath.NewObjectPathFromString(valuePath); err != nil { b.appendError(err) } else { @@ -52,26 +49,22 @@ func (b *RuleBuilder) WhenValueAtPathString(valuePath string) RuleBuilderConditi return b } -// IsEqualTo sets the value to compare to. -func (b *RuleBuilder) IsEqualTo(value any) RuleBuilderTypeAssigner { +func (b *ruleBuilder) IsEqualTo(value any) ruleBuilderTypeAssigner { b.comparatorFunc = func(v any) bool { return v == value } return b } -// Matches sets the function to use to compare the value at ValuePath to. -func (b *RuleBuilder) Matches(comparator func(any) bool) RuleBuilderTypeAssigner { +func (b *ruleBuilder) Matches(comparator func(any) bool) ruleBuilderTypeAssigner { b.comparatorFunc = comparator return b } -// ThenAssignType sets the type to assign to the target if the rule matches. -func (b *RuleBuilder) ThenAssignType(newType reflect.Type) RuleBuilderFinalizer { +func (b *ruleBuilder) ThenAssignType(newType reflect.Type) ruleBuilderFinalizer { b.newType = newType return b } -// Build builds the Rule and returns the errors encountered while building. -func (b *RuleBuilder) Build() ([]error, Rule) { +func (b *ruleBuilder) Build() ([]error, Rule) { return b.errors, Rule{ b.valuePath, b.comparatorFunc, @@ -79,6 +72,6 @@ func (b *RuleBuilder) Build() ([]error, Rule) { } } -func (b *RuleBuilder) appendError(err error) { +func (b *ruleBuilder) appendError(err error) { b.errors = append(b.errors, err) } diff --git a/rule_builder_test.go b/rule_builder_test.go index e8899e4..eaf3131 100644 --- a/rule_builder_test.go +++ b/rule_builder_test.go @@ -29,7 +29,7 @@ func TestRuleBuilder(t *testing.T) { // Act errors, rule := NewRuleBuilder(). - WhenValueAtPathString(valuePathString). + WhenValueAt(valuePathString). IsEqualTo(comparatorValue). ThenAssignType(newType). Build() diff --git a/rule_polymorphism.go b/rule_polymorphism.go index 4d1e5b9..bebcda2 100644 --- a/rule_polymorphism.go +++ b/rule_polymorphism.go @@ -6,22 +6,22 @@ import ( "github.com/SoulKa/golymorph/objectpath" ) -// RulePolymorphism is a mapper that assigns a target type based on the given rules +// RulePolymorphism is a mapper that assigns a target type based on the given Rules type RulePolymorphism struct { Polymorphism - // rules is a list of rules to apply. The first rule that matches is used to determine the target type. - rules []Rule + // Rules is a list of Rules to apply. The first rule that matches is used to determine the target type. + Rules []Rule } func (p *RulePolymorphism) AssignTargetType(source any, target any) error { // check for each rule if it matches and assign type if it does - for _, rule := range p.rules { + for _, rule := range p.Rules { if err, matches := rule.Matches(source); err != nil { return errors.Join(errors.New("error applying rule"), err) } else if matches { - if err := objectpath.AssignTypeAtPath(target, p.targetPath, rule.NewType); err != nil { + if err := objectpath.AssignTypeAtPath(target, p.TargetPath, rule.NewType); err != nil { return errors.Join(errors.New("error assigning type to target"), err) } return nil @@ -31,6 +31,6 @@ func (p *RulePolymorphism) AssignTargetType(source any, target any) error { // no rule matched return &golimorphError.UnresolvedTypeError{ Err: errors.New("no rule matched"), - TargetPath: p.targetPath.String(), + TargetPath: p.TargetPath.String(), } } diff --git a/type_map_polymorphism.go b/type_map_polymorphism.go index fb03323..500110e 100644 --- a/type_map_polymorphism.go +++ b/type_map_polymorphism.go @@ -12,30 +12,29 @@ import ( type TypeMapPolymorphism struct { Polymorphism - // discriminatorPath is the path to the discriminator value - discriminatorPath objectpath.ObjectPath + // DiscriminatorPath is the path to the discriminator value + DiscriminatorPath objectpath.ObjectPath - // typeMap is a map of discriminator values to types - typeMap TypeMap + // TypeMap is a map of discriminator values to types + TypeMap TypeMap } func (p *TypeMapPolymorphism) AssignTargetType(source any, target any) error { // get discriminator value var discriminatorValue reflect.Value - if err := objectpath.GetValueAtPath(source, p.discriminatorPath, &discriminatorValue); err != nil { + if err := objectpath.GetValueAtPath(source, p.DiscriminatorPath, &discriminatorValue); err != nil { return errors.Join(errors.New("error getting discriminator value"), err) } rawDiscriminatorValue := discriminatorValue.Interface() - fmt.Printf("discriminator value: %+v\n", rawDiscriminatorValue) // get type from type map - if newType, ok := p.typeMap[rawDiscriminatorValue]; !ok { + if newType, ok := p.TypeMap[rawDiscriminatorValue]; !ok { return &golimorphError.UnresolvedTypeError{ Err: fmt.Errorf("type map does not contain any key of value [%+v]", rawDiscriminatorValue), - TargetPath: p.targetPath.String(), + TargetPath: p.TargetPath.String(), } - } else if err := objectpath.AssignTypeAtPath(target, p.targetPath, newType); err != nil { + } else if err := objectpath.AssignTypeAtPath(target, p.TargetPath, newType); err != nil { return errors.Join(errors.New("error assigning type to target"), err) } return nil diff --git a/type_resolver.go b/type_resolver.go index 088c1c0..808bf99 100644 --- a/type_resolver.go +++ b/type_resolver.go @@ -1,14 +1,54 @@ package golymorph import ( + "encoding/json" + "github.com/mitchellh/mapstructure" "reflect" ) +// TypeMap is a map of values to types. type TypeMap map[any]reflect.Type +// TypeResolver is an interface that can resolve the type of a target based on the values of a source. type TypeResolver interface { // AssignTargetType assigns the determined type to target based on the polymorphism rules. The matching rule with the // highest priority is used. If no rule matches, the target type is not changed. The source and target must be pointers. // If no matching type can be determined, an error.UnresolvedTypeError is returned. AssignTargetType(source any, target any) error } + +// UnmarshalJSON unmarshals the given JSON data into the given output object using the given TypeResolver. +func UnmarshalJSON(resolver TypeResolver, data []byte, output any) error { + + // parse JSON + var jsonMap map[string]any + if err := json.Unmarshal(data, &jsonMap); err != nil { + return err + } + + // resolve polymorphism + if err := Decode(resolver, jsonMap, output); err != nil { + return err + } + + // success + return nil +} + +// Decode the given source map into the given output object using the given TypeResolver and mapstructure. +// The output object must be a pointer. +func Decode(resolver TypeResolver, source map[string]any, output any) error { + + // create a new event + if err := resolver.AssignTargetType(&source, output); err != nil { + return err + } + + // use mapstructure to unmarshal the payload into the event + if err := mapstructure.Decode(source, output); err != nil { + return err + } + + // success + return nil +} diff --git a/type_resolver_test.go b/type_resolver_test.go index 5c49e02..747fd29 100644 --- a/type_resolver_test.go +++ b/type_resolver_test.go @@ -2,6 +2,7 @@ package golymorph import ( "encoding/json" + "github.com/SoulKa/golymorph/objectpath" "github.com/mitchellh/mapstructure" "reflect" "testing" @@ -38,13 +39,19 @@ var testCases = []TestCase{ func TestPolymorphism_AssignTargetType(t *testing.T) { // Arrange - err, polymorphism := NewPolymorphismBuilder(). - DefineTypeAt("specifics"). - UsingTypeMap(animalTypeMap). - WithDiscriminatorAt("type"). - Build() + err, targetPath := objectpath.NewObjectPathFromString("/specifics") if err != nil { - t.Fatalf("error building polymorphism: %s", err) + t.Fatalf("error creating target path: %s", err) + } + err, discriminatorPath := objectpath.NewObjectPathFromString("/specifics/type") + if err != nil { + t.Fatalf("error creating discriminator path: %s", err) + } + polymorphism := &TypeMapPolymorphism{ + Polymorphism: Polymorphism{ + TargetPath: *targetPath}, + DiscriminatorPath: *discriminatorPath, + TypeMap: animalTypeMap, } t.Logf("polymorphism: %+v\n", polymorphism)