Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 16 additions & 3 deletions schema/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,22 @@ type Object interface {
// NewObjectSchema creates a new object definition.
// If you need it tied to a struct, use NewStructMappedObjectSchema instead.
func NewObjectSchema(id string, properties map[string]*PropertySchema) *ObjectSchema {
return newObjectSchema(id, properties, false)
return newObjectSchema(id, properties, false, nil)
}

// NewObjectSchemaWithUnserializeHook creates a new object definition with an unserialize hook to instantiate it from properties.
func NewObjectSchemaWithUnserializeHook(id string, properties map[string]*PropertySchema, hook UnserializeObjectHookFunction) *ObjectSchema {
return newObjectSchema(id, properties, false, hook)
}

// NewUnenforcedIDObjectSchema creates a new object definition with the ID not enforced.
// The unenforced ID checking is useful for generated schemas, where the ID may be insignificant,
// or could burden workflow development.
func NewUnenforcedIDObjectSchema(id string, properties map[string]*PropertySchema) *ObjectSchema {
return newObjectSchema(id, properties, true)
return newObjectSchema(id, properties, true, nil)
}

func newObjectSchema(id string, properties map[string]*PropertySchema, unenforcedIDMatch bool) *ObjectSchema {
func newObjectSchema(id string, properties map[string]*PropertySchema, unenforcedIDMatch bool, hook UnserializeObjectHookFunction) *ObjectSchema {
var anyValue any
return &ObjectSchema{
id,
Expand All @@ -43,9 +48,12 @@ func newObjectSchema(id string, properties map[string]*PropertySchema, unenforce
nil,
reflect.TypeOf(anyValue),
nil,
hook,
}
}

type UnserializeObjectHookFunction func(rawData map[string]any) (any, error)

// ObjectSchema is the implementation of the object schema type.
type ObjectSchema struct {
IDValue string `json:"id"`
Expand All @@ -57,6 +65,7 @@ type ObjectSchema struct {
defaultValue any
defaultValueType reflect.Type
fieldCache map[string]reflect.StructField
unserializeHook UnserializeObjectHookFunction
}

func (o *ObjectSchema) ReflectedType() reflect.Type {
Expand Down Expand Up @@ -126,6 +135,10 @@ func (o *ObjectSchema) Unserialize(data any) (result any, err error) {
return nil, err
}

if o.unserializeHook != nil {
return o.unserializeHook(rawData)
}

if o.fieldCache != nil {
return o.unserializeToStruct(rawData)
}
Expand Down
109 changes: 109 additions & 0 deletions schema/object_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package schema_test

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

Expand Down Expand Up @@ -688,3 +690,110 @@ func TestStructWithPrivateFields(t *testing.T) {
// The unserialization will only be able to fill in the public fields.
assert.Equals(t, inputWithOnlyPublicField, unserializedData.(testdata.TestStructWithPrivateField))
}

type Quantity struct {
value int
unit string
}

func NewStructWithConstructor(input string) (Quantity, error) {
r := regexp.MustCompile("^([0-9]+)([eEinumkKMGTP]+)$")
extracted := r.FindStringSubmatch(input)
if len(extracted) != 3 {
return Quantity{}, fmt.Errorf("invalid quantity format")
}

value, err := strconv.Atoi(extracted[1])
if err != nil {
return Quantity{}, fmt.Errorf("invalid quantity format")
}

return Quantity{
value: value,
unit: extracted[2],
}, nil
}

func (q Quantity) String() string {
return fmt.Sprintf("%d%s", q.value, q.unit)
}

func TestStructWithConstructor(t *testing.T) {
var hook schema.UnserializeObjectHookFunction = func(rawData map[string]any) (any, error) {
return NewStructWithConstructor(rawData[""].(string))
}
resourceQuantity := schema.NewObjectSchemaWithUnserializeHook(
"Test Unserialize Struct With Constructor",
map[string]*schema.PropertySchema{
"": schema.NewPropertySchema(
schema.NewStringSchema(nil, nil, nil),
schema.NewDisplayValue(
schema.PointerTo("Quantity"),
schema.PointerTo("Quantity"),
nil,
),
true,
nil,
nil,
nil,
nil,
nil,
),
},
hook,
)

inputQuantityObj := Quantity{unit: "G", value: 12}
inputQuantityString := inputQuantityObj.String()
unserializedData, err := resourceQuantity.Unserialize(inputQuantityString)
assert.NoError(t, err)
assert.InstanceOf[Quantity](t, unserializedData)
unserializedQuantity := unserializedData.(Quantity)
assert.Equals(t, inputQuantityString, unserializedQuantity.String())
}

// Re-use the properties for testStructSchema.
var testStructSchemaHook = schema.NewObjectSchemaWithUnserializeHook(
"testStrcut",
testStructProperties,
func(rawData map[string]any) (any, error) {
panic("This constructor should never be reached.")
},
)

func TestStructWithConstructor_MissingFields(t *testing.T) {
dataMissing := map[string]any{
"field3": 42,
}
_, err := testStructSchemaHook.Unserialize(dataMissing)
assert.Error(t, err)
assert.Contains(t, err.Error(), "field is required")

dataMissing2 := map[string]any{
"Field1": 42,
}
_, err = testStructSchemaHook.Unserialize(dataMissing2)
assert.Error(t, err)
assert.Contains(t, err.Error(), "field is required")
}

func TestStructWithConstructor_IncorrectType(t *testing.T) {
dataMissing1 := map[string]any{
"Field1": "this cannot be represented as an integer",
"field3": "Hello world!",
}
_, err := testStructSchemaHook.Unserialize(dataMissing1)
assert.Error(t, err)
assert.Contains(t, err.Error(), "parsing")
}

func TestStructWithConstructor_ExtraField(t *testing.T) {
dataMissing1 := map[string]any{
"Field1": 42,
"wrong": "wrong",
"field3": "Hello world!",
}
_, err := testStructSchemaHook.Unserialize(dataMissing1)
assert.Error(t, err)
assert.Contains(t, err.Error(), "Invalid parameter 'wrong'")
}