Skip to content

Commit

Permalink
Underlying type fix (#79)
Browse files Browse the repository at this point in the history
* start

* add tests

* add test for inline discriminator

* add checker for inline field

* add jareds tests

* fix inline discriminator check

* add test for mismatched discriminator types

* fix error message

* fix one-of and subtype discriminator mismatch detection for ints

* refactor conditionals

* remove dead code

* try to panic

* move validation call and refactor tests

* rename tests

* upgrade go-assert

* tidy up

* debug

* fixed it

* ineffectual err

* rm dead code

* whitespace

* rm dead code

* rm unused

* refactor to switch

* make function private

* use panicscontains

* small change

* add map change to validate type

* hopefully fix interface type access

* also lint

* add oneof scope scope schema

* dry out test

* nolint function length on test

* add inline default

* remove import parens

* finish function refactor

* rm unnecessary lines

* improve coverage

* fix lint

* rm comment

* use a string discriminator for test

* clarify scope scope schema

* rm whitespace
  • Loading branch information
mfleader authored Feb 21, 2024
1 parent 840fad3 commit 6e37a6a
Show file tree
Hide file tree
Showing 9 changed files with 565 additions and 45 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.21

require (
github.com/fxamacker/cbor/v2 v2.5.0
go.arcalot.io/assert v1.7.0
go.arcalot.io/assert v1.8.0
go.arcalot.io/log/v2 v2.1.0
gopkg.in/yaml.v3 v3.0.1
)
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADi
github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
go.arcalot.io/assert v1.7.0 h1:PTLyeisNMUKpM9wXRDxResanBhuGOYO1xFK3v5b3FSw=
go.arcalot.io/assert v1.7.0/go.mod h1:nNmWPoNUHFyrPkNrD2aASm5yPuAfiWdB/4X7Lw3ykHk=
go.arcalot.io/assert v1.8.0 h1:hGcHMPncQXwQvjj7MbyOu2gg8VIBB00crUJZpeQOjxs=
go.arcalot.io/assert v1.8.0/go.mod h1:nNmWPoNUHFyrPkNrD2aASm5yPuAfiWdB/4X7Lw3ykHk=
go.arcalot.io/log/v2 v2.1.0 h1:lNO931hJ82LgS6WcCFCxpLWXQXPFhOkz6PyAJ/augq4=
go.arcalot.io/log/v2 v2.1.0/go.mod h1:PNWOSkkPmgS2OMlWTIlB/WqOw0yaBvDYd8ENAP80H4k=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
Expand Down
124 changes: 90 additions & 34 deletions schema/oneof.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package schema

import (
"fmt"
"maps"
"reflect"
"strings"
)
Expand All @@ -18,6 +19,8 @@ type OneOfSchema[KeyType int64 | string] struct {
interfaceType reflect.Type
TypesValue map[KeyType]Object `json:"types"`
DiscriminatorFieldNameValue string `json:"discriminator_field_name"`
// whether or not the discriminator is inlined in the underlying objects' schema
DiscriminatorInlined bool `json:"discriminator_inlined"`
}

func (o OneOfSchema[KeyType]) TypeID() TypeID {
Expand All @@ -44,6 +47,11 @@ func (o OneOfSchema[KeyType]) ApplyScope(scope Scope) {
for _, t := range o.TypesValue {
t.ApplyScope(scope)
}
// scope must be applied before we can access the subtypes' properties
err := o.validateSubtypeDiscriminatorInlineFields()
if err != nil {
panic(err)
}
}

func (o OneOfSchema[KeyType]) ReflectedType() reflect.Type {
Expand All @@ -56,11 +64,14 @@ func (o OneOfSchema[KeyType]) ReflectedType() reflect.Type {

//nolint:funlen
func (o OneOfSchema[KeyType]) UnserializeType(data any) (result any, err error) {
if data == nil {
return nil, fmt.Errorf("bug: data is nil in OneOfSchema UnserializeType")
}
reflectedValue := reflect.ValueOf(data)
if reflectedValue.Kind() != reflect.Map {
return result, &ConstraintError{
Message: fmt.Sprintf(
"Invalid type for one-of type: '%s'. Expected map.",
"Invalid type for one-of type: %q. Expected map.",
reflect.TypeOf(data).Name(),
),
}
Expand All @@ -77,6 +88,7 @@ func (o OneOfSchema[KeyType]) UnserializeType(data any) (result any, err error)
if err != nil {
return result, err
}

typedData := make(map[string]any, reflectedValue.Len())
for _, k := range reflectedValue.MapKeys() {
v := reflectedValue.MapIndex(k)
Expand All @@ -102,32 +114,35 @@ func (o OneOfSchema[KeyType]) UnserializeType(data any) (result any, err error)
}
return result, &ConstraintError{
Message: fmt.Sprintf(
"Invalid value for '%s', expected one of: %s",
"Invalid value for %q, expected one of: %s",
o.DiscriminatorFieldNameValue,
strings.Join(validDiscriminators, ", "),
),
}
}

if _, ok := selectedType.Properties()[o.DiscriminatorFieldNameValue]; !ok {
delete(typedData, o.DiscriminatorFieldNameValue)
}

unserializedData, err := selectedType.Unserialize(typedData)
cloneData := o.deleteDiscriminator(typedData)
unserializedData, err := selectedType.Unserialize(cloneData)
if err != nil {
return result, err
}
if o.interfaceType == nil {
return unserializedData, nil
unserializedMap, ok := unserializedData.(map[string]any)
if ok {
unserializedMap[o.DiscriminatorFieldNameValue] = discriminator
return unserializedMap, nil
}
return saveConvertTo(unserializedData, o.interfaceType)
return saveConvertTo(unserializedData, o.ReflectedType())
}

func (o OneOfSchema[KeyType]) ValidateType(data any) error {
discriminatorValue, underlyingType, err := o.findUnderlyingType(data)
if err != nil {
return err
}
dataMap, ok := data.(map[string]any)
if ok {
data = o.deleteDiscriminator(dataMap)
}
if err := underlyingType.Validate(data); err != nil {
return ConstraintErrorAddPathSegment(err, fmt.Sprintf("{oneof[%v]}", discriminatorValue))
}
Expand All @@ -139,6 +154,10 @@ func (o OneOfSchema[KeyType]) SerializeType(data any) (any, error) {
if err != nil {
return nil, err
}
dataMap, ok := data.(map[string]any)
if ok {
data = o.deleteDiscriminator(dataMap)
}
serializedData, err := underlyingType.Serialize(data)
if err != nil {
return nil, err
Expand All @@ -162,7 +181,8 @@ func (o OneOfSchema[KeyType]) ValidateCompatibility(typeOrData any) error {
// If not, verify it as data.
inputAsMap, ok := typeOrData.(map[string]any)
if ok {
return o.validateMap(inputAsMap)
_, _, err := o.validateMap(inputAsMap)
return err
}
value := reflect.ValueOf(typeOrData)
if reflect.Indirect(value).Kind() != reflect.Struct {
Expand Down Expand Up @@ -217,21 +237,22 @@ func (o OneOfSchema[KeyType]) validateSchema(otherSchema OneOfSchema[KeyType]) e
return nil
}

func (o OneOfSchema[KeyType]) validateMap(data map[string]any) error {
func (o OneOfSchema[KeyType]) validateMap(data map[string]any) (KeyType, Object, error) {
var nilKey KeyType
// Validate that it has the discriminator field.
// If it doesn't, fail
// If it does, pass the non-discriminator fields into the ValidateCompatibility method for the object
selectedTypeID := data[o.DiscriminatorFieldNameValue]
if selectedTypeID == nil {
return &ConstraintError{
return nilKey, nil, &ConstraintError{
Message: fmt.Sprintf(
"validation failed for OneOfSchema. Discriminator field '%s' missing", o.DiscriminatorFieldNameValue),
}
}
// Ensure it's the correct type
selectedTypeIDAsserted, ok := selectedTypeID.(KeyType)
if !ok {
return &ConstraintError{
return nilKey, nil, &ConstraintError{
Message: fmt.Sprintf(
"validation failed for OneOfSchema. Discriminator field '%v' has invalid type '%T'. Expected %T",
o.DiscriminatorFieldNameValue, selectedTypeID, selectedTypeIDAsserted),
Expand All @@ -240,24 +261,22 @@ func (o OneOfSchema[KeyType]) validateMap(data map[string]any) error {
// Find the object that's associated with the selected type
selectedSchema := o.TypesValue[selectedTypeIDAsserted]
if selectedSchema == nil {
return &ConstraintError{
return nilKey, nil, &ConstraintError{
Message: fmt.Sprintf(
"validation failed for OneOfSchema. Discriminator value '%v' is invalid. Expected one of: %v",
selectedTypeIDAsserted, o.getTypeValues()),
}
}
if selectedSchema.Properties()[o.DiscriminatorFieldNameValue] == nil { // Check to see if the discriminator is part of the sub-object.
delete(data, o.DiscriminatorFieldNameValue) // The discriminator isn't part of the object.
}
err := selectedSchema.ValidateCompatibility(data)
cloneData := o.deleteDiscriminator(data)
err := selectedSchema.ValidateCompatibility(cloneData)
if err != nil {
return &ConstraintError{
return nilKey, nil, &ConstraintError{
Message: fmt.Sprintf(
"validation failed for OneOfSchema. Failed to validate as selected schema type '%T' from discriminator value '%v' (%s)",
selectedSchema, selectedTypeIDAsserted, err),
}
}
return nil
return selectedTypeIDAsserted, selectedSchema, nil
}

func (o OneOfSchema[KeyType]) getTypeValues() []KeyType {
Expand All @@ -271,21 +290,15 @@ func (o OneOfSchema[KeyType]) getTypeValues() []KeyType {
}

func (o OneOfSchema[KeyType]) Validate(data any) error {
if o.interfaceType == nil {
return o.ValidateType(data)
}
d, err := saveConvertTo(data, o.interfaceType)
d, err := saveConvertTo(data, o.ReflectedType())
if err != nil {
return err
}
return o.ValidateType(d)
}

func (o OneOfSchema[KeyType]) Serialize(data any) (result any, err error) {
if o.interfaceType == nil {
return nil, o.ValidateType(data)
}
d, err := saveConvertTo(data, o.interfaceType)
d, err := saveConvertTo(data, o.ReflectedType())
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -328,20 +341,29 @@ func (o OneOfSchema[KeyType]) getTypedDiscriminator(discriminator any) (KeyType,
}

func (o OneOfSchema[KeyType]) findUnderlyingType(data any) (KeyType, Object, error) {
var nilKey KeyType

reflectedType := reflect.TypeOf(data)
if reflectedType.Kind() != reflect.Struct &&
reflectedType.Kind() != reflect.Map &&
(reflectedType.Kind() != reflect.Pointer || reflectedType.Elem().Kind() != reflect.Struct) {
var defaultValue KeyType
return defaultValue, nil, &ConstraintError{

return nilKey, nil, &ConstraintError{
Message: fmt.Sprintf(
"Invalid type for one-of type: '%s' expected struct or map.",
"Invalid type for one-of type: %q expected struct or map.",
reflect.TypeOf(data).Name(),
),
}
}

var foundKey *KeyType
if reflectedType.Kind() == reflect.Map {
myKey, mySchemaObj, err := o.validateMap(data.(map[string]any))
if err != nil {
return nilKey, nil, err
}
return myKey, mySchemaObj, nil
}
for key, ref := range o.TypesValue {
underlyingReflectedType := ref.ReflectedType()
if underlyingReflectedType == reflectedType {
Expand All @@ -350,7 +372,6 @@ func (o OneOfSchema[KeyType]) findUnderlyingType(data any) (KeyType, Object, err
}
}
if foundKey == nil {
var defaultValue KeyType
dataType := reflect.TypeOf(data)
values := make([]string, len(o.TypesValue))
i := 0
Expand All @@ -361,7 +382,7 @@ func (o OneOfSchema[KeyType]) findUnderlyingType(data any) (KeyType, Object, err
}
i++
}
return defaultValue, nil, &ConstraintError{
return nilKey, nil, &ConstraintError{
Message: fmt.Sprintf(
"Invalid type for one-of schema: '%s' (valid types are: %s)",
dataType.String(),
Expand All @@ -371,3 +392,38 @@ func (o OneOfSchema[KeyType]) findUnderlyingType(data any) (KeyType, Object, err
}
return *foundKey, o.TypesValue[*foundKey], nil
}

// validateSubtypeDiscriminatorInlineFields checks to see if a subtype's
// discriminator field has been written in accordance with the OneOfSchema's
// declaration.
func (o OneOfSchema[KeyType]) validateSubtypeDiscriminatorInlineFields() error {
for key, typeValue := range o.TypesValue {
typeValueDiscriminatorValue, hasDiscriminator := typeValue.Properties()[o.DiscriminatorFieldNameValue]
switch {
case !o.DiscriminatorInlined && hasDiscriminator:
return fmt.Errorf(
"object id %q has conflicting field %q; either remove that field or set inline to true for %T[%T]",
typeValue.ID(), o.DiscriminatorFieldNameValue, o, key)
case o.DiscriminatorInlined && !hasDiscriminator:
return fmt.Errorf(
"object id %q needs discriminator field %q; either add that field or set inline to false for %T[%T]",
typeValue.ID(), o.DiscriminatorFieldNameValue, o, key)
case o.DiscriminatorInlined && hasDiscriminator &&
(typeValueDiscriminatorValue.ReflectedType().Kind() != reflect.TypeOf(key).Kind()):
return fmt.Errorf(
"the type of object id %v's discriminator field %q does not match OneOfSchema discriminator type; expected %v got %T",
typeValue.ID(), o.DiscriminatorFieldNameValue, typeValueDiscriminatorValue.TypeID(), key)
}
}
return nil
}

func (o OneOfSchema[KeyType]) deleteDiscriminator(mymap map[string]any) map[string]any {
// the discriminator is not a property of the subtype
if !o.DiscriminatorInlined {
cloneData := maps.Clone(mymap)
delete(cloneData, o.DiscriminatorFieldNameValue)
return cloneData
}
return mymap
}
2 changes: 2 additions & 0 deletions schema/oneof_int.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ type OneOfInt interface {
func NewOneOfIntSchema[ItemsInterface any](
types map[int64]Object,
discriminatorFieldName string,
discriminatorInlined bool,
) *OneOfSchema[int64] {
var defaultValue ItemsInterface
return &OneOfSchema[int64]{
reflect.TypeOf(&defaultValue).Elem(),
types,
discriminatorFieldName,
discriminatorInlined,
}
}
4 changes: 4 additions & 0 deletions schema/oneof_int_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ var oneOfIntTestObjectAProperties = map[string]*schema.PropertySchema{
2: schema.NewRefSchema("C", nil),
},
"_type",
false,
),
nil,
true,
Expand All @@ -39,6 +40,7 @@ var oneOfIntTestObjectAbProperties = map[string]*schema.PropertySchema{
2: schema.NewRefSchema("C", nil),
},
"_difftype",
false,
),
nil,
true,
Expand All @@ -59,6 +61,7 @@ var oneOfIntTestObjectAcProperties = map[string]*schema.PropertySchema{
3: schema.NewRefSchema("C", nil),
},
"_type",
false,
),
nil,
true,
Expand All @@ -79,6 +82,7 @@ var oneOfIntTestObjectAdProperties = map[string]*schema.PropertySchema{
2: schema.NewRefSchema("D", nil),
},
"_type",
false,
),
nil,
true,
Expand Down
2 changes: 2 additions & 0 deletions schema/oneof_string.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ type OneOfString interface {
func NewOneOfStringSchema[ItemsInterface any](
types map[string]Object,
discriminatorFieldName string,
discriminatorInlined bool,
) *OneOfSchema[string] {
var defaultValue ItemsInterface
return &OneOfSchema[string]{
reflect.TypeOf(&defaultValue).Elem(),
types,
discriminatorFieldName,
discriminatorInlined,
}
}
Loading

0 comments on commit 6e37a6a

Please sign in to comment.