mirror of https://github.com/databricks/cli.git
delete the old schema code
This commit is contained in:
parent
870e41912a
commit
2dea889621
|
@ -52,6 +52,7 @@ func addInterpolationPatterns(_ reflect.Type, s jsonschema.Schema) jsonschema.Sc
|
|||
// as well. Make sure to pull those in.
|
||||
// TODO: Add unit tests for all permutations of structs, maps and slices for the FromType
|
||||
// method.
|
||||
// TODO: Note the minor regression of losing the bundle descriptions
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
### Overview
|
||||
|
||||
`docs/bundle_descriptions.json` contains both autogenerated as well as manually written
|
||||
descriptions for the json schema. Specifically
|
||||
1. `resources` : almost all descriptions are autogenerated from the OpenAPI spec
|
||||
2. `targets` : almost all descriptions are copied over from root level entities (eg: `bundle`, `artifacts`)
|
||||
3. `bundle` : manually editted
|
||||
4. `include` : manually editted
|
||||
5. `workspace` : manually editted
|
||||
6. `artifacts` : manually editted
|
||||
|
||||
These descriptions are rendered in the inline documentation in an IDE
|
||||
|
||||
### SOP: Add schema descriptions for new fields in bundle config
|
||||
|
||||
Manually edit bundle_descriptions.json to add your descriptions. Note that the
|
||||
descriptions in `resources` block is generated from the OpenAPI spec, and thus
|
||||
any changes there will be overwritten.
|
|
@ -1,109 +0,0 @@
|
|||
package schema
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
|
||||
"github.com/databricks/cli/bundle/config"
|
||||
"github.com/databricks/cli/libs/jsonschema"
|
||||
)
|
||||
|
||||
// A subset of Schema struct
|
||||
type Docs struct {
|
||||
Description string `json:"description"`
|
||||
Properties map[string]*Docs `json:"properties,omitempty"`
|
||||
Items *Docs `json:"items,omitempty"`
|
||||
AdditionalProperties *Docs `json:"additionalproperties,omitempty"`
|
||||
}
|
||||
|
||||
//go:embed docs/bundle_descriptions.json
|
||||
var bundleDocs []byte
|
||||
|
||||
func (docs *Docs) refreshTargetsDocs() error {
|
||||
targetsDocs, ok := docs.Properties["targets"]
|
||||
if !ok || targetsDocs.AdditionalProperties == nil ||
|
||||
targetsDocs.AdditionalProperties.Properties == nil {
|
||||
return fmt.Errorf("invalid targets descriptions")
|
||||
}
|
||||
targetProperties := targetsDocs.AdditionalProperties.Properties
|
||||
propertiesToCopy := []string{"artifacts", "bundle", "resources", "workspace"}
|
||||
for _, p := range propertiesToCopy {
|
||||
targetProperties[p] = docs.Properties[p]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func LoadBundleDescriptions() (*Docs, error) {
|
||||
embedded := Docs{}
|
||||
err := json.Unmarshal(bundleDocs, &embedded)
|
||||
return &embedded, err
|
||||
}
|
||||
|
||||
func UpdateBundleDescriptions(openapiSpecPath string) (*Docs, error) {
|
||||
embedded, err := LoadBundleDescriptions()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Generate schema from the embedded descriptions, and convert it back to docs.
|
||||
// This creates empty descriptions for any properties that were missing in the
|
||||
// embedded descriptions.
|
||||
schema, err := New(reflect.TypeOf(config.Root{}), embedded)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
docs := schemaToDocs(schema)
|
||||
|
||||
// Load the Databricks OpenAPI spec
|
||||
openapiSpec, err := os.ReadFile(openapiSpecPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
spec := &Specification{}
|
||||
err = json.Unmarshal(openapiSpec, spec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
openapiReader := &OpenapiReader{
|
||||
OpenapiSpec: spec,
|
||||
memo: make(map[string]jsonschema.Schema),
|
||||
}
|
||||
|
||||
// Generate descriptions for the "resources" field
|
||||
resourcesDocs, err := openapiReader.ResourcesDocs()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resourceSchema, err := New(reflect.TypeOf(config.Resources{}), resourcesDocs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
docs.Properties["resources"] = schemaToDocs(resourceSchema)
|
||||
docs.refreshTargetsDocs()
|
||||
return docs, nil
|
||||
}
|
||||
|
||||
// *Docs are a subset of *Schema, this function selects that subset
|
||||
func schemaToDocs(jsonSchema *jsonschema.Schema) *Docs {
|
||||
// terminate recursion if schema is nil
|
||||
if jsonSchema == nil {
|
||||
return nil
|
||||
}
|
||||
docs := &Docs{
|
||||
Description: jsonSchema.Description,
|
||||
}
|
||||
if len(jsonSchema.Properties) > 0 {
|
||||
docs.Properties = make(map[string]*Docs)
|
||||
}
|
||||
for k, v := range jsonSchema.Properties {
|
||||
docs.Properties[k] = schemaToDocs(v)
|
||||
}
|
||||
docs.Items = schemaToDocs(jsonSchema.Items)
|
||||
if additionalProperties, ok := jsonSchema.AdditionalProperties.(*jsonschema.Schema); ok {
|
||||
docs.AdditionalProperties = schemaToDocs(additionalProperties)
|
||||
}
|
||||
return docs
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,62 +0,0 @@
|
|||
package schema
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/databricks/cli/libs/jsonschema"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSchemaToDocs(t *testing.T) {
|
||||
jsonSchema := &jsonschema.Schema{
|
||||
Type: "object",
|
||||
Description: "root doc",
|
||||
Properties: map[string]*jsonschema.Schema{
|
||||
"foo": {Type: "number", Description: "foo doc"},
|
||||
"bar": {Type: "string"},
|
||||
"octave": {
|
||||
Type: "object",
|
||||
AdditionalProperties: &jsonschema.Schema{Type: "number"},
|
||||
Description: "octave docs",
|
||||
},
|
||||
"scales": {
|
||||
Type: "object",
|
||||
Description: "scale docs",
|
||||
Items: &jsonschema.Schema{Type: "string"},
|
||||
},
|
||||
},
|
||||
}
|
||||
docs := schemaToDocs(jsonSchema)
|
||||
docsJson, err := json.MarshalIndent(docs, " ", " ")
|
||||
require.NoError(t, err)
|
||||
|
||||
expected :=
|
||||
`{
|
||||
"description": "root doc",
|
||||
"properties": {
|
||||
"bar": {
|
||||
"description": ""
|
||||
},
|
||||
"foo": {
|
||||
"description": "foo doc"
|
||||
},
|
||||
"octave": {
|
||||
"description": "octave docs",
|
||||
"additionalproperties": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"scales": {
|
||||
"description": "scale docs",
|
||||
"items": {
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
t.Log("[DEBUG] actual: ", string(docsJson))
|
||||
t.Log("[DEBUG] expected: ", expected)
|
||||
assert.Equal(t, expected, string(docsJson))
|
||||
}
|
|
@ -1,293 +0,0 @@
|
|||
package schema
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/databricks/cli/libs/jsonschema"
|
||||
)
|
||||
|
||||
type OpenapiReader struct {
|
||||
// OpenAPI spec to read schemas from.
|
||||
OpenapiSpec *Specification
|
||||
|
||||
// In-memory cache of schemas read from the OpenAPI spec.
|
||||
memo map[string]jsonschema.Schema
|
||||
}
|
||||
|
||||
const SchemaPathPrefix = "#/components/schemas/"
|
||||
|
||||
// Read a schema directly from the OpenAPI spec.
|
||||
func (reader *OpenapiReader) readOpenapiSchema(path string) (jsonschema.Schema, error) {
|
||||
schemaKey := strings.TrimPrefix(path, SchemaPathPrefix)
|
||||
|
||||
// return early if we already have a computed schema
|
||||
memoSchema, ok := reader.memo[schemaKey]
|
||||
if ok {
|
||||
return memoSchema, nil
|
||||
}
|
||||
|
||||
// check path is present in openapi spec
|
||||
openapiSchema, ok := reader.OpenapiSpec.Components.Schemas[schemaKey]
|
||||
if !ok {
|
||||
return jsonschema.Schema{}, fmt.Errorf("schema with path %s not found in openapi spec", path)
|
||||
}
|
||||
|
||||
// convert openapi schema to the native schema struct
|
||||
bytes, err := json.Marshal(*openapiSchema)
|
||||
if err != nil {
|
||||
return jsonschema.Schema{}, err
|
||||
}
|
||||
jsonSchema := jsonschema.Schema{}
|
||||
err = json.Unmarshal(bytes, &jsonSchema)
|
||||
if err != nil {
|
||||
return jsonschema.Schema{}, err
|
||||
}
|
||||
|
||||
// A hack to convert a map[string]interface{} to *Schema
|
||||
// We rely on the type of a AdditionalProperties in downstream functions
|
||||
// to do reference interpolation
|
||||
_, ok = jsonSchema.AdditionalProperties.(map[string]interface{})
|
||||
if ok {
|
||||
b, err := json.Marshal(jsonSchema.AdditionalProperties)
|
||||
if err != nil {
|
||||
return jsonschema.Schema{}, err
|
||||
}
|
||||
additionalProperties := &jsonschema.Schema{}
|
||||
err = json.Unmarshal(b, additionalProperties)
|
||||
if err != nil {
|
||||
return jsonschema.Schema{}, err
|
||||
}
|
||||
jsonSchema.AdditionalProperties = additionalProperties
|
||||
}
|
||||
|
||||
// store read schema into memo
|
||||
reader.memo[schemaKey] = jsonSchema
|
||||
|
||||
return jsonSchema, nil
|
||||
}
|
||||
|
||||
// Resolve all nested "$ref" references in the schema. This function unrolls a single
|
||||
// level of "$ref" in the schema and calls into traverseSchema to resolve nested references.
|
||||
// Thus this function and traverseSchema are mutually recursive.
|
||||
//
|
||||
// This function is safe against reference loops. If a reference loop is detected, an error
|
||||
// is returned.
|
||||
func (reader *OpenapiReader) safeResolveRefs(root *jsonschema.Schema, tracker *tracker) (*jsonschema.Schema, error) {
|
||||
if root.Reference == nil {
|
||||
return reader.traverseSchema(root, tracker)
|
||||
}
|
||||
key := *root.Reference
|
||||
|
||||
// HACK to unblock CLI release (13th Feb 2024). This is temporary until proper
|
||||
// support for recursive types is added to the docs generator. PR: https://github.com/databricks/cli/pull/1204
|
||||
if strings.Contains(key, "ForEachTask") {
|
||||
return root, nil
|
||||
}
|
||||
|
||||
if tracker.hasCycle(key) {
|
||||
// self reference loops can be supported however the logic is non-trivial because
|
||||
// cross refernce loops are not allowed (see: http://json-schema.org/understanding-json-schema/structuring.html#recursion)
|
||||
return nil, fmt.Errorf("references loop detected")
|
||||
}
|
||||
ref := *root.Reference
|
||||
description := root.Description
|
||||
tracker.push(ref, ref)
|
||||
|
||||
// Mark reference nil, so we do not traverse this again. This is tracked
|
||||
// in the memo
|
||||
root.Reference = nil
|
||||
|
||||
// unroll one level of reference.
|
||||
selfRef, err := reader.readOpenapiSchema(ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
root = &selfRef
|
||||
root.Description = description
|
||||
|
||||
// traverse again to find new references
|
||||
root, err = reader.traverseSchema(root, tracker)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tracker.pop(ref)
|
||||
return root, err
|
||||
}
|
||||
|
||||
// Traverse the nested properties of the schema to resolve "$ref" references. This function
|
||||
// and safeResolveRefs are mutually recursive.
|
||||
func (reader *OpenapiReader) traverseSchema(root *jsonschema.Schema, tracker *tracker) (*jsonschema.Schema, error) {
|
||||
// case primitive (or invalid)
|
||||
if root.Type != jsonschema.ObjectType && root.Type != jsonschema.ArrayType {
|
||||
return root, nil
|
||||
}
|
||||
// only root references are resolved
|
||||
if root.Reference != nil {
|
||||
return reader.safeResolveRefs(root, tracker)
|
||||
}
|
||||
// case struct
|
||||
if len(root.Properties) > 0 {
|
||||
for k, v := range root.Properties {
|
||||
childSchema, err := reader.safeResolveRefs(v, tracker)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
root.Properties[k] = childSchema
|
||||
}
|
||||
}
|
||||
// case array
|
||||
if root.Items != nil {
|
||||
itemsSchema, err := reader.safeResolveRefs(root.Items, tracker)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
root.Items = itemsSchema
|
||||
}
|
||||
// case map
|
||||
additionalProperties, ok := root.AdditionalProperties.(*jsonschema.Schema)
|
||||
if ok && additionalProperties != nil {
|
||||
valueSchema, err := reader.safeResolveRefs(additionalProperties, tracker)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
root.AdditionalProperties = valueSchema
|
||||
}
|
||||
return root, nil
|
||||
}
|
||||
|
||||
func (reader *OpenapiReader) readResolvedSchema(path string) (*jsonschema.Schema, error) {
|
||||
root, err := reader.readOpenapiSchema(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tracker := newTracker()
|
||||
tracker.push(path, path)
|
||||
resolvedRoot, err := reader.safeResolveRefs(&root, tracker)
|
||||
if err != nil {
|
||||
return nil, tracker.errWithTrace(err.Error(), "")
|
||||
}
|
||||
return resolvedRoot, nil
|
||||
}
|
||||
|
||||
func (reader *OpenapiReader) jobsDocs() (*Docs, error) {
|
||||
jobSettingsSchema, err := reader.readResolvedSchema(SchemaPathPrefix + "jobs.JobSettings")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
jobDocs := schemaToDocs(jobSettingsSchema)
|
||||
// TODO: add description for id if needed.
|
||||
// Tracked in https://github.com/databricks/cli/issues/242
|
||||
jobsDocs := &Docs{
|
||||
Description: "List of Databricks jobs",
|
||||
AdditionalProperties: jobDocs,
|
||||
}
|
||||
return jobsDocs, nil
|
||||
}
|
||||
|
||||
func (reader *OpenapiReader) pipelinesDocs() (*Docs, error) {
|
||||
pipelineSpecSchema, err := reader.readResolvedSchema(SchemaPathPrefix + "pipelines.PipelineSpec")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pipelineDocs := schemaToDocs(pipelineSpecSchema)
|
||||
// TODO: Two fields in resources.Pipeline have the json tag id. Clarify the
|
||||
// semantics and then add a description if needed. (https://github.com/databricks/cli/issues/242)
|
||||
pipelinesDocs := &Docs{
|
||||
Description: "List of DLT pipelines",
|
||||
AdditionalProperties: pipelineDocs,
|
||||
}
|
||||
return pipelinesDocs, nil
|
||||
}
|
||||
|
||||
func (reader *OpenapiReader) experimentsDocs() (*Docs, error) {
|
||||
experimentSpecSchema, err := reader.readResolvedSchema(SchemaPathPrefix + "ml.Experiment")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
experimentDocs := schemaToDocs(experimentSpecSchema)
|
||||
experimentsDocs := &Docs{
|
||||
Description: "List of MLflow experiments",
|
||||
AdditionalProperties: experimentDocs,
|
||||
}
|
||||
return experimentsDocs, nil
|
||||
}
|
||||
|
||||
func (reader *OpenapiReader) modelsDocs() (*Docs, error) {
|
||||
modelSpecSchema, err := reader.readResolvedSchema(SchemaPathPrefix + "ml.Model")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
modelDocs := schemaToDocs(modelSpecSchema)
|
||||
modelsDocs := &Docs{
|
||||
Description: "List of MLflow models",
|
||||
AdditionalProperties: modelDocs,
|
||||
}
|
||||
return modelsDocs, nil
|
||||
}
|
||||
|
||||
func (reader *OpenapiReader) modelServingEndpointsDocs() (*Docs, error) {
|
||||
modelServingEndpointsSpecSchema, err := reader.readResolvedSchema(SchemaPathPrefix + "serving.CreateServingEndpoint")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
modelServingEndpointsDocs := schemaToDocs(modelServingEndpointsSpecSchema)
|
||||
modelServingEndpointsAllDocs := &Docs{
|
||||
Description: "List of Model Serving Endpoints",
|
||||
AdditionalProperties: modelServingEndpointsDocs,
|
||||
}
|
||||
return modelServingEndpointsAllDocs, nil
|
||||
}
|
||||
|
||||
func (reader *OpenapiReader) registeredModelDocs() (*Docs, error) {
|
||||
registeredModelsSpecSchema, err := reader.readResolvedSchema(SchemaPathPrefix + "catalog.CreateRegisteredModelRequest")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
registeredModelsDocs := schemaToDocs(registeredModelsSpecSchema)
|
||||
registeredModelsAllDocs := &Docs{
|
||||
Description: "List of Registered Models",
|
||||
AdditionalProperties: registeredModelsDocs,
|
||||
}
|
||||
return registeredModelsAllDocs, nil
|
||||
}
|
||||
|
||||
func (reader *OpenapiReader) ResourcesDocs() (*Docs, error) {
|
||||
jobsDocs, err := reader.jobsDocs()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pipelinesDocs, err := reader.pipelinesDocs()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
experimentsDocs, err := reader.experimentsDocs()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
modelsDocs, err := reader.modelsDocs()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
modelServingEndpointsDocs, err := reader.modelServingEndpointsDocs()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
registeredModelsDocs, err := reader.registeredModelDocs()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Docs{
|
||||
Description: "Collection of Databricks resources to deploy.",
|
||||
Properties: map[string]*Docs{
|
||||
"jobs": jobsDocs,
|
||||
"pipelines": pipelinesDocs,
|
||||
"experiments": experimentsDocs,
|
||||
"models": modelsDocs,
|
||||
"model_serving_endpoints": modelServingEndpointsDocs,
|
||||
"registered_models": registeredModelsDocs,
|
||||
},
|
||||
}, nil
|
||||
}
|
|
@ -1,493 +0,0 @@
|
|||
package schema
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/databricks/cli/libs/jsonschema"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestReadSchemaForObject(t *testing.T) {
|
||||
specString := `
|
||||
{
|
||||
"components": {
|
||||
"schemas": {
|
||||
"foo": {
|
||||
"type": "number"
|
||||
},
|
||||
"fruits": {
|
||||
"type": "object",
|
||||
"description": "fruits that are cool",
|
||||
"properties": {
|
||||
"guava": {
|
||||
"type": "string",
|
||||
"description": "a guava for my schema"
|
||||
},
|
||||
"mango": {
|
||||
"type": "object",
|
||||
"description": "a mango for my schema",
|
||||
"$ref": "#/components/schemas/mango"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mango": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"foo": {
|
||||
"$ref": "#/components/schemas/foo"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
spec := &Specification{}
|
||||
reader := &OpenapiReader{
|
||||
OpenapiSpec: spec,
|
||||
memo: make(map[string]jsonschema.Schema),
|
||||
}
|
||||
err := json.Unmarshal([]byte(specString), spec)
|
||||
require.NoError(t, err)
|
||||
|
||||
fruitsSchema, err := reader.readResolvedSchema("#/components/schemas/fruits")
|
||||
require.NoError(t, err)
|
||||
|
||||
fruitsSchemaJson, err := json.MarshalIndent(fruitsSchema, " ", " ")
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := `{
|
||||
"type": "object",
|
||||
"description": "fruits that are cool",
|
||||
"properties": {
|
||||
"guava": {
|
||||
"type": "string",
|
||||
"description": "a guava for my schema"
|
||||
},
|
||||
"mango": {
|
||||
"type": "object",
|
||||
"description": "a mango for my schema",
|
||||
"properties": {
|
||||
"foo": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
t.Log("[DEBUG] actual: ", string(fruitsSchemaJson))
|
||||
t.Log("[DEBUG] expected: ", expected)
|
||||
assert.Equal(t, expected, string(fruitsSchemaJson))
|
||||
}
|
||||
|
||||
func TestReadSchemaForArray(t *testing.T) {
|
||||
specString := `
|
||||
{
|
||||
"components": {
|
||||
"schemas": {
|
||||
"fruits": {
|
||||
"type": "object",
|
||||
"description": "fruits that are cool",
|
||||
"items": {
|
||||
"description": "some papayas, because papayas are fruits too",
|
||||
"$ref": "#/components/schemas/papaya"
|
||||
}
|
||||
},
|
||||
"papaya": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
spec := &Specification{}
|
||||
reader := &OpenapiReader{
|
||||
OpenapiSpec: spec,
|
||||
memo: make(map[string]jsonschema.Schema),
|
||||
}
|
||||
err := json.Unmarshal([]byte(specString), spec)
|
||||
require.NoError(t, err)
|
||||
|
||||
fruitsSchema, err := reader.readResolvedSchema("#/components/schemas/fruits")
|
||||
require.NoError(t, err)
|
||||
|
||||
fruitsSchemaJson, err := json.MarshalIndent(fruitsSchema, " ", " ")
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := `{
|
||||
"type": "object",
|
||||
"description": "fruits that are cool",
|
||||
"items": {
|
||||
"type": "number",
|
||||
"description": "some papayas, because papayas are fruits too"
|
||||
}
|
||||
}`
|
||||
|
||||
t.Log("[DEBUG] actual: ", string(fruitsSchemaJson))
|
||||
t.Log("[DEBUG] expected: ", expected)
|
||||
assert.Equal(t, expected, string(fruitsSchemaJson))
|
||||
}
|
||||
|
||||
func TestReadSchemaForMap(t *testing.T) {
|
||||
specString := `{
|
||||
"components": {
|
||||
"schemas": {
|
||||
"fruits": {
|
||||
"type": "object",
|
||||
"description": "fruits that are meh",
|
||||
"additionalProperties": {
|
||||
"description": "watermelons. watermelons.",
|
||||
"$ref": "#/components/schemas/watermelon"
|
||||
}
|
||||
},
|
||||
"watermelon": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
spec := &Specification{}
|
||||
reader := &OpenapiReader{
|
||||
OpenapiSpec: spec,
|
||||
memo: make(map[string]jsonschema.Schema),
|
||||
}
|
||||
err := json.Unmarshal([]byte(specString), spec)
|
||||
require.NoError(t, err)
|
||||
|
||||
fruitsSchema, err := reader.readResolvedSchema("#/components/schemas/fruits")
|
||||
require.NoError(t, err)
|
||||
|
||||
fruitsSchemaJson, err := json.MarshalIndent(fruitsSchema, " ", " ")
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := `{
|
||||
"type": "object",
|
||||
"description": "fruits that are meh",
|
||||
"additionalProperties": {
|
||||
"type": "number",
|
||||
"description": "watermelons. watermelons."
|
||||
}
|
||||
}`
|
||||
|
||||
t.Log("[DEBUG] actual: ", string(fruitsSchemaJson))
|
||||
t.Log("[DEBUG] expected: ", expected)
|
||||
assert.Equal(t, expected, string(fruitsSchemaJson))
|
||||
}
|
||||
|
||||
func TestRootReferenceIsResolved(t *testing.T) {
|
||||
specString := `{
|
||||
"components": {
|
||||
"schemas": {
|
||||
"foo": {
|
||||
"type": "object",
|
||||
"description": "this description is ignored",
|
||||
"properties": {
|
||||
"abc": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"fruits": {
|
||||
"type": "object",
|
||||
"description": "foo fighters fighting fruits",
|
||||
"$ref": "#/components/schemas/foo"
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
spec := &Specification{}
|
||||
reader := &OpenapiReader{
|
||||
OpenapiSpec: spec,
|
||||
memo: make(map[string]jsonschema.Schema),
|
||||
}
|
||||
err := json.Unmarshal([]byte(specString), spec)
|
||||
require.NoError(t, err)
|
||||
|
||||
schema, err := reader.readResolvedSchema("#/components/schemas/fruits")
|
||||
require.NoError(t, err)
|
||||
fruitsSchemaJson, err := json.MarshalIndent(schema, " ", " ")
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := `{
|
||||
"type": "object",
|
||||
"description": "foo fighters fighting fruits",
|
||||
"properties": {
|
||||
"abc": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
t.Log("[DEBUG] actual: ", string(fruitsSchemaJson))
|
||||
t.Log("[DEBUG] expected: ", expected)
|
||||
assert.Equal(t, expected, string(fruitsSchemaJson))
|
||||
}
|
||||
|
||||
func TestSelfReferenceLoopErrors(t *testing.T) {
|
||||
specString := `{
|
||||
"components": {
|
||||
"schemas": {
|
||||
"foo": {
|
||||
"type": "object",
|
||||
"description": "this description is ignored",
|
||||
"properties": {
|
||||
"bar": {
|
||||
"type": "object",
|
||||
"$ref": "#/components/schemas/foo"
|
||||
}
|
||||
}
|
||||
},
|
||||
"fruits": {
|
||||
"type": "object",
|
||||
"description": "foo fighters fighting fruits",
|
||||
"$ref": "#/components/schemas/foo"
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
spec := &Specification{}
|
||||
reader := &OpenapiReader{
|
||||
OpenapiSpec: spec,
|
||||
memo: make(map[string]jsonschema.Schema),
|
||||
}
|
||||
err := json.Unmarshal([]byte(specString), spec)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = reader.readResolvedSchema("#/components/schemas/fruits")
|
||||
assert.ErrorContains(t, err, "references loop detected. traversal trace: -> #/components/schemas/fruits -> #/components/schemas/foo")
|
||||
}
|
||||
|
||||
func TestCrossReferenceLoopErrors(t *testing.T) {
|
||||
specString := `{
|
||||
"components": {
|
||||
"schemas": {
|
||||
"foo": {
|
||||
"type": "object",
|
||||
"description": "this description is ignored",
|
||||
"properties": {
|
||||
"bar": {
|
||||
"type": "object",
|
||||
"$ref": "#/components/schemas/fruits"
|
||||
}
|
||||
}
|
||||
},
|
||||
"fruits": {
|
||||
"type": "object",
|
||||
"description": "foo fighters fighting fruits",
|
||||
"$ref": "#/components/schemas/foo"
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
spec := &Specification{}
|
||||
reader := &OpenapiReader{
|
||||
OpenapiSpec: spec,
|
||||
memo: make(map[string]jsonschema.Schema),
|
||||
}
|
||||
err := json.Unmarshal([]byte(specString), spec)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = reader.readResolvedSchema("#/components/schemas/fruits")
|
||||
assert.ErrorContains(t, err, "references loop detected. traversal trace: -> #/components/schemas/fruits -> #/components/schemas/foo")
|
||||
}
|
||||
|
||||
func TestReferenceResolutionForMapInObject(t *testing.T) {
|
||||
specString := `
|
||||
{
|
||||
"components": {
|
||||
"schemas": {
|
||||
"foo": {
|
||||
"type": "number"
|
||||
},
|
||||
"fruits": {
|
||||
"type": "object",
|
||||
"description": "fruits that are cool",
|
||||
"properties": {
|
||||
"guava": {
|
||||
"type": "string",
|
||||
"description": "a guava for my schema"
|
||||
},
|
||||
"mangos": {
|
||||
"type": "object",
|
||||
"description": "multiple mangos",
|
||||
"$ref": "#/components/schemas/mango"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mango": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"description": "a single mango",
|
||||
"$ref": "#/components/schemas/foo"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
spec := &Specification{}
|
||||
reader := &OpenapiReader{
|
||||
OpenapiSpec: spec,
|
||||
memo: make(map[string]jsonschema.Schema),
|
||||
}
|
||||
err := json.Unmarshal([]byte(specString), spec)
|
||||
require.NoError(t, err)
|
||||
|
||||
fruitsSchema, err := reader.readResolvedSchema("#/components/schemas/fruits")
|
||||
require.NoError(t, err)
|
||||
|
||||
fruitsSchemaJson, err := json.MarshalIndent(fruitsSchema, " ", " ")
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := `{
|
||||
"type": "object",
|
||||
"description": "fruits that are cool",
|
||||
"properties": {
|
||||
"guava": {
|
||||
"type": "string",
|
||||
"description": "a guava for my schema"
|
||||
},
|
||||
"mangos": {
|
||||
"type": "object",
|
||||
"description": "multiple mangos",
|
||||
"additionalProperties": {
|
||||
"type": "number",
|
||||
"description": "a single mango"
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
t.Log("[DEBUG] actual: ", string(fruitsSchemaJson))
|
||||
t.Log("[DEBUG] expected: ", expected)
|
||||
assert.Equal(t, expected, string(fruitsSchemaJson))
|
||||
}
|
||||
|
||||
func TestReferenceResolutionForArrayInObject(t *testing.T) {
|
||||
specString := `{
|
||||
"components": {
|
||||
"schemas": {
|
||||
"foo": {
|
||||
"type": "number"
|
||||
},
|
||||
"fruits": {
|
||||
"type": "object",
|
||||
"description": "fruits that are cool",
|
||||
"properties": {
|
||||
"guava": {
|
||||
"type": "string",
|
||||
"description": "a guava for my schema"
|
||||
},
|
||||
"mangos": {
|
||||
"type": "object",
|
||||
"description": "multiple mangos",
|
||||
"$ref": "#/components/schemas/mango"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mango": {
|
||||
"type": "object",
|
||||
"items": {
|
||||
"description": "a single mango",
|
||||
"$ref": "#/components/schemas/foo"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
spec := &Specification{}
|
||||
reader := &OpenapiReader{
|
||||
OpenapiSpec: spec,
|
||||
memo: make(map[string]jsonschema.Schema),
|
||||
}
|
||||
err := json.Unmarshal([]byte(specString), spec)
|
||||
require.NoError(t, err)
|
||||
|
||||
fruitsSchema, err := reader.readResolvedSchema("#/components/schemas/fruits")
|
||||
require.NoError(t, err)
|
||||
|
||||
fruitsSchemaJson, err := json.MarshalIndent(fruitsSchema, " ", " ")
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := `{
|
||||
"type": "object",
|
||||
"description": "fruits that are cool",
|
||||
"properties": {
|
||||
"guava": {
|
||||
"type": "string",
|
||||
"description": "a guava for my schema"
|
||||
},
|
||||
"mangos": {
|
||||
"type": "object",
|
||||
"description": "multiple mangos",
|
||||
"items": {
|
||||
"type": "number",
|
||||
"description": "a single mango"
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
t.Log("[DEBUG] actual: ", string(fruitsSchemaJson))
|
||||
t.Log("[DEBUG] expected: ", expected)
|
||||
assert.Equal(t, expected, string(fruitsSchemaJson))
|
||||
}
|
||||
|
||||
func TestReferenceResolutionDoesNotOverwriteDescriptions(t *testing.T) {
|
||||
specString := `{
|
||||
"components": {
|
||||
"schemas": {
|
||||
"foo": {
|
||||
"type": "number"
|
||||
},
|
||||
"fruits": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"guava": {
|
||||
"type": "object",
|
||||
"description": "Guava is a fruit",
|
||||
"$ref": "#/components/schemas/foo"
|
||||
},
|
||||
"mango": {
|
||||
"type": "object",
|
||||
"description": "What is a mango?",
|
||||
"$ref": "#/components/schemas/foo"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
spec := &Specification{}
|
||||
reader := &OpenapiReader{
|
||||
OpenapiSpec: spec,
|
||||
memo: make(map[string]jsonschema.Schema),
|
||||
}
|
||||
err := json.Unmarshal([]byte(specString), spec)
|
||||
require.NoError(t, err)
|
||||
|
||||
fruitsSchema, err := reader.readResolvedSchema("#/components/schemas/fruits")
|
||||
require.NoError(t, err)
|
||||
|
||||
fruitsSchemaJson, err := json.MarshalIndent(fruitsSchema, " ", " ")
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := `{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"guava": {
|
||||
"type": "number",
|
||||
"description": "Guava is a fruit"
|
||||
},
|
||||
"mango": {
|
||||
"type": "number",
|
||||
"description": "What is a mango?"
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
t.Log("[DEBUG] actual: ", string(fruitsSchemaJson))
|
||||
t.Log("[DEBUG] expected: ", expected)
|
||||
assert.Equal(t, expected, string(fruitsSchemaJson))
|
||||
}
|
|
@ -1,307 +0,0 @@
|
|||
package schema
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/databricks/cli/libs/jsonschema"
|
||||
)
|
||||
|
||||
// // Fields tagged "readonly" should not be emitted in the schema as they are
|
||||
// // computed at runtime, and should not be assigned a value by the bundle author.
|
||||
// const readonlyTag = "readonly"
|
||||
|
||||
// // Annotation for internal bundle fields that should not be exposed to customers.
|
||||
// // Fields can be tagged as "internal" to remove them from the generated schema.
|
||||
// const internalTag = "internal"
|
||||
|
||||
// // Annotation for bundle fields that have been deprecated.
|
||||
// // Fields tagged as "deprecated" are removed/omitted from the generated schema.
|
||||
// const deprecatedTag = "deprecated"
|
||||
|
||||
// This function translates golang types into json schema. Here is the mapping
|
||||
// between json schema types and golang types
|
||||
//
|
||||
// - GolangType -> Javascript type / Json Schema2
|
||||
//
|
||||
// - bool -> boolean
|
||||
//
|
||||
// - string -> string
|
||||
//
|
||||
// - int (all variants) -> number
|
||||
//
|
||||
// - float (all variants) -> number
|
||||
//
|
||||
// - map[string]MyStruct -> { type: object, additionalProperties: {}}
|
||||
// for details visit: https://json-schema.org/understanding-json-schema/reference/object.html#additional-properties
|
||||
//
|
||||
// - []MyStruct -> {type: array, items: {}}
|
||||
// for details visit: https://json-schema.org/understanding-json-schema/reference/array.html#items
|
||||
//
|
||||
// - []MyStruct -> {type: object, properties: {}, additionalProperties: false}
|
||||
// for details visit: https://json-schema.org/understanding-json-schema/reference/object.html#properties
|
||||
func New(golangType reflect.Type, docs *Docs) (*jsonschema.Schema, error) {
|
||||
|
||||
return nil, nil
|
||||
|
||||
// s, err := jsonschema.FromType(golangType, func(s jsonschema.Schema) jsonschema.Schema {
|
||||
// if s.Type == jsonschema.NumberType || s.Type == jsonschema.BooleanType {
|
||||
// s = jsonschema.Schema{
|
||||
// AnyOf: []jsonschema.Schema{
|
||||
// s,
|
||||
// {
|
||||
// Type: jsonschema.StringType,
|
||||
// // TODO: Narrow down the scope of the regex match.
|
||||
// // Also likely need to rename this variable.
|
||||
// Pattern: dynvar.ReferenceRegex,
|
||||
// },
|
||||
// },
|
||||
// }
|
||||
// }
|
||||
// return s
|
||||
// })
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// return &s, nil
|
||||
|
||||
// tracker := newTracker()
|
||||
// schema, err := safeToSchema(golangType, docs, "", tracker)
|
||||
// if err != nil {
|
||||
// return nil, tracker.errWithTrace(err.Error(), "root")
|
||||
// }
|
||||
// return schema, nil
|
||||
}
|
||||
|
||||
// func jsonSchemaType(golangType reflect.Type) (jsonschema.Type, error) {
|
||||
// switch golangType.Kind() {
|
||||
// case reflect.Bool:
|
||||
// return jsonschema.BooleanType, nil
|
||||
// case reflect.String:
|
||||
// return jsonschema.StringType, nil
|
||||
// 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 jsonschema.NumberType, nil
|
||||
// case reflect.Struct:
|
||||
// return jsonschema.ObjectType, nil
|
||||
// case reflect.Map:
|
||||
// if golangType.Key().Kind() != reflect.String {
|
||||
// return jsonschema.InvalidType, fmt.Errorf("only strings map keys are valid. key type: %v", golangType.Key().Kind())
|
||||
// }
|
||||
// return jsonschema.ObjectType, nil
|
||||
// case reflect.Array, reflect.Slice:
|
||||
// return jsonschema.ArrayType, nil
|
||||
// default:
|
||||
// return jsonschema.InvalidType, fmt.Errorf("unhandled golang type: %s", golangType)
|
||||
// }
|
||||
// }
|
||||
|
||||
// A wrapper over toSchema function to:
|
||||
// 1. Detect cycles in the bundle config struct.
|
||||
// 2. Update tracker
|
||||
//
|
||||
// params:
|
||||
//
|
||||
// - golangType: Golang type to generate json schema for
|
||||
//
|
||||
// - docs: Contains documentation to be injected into the generated json schema
|
||||
//
|
||||
// - traceId: An identifier for the current type, to trace recursive traversal.
|
||||
// Its value is the first json tag in case of struct fields and "" in other cases
|
||||
// like array, map or no json tags
|
||||
//
|
||||
// - tracker: Keeps track of types / traceIds seen during recursive traversal
|
||||
// func safeToSchema(golangType reflect.Type, docs *Docs, traceId string, tracker *tracker) (*jsonschema.Schema, error) {
|
||||
// // HACK to unblock CLI release (13th Feb 2024). This is temporary until proper
|
||||
// // support for recursive types is added to the schema generator. PR: https://github.com/databricks/cli/pull/1204
|
||||
// if traceId == "for_each_task" {
|
||||
// return &jsonschema.Schema{
|
||||
// Type: jsonschema.ObjectType,
|
||||
// }, nil
|
||||
// }
|
||||
|
||||
// // WE ERROR OUT IF THERE ARE CYCLES IN THE JSON SCHEMA
|
||||
// // There are mechanisms to deal with cycles though recursive identifiers in json
|
||||
// // schema. However if we use them, we would need to make sure we are able to detect
|
||||
// // cycles where two properties (directly or indirectly) pointing to each other
|
||||
// //
|
||||
// // see: https://json-schema.org/understanding-json-schema/structuring.html#recursion
|
||||
// // for details
|
||||
// if tracker.hasCycle(golangType) {
|
||||
// return nil, fmt.Errorf("cycle detected")
|
||||
// }
|
||||
|
||||
// tracker.push(golangType, traceId)
|
||||
// props, err := toSchema(golangType, docs, tracker)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// tracker.pop(golangType)
|
||||
// return props, nil
|
||||
// }
|
||||
|
||||
// This function returns all member fields of the provided type.
|
||||
// If the type has embedded (aka anonymous) fields, this function traverses
|
||||
// those in a breadth first manner
|
||||
// func getStructFields(golangType reflect.Type) []reflect.StructField {
|
||||
// fields := []reflect.StructField{}
|
||||
// bfsQueue := list.New()
|
||||
|
||||
// for i := 0; i < golangType.NumField(); i++ {
|
||||
// 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
|
||||
// }
|
||||
|
||||
// fieldType := field.Type
|
||||
// if fieldType.Kind() == reflect.Pointer {
|
||||
// fieldType = fieldType.Elem()
|
||||
// }
|
||||
|
||||
// for i := 0; i < fieldType.NumField(); i++ {
|
||||
// bfsQueue.PushBack(fieldType.Field(i))
|
||||
// }
|
||||
// }
|
||||
// return fields
|
||||
// }
|
||||
|
||||
// func toSchema(golangType reflect.Type, docs *Docs, tracker *tracker) (*jsonschema.Schema, error) {
|
||||
// // *Struct and Struct generate identical json schemas
|
||||
// if golangType.Kind() == reflect.Pointer {
|
||||
// return safeToSchema(golangType.Elem(), docs, "", tracker)
|
||||
// }
|
||||
// if golangType.Kind() == reflect.Interface {
|
||||
// return &jsonschema.Schema{}, nil
|
||||
// }
|
||||
|
||||
// rootJavascriptType, err := jsonSchemaType(golangType)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// jsonSchema := &jsonschema.Schema{Type: rootJavascriptType}
|
||||
|
||||
// // If the type is a non-string primitive, then we allow it to be a string
|
||||
// // provided it's a pure variable reference (ie only a single variable reference).
|
||||
// if rootJavascriptType == jsonschema.BooleanType || rootJavascriptType == jsonschema.NumberType {
|
||||
// jsonSchema = &jsonschema.Schema{
|
||||
// AnyOf: []*jsonschema.Schema{
|
||||
// {
|
||||
// Type: rootJavascriptType,
|
||||
// },
|
||||
// {
|
||||
// Type: jsonschema.StringType,
|
||||
// Pattern: dynvar.VariableRegex,
|
||||
// },
|
||||
// },
|
||||
// }
|
||||
// }
|
||||
|
||||
// if docs != nil {
|
||||
// jsonSchema.Description = docs.Description
|
||||
// }
|
||||
|
||||
// // case array/slice
|
||||
// if golangType.Kind() == reflect.Array || golangType.Kind() == reflect.Slice {
|
||||
// elemGolangType := golangType.Elem()
|
||||
// elemJavascriptType, err := jsonSchemaType(elemGolangType)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// var childDocs *Docs
|
||||
// if docs != nil {
|
||||
// childDocs = docs.Items
|
||||
// }
|
||||
// elemProps, err := safeToSchema(elemGolangType, childDocs, "", tracker)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// jsonSchema.Items = &jsonschema.Schema{
|
||||
// Type: elemJavascriptType,
|
||||
// Properties: elemProps.Properties,
|
||||
// AdditionalProperties: elemProps.AdditionalProperties,
|
||||
// Items: elemProps.Items,
|
||||
// Required: elemProps.Required,
|
||||
// }
|
||||
// }
|
||||
|
||||
// // case map
|
||||
// if golangType.Kind() == reflect.Map {
|
||||
// if golangType.Key().Kind() != reflect.String {
|
||||
// return nil, fmt.Errorf("only string keyed maps allowed")
|
||||
// }
|
||||
// var childDocs *Docs
|
||||
// if docs != nil {
|
||||
// childDocs = docs.AdditionalProperties
|
||||
// }
|
||||
// jsonSchema.AdditionalProperties, err = safeToSchema(golangType.Elem(), childDocs, "", tracker)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// }
|
||||
|
||||
// // case struct
|
||||
// if golangType.Kind() == reflect.Struct {
|
||||
// children := getStructFields(golangType)
|
||||
// properties := map[string]*jsonschema.Schema{}
|
||||
// required := []string{}
|
||||
// for _, child := range children {
|
||||
// bundleTag := child.Tag.Get("bundle")
|
||||
// // Fields marked as "readonly", "internal" or "deprecated" are skipped
|
||||
// // while generating the schema
|
||||
// if bundleTag == readonlyTag || bundleTag == internalTag || bundleTag == deprecatedTag {
|
||||
// continue
|
||||
// }
|
||||
|
||||
// // get child json tags
|
||||
// childJsonTag := strings.Split(child.Tag.Get("json"), ",")
|
||||
// childName := childJsonTag[0]
|
||||
|
||||
// // skip children that have no json tags, the first json tag is ""
|
||||
// // or the first json tag is "-"
|
||||
// if childName == "" || childName == "-" {
|
||||
// continue
|
||||
// }
|
||||
|
||||
// // get docs for the child if they exist
|
||||
// var childDocs *Docs
|
||||
// if docs != nil {
|
||||
// if val, ok := docs.Properties[childName]; ok {
|
||||
// childDocs = val
|
||||
// }
|
||||
// }
|
||||
|
||||
// // compute if the child is a required field. Determined by the
|
||||
// // presence of "omitempty" in the json tags
|
||||
// hasOmitEmptyTag := false
|
||||
// for i := 1; i < len(childJsonTag); i++ {
|
||||
// if childJsonTag[i] == "omitempty" {
|
||||
// hasOmitEmptyTag = true
|
||||
// }
|
||||
// }
|
||||
// if !hasOmitEmptyTag {
|
||||
// required = append(required, childName)
|
||||
// }
|
||||
|
||||
// // compute Schema.Properties for the child recursively
|
||||
// fieldProps, err := safeToSchema(child.Type, childDocs, childName, tracker)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// properties[childName] = fieldProps
|
||||
// }
|
||||
|
||||
// jsonSchema.AdditionalProperties = false
|
||||
// jsonSchema.Properties = properties
|
||||
// jsonSchema.Required = required
|
||||
// }
|
||||
|
||||
// return jsonSchema, nil
|
||||
// }
|
|
@ -1,341 +0,0 @@
|
|||
package schema
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TODO: Add a test that checks the primitive overrides for reference regexs work.
|
||||
// Basically that the custom override for bundle regex works.
|
||||
|
||||
// TODO: Add a bundle of end to end tests, that both fail and pass the schema validation.
|
||||
|
||||
func TestDocIngestionForObject(t *testing.T) {
|
||||
docs := &Docs{
|
||||
Description: "docs for root",
|
||||
Properties: map[string]*Docs{
|
||||
"my_struct": {
|
||||
Description: "docs for my struct",
|
||||
Properties: map[string]*Docs{
|
||||
"a": {
|
||||
Description: "docs for a",
|
||||
},
|
||||
"c": {
|
||||
Description: "docs for c which does not exist on my_struct",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type MyStruct struct {
|
||||
A string `json:"a"`
|
||||
B int `json:"b"`
|
||||
}
|
||||
|
||||
type Root struct {
|
||||
MyStruct *MyStruct `json:"my_struct"`
|
||||
}
|
||||
|
||||
elem := Root{}
|
||||
|
||||
schema, err := New(reflect.TypeOf(elem), docs)
|
||||
require.NoError(t, err)
|
||||
|
||||
jsonSchema, err := json.MarshalIndent(schema, " ", " ")
|
||||
assert.NoError(t, err)
|
||||
|
||||
expectedSchema :=
|
||||
`{
|
||||
"type": "object",
|
||||
"description": "docs for root",
|
||||
"properties": {
|
||||
"my_struct": {
|
||||
"type": "object",
|
||||
"description": "docs for my struct",
|
||||
"properties": {
|
||||
"a": {
|
||||
"type": "string",
|
||||
"description": "docs for a"
|
||||
},
|
||||
"b": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "\\$\\{([a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\.[a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\[[0-9]+\\])*)*(\\[[0-9]+\\])*)\\}"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"a",
|
||||
"b"
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"my_struct"
|
||||
]
|
||||
}`
|
||||
|
||||
t.Log("[DEBUG] actual: ", string(jsonSchema))
|
||||
t.Log("[DEBUG] expected: ", expectedSchema)
|
||||
|
||||
assert.Equal(t, expectedSchema, string(jsonSchema))
|
||||
}
|
||||
|
||||
func TestDocIngestionForSlice(t *testing.T) {
|
||||
docs := &Docs{
|
||||
Description: "docs for root",
|
||||
Properties: map[string]*Docs{
|
||||
"my_slice": {
|
||||
Description: "docs for my slice",
|
||||
Items: &Docs{
|
||||
Properties: map[string]*Docs{
|
||||
"guava": {
|
||||
Description: "docs for guava",
|
||||
},
|
||||
"pineapple": {
|
||||
Description: "docs for pineapple",
|
||||
},
|
||||
"watermelon": {
|
||||
Description: "docs for watermelon which does not exist in schema",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type Bar struct {
|
||||
Guava int `json:"guava"`
|
||||
Pineapple int `json:"pineapple"`
|
||||
}
|
||||
|
||||
type Root struct {
|
||||
MySlice []Bar `json:"my_slice"`
|
||||
}
|
||||
|
||||
elem := Root{}
|
||||
|
||||
schema, err := New(reflect.TypeOf(elem), docs)
|
||||
require.NoError(t, err)
|
||||
|
||||
jsonSchema, err := json.MarshalIndent(schema, " ", " ")
|
||||
assert.NoError(t, err)
|
||||
|
||||
expectedSchema :=
|
||||
`{
|
||||
"type": "object",
|
||||
"description": "docs for root",
|
||||
"properties": {
|
||||
"my_slice": {
|
||||
"type": "array",
|
||||
"description": "docs for my slice",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"guava": {
|
||||
"description": "docs for guava",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "\\$\\{([a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\.[a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\[[0-9]+\\])*)*(\\[[0-9]+\\])*)\\}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"pineapple": {
|
||||
"description": "docs for pineapple",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "\\$\\{([a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\.[a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\[[0-9]+\\])*)*(\\[[0-9]+\\])*)\\}"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"guava",
|
||||
"pineapple"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"my_slice"
|
||||
]
|
||||
}`
|
||||
|
||||
t.Log("[DEBUG] actual: ", string(jsonSchema))
|
||||
t.Log("[DEBUG] expected: ", expectedSchema)
|
||||
|
||||
assert.Equal(t, expectedSchema, string(jsonSchema))
|
||||
}
|
||||
|
||||
func TestDocIngestionForMap(t *testing.T) {
|
||||
docs := &Docs{
|
||||
Description: "docs for root",
|
||||
Properties: map[string]*Docs{
|
||||
"my_map": {
|
||||
Description: "docs for my map",
|
||||
AdditionalProperties: &Docs{
|
||||
Properties: map[string]*Docs{
|
||||
"apple": {
|
||||
Description: "docs for apple",
|
||||
},
|
||||
"mango": {
|
||||
Description: "docs for mango",
|
||||
},
|
||||
"watermelon": {
|
||||
Description: "docs for watermelon which does not exist in schema",
|
||||
},
|
||||
"papaya": {
|
||||
Description: "docs for papaya which does not exist in schema",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type Foo struct {
|
||||
Apple int `json:"apple"`
|
||||
Mango int `json:"mango"`
|
||||
}
|
||||
|
||||
type Root struct {
|
||||
MyMap map[string]*Foo `json:"my_map"`
|
||||
}
|
||||
|
||||
elem := Root{}
|
||||
|
||||
schema, err := New(reflect.TypeOf(elem), docs)
|
||||
require.NoError(t, err)
|
||||
|
||||
jsonSchema, err := json.MarshalIndent(schema, " ", " ")
|
||||
assert.NoError(t, err)
|
||||
|
||||
expectedSchema :=
|
||||
`{
|
||||
"type": "object",
|
||||
"description": "docs for root",
|
||||
"properties": {
|
||||
"my_map": {
|
||||
"type": "object",
|
||||
"description": "docs for my map",
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"apple": {
|
||||
"description": "docs for apple",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "\\$\\{([a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\.[a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\[[0-9]+\\])*)*(\\[[0-9]+\\])*)\\}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"mango": {
|
||||
"description": "docs for mango",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "\\$\\{([a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\.[a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\[[0-9]+\\])*)*(\\[[0-9]+\\])*)\\}"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"apple",
|
||||
"mango"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"my_map"
|
||||
]
|
||||
}`
|
||||
|
||||
t.Log("[DEBUG] actual: ", string(jsonSchema))
|
||||
t.Log("[DEBUG] expected: ", expectedSchema)
|
||||
|
||||
assert.Equal(t, expectedSchema, string(jsonSchema))
|
||||
}
|
||||
|
||||
func TestDocIngestionForTopLevelPrimitive(t *testing.T) {
|
||||
docs := &Docs{
|
||||
Description: "docs for root",
|
||||
Properties: map[string]*Docs{
|
||||
"my_val": {
|
||||
Description: "docs for my val",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type Root struct {
|
||||
MyVal int `json:"my_val"`
|
||||
}
|
||||
|
||||
elem := Root{}
|
||||
|
||||
schema, err := New(reflect.TypeOf(elem), docs)
|
||||
require.NoError(t, err)
|
||||
|
||||
jsonSchema, err := json.MarshalIndent(schema, " ", " ")
|
||||
assert.NoError(t, err)
|
||||
|
||||
expectedSchema :=
|
||||
`{
|
||||
"type": "object",
|
||||
"description": "docs for root",
|
||||
"properties": {
|
||||
"my_val": {
|
||||
"description": "docs for my val",
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "\\$\\{([a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\.[a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\[[0-9]+\\])*)*(\\[[0-9]+\\])*)\\}"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"my_val"
|
||||
]
|
||||
}`
|
||||
|
||||
t.Log("[DEBUG] actual: ", string(jsonSchema))
|
||||
t.Log("[DEBUG] expected: ", expectedSchema)
|
||||
|
||||
assert.Equal(t, expectedSchema, string(jsonSchema))
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
package schema
|
||||
|
||||
import "github.com/databricks/cli/libs/jsonschema"
|
||||
|
||||
type Specification struct {
|
||||
Components *Components `json:"components"`
|
||||
}
|
||||
|
||||
type Components struct {
|
||||
Schemas map[string]*jsonschema.Schema `json:"schemas,omitempty"`
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
package schema
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type tracker struct {
|
||||
// Nodes encountered in current path during the recursive traversal. Used to
|
||||
// check for cycles
|
||||
seenNodes map[interface{}]struct{}
|
||||
|
||||
// List of node names encountered in order in current path during the recursive traversal.
|
||||
// Used to hydrate errors with path to the exact node where error occured.
|
||||
//
|
||||
// NOTE: node and node names can be the same
|
||||
listOfNodes *list.List
|
||||
}
|
||||
|
||||
func newTracker() *tracker {
|
||||
return &tracker{
|
||||
seenNodes: map[interface{}]struct{}{},
|
||||
listOfNodes: list.New(),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *tracker) errWithTrace(prefix string, initTrace string) error {
|
||||
traceString := initTrace
|
||||
curr := t.listOfNodes.Front()
|
||||
for curr != nil {
|
||||
if curr.Value.(string) != "" {
|
||||
traceString += " -> " + curr.Value.(string)
|
||||
}
|
||||
curr = curr.Next()
|
||||
}
|
||||
return fmt.Errorf(prefix + ". traversal trace: " + traceString)
|
||||
}
|
||||
|
||||
func (t *tracker) hasCycle(node interface{}) bool {
|
||||
_, ok := t.seenNodes[node]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (t *tracker) push(node interface{}, name string) {
|
||||
t.seenNodes[node] = struct{}{}
|
||||
t.listOfNodes.PushBack(name)
|
||||
}
|
||||
|
||||
func (t *tracker) pop(nodeType interface{}) {
|
||||
back := t.listOfNodes.Back()
|
||||
t.listOfNodes.Remove(back)
|
||||
delete(t.seenNodes, nodeType)
|
||||
}
|
Loading…
Reference in New Issue