From 0409b5045ecf9d770c1f76e2d994d287926d6e33 Mon Sep 17 00:00:00 2001 From: Zhenhua Li Date: Wed, 13 Mar 2024 13:25:05 -0700 Subject: [PATCH] Rewrite versions related functions (#10181) --- mmv1/api/product.go | 136 ++++++++++++----------- mmv1/api/product/version.go | 8 ++ mmv1/api/product_test.go | 129 ++++++++++++++++++++++ mmv1/api/resource.go | 38 +++++++ mmv1/api/resource_test.go | 145 ++++++++++++++++++++++++ mmv1/api/type.go | 43 +++++--- mmv1/api/type_test.go | 214 ++++++++++++++++++++++++++++++++++++ 7 files changed, 632 insertions(+), 81 deletions(-) create mode 100644 mmv1/api/product_test.go create mode 100644 mmv1/api/resource_test.go create mode 100644 mmv1/api/type_test.go diff --git a/mmv1/api/product.go b/mmv1/api/product.go index bfaed3a19869..5c5b718d8767 100644 --- a/mmv1/api/product.go +++ b/mmv1/api/product.go @@ -14,6 +14,9 @@ package api import ( + "log" + "strings" + "github.com/GoogleCloudPlatform/magic-modules/mmv1/api/product" "github.com/GoogleCloudPlatform/magic-modules/mmv1/google" "golang.org/x/exp/slices" @@ -50,7 +53,7 @@ type Product struct { // The API versions of this product - Versions []product.Version + Versions []*product.Version // The base URL for the service API endpoint // For example: `https://www.googleapis.com/compute/v1/` @@ -77,6 +80,9 @@ func (p *Product) Validate() { for _, o := range p.Objects { o.ProductMetadata = p } + + p.SetApiName() + p.SetDisplayName() } // def validate @@ -101,73 +107,82 @@ func (p *Product) Validate() { // check :versions, type: Array, item_type: Api::Product::Version, required: true // end -// // ==================== -// // Custom Getters -// // ==================== +// ==================== +// Custom Setters +// ==================== -// // The name of the product's API; "compute", "accesscontextmanager" -// def api_name -// name.downcase -// end +func (p *Product) SetApiName() { + // The name of the product's API; "compute", "accesscontextmanager" + p.ApiName = strings.ToLower(p.Name) +} // The product full name is the "display name" in string form intended for // users to read in documentation; "Google Compute Engine", "Cloud Bigtable" -func (p Product) GetDisplayName() string { +func (p *Product) SetDisplayName() { if p.DisplayName == "" { - return google.SpaceSeparated(p.Name) + p.DisplayName = google.SpaceSeparated(p.Name) } - - return p.DisplayName } -// // Most general version that exists for the product -// // If GA is present, use that, else beta, else alpha -// def lowest_version -// Version::ORDER.each do |ordered_version_name| -// @versions.each do |product_version| -// return product_version if ordered_version_name == product_version.name -// end -// end -// raise "Unable to find lowest version for product //{display_name}" -// end - -// def version_obj(name) -// @versions.each do |v| -// return v if v.name == name -// end +// ==================== +// Version-related methods +// ==================== + +// Most general version that exists for the product +// If GA is present, use that, else beta, else alpha +func (p Product) lowestVersion() *product.Version { + for _, orderedVersionName := range product.ORDER { + for _, productVersion := range p.Versions { + if orderedVersionName == productVersion.Name { + return productVersion + } + } + } -// raise "API version '//{name}' does not exist for product '//{@name}'" -// end + log.Fatalf("Unable to find lowest version for product %s", p.DisplayName) + return nil +} -// // Get the version of the object specified by the version given if present -// // Or else fall back to the closest version in the chain defined by Version::ORDER -// def version_obj_or_closest(name) -// return version_obj(name) if exists_at_version(name) +func (p Product) versionObj(name string) *product.Version { + for _, v := range p.Versions { + if v.Name == name { + return v + } + } -// // versions should fall back to the closest version to them that exists -// name ||= Version::ORDER[0] -// lower_versions = Version::ORDER[0..Version::ORDER.index(name)] + log.Fatalf("API version '%s' does not exist for product '%s'", name, p.Name) + return nil +} -// lower_versions.reverse_each do |version| -// return version_obj(version) if exists_at_version(version) -// end +// Get the version of the object specified by the version given if present +// Or else fall back to the closest version in the chain defined by product.ORDER +func (p Product) VersionObjOrClosest(name string) *product.Version { + if p.ExistsAtVersion(name) { + return p.versionObj(name) + } -// raise "Could not find object for version //{name} and product //{display_name}" -// end + // versions should fall back to the closest version to them that exists + if name == "" { + name = product.ORDER[0] + } -// def exists_at_version_or_lower(name) -// // Versions aren't normally going to be empty since products need a -// // base_url. This nil check exists for atypical products, like _bundle. -// return true if @versions.nil? + lowerVersions := make([]string, 0) + for _, v := range product.ORDER { + lowerVersions = append(lowerVersions, v) + if v == name { + break + } + } -// name ||= Version::ORDER[0] -// return false unless Version::ORDER.include?(name) + for i := len(lowerVersions) - 1; i >= 0; i-- { + if p.ExistsAtVersion(lowerVersions[i]) { + return p.versionObj(lowerVersions[i]) + } + } -// (0..Version::ORDER.index(name)).each do |i| -// return true if exists_at_version(Version::ORDER[i]) -// end -// false -// end + log.Fatalf("Could not find object for version %s and product %s", name, p.DisplayName) + return nil +} func (p *Product) ExistsAtVersionOrLower(name string) bool { if !slices.Contains(product.ORDER, name) { @@ -192,20 +207,9 @@ func (p *Product) ExistsAtVersion(name string) bool { return false } -// def exists_at_version(name) -// // Versions aren't normally going to be empty since products need a -// // base_url. This nil check exists for atypical products, like _bundle. -// return true if @versions.nil? - -// @versions.any? { |v| v.name == name } -// end - -// // Not a conventional setter, so ignore rubocop's warning -// // rubocop:disable Naming/AccessorMethodName -// def set_properties_based_on_version(version) -// @base_url = version.base_url -// end -// // rubocop:enable Naming/AccessorMethodName +func (p *Product) SetPropertiesBasedOnVersion(version *product.Version) { + p.BaseUrl = version.BaseUrl +} // // ==================== // // Debugging Methods diff --git a/mmv1/api/product/version.go b/mmv1/api/product/version.go index 3c2ef6a670c0..aa5bdd335c10 100644 --- a/mmv1/api/product/version.go +++ b/mmv1/api/product/version.go @@ -13,6 +13,10 @@ package product +import ( + "golang.org/x/exp/slices" +) + // require 'api/object' var ORDER = []string{"ga", "beta", "alpha", "private"} @@ -50,3 +54,7 @@ type Version struct { // def <=>(other) // ORDER.index(name) <=> ORDER.index(other.name) if other.is_a?(Version) // end + +func (v *Version) CompareTo(other *Version) int { + return slices.Index(ORDER, v.Name) - slices.Index(ORDER, other.Name) +} diff --git a/mmv1/api/product_test.go b/mmv1/api/product_test.go new file mode 100644 index 000000000000..682cd7b6a784 --- /dev/null +++ b/mmv1/api/product_test.go @@ -0,0 +1,129 @@ +package api + +import ( + "reflect" + "testing" + + "github.com/GoogleCloudPlatform/magic-modules/mmv1/api/product" +) + +func TestProductLowestVersion(t *testing.T) { + t.Parallel() + + cases := []struct { + description string + obj Product + expected string + }{ + { + description: "lowest version is ga", + obj: Product{ + Versions: []*product.Version{ + &product.Version{ + Name: "beta", + BaseUrl: "beta_url", + }, + &product.Version{ + Name: "ga", + BaseUrl: "ga_url", + }, + &product.Version{ + Name: "alpha", + BaseUrl: "alpha_url", + }, + }, + }, + expected: "ga", + }, + { + description: "lowest version is ga", + obj: Product{ + Versions: []*product.Version{ + &product.Version{ + Name: "beta", + BaseUrl: "beta_url", + }, + &product.Version{ + Name: "alpha", + BaseUrl: "alpha_url", + }, + }, + }, + expected: "beta", + }, + } + + for _, tc := range cases { + tc := tc + + t.Run(tc.description, func(t *testing.T) { + t.Parallel() + + versionObj := tc.obj.lowestVersion() + + if got, want := versionObj.Name, tc.expected; !reflect.DeepEqual(got, want) { + t.Errorf("expected %v to be %v", got, want) + } + }) + } +} + +func TestProductVersionObjOrClosest(t *testing.T) { + t.Parallel() + + cases := []struct { + description string + obj Product + input string + expected string + }{ + { + description: "closest version object to ga", + obj: Product{ + Versions: []*product.Version{ + &product.Version{ + Name: "beta", + BaseUrl: "beta_url", + }, + &product.Version{ + Name: "ga", + BaseUrl: "ga_url", + }, + }, + }, + input: "ga", + expected: "ga", + }, + { + description: "closest version object to alpha", + obj: Product{ + Versions: []*product.Version{ + &product.Version{ + Name: "beta", + BaseUrl: "beta_url", + }, + &product.Version{ + Name: "ga", + BaseUrl: "ga_url", + }, + }, + }, + input: "alpha", + expected: "beta", + }, + } + + for _, tc := range cases { + tc := tc + + t.Run(tc.description, func(t *testing.T) { + t.Parallel() + + versionObj := tc.obj.VersionObjOrClosest(tc.input) + + if got, want := versionObj.Name, tc.expected; !reflect.DeepEqual(got, want) { + t.Errorf("expected %v to be %v", got, want) + } + }) + } +} diff --git a/mmv1/api/resource.go b/mmv1/api/resource.go index 894850e765c1..069c935af0d8 100644 --- a/mmv1/api/resource.go +++ b/mmv1/api/resource.go @@ -13,6 +13,7 @@ package api import ( + "github.com/GoogleCloudPlatform/magic-modules/mmv1/api/product" "github.com/GoogleCloudPlatform/magic-modules/mmv1/api/resource" "github.com/GoogleCloudPlatform/magic-modules/mmv1/provider/terraform" ) @@ -292,3 +293,40 @@ func (r *Resource) setResourceMetada(properties []*Type) { property.ResourceMetadata = r } } + +// ==================== +// Version-related methods +// ==================== + +func (r Resource) MinVersionObj() *product.Version { + if r.MinVersion != "" { + return r.ProductMetadata.versionObj(r.MinVersion) + } else { + return r.ProductMetadata.lowestVersion() + } +} + +func (r Resource) NotInVersion(version *product.Version) bool { + return version.CompareTo(r.MinVersionObj()) < 0 +} + +// Recurses through all nested properties and parameters and changes their +// 'exclude' instance variable if the property is at a version below the +// one that is passed in. +func (r *Resource) ExcludeIfNotInVersion(version *product.Version) { + if !r.Exclude { + r.Exclude = r.NotInVersion(version) + } + + if r.Properties != nil { + for _, p := range r.Properties { + p.ExcludeIfNotInVersion(version) + } + } + + if r.Parameters != nil { + for _, p := range r.Parameters { + p.ExcludeIfNotInVersion(version) + } + } +} diff --git a/mmv1/api/resource_test.go b/mmv1/api/resource_test.go new file mode 100644 index 000000000000..a9cb0c3c4a65 --- /dev/null +++ b/mmv1/api/resource_test.go @@ -0,0 +1,145 @@ +package api + +import ( + "reflect" + "testing" + + "github.com/GoogleCloudPlatform/magic-modules/mmv1/api/product" +) + +func TestResourceMinVersionObj(t *testing.T) { + t.Parallel() + p := Product{ + NamedObject: NamedObject{ + Name: "test", + }, + Versions: []*product.Version{ + &product.Version{ + Name: "beta", + BaseUrl: "beta_url", + }, + &product.Version{ + Name: "ga", + BaseUrl: "ga_url", + }, + &product.Version{ + Name: "alpha", + BaseUrl: "alpha_url", + }, + }, + } + + cases := []struct { + description string + obj Resource + expected string + }{ + { + description: "resource minVersion is empty", + obj: Resource{ + NamedObject: NamedObject{ + Name: "test", + }, + MinVersion: "", + ProductMetadata: &p, + }, + expected: "ga", + }, + { + description: "resource minVersion is not empty", + obj: Resource{ + NamedObject: NamedObject{ + Name: "test", + }, + MinVersion: "beta", + ProductMetadata: &p, + }, + expected: "beta", + }, + } + + for _, tc := range cases { + tc := tc + + t.Run(tc.description, func(t *testing.T) { + t.Parallel() + + versionObj := tc.obj.MinVersionObj() + + if got, want := versionObj.Name, tc.expected; !reflect.DeepEqual(got, want) { + t.Errorf("expected %v to be %v", got, want) + } + }) + } +} + +func TestResourceNotInVersion(t *testing.T) { + t.Parallel() + p := Product{ + NamedObject: NamedObject{ + Name: "test", + }, + Versions: []*product.Version{ + &product.Version{ + Name: "beta", + BaseUrl: "beta_url", + }, + &product.Version{ + Name: "ga", + BaseUrl: "ga_url", + }, + &product.Version{ + Name: "alpha", + BaseUrl: "alpha_url", + }, + }, + } + + cases := []struct { + description string + obj Resource + input *product.Version + expected bool + }{ + { + description: "ga is in version if MinVersion is empty", + obj: Resource{ + NamedObject: NamedObject{ + Name: "test", + }, + MinVersion: "", + ProductMetadata: &p, + }, + input: &product.Version{ + Name: "ga", + }, + expected: false, + }, + { + description: "ga is not in version if MinVersion is beta", + obj: Resource{ + NamedObject: NamedObject{ + Name: "test", + }, + MinVersion: "beta", + ProductMetadata: &p, + }, + input: &product.Version{ + Name: "ga", + }, + expected: true, + }, + } + + for _, tc := range cases { + tc := tc + + t.Run(tc.description, func(t *testing.T) { + t.Parallel() + + if got, want := tc.obj.NotInVersion(tc.input), tc.expected; !reflect.DeepEqual(got, want) { + t.Errorf("expected %v to be %v", got, want) + } + }) + } +} diff --git a/mmv1/api/type.go b/mmv1/api/type.go index 0fe6f165e6dd..f80777231810 100644 --- a/mmv1/api/type.go +++ b/mmv1/api/type.go @@ -13,6 +13,10 @@ package api +import ( + "github.com/GoogleCloudPlatform/magic-modules/mmv1/api/product" +) + // require 'api/object' // require 'google/string_utils' // require 'provider/terraform/validation' @@ -441,24 +445,33 @@ const MAX_NAME = 20 // // @__parent // } -// func (t *Type) min_version() { -// // if @min_version.nil? -// // @__resource.min_version -// // else -// // @__resource.__product.version_obj(@min_version) -// // end -// } +func (t *Type) MinVersionObj() *product.Version { + if t.MinVersion != "" { + return t.ResourceMetadata.ProductMetadata.versionObj(t.MinVersion) + } else { + return t.ResourceMetadata.MinVersionObj() + } +} -// func (t *Type) exact_version() { -// // return nil if @exact_version.nil? || @exact_version.empty? +func (t *Type) exactVersionObj() *product.Version { + if t.ExactVersion == "" { + return nil + } -// // @__resource.__product.version_obj(@exact_version) -// } + return t.ResourceMetadata.ProductMetadata.versionObj(t.ExactVersion) +} -// func (t *Type) exclude_if_not_in_version!(version) { -// // @exclude ||= exact_version != version unless exact_version.nil? -// // @exclude ||= version < min_version -// } +func (t *Type) ExcludeIfNotInVersion(version *product.Version) { + if !t.Exclude { + if versionObj := t.exactVersionObj(); versionObj != nil { + t.Exclude = versionObj.CompareTo(version) != 0 + } + + if !t.Exclude { + t.Exclude = version.CompareTo(t.MinVersionObj()) < 0 + } + } +} // // Overriding is_a? to enable class overrides. // // Ruby does not let you natively change types, so this is the next best diff --git a/mmv1/api/type_test.go b/mmv1/api/type_test.go new file mode 100644 index 000000000000..86c3e2e57b1e --- /dev/null +++ b/mmv1/api/type_test.go @@ -0,0 +1,214 @@ +package api + +import ( + "reflect" + "testing" + + "github.com/GoogleCloudPlatform/magic-modules/mmv1/api/product" +) + +func TestTypeMinVersionObj(t *testing.T) { + t.Parallel() + + p := Product{ + NamedObject: NamedObject{ + Name: "test", + }, + Versions: []*product.Version{ + &product.Version{ + Name: "beta", + BaseUrl: "beta_url", + }, + &product.Version{ + Name: "ga", + BaseUrl: "ga_url", + }, + &product.Version{ + Name: "alpha", + BaseUrl: "alpha_url", + }, + }, + } + + cases := []struct { + description string + obj Type + expected string + }{ + { + description: "type minVersion is empty and resource minVersion is empty", + obj: Type{ + NamedObject: NamedObject{ + Name: "test", + }, + MinVersion: "", + ResourceMetadata: &Resource{ + NamedObject: NamedObject{ + Name: "test", + }, + MinVersion: "", + ProductMetadata: &p, + }, + }, + expected: "ga", + }, + { + description: "type minVersion is empty and resource minVersion is beta", + obj: Type{ + NamedObject: NamedObject{ + Name: "test", + }, + MinVersion: "", + ResourceMetadata: &Resource{ + NamedObject: NamedObject{ + Name: "test", + }, + MinVersion: "beta", + ProductMetadata: &p, + }, + }, + expected: "beta", + }, + { + description: "type minVersion is not empty", + obj: Type{ + NamedObject: NamedObject{ + Name: "test", + }, + MinVersion: "beta", + ResourceMetadata: &Resource{ + NamedObject: NamedObject{ + Name: "test", + }, + MinVersion: "", + ProductMetadata: &p, + }, + }, + expected: "beta", + }, + } + + for _, tc := range cases { + tc := tc + + t.Run(tc.description, func(t *testing.T) { + t.Parallel() + + versionObj := tc.obj.MinVersionObj() + + if got, want := versionObj.Name, tc.expected; !reflect.DeepEqual(got, want) { + t.Errorf("expected %v to be %v", got, want) + } + }) + } +} + +func TestTypeExcludeIfNotInVersion(t *testing.T) { + t.Parallel() + + p := Product{ + NamedObject: NamedObject{ + Name: "test", + }, + Versions: []*product.Version{ + &product.Version{ + Name: "beta", + BaseUrl: "beta_url", + }, + &product.Version{ + Name: "ga", + BaseUrl: "ga_url", + }, + &product.Version{ + Name: "alpha", + BaseUrl: "alpha_url", + }, + }, + } + + cases := []struct { + description string + obj Type + input *product.Version + expected bool + }{ + { + description: "type has Exclude true", + obj: Type{ + NamedObject: NamedObject{ + Name: "test", + }, + Exclude: true, + MinVersion: "", + ResourceMetadata: &Resource{ + NamedObject: NamedObject{ + Name: "test", + }, + MinVersion: "", + ProductMetadata: &p, + }, + }, + input: &product.Version{ + Name: "ga", + }, + expected: true, + }, + { + description: "type has Exclude false and not empty ExactVersion", + obj: Type{ + NamedObject: NamedObject{ + Name: "test", + }, + MinVersion: "", + Exclude: false, + ExactVersion: "beta", + ResourceMetadata: &Resource{ + NamedObject: NamedObject{ + Name: "test", + }, + MinVersion: "beta", + ProductMetadata: &p, + }, + }, + input: &product.Version{ + Name: "ga", + }, + expected: true, + }, + { + description: "type has Exclude false and empty ExactVersion", + obj: Type{ + NamedObject: NamedObject{ + Name: "test", + }, + MinVersion: "beta", + Exclude: false, + ExactVersion: "", + ResourceMetadata: &Resource{ + NamedObject: NamedObject{ + Name: "test", + }, + MinVersion: "", + ProductMetadata: &p, + }, + }, + input: &product.Version{ + Name: "ga", + }, + expected: true, + }, + } + + for _, tc := range cases { + tc := tc + + t.Run(tc.description, func(t *testing.T) { + t.Parallel() + + tc.obj.ExcludeIfNotInVersion(tc.input) + if got, want := tc.obj.Exclude, tc.expected; !reflect.DeepEqual(got, want) { + t.Errorf("expected %v to be %v", got, want) + } + }) + } +}