From 1c26e77880159064bee6ef3c42636c896388b626 Mon Sep 17 00:00:00 2001 From: Jared O'Connell <46976761+jaredoconnell@users.noreply.github.com> Date: Fri, 15 Nov 2024 15:34:37 -0500 Subject: [PATCH] Inlined object fields (#108) * Allow objects with single field to inline the field * Test structs with private fields * Added internal check * Remove stale check * Address compile error --- schema/object.go | 32 +++++++++++++++-- schema/object_test.go | 81 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 3 deletions(-) diff --git a/schema/object.go b/schema/object.go index fc677e7..1e3c13d 100644 --- a/schema/object.go +++ b/schema/object.go @@ -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 } @@ -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 diff --git a/schema/object_test.go b/schema/object_test.go index c988930..64abaeb 100644 --- a/schema/object_test.go +++ b/schema/object_test.go @@ -2,6 +2,7 @@ package schema_test import ( "go.arcalot.io/assert" + "go.flow.arcalot.io/pluginsdk/schema/testdata" "strconv" "testing" @@ -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)) +}