Skip to content

Generator redesign read meta for resource name to class struct (DCNE-345) #1350

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: generator_redesign
Choose a base branch
from
84 changes: 80 additions & 4 deletions gen/utils/data/class.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import (
"encoding/json"
"fmt"
"os"
"regexp"
"unicode"

"github.com/CiscoDevNet/terraform-provider-aci/v2/gen/utils"
"github.com/CiscoDevNet/terraform-provider-aci/v2/internal/provider"
"golang.org/x/text/cases"
"golang.org/x/text/language"
Expand All @@ -27,6 +29,8 @@ type Class struct {
Children []Child
// List of all possible parent classes.
ContainedBy []string
// Deprecated resources include a warning the resource and datasource schemas.
Deprecated bool
// The APIC versions in which the class is deprecated.
DeprecatedVersions []VersionRange
// Documentation specific information for the class.
Expand Down Expand Up @@ -178,20 +182,92 @@ func (c *Class) loadMetaFile() error {
func (c *Class) setClassData() error {
genLogger.Debug(fmt.Sprintf("Setting class data for class '%s'.", c.ClassName))

c.setResourceName()
// TODO: add function to set AllowDelete

// TODO: add functions to set the other class data
// TODO: add function to set Children

// TODO: add function to set ContainedBy

// TODO: add placeholder function for Deprecated

// TODO: add placeholder function for DeprecatedVersions

// TODO: add function to set Documentation

// TODO: add function to set IdentifiedBy

// TODO: add function to set IsMigration

// TODO: add function to set IsRelational

// TODO: add function to set IsSingleNested

// TODO: add function to set PlatformType

if properties, ok := c.MetaFileContent["properties"]; ok {
c.setProperties(properties.(map[string]interface{}))
}

// TODO: add function to set RequiredAsChild

err := c.setResourceName()
if err != nil {
return err
}

err = c.setResourceNameNested()
if err != nil {
return err
}

// TODO: add function to set RnFormat

// TODO: add function to set Versions

genLogger.Debug(fmt.Sprintf("Successfully set class data for class '%s'.", c.ClassName))
return nil
}

func (c *Class) setResourceName() {
genLogger.Trace(fmt.Sprintf("Implement setting resourceName for class '%s'.", c.ClassName))
func (c *Class) setResourceName() error {
genLogger.Debug(fmt.Sprintf("Setting resource name for class '%s'.", c.ClassName))

// TODO: add logic to override the resource name from a definition file.
if label, ok := c.MetaFileContent["label"]; ok && label != "" {
c.ResourceName = utils.Underscore(label.(string))
} else {
return fmt.Errorf("failed to set resource name for class '%s': label not found", c.ClassName)
}
Comment on lines +235 to +239
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also does not account for relational classes right where we need to add relation_ right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok will include logic for this


genLogger.Debug(fmt.Sprintf("Successfully set resource name '%s' for class '%s'.", c.ResourceName, c.ClassName))
return nil
}

func (c *Class) setResourceNameNested() error {
genLogger.Debug(fmt.Sprintf("Setting resource name when used as nested attribute for class '%s'.", c.ClassName))

// The assumption is made that when a resource name has no identifying properties, there will be only one configurable item.
// This means that the resource name will not be in plural form when exposed as a nested attribute in its parent.
if len(c.IdentifiedBy) == 0 {
c.ResourceNameNested = c.ResourceName
} else {
// ex. 'relation_to_consumed_contract' would be pluralized to 'relation_to_consumed_contracts'.
// ex. 'relation_from_bridge_domain_to_netflow_monitor_policy' would be pluralized to 'relation_from_bridge_domain_to_netflow_monitor_policies'.
pluralForm, err := utils.Plural(c.ResourceName)
if err != nil {
return err
}
c.ResourceNameNested = pluralForm
}

// For relational class nested attributes the name is changed to 'to_resource_name' when the resource name includes 'from_resource_name'.
// ex. 'relation_from_bridge_domain_to_netflow_monitor_policy' would translate to 'relation_to_netflow_monitor_policy'.
m := regexp.MustCompile("from_(.*)_to_")
if m.MatchString(c.ResourceNameNested) {
c.ResourceNameNested = m.ReplaceAllString(c.ResourceNameNested, "to_")
}

genLogger.Debug(fmt.Sprintf("Successfully set resource name when used as nested attribute '%s' for class '%s'.", c.ResourceNameNested, c.ClassName))
return nil
}

func (c *Class) setProperties(properties map[string]interface{}) {
Expand Down
54 changes: 51 additions & 3 deletions gen/utils/data/class_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,76 @@ const (
constTestClassNameSingleWordInShortName = "fvTenant"
constTestClassNameMultipleWordsInShortName = "fvRsIpslaMonPol"
constTestClassNameErrorInShortName = "error"
constTestMetaFileContentForLabel = "tenant"
)

func TestSplitClassNameToPackageNameAndShortNameSingle(t *testing.T) {
test.InitializeTest(t, 0)
test.InitializeTest(t)
packageName, shortName, err := splitClassNameToPackageNameAndShortName(constTestClassNameSingleWordInShortName)
assert.Equal(t, packageName, "fv", fmt.Sprintf("Expected package name to be 'fv', but got '%s'", packageName))
assert.Equal(t, shortName, "Tenant", fmt.Sprintf("Expected short name to be 'Tenant', but got '%s'", shortName))
assert.NoError(t, err, fmt.Sprintf("Expected no error, but got '%s'", err))
}

func TestSplitClassNameToPackageNameAndShortNameMultiple(t *testing.T) {
test.InitializeTest(t, 0)
test.InitializeTest(t)
packageName, shortName, err := splitClassNameToPackageNameAndShortName(constTestClassNameMultipleWordsInShortName)
assert.Equal(t, packageName, "fv", fmt.Sprintf("Expected package name to be 'fv', but got '%s'", packageName))
assert.Equal(t, shortName, "RsIpslaMonPol", fmt.Sprintf("Expected short name to be 'RsIpslaMonPol', but got '%s'", shortName))
assert.NoError(t, err, fmt.Sprintf("Expected no error, but got '%s'", err))
}

func TestSplitClassNameToPackageNameAndShortNameError(t *testing.T) {
test.InitializeTest(t, 0)
test.InitializeTest(t)
packageName, shortName, err := splitClassNameToPackageNameAndShortName(constTestClassNameErrorInShortName)
assert.Equal(t, packageName, "", fmt.Sprintf("Expected package name to be '', but got '%s'", packageName))
assert.Equal(t, shortName, "", fmt.Sprintf("Expected short name to be '', but got '%s'", shortName))
assert.Error(t, err, fmt.Sprintf("Expected error, but got '%s'", err))
}

func TestSetResourceNameFromLabel(t *testing.T) {
test.InitializeTest(t)
class := Class{ClassName: constTestClassNameSingleWordInShortName}
class.MetaFileContent = map[string]interface{}{"label": constTestMetaFileContentForLabel}
err := class.setResourceName()
assert.NoError(t, err, fmt.Sprintf("Expected no error, but got '%s'", err))
assert.Equal(t, class.ResourceName, constTestMetaFileContentForLabel, fmt.Sprintf("Expected resource name to be 'tenant', but got '%s'", class.ResourceName))
}

func TestSetResourceNameFromEmptyLabelError(t *testing.T) {
test.InitializeTest(t)
class := Class{ClassName: constTestClassNameSingleWordInShortName}
class.MetaFileContent = map[string]interface{}{"label": ""}
err := class.setResourceName()
assert.Error(t, err, fmt.Sprintf("Expected error, but got '%s'", err))
}

func TestSetResourceNameFromNoLabelError(t *testing.T) {
test.InitializeTest(t)
class := Class{ClassName: constTestClassNameSingleWordInShortName}
class.MetaFileContent = map[string]interface{}{"no_label": ""}
err := class.setResourceName()
assert.Error(t, err, fmt.Sprintf("Expected error, but got '%s'", err))
}

func TestSetResourceNameNested(t *testing.T) {
test.InitializeTest(t)
class := Class{ClassName: constTestClassNameSingleWordInShortName}

tests := []map[string]interface{}{
{"resource_name": "relation_from_bridge_domain_to_netflow_monitor_policy", "identifiers": []string{"tnNetflowMonitorPolName", "fltType"}, "expected": "relation_to_netflow_monitor_policies"},
{"resource_name": "annotation", "identifiers": []string{"key"}, "expected": "annotations"},
{"resource_name": "relation_to_consumed_contract", "identifiers": []string{"tnVzBrCPName"}, "expected": "relation_to_consumed_contracts"},
{"resource_name": "associated_site", "identifiers": []string{}, "expected": "associated_site"},
{"resource_name": "relation_from_netflow_exporter_to_vrf", "identifiers": []string{}, "expected": "relation_to_vrf"},
}

for _, test := range tests {
genLogger.Info(fmt.Sprintf("Executing: %s' with input '%s' and expected output '%s'", t.Name(), test["resource_name"], test["expected"]))
class.ResourceName = test["resource_name"].(string)
class.IdentifiedBy = test["identifiers"].([]string)
class.setResourceNameNested()
assert.Equal(t, test["expected"], class.ResourceNameNested, fmt.Sprintf("Expected '%s', but got '%s'", test["expected"], class.ResourceNameNested))
}

}
2 changes: 1 addition & 1 deletion gen/utils/data/data_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const (
)

func initializeDataStoreTest(t *testing.T) *DataStore {
test.InitializeTest(t, 1)
test.InitializeTest(t)
return &DataStore{}
}

Expand Down
32 changes: 31 additions & 1 deletion gen/utils/data/property.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,37 @@ func NewProperty(name string, details map[string]interface{}) *Property {
genLogger.Trace(fmt.Sprintf("Creating new property struct for property: %s.", name))
property := &Property{PropertyName: name}

// TODO: add functions to set the values of the property.
// TODO: add function to set AttributeName

// TODO: add function to set Computed

// TODO: add function to set CustomType

// TODO: add placeholder function for Deprecated

// TODO: add placeholder function for DeprecatedVersions

// TODO: add function to set Documentation

// TODO: add function to set MigrationValues

// TODO: add function to set Optional

// TODO: add function to set PointsToClass

// TODO: add function to set ReadOnly

// TODO: add function to set Required

// TODO: add function to set TestValues

// TODO: add function to set Validators

// TODO: add function to set ValidValues

// TODO: add function to set ValueType

// TODO: add function to set Versions

return property
}
2 changes: 1 addition & 1 deletion gen/utils/logger/logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const (
)

func initializeLogTest(t *testing.T) *Logger {
test.InitializeTest(t, 1)
test.InitializeTest(t)
genLogger := InitializeLogger()

if genLogger == nil {
Expand Down
16 changes: 3 additions & 13 deletions gen/utils/test/test.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,10 @@
package test

import (
"runtime"
"strings"
"fmt"
"testing"
)

func InitializeTest(t *testing.T, functionCounter int) {
println("Executing:", GetTestName(t, functionCounter+1))
}

func GetTestName(t *testing.T, functionCounter int) string {
counter, _, _, success := runtime.Caller(functionCounter + 1)
if !success {
t.Fatalf("Failed to get caller information: %v", success)
}
splittedName := strings.Split(runtime.FuncForPC(counter).Name(), ".")
return splittedName[len(splittedName)-1]
func InitializeTest(t *testing.T) {
println(fmt.Sprintf("Executing: %s", t.Name()))
}
24 changes: 24 additions & 0 deletions gen/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"

"github.com/CiscoDevNet/terraform-provider-aci/v2/gen/utils/logger"
Expand Down Expand Up @@ -33,3 +34,26 @@ func GetFileNamesFromDirectory(path string, removeExtension bool) []string {
genLogger.Debug(fmt.Sprintf("The directory '%s' contains the file names: %s.", path, names))
return names
}

// A simplified version of a function to create plural forms for words.
// This function is not comprehensive and is not intended to cover all pluralization rules in English.
// It is intended for basic use cases and does not handle irregular plurals like "mouse" -> "mice".
// If we need a more robust pluralization solution, we should consider using a external package or expanding the logic.
func Plural(notPlural string) (string, error) {
if strings.HasSuffix(notPlural, "y") {
return fmt.Sprintf("%s%s", strings.TrimSuffix(notPlural, "y"), "ies"), nil
} else if !strings.HasSuffix(notPlural, "s") {
return fmt.Sprintf("%ss", notPlural), nil
}
genLogger.Error(fmt.Sprintf("The word '%s' has no plural rule defined.", notPlural))
return "", fmt.Errorf("the word '%s' has no plural rule defined", notPlural)
}

// Reused from https://github.com/buxizhizhoum/inflection/blob/master/inflection.go#L8 to avoid importing the whole package
func Underscore(s string) string {
for _, reStr := range []string{`([A-Z]+)([A-Z][a-z])`, `([a-z\d])([A-Z])`} {
re := regexp.MustCompile(reStr)
s = re.ReplaceAllString(s, "${1}_${2}")
}
return strings.ReplaceAll(strings.ToLower(s), " ", "_")
}
45 changes: 43 additions & 2 deletions gen/utils/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const (
)

func TestGetFileNamesFromDirectoryWithExtension(t *testing.T) {
test.InitializeTest(t, 0)
test.InitializeTest(t)
filenames := GetFileNamesFromDirectory(constTestDirectoryForGetFileNamesFromDirectory, false)
assert.NotEmpty(t, filenames, "Expected to get file names from directory, but got empty list")
assert.Equal(t, len(filenames), 3, fmt.Sprintf("Expected to get 2 file names from directory, but got %d", len(filenames)))
Expand All @@ -30,7 +30,7 @@ func TestGetFileNamesFromDirectoryWithExtension(t *testing.T) {
}

func TestGetFileNamesFromDirectoryWithoutExtension(t *testing.T) {
test.InitializeTest(t, 0)
test.InitializeTest(t)
filenames := GetFileNamesFromDirectory(constTestDirectoryForGetFileNamesFromDirectory, true)
assert.NotEmpty(t, filenames, "Expected to get file names from directory, but got empty list")
assert.Equal(t, len(filenames), 3, fmt.Sprintf("Expected to get 2 file names from directory, but got %d", len(filenames)))
Expand All @@ -39,3 +39,44 @@ func TestGetFileNamesFromDirectoryWithoutExtension(t *testing.T) {
assert.Contains(t, filenames, constTestFile3, fmt.Sprintf("Expected to find file name '%s' in the list, but it was not found", constTestFile3))
assert.NotContains(t, filenames, constTestDir1, fmt.Sprintf("Expected to not find directory name '%s' in the list, but it was found", constTestDir1))
}

func TestUnderscore(t *testing.T) {
test.InitializeTest(t)

tests := []map[string]string{
{"input": "tenant", "expected": "tenant"},
{"input": "Tenant", "expected": "tenant"},
{"input": "Tenant1", "expected": "tenant1"},
{"input": "ApplicationEndpointGroup", "expected": "application_endpoint_group"},
{"input": "Application Endpoint Group", "expected": "application_endpoint_group"},
}

for _, test := range tests {
genLogger.Info(fmt.Sprintf("Executing: %s' with input '%s' and expected output '%s'", t.Name(), test["input"], test["expected"]))
result := Underscore(test["input"])
assert.Equal(t, test["expected"], result, fmt.Sprintf("Expected '%s', but got '%s'", test["expected"], result))
}

}

func TestPlural(t *testing.T) {
test.InitializeTest(t)

tests := []map[string]string{
{"input": "monitor_policy", "expected": "monitor_policies"},
{"input": "annotation", "expected": "annotations"},
}

for _, test := range tests {
genLogger.Info(fmt.Sprintf("Executing: %s' with input '%s' and expected output '%s'", t.Name(), test["input"], test["expected"]))
result, err := Plural(test["input"])
assert.NoError(t, err, fmt.Sprintf("Expected no error, but got '%s'", err))
assert.Equal(t, test["expected"], result, fmt.Sprintf("Expected '%s', but got '%s'", test["expected"], result))
}
Comment on lines +70 to +75
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be repeated in some fashion quite a bit. Should this be a function that accept a series of attribute including the tested function?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will have a look at creating functions for all the repeating tests

}

func TestPluralError(t *testing.T) {
test.InitializeTest(t)
_, err := Plural("contracts")
assert.Error(t, err, fmt.Sprintf("Expected error, but got '%s'", err))
}
Loading