From e9f8e7a147af33226291ef95fa9e8ef3315c310e Mon Sep 17 00:00:00 2001 From: Vivek Singh Chauhan Date: Mon, 24 Jun 2024 10:43:51 -0700 Subject: [PATCH] APIGOV-26190 - Update to set property dependencies using schema builder (#795) - Changes to setup dependent properties for string based schema properties - Updates to generate property and schema dependencies - Updated OAuth external CRD to setup dependencies for private_key_jwt and tls_client_auth grant types --- pkg/agent/provisioning.go | 99 ++++---- pkg/apic/provisioning/propertybuilder.go | 141 +++++++++-- pkg/apic/provisioning/propertybuilder_test.go | 228 ++++++++++++++++-- pkg/apic/provisioning/schemabuilder.go | 26 +- pkg/apic/provisioning/schemabuilder_test.go | 34 +++ 5 files changed, 442 insertions(+), 86 deletions(-) diff --git a/pkg/agent/provisioning.go b/pkg/agent/provisioning.go index 9a025c68c..5508d9e9c 100644 --- a/pkg/agent/provisioning.go +++ b/pkg/agent/provisioning.go @@ -256,24 +256,10 @@ func WithCRDForIDP(p oauth.Provider, scopes []string) func(c *crdBuilderOptions) setIDPClientSecretSchemaProperty(c) setIDPTokenURLSchemaProperty(p, c) - setIDPScopesSchemaProperty(p, scopes, c) + setIDPScopesSchemaProperty(scopes, c) setIDPGrantTypesSchemaProperty(p, c) - tokenAuthMethods := setIDPTokenAuthMethodSchemaProperty(p, c) - setIDPRedirectURIsSchemaProperty(p, c) - - usePrivateKeyJWTAuth := idpUsesPrivateKeyJWTAuth(tokenAuthMethods) - useTLSClientAuth := idpUsesTLSClientAuth(tokenAuthMethods) - if usePrivateKeyJWTAuth || useTLSClientAuth { - setIDPJWKSURISchemaProperty(p, c) - } - - if usePrivateKeyJWTAuth { - setIDPJWKSSchemaProperty(p, c) - } - - if useTLSClientAuth { - setIDPTLSClientAuthSchemaProperty(p, c) - } + setIDPTokenAuthMethodSchemaProperty(p, c) + setIDPRedirectURIsSchemaProperty(c) } } @@ -297,7 +283,7 @@ func setIDPTokenURLSchemaProperty(p oauth.Provider, c *crdBuilderOptions) { SetDefaultValue(p.GetTokenEndpoint())) } -func setIDPScopesSchemaProperty(p oauth.Provider, scopes []string, c *crdBuilderOptions) { +func setIDPScopesSchemaProperty(scopes []string, c *crdBuilderOptions) { c.reqProps = append(c.reqProps, provisioning.NewSchemaPropertyBuilder(). SetName(provisioning.OauthScopes). @@ -348,17 +334,28 @@ func setIDPTokenAuthMethodSchemaProperty(p oauth.Provider, c *crdBuilderOptions) tokenAuthMethods, defaultTokenMethod := removeUnsupportedTypes( p.GetSupportedTokenAuthMethods(), supportedIDPTokenAuthMethods, config.ClientSecretBasic) - c.reqProps = append(c.reqProps, - provisioning.NewSchemaPropertyBuilder(). - SetName(provisioning.OauthTokenAuthMethod). - SetLabel("Token Auth Method"). - IsString(). - SetDefaultValue(defaultTokenMethod). - SetEnumValues(tokenAuthMethods)) + tmBuilder := provisioning.NewSchemaPropertyBuilder(). + SetName(provisioning.OauthTokenAuthMethod). + SetLabel("Token Auth Method"). + IsString(). + SetDefaultValue(defaultTokenMethod). + SetEnumValues(tokenAuthMethods) + + if idpUsesPrivateKeyJWTAuth(tokenAuthMethods) { + setIDPJWKSURISchemaProperty(config.PrivateKeyJWT, tmBuilder) + setIDPJWKSSchemaProperty(config.PrivateKeyJWT, tmBuilder) + } + + if idpUsesTLSClientAuth(tokenAuthMethods) { + setIDPJWKSURISchemaProperty(config.TLSClientAuth, tmBuilder) + setIDPTLSClientAuthSchemaProperty(tmBuilder) + } + + c.reqProps = append(c.reqProps, tmBuilder) return tokenAuthMethods } -func setIDPRedirectURIsSchemaProperty(p oauth.Provider, c *crdBuilderOptions) { +func setIDPRedirectURIsSchemaProperty(c *crdBuilderOptions) { c.reqProps = append(c.reqProps, provisioning.NewSchemaPropertyBuilder(). SetName(provisioning.OauthRedirectURIs). @@ -370,52 +367,66 @@ func setIDPRedirectURIsSchemaProperty(p oauth.Provider, c *crdBuilderOptions) { IsString())) } -func setIDPJWKSURISchemaProperty(p oauth.Provider, c *crdBuilderOptions) { - c.reqProps = append(c.reqProps, +func setIDPJWKSURISchemaProperty(depValue string, propBuilder provisioning.StringPropertyBuilder) { + propBuilder.AddDependency( + depValue, provisioning.NewSchemaPropertyBuilder(). SetName(provisioning.OauthJwksURI). SetLabel("JWKS URI"). IsString()) } -func setIDPJWKSSchemaProperty(p oauth.Provider, c *crdBuilderOptions) { - c.reqProps = append(c.reqProps, +func setIDPJWKSSchemaProperty(depValue string, propBuilder provisioning.StringPropertyBuilder) { + propBuilder.AddDependency( + depValue, provisioning.NewSchemaPropertyBuilder(). SetName(provisioning.OauthJwks). SetLabel("Public Key"). IsString()) - } -func setIDPTLSClientAuthSchemaProperty(p oauth.Provider, c *crdBuilderOptions) { - c.reqProps = append(c.reqProps, +func setIDPTLSClientAuthSchemaProperty(propBuilder provisioning.StringPropertyBuilder) { + propBuilder.AddDependency( + config.TLSClientAuth, provisioning.NewSchemaPropertyBuilder(). SetName(provisioning.OauthCertificate). SetLabel("Public Certificate"). IsString()) - c.reqProps = append(c.reqProps, - provisioning.NewSchemaPropertyBuilder(). - SetName(provisioning.OauthCertificateMetadata). - SetLabel("Certificate Metadata"). - IsString(). - SetDefaultValue(oauth.TLSClientAuthSubjectDN). - SetEnumValues(tlsAuthCertificateMetadata)) - c.reqProps = append(c.reqProps, + + certMetadataBuilder := provisioning.NewSchemaPropertyBuilder(). + SetName(provisioning.OauthCertificateMetadata). + SetLabel("Certificate Metadata"). + IsString(). + SetDefaultValue(oauth.TLSClientAuthSubjectDN). + SetEnumValues(tlsAuthCertificateMetadata) + + propBuilder.AddDependency( + config.TLSClientAuth, + certMetadataBuilder, + ) + certMetadataBuilder.AddDependency( + oauth.TLSClientAuthSanDNS, provisioning.NewSchemaPropertyBuilder(). SetName(provisioning.OauthTLSAuthSANDNS). SetLabel("Certificate Subject Alternative Name, DNS"). IsString()) - c.reqProps = append(c.reqProps, + + certMetadataBuilder.AddDependency( + oauth.TLSClientAuthSanEmail, provisioning.NewSchemaPropertyBuilder(). SetName(provisioning.OauthTLSAuthSANEmail). SetLabel("Certificate Subject Alternative Name, Email"). IsString()) - c.reqProps = append(c.reqProps, + + certMetadataBuilder.AddDependency( + oauth.TLSClientAuthSanIP, provisioning.NewSchemaPropertyBuilder(). SetName(provisioning.OauthTLSAuthSANIP). SetLabel("Certificate Subject Alternative Name, IP address"). IsString()) - c.reqProps = append(c.reqProps, + + certMetadataBuilder.AddDependency( + oauth.TLSClientAuthSanURI, provisioning.NewSchemaPropertyBuilder(). SetName(provisioning.OauthTLSAuthSANURI). SetLabel("Certificate Subject Alternative Name, URI"). diff --git a/pkg/apic/provisioning/propertybuilder.go b/pkg/apic/provisioning/propertybuilder.go index 314e75b33..0b689ec08 100644 --- a/pkg/apic/provisioning/propertybuilder.go +++ b/pkg/apic/provisioning/propertybuilder.go @@ -14,6 +14,11 @@ const ( DataTypeObject = "object" ) +// oneOfPropertyDefinitions - used for items of propertyDefinition +type oneOfPropertyDefinitions struct { + OneOf []*propertyDefinition `json:"oneOf,omitempty"` +} + // anyOfPropertyDefinitions - used for items of propertyDefinition type anyOfPropertyDefinitions struct { AnyOf []propertyDefinition `json:"anyOf,omitempty"` @@ -27,26 +32,27 @@ type PropertyDefinition interface { // propertyDefinition - type propertyDefinition struct { - Type string `json:"type"` - Title string `json:"title"` - Description string `json:"description,omitempty"` - Enum []string `json:"enum,omitempty"` - DefaultValue interface{} `json:"default,omitempty"` - ReadOnly bool `json:"readOnly,omitempty"` - Format string `json:"format,omitempty"` - Properties map[string]propertyDefinition `json:"properties,omitempty"` - RequiredProperties []string `json:"required,omitempty"` - Items *anyOfPropertyDefinitions `json:"items,omitempty"` // We use a pointer to avoid generating an empty struct if not set - MinItems *uint `json:"minItems,omitempty"` // We use a pointer to differentiate the "blank value" from a chosen 0 min value - MaxItems *uint `json:"maxItems,omitempty"` // We use a pointer to differentiate the "blank value" from a chosen 0 min value - Minimum *float64 `json:"minimum,omitempty"` // We use a pointer to differentiate the "blank value" from a chosen 0 min value - Maximum *float64 `json:"maximum,omitempty"` // We use a pointer to differentiate the "blank value" from a chosen 0 max value - IsEncrypted bool `json:"x-axway-encrypted,omitempty"` - Widget string `json:"x-axway-widget,omitempty"` - IsCopyable bool `json:"x-axway-copyable,omitempty"` - UniqueItems bool `json:"uniqueItems,omitempty"` - Name string `json:"-"` - Required bool `json:"-"` + Type string `json:"type,omitempty"` + Title string `json:"title,omitempty"` + Description string `json:"description,omitempty"` + Enum []string `json:"enum,omitempty"` + DefaultValue interface{} `json:"default,omitempty"` + ReadOnly bool `json:"readOnly,omitempty"` + Format string `json:"format,omitempty"` + Properties map[string]propertyDefinition `json:"properties,omitempty"` + RequiredProperties []string `json:"required,omitempty"` + Dependencies map[string]*oneOfPropertyDefinitions `json:"dependencies,omitempty"` + Items *anyOfPropertyDefinitions `json:"items,omitempty"` // We use a pointer to avoid generating an empty struct if not set + MinItems *uint `json:"minItems,omitempty"` // We use a pointer to differentiate the "blank value" from a chosen 0 min value + MaxItems *uint `json:"maxItems,omitempty"` // We use a pointer to differentiate the "blank value" from a chosen 0 min value + Minimum *float64 `json:"minimum,omitempty"` // We use a pointer to differentiate the "blank value" from a chosen 0 min value + Maximum *float64 `json:"maximum,omitempty"` // We use a pointer to differentiate the "blank value" from a chosen 0 max value + IsEncrypted bool `json:"x-axway-encrypted,omitempty"` + Widget string `json:"x-axway-widget,omitempty"` + IsCopyable bool `json:"x-axway-copyable,omitempty"` + UniqueItems bool `json:"uniqueItems,omitempty"` + Name string `json:"-"` + Required bool `json:"-"` } func (p *propertyDefinition) GetType() string { @@ -71,6 +77,8 @@ func (p *propertyDefinition) GetEnums() []string { type PropertyBuilder interface { // Build - builds the property, this is called automatically by the schema builder Build() (*propertyDefinition, error) + // BuildDependencies - builds the dependencies for the property, this is called automatically by the schema builder + BuildDependencies() (*oneOfPropertyDefinitions, error) } // TypePropertyBuilder - common methods related to type property builders @@ -118,6 +126,8 @@ type StringPropertyBuilder interface { SetDefaultValue(value string) StringPropertyBuilder // SetAsTextArea - Set value to be rendered as a textarea box within the UI SetAsTextArea() StringPropertyBuilder + // AddDependency - Add property dependencies + AddDependency(value string, property PropertyBuilder) StringPropertyBuilder PropertyBuilder } @@ -298,6 +308,7 @@ type stringSchemaProperty struct { enums []string widget string defaultValue string + dependencies map[string][]PropertyBuilder StringPropertyBuilder } @@ -370,9 +381,20 @@ func (p *stringSchemaProperty) IsCopyable() StringPropertyBuilder { return p } +func (p *stringSchemaProperty) AddDependency(value string, property PropertyBuilder) StringPropertyBuilder { + if p.dependencies == nil { + p.dependencies = map[string][]PropertyBuilder{} + } + _, ok := p.dependencies[value] + if !ok { + p.dependencies[value] = make([]PropertyBuilder, 0) + } + p.dependencies[value] = append(p.dependencies[value], property) + return p +} + // Build - create a string propertyDefinition for use in the subscription schema builder func (p *stringSchemaProperty) Build() (def *propertyDefinition, err error) { - def, err = p.schemaProperty.Build() if err != nil { return @@ -419,6 +441,68 @@ func (p *stringSchemaProperty) Build() (def *propertyDefinition, err error) { return def, err } +// BuildDependencies - builds the dependencies for the property, this is called automatically by the schema builder +func (p *stringSchemaProperty) BuildDependencies() (*oneOfPropertyDefinitions, error) { + if len(p.dependencies) > 0 { + deps := &oneOfPropertyDefinitions{ + OneOf: make([]*propertyDefinition, 0), + } + noDep := make(map[string]bool) + for _, enum := range p.enums { + props, ok := p.dependencies[enum] + if !ok { + noDep[enum] = true + continue + } + + depDef, err := p.buildDependenciesDef(enum, props) + if err != nil { + return nil, err + } + deps.OneOf = append(deps.OneOf, depDef) + } + if len(noDep) > 0 { + for enum := range noDep { + depDef := &propertyDefinition{ + Properties: make(map[string]propertyDefinition), + } + depDef.Properties[p.schemaProperty.name] = propertyDefinition{Enum: []string{enum}} + deps.OneOf = append(deps.OneOf, depDef) + } + } + + return deps, nil + } + return nil, nil +} + +func (p *stringSchemaProperty) buildDependenciesDef(val string, props []PropertyBuilder) (*propertyDefinition, error) { + depDef := &propertyDefinition{ + Properties: make(map[string]propertyDefinition), + Dependencies: make(map[string]*oneOfPropertyDefinitions), + } + // value match property + depDef.Properties[p.schemaProperty.name] = propertyDefinition{Enum: []string{val}} + + for _, prop := range props { + dp, err := prop.Build() + if err != nil { + return nil, err + } + + depDef.Properties[dp.Name] = *dp + dep, err := prop.BuildDependencies() + if err != nil { + return nil, err + } + + if dep != nil { + depDef.Dependencies[dp.Name] = dep + } + } + return depDef, nil +} + /** number property datatype builder */ @@ -475,6 +559,11 @@ func (p *numberSchemaProperty) Build() (def *propertyDefinition, err error) { return def, err } +// BuildDependencies - builds the dependencies for the property, this is called automatically by the schema builder +func (p *numberSchemaProperty) BuildDependencies() (*oneOfPropertyDefinitions, error) { + return nil, nil +} + /** integer property datatype builder */ @@ -575,6 +664,11 @@ func (p *arraySchemaProperty) Build() (def *propertyDefinition, err error) { return def, err } +// BuildDependencies - builds the dependencies for the property, this is called automatically by the schema builder +func (p *arraySchemaProperty) BuildDependencies() (*oneOfPropertyDefinitions, error) { + return nil, nil +} + /** object property datatype builder */ @@ -622,3 +716,8 @@ func (p *objectSchemaProperty) Build() (def *propertyDefinition, err error) { def.RequiredProperties = requiredProperties return def, err } + +// BuildDependencies - builds the dependencies for the property, this is called automatically by the schema builder +func (p *objectSchemaProperty) BuildDependencies() (*oneOfPropertyDefinitions, error) { + return nil, nil +} diff --git a/pkg/apic/provisioning/propertybuilder_test.go b/pkg/apic/provisioning/propertybuilder_test.go index 75e13855f..67e7f6f82 100644 --- a/pkg/apic/provisioning/propertybuilder_test.go +++ b/pkg/apic/provisioning/propertybuilder_test.go @@ -47,6 +47,42 @@ func TestSubscriptionSchemaPropertyBuilderSetters(t *testing.T) { //assert.NotNil(t, err) //assert.Nil(t, prop) + // No datatype in dependent property + builder := NewSchemaPropertyBuilder(). + SetName("name"). + IsString(). + SetEnumValues([]string{"a", "b"}). + AddDependency("a", NewSchemaPropertyBuilder(). + SetName("noDataType")) + + prop, err = builder.Build() + assert.Nil(t, err) + assert.NotNil(t, prop) + + dep, err := builder.BuildDependencies() + assert.NotNil(t, err) + assert.Nil(t, dep) + + // No datatype in child dependent property + builder = NewSchemaPropertyBuilder(). + SetName("name"). + IsString(). + SetEnumValues([]string{"a", "b"}). + AddDependency("a", NewSchemaPropertyBuilder(). + SetName("child"). + IsString(). + SetEnumValues([]string{"c", "d"}). + AddDependency("c", NewSchemaPropertyBuilder(). + SetName("child"))) + + prop, err = builder.Build() + assert.Nil(t, err) + assert.NotNil(t, prop) + + dep, err = builder.BuildDependencies() + assert.NotNil(t, err) + assert.Nil(t, dep) + // good path, no enums prop, err = NewSchemaPropertyBuilder(). SetName("name"). @@ -134,9 +170,10 @@ func getUintPointer(value uint) *uint { func Test_SubscriptionPropertyBuilder_Build_with_valid_values(t *testing.T) { tests := []struct { - name string - builder PropertyBuilder - expectedDef propertyDefinition + name string + builder PropertyBuilder + expectedDef propertyDefinition + expectedDependencies *oneOfPropertyDefinitions }{ {"Minimal String property", NewSchemaPropertyBuilder(). @@ -147,7 +184,119 @@ func Test_SubscriptionPropertyBuilder_Build_with_valid_values(t *testing.T) { Name: "TheName", Title: "The Label", Type: DataTypeString, - }}, + }, + nil}, + {"String property with dependencies", + NewSchemaPropertyBuilder(). + SetName("TheName"). + SetLabel("The Label"). + IsString(). + SetEnumValues([]string{"a", "b", "c"}). + AddDependency("a", NewSchemaPropertyBuilder(). + SetName("TheDependent"). + SetLabel("The Dependent").IsString()), + propertyDefinition{ + Name: "TheName", + Title: "The Label", + Type: DataTypeString, + Enum: []string{"a", "b", "c"}, + }, + &oneOfPropertyDefinitions{ + OneOf: []*propertyDefinition{ + { + Properties: map[string]propertyDefinition{ + "TheName": { + Enum: []string{"a"}, + }, + "TheDependent": { + Type: DataTypeString, + Name: "TheDependent", + Title: "The Dependent", + }, + }, + }, + { + Properties: map[string]propertyDefinition{ + "TheName": { + Enum: []string{"b"}, + }, + }, + }, + { + Properties: map[string]propertyDefinition{ + "TheName": { + Enum: []string{"c"}, + }, + }, + }, + }, + }, + }, + {"String child property with dependencies", + NewSchemaPropertyBuilder(). + SetName("TheName"). + SetLabel("The Label"). + IsString(). + SetEnumValues([]string{"a", "b"}). + AddDependency("a", NewSchemaPropertyBuilder(). + SetName("TheChild"). + SetLabel("The Child"). + IsString(). + SetEnumValues([]string{"a", "b"}). + AddDependency("a", NewSchemaPropertyBuilder(). + SetName("TheDependent"). + SetLabel("The Dependent"). + IsString()), + ), + propertyDefinition{ + Name: "TheName", + Title: "The Label", + Type: DataTypeString, + Enum: []string{"a", "b"}, + }, + &oneOfPropertyDefinitions{ + OneOf: []*propertyDefinition{ + { + Properties: map[string]propertyDefinition{ + "TheName": { + Enum: []string{"a"}, + }, + "TheChild": { + Type: DataTypeString, + Name: "TheChild", + Title: "The Child", + Enum: []string{"a", "b"}, + }, + }, + Dependencies: map[string]*oneOfPropertyDefinitions{ + "TheChild": { + OneOf: []*propertyDefinition{ + { + Properties: map[string]propertyDefinition{ + "TheChild": { + Enum: []string{"a"}, + }, + "TheDependent": { + Type: DataTypeString, + Name: "TheDependent", + Title: "The Dependent", + }, + }, + }, + }, + }, + }, + }, + { + Properties: map[string]propertyDefinition{ + "TheName": { + Enum: []string{"b"}, + }, + }, + }, + }, + }, + }, {"Full String property with unsorted enum and first value", NewSchemaPropertyBuilder(). SetName("TheName"). @@ -175,7 +324,8 @@ func Test_SubscriptionPropertyBuilder_Build_with_valid_values(t *testing.T) { Type: DataTypeString, Enum: []string{"firstValue", "c", "a", "b", "addedValue"}, DefaultValue: "a", - }}, + }, + nil}, {"Full String property with sorted enum and first value", NewSchemaPropertyBuilder(). SetName("TheName"). @@ -204,7 +354,8 @@ func Test_SubscriptionPropertyBuilder_Build_with_valid_values(t *testing.T) { Type: DataTypeString, Enum: []string{"firstValue", "a", "addedValue", "b", "c"}, DefaultValue: "a", - }}, + }, + nil}, {"Minimal Number property", NewSchemaPropertyBuilder(). SetName("TheName"). @@ -214,7 +365,8 @@ func Test_SubscriptionPropertyBuilder_Build_with_valid_values(t *testing.T) { Name: "TheName", Title: "The Label", Type: DataTypeNumber, - }}, + }, + nil}, {"Full Number property", NewSchemaPropertyBuilder(). SetName("TheName"). @@ -238,7 +390,8 @@ func Test_SubscriptionPropertyBuilder_Build_with_valid_values(t *testing.T) { Minimum: getFloat64Pointer(0.0), Maximum: getFloat64Pointer(100.5), DefaultValue: getFloat64Pointer(50.5), - }}, + }, + nil}, {"Minimal Integer property", NewSchemaPropertyBuilder(). SetName("TheName"). @@ -248,7 +401,8 @@ func Test_SubscriptionPropertyBuilder_Build_with_valid_values(t *testing.T) { Name: "TheName", Title: "The Label", Type: DataTypeInteger, - }}, + }, + nil}, {"Full Integer property", NewSchemaPropertyBuilder(). SetName("TheName"). @@ -272,7 +426,8 @@ func Test_SubscriptionPropertyBuilder_Build_with_valid_values(t *testing.T) { Minimum: getFloat64Pointer(0), Maximum: getFloat64Pointer(100), DefaultValue: getFloat64Pointer(50), - }}, + }, + nil}, {"Minimal Array property", NewSchemaPropertyBuilder(). SetName("TheName"). @@ -282,7 +437,8 @@ func Test_SubscriptionPropertyBuilder_Build_with_valid_values(t *testing.T) { Name: "TheName", Title: "The Label", Type: DataTypeArray, - }}, + }, + nil}, {"Full Array property", NewSchemaPropertyBuilder(). SetName("TheName"). @@ -318,7 +474,8 @@ func Test_SubscriptionPropertyBuilder_Build_with_valid_values(t *testing.T) { MinItems: getUintPointer(0), MaxItems: getUintPointer(1), UniqueItems: true, - }}, + }, + nil}, {"Minimal Object property", NewSchemaPropertyBuilder(). SetName("TheName"). @@ -326,7 +483,8 @@ func Test_SubscriptionPropertyBuilder_Build_with_valid_values(t *testing.T) { propertyDefinition{ Name: "TheName", Type: DataTypeObject, - }}, + }, + nil}, {"Full Object property", NewSchemaPropertyBuilder(). SetName("TheName"). @@ -360,17 +518,59 @@ func Test_SubscriptionPropertyBuilder_Build_with_valid_values(t *testing.T) { RequiredProperties: []string{ "PropertyName", }, - }}, + }, + nil}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { def, err := tt.builder.Build() assert.Nil(t, err) assert.Equal(t, tt.expectedDef, *def) + deps, err := tt.builder.BuildDependencies() + assert.Nil(t, err) + assertDependencies(t, def.Name, tt.expectedDependencies, deps) }) } } +func assertDependencies(t *testing.T, propName string, expectedDependencies, deps *oneOfPropertyDefinitions) { + if expectedDependencies == nil { + assert.Nil(t, deps) + return + } + assert.NotNil(t, deps) + for index, expectedDepDef := range expectedDependencies.OneOf { + depDef := deps.OneOf[index] + expectedEnumDepProps := make(map[string]propertyDefinition) + enumDepProps := make(map[string]propertyDefinition) + for name, expectedProperty := range expectedDepDef.Properties { + assert.Contains(t, depDef.Properties, name) + if expectedProperty.Name == propName { + expectedEnumDepProps[expectedProperty.Enum[0]] = expectedProperty + } + } + + for _, expectedProperty := range depDef.Properties { + if expectedProperty.Name == propName { + enumDepProps[expectedProperty.Enum[0]] = expectedProperty + } + } + assert.Equal(t, len(enumDepProps), len(expectedEnumDepProps)) + for enum, expectedProperty := range expectedEnumDepProps { + depProp := enumDepProps[enum] + assert.Equal(t, expectedProperty, depProp) + } + if expectedDepDef.Dependencies != nil { + assert.NotNil(t, depDef.Dependencies) + } + for name, expectedChildDependencies := range expectedDepDef.Dependencies { + childDeps, ok := depDef.Dependencies[name] + assert.True(t, ok) + assertDependencies(t, expectedDepDef.Name, expectedChildDependencies, childDeps) + } + } +} + func Test_SubscriptionPropertyBuilder_Build_with_error(t *testing.T) { tests := []struct { name string diff --git a/pkg/apic/provisioning/schemabuilder.go b/pkg/apic/provisioning/schemabuilder.go index 6b200f1fd..89ae0f0ce 100644 --- a/pkg/apic/provisioning/schemabuilder.go +++ b/pkg/apic/provisioning/schemabuilder.go @@ -39,25 +39,28 @@ type schemaBuilder struct { propertyOrder []string uniqueKeys []string properties map[string]propertyDefinition + dependencies map[string]*oneOfPropertyDefinitions schemaVersion string propertyOrderSet bool } // jsonSchema - the schema generated from the builder type jsonSchema struct { - SubscriptionName string `json:"-"` - SchemaType string `json:"type"` - SchemaVersion string `json:"$schema"` - SchemaDescription string `json:"description"` - Properties map[string]propertyDefinition `json:"properties"` - PropertyOrder []string `json:"x-axway-order,omitempty"` - Required []string `json:"required,omitempty"` + SubscriptionName string `json:"-"` + SchemaType string `json:"type"` + SchemaVersion string `json:"$schema"` + SchemaDescription string `json:"description"` + Properties map[string]propertyDefinition `json:"properties"` + Dependencies map[string]*oneOfPropertyDefinitions `json:"dependencies,omitempty"` + PropertyOrder []string `json:"x-axway-order,omitempty"` + Required []string `json:"required,omitempty"` } // NewSchemaBuilder - Creates a new subscription schema builder func NewSchemaBuilder() SchemaBuilder { return &schemaBuilder{ properties: make(map[string]propertyDefinition, 0), + dependencies: make(map[string]*oneOfPropertyDefinitions), uniqueKeys: make([]string, 0), propertyOrder: make([]string, 0), propertyOrderSet: false, @@ -95,6 +98,14 @@ func (s *schemaBuilder) AddProperty(property PropertyBuilder) SchemaBuilder { if !s.propertyOrderSet { s.propertyOrder = append(s.propertyOrder, prop.Name) } + + dep, err := property.BuildDependencies() + if err != nil { + s.err = err + } + if dep != nil { + s.dependencies[prop.Name] = dep + } } else { s.err = err } @@ -166,6 +177,7 @@ func (s *schemaBuilder) Build() (map[string]interface{}, error) { SchemaVersion: s.schemaVersion, SchemaDescription: s.description, Properties: s.properties, + Dependencies: s.dependencies, PropertyOrder: s.propertyOrder, Required: required, } diff --git a/pkg/apic/provisioning/schemabuilder_test.go b/pkg/apic/provisioning/schemabuilder_test.go index d5bf3aeba..cc221a05b 100644 --- a/pkg/apic/provisioning/schemabuilder_test.go +++ b/pkg/apic/provisioning/schemabuilder_test.go @@ -1,6 +1,7 @@ package provisioning import ( + "encoding/json" "testing" "github.com/stretchr/testify/assert" @@ -143,3 +144,36 @@ func TestSubscriptionSchemaBuilderSetters(t *testing.T) { assert.NotEqual(t, item.(string), "name5") } } + +func TestSchemaBuilderWithDependenciesProperties(t *testing.T) { + // set dependent property - dependent property definition error + _, err := NewSchemaBuilder(). + SetName("sch"). + AddProperty(NewSchemaPropertyBuilder(). + SetName("dep"). + IsString(). + SetEnumValues([]string{"a", "b", "c"}). + AddDependency("a", NewSchemaPropertyBuilder(). + SetName("dep"))). + Build() + + assert.NotNil(t, err) + + // set dependent property - good path + s, err := NewSchemaBuilder(). + SetName("sch"). + AddProperty(NewSchemaPropertyBuilder(). + SetName("prop"). + IsString(). + SetEnumValues([]string{"a", "b", "c"}). + AddDependency("a", NewSchemaPropertyBuilder(). + SetName("a-prop"). + IsString())). + Build() + assert.Nil(t, err) + schema := &jsonSchema{} + buf, _ := json.Marshal(s) + json.Unmarshal(buf, schema) + assert.NotNil(t, schema.Dependencies) + +}