github.com/databricks/cli@v0.203.0/bundle/schema/schema.go (about)

     1  package schema
     2  
     3  import (
     4  	"container/list"
     5  	"fmt"
     6  	"reflect"
     7  	"strings"
     8  
     9  	"github.com/databricks/cli/libs/jsonschema"
    10  )
    11  
    12  // This function translates golang types into json schema. Here is the mapping
    13  // between json schema types and golang types
    14  //
    15  //   - GolangType               ->    Javascript type / Json Schema2
    16  //
    17  //   - bool                     ->    boolean
    18  //
    19  //   - string                   ->    string
    20  //
    21  //   - int (all variants)       ->    number
    22  //
    23  //   - float (all variants)     ->    number
    24  //
    25  //   - map[string]MyStruct      ->   { type: object, additionalProperties: {}}
    26  //     for details visit: https://json-schema.org/understanding-json-schema/reference/object.html#additional-properties
    27  //
    28  //   - []MyStruct               ->   {type: array, items: {}}
    29  //     for details visit: https://json-schema.org/understanding-json-schema/reference/array.html#items
    30  //
    31  //   - []MyStruct               ->   {type: object, properties: {}, additionalProperties: false}
    32  //     for details visit: https://json-schema.org/understanding-json-schema/reference/object.html#properties
    33  func New(golangType reflect.Type, docs *Docs) (*jsonschema.Schema, error) {
    34  	tracker := newTracker()
    35  	schema, err := safeToSchema(golangType, docs, "", tracker)
    36  	if err != nil {
    37  		return nil, tracker.errWithTrace(err.Error(), "root")
    38  	}
    39  	return schema, nil
    40  }
    41  
    42  func jsonSchemaType(golangType reflect.Type) (jsonschema.Type, error) {
    43  	switch golangType.Kind() {
    44  	case reflect.Bool:
    45  		return jsonschema.BooleanType, nil
    46  	case reflect.String:
    47  		return jsonschema.StringType, nil
    48  	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
    49  		reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
    50  		reflect.Float32, reflect.Float64:
    51  
    52  		return jsonschema.NumberType, nil
    53  	case reflect.Struct:
    54  		return jsonschema.ObjectType, nil
    55  	case reflect.Map:
    56  		if golangType.Key().Kind() != reflect.String {
    57  			return jsonschema.InvalidType, fmt.Errorf("only strings map keys are valid. key type: %v", golangType.Key().Kind())
    58  		}
    59  		return jsonschema.ObjectType, nil
    60  	case reflect.Array, reflect.Slice:
    61  		return jsonschema.ArrayType, nil
    62  	default:
    63  		return jsonschema.InvalidType, fmt.Errorf("unhandled golang type: %s", golangType)
    64  	}
    65  }
    66  
    67  // A wrapper over toSchema function to:
    68  //  1. Detect cycles in the bundle config struct.
    69  //  2. Update tracker
    70  //
    71  // params:
    72  //
    73  //   - golangType: Golang type to generate json schema for
    74  //
    75  //   - docs: Contains documentation to be injected into the generated json schema
    76  //
    77  //   - traceId: An identifier for the current type, to trace recursive traversal.
    78  //     Its value is the first json tag in case of struct fields and "" in other cases
    79  //     like array, map or no json tags
    80  //
    81  //   - tracker: Keeps track of types / traceIds seen during recursive traversal
    82  func safeToSchema(golangType reflect.Type, docs *Docs, traceId string, tracker *tracker) (*jsonschema.Schema, error) {
    83  	// WE ERROR OUT IF THERE ARE CYCLES IN THE JSON SCHEMA
    84  	// There are mechanisms to deal with cycles though recursive identifiers in json
    85  	// schema. However if we use them, we would need to make sure we are able to detect
    86  	// cycles where two properties (directly or indirectly) pointing to each other
    87  	//
    88  	// see: https://json-schema.org/understanding-json-schema/structuring.html#recursion
    89  	// for details
    90  	if tracker.hasCycle(golangType) {
    91  		return nil, fmt.Errorf("cycle detected")
    92  	}
    93  
    94  	tracker.push(golangType, traceId)
    95  	props, err := toSchema(golangType, docs, tracker)
    96  	if err != nil {
    97  		return nil, err
    98  	}
    99  	tracker.pop(golangType)
   100  	return props, nil
   101  }
   102  
   103  // This function returns all member fields of the provided type.
   104  // If the type has embedded (aka anonymous) fields, this function traverses
   105  // those in a breadth first manner
   106  func getStructFields(golangType reflect.Type) []reflect.StructField {
   107  	fields := []reflect.StructField{}
   108  	bfsQueue := list.New()
   109  
   110  	for i := 0; i < golangType.NumField(); i++ {
   111  		bfsQueue.PushBack(golangType.Field(i))
   112  	}
   113  	for bfsQueue.Len() > 0 {
   114  		front := bfsQueue.Front()
   115  		field := front.Value.(reflect.StructField)
   116  		bfsQueue.Remove(front)
   117  
   118  		if !field.Anonymous {
   119  			fields = append(fields, field)
   120  			continue
   121  		}
   122  
   123  		fieldType := field.Type
   124  		if fieldType.Kind() == reflect.Pointer {
   125  			fieldType = fieldType.Elem()
   126  		}
   127  
   128  		for i := 0; i < fieldType.NumField(); i++ {
   129  			bfsQueue.PushBack(fieldType.Field(i))
   130  		}
   131  	}
   132  	return fields
   133  }
   134  
   135  func toSchema(golangType reflect.Type, docs *Docs, tracker *tracker) (*jsonschema.Schema, error) {
   136  	// *Struct and Struct generate identical json schemas
   137  	if golangType.Kind() == reflect.Pointer {
   138  		return safeToSchema(golangType.Elem(), docs, "", tracker)
   139  	}
   140  	if golangType.Kind() == reflect.Interface {
   141  		return &jsonschema.Schema{}, nil
   142  	}
   143  
   144  	rootJavascriptType, err := jsonSchemaType(golangType)
   145  	if err != nil {
   146  		return nil, err
   147  	}
   148  	jsonSchema := &jsonschema.Schema{Type: rootJavascriptType}
   149  
   150  	if docs != nil {
   151  		jsonSchema.Description = docs.Description
   152  	}
   153  
   154  	// case array/slice
   155  	if golangType.Kind() == reflect.Array || golangType.Kind() == reflect.Slice {
   156  		elemGolangType := golangType.Elem()
   157  		elemJavascriptType, err := jsonSchemaType(elemGolangType)
   158  		if err != nil {
   159  			return nil, err
   160  		}
   161  		var childDocs *Docs
   162  		if docs != nil {
   163  			childDocs = docs.Items
   164  		}
   165  		elemProps, err := safeToSchema(elemGolangType, childDocs, "", tracker)
   166  		if err != nil {
   167  			return nil, err
   168  		}
   169  		jsonSchema.Items = &jsonschema.Schema{
   170  			Type:                 elemJavascriptType,
   171  			Properties:           elemProps.Properties,
   172  			AdditionalProperties: elemProps.AdditionalProperties,
   173  			Items:                elemProps.Items,
   174  			Required:             elemProps.Required,
   175  		}
   176  	}
   177  
   178  	// case map
   179  	if golangType.Kind() == reflect.Map {
   180  		if golangType.Key().Kind() != reflect.String {
   181  			return nil, fmt.Errorf("only string keyed maps allowed")
   182  		}
   183  		var childDocs *Docs
   184  		if docs != nil {
   185  			childDocs = docs.AdditionalProperties
   186  		}
   187  		jsonSchema.AdditionalProperties, err = safeToSchema(golangType.Elem(), childDocs, "", tracker)
   188  		if err != nil {
   189  			return nil, err
   190  		}
   191  	}
   192  
   193  	// case struct
   194  	if golangType.Kind() == reflect.Struct {
   195  		children := getStructFields(golangType)
   196  		properties := map[string]*jsonschema.Schema{}
   197  		required := []string{}
   198  		for _, child := range children {
   199  			bundleTag := child.Tag.Get("bundle")
   200  			if bundleTag == "readonly" {
   201  				continue
   202  			}
   203  
   204  			// get child json tags
   205  			childJsonTag := strings.Split(child.Tag.Get("json"), ",")
   206  			childName := childJsonTag[0]
   207  
   208  			// skip children that have no json tags, the first json tag is ""
   209  			// or the first json tag is "-"
   210  			if childName == "" || childName == "-" {
   211  				continue
   212  			}
   213  
   214  			// get docs for the child if they exist
   215  			var childDocs *Docs
   216  			if docs != nil {
   217  				if val, ok := docs.Properties[childName]; ok {
   218  					childDocs = val
   219  				}
   220  			}
   221  
   222  			// compute if the child is a required field. Determined by the
   223  			// presence of "omitempty" in the json tags
   224  			hasOmitEmptyTag := false
   225  			for i := 1; i < len(childJsonTag); i++ {
   226  				if childJsonTag[i] == "omitempty" {
   227  					hasOmitEmptyTag = true
   228  				}
   229  			}
   230  			if !hasOmitEmptyTag {
   231  				required = append(required, childName)
   232  			}
   233  
   234  			// compute Schema.Properties for the child recursively
   235  			fieldProps, err := safeToSchema(child.Type, childDocs, childName, tracker)
   236  			if err != nil {
   237  				return nil, err
   238  			}
   239  			properties[childName] = fieldProps
   240  		}
   241  
   242  		jsonSchema.AdditionalProperties = false
   243  		jsonSchema.Properties = properties
   244  		jsonSchema.Required = required
   245  	}
   246  
   247  	return jsonSchema, nil
   248  }