Skip to content
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

chore: PoC - Define make command to call code generation logic #2706

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,6 @@ vendor/
*.winfile eol=crlf
/.vs
node_modules

#used for schema code generation but is not commited to avoid constant updates
tools/codegen/open-api-spec.yml
2 changes: 1 addition & 1 deletion GNUmakefile
Original file line number Diff line number Diff line change
Expand Up @@ -155,5 +155,5 @@ jira-release-version:
go run ./tools/jira-release-version/*.go

.PHONY: generate-schema
generate-schema:
generate-schema: # resource_name is optional, if not provided all configured resources will be generated
@go run ./tools/codegen/main.go $(resource_name)
32 changes: 21 additions & 11 deletions tools/codegen/codespec/api_to_provider_spec_mapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,30 +15,40 @@ import (
"github.com/mongodb/terraform-provider-mongodbatlas/tools/codegen/openapi"
)

func ToCodeSpecModel(atlasAdminAPISpecFilePath, configPath string, resourceName string) (*Model, error) {
func ToCodeSpecModel(atlasAdminAPISpecFilePath, configPath string, resourceName *string) (*Model, error) {
apiSpec, err := openapi.ParseAtlasAdminAPI(atlasAdminAPISpecFilePath)
if err != nil {
return nil, fmt.Errorf("unable to parse Atlas Admin API: %v", err)
}

config, err := config.ParseGenConfigYAML(configPath)
configModel, err := config.ParseGenConfigYAML(configPath)
if err != nil {
return nil, fmt.Errorf("unable to parse config file: %v", err)
}

// find resource operations, schemas, etc from OAS
oasResource, err := getAPISpecResource(apiSpec.Model, config.Resources[resourceName], resourceName)
if err != nil {
return nil, fmt.Errorf("unable to get APISpecResource schema: %v", err)
resourceConfigsToIterate := configModel.Resources
if resourceName != nil { // only generate a specific resource
resourceConfigsToIterate = map[string]config.Resource{
*resourceName: configModel.Resources[*resourceName],
}
}

// map OAS resource model to CodeSpecModel
codeSpecResource := apiSpecResourceToCodeSpecModel(oasResource, config.Resources[resourceName], resourceName)
var results []Resource
for name, resourceConfig := range resourceConfigsToIterate {
log.Printf("Generating resource: %s", name)
// find resource operations, schemas, etc from OAS
oasResource, err := getAPISpecResource(apiSpec.Model, resourceConfig, SnakeCaseString(name))
if err != nil {
return nil, fmt.Errorf("unable to get APISpecResource schema: %v", err)
}
// map OAS resource model to CodeSpecModel
results = append(results, *apiSpecResourceToCodeSpecModel(oasResource, resourceConfig, SnakeCaseString(name)))
}

return &Model{Resources: []Resource{*codeSpecResource}}, nil
return &Model{Resources: results}, nil
}

func apiSpecResourceToCodeSpecModel(oasResource APISpecResource, resourceConfig config.Resource, name string) *Resource {
func apiSpecResourceToCodeSpecModel(oasResource APISpecResource, resourceConfig config.Resource, name SnakeCaseString) *Resource {
createOp := oasResource.CreateOp
readOp := oasResource.ReadOp

Expand Down Expand Up @@ -126,7 +136,7 @@ func opResponseToAttributes(op *high.Operation) Attributes {
return responseAttributes
}

func getAPISpecResource(spec high.Document, resourceConfig config.Resource, name string) (APISpecResource, error) {
func getAPISpecResource(spec high.Document, resourceConfig config.Resource, name SnakeCaseString) (APISpecResource, error) {
var errResult error
var resourceDeprecationMsg *string

Expand Down
6 changes: 3 additions & 3 deletions tools/codegen/codespec/api_to_provider_spec_mapper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ func TestConvertToProviderSpec(t *testing.T) {

for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
result, err := codespec.ToCodeSpecModel(tc.inputOpenAPISpecPath, tc.inputConfigPath, tc.inputResourceName)
result, err := codespec.ToCodeSpecModel(tc.inputOpenAPISpecPath, tc.inputConfigPath, &tc.inputResourceName)
require.NoError(t, err)
assert.Equal(t, tc.expectedResult, result, "Expected result to match the specified structure")
})
Expand Down Expand Up @@ -269,7 +269,7 @@ func TestConvertToProviderSpec_nested(t *testing.T) {

for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
result, err := codespec.ToCodeSpecModel(tc.inputOpenAPISpecPath, tc.inputConfigPath, tc.inputResourceName)
result, err := codespec.ToCodeSpecModel(tc.inputOpenAPISpecPath, tc.inputConfigPath, &tc.inputResourceName)
require.NoError(t, err)
assert.Equal(t, tc.expectedResult, result, "Expected result to match the specified structure")
})
Expand Down Expand Up @@ -348,7 +348,7 @@ func TestConvertToProviderSpec_nested_schemaOverrides(t *testing.T) {

for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
result, err := codespec.ToCodeSpecModel(tc.inputOpenAPISpecPath, tc.inputConfigPath, tc.inputResourceName)
result, err := codespec.ToCodeSpecModel(tc.inputOpenAPISpecPath, tc.inputConfigPath, &tc.inputResourceName)
require.NoError(t, err)
assert.Equal(t, tc.expectedResult, result, "Expected result to match the specified structure")
})
Expand Down
2 changes: 1 addition & 1 deletion tools/codegen/codespec/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func applyAlias(attr *Attribute, attrPathName *string, schemaOptions config.Sche
parts[i] = newName

if i == len(parts)-1 {
attr.Name = AttributeName(newName)
attr.Name = SnakeCaseString(newName)
}
}
}
Expand Down
23 changes: 2 additions & 21 deletions tools/codegen/codespec/model.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package codespec

import "strings"

type ElemType int

const (
Expand All @@ -19,7 +17,7 @@ type Model struct {

type Resource struct {
Schema *Schema
Name string
Name SnakeCaseString
}

type Schema struct {
Expand Down Expand Up @@ -48,29 +46,12 @@ type Attribute struct {
SingleNested *SingleNestedAttribute

Description *string
Name AttributeName
Name SnakeCaseString
DeprecationMessage *string
Sensitive *bool
ComputedOptionalRequired ComputedOptionalRequired
}

type AttributeName string // stored in snake case

func (snake AttributeName) SnakeCase() string {
return string(snake)
}

func (snake AttributeName) PascalCase() string {
words := strings.Split(string(snake), "_")
var pascalCase string
for i := range words {
if words[i] != "" {
pascalCase += strings.ToUpper(string(words[i][0])) + strings.ToLower(words[i][1:])
}
}
return pascalCase
}

type BoolAttribute struct {
Default *bool
}
Expand Down
21 changes: 21 additions & 0 deletions tools/codegen/codespec/string_case.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package codespec

import (
"strings"

"github.com/huandu/xstrings"
)

type SnakeCaseString string

func (snake SnakeCaseString) SnakeCase() string {
return string(snake)
}

func (snake SnakeCaseString) PascalCase() string {
Copy link
Member

Choose a reason for hiding this comment

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

knit: this is used by our mock library, but I guess you already have everything you need: https://pkg.go.dev/github.com/huandu/xstrings#ToSnakeCase

via https://vektra.github.io/mockery/latest/configuration/#functions

Copy link
Member Author

Choose a reason for hiding this comment

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

nice, adjusted to use an xstring implementation directly

return xstrings.ToCamelCase(string(snake)) // in xstrings v1.15.0 we can switch to using ToPascalCase for same functionality
}

func (snake SnakeCaseString) LowerCaseNoUnderscore() string {
return strings.ReplaceAll(string(snake), "_", "")
}
6 changes: 3 additions & 3 deletions tools/codegen/codespec/terraform_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ var (
unsupportedCharacters = regexp.MustCompile(`[^a-zA-Z0-9_]+`)
)

func terraformAttrName(attrName string) AttributeName {
func terraformAttrName(attrName string) SnakeCaseString {
if attrName == "" {
return AttributeName(attrName)
return SnakeCaseString(attrName)
}

removedUnsupported := unsupportedCharacters.ReplaceAllString(attrName, "")
Expand All @@ -24,5 +24,5 @@ func terraformAttrName(attrName string) AttributeName {
return fmt.Sprintf("%c_%s", firstChar, strings.ToLower(restOfString))
})

return AttributeName(strings.ToLower(insertedUnderscores))
return SnakeCaseString(strings.ToLower(insertedUnderscores))
}
17 changes: 15 additions & 2 deletions tools/codegen/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,23 @@ resources:
],
definition: "stringvalidator.ConflictsWith(path.MatchRoot(\"name\"))"
}]

prefix_path:
computability:
optional: true
computed: true

timeouts: ["create", "read", "delete"]
timeouts: ["create", "read", "delete"]

search_deployment:
read:
path: /api/atlas/v2/groups/{groupId}/clusters/{clusterName}/search/deployment
method: GET
create:
path: /api/atlas/v2/groups/{groupId}/clusters/{clusterName}/search/deployment
method: POST
schema:
aliases:
group_id: project_id
ignores: ["links"]
timeouts: ["create", "read", "delete"]
29 changes: 22 additions & 7 deletions tools/codegen/main.go
Original file line number Diff line number Diff line change
@@ -1,34 +1,40 @@
package main

import (
"fmt"
"log"
"os"

"github.com/mongodb/terraform-provider-mongodbatlas/tools/codegen/codespec"
"github.com/mongodb/terraform-provider-mongodbatlas/tools/codegen/openapi"
"github.com/mongodb/terraform-provider-mongodbatlas/tools/codegen/schema"
)

const (
atlasAdminAPISpecURL = "https://raw.githubusercontent.com/mongodb/atlas-sdk-go/main/openapi/atlas-api-transformed.yaml"
configPath = "schema-generation/config.yml"
specFilePath = "open-api-spec.yml"
configPath = "tools/codegen/config.yml"
specFilePath = "tools/codegen/open-api-spec.yml"
)

func main() {
resourceName := getOsArg()
if resourceName == nil {
log.Fatal("No resource name provided")
}
log.Printf("Resource name: %s\n", *resourceName)

if err := openapi.DownloadOpenAPISpec(atlasAdminAPISpecURL, specFilePath); err != nil {
log.Fatalf("an error occurred when downloading Atlas Admin API spec: %v", err)
}

_, err := codespec.ToCodeSpecModel(specFilePath, configPath, *resourceName)
model, err := codespec.ToCodeSpecModel(specFilePath, configPath, resourceName)
if err != nil {
log.Fatalf("an error occurred while generating codespec.Model: %v", err)
}

for i := range model.Resources {
resourceModel := model.Resources[i]
schemaCode := schema.GenerateGoCode(resourceModel)
if err := writeToFile(fmt.Sprintf("internal/service/%s/resource_schema.go", resourceModel.Name.LowerCaseNoUnderscore()), schemaCode); err != nil {
log.Fatalf("an error occurred when writing content to file: %v", err)
}
}
}

func getOsArg() *string {
Expand All @@ -37,3 +43,12 @@ func getOsArg() *string {
}
return &os.Args[1]
}

func writeToFile(fileName, content string) error {
// will override content if file exists
err := os.WriteFile(fileName, []byte(content), 0o600)
if err != nil {
return err
}
return nil
}
6 changes: 3 additions & 3 deletions tools/codegen/schema/schema_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@ package schema
import (
"go/format"

genconfigmapper "github.com/mongodb/terraform-provider-mongodbatlas/tools/codegen/codespec"
"github.com/mongodb/terraform-provider-mongodbatlas/tools/codegen/codespec"
"github.com/mongodb/terraform-provider-mongodbatlas/tools/codegen/schema/codetemplate"
)

func GenerateGoCode(input genconfigmapper.Resource) string {
func GenerateGoCode(input codespec.Resource) string {
schemaAttrs := GenerateSchemaAttributes(input.Schema.Attributes)
models := GenerateTypedModels(input.Schema.Attributes)

tmplInputs := codetemplate.SchemaFileInputs{
PackageName: input.Name,
PackageName: input.Name.LowerCaseNoUnderscore(),
Imports: append(schemaAttrs.Imports, models.Imports...),
SchemaAttributes: schemaAttrs.Code,
Models: models.Code,
Expand Down
2 changes: 1 addition & 1 deletion tools/codegen/schema/testdata/nested-attributes.golden.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package test_name
package testname

import (
"context"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package test_name
package testname
Copy link
Member

Choose a reason for hiding this comment

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

it's idiomatic Go to call the package same as last folder, here it is testdata but testname

Copy link
Member Author

Choose a reason for hiding this comment

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

This actually a golden file to verify the output of generated code. In the test the resource name is defined as test_name so this package is now accurate.


import (
"context"
Expand Down