From b0f40b10fefaed5f10f1aaa3462efe2669f80d01 Mon Sep 17 00:00:00 2001 From: monalisa Date: Tue, 15 Aug 2023 00:32:19 +0200 Subject: [PATCH 1/7] Add support for ordering of input prompts --- libs/template/config.go | 78 ++++++++++++++++++++++++++++++------ libs/template/config_test.go | 57 ++++++++++++++++++++++++++ libs/template/materialize.go | 4 +- 3 files changed, 125 insertions(+), 14 deletions(-) diff --git a/libs/template/config.go b/libs/template/config.go index ee5fcbef80..a751903bbb 100644 --- a/libs/template/config.go +++ b/libs/template/config.go @@ -8,31 +8,50 @@ import ( "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/jsonschema" + "golang.org/x/exp/slices" ) +type Metadata struct { + // A ordered subset of names of input parameters defined in the template schema. + // Properties in this array will be prompted for first. Any properties not in this + // array will be prompted for in lexicographical order otherwise. + PromptOrder []string `json:"prompt_order"` +} + type config struct { - ctx context.Context - values map[string]any - schema *jsonschema.Schema + ctx context.Context + values map[string]any + schema *jsonschema.Schema + metadata *Metadata } -func newConfig(ctx context.Context, schemaPath string) (*config, error) { - // Read config schema - schemaBytes, err := os.ReadFile(schemaPath) +func parseFromFile(path string, pointer interface{}) error { + b, err := os.ReadFile(path) if err != nil { - return nil, err + return err } + return json.Unmarshal(b, pointer) +} + +func newConfig(ctx context.Context, schemaPath string, metadataPath string) (*config, error) { + // Read config schema schema := &jsonschema.Schema{} - err = json.Unmarshal(schemaBytes, schema) - if err != nil { + if err := parseFromFile(schemaPath, schema); err != nil { + return nil, err + } + + // Read metadata + metadata := &Metadata{} + if err := parseFromFile(metadataPath, metadata); err != nil { return nil, err } // Return config return &config{ - ctx: ctx, - schema: schema, - values: make(map[string]any, 0), + ctx: ctx, + schema: schema, + values: make(map[string]any, 0), + metadata: metadata, }, nil } @@ -108,9 +127,41 @@ func (c *config) assignDefaultValues() error { return nil } +func (c *config) promptOrder() ([]string, error) { + // First add property order that is explicitly defined in metadata + promptOrder := make([]string, len(c.metadata.PromptOrder)) + for i, name := range c.metadata.PromptOrder { + if _, ok := c.schema.Properties[name]; !ok { + return nil, fmt.Errorf("property not defined in schema but is defined in prompt_order: %s", name) + } + if slices.Contains(promptOrder, name) { + return nil, fmt.Errorf("property defined more than once in prompt_order: %s", name) + } + promptOrder[i] = name + } + + // accumulate and sort leftover properties + leftover := []string{} + for name := range c.schema.Properties { + if slices.Contains(promptOrder, name) { + continue + } + leftover = append(leftover, name) + } + slices.Sort(leftover) + + promptOrder = append(promptOrder, leftover...) + return promptOrder, nil +} + // Prompts user for values for properties that do not have a value set yet func (c *config) promptForValues() error { - for name, property := range c.schema.Properties { + promptOrder, err := c.promptOrder() + if err != nil { + return err + } + + for _, name := range promptOrder { // Config already has a value assigned if _, ok := c.values[name]; ok { continue @@ -118,6 +169,7 @@ func (c *config) promptForValues() error { // Initialize Prompt dialog var err error + property := c.schema.Properties[name] prompt := cmdio.Prompt(c.ctx) prompt.Label = property.Description prompt.AllowEdit = true diff --git a/libs/template/config_test.go b/libs/template/config_test.go index 7b8341ec48..efb6f60386 100644 --- a/libs/template/config_test.go +++ b/libs/template/config_test.go @@ -161,3 +161,60 @@ func TestTemplateConfigValidateTypeForInvalidType(t *testing.T) { err = c.validate() assert.EqualError(t, err, `incorrect type for int_val. expected type integer, but value is "this-should-be-an-int"`) } + +func TestTemplatePromptOrder(t *testing.T) { + c := &config{ + schema: &jsonschema.Schema{ + Properties: map[string]*jsonschema.Schema{ + "a": {Type: jsonschema.IntegerType}, + "b": {Type: jsonschema.BooleanType}, + "c": {Type: jsonschema.NumberType}, + "d": {Type: jsonschema.IntegerType}, + "e": {Type: jsonschema.IntegerType}, + "f": {Type: jsonschema.IntegerType}, + }, + }, + metadata: &Metadata{ + PromptOrder: []string{"f", "b", "c"}, + }, + } + + order, err := c.promptOrder() + require.NoError(t, err) + assert.Equal(t, []string{"f", "b", "c", "a", "d", "e"}, order) +} + +func TestTemplatePromptOrderForUndefinedProperty(t *testing.T) { + c := &config{ + schema: &jsonschema.Schema{ + Properties: map[string]*jsonschema.Schema{ + "a": {Type: jsonschema.IntegerType}, + "b": {Type: jsonschema.BooleanType}, + }, + }, + metadata: &Metadata{ + PromptOrder: []string{"a", "c"}, + }, + } + + _, err := c.promptOrder() + assert.EqualError(t, err, "property not defined in schema but is defined in prompt_order: c") +} + +func TestTemplatePromptOrderForPropertyDefinedMultipleTimes(t *testing.T) { + c := &config{ + schema: &jsonschema.Schema{ + Properties: map[string]*jsonschema.Schema{ + "a": {Type: jsonschema.IntegerType}, + "b": {Type: jsonschema.BooleanType}, + "c": {Type: jsonschema.BooleanType}, + }, + }, + metadata: &Metadata{ + PromptOrder: []string{"a", "b", "a"}, + }, + } + + _, err := c.promptOrder() + assert.EqualError(t, err, "property defined more than once in prompt_order: a") +} diff --git a/libs/template/materialize.go b/libs/template/materialize.go index bbc9e8da30..2a1df055b6 100644 --- a/libs/template/materialize.go +++ b/libs/template/materialize.go @@ -8,6 +8,7 @@ import ( const libraryDirName = "library" const templateDirName = "template" const schemaFileName = "databricks_template_schema.json" +const metadataFileName = "databricks_template_metadata.json" // This function materializes the input templates as a project, using user defined // configurations. @@ -21,8 +22,9 @@ func Materialize(ctx context.Context, configFilePath, templateRoot, projectDir s templatePath := filepath.Join(templateRoot, templateDirName) libraryPath := filepath.Join(templateRoot, libraryDirName) schemaPath := filepath.Join(templateRoot, schemaFileName) + metadataPath := filepath.Join(templateRoot, metadataFileName) - config, err := newConfig(ctx, schemaPath) + config, err := newConfig(ctx, schemaPath, metadataPath) if err != nil { return err } From 843c5ee2c77dbf3c9dd5ba4d2fb0d385dedc6036 Mon Sep 17 00:00:00 2001 From: monalisa Date: Wed, 16 Aug 2023 16:40:14 +0200 Subject: [PATCH 2/7] grammer --- libs/template/config.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/libs/template/config.go b/libs/template/config.go index 149a2b7f29..0b764c9966 100644 --- a/libs/template/config.go +++ b/libs/template/config.go @@ -134,7 +134,7 @@ func (c *config) assignDefaultValues() error { } func (c *config) promptOrder() ([]string, error) { - // First add property order that is explicitly defined in metadata + // First add properties in the order that is explicitly defined in the metadata promptOrder := make([]string, len(c.metadata.PromptOrder)) for i, name := range c.metadata.PromptOrder { if _, ok := c.schema.Properties[name]; !ok { @@ -146,7 +146,7 @@ func (c *config) promptOrder() ([]string, error) { promptOrder[i] = name } - // accumulate and sort leftover properties + // accumulate and sort leftover the properties leftover := []string{} for name := range c.schema.Properties { if slices.Contains(promptOrder, name) { @@ -156,6 +156,7 @@ func (c *config) promptOrder() ([]string, error) { } slices.Sort(leftover) + // add leftover properties promptOrder = append(promptOrder, leftover...) return promptOrder, nil } From 434021b8be15ae9976bb363e3588f76605e245b2 Mon Sep 17 00:00:00 2001 From: monalisa Date: Wed, 16 Aug 2023 17:36:17 +0200 Subject: [PATCH 3/7] - --- libs/template/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/template/config.go b/libs/template/config.go index 0b764c9966..5f6800741d 100644 --- a/libs/template/config.go +++ b/libs/template/config.go @@ -8,7 +8,7 @@ import ( "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/jsonschema" - "golang.org/x/exp/slices" + "slices" ) type Metadata struct { From d8c87007dee926c329708c5f4d5ecd6e1f10c0ff Mon Sep 17 00:00:00 2001 From: Pieter Noordhuis Date: Tue, 5 Sep 2023 10:45:37 +0200 Subject: [PATCH 4/7] Add property ordering to the jsonschema package --- libs/jsonschema/extension.go | 14 ++++++++++++++ libs/jsonschema/schema.go | 37 ++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 libs/jsonschema/extension.go diff --git a/libs/jsonschema/extension.go b/libs/jsonschema/extension.go new file mode 100644 index 0000000000..5e877655c0 --- /dev/null +++ b/libs/jsonschema/extension.go @@ -0,0 +1,14 @@ +package jsonschema + +// Extension defines our custom JSON schema extensions. +// +// JSON schema supports custom extensions through vocabularies: +// https://json-schema.org/understanding-json-schema/reference/schema.html#vocabularies. +// We don't (yet?) define a meta-schema for the extensions below. +// It's not a big issue because the reach/scope of these extensions is limited. +type Extension struct { + // Order defines the order of a field with respect to other fields. + // If not defined, the field is ordered alphabetically after all fields + // that do have an order defined. + Order int `json:"order,omitempty"` +} diff --git a/libs/jsonschema/schema.go b/libs/jsonschema/schema.go index c0d1736c1d..7ebb6db998 100644 --- a/libs/jsonschema/schema.go +++ b/libs/jsonschema/schema.go @@ -4,8 +4,18 @@ import ( "encoding/json" "fmt" "os" + "slices" + "strings" ) +// Property defines a single property of a struct schema. +// This type is not used in the schema itself but rather to +// return the pair of a property name and its schema. +type Property struct { + Name string + Schema *Schema +} + // defines schema for a json object type Schema struct { // Type of the object @@ -40,6 +50,9 @@ type Schema struct { // Default value for the property / object Default any `json:"default,omitempty"` + + // Extension embeds our custom JSON schema extensions. + Extension } type Type string @@ -72,6 +85,30 @@ func (schema *Schema) validate() error { return nil } +// PropertiesOrdered returns the items of the `prope` +func (s *Schema) PropertiesOrdered() []Property { + order := make(map[string]int) + out := make([]Property, len(s.Properties)) + for key, property := range s.Properties { + order[key] = property.Order + out = append(out, Property{ + Name: key, + Schema: property, + }) + } + + // Sort the properties by order and then by name. + slices.SortFunc(out, func(a, b Property) int { + k := order[a.Name] - order[b.Name] + if k != 0 { + return k + } + return strings.Compare(a.Name, b.Name) + }) + + return out +} + func Load(path string) (*Schema, error) { b, err := os.ReadFile(path) if err != nil { From 8254263925802874ccb79be67df2e008797472d4 Mon Sep 17 00:00:00 2001 From: Pieter Noordhuis Date: Tue, 5 Sep 2023 10:45:50 +0200 Subject: [PATCH 5/7] Remove additional metadata file --- libs/template/config.go | 36 ++++++++++-------------------------- libs/template/materialize.go | 4 +--- 2 files changed, 11 insertions(+), 29 deletions(-) diff --git a/libs/template/config.go b/libs/template/config.go index 5f6800741d..9232e14f5d 100644 --- a/libs/template/config.go +++ b/libs/template/config.go @@ -6,49 +6,33 @@ import ( "fmt" "os" + "slices" + "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/jsonschema" - "slices" ) -type Metadata struct { - // A ordered subset of names of input parameters defined in the template schema. - // Properties in this array will be prompted for first. Any properties not in this - // array will be prompted for in lexicographical order otherwise. - PromptOrder []string `json:"prompt_order"` -} - type config struct { - ctx context.Context - values map[string]any - schema *jsonschema.Schema - metadata *Metadata + ctx context.Context + values map[string]any + schema *jsonschema.Schema } -func newConfig(ctx context.Context, schemaPath string, metadataPath string) (*config, error) { +func newConfig(ctx context.Context, schemaPath string) (*config, error) { // Read config schema schema, err := jsonschema.Load(schemaPath) if err != nil { return nil, err } - - // Read metadata - metadata := &Metadata{} - metadataBytes, err := os.ReadFile(metadataPath) - if err != nil { - return nil, err - } - err = json.Unmarshal(metadataBytes, metadata) - if err != nil { + if err := validateSchema(schema); err != nil { return nil, err } // Return config return &config{ - ctx: ctx, - schema: schema, - values: make(map[string]any, 0), - metadata: metadata, + ctx: ctx, + schema: schema, + values: make(map[string]any, 0), }, nil } diff --git a/libs/template/materialize.go b/libs/template/materialize.go index 2a1df055b6..bbc9e8da30 100644 --- a/libs/template/materialize.go +++ b/libs/template/materialize.go @@ -8,7 +8,6 @@ import ( const libraryDirName = "library" const templateDirName = "template" const schemaFileName = "databricks_template_schema.json" -const metadataFileName = "databricks_template_metadata.json" // This function materializes the input templates as a project, using user defined // configurations. @@ -22,9 +21,8 @@ func Materialize(ctx context.Context, configFilePath, templateRoot, projectDir s templatePath := filepath.Join(templateRoot, templateDirName) libraryPath := filepath.Join(templateRoot, libraryDirName) schemaPath := filepath.Join(templateRoot, schemaFileName) - metadataPath := filepath.Join(templateRoot, metadataFileName) - config, err := newConfig(ctx, schemaPath, metadataPath) + config, err := newConfig(ctx, schemaPath) if err != nil { return err } From 2c2b40b8084dc1bc2757187afe880bb34eb35dd8 Mon Sep 17 00:00:00 2001 From: Pieter Noordhuis Date: Tue, 5 Sep 2023 11:00:56 +0200 Subject: [PATCH 6/7] Move all ordering logic into libs/jsonschema --- libs/jsonschema/schema.go | 34 ---------------- libs/jsonschema/schema_order.go | 40 +++++++++++++++++++ libs/jsonschema/schema_order_test.go | 59 ++++++++++++++++++++++++++++ libs/template/config.go | 39 ++---------------- libs/template/config_test.go | 57 --------------------------- 5 files changed, 102 insertions(+), 127 deletions(-) create mode 100644 libs/jsonschema/schema_order.go create mode 100644 libs/jsonschema/schema_order_test.go diff --git a/libs/jsonschema/schema.go b/libs/jsonschema/schema.go index 7ebb6db998..87e9acd566 100644 --- a/libs/jsonschema/schema.go +++ b/libs/jsonschema/schema.go @@ -4,18 +4,8 @@ import ( "encoding/json" "fmt" "os" - "slices" - "strings" ) -// Property defines a single property of a struct schema. -// This type is not used in the schema itself but rather to -// return the pair of a property name and its schema. -type Property struct { - Name string - Schema *Schema -} - // defines schema for a json object type Schema struct { // Type of the object @@ -85,30 +75,6 @@ func (schema *Schema) validate() error { return nil } -// PropertiesOrdered returns the items of the `prope` -func (s *Schema) PropertiesOrdered() []Property { - order := make(map[string]int) - out := make([]Property, len(s.Properties)) - for key, property := range s.Properties { - order[key] = property.Order - out = append(out, Property{ - Name: key, - Schema: property, - }) - } - - // Sort the properties by order and then by name. - slices.SortFunc(out, func(a, b Property) int { - k := order[a.Name] - order[b.Name] - if k != 0 { - return k - } - return strings.Compare(a.Name, b.Name) - }) - - return out -} - func Load(path string) (*Schema, error) { b, err := os.ReadFile(path) if err != nil { diff --git a/libs/jsonschema/schema_order.go b/libs/jsonschema/schema_order.go new file mode 100644 index 0000000000..1f38abad65 --- /dev/null +++ b/libs/jsonschema/schema_order.go @@ -0,0 +1,40 @@ +package jsonschema + +import ( + "slices" + "strings" +) + +// Property defines a single property of a struct schema. +// This type is not used in the schema itself but rather to +// return the pair of a property name and its schema. +type Property struct { + Name string + Schema *Schema +} + +// OrderedProperties returns the properties of the schema ordered according +// to the value of their `order` extension. If this extension is not set, the +// properties are ordered alphabetically. +func (s *Schema) OrderedProperties() []Property { + order := make(map[string]int) + out := make([]Property, 0, len(s.Properties)) + for key, property := range s.Properties { + order[key] = property.Order + out = append(out, Property{ + Name: key, + Schema: property, + }) + } + + // Sort the properties by order and then by name. + slices.SortFunc(out, func(a, b Property) int { + k := order[a.Name] - order[b.Name] + if k != 0 { + return k + } + return strings.Compare(a.Name, b.Name) + }) + + return out +} diff --git a/libs/jsonschema/schema_order_test.go b/libs/jsonschema/schema_order_test.go new file mode 100644 index 0000000000..7914cbc6b7 --- /dev/null +++ b/libs/jsonschema/schema_order_test.go @@ -0,0 +1,59 @@ +package jsonschema + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestOrderedProperties(t *testing.T) { + s := Schema{ + Properties: map[string]*Schema{ + "bbb": { + Type: StringType, + // Implied order: 0 + }, + "ccc": { + Type: StringType, + // Implied order: 0 + }, + "ddd": { + Type: StringType, + // Implied order: 0 + }, + "zzz1": { + Type: StringType, + Extension: Extension{ + Order: -1, + }, + }, + "zzz2": { + Type: StringType, + Extension: Extension{ + Order: -2, + }, + }, + "aaa1": { + Type: StringType, + Extension: Extension{ + Order: 1, + }, + }, + "aaa2": { + Type: StringType, + Extension: Extension{ + Order: 2, + }, + }, + }, + } + + // Test that the properties are ordered by order and then by name. + properties := s.OrderedProperties() + names := make([]string, len(properties)) + for i, property := range properties { + names[i] = property.Name + } + + assert.Equal(t, []string{"zzz2", "zzz1", "bbb", "ccc", "ddd", "aaa1", "aaa2"}, names) +} diff --git a/libs/template/config.go b/libs/template/config.go index 9232e14f5d..8a1ed6c82a 100644 --- a/libs/template/config.go +++ b/libs/template/config.go @@ -6,8 +6,6 @@ import ( "fmt" "os" - "slices" - "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/jsonschema" ) @@ -117,47 +115,16 @@ func (c *config) assignDefaultValues() error { return nil } -func (c *config) promptOrder() ([]string, error) { - // First add properties in the order that is explicitly defined in the metadata - promptOrder := make([]string, len(c.metadata.PromptOrder)) - for i, name := range c.metadata.PromptOrder { - if _, ok := c.schema.Properties[name]; !ok { - return nil, fmt.Errorf("property not defined in schema but is defined in prompt_order: %s", name) - } - if slices.Contains(promptOrder, name) { - return nil, fmt.Errorf("property defined more than once in prompt_order: %s", name) - } - promptOrder[i] = name - } - - // accumulate and sort leftover the properties - leftover := []string{} - for name := range c.schema.Properties { - if slices.Contains(promptOrder, name) { - continue - } - leftover = append(leftover, name) - } - slices.Sort(leftover) - - // add leftover properties - promptOrder = append(promptOrder, leftover...) - return promptOrder, nil -} - // Prompts user for values for properties that do not have a value set yet func (c *config) promptForValues() error { - promptOrder, err := c.promptOrder() - if err != nil { - return err - } + for _, p := range c.schema.OrderedProperties() { + name := p.Name + property := p.Schema - for _, name := range promptOrder { // Config already has a value assigned if _, ok := c.values[name]; ok { continue } - property := c.schema.Properties[name] // Compute default value to display by converting it to a string var defaultVal string diff --git a/libs/template/config_test.go b/libs/template/config_test.go index c06c7ac3e9..335242467f 100644 --- a/libs/template/config_test.go +++ b/libs/template/config_test.go @@ -162,63 +162,6 @@ func TestTemplateConfigValidateTypeForInvalidType(t *testing.T) { assert.EqualError(t, err, `incorrect type for int_val. expected type integer, but value is "this-should-be-an-int"`) } -func TestTemplatePromptOrder(t *testing.T) { - c := &config{ - schema: &jsonschema.Schema{ - Properties: map[string]*jsonschema.Schema{ - "a": {Type: jsonschema.IntegerType}, - "b": {Type: jsonschema.BooleanType}, - "c": {Type: jsonschema.NumberType}, - "d": {Type: jsonschema.IntegerType}, - "e": {Type: jsonschema.IntegerType}, - "f": {Type: jsonschema.IntegerType}, - }, - }, - metadata: &Metadata{ - PromptOrder: []string{"f", "b", "c"}, - }, - } - - order, err := c.promptOrder() - require.NoError(t, err) - assert.Equal(t, []string{"f", "b", "c", "a", "d", "e"}, order) -} - -func TestTemplatePromptOrderForUndefinedProperty(t *testing.T) { - c := &config{ - schema: &jsonschema.Schema{ - Properties: map[string]*jsonschema.Schema{ - "a": {Type: jsonschema.IntegerType}, - "b": {Type: jsonschema.BooleanType}, - }, - }, - metadata: &Metadata{ - PromptOrder: []string{"a", "c"}, - }, - } - - _, err := c.promptOrder() - assert.EqualError(t, err, "property not defined in schema but is defined in prompt_order: c") -} - -func TestTemplatePromptOrderForPropertyDefinedMultipleTimes(t *testing.T) { - c := &config{ - schema: &jsonschema.Schema{ - Properties: map[string]*jsonschema.Schema{ - "a": {Type: jsonschema.IntegerType}, - "b": {Type: jsonschema.BooleanType}, - "c": {Type: jsonschema.BooleanType}, - }, - }, - metadata: &Metadata{ - PromptOrder: []string{"a", "b", "a"}, - }, - } - - _, err := c.promptOrder() - assert.EqualError(t, err, "property defined more than once in prompt_order: a") -} - func TestTemplateValidateSchema(t *testing.T) { var err error toSchema := func(s string) *jsonschema.Schema { From fa0e53d87db3a3dde2c40d4596faed98a577e204 Mon Sep 17 00:00:00 2001 From: Pieter Noordhuis Date: Tue, 5 Sep 2023 12:08:09 +0200 Subject: [PATCH 7/7] Update sorting rules --- libs/jsonschema/extension.go | 2 +- libs/jsonschema/schema_order.go | 25 +++++++++++++++++++++---- libs/jsonschema/schema_order_test.go | 17 +++++++++-------- 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/libs/jsonschema/extension.go b/libs/jsonschema/extension.go index 5e877655c0..bbbde695b5 100644 --- a/libs/jsonschema/extension.go +++ b/libs/jsonschema/extension.go @@ -10,5 +10,5 @@ type Extension struct { // Order defines the order of a field with respect to other fields. // If not defined, the field is ordered alphabetically after all fields // that do have an order defined. - Order int `json:"order,omitempty"` + Order *int `json:"order,omitempty"` } diff --git a/libs/jsonschema/schema_order.go b/libs/jsonschema/schema_order.go index 1f38abad65..3bc3e7d00b 100644 --- a/libs/jsonschema/schema_order.go +++ b/libs/jsonschema/schema_order.go @@ -17,7 +17,7 @@ type Property struct { // to the value of their `order` extension. If this extension is not set, the // properties are ordered alphabetically. func (s *Schema) OrderedProperties() []Property { - order := make(map[string]int) + order := make(map[string]*int) out := make([]Property, 0, len(s.Properties)) for key, property := range s.Properties { order[key] = property.Order @@ -29,10 +29,27 @@ func (s *Schema) OrderedProperties() []Property { // Sort the properties by order and then by name. slices.SortFunc(out, func(a, b Property) int { - k := order[a.Name] - order[b.Name] - if k != 0 { - return k + oa := order[a.Name] + ob := order[b.Name] + cmp := 0 + switch { + case oa != nil && ob != nil: + // Compare the order values if both are set. + cmp = *oa - *ob + case oa == nil && ob != nil: + // If only one is set, the one that is set comes first. + cmp = 1 + case oa != nil && ob == nil: + // If only one is set, the one that is set comes first. + cmp = -1 } + + // If we have a non-zero comparison, return it. + if cmp != 0 { + return cmp + } + + // If the order is the same, compare by name. return strings.Compare(a.Name, b.Name) }) diff --git a/libs/jsonschema/schema_order_test.go b/libs/jsonschema/schema_order_test.go index 7914cbc6b7..56d4d63559 100644 --- a/libs/jsonschema/schema_order_test.go +++ b/libs/jsonschema/schema_order_test.go @@ -7,42 +7,43 @@ import ( ) func TestOrderedProperties(t *testing.T) { + newInt := func(i int) *int { + return &i + } + s := Schema{ Properties: map[string]*Schema{ "bbb": { Type: StringType, - // Implied order: 0 }, "ccc": { Type: StringType, - // Implied order: 0 }, "ddd": { Type: StringType, - // Implied order: 0 }, "zzz1": { Type: StringType, Extension: Extension{ - Order: -1, + Order: newInt(-1), }, }, "zzz2": { Type: StringType, Extension: Extension{ - Order: -2, + Order: newInt(-2), }, }, "aaa1": { Type: StringType, Extension: Extension{ - Order: 1, + Order: newInt(1), }, }, "aaa2": { Type: StringType, Extension: Extension{ - Order: 2, + Order: newInt(2), }, }, }, @@ -55,5 +56,5 @@ func TestOrderedProperties(t *testing.T) { names[i] = property.Name } - assert.Equal(t, []string{"zzz2", "zzz1", "bbb", "ccc", "ddd", "aaa1", "aaa2"}, names) + assert.Equal(t, []string{"zzz2", "zzz1", "aaa1", "aaa2", "bbb", "ccc", "ddd"}, names) }