Skip to content

Commit

Permalink
Inlined object fields (#108)
Browse files Browse the repository at this point in the history
* Allow objects with single field to inline the field

* Test structs with private fields

* Added internal check

* Remove stale check

* Address compile error
  • Loading branch information
jaredoconnell authored Nov 15, 2024
1 parent 2b68585 commit 1c26e77
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 3 deletions.
32 changes: 29 additions & 3 deletions schema/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,18 @@ func (o *ObjectSchema) Properties() map[string]*PropertySchema {

func (o *ObjectSchema) Unserialize(data any) (result any, err error) {
v := reflect.ValueOf(data)
var rawData map[string]any
if v.Kind() != reflect.Map {
return nil, &ConstraintError{
Message: fmt.Sprintf("Must be a map, %T given", data),
if len(o.Properties()) == 1 {
rawData, err = o.unserializeInlinedDataToMap(data)
} else {
return nil, &ConstraintError{
Message: fmt.Sprintf("Must be a map to convert to object, %T given", data),
}
}
} else {
rawData, err = o.convertData(v)
}
rawData, err := o.convertData(v)
if err != nil {
return nil, err
}
Expand All @@ -126,6 +132,26 @@ func (o *ObjectSchema) Unserialize(data any) (result any, err error) {
return rawData, nil
}

func (o *ObjectSchema) unserializeInlinedDataToMap(data any) (map[string]any, error) {
if len(o.Properties()) > 1 {
panic(fmt.Errorf("unserializeInlinedDataToMap called on ObjectSchema with %d"+
" properties; only 1 allowed", len(o.Properties())))
}
for fieldName, property := range o.Properties() {
unserializedProperty, err := property.Unserialize(data)
if err != nil {
return nil,
fmt.Errorf("error while unserializing single inlined property %s for object %s (%q);"+
"fix the property or specify the object as a map",
fieldName, o.ID(), err)
}
return map[string]any{
fieldName: unserializedProperty,
}, nil
}
panic("convertInlinedData called on object with zero properties")
}

func (o *ObjectSchema) unserializeToStruct(rawData map[string]any) (any, error) {
reflectType := reflect.TypeOf(o.defaultValue)
var reflectedValue reflect.Value
Expand Down
81 changes: 81 additions & 0 deletions schema/object_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package schema_test

import (
"go.arcalot.io/assert"
"go.flow.arcalot.io/pluginsdk/schema/testdata"
"strconv"
"testing"

Expand Down Expand Up @@ -607,3 +608,83 @@ func TestObjectSchema_ValidateCompatibility(t *testing.T) {
assert.Error(t, s1.ValidateCompatibility(schema.NewStringEnumSchema(map[string]*schema.DisplayValue{})))
assert.Error(t, s1.ValidateCompatibility(schema.NewIntEnumSchema(map[int64]*schema.DisplayValue{}, nil)))
}

type testStructWithSingleField struct {
Field1 string `json:"field1"`
}

var testStructWithSingleFieldSchema = schema.NewStructMappedObjectSchema[testStructWithSingleField]("testStructWithSingleField", map[string]*schema.PropertySchema{
"field1": schema.NewPropertySchema(schema.NewStringSchema(nil, nil, nil),
schema.NewDisplayValue(schema.PointerTo("field1"), nil, nil),
true,
nil,
nil,
nil,
nil,
nil,
),
})

func TestUnserializeSingleFieldObject(t *testing.T) {
withoutInlineSerialized := map[string]any{
"field1": "hello",
}
expectedOutput := testStructWithSingleField{
"hello",
}

unserializedData, err := testStructWithSingleFieldSchema.Unserialize(withoutInlineSerialized)
assert.NoError(t, err)
assert.InstanceOf[testStructWithSingleField](t, unserializedData)
assert.Equals(t, unserializedData.(testStructWithSingleField), expectedOutput)
}

func TestUnserializeSingleFieldObjectInlined(t *testing.T) {
withoutInlineSerialized := "hello"

expectedOutput := testStructWithSingleField{
"hello",
}

unserializedData, err := testStructWithSingleFieldSchema.Unserialize(withoutInlineSerialized)
assert.NoError(t, err)
assert.InstanceOf[testStructWithSingleField](t, unserializedData)
assert.Equals(t, unserializedData.(testStructWithSingleField), expectedOutput)
}

func TestStructWithPrivateFields(t *testing.T) {
schemaForPrivateFieldStruct := schema.NewStructMappedObjectSchema[testdata.TestStructWithPrivateField](
"structWithPrivateField",
map[string]*schema.PropertySchema{
"field1": schema.NewPropertySchema(
schema.NewStringSchema(nil, nil, nil),
nil,
false,
nil,
nil,
nil,
schema.PointerTo("\"Hello world!\""),
nil,
),
},
)

inputWithOnlyPublicField := testdata.TestStructWithPrivateField{
Field1: "test",
}
serializedData, err := schemaForPrivateFieldStruct.Serialize(inputWithOnlyPublicField)
assert.NoError(t, err)
unserializedData, err := schemaForPrivateFieldStruct.Unserialize(serializedData)
assert.NoError(t, err)
assert.InstanceOf[testdata.TestStructWithPrivateField](t, unserializedData)
assert.Equals(t, inputWithOnlyPublicField, unserializedData.(testdata.TestStructWithPrivateField))

inputWithPrivateField := testdata.GetTestStructWithPrivateFieldPresent()
serializedData, err = schemaForPrivateFieldStruct.Serialize(inputWithPrivateField)
assert.NoError(t, err)
unserializedData, err = schemaForPrivateFieldStruct.Unserialize(serializedData)
assert.NoError(t, err)
assert.InstanceOf[testdata.TestStructWithPrivateField](t, unserializedData)
// The unserialization will only be able to fill in the public fields.
assert.Equals(t, inputWithOnlyPublicField, unserializedData.(testdata.TestStructWithPrivateField))
}

0 comments on commit 1c26e77

Please sign in to comment.