-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(union-types): implementation of union type utilities (#55)
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
1 parent
325d3e0
commit f2801f0
Showing
8 changed files
with
678 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
exclude_paths: | ||
- "**/mock_*.go" | ||
- "**/*_test.go" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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=> "))) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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=> "))) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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=> "))) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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=> "))) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, "")) | ||
} |
Oops, something went wrong.