2023-01-13 03:52:25 +00:00
|
|
|
package schema
|
|
|
|
|
|
|
|
import (
|
2023-01-13 18:08:21 +00:00
|
|
|
"container/list"
|
2023-01-13 03:52:25 +00:00
|
|
|
"fmt"
|
|
|
|
"reflect"
|
|
|
|
"strings"
|
|
|
|
)
|
|
|
|
|
2023-01-13 11:09:19 +00:00
|
|
|
const MaxHistoryOccurances = 3
|
|
|
|
|
|
|
|
// TODO: should omit empty denote non required fields in the json schema?
|
2023-01-13 03:52:25 +00:00
|
|
|
type Schema struct {
|
2023-01-13 11:09:19 +00:00
|
|
|
Type JsType `json:"type"`
|
2023-01-13 16:13:21 +00:00
|
|
|
Properities map[string]*Property `json:"properties,omitempty"`
|
|
|
|
AdditionalProperities *Property `json:"additionalProperties,omitempty"`
|
2023-01-13 03:52:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type Property struct {
|
2023-01-13 11:09:19 +00:00
|
|
|
Type JsType `json:"type"`
|
2023-01-13 16:28:46 +00:00
|
|
|
Items *Item `json:"items,omitempty"`
|
2023-01-13 16:13:21 +00:00
|
|
|
Properities map[string]*Property `json:"properties,omitempty"`
|
|
|
|
AdditionalProperities *Property `json:"additionalProperties,omitempty"`
|
2023-01-13 03:52:25 +00:00
|
|
|
}
|
|
|
|
|
2023-01-13 15:45:01 +00:00
|
|
|
// TODO: panic for now, add support for adding schemas to $defs in case of cycles
|
|
|
|
|
2023-01-13 03:52:25 +00:00
|
|
|
type Item struct {
|
2023-01-13 18:08:21 +00:00
|
|
|
Type JsType `json:"type"`
|
|
|
|
Properities map[string]*Property `json:"properties,omitempty"`
|
2023-01-13 03:52:25 +00:00
|
|
|
}
|
|
|
|
|
2023-01-13 15:45:01 +00:00
|
|
|
func NewSchema(golangType reflect.Type) (*Schema, error) {
|
|
|
|
traceSet := map[reflect.Type]struct{}{}
|
2023-01-13 18:37:35 +00:00
|
|
|
trace := list.New()
|
|
|
|
rootProp, err := toProperity(golangType, traceSet, trace)
|
2023-01-13 03:52:25 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return &Schema{
|
2023-01-13 11:09:19 +00:00
|
|
|
Type: rootProp.Type,
|
|
|
|
Properities: rootProp.Properities,
|
|
|
|
AdditionalProperities: rootProp.AdditionalProperities,
|
2023-01-13 03:52:25 +00:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2023-01-13 18:08:21 +00:00
|
|
|
// TODO: add tests for errors being triggered
|
|
|
|
|
2023-01-13 03:52:25 +00:00
|
|
|
type JsType string
|
|
|
|
|
|
|
|
const (
|
|
|
|
Invalid JsType = "invalid"
|
|
|
|
Boolean = "boolean"
|
|
|
|
String = "string"
|
|
|
|
Number = "number"
|
|
|
|
Object = "object"
|
|
|
|
Array = "array"
|
|
|
|
)
|
|
|
|
|
2023-01-13 15:45:01 +00:00
|
|
|
func javascriptType(golangType reflect.Type) (JsType, error) {
|
|
|
|
switch golangType.Kind() {
|
2023-01-13 03:52:25 +00:00
|
|
|
case reflect.Bool:
|
|
|
|
return Boolean, nil
|
|
|
|
case reflect.String:
|
|
|
|
return String, nil
|
2023-01-13 15:45:01 +00:00
|
|
|
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:
|
|
|
|
|
2023-01-13 03:52:25 +00:00
|
|
|
return Number, nil
|
|
|
|
case reflect.Struct:
|
|
|
|
return Object, nil
|
|
|
|
case reflect.Map:
|
2023-01-13 15:45:01 +00:00
|
|
|
if golangType.Key().Kind() != reflect.String {
|
|
|
|
return Invalid, fmt.Errorf("only strings map keys are valid. key type: %v", golangType.Key().Kind())
|
2023-01-13 03:52:25 +00:00
|
|
|
}
|
|
|
|
return Object, nil
|
|
|
|
case reflect.Array, reflect.Slice:
|
|
|
|
return Array, nil
|
|
|
|
default:
|
2023-01-13 15:45:01 +00:00
|
|
|
return Invalid, fmt.Errorf("unhandled golang type: %s", golangType)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-13 17:01:53 +00:00
|
|
|
// TODO: add a simple test for this
|
2023-01-13 18:37:35 +00:00
|
|
|
func errWithTrace(prefix string, trace *list.List) error {
|
2023-01-13 15:45:01 +00:00
|
|
|
traceString := ""
|
2023-01-13 18:37:35 +00:00
|
|
|
curr := trace.Front()
|
|
|
|
for curr.Next() != nil {
|
|
|
|
traceString += " -> " + curr.Value.(reflect.Type).Name()
|
|
|
|
curr = curr.Next()
|
2023-01-13 03:52:25 +00:00
|
|
|
}
|
2023-01-13 15:45:01 +00:00
|
|
|
return fmt.Errorf("[ERROR] " + prefix + " type traveral trace: " + traceString)
|
2023-01-13 03:52:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: handle case of self referential pointers in structs
|
2023-01-13 17:01:53 +00:00
|
|
|
// TODO: add handling of embedded types
|
|
|
|
|
|
|
|
// TODO: add tests for the error cases, forcefully triggering them
|
2023-01-13 03:52:25 +00:00
|
|
|
|
2023-01-13 18:08:21 +00:00
|
|
|
// checks and errors out for cycles
|
|
|
|
// wraps the error with context
|
2023-01-13 18:37:35 +00:00
|
|
|
func safeToProperty(golangType reflect.Type, traceSet map[reflect.Type]struct{}, trace *list.List) (*Property, error) {
|
|
|
|
trace.PushBack(golangType)
|
2023-01-13 18:08:21 +00:00
|
|
|
// detect cycles. Fail if a cycle is detected
|
|
|
|
// TODO: Add references here for cycles
|
|
|
|
// TODO: move this check somewhere nicer
|
|
|
|
_, ok := traceSet[golangType]
|
|
|
|
if ok {
|
|
|
|
fmt.Println("[DEBUG] traceSet: ", traceSet)
|
2023-01-13 18:37:35 +00:00
|
|
|
return nil, errWithTrace("cycle detected", trace)
|
2023-01-13 18:08:21 +00:00
|
|
|
}
|
|
|
|
// add current child field to history
|
|
|
|
traceSet[golangType] = struct{}{}
|
2023-01-13 18:37:35 +00:00
|
|
|
props, err := toProperity(golangType, traceSet, trace)
|
2023-01-13 18:08:21 +00:00
|
|
|
if err != nil {
|
2023-01-13 18:37:35 +00:00
|
|
|
return nil, errWithTrace(err.Error(), trace)
|
2023-01-13 18:08:21 +00:00
|
|
|
}
|
|
|
|
delete(traceSet, golangType)
|
2023-01-13 18:37:35 +00:00
|
|
|
back := trace.Back()
|
|
|
|
trace.Remove(back)
|
2023-01-13 18:08:21 +00:00
|
|
|
return props, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// travels anonymous embedded fields in a bfs manner to give us a list of all
|
|
|
|
// member fields of a struct
|
|
|
|
// simple Tree based traversal will take place because embbedded fields cannot
|
|
|
|
// form a cycle
|
|
|
|
func addStructFields(fields []reflect.StructField, golangType reflect.Type) []reflect.StructField {
|
|
|
|
bfsQueue := list.New()
|
|
|
|
|
2023-01-13 18:25:42 +00:00
|
|
|
for i := golangType.NumField() - 1; i >= 0; i-- {
|
2023-01-13 18:08:21 +00:00
|
|
|
bfsQueue.PushBack(golangType.Field(i))
|
|
|
|
}
|
|
|
|
for bfsQueue.Len() > 0 {
|
|
|
|
front := bfsQueue.Front()
|
|
|
|
field := front.Value.(reflect.StructField)
|
|
|
|
bfsQueue.Remove(front)
|
|
|
|
|
|
|
|
if !field.Anonymous {
|
|
|
|
fields = append(fields, field)
|
|
|
|
continue
|
|
|
|
}
|
2023-01-13 18:25:42 +00:00
|
|
|
|
|
|
|
// TODO: add test case for pointer too
|
|
|
|
fieldType := field.Type
|
|
|
|
if fieldType.Kind() == reflect.Pointer {
|
|
|
|
fieldType = fieldType.Elem()
|
|
|
|
}
|
|
|
|
|
|
|
|
for i := 0; i < fieldType.NumField(); i++ {
|
|
|
|
bfsQueue.PushBack(fieldType.Field(i))
|
2023-01-13 18:08:21 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return fields
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: add doc string explaining numHistoryOccurances
|
2023-01-13 18:37:35 +00:00
|
|
|
func toProperity(golangType reflect.Type, traceSet map[reflect.Type]struct{}, trace *list.List) (*Property, error) {
|
2023-01-13 03:52:25 +00:00
|
|
|
// *Struct and Struct generate identical json schemas
|
2023-01-13 15:45:01 +00:00
|
|
|
if golangType.Kind() == reflect.Pointer {
|
2023-01-13 18:37:35 +00:00
|
|
|
return toProperity(golangType.Elem(), traceSet, trace)
|
2023-01-13 03:52:25 +00:00
|
|
|
}
|
|
|
|
|
2023-01-13 15:45:01 +00:00
|
|
|
rootJavascriptType, err := javascriptType(golangType)
|
2023-01-13 03:52:25 +00:00
|
|
|
if err != nil {
|
2023-01-13 18:08:21 +00:00
|
|
|
return nil, err
|
2023-01-13 03:52:25 +00:00
|
|
|
}
|
|
|
|
|
2023-01-13 18:08:21 +00:00
|
|
|
// case array/slice
|
2023-01-13 03:52:25 +00:00
|
|
|
var items *Item
|
2023-01-13 15:45:01 +00:00
|
|
|
if golangType.Kind() == reflect.Array || golangType.Kind() == reflect.Slice {
|
2023-01-13 17:01:53 +00:00
|
|
|
elemGolangType := golangType.Elem()
|
|
|
|
elemJavascriptType, err := javascriptType(elemGolangType)
|
2023-01-13 03:52:25 +00:00
|
|
|
if err != nil {
|
2023-01-13 18:08:21 +00:00
|
|
|
return nil, err
|
2023-01-13 17:01:53 +00:00
|
|
|
}
|
2023-01-13 18:37:35 +00:00
|
|
|
elemProps, err := safeToProperty(elemGolangType, traceSet, trace)
|
2023-01-13 17:01:53 +00:00
|
|
|
if err != nil {
|
2023-01-13 18:08:21 +00:00
|
|
|
return nil, err
|
2023-01-13 17:01:53 +00:00
|
|
|
}
|
2023-01-13 03:52:25 +00:00
|
|
|
items = &Item{
|
2023-01-13 17:01:53 +00:00
|
|
|
// TODO: Add a test for slice of object
|
2023-01-13 18:08:21 +00:00
|
|
|
Type: elemJavascriptType,
|
2023-01-13 17:01:53 +00:00
|
|
|
Properities: elemProps.Properities,
|
2023-01-13 03:52:25 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-13 18:08:21 +00:00
|
|
|
// case map
|
|
|
|
var additionalProperties *Property
|
|
|
|
if golangType.Kind() == reflect.Map {
|
|
|
|
if golangType.Key().Kind() != reflect.String {
|
2023-01-13 18:37:35 +00:00
|
|
|
return nil, errWithTrace("only string keyed maps allowed", trace)
|
2023-01-13 18:08:21 +00:00
|
|
|
}
|
2023-01-13 18:37:35 +00:00
|
|
|
additionalProperties, err = safeToProperty(golangType.Elem(), traceSet, trace)
|
2023-01-13 18:08:21 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
2023-01-13 11:09:19 +00:00
|
|
|
|
2023-01-13 18:08:21 +00:00
|
|
|
// case struct
|
2023-01-13 17:01:53 +00:00
|
|
|
properities := map[string]*Property{}
|
2023-01-13 15:45:01 +00:00
|
|
|
if golangType.Kind() == reflect.Struct {
|
2023-01-13 18:08:21 +00:00
|
|
|
children := []reflect.StructField{}
|
|
|
|
children = addStructFields(children, golangType)
|
|
|
|
for _, child := range children {
|
2023-01-13 11:09:19 +00:00
|
|
|
// compute child properties
|
2023-01-13 15:45:01 +00:00
|
|
|
childJsonTag := child.Tag.Get("json")
|
|
|
|
childName := strings.Split(childJsonTag, ",")[0]
|
2023-01-13 11:09:19 +00:00
|
|
|
|
2023-01-13 15:45:01 +00:00
|
|
|
// skip non json annotated fields
|
|
|
|
if childName == "" {
|
|
|
|
continue
|
2023-01-13 11:09:19 +00:00
|
|
|
}
|
2023-01-13 15:45:01 +00:00
|
|
|
|
|
|
|
// recursively compute properties for this child field
|
2023-01-13 18:37:35 +00:00
|
|
|
fieldProps, err := safeToProperty(child.Type, traceSet, trace)
|
2023-01-13 03:52:25 +00:00
|
|
|
if err != nil {
|
2023-01-13 18:37:35 +00:00
|
|
|
return nil, errWithTrace(err.Error(), trace)
|
2023-01-13 03:52:25 +00:00
|
|
|
}
|
2023-01-13 15:45:01 +00:00
|
|
|
properities[childName] = fieldProps
|
2023-01-13 03:52:25 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return &Property{
|
2023-01-13 18:08:21 +00:00
|
|
|
Type: rootJavascriptType,
|
|
|
|
Items: items,
|
|
|
|
Properities: properities,
|
|
|
|
AdditionalProperities: additionalProperties,
|
2023-01-13 03:52:25 +00:00
|
|
|
}, nil
|
|
|
|
}
|