added tests for primitives and basic objects/arrays

This commit is contained in:
Shreyas Goenka 2023-01-13 16:45:01 +01:00
parent 987dd29357
commit 926db54a6d
No known key found for this signature in database
GPG Key ID: 92A07DF49CCB0622
2 changed files with 225 additions and 43 deletions

View File

@ -16,19 +16,22 @@ type Schema struct {
} }
type Property struct { type Property struct {
// TODO: Add a enum for json types
Type JsType `json:"type"` Type JsType `json:"type"`
Items *Item `json:"item,omitempty"` Items *Item `json:"item,omitempty"`
Properities map[string]*Property `json:"properities,omitempty"` Properities map[string]*Property `json:"properities,omitempty"`
AdditionalProperities *Property `json:"additionalProperities,omitempty"` AdditionalProperities *Property `json:"additionalProperities,omitempty"`
} }
// TODO: panic for now, add support for adding schemas to $defs in case of cycles
type Item struct { type Item struct {
Type JsType `json:"type"` Type JsType `json:"type"`
} }
func NewSchema(goType reflect.Type) (*Schema, error) { func NewSchema(golangType reflect.Type) (*Schema, error) {
rootProp, err := properity(goType) traceSet := map[reflect.Type]struct{}{}
traceSlice := []reflect.Type{}
rootProp, err := toProperity(golangType, traceSet, traceSlice)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -50,39 +53,52 @@ const (
Array = "array" Array = "array"
) )
func jsType(goType reflect.Type) (JsType, error) { func javascriptType(golangType reflect.Type) (JsType, error) {
switch goType.Kind() { switch golangType.Kind() {
case reflect.Bool: case reflect.Bool:
return Boolean, nil return Boolean, nil
case reflect.String: case reflect.String:
return String, nil return String, nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Float32, reflect.Float64: case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
reflect.Float32, reflect.Float64:
return Number, nil return Number, nil
case reflect.Struct: case reflect.Struct:
return Object, nil return Object, nil
// TODO: add support for pattern properities to account for maps // TODO: add support for pattern properities to account for maps
case reflect.Map: case reflect.Map:
if goType.Key().Kind() != reflect.String { if golangType.Key().Kind() != reflect.String {
return Invalid, fmt.Errorf("only strings map keys are valid. key type: ", goType.Key().Kind()) return Invalid, fmt.Errorf("only strings map keys are valid. key type: %v", golangType.Key().Kind())
} }
return Object, nil return Object, nil
case reflect.Array, reflect.Slice: case reflect.Array, reflect.Slice:
return Array, nil return Array, nil
default: default:
return Invalid, fmt.Errorf("unhandled golang type: %s", goType) return Invalid, fmt.Errorf("unhandled golang type: %s", golangType)
} }
} }
func errWithTrace(prefix string, trace []reflect.Type) error {
traceString := ""
for _, golangType := range trace {
traceString += " -> " + golangType.Name()
}
return fmt.Errorf("[ERROR] " + prefix + " type traveral trace: " + traceString)
}
// TODO: handle case of self referential pointers in structs // TODO: handle case of self referential pointers in structs
// TODO: add doc string explaining numHistoryOccurances // TODO: add doc string explaining numHistoryOccurances
func properity(goType reflect.Type, numHistoryOccurances map[string]int) (*Property, error) { func toProperity(golangType reflect.Type, traceSet map[reflect.Type]struct{}, traceSlice []reflect.Type) (*Property, error) {
traceSlice = append(traceSlice, golangType)
// *Struct and Struct generate identical json schemas // *Struct and Struct generate identical json schemas
if goType.Kind() == reflect.Pointer { if golangType.Kind() == reflect.Pointer {
return properity(goType.Elem()) return toProperity(golangType.Elem(), traceSet, traceSlice)
} }
rootJsType, err := jsType(goType) rootJavascriptType, err := javascriptType(golangType)
// TODO: recursive debugging can be a pain. Make sure the error localtion // TODO: recursive debugging can be a pain. Make sure the error localtion
// floats up // floats up
@ -91,8 +107,8 @@ func properity(goType reflect.Type, numHistoryOccurances map[string]int) (*Prope
} }
var items *Item var items *Item
if goType.Kind() == reflect.Array || goType.Kind() == reflect.Slice { if golangType.Kind() == reflect.Array || golangType.Kind() == reflect.Slice {
elemJsType, err := jsType(goType.Elem()) elemJsType, err := javascriptType(golangType.Elem())
if err != nil { if err != nil {
// TODO: float up error in case of deep recursion // TODO: float up error in case of deep recursion
return nil, err return nil, err
@ -104,48 +120,52 @@ func properity(goType reflect.Type, numHistoryOccurances map[string]int) (*Prope
} }
properities := map[string]*Property{} properities := map[string]*Property{}
var additionalProperities *Property
// TODO: for reflect.Map case for prop computation // TODO: for reflect.Map case for prop computation
if golangType.Kind() == reflect.Struct {
if goType.Kind() == reflect.Struct { for i := 0; i < golangType.NumField(); i++ {
for i := 0; i < goType.NumField(); i++ { child := golangType.Field(i)
field := goType.Field(i)
// compute child properties // compute child properties
fieldJsonTag := field.Tag.Get("json") childJsonTag := child.Tag.Get("json")
fieldName := strings.Split(fieldJsonTag, ",")[0] childName := strings.Split(childJsonTag, ",")[0]
// stopgap infinite recursion // skip non json annotated fields
numHistoryOccurances[fieldName] += 1 if childName == "" {
if numHistoryOccurances[fieldName] > MaxHistoryOccurances { continue
return nil
} }
fieldProps, err := properity(field.Type)
numHistoryOccurances[fieldName] -= 1 // detect cycles. Fail if a cycle is detected
// TODO: Add references here for cycles
_, ok := traceSet[child.Type]
if ok {
fmt.Println("[DEBUG] traceSet: ", traceSet)
return nil, errWithTrace("cycle detected", traceSlice)
}
// add current child field to history
traceSet[child.Type] = struct{}{}
// recursively compute properties for this child field
fieldProps, err := toProperity(child.Type, traceSet, traceSlice)
// TODO: make sure this error floats up with context // TODO: make sure this error floats up with context
if err != nil { if err != nil {
return nil, err return nil, err
} }
if fieldJsonTag != "" { // traversal complete, delete child from history
properities[fieldName] = fieldProps delete(traceSet, child.Type)
} else if additionalProperities == nil {
// TODO: add error disallowing self referenincing without json tags properities[childName] = fieldProps
additionalProperities = fieldProps
} else {
// TODO: float error up with context
return nil, fmt.Errorf("only one non json annotated field allowed")
}
} }
} }
return &Property{ traceSlice = traceSlice[:len(traceSlice)-1]
Type: rootJsType,
Items: items,
Properities: properities,
AdditionalProperities: additionalProperities,
}, nil
return &Property{
Type: rootJavascriptType,
Items: items,
Properities: properities,
}, nil
} }

View File

@ -0,0 +1,162 @@
package schema
import (
"encoding/json"
"fmt"
"reflect"
"testing"
"github.com/stretchr/testify/assert"
)
// TODO: add tests to assert that these are valid json schemas. Maybe validate some
// json/yaml documents againts them, by unmarshalling a value
func TestNumberStringBooleanSchema(t *testing.T) {
type Foo struct {
IntVal int `json:"int_val"`
Int8Val int8 `json:"int8_val"`
Int16Val int16 `json:"int16_val"`
Int32Val int32 `json:"int32_val"`
Int64Val int64 `json:"int64_val"`
Uint8Val int8 `json:"uint8_val"`
Uint16Val int16 `json:"uint16_val"`
Uint32Val int32 `json:"uint32_val"`
Uint64Val int64 `json:"uint64_val"`
Float32Val int64 `json:"float32_val"`
Float64Val int64 `json:"float64_val"`
StringVal string `json:"string_val"`
BoolVal string `json:"bool_val"`
}
elem := Foo{}
schema, err := NewSchema(reflect.TypeOf(elem))
assert.NoError(t, err)
jsonSchema, err := json.MarshalIndent(schema, " ", " ")
assert.NoError(t, err)
expected :=
`{
"type": "object",
"properities": {
"bool_val": {
"type": "string"
},
"float32_val": {
"type": "number"
},
"float64_val": {
"type": "number"
},
"int16_val": {
"type": "number"
},
"int32_val": {
"type": "number"
},
"int64_val": {
"type": "number"
},
"int8_val": {
"type": "number"
},
"int_val": {
"type": "number"
},
"string_val": {
"type": "string"
},
"uint16_val": {
"type": "number"
},
"uint32_val": {
"type": "number"
},
"uint64_val": {
"type": "number"
},
"uint8_val": {
"type": "number"
}
}
}`
fmt.Println("[DEBUG] actual: ", string(jsonSchema))
fmt.Println("[DEBUG] expected: ", expected)
assert.Equal(t, expected, string(jsonSchema))
}
func TestObjectSchema(t *testing.T) {
type Person struct {
Name string `json:"name"`
Age int `json:"age,omitempty"`
}
type Plot struct {
Stakes []string `json:"stakes"`
}
type Story struct {
Hero Person `json:"hero"`
Villian Person `json:"villian"`
Plot Plot `json:"plot"`
}
elem := Story{}
schema, err := NewSchema(reflect.TypeOf(elem))
assert.NoError(t, err)
jsonSchema, err := json.MarshalIndent(schema, " ", " ")
assert.NoError(t, err)
expected :=
`{
"type": "object",
"properities": {
"hero": {
"type": "object",
"properities": {
"age": {
"type": "number"
},
"name": {
"type": "string"
}
}
},
"plot": {
"type": "object",
"properities": {
"stakes": {
"type": "array",
"item": {
"type": "string"
}
}
}
},
"villian": {
"type": "object",
"properities": {
"age": {
"type": "number"
},
"name": {
"type": "string"
}
}
}
}
}`
fmt.Println("[DEBUG] actual: ", string(jsonSchema))
fmt.Println("[DEBUG] expected: ", expected)
assert.Equal(t, expected, string(jsonSchema))
}