From ab617e49bdd774bc9481eb407ac723e2e994e9d9 Mon Sep 17 00:00:00 2001 From: SoulKa Date: Mon, 27 Nov 2023 23:38:54 +0100 Subject: [PATCH 1/4] made polymorphism kinda abstract --- README.md | 77 +++++++++++- examples/example_test.go | 119 +++++++++++++++++++ polymorphism.go | 52 ++++++++ polymorphism_builder.go | 17 +-- polymorphism_builder_test.go | 12 +- rule_polymorphism.go | 3 +- type_map_polymorphism.go | 3 +- polymorpher.go => type_resolver.go | 6 +- polymorpher_test.go => type_resolver_test.go | 6 +- 9 files changed, 269 insertions(+), 26 deletions(-) create mode 100644 examples/example_test.go create mode 100644 polymorphism.go rename polymorpher.go => type_resolver.go (87%) rename polymorpher_test.go => type_resolver_test.go (93%) diff --git a/README.md b/README.md index 602b53d..55c95b9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,75 @@ -# Golymorph -The golymorph module enables resolving polymorphic typing at runtime. It's usually used during JSON parsing. +# 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. -[![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) +## Installation + +Standard `go get`: + +```shell +go get github.com/SoulKa/golymorph +``` + +## Usage & Example + +For usage and examples see the [Godoc](http://godoc.org/github.com/SoulKa/golymorph). + +```go +// get a JSON that contains a payload with a type field that determines the type of the payload +alertEventJson := `{ "timestamp": "2023-11-27T22:14:09+00:00", "payload": { "type": "alert", "message": "something is broken!" } }` + +type Event struct { + Timestamp string + Payload any +} + +type AlertPayload struct { + Type string + Message string +} + +type PingPayload struct { + Type string + Ip string +} + +typeMap := TypeMap{ + "alert": reflect.TypeOf(AlertPayload{}), + "ping": reflect.TypeOf(PingPayload{}), +} + +// parse the JSON into a map +var jsonMap map[string]any +if err := json.Unmarshal([]byte(alertEventJson), &jsonMap); err != nil { + t.Fatalf("error unmarshalling JSON: %s", err) +} + +// create a polymorpher that assigns the type of the payload based on the type field +err, polymorpher := NewPolymorphismBuilder(). + DefineTypeAt("payload"). + UsingTypeMap(typeMap). + WithDiscriminatorAt("type"). + Build() +if err != nil { + t.Fatalf("error building polymorpher: %s", err) +} + +// create a new event +var event Event +err, assigned := polymorpher.AssignTargetType(&jsonMap, &event) +if err != nil { + t.Fatalf("error assigning target type: %s", err) +} else if !assigned { + t.Fatalf("no type assigned") +} + +// use mapstructure to unmarshal the payload into the event +if err := mapstructure.Decode(jsonMap, &event); err != nil { + t.Fatalf("error decoding JSON map: %s", err) +} + +// continue to work with the event +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/examples/example_test.go b/examples/example_test.go new file mode 100644 index 0000000..fb8245a --- /dev/null +++ b/examples/example_test.go @@ -0,0 +1,119 @@ +package examples + +import ( + "encoding/json" + "fmt" + "github.com/SoulKa/golymorph" + "github.com/mitchellh/mapstructure" + "reflect" + "testing" +) + +func TestBasicPolymorphismFromJson(t *testing.T) { + + // get a JSON that contains a payload with a type field that determines the type of the payload + alertEventJson := `{ "timestamp": "2023-11-27T22:14:09+00:00", "payload": { "type": "alert", "message": "something is broken!" } }` + + type Event struct { + Timestamp string + Payload any + } + + type AlertPayload struct { + Type string + Message string + } + + type PingPayload struct { + Type string + Ip string + } + + // define a mapping from the type value to the type of the payload + typeMap := golymorph.TypeMap{ + "alert": reflect.TypeOf(AlertPayload{}), + "ping": reflect.TypeOf(PingPayload{}), + } + + // create a TypeResolver that assigns the type of the payload based on the type field + err, resolver := golymorph.NewPolymorphismBuilder(). + DefineTypeAt("payload"). + UsingTypeMap(typeMap). + WithDiscriminatorAt("type"). + Build() + if err != nil { + t.Fatalf("error building polymorpher: %s", err) + } + + // create a new event + var event Event + if err, b := golymorph.UnmarshalJSON(resolver, []byte(alertEventJson), &event); err != nil { + t.Fatalf("error unmarshalling event: %s", err) + } else if !b { + t.Fatalf("no type assigned") + } + + // continue to work with the event + fmt.Printf("event: %+v\n", event) + fmt.Printf("event payload: %T %+v\n", event.Payload, event.Payload.(AlertPayload)) +} + +func TestBasicPolymorphismWithManualParsing(t *testing.T) { + + // get a JSON that contains a payload with a type field that determines the type of the payload + alertEventJson := `{ "timestamp": "2023-11-27T22:14:09+00:00", "payload": { "type": "alert", "message": "something is broken!" } }` + + type Event struct { + Timestamp string + Payload any + } + + type AlertPayload struct { + Type string + Message string + } + + type PingPayload struct { + Type string + Ip string + } + + typeMap := golymorph.TypeMap{ + "alert": reflect.TypeOf(AlertPayload{}), + "ping": reflect.TypeOf(PingPayload{}), + } + + // parse the JSON into a map + var jsonMap map[string]any + if err := json.Unmarshal([]byte(alertEventJson), &jsonMap); err != nil { + t.Fatalf("error unmarshalling JSON: %s", err) + } + + // create a polymorpher that assigns the type of the payload based on the type field + err, polymorpher := golymorph.NewPolymorphismBuilder(). + DefineTypeAt("payload"). + UsingTypeMap(typeMap). + WithDiscriminatorAt("type"). + Build() + if err != nil { + t.Fatalf("error building polymorpher: %s", err) + } + + // create a new event + var event Event + err, assigned := polymorpher.AssignTargetType(&jsonMap, &event) + if err != nil { + t.Fatalf("error assigning target type: %s", err) + } else if !assigned { + t.Fatalf("no type assigned") + } + + // use mapstructure to unmarshal the payload into the event + if err := mapstructure.Decode(jsonMap, &event); err != nil { + t.Fatalf("error decoding JSON map: %s", err) + } + + // continue to work with the event + fmt.Printf("event: %+v\n", event) + fmt.Printf("event payload: %T %+v\n", event.Payload, event.Payload.(AlertPayload)) +} diff --git a/polymorphism.go b/polymorphism.go new file mode 100644 index 0000000..b5bb022 --- /dev/null +++ b/polymorphism.go @@ -0,0 +1,52 @@ +package golymorph + +import ( + "encoding/json" + "github.com/SoulKa/golymorph/objectpath" + "github.com/mitchellh/mapstructure" +) + +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, bool) { + + // parse JSON + var jsonMap map[string]any + if err := json.Unmarshal(data, &jsonMap); err != nil { + return err, false + } + + // resolve polymorphism + if err, b := Decode(resolver, jsonMap, output); err != nil { + return err, false + } else if !b { + return nil, false + } + + // success + return nil, true +} + +// 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, bool) { + + // create a new event + err, assigned := resolver.AssignTargetType(&source, output) + if err != nil { + return err, false + } else if !assigned { + return nil, false + } + + // use mapstructure to unmarshal the payload into the event + if err := mapstructure.Decode(source, output); err != nil { + return err, false + } + + // success + return nil, true +} diff --git a/polymorphism_builder.go b/polymorphism_builder.go index 1c1a494..5eed5c5 100644 --- a/polymorphism_builder.go +++ b/polymorphism_builder.go @@ -1,6 +1,7 @@ package golymorph import ( + "errors" "github.com/SoulKa/golymorph/objectpath" "strings" ) @@ -32,7 +33,7 @@ type PolymorphismBuilderStrategySelector interface { type PolymorphismBuilderRuleAdder interface { UsingRule(rule Rule) PolymorphismBuilderRuleAdder - Build() ([]error, Polymorpher) + Build() (error, TypeResolver) } type PolymorphismBuilderDiscriminatorKeyDefiner interface { @@ -40,7 +41,7 @@ type PolymorphismBuilderDiscriminatorKeyDefiner interface { } type PolymorphismBuilderFinalizer interface { - Build() ([]error, Polymorpher) + Build() (error, TypeResolver) } func NewPolymorphismBuilder() PolymorphismBuilderEmpty { @@ -92,16 +93,16 @@ func (b *PolymorphismTypeMapBuilder) WithDiscriminatorAt(discriminatorKey string return b } -func (b *PolymorphismRuleBuilder) Build() ([]error, Polymorpher) { +func (b *PolymorphismRuleBuilder) Build() (error, TypeResolver) { if len(b.errors) > 0 { - return b.errors, nil + return errors.Join(b.errors...), nil } - return b.errors, &RulePolymorphism{b.targetPath, b.rules} + return nil, &RulePolymorphism{Polymorphism{b.targetPath}, b.rules} } -func (b *PolymorphismTypeMapBuilder) Build() ([]error, Polymorpher) { +func (b *PolymorphismTypeMapBuilder) Build() (error, TypeResolver) { if len(b.errors) > 0 { - return b.errors, nil + return errors.Join(b.errors...), nil } - return b.errors, &TypeMapPolymorphism{b.targetPath, b.discriminatorPath, b.typeMap} + return nil, &TypeMapPolymorphism{Polymorphism{b.targetPath}, b.discriminatorPath, b.typeMap} } diff --git a/polymorphism_builder_test.go b/polymorphism_builder_test.go index e406029..7ce6bfe 100644 --- a/polymorphism_builder_test.go +++ b/polymorphism_builder_test.go @@ -28,15 +28,15 @@ func TestPolymorphismBuilder_UsingRule(t *testing.T) { } // Act - errors, polymorphism := NewPolymorphismBuilder(). + err, polymorphism := NewPolymorphismBuilder(). DefineTypeAt("foo/bar"). UsingRule(rule1). UsingRule(rule2). Build() // Assert - if HasErrors(t, errors) { - t.Fatalf("expected no errors, but got %d errors", len(errors)) + if err != nil { + t.Fatalf("expected no errors, but got %s", err) } else if polymorphism == nil { t.Fatalf("expected polymorphism to not be nil") } @@ -50,15 +50,15 @@ func TestPolymorphismBuilder_UsingTypeMap(t *testing.T) { } // Act - errors, polymorphism := NewPolymorphismBuilder(). + err, polymorphism := NewPolymorphismBuilder(). DefineTypeAt("foo/bar"). UsingTypeMap(typeMap). WithDiscriminatorAt("foo/bar/discriminator"). Build() // Assert - if HasErrors(t, errors) { - t.Fatalf("expected no errors, but got %d errors", len(errors)) + if err != nil { + t.Fatalf("expected no errors, but got %s", err) } else if polymorphism == nil { t.Fatalf("expected polymorphism to not be nil") } diff --git a/rule_polymorphism.go b/rule_polymorphism.go index 867b15e..6f2b441 100644 --- a/rule_polymorphism.go +++ b/rule_polymorphism.go @@ -7,8 +7,7 @@ import ( // RulePolymorphism is a mapper that assigns a target type based on the given rules type RulePolymorphism struct { - // targetPath is the path to the object to assign the new type to - targetPath objectpath.ObjectPath + Polymorphism // rules is a list of rules to apply. The first rule that matches is used to determine the target type. rules []Rule diff --git a/type_map_polymorphism.go b/type_map_polymorphism.go index fc7f625..e93980d 100644 --- a/type_map_polymorphism.go +++ b/type_map_polymorphism.go @@ -9,8 +9,7 @@ import ( // TypeMapPolymorphism is a mapper that assigns a target type based on a discriminator value and a type map type TypeMapPolymorphism struct { - // targetPath is the path to the object to assign the new type to - targetPath objectpath.ObjectPath + Polymorphism // discriminatorPath is the path to the discriminator value discriminatorPath objectpath.ObjectPath diff --git a/polymorpher.go b/type_resolver.go similarity index 87% rename from polymorpher.go rename to type_resolver.go index af147de..f47a0a3 100644 --- a/polymorpher.go +++ b/type_resolver.go @@ -1,10 +1,12 @@ package golymorph -import "reflect" +import ( + "reflect" +) type TypeMap map[any]reflect.Type -type Polymorpher interface { +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. AssignTargetType(source any, target any) (error, bool) diff --git a/polymorpher_test.go b/type_resolver_test.go similarity index 93% rename from polymorpher_test.go rename to type_resolver_test.go index da1ed52..8d18843 100644 --- a/polymorpher_test.go +++ b/type_resolver_test.go @@ -38,13 +38,13 @@ var testCases = []TestCase{ func TestPolymorphism_AssignTargetType(t *testing.T) { // Arrange - errs, polymorphism := NewPolymorphismBuilder(). + err, polymorphism := NewPolymorphismBuilder(). DefineTypeAt("specifics"). UsingTypeMap(animalTypeMap). WithDiscriminatorAt("type"). Build() - if HasErrors(t, errs) { - t.Fatalf("error creating polymorphism: %s", errs) + if err != nil { + t.Fatalf("error building polymorphism: %s", err) } t.Logf("polymorphism: %+v\n", polymorphism) From e4d0eed41356f950c3cf2407c81d28ede1b1f253 Mon Sep 17 00:00:00 2001 From: SoulKa Date: Tue, 28 Nov 2023 11:46:04 +0100 Subject: [PATCH 2/4] removed bool return value from AssignTargetType() --- examples/example_test.go | 9 ++------- polymorphism.go | 25 ++++++++++--------------- rule_polymorphism.go | 10 +++++----- type_map_polymorphism.go | 10 +++++----- type_resolver.go | 2 +- type_resolver_test.go | 4 +--- 6 files changed, 24 insertions(+), 36 deletions(-) diff --git a/examples/example_test.go b/examples/example_test.go index fb8245a..0773618 100644 --- a/examples/example_test.go +++ b/examples/example_test.go @@ -47,10 +47,8 @@ func TestBasicPolymorphismFromJson(t *testing.T) { // create a new event var event Event - if err, b := golymorph.UnmarshalJSON(resolver, []byte(alertEventJson), &event); err != nil { + if err := golymorph.UnmarshalJSON(resolver, []byte(alertEventJson), &event); err != nil { t.Fatalf("error unmarshalling event: %s", err) - } else if !b { - t.Fatalf("no type assigned") } // continue to work with the event @@ -101,11 +99,8 @@ func TestBasicPolymorphismWithManualParsing(t *testing.T) { // create a new event var event Event - err, assigned := polymorpher.AssignTargetType(&jsonMap, &event) - if err != nil { + if err := polymorpher.AssignTargetType(&jsonMap, &event); err != nil { t.Fatalf("error assigning target type: %s", err) - } else if !assigned { - t.Fatalf("no type assigned") } // use mapstructure to unmarshal the payload into the event diff --git a/polymorphism.go b/polymorphism.go index b5bb022..4aa2c72 100644 --- a/polymorphism.go +++ b/polymorphism.go @@ -12,41 +12,36 @@ type Polymorphism struct { } // UnmarshalJSON unmarshals the given JSON data into the given output object using the given TypeResolver. -func UnmarshalJSON(resolver TypeResolver, data []byte, output any) (error, bool) { +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, false + return err } // resolve polymorphism - if err, b := Decode(resolver, jsonMap, output); err != nil { - return err, false - } else if !b { - return nil, false + if err := Decode(resolver, jsonMap, output); err != nil { + return err } // success - return nil, true + 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, bool) { +func Decode(resolver TypeResolver, source map[string]any, output any) error { // create a new event - err, assigned := resolver.AssignTargetType(&source, output) - if err != nil { - return err, false - } else if !assigned { - return nil, false + 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, false + return err } // success - return nil, true + return nil } diff --git a/rule_polymorphism.go b/rule_polymorphism.go index 6f2b441..abc3ad2 100644 --- a/rule_polymorphism.go +++ b/rule_polymorphism.go @@ -13,19 +13,19 @@ type RulePolymorphism struct { rules []Rule } -func (p *RulePolymorphism) AssignTargetType(source any, target any) (error, bool) { +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 { if err, matches := rule.Matches(source); err != nil { - return errors.Join(errors.New("error applying rule"), err), false + return errors.Join(errors.New("error applying rule"), err) } else if matches { if err := objectpath.AssignTypeAtPath(target, p.targetPath, rule.NewType); err != nil { - return errors.Join(errors.New("error assigning type to target"), err), false + return errors.Join(errors.New("error assigning type to target"), err) } - return nil, true + return nil } } - return nil, false + return nil } diff --git a/type_map_polymorphism.go b/type_map_polymorphism.go index e93980d..eb282d5 100644 --- a/type_map_polymorphism.go +++ b/type_map_polymorphism.go @@ -18,21 +18,21 @@ type TypeMapPolymorphism struct { typeMap TypeMap } -func (p *TypeMapPolymorphism) AssignTargetType(source any, target any) (error, bool) { +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 { - return errors.Join(errors.New("error getting discriminator value"), err), false + 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 { - return nil, false + return nil } else if err := objectpath.AssignTypeAtPath(target, p.targetPath, newType); err != nil { - return errors.Join(errors.New("error assigning type to target"), err), false + return errors.Join(errors.New("error assigning type to target"), err) } - return nil, true + return nil } diff --git a/type_resolver.go b/type_resolver.go index f47a0a3..2d23b1d 100644 --- a/type_resolver.go +++ b/type_resolver.go @@ -9,5 +9,5 @@ type TypeMap map[any]reflect.Type 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. - AssignTargetType(source any, target any) (error, bool) + AssignTargetType(source any, target any) error } diff --git a/type_resolver_test.go b/type_resolver_test.go index 8d18843..5c49e02 100644 --- a/type_resolver_test.go +++ b/type_resolver_test.go @@ -59,10 +59,8 @@ func TestPolymorphism_AssignTargetType(t *testing.T) { // Act var actualAnimal Animal - if err, applied := polymorphism.AssignTargetType(&actualAnimalJson, &actualAnimal); err != nil { + if err := polymorphism.AssignTargetType(&actualAnimalJson, &actualAnimal); err != nil { t.Fatalf("error assigning target type to horse: %s", err) - } else if !applied { - t.Fatalf("expected polymorphism to be applied") } t.Logf("actualAnimal: %+v\n", actualAnimal) From ff7710f027805aafd6fc88f539cff5b35597fc8b Mon Sep 17 00:00:00 2001 From: SoulKa Date: Tue, 28 Nov 2023 13:24:30 +0100 Subject: [PATCH 3/4] returning UnresolvedTypeError if no rule matched --- error/unresolved_type_error.go | 13 +++++++++++++ rule_polymorphism.go | 7 ++++++- type_map_polymorphism.go | 6 +++++- type_resolver.go | 1 + 4 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 error/unresolved_type_error.go diff --git a/error/unresolved_type_error.go b/error/unresolved_type_error.go new file mode 100644 index 0000000..874cec6 --- /dev/null +++ b/error/unresolved_type_error.go @@ -0,0 +1,13 @@ +package error + +import "fmt" + +// UnresolvedTypeError is an error that occurs when a type cannot be resolved when applying a polymorphism +type UnresolvedTypeError struct { + Err error + TargetPath string +} + +func (e *UnresolvedTypeError) Error() string { + return fmt.Sprintf("unresolved type error at [%s]: %s", e.TargetPath, e.Err.Error()) +} diff --git a/rule_polymorphism.go b/rule_polymorphism.go index abc3ad2..4d1e5b9 100644 --- a/rule_polymorphism.go +++ b/rule_polymorphism.go @@ -2,6 +2,7 @@ package golymorph import ( "errors" + golimorphError "github.com/SoulKa/golymorph/error" "github.com/SoulKa/golymorph/objectpath" ) @@ -26,6 +27,10 @@ func (p *RulePolymorphism) AssignTargetType(source any, target any) error { return nil } } - return nil + // no rule matched + return &golimorphError.UnresolvedTypeError{ + Err: errors.New("no rule matched"), + TargetPath: p.targetPath.String(), + } } diff --git a/type_map_polymorphism.go b/type_map_polymorphism.go index eb282d5..fb03323 100644 --- a/type_map_polymorphism.go +++ b/type_map_polymorphism.go @@ -3,6 +3,7 @@ package golymorph import ( "errors" "fmt" + golimorphError "github.com/SoulKa/golymorph/error" "github.com/SoulKa/golymorph/objectpath" "reflect" ) @@ -30,7 +31,10 @@ func (p *TypeMapPolymorphism) AssignTargetType(source any, target any) error { // get type from type map if newType, ok := p.typeMap[rawDiscriminatorValue]; !ok { - return nil + return &golimorphError.UnresolvedTypeError{ + Err: fmt.Errorf("type map does not contain any key of value [%+v]", rawDiscriminatorValue), + TargetPath: p.targetPath.String(), + } } else if err := objectpath.AssignTypeAtPath(target, p.targetPath, newType); err != nil { return errors.Join(errors.New("error assigning type to target"), err) } diff --git a/type_resolver.go b/type_resolver.go index 2d23b1d..088c1c0 100644 --- a/type_resolver.go +++ b/type_resolver.go @@ -9,5 +9,6 @@ type TypeMap map[any]reflect.Type 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 } From 594b6dbeeb88b133a990e973132ce3273fcbe0d0 Mon Sep 17 00:00:00 2001 From: SoulKa Date: Tue, 28 Nov 2023 13:31:56 +0100 Subject: [PATCH 4/4] update examples --- README.md | 99 ++++++++++++++++++++-------------------- examples/example_test.go | 7 +-- 2 files changed, 54 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 55c95b9..d583a6e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # 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 + +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. ## Installation @@ -16,60 +19,58 @@ go get github.com/SoulKa/golymorph For usage and examples see the [Godoc](http://godoc.org/github.com/SoulKa/golymorph). ```go -// get a JSON that contains a payload with a type field that determines the type of the payload -alertEventJson := `{ "timestamp": "2023-11-27T22:14:09+00:00", "payload": { "type": "alert", "message": "something is broken!" } }` +package main -type Event struct { - Timestamp string - Payload any -} +import ( + "fmt" + "github.com/SoulKa/golymorph" + "reflect" +) -type AlertPayload struct { - Type string - Message string -} +func main() { + // get a JSON that contains a payload with a type field that determines the type of the payload + alertEventJson := `{ "timestamp": "2023-11-27T22:14:09+00:00", "payload": { "type": "alert", "message": "something is broken!" } }` -type PingPayload struct { - Type string - Ip string -} + // the parent type that contains the polymorphic payload + type Event struct { + Timestamp string + Payload any + } -typeMap := TypeMap{ - "alert": reflect.TypeOf(AlertPayload{}), - "ping": reflect.TypeOf(PingPayload{}), -} + // the polymorphic child types + type AlertPayload struct { + Type string + Message string + } + type PingPayload struct { + Type string + Ip string + } -// parse the JSON into a map -var jsonMap map[string]any -if err := json.Unmarshal([]byte(alertEventJson), &jsonMap); err != nil { - t.Fatalf("error unmarshalling JSON: %s", err) -} + // define a mapping from the type value to the type of the payload + typeMap := golymorph.TypeMap{ + "alert": reflect.TypeOf(AlertPayload{}), + "ping": reflect.TypeOf(PingPayload{}), + } -// create a polymorpher that assigns the type of the payload based on the type field -err, polymorpher := NewPolymorphismBuilder(). - DefineTypeAt("payload"). - UsingTypeMap(typeMap). - WithDiscriminatorAt("type"). - Build() -if err != nil { - t.Fatalf("error building polymorpher: %s", err) -} + // create a TypeResolver that assigns the type of the payload based on the type field + err, resolver := golymorph.NewPolymorphismBuilder(). + DefineTypeAt("payload"). + UsingTypeMap(typeMap). + WithDiscriminatorAt("type"). + Build() + if err != nil { + panic(fmt.Sprintf("error building polymorpher: %s", err)) + } -// create a new event -var event Event -err, assigned := polymorpher.AssignTargetType(&jsonMap, &event) -if err != nil { - t.Fatalf("error assigning target type: %s", err) -} else if !assigned { - t.Fatalf("no type assigned") -} + // create a new event + var event Event + if err := golymorph.UnmarshalJSON(resolver, []byte(alertEventJson), &event); err != nil { + panic(fmt.Sprintf("error unmarshalling event: %s", err)) + } -// use mapstructure to unmarshal the payload into the event -if err := mapstructure.Decode(jsonMap, &event); err != nil { - t.Fatalf("error decoding JSON map: %s", err) + // continue to work with the event + fmt.Printf("event: %+v\n", event) + fmt.Printf("event payload: %T %+v\n", event.Payload, event.Payload.(AlertPayload)) } - -// continue to work with the event -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/examples/example_test.go b/examples/example_test.go index 0773618..b55d3ef 100644 --- a/examples/example_test.go +++ b/examples/example_test.go @@ -14,16 +14,17 @@ func TestBasicPolymorphismFromJson(t *testing.T) { // get a JSON that contains a payload with a type field that determines the type of the payload alertEventJson := `{ "timestamp": "2023-11-27T22:14:09+00:00", "payload": { "type": "alert", "message": "something is broken!" } }` + // the parent type that contains the polymorphic payload type Event struct { Timestamp string Payload any } + // the polymorphic child types type AlertPayload struct { Type string Message string } - type PingPayload struct { Type string Ip string @@ -42,13 +43,13 @@ func TestBasicPolymorphismFromJson(t *testing.T) { WithDiscriminatorAt("type"). Build() if err != nil { - t.Fatalf("error building polymorpher: %s", err) + panic(fmt.Sprintf("error building polymorpher: %s", err)) } // create a new event var event Event if err := golymorph.UnmarshalJSON(resolver, []byte(alertEventJson), &event); err != nil { - t.Fatalf("error unmarshalling event: %s", err) + panic(fmt.Sprintf("error unmarshalling event: %s", err)) } // continue to work with the event