Skip to content

Commit

Permalink
feat(union-types): implementation of union type utilities (#55)
Browse files Browse the repository at this point in the history
This commit adds the utility functions for the following use cases:
- Unmarshalling OneOf types
- Unmarshalling AnyOf types
- Unmarshalling OneOf types with discriminators
- Unmarshalling AnyOf types with discriminators

It also adds the complete code coverage for these use cases with different types
  • Loading branch information
asadali214 authored Mar 1, 2024
1 parent 325d3e0 commit f2801f0
Show file tree
Hide file tree
Showing 8 changed files with 678 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .codeclimate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
exclude_paths:
- "**/mock_*.go"
- "**/*_test.go"
28 changes: 28 additions & 0 deletions utilities/marshal_error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package utilities

import (
"fmt"
)

type MarshalError struct {
structName string
innerError error
}

func NewMarshalError(structName string, err error) MarshalError {
return MarshalError{
structName: structName,
innerError: err,
}
}

// Error implements the Error function for the error interface.
// It returns a string representation of the MarshalError instance when used in an error context.
func (a MarshalError) Error() string {
indent := "\n\t=>"
switch a.innerError.(type) {
case MarshalError:
indent = "."
}
return fmt.Sprintf("%v %v %v", a.structName, indent, a.innerError)
}
46 changes: 46 additions & 0 deletions utilities/mock_model_atom.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package utilities

import (
"encoding/json"
"errors"
"strings"
)

type Atom struct {
NumberOfElectrons int `json:"number_of_electrons"`
NumberOfProtons int `json:"number_of_protons"`
}

func (a *Atom) UnmarshalJSON(input []byte) error {
var temp atom
err := json.Unmarshal(input, &temp)
if err != nil {
return NewMarshalError("Atom", err)
}
err = temp.validate(input)
if err != nil {
return err
}
a.NumberOfElectrons = *temp.NumberOfElectrons
a.NumberOfProtons = *temp.NumberOfProtons
return nil
}

type atom struct {
NumberOfElectrons *int `json:"number_of_electrons"`
NumberOfProtons *int `json:"number_of_protons"`
}

func (a *atom) validate(input []byte) error {
var errs []string
if a.NumberOfElectrons == nil {
errs = append(errs, "required field `NumberOfElectrons` is missing")
}
if a.NumberOfProtons == nil {
errs = append(errs, "required field `NumberOfProtons` is missing")
}
if len(errs) == 0 {
return nil
}
return NewMarshalError("Atom", errors.New(strings.Join(errs, "\n\t=> ")))
}
50 changes: 50 additions & 0 deletions utilities/mock_model_bike.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package utilities

import (
"encoding/json"
"errors"
"strings"

"github.com/apimatic/go-core-runtime/types"
)

type Bike struct {
Id int `json:"id"`
Roof *string `json:"roof"`
AirLevel types.Optional[Atom] `json:"air_level"`
Type *string `json:"type"`
}

func (b *Bike) UnmarshalJSON(input []byte) error {
var temp bike
err := json.Unmarshal(input, &temp)
if err != nil {
return NewMarshalError("Bike", err)
}
err = temp.validate(input)
if err != nil {
return err
}
b.Id = *temp.Id
b.Roof = temp.Roof
b.Type = temp.Type
return nil
}

type bike struct {
Id *int `json:"id"`
Roof *string `json:"roof"`
AirLevel types.Optional[Atom] `json:"air_level"`
Type *string `json:"type"`
}

func (b *bike) validate(input []byte) error {
var errs []string
if b.Id == nil {
errs = append(errs, "required field `Id` is missing")
}
if len(errs) == 0 {
return nil
}
return NewMarshalError("Bike", errors.New(strings.Join(errs, "\n\t=> ")))
}
46 changes: 46 additions & 0 deletions utilities/mock_model_car.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package utilities

import (
"encoding/json"
"errors"
"strings"
)

type Car struct {
Id int `json:"id"`
Roof *string `json:"roof"`
Type *string `json:"type"`
}

func (c *Car) UnmarshalJSON(input []byte) error {
var temp car
err := json.Unmarshal(input, &temp)
if err != nil {
return NewMarshalError("Car", err)
}
err = temp.validate(input)
if err != nil {
return err
}
c.Id = *temp.Id
c.Roof = temp.Roof
c.Type = temp.Type
return nil
}

type car struct {
Id *int `json:"id"`
Roof *string `json:"roof"`
Type *string `json:"type"`
}

func (c *car) validate(input []byte) error {
var errs []string
if c.Id == nil {
errs = append(errs, "required field `Id` is missing")
}
if len(errs) == 0 {
return nil
}
return NewMarshalError("Car", errors.New(strings.Join(errs, "\n\t=> ")))
}
49 changes: 49 additions & 0 deletions utilities/mock_model_truck.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package utilities

import (
"encoding/json"
"errors"
"strings"
)

type Truck struct {
Id int `json:"id"`
Weight string `json:"weight"`
Roof *string `json:"roof"`
}

func (c *Truck) UnmarshalJSON(input []byte) error {
var temp truck
err := json.Unmarshal(input, &temp)
if err != nil {
return NewMarshalError("Truck", err)
}
err = temp.validate(input)
if err != nil {
return err
}
c.Id = *temp.Id
c.Weight = *temp.Weight
c.Roof = temp.Roof
return nil
}

type truck struct {
Id *int `json:"id"`
Weight *string `json:"weight"`
Roof *string `json:"roof"`
}

func (t *truck) validate(input []byte) error {
var errs []string
if t.Id == nil {
errs = append(errs, "required field `Id` is missing")
}
if t.Weight == nil {
errs = append(errs, "required field `Weight` is missing")
}
if len(errs) == 0 {
return nil
}
return NewMarshalError("Truck", errors.New(strings.Join(errs, "\n\t=> ")))
}
135 changes: 135 additions & 0 deletions utilities/unionTypeHelper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package utilities

import (
"encoding/json"
"errors"
"fmt"
"reflect"
"strings"
)

type TypeHolder struct {
value any
isSelected *bool
discriminator string
typeError error
}

func NewTypeHolder(val any, isSelected *bool) *TypeHolder {
return &TypeHolder{
value: val,
isSelected: isSelected,
}
}

func NewTypeHolderDiscriminator(val any, flag *bool, discriminator string) *TypeHolder {
return &TypeHolder{
value: val,
isSelected: flag,
discriminator: discriminator,
}
}

func (t *TypeHolder) selectValue() any {
*t.isSelected = true
return t.value
}

func (t *TypeHolder) tryUnmarshall(data []byte) bool {
err := json.Unmarshal(data, t.value)
t.typeError = err
return err == nil
}

// UnmarshallAnyOf tries to unmarshal the data into each of the provided types as an AnyOf group
// and return the converted value
func UnmarshallAnyOf(data []byte, types ...*TypeHolder) (any, error) {
return unmarshallUnionType(data, types, false)
}

// UnmarshallAnyOfWithDiscriminator tries to unmarshal the data into each of the provided types
// as an AnyOf group with discriminators and return the converted value
func UnmarshallAnyOfWithDiscriminator(data []byte, discField string, types ...*TypeHolder) (any, error) {
return unmarshallUnionType(data, filterTypeHolders(data, types, discField), false)
}

// UnmarshallOneOf tries to unmarshal the data into each of the provided types as a OneOf group
// and return the converted value
func UnmarshallOneOf(data []byte, types ...*TypeHolder) (any, error) {
return unmarshallUnionType(data, types, true)
}

// UnmarshallOneOfWithDiscriminator tries to unmarshal the data into each of the provided types
// as a OneOf group with discriminators and return the converted value
func UnmarshallOneOfWithDiscriminator(data []byte, discField string, types ...*TypeHolder) (any, error) {
return unmarshallUnionType(data, filterTypeHolders(data, types, discField), true)
}

// filterTypeHolders filter out the typeholders from given list based on
// available discriminator field's value in the data
func filterTypeHolders(data []byte, types []*TypeHolder, discField string) []*TypeHolder {
discValue, ok := extractDiscriminatorValue(data, discField)
if !ok {
return types
}
for _, t := range types {
if t.discriminator != "" && t.discriminator == discValue {
return []*TypeHolder{t}
}
}
return types
}

// extractDiscriminatorValue extracts the discriminator value using the discriminator field
func extractDiscriminatorValue(data []byte, discField string) (any, bool) {
if discField == "" {
return nil, false
}
dict := map[string]any{}
err := json.Unmarshal(data, &dict)

if err != nil {
return nil, false
}
discValue, ok := dict[discField]

return discValue, ok
}

// unmarshallUnionType tries to unmarshal the byte array into each of the provided types
// and return the converted value
func unmarshallUnionType(data []byte, types []*TypeHolder, matchExactlyOneType bool) (any, error) {
var selected *TypeHolder
for _, t := range types {
if t.tryUnmarshall(data) {
if !matchExactlyOneType {
return t.selectValue(), nil
} else if selected != nil {
return nil, moreThenOneTypeMatchesError(selected, t, data)
}
selected = t
}
}
if matchExactlyOneType && selected != nil {
return selected.selectValue(), nil
}
return nil, noneTypeMatchesError(types, data)
}

func moreThenOneTypeMatchesError(type1 *TypeHolder, type2 *TypeHolder, data []byte) error {
type1Name := reflect.TypeOf(type1.value).String()
type2Name := reflect.TypeOf(type2.value).String()
return errors.New("There are more than one matching types i.e. {" + type1Name + " and " + type2Name + "} on: " + string(data))
}

func noneTypeMatchesError(types []*TypeHolder, data []byte) error {
names := make([]string, len(types))
reasons := make([]string, len(types))

for i, t := range types {
names[i] = reflect.TypeOf(t.value).String()
reasons[i] = "\n\nError " + fmt.Sprint(i+1) + ":\n => " + t.typeError.Error()
}

return errors.New("We could not match any acceptable type from {" + strings.Join(names, ", ") + "} on: " + string(data) + strings.Join(reasons, ""))
}
Loading

0 comments on commit f2801f0

Please sign in to comment.