diff --git a/.changes/unreleased/BREAKING CHANGES-20240227-112229.yaml b/.changes/unreleased/BREAKING CHANGES-20240227-112229.yaml new file mode 100644 index 000000000..de6c8d805 --- /dev/null +++ b/.changes/unreleased/BREAKING CHANGES-20240227-112229.yaml @@ -0,0 +1,5 @@ +kind: BREAKING CHANGES +body: 'function: Altered the `RunResponse` type, replacing `Diagnostics` with `FuncError`' +time: 2024-02-27T11:22:29.392126Z +custom: + Issue: "925" diff --git a/.changes/unreleased/BREAKING CHANGES-20240227-113128.yaml b/.changes/unreleased/BREAKING CHANGES-20240227-113128.yaml new file mode 100644 index 000000000..5b3697272 --- /dev/null +++ b/.changes/unreleased/BREAKING CHANGES-20240227-113128.yaml @@ -0,0 +1,8 @@ +kind: BREAKING CHANGES +body: 'diag: Removed `DiagnosticWithFunctionArgument` interface. Removed + `NewArgumentErrorDiagnostic()`, `NewArgumentWarningDiagnostic()` and + `WithFunctionArgument()` functions. Removed `AddArgumentError()` and + `AddArgumentWarning()` methods from `Diagnostics`.' +time: 2024-02-27T11:31:28.09588Z +custom: + Issue: "925" diff --git a/.changes/unreleased/ENHANCEMENTS-20240227-112448.yaml b/.changes/unreleased/ENHANCEMENTS-20240227-112448.yaml new file mode 100644 index 000000000..0f80f4f0a --- /dev/null +++ b/.changes/unreleased/ENHANCEMENTS-20240227-112448.yaml @@ -0,0 +1,5 @@ +kind: ENHANCEMENTS +body: 'function: Added `FuncError` type, required for `RunResponse`' +time: 2024-02-27T11:24:48.711538Z +custom: + Issue: "925" diff --git a/.changes/unreleased/ENHANCEMENTS-20240227-112633.yaml b/.changes/unreleased/ENHANCEMENTS-20240227-112633.yaml new file mode 100644 index 000000000..3bb29eaec --- /dev/null +++ b/.changes/unreleased/ENHANCEMENTS-20240227-112633.yaml @@ -0,0 +1,6 @@ +kind: ENHANCEMENTS +body: 'function: Added `NewFuncError()` and `NewArgumentFuncError()` functions, which + create a `FuncError`' +time: 2024-02-27T11:26:33.856219Z +custom: + Issue: "925" diff --git a/.changes/unreleased/ENHANCEMENTS-20240227-112752.yaml b/.changes/unreleased/ENHANCEMENTS-20240227-112752.yaml new file mode 100644 index 000000000..a93ebe2b4 --- /dev/null +++ b/.changes/unreleased/ENHANCEMENTS-20240227-112752.yaml @@ -0,0 +1,6 @@ +kind: ENHANCEMENTS +body: 'function: Added `ConcatFuncErrors()` and `FuncErrorFromDiags()` helper functions + for use when working with `FuncError`' +time: 2024-02-27T11:27:52.288519Z +custom: + Issue: "925" diff --git a/diag/argument_error_diagnostic.go b/diag/argument_error_diagnostic.go deleted file mode 100644 index 1b2d2956e..000000000 --- a/diag/argument_error_diagnostic.go +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package diag - -// NewArgumentErrorDiagnostic returns a new error severity diagnostic with the -// given summary, detail, and function argument. -func NewArgumentErrorDiagnostic(functionArgument int, summary string, detail string) DiagnosticWithFunctionArgument { - return withFunctionArgument{ - Diagnostic: NewErrorDiagnostic(summary, detail), - functionArgument: functionArgument, - } -} diff --git a/diag/argument_warning_diagnostic.go b/diag/argument_warning_diagnostic.go deleted file mode 100644 index 895ad9a66..000000000 --- a/diag/argument_warning_diagnostic.go +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package diag - -// NewArgumentWarningDiagnostic returns a new warning severity diagnostic with -// the given summary, detail, and function argument. -func NewArgumentWarningDiagnostic(functionArgument int, summary string, detail string) DiagnosticWithFunctionArgument { - return withFunctionArgument{ - Diagnostic: NewWarningDiagnostic(summary, detail), - functionArgument: functionArgument, - } -} diff --git a/diag/diagnostic.go b/diag/diagnostic.go index 080800452..74af0143e 100644 --- a/diag/diagnostic.go +++ b/diag/diagnostic.go @@ -38,21 +38,6 @@ type Diagnostic interface { Equal(Diagnostic) bool } -// DiagnosticWithFunctionArgument is a diagnostic associated with a -// function argument. -// -// This information is used to display contextual source configuration to -// practitioners. -type DiagnosticWithFunctionArgument interface { - Diagnostic - - // FunctionArgument points to a specific function argument position. - // - // If present, this enables the display of source configuration context for - // supporting implementations such as Terraform CLI commands. - FunctionArgument() int -} - // DiagnosticWithPath is a diagnostic associated with an attribute path. // // This attribute information is used to display contextual source configuration diff --git a/diag/diagnostics.go b/diag/diagnostics.go index e092f9266..3cd99cbf8 100644 --- a/diag/diagnostics.go +++ b/diag/diagnostics.go @@ -13,18 +13,6 @@ import ( // or consistent. type Diagnostics []Diagnostic -// AddArgumentError adds a generic function argument error diagnostic to the -// collection. -func (diags *Diagnostics) AddArgumentError(position int, summary string, detail string) { - diags.Append(NewArgumentErrorDiagnostic(position, summary, detail)) -} - -// AddArgumentWarning adds a function argument warning diagnostic to the -// collection. -func (diags *Diagnostics) AddArgumentWarning(position int, summary string, detail string) { - diags.Append(NewArgumentWarningDiagnostic(position, summary, detail)) -} - // AddAttributeError adds a generic attribute error diagnostic to the collection. func (diags *Diagnostics) AddAttributeError(path path.Path, summary string, detail string) { diags.Append(NewAttributeErrorDiagnostic(path, summary, detail)) diff --git a/diag/diagnostics_test.go b/diag/diagnostics_test.go index b34b24767..82def641e 100644 --- a/diag/diagnostics_test.go +++ b/diag/diagnostics_test.go @@ -12,130 +12,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" ) -func TestDiagnosticsAddArgumentError(t *testing.T) { - t.Parallel() - - testCases := map[string]struct { - diags diag.Diagnostics - position int - summary string - detail string - expected diag.Diagnostics - }{ - "nil-add": { - diags: nil, - position: 0, - summary: "one summary", - detail: "one detail", - expected: diag.Diagnostics{ - diag.NewArgumentErrorDiagnostic(0, "one summary", "one detail"), - }, - }, - "add": { - diags: diag.Diagnostics{ - diag.NewArgumentErrorDiagnostic(0, "one summary", "one detail"), - diag.NewArgumentWarningDiagnostic(0, "two summary", "two detail"), - }, - position: 0, - summary: "three summary", - detail: "three detail", - expected: diag.Diagnostics{ - diag.NewArgumentErrorDiagnostic(0, "one summary", "one detail"), - diag.NewArgumentWarningDiagnostic(0, "two summary", "two detail"), - diag.NewArgumentErrorDiagnostic(0, "three summary", "three detail"), - }, - }, - "duplicate": { - diags: diag.Diagnostics{ - diag.NewArgumentErrorDiagnostic(0, "one summary", "one detail"), - diag.NewArgumentWarningDiagnostic(0, "two summary", "two detail"), - }, - position: 0, - summary: "one summary", - detail: "one detail", - expected: diag.Diagnostics{ - diag.NewArgumentErrorDiagnostic(0, "one summary", "one detail"), - diag.NewArgumentWarningDiagnostic(0, "two summary", "two detail"), - }, - }, - } - - for name, tc := range testCases { - name, tc := name, tc - t.Run(name, func(t *testing.T) { - t.Parallel() - - tc.diags.AddArgumentError(tc.position, tc.summary, tc.detail) - - if diff := cmp.Diff(tc.diags, tc.expected); diff != "" { - t.Errorf("Unexpected response (+wanted, -got): %s", diff) - } - }) - } -} - -func TestDiagnosticsAddArgumentWarning(t *testing.T) { - t.Parallel() - - testCases := map[string]struct { - diags diag.Diagnostics - position int - summary string - detail string - expected diag.Diagnostics - }{ - "nil-add": { - diags: nil, - position: 0, - summary: "one summary", - detail: "one detail", - expected: diag.Diagnostics{ - diag.NewArgumentWarningDiagnostic(0, "one summary", "one detail"), - }, - }, - "add": { - diags: diag.Diagnostics{ - diag.NewArgumentErrorDiagnostic(0, "one summary", "one detail"), - diag.NewArgumentWarningDiagnostic(0, "two summary", "two detail"), - }, - position: 0, - summary: "three summary", - detail: "three detail", - expected: diag.Diagnostics{ - diag.NewArgumentErrorDiagnostic(0, "one summary", "one detail"), - diag.NewArgumentWarningDiagnostic(0, "two summary", "two detail"), - diag.NewArgumentWarningDiagnostic(0, "three summary", "three detail"), - }, - }, - "duplicate": { - diags: diag.Diagnostics{ - diag.NewArgumentErrorDiagnostic(0, "one summary", "one detail"), - diag.NewArgumentWarningDiagnostic(0, "two summary", "two detail"), - }, - position: 0, - summary: "two summary", - detail: "two detail", - expected: diag.Diagnostics{ - diag.NewArgumentErrorDiagnostic(0, "one summary", "one detail"), - diag.NewArgumentWarningDiagnostic(0, "two summary", "two detail"), - }, - }, - } - - for name, tc := range testCases { - name, tc := name, tc - t.Run(name, func(t *testing.T) { - t.Parallel() - - tc.diags.AddArgumentWarning(tc.position, tc.summary, tc.detail) - - if diff := cmp.Diff(tc.diags, tc.expected); diff != "" { - t.Errorf("Unexpected response (+wanted, -got): %s", diff) - } - }) - } -} - func TestDiagnosticsAddAttributeError(t *testing.T) { t.Parallel() diff --git a/diag/doc.go b/diag/doc.go index 79f820d58..95e332d6d 100644 --- a/diag/doc.go +++ b/diag/doc.go @@ -5,4 +5,7 @@ // feedback mechanism for providers. It is designed for display in Terraform // user interfaces, rather than logging based feedback, which is generally // saved to a file for later inspection and troubleshooting. +// +// Practitioner feedback for provider defined functions is provided by the +// [function.FuncError] type, rather than the [diag.Diagnostic] type. package diag diff --git a/diag/with_function_argument.go b/diag/with_function_argument.go deleted file mode 100644 index 715b00732..000000000 --- a/diag/with_function_argument.go +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package diag - -var _ DiagnosticWithFunctionArgument = withFunctionArgument{} - -// withFunctionArgument wraps a diagnostic with function argument information. -type withFunctionArgument struct { - Diagnostic - - functionArgument int -} - -// Equal returns true if the other diagnostic is wholly equivalent. -func (d withFunctionArgument) Equal(other Diagnostic) bool { - o, ok := other.(withFunctionArgument) - - if !ok { - return false - } - - if d.functionArgument != o.functionArgument { - return false - } - - if d.Diagnostic == nil { - return d.Diagnostic == o.Diagnostic - } - - return d.Diagnostic.Equal(o.Diagnostic) -} - -// FunctionArgument returns the diagnostic function argument. -func (d withFunctionArgument) FunctionArgument() int { - return d.functionArgument -} - -// WithFunctionArgument wraps a diagnostic with function argument information -// or overwrites the function argument. -func WithFunctionArgument(functionArgument int, d Diagnostic) DiagnosticWithFunctionArgument { - wp, ok := d.(withFunctionArgument) - - if !ok { - return withFunctionArgument{ - Diagnostic: d, - functionArgument: functionArgument, - } - } - - wp.functionArgument = functionArgument - - return wp -} diff --git a/function/arguments_data.go b/function/arguments_data.go index 7c6e36321..40b4e00de 100644 --- a/function/arguments_data.go +++ b/function/arguments_data.go @@ -8,7 +8,6 @@ import ( "fmt" "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" fwreflect "github.com/hashicorp/terraform-plugin-framework/internal/reflect" "github.com/hashicorp/terraform-plugin-framework/path" ) @@ -49,30 +48,28 @@ func (d ArgumentsData) Equal(o ArgumentsData) bool { // type with an element type appropriate for the parameter definition ([]T). The // framework automatically populates this tuple with elements matching the zero, // one, or more arguments passed. -func (d ArgumentsData) Get(ctx context.Context, targets ...any) diag.Diagnostics { - var diags diag.Diagnostics +func (d ArgumentsData) Get(ctx context.Context, targets ...any) *FuncError { + var funcErr *FuncError if len(d.values) == 0 { - diags.AddError( - "Invalid Argument Data Usage", - "When attempting to fetch argument data during the function call, the provider code incorrectly attempted to read argument data. "+ - "This is always an issue in the provider code and should be reported to the provider developers.\n\n"+ - "Function does not have argument data.", - ) - - return diags + errMsg := "Invalid Argument Data Usage: When attempting to fetch argument data during the function call, the provider code incorrectly attempted to read argument data. " + + "This is always an issue in the provider code and should be reported to the provider developers.\n\n" + + "Function does not have argument data." + + funcErr = ConcatFuncErrors(funcErr, NewFuncError(errMsg)) + + return funcErr } if len(targets) != len(d.values) { - diags.AddError( - "Invalid Argument Data Usage", - "When attempting to fetch argument data during the function call, the provider code incorrectly attempted to read argument data. "+ - "The Get call requires all parameters and the final variadic parameter, if implemented, to be in the targets. "+ - "This is always an error in the provider code and should be reported to the provider developers.\n\n"+ - fmt.Sprintf("Given targets count: %d, expected targets count: %d", len(targets), len(d.values)), - ) + errMsg := "Invalid Argument Data Usage: When attempting to fetch argument data during the function call, the provider code incorrectly attempted to read argument data. " + + "The Get call requires all parameters and the final variadic parameter, if implemented, to be in the targets. " + + "This is always an error in the provider code and should be reported to the provider developers.\n\n" + + fmt.Sprintf("Given targets count: %d, expected targets count: %d", len(targets), len(d.values)) + + funcErr = ConcatFuncErrors(funcErr, NewFuncError(errMsg)) - return diags + return funcErr } for position, attrValue := range d.values { @@ -85,27 +82,26 @@ func (d ArgumentsData) Get(ctx context.Context, targets ...any) diag.Diagnostics continue } - tfValue, err := attrValue.ToTerraformValue(ctx) + tfValue, tfValueErr := attrValue.ToTerraformValue(ctx) - if err != nil { - diags.AddError( - "Argument Value Conversion Error", - fmt.Sprintf("An unexpected error was encountered converting a %T to its equivalent Terraform representation. "+ - "This is always an error in the provider code and should be reported to the provider developers.\n\n"+ - "Position: %d\n"+ - "Error: %s", - attrValue, position, err), - ) + if tfValueErr != nil { + errMsg := fmt.Sprintf("Argument Value Conversion Error: An unexpected error was encountered converting a %T to its equivalent Terraform representation. "+ + "This is always an error in the provider code and should be reported to the provider developers.\n\n"+ + "Position: %d\n"+ + "Error: %s", + attrValue, position, tfValueErr) + + funcErr = ConcatFuncErrors(funcErr, NewArgumentFuncError(int64(position), errMsg)) continue } reflectDiags := fwreflect.Into(ctx, attrValue.Type(ctx), tfValue, target, fwreflect.Options{}, path.Empty()) - diags.Append(reflectDiags...) + funcErr = ConcatFuncErrors(funcErr, FuncErrorFromDiags(ctx, reflectDiags)) } - return diags + return funcErr } // GetArgument retrieves the argument data found at the given zero-based @@ -116,30 +112,28 @@ func (d ArgumentsData) Get(ctx context.Context, targets ...any) diag.Diagnostics // type with an element type appropriate for the parameter definition ([]T) at // the position after all parameters. The framework automatically populates this // tuple with elements matching the zero, one, or more arguments passed. -func (d ArgumentsData) GetArgument(ctx context.Context, position int, target any) diag.Diagnostics { - var diags diag.Diagnostics +func (d ArgumentsData) GetArgument(ctx context.Context, position int, target any) *FuncError { + var funcErr *FuncError if len(d.values) == 0 { - diags.AddError( - "Invalid Argument Data Usage", - "When attempting to fetch argument data during the function call, the provider code incorrectly attempted to read argument data. "+ - "This is always an issue in the provider code and should be reported to the provider developers.\n\n"+ - "Function does not have argument data.", - ) - - return diags + errMsg := "Invalid Argument Data Usage: When attempting to fetch argument data during the function call, the provider code incorrectly attempted to read argument data. " + + "This is always an issue in the provider code and should be reported to the provider developers.\n\n" + + "Function does not have argument data." + + funcErr = ConcatFuncErrors(funcErr, NewArgumentFuncError(int64(position), errMsg)) + + return funcErr } if position >= len(d.values) { - diags.AddError( - "Invalid Argument Data Position", - "When attempting to fetch argument data during the function call, the provider code attempted to read a non-existent argument position. "+ - "Function argument positions are 0-based and any final variadic parameter is represented as one argument position with a tuple where each element "+ - "type matches the parameter data type. This is always an error in the provider code and should be reported to the provider developers.\n\n"+ - fmt.Sprintf("Given argument position: %d, last argument position: %d", position, len(d.values)-1), - ) - - return diags + errMsg := "Invalid Argument Data Position: When attempting to fetch argument data during the function call, the provider code attempted to read a non-existent argument position. " + + "Function argument positions are 0-based and any final variadic parameter is represented as one argument position with a tuple where each element " + + "type matches the parameter data type. This is always an error in the provider code and should be reported to the provider developers.\n\n" + + fmt.Sprintf("Given argument position: %d, last argument position: %d", position, len(d.values)-1) + + funcErr = ConcatFuncErrors(funcErr, NewArgumentFuncError(int64(position), errMsg)) + + return funcErr } attrValue := d.values[position] @@ -154,20 +148,20 @@ func (d ArgumentsData) GetArgument(ctx context.Context, position int, target any tfValue, err := attrValue.ToTerraformValue(ctx) if err != nil { - diags.AddError( - "Argument Value Conversion Error", - fmt.Sprintf("An unexpected error was encountered converting a %T to its equivalent Terraform representation. "+ - "This is always an error in the provider code and should be reported to the provider developers.\n\n"+ - "Error: %s", attrValue, err), - ) - return diags + errMsg := fmt.Sprintf("Argument Value Conversion Error: An unexpected error was encountered converting a %T to its equivalent Terraform representation. "+ + "This is always an error in the provider code and should be reported to the provider developers.\n\n"+ + "Error: %s", attrValue, err) + + funcErr = ConcatFuncErrors(funcErr, NewArgumentFuncError(int64(position), errMsg)) + + return funcErr } reflectDiags := fwreflect.Into(ctx, attrValue.Type(ctx), tfValue, target, fwreflect.Options{}, path.Empty()) - diags.Append(reflectDiags...) + funcErr = ConcatFuncErrors(funcErr, FuncErrorFromDiags(ctx, reflectDiags)) - return diags + return funcErr } // NewArgumentsData creates an ArgumentsData. This is only necessary for unit diff --git a/function/arguments_data_test.go b/function/arguments_data_test.go index 01ab7b29b..a4fdfe62a 100644 --- a/function/arguments_data_test.go +++ b/function/arguments_data_test.go @@ -5,15 +5,12 @@ package function_test import ( "context" - "reflect" "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/function" - fwreflect "github.com/hashicorp/terraform-plugin-framework/internal/reflect" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" ) @@ -88,23 +85,18 @@ func TestArgumentsDataGet(t *testing.T) { t.Parallel() testCases := map[string]struct { - argumentsData function.ArgumentsData - targets []any - expected []any - expectedDiagnotics diag.Diagnostics + argumentsData function.ArgumentsData + targets []any + expected []any + expectedErr *function.FuncError }{ "no-argument-data": { argumentsData: function.NewArgumentsData(nil), targets: []any{new(bool)}, expected: []any{new(bool)}, - expectedDiagnotics: diag.Diagnostics{ - diag.NewErrorDiagnostic( - "Invalid Argument Data Usage", - "When attempting to fetch argument data during the function call, the provider code incorrectly attempted to read argument data. "+ - "This is always an issue in the provider code and should be reported to the provider developers.\n\n"+ - "Function does not have argument data.", - ), - }, + expectedErr: function.NewFuncError("Invalid Argument Data Usage: When attempting to fetch argument data during the function call, the provider code incorrectly attempted to read argument data. " + + "This is always an issue in the provider code and should be reported to the provider developers.\n\n" + + "Function does not have argument data."), }, "invalid-targets-too-few": { argumentsData: function.NewArgumentsData([]attr.Value{ @@ -113,15 +105,10 @@ func TestArgumentsDataGet(t *testing.T) { }), targets: []any{new(bool)}, expected: []any{new(bool)}, - expectedDiagnotics: diag.Diagnostics{ - diag.NewErrorDiagnostic( - "Invalid Argument Data Usage", - "When attempting to fetch argument data during the function call, the provider code incorrectly attempted to read argument data. "+ - "The Get call requires all parameters and the final variadic parameter, if implemented, to be in the targets. "+ - "This is always an error in the provider code and should be reported to the provider developers.\n\n"+ - "Given targets count: 1, expected targets count: 2", - ), - }, + expectedErr: function.NewFuncError("Invalid Argument Data Usage: When attempting to fetch argument data during the function call, the provider code incorrectly attempted to read argument data. " + + "The Get call requires all parameters and the final variadic parameter, if implemented, to be in the targets. " + + "This is always an error in the provider code and should be reported to the provider developers.\n\n" + + "Given targets count: 1, expected targets count: 2"), }, "invalid-targets-too-many": { argumentsData: function.NewArgumentsData([]attr.Value{ @@ -129,15 +116,10 @@ func TestArgumentsDataGet(t *testing.T) { }), targets: []any{new(bool), new(bool)}, expected: []any{new(bool), new(bool)}, - expectedDiagnotics: diag.Diagnostics{ - diag.NewErrorDiagnostic( - "Invalid Argument Data Usage", - "When attempting to fetch argument data during the function call, the provider code incorrectly attempted to read argument data. "+ - "The Get call requires all parameters and the final variadic parameter, if implemented, to be in the targets. "+ - "This is always an error in the provider code and should be reported to the provider developers.\n\n"+ - "Given targets count: 2, expected targets count: 1", - ), - }, + expectedErr: function.NewFuncError("Invalid Argument Data Usage: When attempting to fetch argument data during the function call, the provider code incorrectly attempted to read argument data. " + + "The Get call requires all parameters and the final variadic parameter, if implemented, to be in the targets. " + + "This is always an error in the provider code and should be reported to the provider developers.\n\n" + + "Given targets count: 2, expected targets count: 1"), }, "invalid-target": { argumentsData: function.NewArgumentsData([]attr.Value{ @@ -145,16 +127,9 @@ func TestArgumentsDataGet(t *testing.T) { }), targets: []any{new(basetypes.StringValue)}, expected: []any{new(basetypes.StringValue)}, - expectedDiagnotics: diag.Diagnostics{ - diag.WithPath( - path.Empty(), - fwreflect.DiagNewAttributeValueIntoWrongType{ - ValType: reflect.TypeOf(basetypes.BoolValue{}), - TargetType: reflect.TypeOf(basetypes.StringValue{}), - SchemaType: basetypes.BoolType{}, - }, - ), - }, + expectedErr: function.NewFuncError("Value Conversion Error: An unexpected error was encountered trying to convert into a Terraform value. " + + "This is always an error in the provider. Please report the following to the provider developer:\n\n" + + "Cannot use attr.Value basetypes.StringValue, only basetypes.BoolValue is supported because basetypes.BoolType is the type in the schema"), }, "attr-value": { argumentsData: function.NewArgumentsData([]attr.Value{ @@ -332,7 +307,7 @@ func TestArgumentsDataGet(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() - diags := testCase.argumentsData.Get(context.Background(), testCase.targets...) + err := testCase.argumentsData.Get(context.Background(), testCase.targets...) // Prevent awkwardness with comparing pointers in []any options := cmp.Options{ @@ -354,7 +329,7 @@ func TestArgumentsDataGet(t *testing.T) { t.Errorf("unexpected difference: %s", diff) } - if diff := cmp.Diff(diags, testCase.expectedDiagnotics); diff != "" { + if diff := cmp.Diff(err, testCase.expectedErr); diff != "" { t.Errorf("unexpected diagnostics difference: %s", diff) } }) @@ -365,25 +340,20 @@ func TestArgumentsDataGetArgument(t *testing.T) { t.Parallel() testCases := map[string]struct { - argumentsData function.ArgumentsData - position int - target any - expected any - expectedDiagnotics diag.Diagnostics + argumentsData function.ArgumentsData + position int + target any + expected any + expectedErr *function.FuncError }{ "no-argument-data": { argumentsData: function.NewArgumentsData(nil), position: 0, target: new(bool), expected: new(bool), - expectedDiagnotics: diag.Diagnostics{ - diag.NewErrorDiagnostic( - "Invalid Argument Data Usage", - "When attempting to fetch argument data during the function call, the provider code incorrectly attempted to read argument data. "+ - "This is always an issue in the provider code and should be reported to the provider developers.\n\n"+ - "Function does not have argument data.", - ), - }, + expectedErr: function.NewArgumentFuncError(int64(0), "Invalid Argument Data Usage: When attempting to fetch argument data during the function call, the provider code incorrectly attempted to read argument data. "+ + "This is always an issue in the provider code and should be reported to the provider developers.\n\n"+ + "Function does not have argument data."), }, "invalid-position": { argumentsData: function.NewArgumentsData([]attr.Value{ @@ -392,15 +362,11 @@ func TestArgumentsDataGetArgument(t *testing.T) { position: 1, target: new(bool), expected: new(bool), - expectedDiagnotics: diag.Diagnostics{ - diag.NewErrorDiagnostic( - "Invalid Argument Data Position", - "When attempting to fetch argument data during the function call, the provider code attempted to read a non-existent argument position. "+ - "Function argument positions are 0-based and any final variadic parameter is represented as one argument position with a tuple where each element "+ - "type matches the parameter data type. This is always an error in the provider code and should be reported to the provider developers.\n\n"+ - "Given argument position: 1, last argument position: 0", - ), - }, + expectedErr: function.NewArgumentFuncError(int64(1), "Invalid Argument Data Position: When attempting to fetch argument data during the function call, the provider code attempted to read a non-existent argument position. "+ + "Function argument positions are 0-based and any final variadic parameter is represented as one argument position with a tuple where each element "+ + "type matches the parameter data type. This is always an error in the provider code and should be reported to the provider developers.\n\n"+ + "Given argument position: 1, last argument position: 0", + ), }, "invalid-target": { argumentsData: function.NewArgumentsData([]attr.Value{ @@ -409,16 +375,9 @@ func TestArgumentsDataGetArgument(t *testing.T) { position: 0, target: new(basetypes.StringValue), expected: new(basetypes.StringValue), - expectedDiagnotics: diag.Diagnostics{ - diag.WithPath( - path.Empty(), - fwreflect.DiagNewAttributeValueIntoWrongType{ - ValType: reflect.TypeOf(basetypes.BoolValue{}), - TargetType: reflect.TypeOf(basetypes.StringValue{}), - SchemaType: basetypes.BoolType{}, - }, - ), - }, + expectedErr: function.NewFuncError("Value Conversion Error: An unexpected error was encountered trying to convert into a Terraform value. " + + "This is always an error in the provider. Please report the following to the provider developer:\n\n" + + "Cannot use attr.Value basetypes.StringValue, only basetypes.BoolValue is supported because basetypes.BoolType is the type in the schema"), }, "attr-value": { argumentsData: function.NewArgumentsData([]attr.Value{ @@ -452,7 +411,7 @@ func TestArgumentsDataGetArgument(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() - diags := testCase.argumentsData.GetArgument(context.Background(), testCase.position, testCase.target) + err := testCase.argumentsData.GetArgument(context.Background(), testCase.position, testCase.target) // Prevent awkwardness with comparing empty interface pointers options := cmp.Options{ @@ -468,7 +427,7 @@ func TestArgumentsDataGetArgument(t *testing.T) { t.Errorf("unexpected difference: %s", diff) } - if diff := cmp.Diff(diags, testCase.expectedDiagnotics); diff != "" { + if diff := cmp.Diff(err, testCase.expectedErr); diff != "" { t.Errorf("unexpected diagnostics difference: %s", diff) } }) diff --git a/function/bool_return.go b/function/bool_return.go index 9ef1d138f..0410b38ee 100644 --- a/function/bool_return.go +++ b/function/bool_return.go @@ -7,7 +7,6 @@ import ( "context" "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" ) @@ -38,7 +37,7 @@ func (r BoolReturn) GetType() attr.Type { } // NewResultData returns a new result data based on the type. -func (r BoolReturn) NewResultData(ctx context.Context) (ResultData, diag.Diagnostics) { +func (r BoolReturn) NewResultData(ctx context.Context) (ResultData, *FuncError) { value := basetypes.NewBoolUnknown() if r.CustomType == nil { @@ -47,5 +46,5 @@ func (r BoolReturn) NewResultData(ctx context.Context) (ResultData, diag.Diagnos valuable, diags := r.CustomType.ValueFromBool(ctx, value) - return NewResultData(valuable), diags + return NewResultData(valuable), FuncErrorFromDiags(ctx, diags) } diff --git a/function/doc.go b/function/doc.go index 419b408b5..2c71069b9 100644 --- a/function/doc.go +++ b/function/doc.go @@ -15,4 +15,7 @@ // argument data when called. The [Function] implementations are referenced by a // [provider.Provider] type Functions method, which enables the function for // practitioner and testing usage. +// +// Practitioner feedback is provided by the [FuncError] type, rather than +// the [diag.Diagnostic] type. package function diff --git a/function/float64_return.go b/function/float64_return.go index aa204d054..e653c2d3a 100644 --- a/function/float64_return.go +++ b/function/float64_return.go @@ -7,7 +7,6 @@ import ( "context" "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" ) @@ -39,7 +38,7 @@ func (r Float64Return) GetType() attr.Type { } // NewResultData returns a new result data based on the type. -func (r Float64Return) NewResultData(ctx context.Context) (ResultData, diag.Diagnostics) { +func (r Float64Return) NewResultData(ctx context.Context) (ResultData, *FuncError) { value := basetypes.NewFloat64Unknown() if r.CustomType == nil { @@ -48,5 +47,5 @@ func (r Float64Return) NewResultData(ctx context.Context) (ResultData, diag.Diag valuable, diags := r.CustomType.ValueFromFloat64(ctx, value) - return NewResultData(valuable), diags + return NewResultData(valuable), FuncErrorFromDiags(ctx, diags) } diff --git a/function/func_error.go b/function/func_error.go new file mode 100644 index 000000000..78da0053b --- /dev/null +++ b/function/func_error.go @@ -0,0 +1,125 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-log/tflog" + + "github.com/hashicorp/terraform-plugin-framework/diag" +) + +// NewFuncError returns a new function error with the +// given message. +func NewFuncError(text string) *FuncError { + return &FuncError{ + Text: text, + } +} + +// NewArgumentFuncError returns a new function error with the +// given message and function argument. +func NewArgumentFuncError(functionArgument int64, text string) *FuncError { + return &FuncError{ + Text: text, + FunctionArgument: &functionArgument, + } +} + +// FuncError is an error type specifically for function errors. +type FuncError struct { + // Text is a practitioner-oriented description of the problem. This should + // contain sufficient detail to provide both general and more specific information + // regarding the issue. For example "Error executing function: foo can only contain + // letters, numbers, and digits." + Text string + // FunctionArgument is a zero-based, int64 value that identifies the specific + // function argument position that caused the error. Only errors that pertain + // to a function argument will include this information. + FunctionArgument *int64 +} + +// Equal returns true if the other function error is wholly equivalent. +func (fe *FuncError) Equal(other *FuncError) bool { + if fe == nil && other == nil { + return true + } + + if fe == nil || other == nil { + return false + } + + if fe.Text != other.Text { + return false + } + + if fe.FunctionArgument == nil && other.FunctionArgument == nil { + return true + } + + if fe.FunctionArgument == nil || other.FunctionArgument == nil { + return false + } + + return *fe.FunctionArgument == *other.FunctionArgument +} + +// Error returns the error text. +func (fe *FuncError) Error() string { + return fe.Text +} + +// ConcatFuncErrors returns a new function error with the text from all supplied +// function errors concatenated together. If any of the function errors have a +// function argument, the first one encountered will be used. +func ConcatFuncErrors(funcErrs ...*FuncError) *FuncError { + var text string + var functionArgument *int64 + + for _, f := range funcErrs { + if f == nil { + continue + } + + if text != "" && f.Text != "" { + text += "\n" + } + + text += f.Text + + if functionArgument == nil { + functionArgument = f.FunctionArgument + } + } + + if text != "" || functionArgument != nil { + return &FuncError{ + Text: text, + FunctionArgument: functionArgument, + } + } + + return nil +} + +// FuncErrorFromDiags iterates over the given diagnostics and returns a new function error +// with the summary and detail text from all error diagnostics concatenated together. +// Diagnostics with a severity of warning are logged but are not included in the returned +// function error. +func FuncErrorFromDiags(ctx context.Context, diags diag.Diagnostics) *FuncError { + var funcErr *FuncError + + for _, d := range diags { + switch d.Severity() { + case diag.SeverityError: + funcErr = ConcatFuncErrors(funcErr, NewFuncError(fmt.Sprintf("%s: %s", d.Summary(), d.Detail()))) + case diag.SeverityWarning: + tflog.Warn(ctx, "warning: call function", map[string]interface{}{"summary": d.Summary(), "detail": d.Detail()}) + } + } + + return funcErr +} diff --git a/function/func_error_test.go b/function/func_error_test.go new file mode 100644 index 000000000..b5e7816c7 --- /dev/null +++ b/function/func_error_test.go @@ -0,0 +1,303 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function_test + +import ( + "bytes" + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-log/tflogtest" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/function" +) + +func TestFunctionError_Equal(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + funcErr *function.FuncError + other *function.FuncError + expected bool + }{ + "nil-nil": { + expected: true, + }, + "empty-nil": { + funcErr: &function.FuncError{}, + other: nil, + expected: false, + }, + "nil-empty": { + funcErr: nil, + other: &function.FuncError{}, + expected: false, + }, + "error-nil": { + funcErr: function.NewFuncError("test summary: test detail"), + other: nil, + expected: false, + }, + "nil-error": { + funcErr: nil, + other: function.NewFuncError("test summary: test detail"), + expected: false, + }, + "different-text": { + funcErr: function.NewFuncError("test summary: test detail"), + other: function.NewFuncError("test summary: different detail"), + expected: false, + }, + "different-type": { + funcErr: function.NewFuncError("test summary: test detail"), + other: function.NewArgumentFuncError(int64(0), "test summary: test detail"), + expected: false, + }, + "different-argument": { + funcErr: function.NewArgumentFuncError(int64(0), "test summary: test detail"), + other: function.NewArgumentFuncError(int64(1), "test summary: test detail"), + expected: false, + }, + "matching-text": { + funcErr: function.NewFuncError("test summary: test detail"), + other: function.NewFuncError("test summary: test detail"), + expected: true, + }, + "matching-argument": { + funcErr: function.NewArgumentFuncError(int64(0), "test summary: test detail"), + other: function.NewArgumentFuncError(int64(0), "test summary: test detail"), + expected: true, + }, + } + + for name, tc := range testCases { + name, tc := name, tc + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := tc.funcErr.Equal(tc.other) + + if got != tc.expected { + t.Errorf("Unexpected response: got: %t, wanted: %t", got, tc.expected) + } + }) + } +} + +func TestConcatFuncErrors(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + funcErr *function.FuncError + other *function.FuncError + expected *function.FuncError + }{ + "nil-nil": {}, + "empty-nil-slice": { + funcErr: &function.FuncError{}, + other: nil, + }, + "empty-empty": { + funcErr: &function.FuncError{}, + other: &function.FuncError{}, + }, + "text-nil": { + funcErr: &function.FuncError{ + Text: "function error one", + }, + other: nil, + expected: &function.FuncError{ + Text: "function error one", + }, + }, + "text-empty": { + funcErr: &function.FuncError{ + Text: "function error one", + }, + other: &function.FuncError{}, + expected: &function.FuncError{ + Text: "function error one", + }, + }, + "nil-text": { + funcErr: nil, + other: &function.FuncError{ + Text: "function error two", + }, + expected: &function.FuncError{ + Text: "function error two", + }, + }, + "empty-text": { + funcErr: &function.FuncError{}, + other: &function.FuncError{ + Text: "function error two", + }, + expected: &function.FuncError{ + Text: "function error two", + }, + }, + "text-text": { + funcErr: &function.FuncError{ + Text: "function error one", + }, + other: &function.FuncError{ + Text: "function error two", + }, + expected: &function.FuncError{ + Text: "function error one\nfunction error two", + }, + }, + "nil-argument": { + funcErr: nil, + other: &function.FuncError{ + FunctionArgument: pointer(int64(0)), + }, + expected: &function.FuncError{ + FunctionArgument: pointer(int64(0)), + }, + }, + "argument-nil": { + funcErr: &function.FuncError{ + FunctionArgument: pointer(int64(0)), + }, + other: nil, + expected: &function.FuncError{ + FunctionArgument: pointer(int64(0)), + }, + }, + "argument-precedence": { + funcErr: &function.FuncError{ + FunctionArgument: pointer(int64(0)), + }, + other: &function.FuncError{ + FunctionArgument: pointer(int64(1)), + }, + expected: &function.FuncError{ + FunctionArgument: pointer(int64(0)), + }, + }, + } + + for name, tc := range testCases { + name, tc := name, tc + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := function.ConcatFuncErrors(tc.funcErr, tc.other) + + if diff := cmp.Diff(got, tc.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFuncErrorFromDiags(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + diags diag.Diagnostics + expected *function.FuncError + expectedLog []map[string]interface{} + }{ + "nil": {}, + "empty": { + diags: diag.Diagnostics{}, + }, + "error": { + diags: diag.Diagnostics{ + diag.NewErrorDiagnostic("one summary", "one detail"), + }, + expected: &function.FuncError{ + Text: "one summary: one detail", + }, + }, + "warning": { + diags: diag.Diagnostics{ + diag.NewWarningDiagnostic("one summary", "one detail"), + }, + expectedLog: []map[string]interface{}{ + { + "@level": "warn", + "@message": "warning: call function", + "@module": "provider", + "detail": "one detail", + "summary": "one summary", + }, + }, + }, + "error-warning": { + diags: diag.Diagnostics{ + diag.NewErrorDiagnostic("one summary", "one detail"), + diag.NewWarningDiagnostic("two summary", "two detail"), + }, + expected: function.NewFuncError("one summary: one detail"), + expectedLog: []map[string]interface{}{ + { + "@level": "warn", + "@message": "warning: call function", + "@module": "provider", + "detail": "two detail", + "summary": "two summary", + }, + }, + }, + "multiple": { + diags: diag.Diagnostics{ + diag.NewErrorDiagnostic("one summary", "one detail"), + diag.NewWarningDiagnostic("two summary", "two detail"), + diag.NewErrorDiagnostic("three summary", "three detail"), + diag.NewWarningDiagnostic("four summary", "four detail"), + }, + expected: function.NewFuncError("one summary: one detail\nthree summary: three detail"), + expectedLog: []map[string]interface{}{ + { + "@level": "warn", + "@message": "warning: call function", + "@module": "provider", + "detail": "two detail", + "summary": "two summary", + }, + { + "@level": "warn", + "@message": "warning: call function", + "@module": "provider", + "detail": "four detail", + "summary": "four summary", + }, + }, + }, + } + + for name, tc := range testCases { + name, tc := name, tc + + t.Run(name, func(t *testing.T) { + t.Parallel() + + var output bytes.Buffer + + ctx := tflogtest.RootLogger(context.Background(), &output) + + got := function.FuncErrorFromDiags(ctx, tc.diags) + + entries, err := tflogtest.MultilineJSONDecode(&output) + + if err != nil { + t.Fatalf("unable to read multiple line JSON: %s", err) + } + + if diff := cmp.Diff(got, tc.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(entries, tc.expectedLog); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/function/int64_return.go b/function/int64_return.go index e634a5d1f..b7345b652 100644 --- a/function/int64_return.go +++ b/function/int64_return.go @@ -7,7 +7,6 @@ import ( "context" "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" ) @@ -38,7 +37,7 @@ func (r Int64Return) GetType() attr.Type { } // NewResultData returns a new result data based on the type. -func (r Int64Return) NewResultData(ctx context.Context) (ResultData, diag.Diagnostics) { +func (r Int64Return) NewResultData(ctx context.Context) (ResultData, *FuncError) { value := basetypes.NewInt64Unknown() if r.CustomType == nil { @@ -47,5 +46,5 @@ func (r Int64Return) NewResultData(ctx context.Context) (ResultData, diag.Diagno valuable, diags := r.CustomType.ValueFromInt64(ctx, value) - return NewResultData(valuable), diags + return NewResultData(valuable), FuncErrorFromDiags(ctx, diags) } diff --git a/function/list_return.go b/function/list_return.go index 582ddf88f..88238aee4 100644 --- a/function/list_return.go +++ b/function/list_return.go @@ -7,7 +7,6 @@ import ( "context" "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" ) @@ -46,7 +45,7 @@ func (r ListReturn) GetType() attr.Type { } // NewResultData returns a new result data based on the type. -func (r ListReturn) NewResultData(ctx context.Context) (ResultData, diag.Diagnostics) { +func (r ListReturn) NewResultData(ctx context.Context) (ResultData, *FuncError) { value := basetypes.NewListUnknown(r.ElementType) if r.CustomType == nil { @@ -55,5 +54,5 @@ func (r ListReturn) NewResultData(ctx context.Context) (ResultData, diag.Diagnos valuable, diags := r.CustomType.ValueFromList(ctx, value) - return NewResultData(valuable), diags + return NewResultData(valuable), FuncErrorFromDiags(ctx, diags) } diff --git a/function/map_return.go b/function/map_return.go index f12b595f1..afc8be0d8 100644 --- a/function/map_return.go +++ b/function/map_return.go @@ -7,7 +7,6 @@ import ( "context" "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" ) @@ -46,7 +45,7 @@ func (r MapReturn) GetType() attr.Type { } // NewResultData returns a new result data based on the type. -func (r MapReturn) NewResultData(ctx context.Context) (ResultData, diag.Diagnostics) { +func (r MapReturn) NewResultData(ctx context.Context) (ResultData, *FuncError) { value := basetypes.NewMapUnknown(r.ElementType) if r.CustomType == nil { @@ -55,5 +54,5 @@ func (r MapReturn) NewResultData(ctx context.Context) (ResultData, diag.Diagnost valuable, diags := r.CustomType.ValueFromMap(ctx, value) - return NewResultData(valuable), diags + return NewResultData(valuable), FuncErrorFromDiags(ctx, diags) } diff --git a/function/number_return.go b/function/number_return.go index 514997c9b..ad94cfead 100644 --- a/function/number_return.go +++ b/function/number_return.go @@ -7,7 +7,6 @@ import ( "context" "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" ) @@ -39,7 +38,7 @@ func (r NumberReturn) GetType() attr.Type { } // NewResultData returns a new result data based on the type. -func (r NumberReturn) NewResultData(ctx context.Context) (ResultData, diag.Diagnostics) { +func (r NumberReturn) NewResultData(ctx context.Context) (ResultData, *FuncError) { value := basetypes.NewNumberUnknown() if r.CustomType == nil { @@ -48,5 +47,5 @@ func (r NumberReturn) NewResultData(ctx context.Context) (ResultData, diag.Diagn valuable, diags := r.CustomType.ValueFromNumber(ctx, value) - return NewResultData(valuable), diags + return NewResultData(valuable), FuncErrorFromDiags(ctx, diags) } diff --git a/function/object_return.go b/function/object_return.go index 312f49b9b..526ab96f1 100644 --- a/function/object_return.go +++ b/function/object_return.go @@ -7,7 +7,6 @@ import ( "context" "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" ) @@ -42,7 +41,7 @@ func (r ObjectReturn) GetType() attr.Type { } // NewResultData returns a new result data based on the type. -func (r ObjectReturn) NewResultData(ctx context.Context) (ResultData, diag.Diagnostics) { +func (r ObjectReturn) NewResultData(ctx context.Context) (ResultData, *FuncError) { value := basetypes.NewObjectUnknown(r.AttributeTypes) if r.CustomType == nil { @@ -51,5 +50,5 @@ func (r ObjectReturn) NewResultData(ctx context.Context) (ResultData, diag.Diagn valuable, diags := r.CustomType.ValueFromObject(ctx, value) - return NewResultData(valuable), diags + return NewResultData(valuable), FuncErrorFromDiags(ctx, diags) } diff --git a/function/result_data.go b/function/result_data.go index 4cfad6a7d..ef8400abb 100644 --- a/function/result_data.go +++ b/function/result_data.go @@ -7,7 +7,6 @@ import ( "context" "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" fwreflect "github.com/hashicorp/terraform-plugin-framework/internal/reflect" "github.com/hashicorp/terraform-plugin-framework/path" ) @@ -32,20 +31,18 @@ func (d ResultData) Equal(o ResultData) bool { // Set saves the result data. The value type must be acceptable for the data // type in the result definition. -func (d *ResultData) Set(ctx context.Context, value any) diag.Diagnostics { - var diags diag.Diagnostics - +func (d *ResultData) Set(ctx context.Context, value any) *FuncError { reflectValue, reflectDiags := fwreflect.FromValue(ctx, d.value.Type(ctx), value, path.Empty()) - diags.Append(reflectDiags...) + funcErr := FuncErrorFromDiags(ctx, reflectDiags) - if diags.HasError() { - return diags + if funcErr != nil { + return funcErr } d.value = reflectValue - return diags + return nil } // Value returns the saved value. diff --git a/function/result_data_test.go b/function/result_data_test.go index 984a374c0..7bd59010a 100644 --- a/function/result_data_test.go +++ b/function/result_data_test.go @@ -8,10 +8,9 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/function" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" ) @@ -19,40 +18,28 @@ func TestResultDataSet(t *testing.T) { t.Parallel() testCases := map[string]struct { - resultData function.ResultData - value any - expected attr.Value - expectedDiagnostics diag.Diagnostics + resultData function.ResultData + value any + expected attr.Value + expectedErr *function.FuncError }{ "nil": { resultData: function.NewResultData(basetypes.NewBoolUnknown()), value: nil, expected: basetypes.NewBoolUnknown(), - expectedDiagnostics: diag.Diagnostics{ - diag.NewAttributeErrorDiagnostic( - path.Empty(), - "Value Conversion Error", - "An unexpected error was encountered trying to convert from value. "+ - "This is always an error in the provider. Please report the following to the provider developer:\n\n"+ - "cannot construct attr.Type from (invalid)", - ), - }, + expectedErr: function.NewFuncError("Value Conversion Error: An unexpected error was encountered trying to convert from value. " + + "This is always an error in the provider. Please report the following to the provider developer:\n\n" + + "cannot construct attr.Type from (invalid)"), }, "invalid-type": { resultData: function.NewResultData(basetypes.NewBoolUnknown()), value: basetypes.NewStringValue("test"), expected: basetypes.NewBoolUnknown(), - expectedDiagnostics: diag.Diagnostics{ - diag.NewAttributeErrorDiagnostic( - path.Empty(), - "Value Conversion Error", - "An unexpected error was encountered while verifying an attribute value matched its expected type to prevent unexpected behavior or panics. "+ - "This is always an error in the provider. Please report the following to the provider developer:\n\n"+ - "Expected framework type from provider logic: basetypes.BoolType / underlying type: tftypes.Bool\n"+ - "Received framework type from provider logic: basetypes.StringType / underlying type: tftypes.String\n"+ - "Path: ", - ), - }, + expectedErr: function.NewFuncError("Value Conversion Error: An unexpected error was encountered while verifying an attribute value matched its expected type to prevent unexpected behavior or panics. " + + "This is always an error in the provider. Please report the following to the provider developer:\n\n" + + "Expected framework type from provider logic: basetypes.BoolType / underlying type: tftypes.Bool\n" + + "Received framework type from provider logic: basetypes.StringType / underlying type: tftypes.String\n" + + "Path: "), }, "framework-type": { resultData: function.NewResultData(basetypes.NewBoolUnknown()), @@ -72,13 +59,13 @@ func TestResultDataSet(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() - diags := testCase.resultData.Set(context.Background(), testCase.value) + err := testCase.resultData.Set(context.Background(), testCase.value) if diff := cmp.Diff(testCase.resultData.Value(), testCase.expected); diff != "" { t.Errorf("unexpected difference: %s", diff) } - if diff := cmp.Diff(diags, testCase.expectedDiagnostics); diff != "" { + if diff := cmp.Diff(err, testCase.expectedErr); diff != "" { t.Errorf("unexpected diagnostics difference: %s", diff) } }) diff --git a/function/return.go b/function/return.go index ed7779df8..87c26a087 100644 --- a/function/return.go +++ b/function/return.go @@ -7,7 +7,6 @@ import ( "context" "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" ) // Return is the interface for defining function return data. @@ -22,5 +21,5 @@ type Return interface { // best approximation of an invalid value) of the corresponding data type. // The Function type Run method is expected to overwrite the value before // returning. - NewResultData(context.Context) (ResultData, diag.Diagnostics) + NewResultData(context.Context) (ResultData, *FuncError) } diff --git a/function/run.go b/function/run.go index 5c8f064af..05108a750 100644 --- a/function/run.go +++ b/function/run.go @@ -3,10 +3,6 @@ package function -import ( - "github.com/hashicorp/terraform-plugin-framework/diag" -) - // RunRequest represents a request for the Function to call its implementation // logic. An instance of this request struct is supplied as an argument to the // Function type Run method. @@ -19,9 +15,10 @@ type RunRequest struct { // RunResponse represents a response to a RunRequest. An instance of this // response struct is supplied as an argument to the Function type Run method. type RunResponse struct { - // Diagnostics report errors or warnings related to defining the function. - // An empty slice indicates success, with no warnings or errors generated. - Diagnostics diag.Diagnostics + // Error contains errors related to running the function. + // A nil error indicates success, with no errors generated. + // [ConcatFuncErrors] can be used to combine multiple errors into a single error. + Error *FuncError // Result is the data to be returned to Terraform matching the function // result definition. This must be set or an error diagnostic is raised. Use diff --git a/function/set_return.go b/function/set_return.go index b2c9f3236..97ea8e79c 100644 --- a/function/set_return.go +++ b/function/set_return.go @@ -7,7 +7,6 @@ import ( "context" "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" ) @@ -46,7 +45,7 @@ func (r SetReturn) GetType() attr.Type { } // NewResultData returns a new result data based on the type. -func (r SetReturn) NewResultData(ctx context.Context) (ResultData, diag.Diagnostics) { +func (r SetReturn) NewResultData(ctx context.Context) (ResultData, *FuncError) { value := basetypes.NewSetUnknown(r.ElementType) if r.CustomType == nil { @@ -55,5 +54,5 @@ func (r SetReturn) NewResultData(ctx context.Context) (ResultData, diag.Diagnost valuable, diags := r.CustomType.ValueFromSet(ctx, value) - return NewResultData(valuable), diags + return NewResultData(valuable), FuncErrorFromDiags(ctx, diags) } diff --git a/function/string_return.go b/function/string_return.go index bf5f63e13..73894b584 100644 --- a/function/string_return.go +++ b/function/string_return.go @@ -7,7 +7,6 @@ import ( "context" "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" ) @@ -38,7 +37,7 @@ func (r StringReturn) GetType() attr.Type { } // NewResultData returns a new result data based on the type. -func (r StringReturn) NewResultData(ctx context.Context) (ResultData, diag.Diagnostics) { +func (r StringReturn) NewResultData(ctx context.Context) (ResultData, *FuncError) { value := basetypes.NewStringUnknown() if r.CustomType == nil { @@ -47,5 +46,5 @@ func (r StringReturn) NewResultData(ctx context.Context) (ResultData, diag.Diagn valuable, diags := r.CustomType.ValueFromString(ctx, value) - return NewResultData(valuable), diags + return NewResultData(valuable), FuncErrorFromDiags(ctx, diags) } diff --git a/go.mod b/go.mod index 08400a844..ac56cf1b9 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,10 @@ module github.com/hashicorp/terraform-plugin-framework -go 1.20 +go 1.21 require ( github.com/google/go-cmp v0.6.0 - github.com/hashicorp/terraform-plugin-go v0.21.0 + github.com/hashicorp/terraform-plugin-go v0.22.0 github.com/hashicorp/terraform-plugin-log v0.9.0 ) @@ -27,6 +27,6 @@ require ( golang.org/x/sys v0.14.0 // indirect golang.org/x/text v0.14.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 // indirect - google.golang.org/grpc v1.61.0 // indirect + google.golang.org/grpc v1.61.1 // indirect google.golang.org/protobuf v1.32.0 // indirect ) diff --git a/go.sum b/go.sum index 8958bc3e3..451562807 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,5 @@ github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= +github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -16,8 +17,8 @@ github.com/hashicorp/go-plugin v1.6.0 h1:wgd4KxHJTVGGqWBq4QPB1i5BZNEx9BR8+OFmHDm github.com/hashicorp/go-plugin v1.6.0/go.mod h1:lBS5MtSSBZk0SHc66KACcjjlU6WzEVP/8pwz68aMkCI= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/terraform-plugin-go v0.21.0 h1:VSjdVQYNDKR0l2pi3vsFK1PdMQrw6vGOshJXMNFeVc0= -github.com/hashicorp/terraform-plugin-go v0.21.0/go.mod h1:piJp8UmO1uupCvC9/H74l2C6IyKG0rW4FDedIpwW5RQ= +github.com/hashicorp/terraform-plugin-go v0.22.0 h1:1OS1Jk5mO0f5hrziWJGXXIxBrMe2j/B8E+DVGw43Xmc= +github.com/hashicorp/terraform-plugin-go v0.22.0/go.mod h1:mPULV91VKss7sik6KFEcEu7HuTogMLLO/EvWCuFkRVE= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI= @@ -27,6 +28,7 @@ github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv2 github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= +github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= @@ -60,8 +62,8 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 h1:Jyp0Hsi0bmHXG6k9eATXoYtjd6e2UzZ1SCn/wIupY14= google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:oQ5rr10WTTMvP4A36n8JpR1OrO1BEiV4f78CneXZxkA= -google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0= -google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= +google.golang.org/grpc v1.61.1 h1:kLAiWrZs7YeDM6MumDe7m3y4aM6wacLzM1Y/wiLP9XY= +google.golang.org/grpc v1.61.1/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= diff --git a/internal/fwserver/server_callfunction.go b/internal/fwserver/server_callfunction.go index 36a19545d..99e164e4e 100644 --- a/internal/fwserver/server_callfunction.go +++ b/internal/fwserver/server_callfunction.go @@ -6,7 +6,6 @@ package fwserver import ( "context" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/internal/logging" ) @@ -22,8 +21,8 @@ type CallFunctionRequest struct { // CallFunctionResponse is the framework server response for the // CallFunction RPC. type CallFunctionResponse struct { - Diagnostics diag.Diagnostics - Result function.ResultData + Error *function.FuncError + Result function.ResultData } // CallFunction implements the framework server CallFunction RPC. @@ -32,11 +31,11 @@ func (s *Server) CallFunction(ctx context.Context, req *CallFunctionRequest, res return } - resultData, diags := req.FunctionDefinition.Return.NewResultData(ctx) + resultData, err := req.FunctionDefinition.Return.NewResultData(ctx) - resp.Diagnostics.Append(diags...) + resp.Error = function.ConcatFuncErrors(resp.Error, err) - if resp.Diagnostics.HasError() { + if resp.Error != nil { return } @@ -51,6 +50,7 @@ func (s *Server) CallFunction(ctx context.Context, req *CallFunctionRequest, res req.Function.Run(ctx, runReq, &runResp) logging.FrameworkTrace(ctx, "Called provider defined Function Run") - resp.Diagnostics = runResp.Diagnostics + resp.Error = function.ConcatFuncErrors(resp.Error, runResp.Error) + resp.Result = runResp.Result } diff --git a/internal/fwserver/server_callfunction_test.go b/internal/fwserver/server_callfunction_test.go index 5564f74cb..dfb3ee1cb 100644 --- a/internal/fwserver/server_callfunction_test.go +++ b/internal/fwserver/server_callfunction_test.go @@ -11,7 +11,6 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" @@ -48,34 +47,25 @@ func TestServerCallFunction(t *testing.T) { var arg1 basetypes.Int64Value var arg2 basetypes.StringValue - resp.Diagnostics.Append(req.Arguments.Get(ctx, &arg0, &arg1, &arg2)...) + resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.Get(ctx, &arg0, &arg1, &arg2)) expectedArg0 := basetypes.NewBoolNull() expectedArg1 := basetypes.NewInt64Unknown() expectedArg2 := basetypes.NewStringValue("arg2") if !arg0.Equal(expectedArg0) { - resp.Diagnostics.AddError( - "Unexpected Argument 0 Difference", - fmt.Sprintf("got: %s, expected: %s", arg0, expectedArg0), - ) + resp.Error = function.ConcatFuncErrors(resp.Error, function.NewFuncError(fmt.Sprintf("Unexpected Argument 0 Difference: got: %s, expected: %s", arg0, expectedArg0))) } if !arg1.Equal(expectedArg1) { - resp.Diagnostics.AddError( - "Unexpected Argument 1 Difference", - fmt.Sprintf("got: %s, expected: %s", arg1, expectedArg1), - ) + resp.Error = function.ConcatFuncErrors(resp.Error, function.NewFuncError(fmt.Sprintf("Unexpected Argument 1 Difference: got: %s, expected: %s", arg1, expectedArg1))) } if !arg2.Equal(expectedArg2) { - resp.Diagnostics.AddError( - "Unexpected Argument 2 Difference", - fmt.Sprintf("got: %s, expected: %s", arg2, expectedArg2), - ) + resp.Error = function.ConcatFuncErrors(resp.Error, function.NewFuncError(fmt.Sprintf("Unexpected Argument 2 Difference: got: %s, expected: %s", arg2, expectedArg2))) } - resp.Diagnostics.Append(resp.Result.Set(ctx, basetypes.NewStringValue("result"))...) + resp.Error = function.ConcatFuncErrors(resp.Error, resp.Result.Set(ctx, basetypes.NewStringValue("result"))) }, }, FunctionDefinition: function.Definition{ @@ -83,8 +73,8 @@ func TestServerCallFunction(t *testing.T) { }, }, expectedResponse: &fwserver.CallFunctionResponse{ - Diagnostics: nil, - Result: function.NewResultData(basetypes.NewStringValue("result")), + Error: nil, + Result: function.NewResultData(basetypes.NewStringValue("result")), }, }, "request-arguments-get-reflection": { @@ -101,25 +91,19 @@ func TestServerCallFunction(t *testing.T) { var arg0 string var arg1 *string - resp.Diagnostics.Append(req.Arguments.Get(ctx, &arg0, &arg1)...) + resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.Get(ctx, &arg0, &arg1)) expectedArg0 := "arg0" if arg0 != expectedArg0 { - resp.Diagnostics.AddError( - "Unexpected Argument 0 Difference", - fmt.Sprintf("got: %s, expected: %s", arg0, expectedArg0), - ) + resp.Error = function.ConcatFuncErrors(resp.Error, function.NewFuncError(fmt.Sprintf("Unexpected Argument 0 Difference: got: %s, expected: %s", arg0, expectedArg0))) } if arg1 != nil { - resp.Diagnostics.AddError( - "Unexpected Argument 1 Difference", - fmt.Sprintf("got: %s, expected: nil", *arg1), - ) + resp.Error = function.ConcatFuncErrors(resp.Error, function.NewFuncError(fmt.Sprintf("Unexpected Argument 1 Difference: got: %s, expected: nil", *arg1))) } - resp.Diagnostics.Append(resp.Result.Set(ctx, basetypes.NewStringValue("result"))...) + resp.Error = function.ConcatFuncErrors(resp.Error, resp.Result.Set(ctx, basetypes.NewStringValue("result"))) }, }, FunctionDefinition: function.Definition{ @@ -127,8 +111,8 @@ func TestServerCallFunction(t *testing.T) { }, }, expectedResponse: &fwserver.CallFunctionResponse{ - Diagnostics: nil, - Result: function.NewResultData(basetypes.NewStringValue("result")), + Error: nil, + Result: function.NewResultData(basetypes.NewStringValue("result")), }, }, "request-arguments-get-variadic": { @@ -154,7 +138,7 @@ func TestServerCallFunction(t *testing.T) { var arg0 basetypes.StringValue var arg1 basetypes.TupleValue - resp.Diagnostics.Append(req.Arguments.Get(ctx, &arg0, &arg1)...) + resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.Get(ctx, &arg0, &arg1)) expectedArg0 := basetypes.NewStringValue("arg0") expectedArg1 := basetypes.NewTupleValueMust( @@ -169,20 +153,14 @@ func TestServerCallFunction(t *testing.T) { ) if !arg0.Equal(expectedArg0) { - resp.Diagnostics.AddError( - "Unexpected Argument 0 Difference", - fmt.Sprintf("got: %s, expected: %s", arg0, expectedArg0), - ) + resp.Error = function.ConcatFuncErrors(resp.Error, function.NewFuncError(fmt.Sprintf("Unexpected Argument 0 Difference: got: %s, expected: %s", arg0, expectedArg0))) } if !arg1.Equal(expectedArg1) { - resp.Diagnostics.AddError( - "Unexpected Argument 1 Difference", - fmt.Sprintf("got: %s, expected: %s", arg1, expectedArg1), - ) + resp.Error = function.ConcatFuncErrors(resp.Error, function.NewFuncError(fmt.Sprintf("Unexpected Argument 0 Difference: got: %s, expected: %s", arg1, expectedArg1))) } - resp.Diagnostics.Append(resp.Result.Set(ctx, basetypes.NewStringValue("result"))...) + resp.Error = function.ConcatFuncErrors(resp.Error, resp.Result.Set(ctx, basetypes.NewStringValue("result"))) }, }, FunctionDefinition: function.Definition{ @@ -190,8 +168,8 @@ func TestServerCallFunction(t *testing.T) { }, }, expectedResponse: &fwserver.CallFunctionResponse{ - Diagnostics: nil, - Result: function.NewResultData(basetypes.NewStringValue("result")), + Error: nil, + Result: function.NewResultData(basetypes.NewStringValue("result")), }, }, "request-arguments-getargument": { @@ -210,36 +188,27 @@ func TestServerCallFunction(t *testing.T) { var arg1 basetypes.Int64Value var arg2 basetypes.StringValue - resp.Diagnostics.Append(req.Arguments.GetArgument(ctx, 0, &arg0)...) - resp.Diagnostics.Append(req.Arguments.GetArgument(ctx, 1, &arg1)...) - resp.Diagnostics.Append(req.Arguments.GetArgument(ctx, 2, &arg2)...) + resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.GetArgument(ctx, 0, &arg0)) + resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.GetArgument(ctx, 1, &arg1)) + resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.GetArgument(ctx, 2, &arg2)) expectedArg0 := basetypes.NewBoolNull() expectedArg1 := basetypes.NewInt64Unknown() expectedArg2 := basetypes.NewStringValue("arg2") if !arg0.Equal(expectedArg0) { - resp.Diagnostics.AddError( - "Unexpected Argument 0 Difference", - fmt.Sprintf("got: %s, expected: %s", arg0, expectedArg0), - ) + resp.Error = function.ConcatFuncErrors(resp.Error, function.NewFuncError(fmt.Sprintf("Unexpected Argument 0 Difference: got: %s, expected: %s", arg0, expectedArg0))) } if !arg1.Equal(expectedArg1) { - resp.Diagnostics.AddError( - "Unexpected Argument 1 Difference", - fmt.Sprintf("got: %s, expected: %s", arg1, expectedArg1), - ) + resp.Error = function.ConcatFuncErrors(resp.Error, function.NewFuncError(fmt.Sprintf("Unexpected Argument 1 Difference: got: %s, expected: %s", arg1, expectedArg1))) } if !arg2.Equal(expectedArg2) { - resp.Diagnostics.AddError( - "Unexpected Argument 2 Difference", - fmt.Sprintf("got: %s, expected: %s", arg2, expectedArg2), - ) + resp.Error = function.ConcatFuncErrors(resp.Error, function.NewFuncError(fmt.Sprintf("Unexpected Argument 2 Difference: got: %s, expected: %s", arg2, expectedArg2))) } - resp.Diagnostics.Append(resp.Result.Set(ctx, basetypes.NewStringValue("result"))...) + resp.Error = function.ConcatFuncErrors(resp.Error, resp.Result.Set(ctx, basetypes.NewStringValue("result"))) }, }, FunctionDefinition: function.Definition{ @@ -247,8 +216,8 @@ func TestServerCallFunction(t *testing.T) { }, }, expectedResponse: &fwserver.CallFunctionResponse{ - Diagnostics: nil, - Result: function.NewResultData(basetypes.NewStringValue("result")), + Error: nil, + Result: function.NewResultData(basetypes.NewStringValue("result")), }, }, "request-arguments-getargument-reflection": { @@ -265,26 +234,20 @@ func TestServerCallFunction(t *testing.T) { var arg0 string var arg1 *string - resp.Diagnostics.Append(req.Arguments.GetArgument(ctx, 0, &arg0)...) - resp.Diagnostics.Append(req.Arguments.GetArgument(ctx, 1, &arg1)...) + resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.GetArgument(ctx, 0, &arg0)) + resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.GetArgument(ctx, 1, &arg1)) expectedArg0 := "arg0" if arg0 != expectedArg0 { - resp.Diagnostics.AddError( - "Unexpected Argument 0 Difference", - fmt.Sprintf("got: %s, expected: %s", arg0, expectedArg0), - ) + resp.Error = function.ConcatFuncErrors(resp.Error, function.NewFuncError(fmt.Sprintf("Unexpected Argument 0 Difference: got: %s, expected: %s", arg0, expectedArg0))) } if arg1 != nil { - resp.Diagnostics.AddError( - "Unexpected Argument 1 Difference", - fmt.Sprintf("got: %s, expected: nil", *arg1), - ) + resp.Error = function.ConcatFuncErrors(resp.Error, function.NewFuncError(fmt.Sprintf("Unexpected Argument 0 Difference: got: %s, expected: nil", *arg1))) } - resp.Diagnostics.Append(resp.Result.Set(ctx, basetypes.NewStringValue("result"))...) + resp.Error = function.ConcatFuncErrors(resp.Error, resp.Result.Set(ctx, basetypes.NewStringValue("result"))) }, }, FunctionDefinition: function.Definition{ @@ -292,8 +255,8 @@ func TestServerCallFunction(t *testing.T) { }, }, expectedResponse: &fwserver.CallFunctionResponse{ - Diagnostics: nil, - Result: function.NewResultData(basetypes.NewStringValue("result")), + Error: nil, + Result: function.NewResultData(basetypes.NewStringValue("result")), }, }, "request-arguments-getargument-variadic": { @@ -319,8 +282,8 @@ func TestServerCallFunction(t *testing.T) { var arg0 basetypes.StringValue var arg1 basetypes.TupleValue - resp.Diagnostics.Append(req.Arguments.GetArgument(ctx, 0, &arg0)...) - resp.Diagnostics.Append(req.Arguments.GetArgument(ctx, 1, &arg1)...) + resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.GetArgument(ctx, 0, &arg0)) + resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.GetArgument(ctx, 1, &arg1)) expectedArg0 := basetypes.NewStringValue("arg0") expectedArg1 := basetypes.NewTupleValueMust( @@ -335,20 +298,14 @@ func TestServerCallFunction(t *testing.T) { ) if !arg0.Equal(expectedArg0) { - resp.Diagnostics.AddError( - "Unexpected Argument 0 Difference", - fmt.Sprintf("got: %s, expected: %s", arg0, expectedArg0), - ) + resp.Error = function.ConcatFuncErrors(resp.Error, function.NewFuncError(fmt.Sprintf("Unexpected Argument 0 Difference: got: %s, expected: %s", arg0, expectedArg0))) } if !arg1.Equal(expectedArg1) { - resp.Diagnostics.AddError( - "Unexpected Argument 1 Difference", - fmt.Sprintf("got: %s, expected: %s", arg1, expectedArg1), - ) + resp.Error = function.ConcatFuncErrors(resp.Error, function.NewFuncError(fmt.Sprintf("Unexpected Argument 0 Difference: got: %s, expected: %s", arg1, expectedArg1))) } - resp.Diagnostics.Append(resp.Result.Set(ctx, basetypes.NewStringValue("result"))...) + resp.Error = function.ConcatFuncErrors(resp.Error, resp.Result.Set(ctx, basetypes.NewStringValue("result"))) }, }, FunctionDefinition: function.Definition{ @@ -356,8 +313,8 @@ func TestServerCallFunction(t *testing.T) { }, }, expectedResponse: &fwserver.CallFunctionResponse{ - Diagnostics: nil, - Result: function.NewResultData(basetypes.NewStringValue("result")), + Error: nil, + Result: function.NewResultData(basetypes.NewStringValue("result")), }, }, "response-diagnostics": { @@ -368,8 +325,7 @@ func TestServerCallFunction(t *testing.T) { Arguments: function.NewArgumentsData(nil), Function: &testprovider.Function{ RunMethod: func(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { - resp.Diagnostics.AddWarning("warning summary", "warning detail") - resp.Diagnostics.AddError("error summary", "error detail") + resp.Error = function.ConcatFuncErrors(resp.Error, function.NewFuncError("error summary: error detail")) }, }, FunctionDefinition: function.Definition{ @@ -377,10 +333,7 @@ func TestServerCallFunction(t *testing.T) { }, }, expectedResponse: &fwserver.CallFunctionResponse{ - Diagnostics: diag.Diagnostics{ - diag.NewWarningDiagnostic("warning summary", "warning detail"), - diag.NewErrorDiagnostic("error summary", "error detail"), - }, + Error: function.NewFuncError("error summary: error detail"), Result: function.NewResultData(basetypes.NewStringUnknown()), }, }, @@ -392,7 +345,7 @@ func TestServerCallFunction(t *testing.T) { Arguments: function.NewArgumentsData(nil), Function: &testprovider.Function{ RunMethod: func(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { - resp.Diagnostics.Append(resp.Result.Set(ctx, basetypes.NewStringValue("result"))...) + resp.Error = function.ConcatFuncErrors(resp.Error, resp.Result.Set(ctx, basetypes.NewStringValue("result"))) }, }, FunctionDefinition: function.Definition{ @@ -400,8 +353,8 @@ func TestServerCallFunction(t *testing.T) { }, }, expectedResponse: &fwserver.CallFunctionResponse{ - Diagnostics: nil, - Result: function.NewResultData(basetypes.NewStringValue("result")), + Error: nil, + Result: function.NewResultData(basetypes.NewStringValue("result")), }, }, "response-result-reflection": { @@ -412,7 +365,7 @@ func TestServerCallFunction(t *testing.T) { Arguments: function.NewArgumentsData(nil), Function: &testprovider.Function{ RunMethod: func(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { - resp.Diagnostics.Append(resp.Result.Set(ctx, "result")...) + resp.Error = function.ConcatFuncErrors(resp.Error, resp.Result.Set(ctx, "result")) }, }, FunctionDefinition: function.Definition{ @@ -420,8 +373,8 @@ func TestServerCallFunction(t *testing.T) { }, }, expectedResponse: &fwserver.CallFunctionResponse{ - Diagnostics: nil, - Result: function.NewResultData(basetypes.NewStringValue("result")), + Error: nil, + Result: function.NewResultData(basetypes.NewStringValue("result")), }, }, } @@ -435,7 +388,7 @@ func TestServerCallFunction(t *testing.T) { response := &fwserver.CallFunctionResponse{} testCase.server.CallFunction(context.Background(), testCase.request, response) - if diff := cmp.Diff(response, testCase.expectedResponse); diff != "" { + if diff := cmp.Diff(response, testCase.expectedResponse, cmp.AllowUnexported(function.ResultData{})); diff != "" { t.Errorf("unexpected difference: %s", diff) } }) diff --git a/internal/fwserver/server_functions.go b/internal/fwserver/server_functions.go index 6d02ad879..c8e6142d2 100644 --- a/internal/fwserver/server_functions.go +++ b/internal/fwserver/server_functions.go @@ -14,26 +14,25 @@ import ( ) // Function returns the Function for a given name. -func (s *Server) Function(ctx context.Context, name string) (function.Function, diag.Diagnostics) { +func (s *Server) Function(ctx context.Context, name string) (function.Function, *function.FuncError) { functionFuncs, diags := s.FunctionFuncs(ctx) + funcErr := function.FuncErrorFromDiags(ctx, diags) + functionFunc, ok := functionFuncs[name] if !ok { - diags.AddError( - "Function Not Found", - fmt.Sprintf("No function named %q was found in the provider.", name), - ) + funcErr = function.ConcatFuncErrors(funcErr, function.NewFuncError(fmt.Sprintf("Function Not Found: No function named %q was found in the provider.", name))) - return nil, diags + return nil, funcErr } - return functionFunc(), diags + return functionFunc(), nil } // FunctionDefinition returns the Function Definition for the given name and // caches the result for later Function operations. -func (s *Server) FunctionDefinition(ctx context.Context, name string) (function.Definition, diag.Diagnostics) { +func (s *Server) FunctionDefinition(ctx context.Context, name string) (function.Definition, *function.FuncError) { s.functionDefinitionsMutex.RLock() functionDefinition, ok := s.functionDefinitions[name] s.functionDefinitionsMutex.RUnlock() @@ -42,14 +41,10 @@ func (s *Server) FunctionDefinition(ctx context.Context, name string) (function. return functionDefinition, nil } - var diags diag.Diagnostics - - functionImpl, functionDiags := s.Function(ctx, name) - - diags.Append(functionDiags...) + functionImpl, funcErr := s.Function(ctx, name) - if diags.HasError() { - return function.Definition{}, diags + if funcErr != nil { + return function.Definition{}, funcErr } definitionReq := function.DefinitionRequest{} @@ -59,10 +54,10 @@ func (s *Server) FunctionDefinition(ctx context.Context, name string) (function. functionImpl.Definition(ctx, definitionReq, &definitionResp) logging.FrameworkTrace(ctx, "Called provider defined Function Definition method", map[string]interface{}{logging.KeyFunctionName: name}) - diags.Append(definitionResp.Diagnostics...) + funcErr = function.ConcatFuncErrors(funcErr, function.FuncErrorFromDiags(ctx, definitionResp.Diagnostics)) - if diags.HasError() { - return definitionResp.Definition, diags + if funcErr != nil { + return definitionResp.Definition, funcErr } s.functionDefinitionsMutex.Lock() @@ -75,7 +70,7 @@ func (s *Server) FunctionDefinition(ctx context.Context, name string) (function. s.functionDefinitionsMutex.Unlock() - return definitionResp.Definition, diags + return definitionResp.Definition, funcErr } // FunctionDefinitions returns a map of Function Definitions for the diff --git a/internal/proto5server/server_callfunction.go b/internal/proto5server/server_callfunction.go index ad4a4478a..ffd1d6104 100644 --- a/internal/proto5server/server_callfunction.go +++ b/internal/proto5server/server_callfunction.go @@ -6,11 +6,13 @@ package proto5server import ( "context" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/internal/fromproto5" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/logging" "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" - "github.com/hashicorp/terraform-plugin-go/tfprotov5" ) // CallFunction satisfies the tfprotov5.ProviderServer interface. @@ -20,27 +22,30 @@ func (s *Server) CallFunction(ctx context.Context, protoReq *tfprotov5.CallFunct fwResp := &fwserver.CallFunctionResponse{} - function, diags := s.FrameworkServer.Function(ctx, protoReq.Name) + serverFunction, err := s.FrameworkServer.Function(ctx, protoReq.Name) - fwResp.Diagnostics.Append(diags...) + fwResp.Error = err - if fwResp.Diagnostics.HasError() { + if fwResp.Error != nil { + //nolint:nilerr // error is assigned to fwResp.Error return toproto5.CallFunctionResponse(ctx, fwResp), nil } - functionDefinition, diags := s.FrameworkServer.FunctionDefinition(ctx, protoReq.Name) + functionDefinition, err := s.FrameworkServer.FunctionDefinition(ctx, protoReq.Name) - fwResp.Diagnostics.Append(diags...) + fwResp.Error = function.ConcatFuncErrors(fwResp.Error, err) - if fwResp.Diagnostics.HasError() { + if fwResp.Error != nil { + //nolint:nilerr // error is assigned to fwResp.Error return toproto5.CallFunctionResponse(ctx, fwResp), nil } - fwReq, diags := fromproto5.CallFunctionRequest(ctx, protoReq, function, functionDefinition) + fwReq, diags := fromproto5.CallFunctionRequest(ctx, protoReq, serverFunction, functionDefinition) - fwResp.Diagnostics.Append(diags...) + fwResp.Error = function.ConcatFuncErrors(fwResp.Error, function.FuncErrorFromDiags(ctx, diags)) - if fwResp.Diagnostics.HasError() { + if fwResp.Error != nil { + //nolint:nilerr // error is assigned to fwResp.Error return toproto5.CallFunctionResponse(ctx, fwResp), nil } diff --git a/internal/proto5server/server_callfunction_test.go b/internal/proto5server/server_callfunction_test.go index 394dc4ea9..17f119267 100644 --- a/internal/proto5server/server_callfunction_test.go +++ b/internal/proto5server/server_callfunction_test.go @@ -9,13 +9,14 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tfprotov5" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestServerCallFunction(t *testing.T) { @@ -53,34 +54,25 @@ func TestServerCallFunction(t *testing.T) { var arg1 basetypes.Int64Value var arg2 basetypes.StringValue - resp.Diagnostics.Append(req.Arguments.Get(ctx, &arg0, &arg1, &arg2)...) + resp.Error = req.Arguments.Get(ctx, &arg0, &arg1, &arg2) expectedArg0 := basetypes.NewBoolNull() expectedArg1 := basetypes.NewInt64Unknown() expectedArg2 := basetypes.NewStringValue("arg2") if !arg0.Equal(expectedArg0) { - resp.Diagnostics.AddError( - "Unexpected Argument 0 Difference", - fmt.Sprintf("got: %s, expected: %s", arg0, expectedArg0), - ) + resp.Error = function.ConcatFuncErrors(resp.Error, function.NewFuncError(fmt.Sprintf("Unexpected Argument 0 Difference: got: %s, expected: %s", arg0, expectedArg0))) } if !arg1.Equal(expectedArg1) { - resp.Diagnostics.AddError( - "Unexpected Argument 1 Difference", - fmt.Sprintf("got: %s, expected: %s", arg1, expectedArg1), - ) + resp.Error = function.ConcatFuncErrors(resp.Error, function.NewFuncError(fmt.Sprintf("Unexpected Argument 1 Difference: got: %s, expected: %s", arg1, expectedArg1))) } if !arg2.Equal(expectedArg2) { - resp.Diagnostics.AddError( - "Unexpected Argument 2 Difference", - fmt.Sprintf("got: %s, expected: %s", arg2, expectedArg2), - ) + resp.Error = function.ConcatFuncErrors(resp.Error, function.NewFuncError(fmt.Sprintf("Unexpected Argument 2 Difference: got: %s, expected: %s", arg2, expectedArg2))) } - resp.Diagnostics.Append(resp.Result.Set(ctx, basetypes.NewStringValue("result"))...) + resp.Error = function.ConcatFuncErrors(resp.Error, resp.Result.Set(ctx, basetypes.NewStringValue("result"))) }, } }, @@ -125,7 +117,7 @@ func TestServerCallFunction(t *testing.T) { var arg0 basetypes.StringValue var arg1 basetypes.TupleValue - resp.Diagnostics.Append(req.Arguments.Get(ctx, &arg0, &arg1)...) + resp.Error = req.Arguments.Get(ctx, &arg0, &arg1) expectedArg0 := basetypes.NewStringValue("arg0") expectedArg1 := basetypes.NewTupleValueMust( @@ -140,20 +132,14 @@ func TestServerCallFunction(t *testing.T) { ) if !arg0.Equal(expectedArg0) { - resp.Diagnostics.AddError( - "Unexpected Argument 0 Difference", - fmt.Sprintf("got: %s, expected: %s", arg0, expectedArg0), - ) + resp.Error = function.ConcatFuncErrors(resp.Error, function.NewFuncError(fmt.Sprintf("Unexpected Argument 0 Difference: got: %s, expected: %s", arg0, expectedArg0))) } if !arg1.Equal(expectedArg1) { - resp.Diagnostics.AddError( - "Unexpected Argument 1 Difference", - fmt.Sprintf("got: %s, expected: %s", arg1, expectedArg1), - ) + resp.Error = function.ConcatFuncErrors(resp.Error, function.NewFuncError(fmt.Sprintf("Unexpected Argument 1 Difference: got: %s, expected: %s", arg1, expectedArg1))) } - resp.Diagnostics.Append(resp.Result.Set(ctx, basetypes.NewStringValue("result"))...) + resp.Error = function.ConcatFuncErrors(resp.Error, resp.Result.Set(ctx, basetypes.NewStringValue("result"))) }, } }, @@ -174,7 +160,7 @@ func TestServerCallFunction(t *testing.T) { Result: testNewSingleValueDynamicValue(t, tftypes.NewValue(tftypes.String, "result")), }, }, - "response-diagnostics": { + "response-function-errors": { server: &Server{ FrameworkServer: fwserver.Server{ Provider: &testprovider.ProviderWithFunctions{ @@ -191,9 +177,8 @@ func TestServerCallFunction(t *testing.T) { } }, RunMethod: func(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { - resp.Diagnostics.AddWarning("warning summary", "warning detail") - resp.Diagnostics.AddError("error summary", "error detail") - resp.Diagnostics.Append(resp.Result.Set(ctx, basetypes.NewStringValue("result"))...) + resp.Error = function.ConcatFuncErrors(resp.Error, function.NewFuncError("error summary: error detail")) + resp.Error = function.ConcatFuncErrors(resp.Error, resp.Result.Set(ctx, basetypes.NewStringValue("result"))) }, } }, @@ -207,17 +192,8 @@ func TestServerCallFunction(t *testing.T) { Name: "testfunction", }, expectedResponse: &tfprotov5.CallFunctionResponse{ - Diagnostics: []*tfprotov5.Diagnostic{ - { - Severity: tfprotov5.DiagnosticSeverityWarning, - Summary: "warning summary", - Detail: "warning detail", - }, - { - Severity: tfprotov5.DiagnosticSeverityError, - Summary: "error summary", - Detail: "error detail", - }, + Error: &tfprotov5.FunctionError{ + Text: "error summary: error detail", }, Result: testNewSingleValueDynamicValue(t, tftypes.NewValue(tftypes.String, "result")), }, @@ -239,7 +215,7 @@ func TestServerCallFunction(t *testing.T) { } }, RunMethod: func(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { - resp.Diagnostics.Append(resp.Result.Set(ctx, basetypes.NewStringValue("result"))...) + resp.Error = function.ConcatFuncErrors(resp.Error, resp.Result.Set(ctx, basetypes.NewStringValue("result"))) }, } }, diff --git a/internal/proto6server/server_callfunction.go b/internal/proto6server/server_callfunction.go index 8b216d240..eca8255dd 100644 --- a/internal/proto6server/server_callfunction.go +++ b/internal/proto6server/server_callfunction.go @@ -6,11 +6,13 @@ package proto6server import ( "context" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/logging" "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" ) // CallFunction satisfies the tfprotov6.ProviderServer interface. @@ -20,27 +22,30 @@ func (s *Server) CallFunction(ctx context.Context, protoReq *tfprotov6.CallFunct fwResp := &fwserver.CallFunctionResponse{} - function, diags := s.FrameworkServer.Function(ctx, protoReq.Name) + serverFunction, err := s.FrameworkServer.Function(ctx, protoReq.Name) - fwResp.Diagnostics.Append(diags...) + fwResp.Error = err - if fwResp.Diagnostics.HasError() { + if fwResp.Error != nil { + //nolint:nilerr // error is assigned to fwResp.Error return toproto6.CallFunctionResponse(ctx, fwResp), nil } - functionDefinition, diags := s.FrameworkServer.FunctionDefinition(ctx, protoReq.Name) + functionDefinition, err := s.FrameworkServer.FunctionDefinition(ctx, protoReq.Name) - fwResp.Diagnostics.Append(diags...) + fwResp.Error = function.ConcatFuncErrors(fwResp.Error, err) - if fwResp.Diagnostics.HasError() { + if fwResp.Error != nil { + //nolint:nilerr // error is assigned to fwResp.Error return toproto6.CallFunctionResponse(ctx, fwResp), nil } - fwReq, diags := fromproto6.CallFunctionRequest(ctx, protoReq, function, functionDefinition) + fwReq, diags := fromproto6.CallFunctionRequest(ctx, protoReq, serverFunction, functionDefinition) - fwResp.Diagnostics.Append(diags...) + fwResp.Error = function.ConcatFuncErrors(fwResp.Error, function.FuncErrorFromDiags(ctx, diags)) - if fwResp.Diagnostics.HasError() { + if fwResp.Error != nil { + //nolint:nilerr // error is assigned to fwResp.Error return toproto6.CallFunctionResponse(ctx, fwResp), nil } diff --git a/internal/proto6server/server_callfunction_test.go b/internal/proto6server/server_callfunction_test.go index 8d21e1888..e4710ff26 100644 --- a/internal/proto6server/server_callfunction_test.go +++ b/internal/proto6server/server_callfunction_test.go @@ -9,13 +9,14 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestServerCallFunction(t *testing.T) { @@ -53,34 +54,24 @@ func TestServerCallFunction(t *testing.T) { var arg1 basetypes.Int64Value var arg2 basetypes.StringValue - resp.Diagnostics.Append(req.Arguments.Get(ctx, &arg0, &arg1, &arg2)...) + resp.Error = req.Arguments.Get(ctx, &arg0, &arg1, &arg2) expectedArg0 := basetypes.NewBoolNull() expectedArg1 := basetypes.NewInt64Unknown() expectedArg2 := basetypes.NewStringValue("arg2") if !arg0.Equal(expectedArg0) { - resp.Diagnostics.AddError( - "Unexpected Argument 0 Difference", - fmt.Sprintf("got: %s, expected: %s", arg0, expectedArg0), - ) + resp.Error = function.ConcatFuncErrors(resp.Error, function.NewFuncError(fmt.Sprintf("Unexpected Argument 0 Difference: got: %s, expected: %s", arg0, expectedArg0))) } if !arg1.Equal(expectedArg1) { - resp.Diagnostics.AddError( - "Unexpected Argument 1 Difference", - fmt.Sprintf("got: %s, expected: %s", arg1, expectedArg1), - ) + resp.Error = function.ConcatFuncErrors(resp.Error, function.NewFuncError(fmt.Sprintf("Unexpected Argument 1 Difference: got: %s, expected: %s", arg1, expectedArg1))) } if !arg2.Equal(expectedArg2) { - resp.Diagnostics.AddError( - "Unexpected Argument 2 Difference", - fmt.Sprintf("got: %s, expected: %s", arg2, expectedArg2), - ) + resp.Error = function.ConcatFuncErrors(resp.Error, function.NewFuncError(fmt.Sprintf("Unexpected Argument 2 Difference: got: %s, expected: %s", arg2, expectedArg2))) } - - resp.Diagnostics.Append(resp.Result.Set(ctx, basetypes.NewStringValue("result"))...) + resp.Error = function.ConcatFuncErrors(resp.Error, resp.Result.Set(ctx, basetypes.NewStringValue("result"))) }, } }, @@ -125,7 +116,7 @@ func TestServerCallFunction(t *testing.T) { var arg0 basetypes.StringValue var arg1 basetypes.TupleValue - resp.Diagnostics.Append(req.Arguments.Get(ctx, &arg0, &arg1)...) + resp.Error = req.Arguments.Get(ctx, &arg0, &arg1) expectedArg0 := basetypes.NewStringValue("arg0") expectedArg1 := basetypes.NewTupleValueMust( @@ -140,20 +131,14 @@ func TestServerCallFunction(t *testing.T) { ) if !arg0.Equal(expectedArg0) { - resp.Diagnostics.AddError( - "Unexpected Argument 0 Difference", - fmt.Sprintf("got: %s, expected: %s", arg0, expectedArg0), - ) + resp.Error = function.ConcatFuncErrors(resp.Error, function.NewFuncError(fmt.Sprintf("Unexpected Argument 0 Difference: got: %s, expected: %s", arg0, expectedArg0))) } if !arg1.Equal(expectedArg1) { - resp.Diagnostics.AddError( - "Unexpected Argument 1 Difference", - fmt.Sprintf("got: %s, expected: %s", arg1, expectedArg1), - ) + resp.Error = function.ConcatFuncErrors(resp.Error, function.NewFuncError(fmt.Sprintf("Unexpected Argument 1 Difference: got: %s, expected: %s", arg1, expectedArg1))) } - resp.Diagnostics.Append(resp.Result.Set(ctx, basetypes.NewStringValue("result"))...) + resp.Error = function.ConcatFuncErrors(resp.Error, resp.Result.Set(ctx, basetypes.NewStringValue("result"))) }, } }, @@ -174,7 +159,7 @@ func TestServerCallFunction(t *testing.T) { Result: testNewSingleValueDynamicValue(t, tftypes.NewValue(tftypes.String, "result")), }, }, - "response-diagnostics": { + "response-function-errors": { server: &Server{ FrameworkServer: fwserver.Server{ Provider: &testprovider.ProviderWithFunctions{ @@ -191,9 +176,8 @@ func TestServerCallFunction(t *testing.T) { } }, RunMethod: func(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { - resp.Diagnostics.AddWarning("warning summary", "warning detail") - resp.Diagnostics.AddError("error summary", "error detail") - resp.Diagnostics.Append(resp.Result.Set(ctx, basetypes.NewStringValue("result"))...) + resp.Error = function.ConcatFuncErrors(resp.Error, function.NewFuncError("error summary: error detail")) + resp.Error = function.ConcatFuncErrors(resp.Error, resp.Result.Set(ctx, basetypes.NewStringValue("result"))) }, } }, @@ -207,17 +191,8 @@ func TestServerCallFunction(t *testing.T) { Name: "testfunction", }, expectedResponse: &tfprotov6.CallFunctionResponse{ - Diagnostics: []*tfprotov6.Diagnostic{ - { - Severity: tfprotov6.DiagnosticSeverityWarning, - Summary: "warning summary", - Detail: "warning detail", - }, - { - Severity: tfprotov6.DiagnosticSeverityError, - Summary: "error summary", - Detail: "error detail", - }, + Error: &tfprotov6.FunctionError{ + Text: "error summary: error detail", }, Result: testNewSingleValueDynamicValue(t, tftypes.NewValue(tftypes.String, "result")), }, @@ -239,7 +214,7 @@ func TestServerCallFunction(t *testing.T) { } }, RunMethod: func(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { - resp.Diagnostics.Append(resp.Result.Set(ctx, basetypes.NewStringValue("result"))...) + resp.Error = function.ConcatFuncErrors(resp.Error, resp.Result.Set(ctx, basetypes.NewStringValue("result"))) }, } }, diff --git a/internal/toproto5/callfunction.go b/internal/toproto5/callfunction.go index c1a6f89ea..a5105a94b 100644 --- a/internal/toproto5/callfunction.go +++ b/internal/toproto5/callfunction.go @@ -6,8 +6,10 @@ package toproto5 import ( "context" - "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-go/tfprotov5" + + "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" ) // CallFunctionResponse returns the *tfprotov5.CallFunctionResponse @@ -17,14 +19,12 @@ func CallFunctionResponse(ctx context.Context, fw *fwserver.CallFunctionResponse return nil } - proto := &tfprotov5.CallFunctionResponse{ - Diagnostics: Diagnostics(ctx, fw.Diagnostics), - } - - result, diags := FunctionResultData(ctx, fw.Result) + result, resultErr := FunctionResultData(ctx, fw.Result) - proto.Diagnostics = append(proto.Diagnostics, Diagnostics(ctx, diags)...) - proto.Result = result + funcErr := function.ConcatFuncErrors(fw.Error, resultErr) - return proto + return &tfprotov5.CallFunctionResponse{ + Error: FunctionError(ctx, funcErr), + Result: result, + } } diff --git a/internal/toproto5/callfunction_test.go b/internal/toproto5/callfunction_test.go index 9714f77d3..91c26725b 100644 --- a/internal/toproto5/callfunction_test.go +++ b/internal/toproto5/callfunction_test.go @@ -8,13 +8,13 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tfprotov5" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestCallFunctionResponse(t *testing.T) { @@ -28,25 +28,17 @@ func TestCallFunctionResponse(t *testing.T) { input: nil, expected: nil, }, - "diagnostics": { + "error": { input: &fwserver.CallFunctionResponse{ - Diagnostics: diag.Diagnostics{ - diag.NewWarningDiagnostic("warning summary", "warning detail"), - diag.NewErrorDiagnostic("error summary", "error detail"), - }, + Error: function.ConcatFuncErrors( + function.NewFuncError("error summary one: error detail one"), + function.NewArgumentFuncError(0, "error summary two: error detail two"), + ), }, expected: &tfprotov5.CallFunctionResponse{ - Diagnostics: []*tfprotov5.Diagnostic{ - { - Severity: tfprotov5.DiagnosticSeverityWarning, - Summary: "warning summary", - Detail: "warning detail", - }, - { - Severity: tfprotov5.DiagnosticSeverityError, - Summary: "error summary", - Detail: "error detail", - }, + Error: &tfprotov5.FunctionError{ + Text: "error summary one: error detail one\nerror summary two: error detail two", + FunctionArgument: pointer(int64(0)), }, }, }, diff --git a/internal/toproto5/diagnostics.go b/internal/toproto5/diagnostics.go index 93b0f39f0..564d89796 100644 --- a/internal/toproto5/diagnostics.go +++ b/internal/toproto5/diagnostics.go @@ -6,9 +6,10 @@ package toproto5 import ( "context" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/totftypes" - "github.com/hashicorp/terraform-plugin-go/tfprotov5" ) // DiagnosticSeverity converts diag.Severity into tfprotov5.DiagnosticSeverity. @@ -34,11 +35,6 @@ func Diagnostics(ctx context.Context, diagnostics diag.Diagnostics) []*tfprotov5 Summary: diagnostic.Summary(), } - if diagWithFunctionArgument, ok := diagnostic.(diag.DiagnosticWithFunctionArgument); ok { - functionArgument := int64(diagWithFunctionArgument.FunctionArgument()) - tfprotov5Diagnostic.FunctionArgument = &functionArgument - } - if diagWithPath, ok := diagnostic.(diag.DiagnosticWithPath); ok { var diags diag.Diagnostics diff --git a/internal/toproto5/diagnostics_test.go b/internal/toproto5/diagnostics_test.go index fabf775ae..7e888cb19 100644 --- a/internal/toproto5/diagnostics_test.go +++ b/internal/toproto5/diagnostics_test.go @@ -8,11 +8,12 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-go/tfprotov5" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestDiagnosticSeverity(t *testing.T) { @@ -92,26 +93,6 @@ func TestDiagnostics(t *testing.T) { }, }, }, - "DiagnosticWithFunctionArgument": { - diags: diag.Diagnostics{ - diag.NewArgumentErrorDiagnostic(1, "one summary", "one detail"), - diag.NewArgumentWarningDiagnostic(2, "two summary", "two detail"), - }, - expected: []*tfprotov5.Diagnostic{ - { - Detail: "one detail", - FunctionArgument: pointer(int64(1)), - Severity: tfprotov5.DiagnosticSeverityError, - Summary: "one summary", - }, - { - Detail: "two detail", - FunctionArgument: pointer(int64(2)), - Severity: tfprotov5.DiagnosticSeverityWarning, - Summary: "two summary", - }, - }, - }, "DiagnosticWithPath": { diags: diag.Diagnostics{ diag.NewAttributeErrorDiagnostic(path.Empty(), "one summary", "one detail"), diff --git a/internal/toproto5/function.go b/internal/toproto5/function.go index e9f0c3934..07388f07a 100644 --- a/internal/toproto5/function.go +++ b/internal/toproto5/function.go @@ -6,10 +6,10 @@ package toproto5 import ( "context" - "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" - "github.com/hashicorp/terraform-plugin-go/tfprotov5" ) // Function returns the *tfprotov5.Function for a function.Definition. @@ -88,9 +88,7 @@ func FunctionReturn(ctx context.Context, fw function.Return) *tfprotov5.Function // FunctionResultData returns the *tfprotov5.DynamicValue for a given // function.ResultData. -func FunctionResultData(ctx context.Context, data function.ResultData) (*tfprotov5.DynamicValue, diag.Diagnostics) { - var diags diag.Diagnostics - +func FunctionResultData(ctx context.Context, data function.ResultData) (*tfprotov5.DynamicValue, *function.FuncError) { attrValue := data.Value() if attrValue == nil { @@ -101,27 +99,21 @@ func FunctionResultData(ctx context.Context, data function.ResultData) (*tfproto tfValue, err := attrValue.ToTerraformValue(ctx) if err != nil { - diags.AddError( - "Unable to Convert Function Result Data", - "An unexpected error was encountered when converting the function result data to the protocol type. "+ - "Please report this to the provider developer:\n\n"+ - "Unable to convert framework type to tftypes: "+err.Error(), - ) - - return nil, diags + msg := "Unable to Convert Function Result Data: An unexpected error was encountered when converting the function result data to the protocol type. " + + "Please report this to the provider developer:\n\n" + + "Unable to convert framework type to tftypes: " + err.Error() + + return nil, function.NewFuncError(msg) } dynamicValue, err := tfprotov5.NewDynamicValue(tfType, tfValue) if err != nil { - diags.AddError( - "Unable to Convert Function Result Data", - "An unexpected error was encountered when converting the function result data to the protocol type. "+ - "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ - "Unable to create DynamicValue: "+err.Error(), - ) - - return nil, diags + msg := "Unable to Convert Function Result Data: An unexpected error was encountered when converting the function result data to the protocol type. " + + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n" + + "Unable to create DynamicValue: " + err.Error() + + return nil, function.NewFuncError(msg) } return &dynamicValue, nil diff --git a/internal/toproto5/function_errors.go b/internal/toproto5/function_errors.go new file mode 100644 index 000000000..7c58f36eb --- /dev/null +++ b/internal/toproto5/function_errors.go @@ -0,0 +1,24 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + + "github.com/hashicorp/terraform-plugin-framework/function" +) + +// FunctionError converts the function error into the tfprotov5 function error. +func FunctionError(ctx context.Context, funcErr *function.FuncError) *tfprotov5.FunctionError { + if funcErr == nil { + return nil + } + + return &tfprotov5.FunctionError{ + Text: funcErr.Text, + FunctionArgument: funcErr.FunctionArgument, + } +} diff --git a/internal/toproto5/function_test.go b/internal/toproto5/function_test.go index 7f6b9f414..0f10ea6b9 100644 --- a/internal/toproto5/function_test.go +++ b/internal/toproto5/function_test.go @@ -8,14 +8,14 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tfprotov5" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestFunction(t *testing.T) { @@ -388,24 +388,24 @@ func TestFunctionResultData(t *testing.T) { t.Parallel() testCases := map[string]struct { - fw function.ResultData - expected *tfprotov5.DynamicValue - expectedDiagnostics diag.Diagnostics + fw function.ResultData + expected *tfprotov5.DynamicValue + expectedErr *function.FuncError }{ "empty": { - fw: function.ResultData{}, - expected: nil, - expectedDiagnostics: nil, + fw: function.ResultData{}, + expected: nil, + expectedErr: nil, }, "value-nil": { - fw: function.NewResultData(nil), - expected: nil, - expectedDiagnostics: nil, + fw: function.NewResultData(nil), + expected: nil, + expectedErr: nil, }, "value": { - fw: function.NewResultData(basetypes.NewBoolValue(true)), - expected: DynamicValueMust(tftypes.NewValue(tftypes.Bool, true)), - expectedDiagnostics: nil, + fw: function.NewResultData(basetypes.NewBoolValue(true)), + expected: DynamicValueMust(tftypes.NewValue(tftypes.Bool, true)), + expectedErr: nil, }, } @@ -421,7 +421,7 @@ func TestFunctionResultData(t *testing.T) { t.Errorf("unexpected diagnostics difference: %s", diff) } - if diff := cmp.Diff(diags, testCase.expectedDiagnostics); diff != "" { + if diff := cmp.Diff(diags, testCase.expectedErr); diff != "" { t.Errorf("unexpected diagnostics difference: %s", diff) } }) diff --git a/internal/toproto6/callfunction.go b/internal/toproto6/callfunction.go index 0eba3ee8d..14afb8f20 100644 --- a/internal/toproto6/callfunction.go +++ b/internal/toproto6/callfunction.go @@ -6,8 +6,10 @@ package toproto6 import ( "context" - "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" ) // CallFunctionResponse returns the *tfprotov6.CallFunctionResponse @@ -17,14 +19,12 @@ func CallFunctionResponse(ctx context.Context, fw *fwserver.CallFunctionResponse return nil } - proto := &tfprotov6.CallFunctionResponse{ - Diagnostics: Diagnostics(ctx, fw.Diagnostics), - } - - result, diags := FunctionResultData(ctx, fw.Result) + result, resultErr := FunctionResultData(ctx, fw.Result) - proto.Diagnostics = append(proto.Diagnostics, Diagnostics(ctx, diags)...) - proto.Result = result + funcErr := function.ConcatFuncErrors(fw.Error, resultErr) - return proto + return &tfprotov6.CallFunctionResponse{ + Error: FunctionError(ctx, funcErr), + Result: result, + } } diff --git a/internal/toproto6/callfunction_test.go b/internal/toproto6/callfunction_test.go index 34078fa41..2f8df94d0 100644 --- a/internal/toproto6/callfunction_test.go +++ b/internal/toproto6/callfunction_test.go @@ -8,13 +8,13 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestCallFunctionResponse(t *testing.T) { @@ -28,25 +28,17 @@ func TestCallFunctionResponse(t *testing.T) { input: nil, expected: nil, }, - "diagnostics": { + "error": { input: &fwserver.CallFunctionResponse{ - Diagnostics: diag.Diagnostics{ - diag.NewWarningDiagnostic("warning summary", "warning detail"), - diag.NewErrorDiagnostic("error summary", "error detail"), - }, + Error: function.ConcatFuncErrors( + function.NewFuncError("error summary one: error detail one"), + function.NewArgumentFuncError(0, "error summary two: error detail two"), + ), }, expected: &tfprotov6.CallFunctionResponse{ - Diagnostics: []*tfprotov6.Diagnostic{ - { - Severity: tfprotov6.DiagnosticSeverityWarning, - Summary: "warning summary", - Detail: "warning detail", - }, - { - Severity: tfprotov6.DiagnosticSeverityError, - Summary: "error summary", - Detail: "error detail", - }, + Error: &tfprotov6.FunctionError{ + Text: "error summary one: error detail one\nerror summary two: error detail two", + FunctionArgument: pointer(int64(0)), }, }, }, diff --git a/internal/toproto6/diagnostics.go b/internal/toproto6/diagnostics.go index ee258e144..42e7fb5e9 100644 --- a/internal/toproto6/diagnostics.go +++ b/internal/toproto6/diagnostics.go @@ -6,9 +6,10 @@ package toproto6 import ( "context" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/totftypes" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" ) // DiagnosticSeverity converts diag.Severity into tfprotov6.DiagnosticSeverity. @@ -34,11 +35,6 @@ func Diagnostics(ctx context.Context, diagnostics diag.Diagnostics) []*tfprotov6 Summary: diagnostic.Summary(), } - if diagWithFunctionArgument, ok := diagnostic.(diag.DiagnosticWithFunctionArgument); ok { - functionArgument := int64(diagWithFunctionArgument.FunctionArgument()) - tfprotov6Diagnostic.FunctionArgument = &functionArgument - } - if diagWithPath, ok := diagnostic.(diag.DiagnosticWithPath); ok { var diags diag.Diagnostics diff --git a/internal/toproto6/diagnostics_test.go b/internal/toproto6/diagnostics_test.go index 08f9c15f8..d473131b0 100644 --- a/internal/toproto6/diagnostics_test.go +++ b/internal/toproto6/diagnostics_test.go @@ -8,11 +8,12 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestDiagnosticSeverity(t *testing.T) { @@ -92,26 +93,6 @@ func TestDiagnostics(t *testing.T) { }, }, }, - "DiagnosticWithFunctionArgument": { - diags: diag.Diagnostics{ - diag.NewArgumentErrorDiagnostic(1, "one summary", "one detail"), - diag.NewArgumentWarningDiagnostic(2, "two summary", "two detail"), - }, - expected: []*tfprotov6.Diagnostic{ - { - Detail: "one detail", - FunctionArgument: pointer(int64(1)), - Severity: tfprotov6.DiagnosticSeverityError, - Summary: "one summary", - }, - { - Detail: "two detail", - FunctionArgument: pointer(int64(2)), - Severity: tfprotov6.DiagnosticSeverityWarning, - Summary: "two summary", - }, - }, - }, "DiagnosticWithPath": { diags: diag.Diagnostics{ diag.NewAttributeErrorDiagnostic(path.Empty(), "one summary", "one detail"), diff --git a/internal/toproto6/function.go b/internal/toproto6/function.go index 243f8fd2f..e77be9245 100644 --- a/internal/toproto6/function.go +++ b/internal/toproto6/function.go @@ -6,10 +6,10 @@ package toproto6 import ( "context" - "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" ) // Function returns the *tfprotov6.Function for a function.Definition. @@ -88,9 +88,7 @@ func FunctionReturn(ctx context.Context, fw function.Return) *tfprotov6.Function // FunctionResultData returns the *tfprotov6.DynamicValue for a given // function.ResultData. -func FunctionResultData(ctx context.Context, data function.ResultData) (*tfprotov6.DynamicValue, diag.Diagnostics) { - var diags diag.Diagnostics - +func FunctionResultData(ctx context.Context, data function.ResultData) (*tfprotov6.DynamicValue, *function.FuncError) { attrValue := data.Value() if attrValue == nil { @@ -101,27 +99,21 @@ func FunctionResultData(ctx context.Context, data function.ResultData) (*tfproto tfValue, err := attrValue.ToTerraformValue(ctx) if err != nil { - diags.AddError( - "Unable to Convert Function Return Data", - "An unexpected error was encountered when converting the function result data to the protocol type. "+ - "Please report this to the provider developer:\n\n"+ - "Unable to convert framework type to tftypes: "+err.Error(), - ) - - return nil, diags + msg := "Unable to Convert Function Result Data: An unexpected error was encountered when converting the function result data to the protocol type. " + + "Please report this to the provider developer:\n\n" + + "Unable to convert framework type to tftypes: " + err.Error() + + return nil, function.NewFuncError(msg) } dynamicValue, err := tfprotov6.NewDynamicValue(tfType, tfValue) if err != nil { - diags.AddError( - "Unable to Convert Function Return Data", - "An unexpected error was encountered when converting the function result data to the protocol type. "+ - "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ - "Unable to create DynamicValue: "+err.Error(), - ) - - return nil, diags + msg := "Unable to Convert Function Result Data: An unexpected error was encountered when converting the function result data to the protocol type. " + + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n" + + "Unable to create DynamicValue: " + err.Error() + + return nil, function.NewFuncError(msg) } return &dynamicValue, nil diff --git a/internal/toproto6/function_errors.go b/internal/toproto6/function_errors.go new file mode 100644 index 000000000..062d3eae0 --- /dev/null +++ b/internal/toproto6/function_errors.go @@ -0,0 +1,24 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + "github.com/hashicorp/terraform-plugin-framework/function" +) + +// FunctionError converts the function error into the tfprotov6 function error. +func FunctionError(ctx context.Context, funcErr *function.FuncError) *tfprotov6.FunctionError { + if funcErr == nil { + return nil + } + + return &tfprotov6.FunctionError{ + Text: funcErr.Text, + FunctionArgument: funcErr.FunctionArgument, + } +} diff --git a/internal/toproto6/function_test.go b/internal/toproto6/function_test.go index 82fe4bf0b..9e7e88bd2 100644 --- a/internal/toproto6/function_test.go +++ b/internal/toproto6/function_test.go @@ -8,14 +8,14 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" - "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestFunction(t *testing.T) { @@ -388,24 +388,24 @@ func TestFunctionResultData(t *testing.T) { t.Parallel() testCases := map[string]struct { - fw function.ResultData - expected *tfprotov6.DynamicValue - expectedDiagnostics diag.Diagnostics + fw function.ResultData + expected *tfprotov6.DynamicValue + expectedErr *function.FuncError }{ "empty": { - fw: function.ResultData{}, - expected: nil, - expectedDiagnostics: nil, + fw: function.ResultData{}, + expected: nil, + expectedErr: nil, }, "value-nil": { - fw: function.NewResultData(nil), - expected: nil, - expectedDiagnostics: nil, + fw: function.NewResultData(nil), + expected: nil, + expectedErr: nil, }, "value": { - fw: function.NewResultData(basetypes.NewBoolValue(true)), - expected: DynamicValueMust(tftypes.NewValue(tftypes.Bool, true)), - expectedDiagnostics: nil, + fw: function.NewResultData(basetypes.NewBoolValue(true)), + expected: DynamicValueMust(tftypes.NewValue(tftypes.Bool, true)), + expectedErr: nil, }, } @@ -421,7 +421,7 @@ func TestFunctionResultData(t *testing.T) { t.Errorf("unexpected diagnostics difference: %s", diff) } - if diff := cmp.Diff(diags, testCase.expectedDiagnostics); diff != "" { + if diff := cmp.Diff(diags, testCase.expectedErr); diff != "" { t.Errorf("unexpected diagnostics difference: %s", diff) } }) diff --git a/website/data/plugin-framework-nav-data.json b/website/data/plugin-framework-nav-data.json index 4038debe5..b5e827a16 100644 --- a/website/data/plugin-framework-nav-data.json +++ b/website/data/plugin-framework-nav-data.json @@ -227,6 +227,10 @@ } ] }, + { + "title": "Errors", + "path": "functions/errors" + }, { "title": "Testing", "path": "functions/testing" diff --git a/website/docs/plugin/framework/diagnostics.mdx b/website/docs/plugin/framework/diagnostics.mdx index 2d4f17318..8a87d4326 100644 --- a/website/docs/plugin/framework/diagnostics.mdx +++ b/website/docs/plugin/framework/diagnostics.mdx @@ -251,23 +251,6 @@ func (s exampleType) Validate(ctx context.Context, in tftypes.Value, path path.P // ... further logic ... ``` -#### AddArgumentError and AddArgumentWarning - -When creating diagnostics that affect only a single function argument, the [`AddArgumentError(position int, summary string, detail string)` method](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/diag#Diagnostics.AddArgumentError) and [`AddArgumentWarning(position int, summary string, detail string)` method](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/diag#Diagnostics.AddArgumentWarning) append a new error or warning diagnostic pointing specifically at the function argument. This provides additional context to practitioners, such as showing the specific line(s) and value(s) of configuration where possible. - -For example: - -```go -func (f ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { - // ... other logic ... - - // Add warning diagnostic associated with first function argument position - resp.Diagnostics.AddArgumentWarning(0, "Example Warning Summary", "Example Warning Detail") - - // ... other logic ... -} -``` - ### Consistent Diagnostic Creation Create a helper function in your provider code using the diagnostic creation functions available in the [`diag` package](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/diag) to generate consistent diagnostics for types of errors/warnings. It is also possible to use [custom diagnostics types](#custom-diagnostics-types) to accomplish this same goal. diff --git a/website/docs/plugin/framework/functions/errors.mdx b/website/docs/plugin/framework/functions/errors.mdx new file mode 100644 index 000000000..8ff7b1501 --- /dev/null +++ b/website/docs/plugin/framework/functions/errors.mdx @@ -0,0 +1,162 @@ +--- +page_title: 'Plugin Development - Framework: Function Errors' +description: |- + How to return function errors from the Terraform provider development + framework. +--- + +# Returning Function Errors + +Providers use [`FuncError`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#FuncError) to +surface a practitioner-facing error generated during execution of provider-defined functions. These errors are +returned from Terraform CLI at the end of command output: + +```console +$ terraform plan +# ... other plan output ... +╷ +│ Error: Error in function call +│ +│ on example.tf line #: +│ #: source configuration line +│ +│ Call to function "{FUNCTION}" failed: {TEXT}. +``` + +```console +$ terraform plan +# ... other plan output ... +╷ +│ Error: Invalid function argument +│ +│ on example.tf line #: +│ #: source configuration line +│ +│ Invalid value for "{PARAMETER_NAME}" parameter: {TEXT}. +``` + +In the framework, you may encounter them in response structs or as returns from +provider-defined function execution.: + +```go +func (f *ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { +``` + +This is the most common form for `FuncError`: a single error whose text +is the concatenated error text from one or more errors. This approach allows +your provider to inform practitioners about all relevant errors at the same +time, allowing practitioners to fix their configuration or environment more +quickly. You should only concatenate a `FuncError` and never replace or +remove information it. + +The next section will detail the concepts and typical behaviors of +function error, while the final section will outline the typical methods for +working with function error, using functionality from the available +[`function` package](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function). + +## Function Error Concepts + +### Text + +`Text` is a practitioner-oriented description of the problem. This should +contain sufficient detail to provide both general and more specific information +regarding the issue. For example "Error executing function: foo can only contain +letters, numbers, and digits." + +### FunctionArgument + +`FunctionArgument` is a zero-based, int64 value that identifies the specific +function argument position that caused the error. Only errors that pertain +to a function argument will include this information. + +### Working With Existing Function Errors + +#### ConcatFuncErrors + +When receiving `function.FuncError` from a function or method, such as +`Run()`, these should typically be concatenated with the +response function error for the method. This can be accomplished with the +[`ConcatFuncErrors(in ...*FuncError)` method](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#ConcatFuncErrors). + +For example: + +```go +func (f *ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + var boolArg bool + var stringArg string + + resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.Get(ctx, &boolArg, &stringArg)) + + // ... other logic ... +} +``` + +This method automatically ignores `nil` function errors. + +### Creating Function Errors + +To craft the message of a function error, it is recommended to use sufficient +detail to convey both the cause of the error and as much contextual, +troubleshooting, and next action information as possible. These details can +use newlines for easier readability where necessary. + +#### NewFuncError + +When creating function errors where a `function.FunctionError` is already available, +such as within a response type, the [`ConcatFuncErrors(in ...*FuncError)` function](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#NewFuncError.AddError) +can be used with the [`NewFuncError(text string)` function](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#NewFuncError) to concatenate a new +function error. + +For example: + +```go +func (f *ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + // ... prior logic ... + + val, err := // operation that may return an error + + if err != nil { + resp.Error = ConcatFuncErrors(resp.Error, function.NewFuncError("Error performing operation: " + err.Error())) + return + } + + // ... further logic ... +} +``` + +#### NewArgumentFuncError + +When creating function errors that affect only a single function argument, the [`NewArgumentFuncError(functionArgument int, msg string)` function](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#NewArgumentFuncError) +can be used in conjunction with the [`ConcatFuncErrors(in ...*FuncError)` function](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#NewArgumentFuncError). This provides additional context to practitioners, such as showing the specific line(s) and value(s) of configuration where possible. + +For example: + +```go +func (f ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + // ... other logic ... + + // Add function error associated with first function argument position + resp.Error = function.ConcatFuncErrors(resp.Error, function.NewArgumentFuncError(0, "Example Error Summary: Example Error Detail")) + + // ... other logic ... +} +``` + +#### FuncErrorFromDiags + +A function error is created from diagnostics by using the [`FuncErrorFromDiags(context.Context, diag.Diagnostics)` function](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#FuncErrorFromDiags). The function error will contain the concatenated summary and details of error-level +diagnostics. + +~> **Note**: The [`FuncErrorFromDiags(context.Context, diag.Diagnostics)` function](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#FuncErrorFromDiags) does not include warning-level diagnostics in the function error. Warning-level diagnostics are logged instead. + +For example: + +```go +func (f *ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + // ... other logic ... + + _, diags := // operation that may return diagnostics + + resp.Error = function.ConcatFuncErrors(resp.Error, function.FuncErrorFromDiags(ctx, diags)) +} +``` \ No newline at end of file diff --git a/website/docs/plugin/framework/functions/implementation.mdx b/website/docs/plugin/framework/functions/implementation.mdx index 9da2c893f..9fe731837 100644 --- a/website/docs/plugin/framework/functions/implementation.mdx +++ b/website/docs/plugin/framework/functions/implementation.mdx @@ -63,10 +63,10 @@ func (f *EchoFunction) Run(ctx context.Context, req function.RunRequest, resp *f var input string // Read Terraform argument data into the variable - resp.Diagnostics.Append(req.Arguments.Get(ctx, &input)...) + resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.Get(ctx, &input)) // Set the result to the same data - resp.Diagnostics.Append(resp.Result.Set(ctx, &input)...) + resp.Error = function.ConcatFuncErrors(resp.Error, resp.Result.Set(ctx, input)) } ``` @@ -137,13 +137,13 @@ Implement the `Run` method by: 1. Performing any computational logic. 1. Setting the result value, based on the return definition, into the [`function.RunResponse.Result` field](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#RunResponse.Result). Refer to the [returns](/terraform/plugin/framework/functions/returns) documentation for details about all available return types and how to handle data with each type. -If the logic needs to return [warning or error diagnostics](/terraform/plugin/framework/diagnostics), they can be added into the [`function.RunResponse.Diagnostics` field](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#RunResponse.Diagnostics). +If the logic needs to return a [function error](/terraform/plugin/framework/functions/errors), it can be added into the [`function.RunResponse.Error` field](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#RunResponse.Error). ### Reading Argument Data The framework supports two methodologies for reading argument data from the [`function.RunRequest.Arguments` field](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#RunRequest.Arguments), which is of the [`function.ArgumentsData` type](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#ArgumentsData). -The first option is using the [`(function.ArgumentsData).Get()` method](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#ArgumentsData.Get) to read all arguments at once. The framework will return errors if the number and types of target variables does not match the argument data. +The first option is using the [`(function.ArgumentsData).Get()` method](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#ArgumentsData.Get) to read all arguments at once. The framework will return an error if the number and types of target variables does not match the argument data. In this example, the parameters are defined as a boolean and string which are read into Go built-in `bool` and `string` variables since they do not opt into null or unknown value handling: @@ -162,13 +162,13 @@ func (f *ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp var boolArg bool var stringArg string - resp.Diagnostics.Append(req.Arguments.Get(ctx, &boolArg, &stringArg)...) + resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.Get(ctx, &boolArg, &stringArg)) // ... other logic ... } ``` -The second option is using [`(function.ArgumentsData).GetArgument()` method](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#ArgumentsData.GetArgument) to read individual arguments. The framework will return errors if the argument position does not exist or if the type of the target variable does not match the argument data. +The second option is using [`(function.ArgumentsData).GetArgument()` method](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#ArgumentsData.GetArgument) to read individual arguments. The framework will return an error if the argument position does not exist or if the type of the target variable does not match the argument data. In this example, the parameters are defined as a boolean and string and the first argument is read into a Go built-in `bool` variable since it does not opt into null or unknown value handling: @@ -186,7 +186,7 @@ func (f *ExampleFunction) Definition(ctx context.Context, req function.Definitio func (f *ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { var boolArg bool - resp.Diagnostics.Append(req.Arguments.GetArgument(ctx, 0, &boolArg)...) + resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.GetArgument(ctx, 0, &boolArg)) // ... other logic ... } @@ -208,7 +208,7 @@ func (f *ExampleFunction) Definition(ctx context.Context, req function.Definitio function.BoolParameter{}, }, VariadicParameter: function.StringParameter{ - Name: "variadic_param", + Name: "variadic_param", }, } } @@ -217,15 +217,15 @@ func (f *ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp var boolArg bool var stringVarg []string - resp.Diagnostics.Append(req.Arguments.Get(ctx, &boolArg, &stringVarg)...) + resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.Get(ctx, &boolArg, &stringVarg)) // ... other logic ... } ``` -If necessary to return diagnostics for a specific variadic argument, note that Terraform treats each zero-based argument position individually unlike how the framework exposes the argument data. Add the number of non-variadic parameters (if any) to the variadic argument tuple element index to ensure the diagnostic is aligned to the correct argument in the configuration. +If it is necessary to return a [function error](/terraform/plugin/framework/functions/errors) for a specific variadic argument, note that Terraform treats each zero-based argument position individually unlike how the framework exposes the argument data. Add the number of non-variadic parameters (if any) to the variadic argument tuple element index to ensure the error is aligned to the correct argument in the configuration. -In this example with two parameters and one variadic parameter, warning diagnostics are returned for all variadic arguments: +In this example with two parameters and one variadic parameter, an error is returned for variadic arguments: ```go func (f *ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { @@ -236,7 +236,7 @@ func (f *ExampleFunction) Definition(ctx context.Context, req function.Definitio function.Int64Parameter{}, }, VariadicParameter: function.StringParameter{ - Name: "variadic_param", + Name: "variadic_param", }, } } @@ -246,11 +246,11 @@ func (f *ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp var int64Arg int64 var stringVarg []string - resp.Diagnostics.Append(req.Arguments.Get(ctx, &boolArg, &int64arg, &stringVarg)...) + resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.Get(ctx, &boolArg, &int64arg, &stringVarg)) for index, element := range stringVarg { // Added by 2 to match the definition including two parameters. - resp.Diagnostic.AddArgumentWarning(2+index, "example summary", "example detail") + resp.Error = function.ConcatFuncErrors(resp.Error, function.NewArgumentFuncError(2+index, "example summary: example detail")) } // ... other logic ... @@ -277,7 +277,7 @@ func (f *ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp // Value based on the return type. Returns can also use the framework type system. result := "hardcoded example" - resp.Diagnostics.Append(resp.Result.Set(ctx, result)...) + resp.Error = function.ConcatFuncErrors(resp.Error, resp.Result.Set(ctx, result)) } ``` diff --git a/website/docs/plugin/framework/functions/parameters/bool.mdx b/website/docs/plugin/framework/functions/parameters/bool.mdx index 6f8147045..7607f1bb3 100644 --- a/website/docs/plugin/framework/functions/parameters/bool.mdx +++ b/website/docs/plugin/framework/functions/parameters/bool.mdx @@ -83,7 +83,7 @@ func (f ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp // var boolArg *bool // e.g. with AllowNullValue, where Go nil equals Terraform null // var boolArg types.Bool // e.g. with AllowUnknownValues or AllowNullValue - resp.Diagnostics.Append(req.Arguments.Get(ctx, &boolArg)...) + resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.Get(ctx, &boolArg)) // boolArg is now populated // ... other logic ... diff --git a/website/docs/plugin/framework/functions/parameters/float64.mdx b/website/docs/plugin/framework/functions/parameters/float64.mdx index be8ebf464..d9b26196d 100644 --- a/website/docs/plugin/framework/functions/parameters/float64.mdx +++ b/website/docs/plugin/framework/functions/parameters/float64.mdx @@ -89,7 +89,7 @@ func (f ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp // var float64Arg *float64 // e.g. with AllowNullValue, where Go nil equals Terraform null // var float64Arg types.Float64 // e.g. with AllowUnknownValues or AllowNullValue - resp.Diagnostics.Append(req.Arguments.Get(ctx, &float64Arg)...) + resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.Get(ctx, &float64Arg)) // float64Arg is now populated // ... other logic ... diff --git a/website/docs/plugin/framework/functions/parameters/int64.mdx b/website/docs/plugin/framework/functions/parameters/int64.mdx index dd116b373..e09432547 100644 --- a/website/docs/plugin/framework/functions/parameters/int64.mdx +++ b/website/docs/plugin/framework/functions/parameters/int64.mdx @@ -89,7 +89,7 @@ func (f ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp // var int64Arg *int64 // e.g. with AllowNullValue, where Go nil equals Terraform null // var int64Arg types.Int64 // e.g. with AllowUnknownValues or AllowNullValue - resp.Diagnostics.Append(req.Arguments.Get(ctx, &int64Arg)...) + resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.Get(ctx, &int64Arg)) // int64Arg is now populated // ... other logic ... diff --git a/website/docs/plugin/framework/functions/parameters/list.mdx b/website/docs/plugin/framework/functions/parameters/list.mdx index 3cec58e83..9109ba680 100644 --- a/website/docs/plugin/framework/functions/parameters/list.mdx +++ b/website/docs/plugin/framework/functions/parameters/list.mdx @@ -92,7 +92,7 @@ func (f ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp var listArg []*string // Go nil equals Terraform null // var listArg types.List // e.g. with AllowUnknownValues - resp.Diagnostics.Append(req.Arguments.Get(ctx, &listArg)...) + resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.Get(ctx, &listArg)) // listArg is now populated // ... other logic ... diff --git a/website/docs/plugin/framework/functions/parameters/map.mdx b/website/docs/plugin/framework/functions/parameters/map.mdx index 3c34463d2..158a2612b 100644 --- a/website/docs/plugin/framework/functions/parameters/map.mdx +++ b/website/docs/plugin/framework/functions/parameters/map.mdx @@ -95,7 +95,7 @@ func (f ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp var mapArg map[string]*string // Go nil equals Terraform null // var mapArg types.Map // e.g. with AllowUnknownValues - resp.Diagnostics.Append(req.Arguments.Get(ctx, &mapArg)...) + resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.Get(ctx, &mapArg)) // mapArg is now populated // ... other logic ... diff --git a/website/docs/plugin/framework/functions/parameters/number.mdx b/website/docs/plugin/framework/functions/parameters/number.mdx index c8a0801e6..f97a40289 100644 --- a/website/docs/plugin/framework/functions/parameters/number.mdx +++ b/website/docs/plugin/framework/functions/parameters/number.mdx @@ -87,7 +87,7 @@ func (f ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp var numberArg *big.Float // var numberArg types.Number // e.g. with AllowUnknownValues or AllowNullValue - resp.Diagnostics.Append(req.Arguments.Get(ctx, &numberArg)...) + resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.Get(ctx, &numberArg)) // numberArg is now populated // ... other logic ... diff --git a/website/docs/plugin/framework/functions/parameters/object.mdx b/website/docs/plugin/framework/functions/parameters/object.mdx index 0ae535021..511a3afa3 100644 --- a/website/docs/plugin/framework/functions/parameters/object.mdx +++ b/website/docs/plugin/framework/functions/parameters/object.mdx @@ -110,7 +110,7 @@ func (f ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp // } // var objectArg types.Object // e.g. with AllowUnknownValues or AllowNullValues - resp.Diagnostics.Append(req.Arguments.Get(ctx, &objectArg)...) + resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.Get(ctx, &objectArg)) // objectArg is now populated // ... other logic ... diff --git a/website/docs/plugin/framework/functions/parameters/set.mdx b/website/docs/plugin/framework/functions/parameters/set.mdx index 2e4bc5fe9..dfef8490c 100644 --- a/website/docs/plugin/framework/functions/parameters/set.mdx +++ b/website/docs/plugin/framework/functions/parameters/set.mdx @@ -92,7 +92,7 @@ func (f ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp var setArg []*string // Go nil equals Terraform null // var setArg types.Set // e.g. with AllowUnknownValues - resp.Diagnostics.Append(req.Arguments.Get(ctx, &setArg)...) + resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.Get(ctx, &setArg)) // setArg is now populated // ... other logic ... diff --git a/website/docs/plugin/framework/functions/parameters/string.mdx b/website/docs/plugin/framework/functions/parameters/string.mdx index 67ea3aa73..5f04df8af 100644 --- a/website/docs/plugin/framework/functions/parameters/string.mdx +++ b/website/docs/plugin/framework/functions/parameters/string.mdx @@ -83,7 +83,7 @@ func (f ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp // var stringArg *string // e.g. with AllowNullValue, where Go nil equals Terraform null // var stringArg types.String // e.g. with AllowUnknownValues or AllowNullValue - resp.Diagnostics.Append(req.Arguments.Get(ctx, &stringArg)...) + resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.Get(ctx, &stringArg)) // stringArg is now populated // ... other logic ... diff --git a/website/docs/plugin/framework/functions/returns/bool.mdx b/website/docs/plugin/framework/functions/returns/bool.mdx index 8b4cd5085..c20a5a27e 100644 --- a/website/docs/plugin/framework/functions/returns/bool.mdx +++ b/website/docs/plugin/framework/functions/returns/bool.mdx @@ -60,6 +60,6 @@ func (f ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp // hardcoded value for example brevity result := true - resp.Diagnostics.Append(resp.Result.Set(ctx, &result)...) + resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.Set(ctx, &result)) } ``` diff --git a/website/docs/plugin/framework/functions/returns/float64.mdx b/website/docs/plugin/framework/functions/returns/float64.mdx index 564c63823..b60f1fe29 100644 --- a/website/docs/plugin/framework/functions/returns/float64.mdx +++ b/website/docs/plugin/framework/functions/returns/float64.mdx @@ -66,6 +66,6 @@ func (f ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp // hardcoded value for example brevity result := 1.23 - resp.Diagnostics.Append(resp.Result.Set(ctx, &result)...) + resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.Set(ctx, &result)) } ``` diff --git a/website/docs/plugin/framework/functions/returns/int64.mdx b/website/docs/plugin/framework/functions/returns/int64.mdx index 4c9f94b84..9a24ca6e2 100644 --- a/website/docs/plugin/framework/functions/returns/int64.mdx +++ b/website/docs/plugin/framework/functions/returns/int64.mdx @@ -66,6 +66,6 @@ func (f ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp // hardcoded value for example brevity result := 123 - resp.Diagnostics.Append(resp.Result.Set(ctx, &result)...) + resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.Set(ctx, &result)) } ``` diff --git a/website/docs/plugin/framework/functions/returns/list.mdx b/website/docs/plugin/framework/functions/returns/list.mdx index 8b203d9da..625486ef5 100644 --- a/website/docs/plugin/framework/functions/returns/list.mdx +++ b/website/docs/plugin/framework/functions/returns/list.mdx @@ -65,6 +65,6 @@ func (f ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp // hardcoded value for example brevity result := []string{"one", "two"} - resp.Diagnostics.Append(resp.Result.Set(ctx, &result)...) + resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.Set(ctx, &result)) } ``` diff --git a/website/docs/plugin/framework/functions/returns/map.mdx b/website/docs/plugin/framework/functions/returns/map.mdx index 968b15f03..e95549bea 100644 --- a/website/docs/plugin/framework/functions/returns/map.mdx +++ b/website/docs/plugin/framework/functions/returns/map.mdx @@ -68,6 +68,6 @@ func (f ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp "key2": "value2", } - resp.Diagnostics.Append(resp.Result.Set(ctx, &result)...) + resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.Set(ctx, &result)) } ``` diff --git a/website/docs/plugin/framework/functions/returns/number.mdx b/website/docs/plugin/framework/functions/returns/number.mdx index 17c52a05a..f05419f70 100644 --- a/website/docs/plugin/framework/functions/returns/number.mdx +++ b/website/docs/plugin/framework/functions/returns/number.mdx @@ -66,6 +66,6 @@ func (f ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp // hardcoded value for example brevity result := big.NewFloat(1.23) - resp.Diagnostics.Append(resp.Result.Set(ctx, &result)...) + resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.Set(ctx, &result)) } ``` diff --git a/website/docs/plugin/framework/functions/returns/object.mdx b/website/docs/plugin/framework/functions/returns/object.mdx index de70109f1..299b95062 100644 --- a/website/docs/plugin/framework/functions/returns/object.mdx +++ b/website/docs/plugin/framework/functions/returns/object.mdx @@ -77,6 +77,6 @@ func (f ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp Attr2: 123, } - resp.Diagnostics.Append(resp.Result.Set(ctx, &result)...) + resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.Set(ctx, &result)) } ``` diff --git a/website/docs/plugin/framework/functions/returns/set.mdx b/website/docs/plugin/framework/functions/returns/set.mdx index af6dac7dd..00a4211c7 100644 --- a/website/docs/plugin/framework/functions/returns/set.mdx +++ b/website/docs/plugin/framework/functions/returns/set.mdx @@ -65,6 +65,6 @@ func (f ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp // hardcoded value for example brevity result := []string{"one", "two"} - resp.Diagnostics.Append(resp.Result.Set(ctx, &result)...) + resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.Set(ctx, &result)) } ``` diff --git a/website/docs/plugin/framework/functions/returns/string.mdx b/website/docs/plugin/framework/functions/returns/string.mdx index f5f4e8fe3..bfed72434 100644 --- a/website/docs/plugin/framework/functions/returns/string.mdx +++ b/website/docs/plugin/framework/functions/returns/string.mdx @@ -60,6 +60,6 @@ func (f ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp // hardcoded value for example brevity result := "example" - resp.Diagnostics.Append(resp.Result.Set(ctx, &result)...) + resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.Set(ctx, &result)) } ``` diff --git a/website/docs/plugin/framework/functions/testing.mdx b/website/docs/plugin/framework/functions/testing.mdx index 2e20de799..0580944a2 100644 --- a/website/docs/plugin/framework/functions/testing.mdx +++ b/website/docs/plugin/framework/functions/testing.mdx @@ -25,7 +25,7 @@ Testing a provider-defined function should ensure at least the following behavio * For any list, map, object, and set parameters, null values for collection elements or object attributes. The `AllowNullValue` parameter setting does not affect Terraform sending these types of null values. * If any parameters enable `AllowNullValue`, null values for those arguments. * If any parameters enable `AllowUnknownValues`, unknown values for those arguments. -* Any diagnostics, such as argument validation errors. +* Any errors, such as argument validation errors. ## Acceptance Testing @@ -65,7 +65,7 @@ output "test" { }) } -// The example implementation does not return any error diagnostics, however +// The example implementation does not return any errors, however // this acceptance test verifies how the function should behave if it did. func TestEchoFunction_Invalid(t *testing.T) { t.Parallel() @@ -154,7 +154,6 @@ import ( "example.com/terraform-provider-example/internal/provider" "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -196,7 +195,7 @@ func TestEchoFunctionRun(t *testing.T) { Result: function.NewResultData(types.StringValue("test-value")), }, }, - // The example implementation does not return any diagnostics, however + // The example implementation does not return an error, however // this test case shows how the function would be expected to behave if // it did. "value-invalid": { @@ -204,9 +203,7 @@ func TestEchoFunctionRun(t *testing.T) { Arguments: function.NewArgumentsData(types.StringValue()), }, Expected: function.RunResponse{ - Diagnostics: diag.Diagnostics{ - diag.NewArgumentErrorDiagnostic(0, "error summary", "error detail"), - }, + Error: function.NewArgumentFuncError(0, "error summary: error detail"), Result: function.NewResultData(types.StringUnknown()), }, },