Skip to content

Commit

Permalink
feat: Initialize SDK generator (#2033)
Browse files Browse the repository at this point in the history
* Start generator PoC

* Generate nested structs

* Ensure tags order

* Generate basic impl

* Generate empty unit tests

* Generate empty validations

* Handle basic validations on the top level

* Extract validation template (WIP)

* Invoke inner validations (WIP)

* Add at least one of validation

* Add comment which test should be generated

* Extract print to std out method

* Extract running all templates into method

* Extract methods

* Write generated to files

* Extract example package

* Add generate directive

* Add package

* Add TODO comments for each validation

* Fix filename

* Generate DTOs

* Use DTOs for interface and for impl

* Generate builders

* Add required to field

* Add SDK definitions to be closer to compile

* Add packages temporarily

* Add toOpts mapping placeholders

* Fix identifier and random

* Try to map dto -> options (WIP)

* Map non-struct fields

* Map struct fields (WIP)

* Map struct fields (without path still)

* Add integration tests placeholder

* Add identifier definitions

* Extract OptsField inside Operation

* Fix validations

* Extract IsRoot()

* Handle path for validations and mappings

* Simplify definition

* Generate nested DTOs

* Add nested validations

* Add nested validations for unit tests

* Extract template executors

* Change mapping entries

* Add README

* Add generated files

* Document generation model
  • Loading branch information
sfc-gh-asawicki committed Sep 8, 2023
1 parent 2d0eaeb commit 96b47e5
Show file tree
Hide file tree
Showing 20 changed files with 1,291 additions and 1 deletion.
10 changes: 10 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,13 @@ generate-all-dto: ## Generate all DTOs for SDK interfaces

generate-dto-%: ./pkg/sdk/%_dto.go ## Generate DTO for given SDK interface
go generate $<

run-generator-poc:
go generate ./pkg/sdk/poc/example/*_def.go
go generate ./pkg/sdk/poc/example/*_dto_gen.go
.PHONY: run-generator-poc

clean-generator-poc:
rm -f ./pkg/sdk/poc/example/*_gen.go
rm -f ./pkg/sdk/poc/example/*_gen_test.go
.PHONY: run-generator-poc
3 changes: 2 additions & 1 deletion pkg/sdk/dto-builder-generator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ func setUpGenerator(astFile *ast.File) *Generator {

file := os.Getenv("GOFILE")
fileWithoutSuffix, _ := strings.CutSuffix(file, ".go")
baseName := fmt.Sprintf("%s_generated.go", fileWithoutSuffix)
fileWithoutSuffix, _ = strings.CutSuffix(fileWithoutSuffix, "_gen")
baseName := fmt.Sprintf("%s_builders_gen.go", fileWithoutSuffix)
outputName := filepath.Join(wd, baseName)

return &Generator{
Expand Down
8 changes: 8 additions & 0 deletions pkg/sdk/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ func errOneOf(fieldNames ...string) error {
return fmt.Errorf("fields %v are incompatible and cannot be set at once", fieldNames)
}

func errExactlyOneOf(fieldNames ...string) error {
return fmt.Errorf("exactly one of %v must be set", fieldNames)
}

func errAtLeastOneOf(fieldNames ...string) error {
return fmt.Errorf("at least one of %v must be set", fieldNames)
}

func decodeDriverError(err error) error {
if err == nil {
return nil
Expand Down
51 changes: 51 additions & 0 deletions pkg/sdk/poc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
## SDK generator PoC

PoC of generating full object implementation based on object definition.

### Description

There is an example file ready for generation [database_role_def.go](example/database_role_def.go) which creates files:
- [database_role_gen.go](example/database_role_gen.go) - SDK interface, options structs
- [database_role_dto_gen.go](example/database_role_dto_gen.go) - SDK Request DTOs
- [database_role_dto_builders_gen.go](example/database_role_dto_builders_gen.go) - SDK Request DTOs constructors and builder methods (this file is generated using [dto-builder-generator](../dto-builder-generator/main.go))
- [database_role_validations_gen.go](example/database_role_validations_gen.go) - options structs validations
- [database_role_impl_gen.go](example/database_role_impl_gen.go) - SDK interface implementation
- [database_role_gen_test.go](example/database_role_gen_test.go) - unit tests placeholders with guidance comments (at least for now)
- [database_role_gen_integration_test.go](example/database_role_gen_integration_test.go) - integration test placeholder file

### How it works
##### Creating object generation definition

To create definition for object generation:

1. Create file `object_name_def.go` (like example [database_role_def.go](example/database_role_def.go) file).
2. Put go generate directive at the top: `//go:generate go run ../main.go`. Remember that you may have to change the path to [main.go](main.go) file.
3. Create object interface definition.
4. Add key-value entry to `definitionMapping` in [main.go](main.go):
- key should be created file name (for [database_role_def.go](example/database_role_def.go) example file: `"database_role_def.go"`)
- value should be created definition (like for [database_role_def.go](example/database_role_def.go) example file: `DatabaseRole`)
5. You are all set to run generation.

##### Invoking generation

To invoke example generation (with first cleaning all the generated files) run:
```shell
make clean-generator-poc run-generator-poc
```

### Next steps
##### Essentials
- use DSL to build object definitions (from branch [go-builder-dsl](https://github.com/Snowflake-Labs/terraform-provider-snowflake/tree/go-builder-dsl)) - ideally leave two options of defining objects and proceed with generation based on definition provided
- differentiate between different actions implementations (now only `Create` and `Alter` has been considered, `Show` on the other hand has totally different implementation)
- generate `struct`s for `Show` and `ShowID`
- handle arrays
- handle more validation types

##### Improvements
- automatic names of nested `struct`s (e.g. `DatabaseRoleRename`)
- check if generating with package name + invoking format removes unnecessary qualifier
- consider merging templates `StructTemplate` and `OptionsTemplate` (requires moving Doc to Field)
- add unit tests to this generator

##### Known issues
- spaces in templates (especially nested validations)
65 changes: 65 additions & 0 deletions pkg/sdk/poc/example/database_role_def.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package example

import g "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk/poc/generator"

//go:generate go run ../main.go

var _ = DatabaseRole

var DatabaseRole = g.NewInterface("DatabaseRoles", "DatabaseRole", "DatabaseObjectIdentifier").WithOperations(
[]*g.Operation{
g.NewOperation("Create", "https://docs.snowflake.com/en/sql-reference/sql/create-database-role").WithOptsField(
g.NewField("<should be updated programmatically>", "<should be updated programmatically>", nil).
WithFields([]*g.Field{
g.NewField("create", "bool", map[string][]string{"ddl": {"static"}, "sql": {"CREATE"}}),
g.NewField("OrReplace", "*bool", map[string][]string{"ddl": {"keyword"}, "sql": {"OR REPLACE"}}),
g.NewField("databaseRole", "bool", map[string][]string{"ddl": {"static"}, "sql": {"DATABASE ROLE"}}),
g.NewField("IfNotExists", "*bool", map[string][]string{"ddl": {"keyword"}, "sql": {"IF NOT EXISTS"}}),
g.NewField("name", "DatabaseObjectIdentifier", map[string][]string{"ddl": {"identifier"}}).WithRequired(true),
g.NewField("Comment", "*string", map[string][]string{"ddl": {"parameter", "single_quotes"}, "sql": {"COMMENT"}}),
}).
WithValidations([]*g.Validation{
g.NewValidation(g.ValidIdentifier, []string{"name"}),
g.NewValidation(g.ConflictingFields, []string{"OrReplace", "IfNotExists"}),
}),
),
g.NewOperation("Alter", "https://docs.snowflake.com/en/sql-reference/sql/alter-database-role").WithOptsField(
g.NewField("<should be updated programmatically>", "<should be updated programmatically>", nil).
WithFields([]*g.Field{
g.NewField("alter", "bool", map[string][]string{"ddl": {"static"}, "sql": {"ALTER"}}),
g.NewField("databaseRole", "bool", map[string][]string{"ddl": {"static"}, "sql": {"DATABASE ROLE"}}),
g.NewField("IfExists", "*bool", map[string][]string{"ddl": {"keyword"}, "sql": {"IF EXISTS"}}),
g.NewField("name", "DatabaseObjectIdentifier", map[string][]string{"ddl": {"identifier"}}).WithRequired(true),
g.NewField("Rename", "*DatabaseRoleRename", map[string][]string{"ddl": {"list,no_parentheses"}, "sql": {"RENAME TO"}}).
WithFields([]*g.Field{
g.NewField("Name", "DatabaseObjectIdentifier", map[string][]string{"ddl": {"identifier"}}).WithRequired(true),
}).
WithValidations([]*g.Validation{
g.NewValidation(g.ValidIdentifier, []string{"Name"}),
}),
g.NewField("Set", "*DatabaseRoleSet", map[string][]string{"ddl": {"list,no_parentheses"}, "sql": {"SET"}}).
WithFields([]*g.Field{
g.NewField("Comment", "string", map[string][]string{"ddl": {"parameter", "single_quotes"}, "sql": {"COMMENT"}}).WithRequired(true),
g.NewField("NestedThirdLevel", "*NestedThirdLevel", map[string][]string{"ddl": {"list,no_parentheses"}, "sql": {"NESTED"}}).
WithFields([]*g.Field{
g.NewField("Field", "DatabaseObjectIdentifier", map[string][]string{"ddl": {"identifier"}}).WithRequired(true),
}).
WithValidations([]*g.Validation{
g.NewValidation(g.AtLeastOneValueSet, []string{"Field"}),
}),
}),
g.NewField("Unset", "*DatabaseRoleUnset", map[string][]string{"ddl": {"list,no_parentheses"}, "sql": {"UNSET"}}).
WithFields([]*g.Field{
g.NewField("Comment", "bool", map[string][]string{"ddl": {"keyword"}, "sql": {"COMMENT"}}).WithRequired(true),
}).
WithValidations([]*g.Validation{
g.NewValidation(g.AtLeastOneValueSet, []string{"Comment"}),
}),
}).
WithValidations([]*g.Validation{
g.NewValidation(g.ValidIdentifier, []string{"name"}),
g.NewValidation(g.ExactlyOneValueSet, []string{"Rename", "Set", "Unset"}),
}),
),
},
)
93 changes: 93 additions & 0 deletions pkg/sdk/poc/example/database_role_dto_builders_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 40 additions & 0 deletions pkg/sdk/poc/example/database_role_dto_gen.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package example

//go:generate go run ../../dto-builder-generator/main.go

var (
_ optionsProvider[CreateDatabaseRoleOptions] = new(CreateDatabaseRoleRequest)
_ optionsProvider[AlterDatabaseRoleOptions] = new(AlterDatabaseRoleRequest)
)

type CreateDatabaseRoleRequest struct {
OrReplace *bool
IfNotExists *bool
name DatabaseObjectIdentifier // required
Comment *string
}

type AlterDatabaseRoleRequest struct {
IfExists *bool
name DatabaseObjectIdentifier // required
Rename *DatabaseRoleRenameRequest
Set *DatabaseRoleSetRequest
Unset *DatabaseRoleUnsetRequest
}

type DatabaseRoleRenameRequest struct {
Name DatabaseObjectIdentifier // required
}

type DatabaseRoleSetRequest struct {
Comment string // required
NestedThirdLevel *NestedThirdLevelRequest
}

type NestedThirdLevelRequest struct {
Field DatabaseObjectIdentifier // required
}

type DatabaseRoleUnsetRequest struct {
Comment bool // required
}
46 changes: 46 additions & 0 deletions pkg/sdk/poc/example/database_role_gen.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package example

import "context"

type DatabaseRoles interface {
Create(ctx context.Context, request *CreateDatabaseRoleRequest) error
Alter(ctx context.Context, request *AlterDatabaseRoleRequest) error
}

// CreateDatabaseRoleOptions is based on https://docs.snowflake.com/en/sql-reference/sql/create-database-role.
type CreateDatabaseRoleOptions struct {
create bool `ddl:"static" sql:"CREATE"`
OrReplace *bool `ddl:"keyword" sql:"OR REPLACE"`
databaseRole bool `ddl:"static" sql:"DATABASE ROLE"`
IfNotExists *bool `ddl:"keyword" sql:"IF NOT EXISTS"`
name DatabaseObjectIdentifier `ddl:"identifier"`
Comment *string `ddl:"parameter,single_quotes" sql:"COMMENT"`
}

// AlterDatabaseRoleOptions is based on https://docs.snowflake.com/en/sql-reference/sql/alter-database-role.
type AlterDatabaseRoleOptions struct {
alter bool `ddl:"static" sql:"ALTER"`
databaseRole bool `ddl:"static" sql:"DATABASE ROLE"`
IfExists *bool `ddl:"keyword" sql:"IF EXISTS"`
name DatabaseObjectIdentifier `ddl:"identifier"`
Rename *DatabaseRoleRename `ddl:"list,no_parentheses" sql:"RENAME TO"`
Set *DatabaseRoleSet `ddl:"list,no_parentheses" sql:"SET"`
Unset *DatabaseRoleUnset `ddl:"list,no_parentheses" sql:"UNSET"`
}

type DatabaseRoleRename struct {
Name DatabaseObjectIdentifier `ddl:"identifier"`
}

type DatabaseRoleSet struct {
Comment string `ddl:"parameter,single_quotes" sql:"COMMENT"`
NestedThirdLevel *NestedThirdLevel `ddl:"list,no_parentheses" sql:"NESTED"`
}

type NestedThirdLevel struct {
Field DatabaseObjectIdentifier `ddl:"identifier"`
}

type DatabaseRoleUnset struct {
Comment bool `ddl:"keyword" sql:"COMMENT"`
}
7 changes: 7 additions & 0 deletions pkg/sdk/poc/example/database_role_gen_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package example

import "testing"

func TestInt_DatabaseRoles(t *testing.T) {
// TODO: fill me
}
45 changes: 45 additions & 0 deletions pkg/sdk/poc/example/database_role_gen_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package example

import "testing"

func TestDatabaseRoles_Create(t *testing.T) {
id := randomDatabaseObjectIdentifier(t)

defaultOpts := func() *CreateDatabaseRoleOptions {
return &CreateDatabaseRoleOptions{
name: id,
}
}
// TODO: remove me
_ = defaultOpts()

// TODO: fill me

// TODO: validate valid identifier for [opts.name]
// TODO: validate conflicting fields for [opts.OrReplace opts.IfNotExists]

}

func TestDatabaseRoles_Alter(t *testing.T) {
id := randomDatabaseObjectIdentifier(t)

defaultOpts := func() *AlterDatabaseRoleOptions {
return &AlterDatabaseRoleOptions{
name: id,
}
}
// TODO: remove me
_ = defaultOpts()

// TODO: fill me

// TODO: validate valid identifier for [opts.name]
// TODO: validate exactly one field from [opts.Rename opts.Set opts.Unset] is present

// TODO: validate valid identifier for [opts.Rename.Name]

// TODO: validate at least one of fields [opts.Set.NestedThirdLevel.Field] set

// TODO: validate at least one of fields [opts.Unset.Comment] set

}

0 comments on commit 96b47e5

Please sign in to comment.