diff --git a/bundle/schema/docs.go b/bundle/schema/docs.go index fe63e432..0073c546 100644 --- a/bundle/schema/docs.go +++ b/bundle/schema/docs.go @@ -52,7 +52,7 @@ func UpdateBundleDescriptions(openapiSpecPath string) (*Docs, error) { // 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) + schema, err := New(reflect.TypeOf(config.Root{}), embedded, nil) if err != nil { return nil, err } @@ -78,7 +78,7 @@ func UpdateBundleDescriptions(openapiSpecPath string) (*Docs, error) { if err != nil { return nil, err } - resourceSchema, err := New(reflect.TypeOf(config.Resources{}), resourcesDocs) + resourceSchema, err := New(reflect.TypeOf(config.Resources{}), resourcesDocs, nil) if err != nil { return nil, err } diff --git a/bundle/schema/schema.go b/bundle/schema/schema.go index 8b5c36d1..86c75689 100644 --- a/bundle/schema/schema.go +++ b/bundle/schema/schema.go @@ -4,6 +4,7 @@ import ( "container/list" "fmt" "reflect" + "slices" "strings" "github.com/databricks/cli/libs/jsonschema" @@ -42,9 +43,9 @@ const deprecatedTag = "deprecated" // // - []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) { +func New(golangType reflect.Type, docs *Docs, includeTags []string) (*jsonschema.Schema, error) { tracker := newTracker() - schema, err := safeToSchema(golangType, docs, "", tracker) + schema, err := safeToSchema(golangType, docs, "", tracker, includeTags) if err != nil { return nil, tracker.errWithTrace(err.Error(), "root") } @@ -91,7 +92,7 @@ func jsonSchemaType(golangType reflect.Type) (jsonschema.Type, error) { // 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) { +func safeToSchema(golangType reflect.Type, docs *Docs, traceId string, tracker *tracker, includeTags []string) (*jsonschema.Schema, error) { // 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 @@ -104,7 +105,7 @@ func safeToSchema(golangType reflect.Type, docs *Docs, traceId string, tracker * } tracker.push(golangType, traceId) - props, err := toSchema(golangType, docs, tracker) + props, err := toSchema(golangType, docs, tracker, includeTags) if err != nil { return nil, err } @@ -144,10 +145,10 @@ func getStructFields(golangType reflect.Type) []reflect.StructField { return fields } -func toSchema(golangType reflect.Type, docs *Docs, tracker *tracker) (*jsonschema.Schema, error) { +func toSchema(golangType reflect.Type, docs *Docs, tracker *tracker, includeTags []string) (*jsonschema.Schema, error) { // *Struct and Struct generate identical json schemas if golangType.Kind() == reflect.Pointer { - return safeToSchema(golangType.Elem(), docs, "", tracker) + return safeToSchema(golangType.Elem(), docs, "", tracker, includeTags) } if golangType.Kind() == reflect.Interface { return &jsonschema.Schema{}, nil @@ -174,7 +175,7 @@ func toSchema(golangType reflect.Type, docs *Docs, tracker *tracker) (*jsonschem if docs != nil { childDocs = docs.Items } - elemProps, err := safeToSchema(elemGolangType, childDocs, "", tracker) + elemProps, err := safeToSchema(elemGolangType, childDocs, "", tracker, includeTags) if err != nil { return nil, err } @@ -196,7 +197,7 @@ func toSchema(golangType reflect.Type, docs *Docs, tracker *tracker) (*jsonschem if docs != nil { childDocs = docs.AdditionalProperties } - jsonSchema.AdditionalProperties, err = safeToSchema(golangType.Elem(), childDocs, "", tracker) + jsonSchema.AdditionalProperties, err = safeToSchema(golangType.Elem(), childDocs, "", tracker, includeTags) if err != nil { return nil, err } @@ -211,7 +212,7 @@ func toSchema(golangType reflect.Type, docs *Docs, tracker *tracker) (*jsonschem 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 { + if (bundleTag == readonlyTag || bundleTag == internalTag || bundleTag == deprecatedTag) && !slices.Contains(includeTags, bundleTag) { continue } @@ -246,7 +247,7 @@ func toSchema(golangType reflect.Type, docs *Docs, tracker *tracker) (*jsonschem } // compute Schema.Properties for the child recursively - fieldProps, err := safeToSchema(child.Type, childDocs, childName, tracker) + fieldProps, err := safeToSchema(child.Type, childDocs, childName, tracker, includeTags) if err != nil { return nil, err } diff --git a/bundle/schema/schema_test.go b/bundle/schema/schema_test.go index d44a2082..1ded1247 100644 --- a/bundle/schema/schema_test.go +++ b/bundle/schema/schema_test.go @@ -17,7 +17,7 @@ func TestIntSchema(t *testing.T) { "type": "number" }` - schema, err := New(reflect.TypeOf(elemInt), nil) + schema, err := New(reflect.TypeOf(elemInt), nil, nil) require.NoError(t, err) jsonSchema, err := json.MarshalIndent(schema, " ", " ") @@ -36,7 +36,7 @@ func TestBooleanSchema(t *testing.T) { "type": "boolean" }` - schema, err := New(reflect.TypeOf(elem), nil) + schema, err := New(reflect.TypeOf(elem), nil, nil) require.NoError(t, err) jsonSchema, err := json.MarshalIndent(schema, " ", " ") @@ -55,7 +55,7 @@ func TestStringSchema(t *testing.T) { "type": "string" }` - schema, err := New(reflect.TypeOf(elem), nil) + schema, err := New(reflect.TypeOf(elem), nil, nil) require.NoError(t, err) jsonSchema, err := json.MarshalIndent(schema, " ", " ") @@ -90,7 +90,7 @@ func TestStructOfPrimitivesSchema(t *testing.T) { elem := Foo{} - schema, err := New(reflect.TypeOf(elem), nil) + schema, err := New(reflect.TypeOf(elem), nil, nil) assert.NoError(t, err) jsonSchema, err := json.MarshalIndent(schema, " ", " ") @@ -183,7 +183,7 @@ func TestStructOfStructsSchema(t *testing.T) { elem := MyStruct{} - schema, err := New(reflect.TypeOf(elem), nil) + schema, err := New(reflect.TypeOf(elem), nil, nil) assert.NoError(t, err) jsonSchema, err := json.MarshalIndent(schema, " ", " ") @@ -241,7 +241,7 @@ func TestStructOfMapsSchema(t *testing.T) { elem := Foo{} - schema, err := New(reflect.TypeOf(elem), nil) + schema, err := New(reflect.TypeOf(elem), nil, nil) assert.NoError(t, err) jsonSchema, err := json.MarshalIndent(schema, " ", " ") @@ -289,7 +289,7 @@ func TestStructOfSliceSchema(t *testing.T) { elem := Foo{} - schema, err := New(reflect.TypeOf(elem), nil) + schema, err := New(reflect.TypeOf(elem), nil, nil) assert.NoError(t, err) jsonSchema, err := json.MarshalIndent(schema, " ", " ") @@ -329,7 +329,7 @@ func TestStructOfSliceSchema(t *testing.T) { func TestMapOfPrimitivesSchema(t *testing.T) { var elem map[string]int - schema, err := New(reflect.TypeOf(elem), nil) + schema, err := New(reflect.TypeOf(elem), nil, nil) assert.NoError(t, err) jsonSchema, err := json.MarshalIndent(schema, " ", " ") @@ -355,7 +355,7 @@ func TestMapOfStructSchema(t *testing.T) { var elem map[string]Foo - schema, err := New(reflect.TypeOf(elem), nil) + schema, err := New(reflect.TypeOf(elem), nil, nil) assert.NoError(t, err) jsonSchema, err := json.MarshalIndent(schema, " ", " ") @@ -386,7 +386,7 @@ func TestMapOfStructSchema(t *testing.T) { func TestMapOfMapSchema(t *testing.T) { var elem map[string]map[string]int - schema, err := New(reflect.TypeOf(elem), nil) + schema, err := New(reflect.TypeOf(elem), nil, nil) assert.NoError(t, err) jsonSchema, err := json.MarshalIndent(schema, " ", " ") @@ -411,7 +411,7 @@ func TestMapOfMapSchema(t *testing.T) { func TestMapOfSliceSchema(t *testing.T) { var elem map[string][]string - schema, err := New(reflect.TypeOf(elem), nil) + schema, err := New(reflect.TypeOf(elem), nil, nil) assert.NoError(t, err) jsonSchema, err := json.MarshalIndent(schema, " ", " ") @@ -436,7 +436,7 @@ func TestMapOfSliceSchema(t *testing.T) { func TestSliceOfPrimitivesSchema(t *testing.T) { var elem []float32 - schema, err := New(reflect.TypeOf(elem), nil) + schema, err := New(reflect.TypeOf(elem), nil, nil) assert.NoError(t, err) jsonSchema, err := json.MarshalIndent(schema, " ", " ") @@ -458,7 +458,7 @@ func TestSliceOfPrimitivesSchema(t *testing.T) { func TestSliceOfSliceSchema(t *testing.T) { var elem [][]string - schema, err := New(reflect.TypeOf(elem), nil) + schema, err := New(reflect.TypeOf(elem), nil, nil) assert.NoError(t, err) jsonSchema, err := json.MarshalIndent(schema, " ", " ") @@ -483,7 +483,7 @@ func TestSliceOfSliceSchema(t *testing.T) { func TestSliceOfMapSchema(t *testing.T) { var elem []map[string]int - schema, err := New(reflect.TypeOf(elem), nil) + schema, err := New(reflect.TypeOf(elem), nil, nil) assert.NoError(t, err) jsonSchema, err := json.MarshalIndent(schema, " ", " ") @@ -512,7 +512,7 @@ func TestSliceOfStructSchema(t *testing.T) { var elem []Foo - schema, err := New(reflect.TypeOf(elem), nil) + schema, err := New(reflect.TypeOf(elem), nil, nil) assert.NoError(t, err) jsonSchema, err := json.MarshalIndent(schema, " ", " ") @@ -564,7 +564,7 @@ func TestEmbeddedStructSchema(t *testing.T) { elem := Story{} - schema, err := New(reflect.TypeOf(elem), nil) + schema, err := New(reflect.TypeOf(elem), nil, nil) assert.NoError(t, err) jsonSchema, err := json.MarshalIndent(schema, " ", " ") @@ -683,7 +683,7 @@ func TestNonAnnotatedFieldsAreSkipped(t *testing.T) { elem := MyStruct{} - schema, err := New(reflect.TypeOf(elem), nil) + schema, err := New(reflect.TypeOf(elem), nil, nil) require.NoError(t, err) jsonSchema, err := json.MarshalIndent(schema, " ", " ") @@ -717,7 +717,7 @@ func TestDashFieldsAreSkipped(t *testing.T) { elem := MyStruct{} - schema, err := New(reflect.TypeOf(elem), nil) + schema, err := New(reflect.TypeOf(elem), nil, nil) require.NoError(t, err) jsonSchema, err := json.MarshalIndent(schema, " ", " ") @@ -759,7 +759,7 @@ func TestPointerInStructSchema(t *testing.T) { elem := Foo{} - schema, err := New(reflect.TypeOf(elem), nil) + schema, err := New(reflect.TypeOf(elem), nil, nil) require.NoError(t, err) jsonSchema, err := json.MarshalIndent(schema, " ", " ") @@ -846,7 +846,7 @@ func TestGenericSchema(t *testing.T) { elem := Story{} - schema, err := New(reflect.TypeOf(elem), nil) + schema, err := New(reflect.TypeOf(elem), nil, nil) assert.NoError(t, err) jsonSchema, err := json.MarshalIndent(schema, " ", " ") @@ -1017,7 +1017,7 @@ func TestFieldsWithoutOmitEmptyAreRequired(t *testing.T) { elem := MyStruct{} - schema, err := New(reflect.TypeOf(elem), nil) + schema, err := New(reflect.TypeOf(elem), nil, nil) require.NoError(t, err) jsonSchema, err := json.MarshalIndent(schema, " ", " ") @@ -1091,7 +1091,7 @@ func TestDocIngestionForObject(t *testing.T) { elem := Root{} - schema, err := New(reflect.TypeOf(elem), docs) + schema, err := New(reflect.TypeOf(elem), docs, nil) require.NoError(t, err) jsonSchema, err := json.MarshalIndent(schema, " ", " ") @@ -1167,7 +1167,7 @@ func TestDocIngestionForSlice(t *testing.T) { elem := Root{} - schema, err := New(reflect.TypeOf(elem), docs) + schema, err := New(reflect.TypeOf(elem), docs, nil) require.NoError(t, err) jsonSchema, err := json.MarshalIndent(schema, " ", " ") @@ -1250,7 +1250,7 @@ func TestDocIngestionForMap(t *testing.T) { elem := Root{} - schema, err := New(reflect.TypeOf(elem), docs) + schema, err := New(reflect.TypeOf(elem), docs, nil) require.NoError(t, err) jsonSchema, err := json.MarshalIndent(schema, " ", " ") @@ -1312,7 +1312,7 @@ func TestDocIngestionForTopLevelPrimitive(t *testing.T) { elem := Root{} - schema, err := New(reflect.TypeOf(elem), docs) + schema, err := New(reflect.TypeOf(elem), docs, nil) require.NoError(t, err) jsonSchema, err := json.MarshalIndent(schema, " ", " ") @@ -1345,7 +1345,7 @@ func TestErrorOnMapWithoutStringKey(t *testing.T) { Bar map[int]string `json:"bar"` } elem := Foo{} - _, err := New(reflect.TypeOf(elem), nil) + _, err := New(reflect.TypeOf(elem), nil, nil) assert.ErrorContains(t, err, "only strings map keys are valid. key type: int") } @@ -1355,7 +1355,7 @@ func TestErrorIfStructRefersToItself(t *testing.T) { } elem := Foo{} - _, err := New(reflect.TypeOf(elem), nil) + _, err := New(reflect.TypeOf(elem), nil, nil) assert.ErrorContains(t, err, "cycle detected. traversal trace: root -> my_foo") } @@ -1372,7 +1372,7 @@ func TestErrorIfStructHasLoop(t *testing.T) { } elem := Apple{} - _, err := New(reflect.TypeOf(elem), nil) + _, err := New(reflect.TypeOf(elem), nil, nil) assert.ErrorContains(t, err, "cycle detected. traversal trace: root -> my_mango -> my_guava -> my_papaya -> my_apple") } @@ -1384,7 +1384,7 @@ func TestInterfaceGeneratesEmptySchema(t *testing.T) { elem := Foo{} - schema, err := New(reflect.TypeOf(elem), nil) + schema, err := New(reflect.TypeOf(elem), nil, nil) assert.NoError(t, err) jsonSchema, err := json.MarshalIndent(schema, " ", " ") @@ -1425,7 +1425,7 @@ func TestBundleReadOnlytag(t *testing.T) { elem := Foo{} - schema, err := New(reflect.TypeOf(elem), nil) + schema, err := New(reflect.TypeOf(elem), nil, nil) assert.NoError(t, err) jsonSchema, err := json.MarshalIndent(schema, " ", " ") @@ -1477,7 +1477,7 @@ func TestBundleInternalTag(t *testing.T) { elem := Foo{} - schema, err := New(reflect.TypeOf(elem), nil) + schema, err := New(reflect.TypeOf(elem), nil, nil) assert.NoError(t, err) jsonSchema, err := json.MarshalIndent(schema, " ", " ") @@ -1514,3 +1514,118 @@ func TestBundleInternalTag(t *testing.T) { t.Log("[DEBUG] expected: ", expected) assert.Equal(t, expected, string(jsonSchema)) } + +func TestBundleSingleIncludeTag(t *testing.T) { + type Pokemon struct { + Pikachu string `json:"pikachu" bundle:"internal"` + Raichu string `json:"raichu"` + } + + type Foo struct { + Pokemon *Pokemon `json:"pokemon"` + Apple int `json:"apple"` + Banana int `json:"banana,omitempty" bundle:"readonly"` + Mango string `json:"mango" bundle:"internal"` + } + + elem := Foo{} + + schema, err := New(reflect.TypeOf(elem), nil, []string{"readonly"}) + assert.NoError(t, err) + + jsonSchema, err := json.MarshalIndent(schema, " ", " ") + assert.NoError(t, err) + + expected := + `{ + "type": "object", + "properties": { + "apple": { + "type": "number" + }, + "banana": { + "type": "number" + }, + "pokemon": { + "type": "object", + "properties": { + "raichu": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "raichu" + ] + } + }, + "additionalProperties": false, + "required": [ + "pokemon", + "apple" + ] + }` + + t.Log("[DEBUG] actual: ", string(jsonSchema)) + t.Log("[DEBUG] expected: ", expected) + assert.Equal(t, expected, string(jsonSchema)) +} + +func TestBundleMultipleIncludeTag(t *testing.T) { + type Pokemon struct { + Pikachu string `json:"pikachu" bundle:"deprecated"` + Raichu string `json:"raichu"` + } + + type Foo struct { + Pokemon *Pokemon `json:"pokemon"` + Apple int `json:"apple"` + Banana int `json:"banana,omitempty" bundle:"readonly"` + Mango string `json:"mango,omitempty" bundle:"internal"` + } + + elem := Foo{} + + schema, err := New(reflect.TypeOf(elem), nil, []string{"readonly", "internal"}) + assert.NoError(t, err) + + jsonSchema, err := json.MarshalIndent(schema, " ", " ") + assert.NoError(t, err) + + expected := + `{ + "type": "object", + "properties": { + "apple": { + "type": "number" + }, + "banana": { + "type": "number" + }, + "mango": { + "type": "string" + }, + "pokemon": { + "type": "object", + "properties": { + "raichu": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "raichu" + ] + } + }, + "additionalProperties": false, + "required": [ + "pokemon", + "apple" + ] + }` + + t.Log("[DEBUG] actual: ", string(jsonSchema)) + t.Log("[DEBUG] expected: ", expected) + assert.Equal(t, expected, string(jsonSchema)) +} diff --git a/cmd/bundle/schema.go b/cmd/bundle/schema.go index f516695c..025e32a5 100644 --- a/cmd/bundle/schema.go +++ b/cmd/bundle/schema.go @@ -14,6 +14,8 @@ func newSchemaCommand() *cobra.Command { Use: "schema", Short: "Generate JSON Schema for bundle configuration", } + var includeTags []string + cmd.Flags().StringSliceVar(&includeTags, "include-tags", []string{}, "Also include fields with these tags.") cmd.RunE = func(cmd *cobra.Command, args []string) error { // Load embedded schema descriptions. @@ -23,7 +25,7 @@ func newSchemaCommand() *cobra.Command { } // Generate the JSON schema from the bundle configuration struct in Go. - schema, err := schema.New(reflect.TypeOf(config.Root{}), docs) + schema, err := schema.New(reflect.TypeOf(config.Root{}), docs, includeTags) if err != nil { return err }