Skip to content

Commit

Permalink
Merge pull request #3 from SoulKa/feature/code-polishing
Browse files Browse the repository at this point in the history
Feature/code polishing
  • Loading branch information
SoulKa authored Nov 28, 2023
2 parents d56b7b8 + 061e0d5 commit 28df883
Show file tree
Hide file tree
Showing 12 changed files with 228 additions and 161 deletions.
45 changes: 38 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
Expand Down Expand Up @@ -73,4 +104,4 @@ func main() {
fmt.Printf("event: %+v\n", event)
fmt.Printf("event payload: %T %+v\n", event.Payload, event.Payload.(AlertPayload))
}
```
```
42 changes: 3 additions & 39 deletions polymorphism.go
Original file line number Diff line number Diff line change
@@ -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
}
94 changes: 34 additions & 60 deletions polymorphism_builder.go
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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}
}
4 changes: 2 additions & 2 deletions polymorphism_builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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()
Expand Down
25 changes: 25 additions & 0 deletions polymorphism_rule_builder.go
Original file line number Diff line number Diff line change
@@ -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}
}
34 changes: 34 additions & 0 deletions polymorphism_type_map_builder.go
Original file line number Diff line number Diff line change
@@ -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}
}
Loading

0 comments on commit 28df883

Please sign in to comment.