Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Underlying type fix #79

Merged
merged 44 commits into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
77d354c
start
mfleader Feb 9, 2024
4fd462e
add tests
mfleader Feb 10, 2024
683d3bd
add test for inline discriminator
mfleader Feb 10, 2024
772951d
add checker for inline field
mfleader Feb 13, 2024
b940938
add jareds tests
mfleader Feb 13, 2024
d455ff8
fix inline discriminator check
mfleader Feb 13, 2024
c04786b
add test for mismatched discriminator types
mfleader Feb 14, 2024
5ed77ff
fix error message
mfleader Feb 14, 2024
74ed3be
fix one-of and subtype discriminator mismatch detection for ints
mfleader Feb 14, 2024
c674375
refactor conditionals
mfleader Feb 14, 2024
d9a17a2
remove dead code
mfleader Feb 14, 2024
37ecda5
try to panic
mfleader Feb 15, 2024
e5b6c98
move validation call and refactor tests
mfleader Feb 15, 2024
f74c4fa
rename tests
mfleader Feb 15, 2024
581c159
upgrade go-assert
mfleader Feb 15, 2024
d280530
tidy up
mfleader Feb 15, 2024
fe31d42
debug
mfleader Feb 16, 2024
3b9a48c
fixed it
mfleader Feb 16, 2024
40937d1
ineffectual err
mfleader Feb 16, 2024
5142636
rm dead code
mfleader Feb 16, 2024
aeb6228
whitespace
mfleader Feb 16, 2024
1b207dc
rm dead code
mfleader Feb 16, 2024
de3831c
rm unused
mfleader Feb 16, 2024
b8e870c
refactor to switch
mfleader Feb 16, 2024
8bc2099
make function private
mfleader Feb 16, 2024
5239cf6
use panicscontains
mfleader Feb 16, 2024
e7a5f5c
small change
mfleader Feb 16, 2024
2aba5c8
add map change to validate type
mfleader Feb 16, 2024
c24d1ee
hopefully fix interface type access
mfleader Feb 17, 2024
68d8078
also lint
mfleader Feb 17, 2024
6551c0d
add oneof scope scope schema
mfleader Feb 19, 2024
2794142
dry out test
mfleader Feb 19, 2024
0eba29f
nolint function length on test
mfleader Feb 19, 2024
fa78446
add inline default
mfleader Feb 19, 2024
690ed39
remove import parens
mfleader Feb 19, 2024
ac7fd1c
finish function refactor
mfleader Feb 20, 2024
9415a48
rm unnecessary lines
mfleader Feb 20, 2024
5464419
improve coverage
mfleader Feb 20, 2024
a82b000
fix lint
mfleader Feb 20, 2024
df18e50
rm comment
mfleader Feb 20, 2024
0635d61
Merge branch 'main' into underlying-type-fix
mfleader Feb 20, 2024
f2fefed
use a string discriminator for test
mfleader Feb 21, 2024
8fd95f0
clarify scope scope schema
mfleader Feb 21, 2024
3c27f11
rm whitespace
mfleader Feb 21, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
webbnh marked this conversation as resolved.
Show resolved Hide resolved
}

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)
}
webbnh marked this conversation as resolved.
Show resolved Hide resolved
}
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
Loading