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

Require provider-defined function parameter naming #964

Merged
merged 13 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/unreleased/BREAKING CHANGES-20240320-095152.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: BREAKING CHANGES
body: 'function: All parameters must be explicitly named'
bendbennett marked this conversation as resolved.
Show resolved Hide resolved
time: 2024-03-20T09:51:52.869254Z
custom:
Issue: "964"
32 changes: 21 additions & 11 deletions function/definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,41 +89,46 @@ func (d Definition) Parameter(ctx context.Context, position int) (Parameter, dia
// implementation of the definition to prevent unexpected errors or panics. This
// logic runs during the GetProviderSchema RPC, or via provider-defined unit
// testing, and should never include false positives.
func (d Definition) ValidateImplementation(ctx context.Context) diag.Diagnostics {
func (d Definition) ValidateImplementation(ctx context.Context, funcName string) diag.Diagnostics {
Copy link
Member

Choose a reason for hiding this comment

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

Since this is an exported function, should we switch this to request/response types while we are making a breaking change to the signature and to allow for future-proofing?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good call. Have added request-response pattern for ValidateImplementation along with DefinitionValidateRequest and DefinitionValidateResponse.

var diags diag.Diagnostics

if d.Return == nil {
diags.AddError(
"Invalid Function Definition",
"When validating the function definition, an implementation issue was found. "+
"This is always an issue with the provider and should be reported to the provider developers.\n\n"+
"Definition Return field is undefined",
fmt.Sprintf("Function %q - Definition Return field is undefined", funcName),
)
} else if d.Return.GetType() == nil {
diags.AddError(
"Invalid Function Definition",
"When validating the function definition, an implementation issue was found. "+
"This is always an issue with the provider and should be reported to the provider developers.\n\n"+
"Definition return data type is undefined",
fmt.Sprintf("Function %q - Definition return data type is undefined", funcName),
)
}

paramNames := make(map[string]int, len(d.Parameters))
for pos, param := range d.Parameters {
name := param.GetName()
// If name is not set, default the param name based on position: "param1", "param2", etc.
// If name is not set, add an error diagnostic, parameter names are mandatory.
if name == "" {
name = fmt.Sprintf("%s%d", DefaultParameterNamePrefix, pos+1)
diags.AddError(
"Invalid Function Definition",
"When validating the function definition, an implementation issue was found. "+
"This is always an issue with the provider and should be reported to the provider developers.\n\n"+
fmt.Sprintf("Function %q - Parameter at position %d does not have a name", funcName, pos),
)
}

conflictPos, exists := paramNames[name]
if exists {
if exists && name != "" {
diags.AddError(
"Invalid Function Definition",
"When validating the function definition, an implementation issue was found. "+
"This is always an issue with the provider and should be reported to the provider developers.\n\n"+
"Parameter names must be unique. "+
fmt.Sprintf("Parameters at position %d and %d have the same name %q", conflictPos, pos, name),
fmt.Sprintf("Function %q - Parameters at position %d and %d have the same name %q", funcName, conflictPos, pos, name),
)
continue
}
Expand All @@ -133,19 +138,24 @@ func (d Definition) ValidateImplementation(ctx context.Context) diag.Diagnostics

if d.VariadicParameter != nil {
name := d.VariadicParameter.GetName()
// If name is not set, default the variadic param name
// If name is not set, add an error diagnostic, parameter names are mandatory.
if name == "" {
name = DefaultVariadicParameterName
diags.AddError(
"Invalid Function Definition",
"When validating the function definition, an implementation issue was found. "+
"This is always an issue with the provider and should be reported to the provider developers.\n\n"+
fmt.Sprintf("Function %q - The variadic parameter does not have a name", funcName),
)
}

conflictPos, exists := paramNames[name]
if exists {
if exists && name != "" {
diags.AddError(
"Invalid Function Definition",
"When validating the function definition, an implementation issue was found. "+
"This is always an issue with the provider and should be reported to the provider developers.\n\n"+
"Parameter names must be unique. "+
fmt.Sprintf("Parameter at position %d and the variadic parameter have the same name %q", conflictPos, name),
fmt.Sprintf("Function %q - Parameter at position %d and the variadic parameter have the same name %q", funcName, conflictPos, name),
)
}
}
Expand Down
97 changes: 47 additions & 50 deletions function/definition_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"testing"

"github.com/google/go-cmp/cmp"

"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/function"
)
Expand Down Expand Up @@ -157,29 +158,65 @@ func TestDefinitionValidateImplementation(t *testing.T) {
Return: function.StringReturn{},
},
},
"valid-only-variadic": {
"missing-variadic-param-name": {
definition: function.Definition{
VariadicParameter: function.StringParameter{},
Return: function.StringReturn{},
},
expected: diag.Diagnostics{
diag.NewErrorDiagnostic(
"Invalid Function Definition",
"When validating the function definition, an implementation issue was found. "+
"This is always an issue with the provider and should be reported to the provider developers.\n\n"+
"Function \"test-function\" - The variadic parameter does not have a name",
),
},
},
"valid-param-name-defaults": {
"missing-param-names": {
definition: function.Definition{
Parameters: []function.Parameter{
function.StringParameter{},
function.StringParameter{},
},
Return: function.StringReturn{},
},
expected: diag.Diagnostics{
diag.NewErrorDiagnostic(
"Invalid Function Definition",
"When validating the function definition, an implementation issue was found. "+
"This is always an issue with the provider and should be reported to the provider developers.\n\n"+
"Function \"test-function\" - Parameter at position 0 does not have a name",
),
diag.NewErrorDiagnostic(
"Invalid Function Definition",
"When validating the function definition, an implementation issue was found. "+
"This is always an issue with the provider and should be reported to the provider developers.\n\n"+
"Function \"test-function\" - Parameter at position 1 does not have a name",
),
},
},
"valid-param-names-defaults-with-variadic": {
"missing-param-names-with-variadic": {
definition: function.Definition{
Parameters: []function.Parameter{
function.StringParameter{},
},
VariadicParameter: function.NumberParameter{},
Return: function.StringReturn{},
},
expected: diag.Diagnostics{
diag.NewErrorDiagnostic(
"Invalid Function Definition",
"When validating the function definition, an implementation issue was found. "+
"This is always an issue with the provider and should be reported to the provider developers.\n\n"+
"Function \"test-function\" - Parameter at position 0 does not have a name",
),
diag.NewErrorDiagnostic(
"Invalid Function Definition",
"When validating the function definition, an implementation issue was found. "+
"This is always an issue with the provider and should be reported to the provider developers.\n\n"+
"Function \"test-function\" - The variadic parameter does not have a name",
),
},
},
"result-missing": {
definition: function.Definition{},
Expand All @@ -188,7 +225,7 @@ func TestDefinitionValidateImplementation(t *testing.T) {
"Invalid Function Definition",
"When validating the function definition, an implementation issue was found. "+
"This is always an issue with the provider and should be reported to the provider developers.\n\n"+
"Definition Return field is undefined",
"Function \"test-function\" - Definition Return field is undefined",
),
},
},
Expand Down Expand Up @@ -219,27 +256,7 @@ func TestDefinitionValidateImplementation(t *testing.T) {
"When validating the function definition, an implementation issue was found. "+
"This is always an issue with the provider and should be reported to the provider developers.\n\n"+
"Parameter names must be unique. "+
"Parameters at position 2 and 4 have the same name \"param-dup\"",
),
},
},
"conflicting-param-name-with-default": {
definition: function.Definition{
Parameters: []function.Parameter{
function.StringParameter{
Name: "param2",
},
function.Float64Parameter{}, // defaults to param2
},
Return: function.StringReturn{},
},
expected: diag.Diagnostics{
diag.NewErrorDiagnostic(
"Invalid Function Definition",
"When validating the function definition, an implementation issue was found. "+
"This is always an issue with the provider and should be reported to the provider developers.\n\n"+
"Parameter names must be unique. "+
"Parameters at position 0 and 1 have the same name \"param2\"",
"Function \"test-function\" - Parameters at position 2 and 4 have the same name \"param-dup\"",
),
},
},
Expand Down Expand Up @@ -267,7 +284,7 @@ func TestDefinitionValidateImplementation(t *testing.T) {
"When validating the function definition, an implementation issue was found. "+
"This is always an issue with the provider and should be reported to the provider developers.\n\n"+
"Parameter names must be unique. "+
"Parameter at position 1 and the variadic parameter have the same name \"param-dup\"",
"Function \"test-function\" - Parameter at position 1 and the variadic parameter have the same name \"param-dup\"",
),
},
},
Expand Down Expand Up @@ -301,41 +318,21 @@ func TestDefinitionValidateImplementation(t *testing.T) {
"When validating the function definition, an implementation issue was found. "+
"This is always an issue with the provider and should be reported to the provider developers.\n\n"+
"Parameter names must be unique. "+
"Parameters at position 0 and 2 have the same name \"param-dup\"",
"Function \"test-function\" - Parameters at position 0 and 2 have the same name \"param-dup\"",
),
diag.NewErrorDiagnostic(
"Invalid Function Definition",
"When validating the function definition, an implementation issue was found. "+
"This is always an issue with the provider and should be reported to the provider developers.\n\n"+
"Parameter names must be unique. "+
"Parameters at position 0 and 4 have the same name \"param-dup\"",
"Function \"test-function\" - Parameters at position 0 and 4 have the same name \"param-dup\"",
),
diag.NewErrorDiagnostic(
"Invalid Function Definition",
"When validating the function definition, an implementation issue was found. "+
"This is always an issue with the provider and should be reported to the provider developers.\n\n"+
"Parameter names must be unique. "+
"Parameter at position 0 and the variadic parameter have the same name \"param-dup\"",
),
},
},
"conflicting-param-name-with-variadic-default": {
definition: function.Definition{
Parameters: []function.Parameter{
function.Float64Parameter{
Name: function.DefaultVariadicParameterName,
},
},
VariadicParameter: function.BoolParameter{}, // defaults to varparam
Return: function.StringReturn{},
},
expected: diag.Diagnostics{
diag.NewErrorDiagnostic(
"Invalid Function Definition",
"When validating the function definition, an implementation issue was found. "+
"This is always an issue with the provider and should be reported to the provider developers.\n\n"+
"Parameter names must be unique. "+
"Parameter at position 0 and the variadic parameter have the same name \"varparam\"",
"Function \"test-function\" - Parameter at position 0 and the variadic parameter have the same name \"param-dup\"",
),
},
},
Expand All @@ -347,7 +344,7 @@ func TestDefinitionValidateImplementation(t *testing.T) {
t.Run(name, func(t *testing.T) {
t.Parallel()

got := testCase.definition.ValidateImplementation(context.Background())
got := testCase.definition.ValidateImplementation(context.Background(), "test-function")

if diff := cmp.Diff(got, testCase.expected); diff != "" {
t.Errorf("unexpected difference: %s", diff)
Expand Down
4 changes: 4 additions & 0 deletions function/func_error.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ func (fe *FuncError) Equal(other *FuncError) bool {

// Error returns the error text.
func (fe *FuncError) Error() string {
if fe == nil {
return ""
}

return fe.Text
}

Expand Down
36 changes: 36 additions & 0 deletions function/func_error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,42 @@ func TestFunctionError_Equal(t *testing.T) {
}
}

func TestFunctionError_Error(t *testing.T) {
t.Parallel()

testCases := map[string]struct {
funcErr *function.FuncError
expected string
}{
"nil": {
expected: "",
},
"empty": {
funcErr: &function.FuncError{},
expected: "",
},
"text": {
funcErr: &function.FuncError{
Text: "function error",
},
expected: "function error",
},
}

for name, tc := range testCases {
name, tc := name, tc
t.Run(name, func(t *testing.T) {
t.Parallel()

got := tc.funcErr.Error()

if got != tc.expected {
t.Errorf("Unexpected response: got: %s, wanted: %s", got, tc.expected)
}
})
}
}

func TestConcatFuncErrors(t *testing.T) {
t.Parallel()

Expand Down
11 changes: 0 additions & 11 deletions function/parameter.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,6 @@ import (
"github.com/hashicorp/terraform-plugin-framework/attr"
)

const (
// DefaultParameterNamePrefix is the prefix used to default the name of parameters which do not declare
// a name. Use this to prevent Terraform errors for missing names. This prefix is used with the parameter
// position in a function definition to create a unique name (param1, param2, etc.)
DefaultParameterNamePrefix = "param"

// DefaultVariadicParameterName is the default name given to a variadic parameter that does not declare
// a name. Use this to prevent Terraform errors for missing names.
DefaultVariadicParameterName = "varparam"
)
Comment on lines -10 to -19
Copy link
Member

Choose a reason for hiding this comment

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

For breaking change completeness, we'll need to also call out these constant removals.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Have added a changelog entry for this.


// Parameter is the interface for defining function parameters.
type Parameter interface {
// GetAllowNullValue should return if the parameter accepts a null value.
Expand Down
2 changes: 1 addition & 1 deletion internal/fwserver/server_functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ func (s *Server) FunctionDefinitions(ctx context.Context) (map[string]function.D
continue
}

validateDiags := definitionResp.Definition.ValidateImplementation(ctx)
validateDiags := definitionResp.Definition.ValidateImplementation(ctx, name)

diags.Append(validateDiags...)

Expand Down
2 changes: 1 addition & 1 deletion internal/fwserver/server_getfunctions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ func TestServerGetFunctions(t *testing.T) {
"Invalid Function Definition",
"When validating the function definition, an implementation issue was found. "+
"This is always an issue with the provider and should be reported to the provider developers.\n\n"+
"Definition Return field is undefined",
"Function \"function1\" - Definition Return field is undefined",
),
},
FunctionDefinitions: map[string]function.Definition{},
Expand Down
2 changes: 1 addition & 1 deletion internal/fwserver/server_getproviderschema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,7 @@ func TestServerGetProviderSchema(t *testing.T) {
"Invalid Function Definition",
"When validating the function definition, an implementation issue was found. "+
"This is always an issue with the provider and should be reported to the provider developers.\n\n"+
"Definition Return field is undefined",
"Function \"function1\" - Definition Return field is undefined",
),
},
FunctionDefinitions: nil,
Expand Down