Skip to content

Commit

Permalink
chore: Generate marshal json for each model (#3307)
Browse files Browse the repository at this point in the history
- generate marshal and `depends_on` for each resource and data source
model
- replace deprecated `FromModel` with the new one utilizing the
aforementioned marshaling
- fix test with explicit empty list (and add it to special variables)
- fix tests with explicit null value (and add it to special variables)
- generalize multiline handling and test it
- use multiline strings for definitions in functions and procedures
config builders
- simplify function and procedure definitions
- format whitespace in function and procedure definitions

For the next PRs (added to the issue):
- current config building for functions/procedures (especially
definitions) is complex:
  - definition is build using multiline golang strong and sprintf
  - whitespace is formatted
- this is added in the config builder wrapped in special multiline
marker
  - hcl-compatible json is generated
  - json converted to hcl
  - hcl formatted using custom formatters
This should be refined and made simpler (e.g. newline replacement,
encoding problems, etc.)

- marking in config builders generators which fields should be handled
as multiline by default (because now e.g. SQL function requires the
definition, so it's generated in basic builder as a string input, and
not the multiline one; because of that it has to be added again with
`WithFunctionDefinitionValue(...)` which is confusing)
  • Loading branch information
sfc-gh-asawicki authored Dec 20, 2024
1 parent 0d5b45a commit 6fe31ba
Show file tree
Hide file tree
Showing 108 changed files with 1,738 additions and 858 deletions.
49 changes: 2 additions & 47 deletions pkg/acceptance/bettertestspoc/config/config.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package config

import (
"encoding/json"
"fmt"
"reflect"
"strings"
"testing"
Expand Down Expand Up @@ -67,7 +65,8 @@ func ProviderFromModel(t *testing.T, model ProviderModel) string {
return hcl
}

// FromModels allows to combine multiple models.
// FromModels should be used in terraform acceptance tests for Config attribute to get string config from all models.
// FromModels allows to combine multiple model types.
// TODO [SNOW-1501905]: introduce some common interface for all three existing models (ResourceModel, DatasourceModel, and ProviderModel)
func FromModels(t *testing.T, models ...any) string {
t.Helper()
Expand All @@ -91,50 +90,6 @@ func FromModels(t *testing.T, models ...any) string {
return sb.String()
}

// FromModel should be used in terraform acceptance tests for Config attribute to get string config from ResourceModel.
// Current implementation is really straightforward but it could be improved and tested. It may not handle all cases (like objects, lists, sets) correctly.
// TODO [SNOW-1501905]: use reflection to build config directly from model struct (or some other different way)
// TODO [SNOW-1501905]: add support for config.TestStepConfigFunc (to use as ConfigFile); the naive implementation would be to just create a tmp directory and save file there
// TODO [SNOW-1501905]: add generating MarshalJSON() function
// TODO [SNOW-1501905]: migrate resources to new config generation method (above needed first)
// Use ResourceFromModel, DatasourceFromModel, ProviderFromModel, and FromModels instead.
func FromModel(t *testing.T, model ResourceModel) string {
t.Helper()

b, err := json.Marshal(model)
require.NoError(t, err)

var objMap map[string]json.RawMessage
err = json.Unmarshal(b, &objMap)
require.NoError(t, err)

var sb strings.Builder
sb.WriteString(fmt.Sprintf(`resource "%s" "%s" {`, model.Resource(), model.ResourceName()))
sb.WriteRune('\n')
for k, v := range objMap {
sb.WriteString(fmt.Sprintf("\t%s = %s\n", k, v))
}
if len(model.DependsOn()) > 0 {
sb.WriteString(fmt.Sprintf("\tdepends_on = [%s]\n", strings.Join(model.DependsOn(), ", ")))
}
sb.WriteString(`}`)
sb.WriteRune('\n')
s := sb.String()
t.Logf("Generated config:\n%s", s)
return s
}

// FromModelsDeprecated allows to combine multiple resource models.
// Use FromModels instead.
func FromModelsDeprecated(t *testing.T, models ...ResourceModel) string {
t.Helper()
var sb strings.Builder
for _, model := range models {
sb.WriteString(FromModel(t, model) + "\n")
}
return sb.String()
}

// ConfigVariablesFromModel constructs config.Variables needed in acceptance tests that are using ConfigVariables in
// combination with ConfigDirectory. It's necessary for cases not supported by FromModel, like lists of objects.
// Use ResourceFromModel, DatasourceFromModel, ProviderFromModel, and FromModels instead.
Expand Down
10 changes: 10 additions & 0 deletions pkg/acceptance/bettertestspoc/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ resource "snowflake_share" "test" {
Item{IntField: 2, StringField: "second item"},
).
WithSingleObject("one", 2).
WithTextFieldExplicitNull().
WithListFieldEmpty().
WithMultilineField("some\nmultiline\ncontent").
WithDependsOn("some_other_resource.some_name", "other_resource.some_other_name", "third_resource.third_name")
expectedOutput := strings.TrimPrefix(`
resource "snowflake_share" "test" {
Expand All @@ -54,6 +57,13 @@ resource "snowflake_share" "test" {
a = "one"
b = 2
}
text_field = null
list_field = []
multiline_field = <<EOT
some
multiline
content
EOT
depends_on = [some_other_resource.some_name, other_resource.some_other_name, third_resource.third_name]
}
`, "\n")
Expand Down
47 changes: 39 additions & 8 deletions pkg/acceptance/bettertestspoc/config/custom_variables.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,46 @@
package config

import "encoding/json"
import (
"encoding/json"
"fmt"
)

type nullVariable struct{}
type emptyListVariable struct{}

// MarshalJSON returns the JSON encoding of nullVariable.
func (v nullVariable) MarshalJSON() ([]byte, error) {
return json.Marshal(nil)
// MarshalJSON returns the JSON encoding of emptyListVariable.
func (v emptyListVariable) MarshalJSON() ([]byte, error) {
return json.Marshal([]any{})
}

// NullVariable returns nullVariable which implements Variable.
func NullVariable() nullVariable {
return nullVariable{}
// EmptyListVariable returns Variable representing an empty list. This is because the current hcl parser handles empty SetVariable incorrectly.
func EmptyListVariable() emptyListVariable {
return emptyListVariable{}
}

type replacementPlaceholderVariable struct {
placeholder ReplacementPlaceholder
}

// MarshalJSON returns the JSON encoding of replacementPlaceholderVariable.
func (v replacementPlaceholderVariable) MarshalJSON() ([]byte, error) {
return json.Marshal(v.placeholder)
}

// ReplacementPlaceholderVariable returns Variable containing one of the ReplacementPlaceholder which is later replaced by HclFormatter.
func ReplacementPlaceholderVariable(placeholder ReplacementPlaceholder) replacementPlaceholderVariable {
return replacementPlaceholderVariable{placeholder}
}

type multilineWrapperVariable struct {
content string
}

// MarshalJSON returns the JSON encoding of multilineWrapperVariable.
func (v multilineWrapperVariable) MarshalJSON() ([]byte, error) {
return json.Marshal(fmt.Sprintf(`%[1]s%[2]s%[1]s`, SnowflakeProviderConfigMultilineMarker, v.content))
}

// MultilineWrapperVariable returns Variable containing multiline content wrapped with SnowflakeProviderConfigMultilineMarker later replaced by HclFormatter.
func MultilineWrapperVariable(content string) multilineWrapperVariable {
return multilineWrapperVariable{content}
}

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

This file was deleted.

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

Original file line number Diff line number Diff line change
@@ -1,28 +1,9 @@
package datasourcemodel

import (
"encoding/json"

tfconfig "github.com/hashicorp/terraform-plugin-testing/config"
)

// Based on https://medium.com/picus-security-engineering/custom-json-marshaller-in-go-and-common-pitfalls-c43fa774db05.
func (d *DatabasesModel) MarshalJSON() ([]byte, error) {
type Alias DatabasesModel
return json.Marshal(&struct {
*Alias
DependsOn []string `json:"depends_on,omitempty"`
}{
Alias: (*Alias)(d),
DependsOn: d.DependsOn(),
})
}

func (d *DatabasesModel) WithDependsOn(values ...string) *DatabasesModel {
d.SetDependsOn(values...)
return d
}

func (d *DatabasesModel) WithLimit(rows int) *DatabasesModel {
return d.WithLimitValue(
tfconfig.ObjectVariable(map[string]tfconfig.Variable{
Expand Down

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

Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ var (
genhelpers.SnakeCaseToCamel,
)).Parse(definitionTemplateContent)

//go:embed templates/marshal_json.tmpl
marshalJsonTemplateContent string
MarshalJsonTemplate, _ = template.New("marshalJsonTemplate").Funcs(genhelpers.BuildTemplateFuncMap(
genhelpers.FirstLetterLowercase,
genhelpers.FirstLetter,
genhelpers.SnakeCaseToCamel,
)).Parse(marshalJsonTemplateContent)

// TODO [SNOW-1501905]: consider duplicating the builders template from resource (currently same template used for datasources and provider which limits the customization possibilities for just one block type)
AllTemplates = []*template.Template{PreambleTemplate, DefinitionTemplate, resourcemodel.BuildersTemplate}
AllTemplates = []*template.Template{PreambleTemplate, DefinitionTemplate, MarshalJsonTemplate, resourcemodel.BuildersTemplate}
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{{- /*gotype: github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config/model/gen.ResourceConfigBuilderModel*/ -}}

{{- $modelName := .Name | printf "%sModel" -}}
{{- $nameLowerCase := FirstLetterLowercase .Name -}}
{{- $modelVar := FirstLetter $nameLowerCase }}
///////////////////////////////////////////////////////
// set proper json marshalling and handle depends on //
///////////////////////////////////////////////////////

func ({{ $modelVar }} *{{ $modelName }}) MarshalJSON() ([]byte, error) {
type Alias {{ $modelName }}
return json.Marshal(&struct {
*Alias
DependsOn []string `json:"depends_on,omitempty"`
}{
Alias: (*Alias)({{ $modelVar }}),
DependsOn: {{ $modelVar }}.DependsOn(),
})
}

func ({{ $modelVar }} *{{ $modelName }}) WithDependsOn(values ...string) *{{ $modelName }} {
{{ $modelVar }}.SetDependsOn(values...)
return {{ $modelVar }}
}
Loading

0 comments on commit 6fe31ba

Please sign in to comment.