From 791e37bc10ac1f76fd585801a0696663ef73c988 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Tue, 19 Dec 2023 18:02:10 +0100 Subject: [PATCH] function: Initial provider defined functions implementation (#889) Reference: https://github.com/hashicorp/terraform-plugin-go/pull/351 The next versions of the plugin protocol (5.5/6.5) include support for provider defined functions. This change includes initial implementation of that support including: - Temporarily pointing at terraform-plugin-go with provider function support (will be pointed at final terraform-plugin-go release before merge) - New `function` package with all exposed Go types for provider developers to implement provider functions - New `diag` package support for diagnostics with optional function argument information - Implementation of new `GetFunctions` and `CallFunction` RPCs in the internal framework server, protocol 5/6 servers, and data handling between all layers - Initial website documentation This functionality will be released as technical preview without compatibility promises until Terraform 1.8 is generally available. Go and website documentation include additional callouts about the compatibility of this functionality. Co-authored-by: Austin Valle --- .../ENHANCEMENTS-20231211-125057.yaml | 6 + .../ENHANCEMENTS-20231211-125151.yaml | 6 + .../ENHANCEMENTS-20231211-125843.yaml | 6 + .../unreleased/FEATURES-20231211-125122.yaml | 5 + .../unreleased/NOTES-20231214-082931.yaml | 6 + diag/argument_error_diagnostic.go | 13 + diag/argument_warning_diagnostic.go | 13 + diag/diagnostic.go | 15 + diag/diagnostics.go | 12 + diag/diagnostics_test.go | 124 +++++ diag/with_function_argument.go | 54 +++ function/arguments_data.go | 179 ++++++++ function/arguments_data_test.go | 353 ++++++++++++++ function/bool_parameter.go | 108 +++++ function/bool_parameter_test.go | 248 ++++++++++ function/bool_return.go | 51 +++ function/bool_return_test.go | 48 ++ function/definition.go | 130 ++++++ function/definition_test.go | 186 ++++++++ function/doc.go | 18 + function/float64_parameter.go | 105 +++++ function/float64_parameter_test.go | 248 ++++++++++ function/float64_return.go | 52 +++ function/float64_return_test.go | 48 ++ function/function.go | 27 ++ function/int64_parameter.go | 104 +++++ function/int64_parameter_test.go | 248 ++++++++++ function/int64_return.go | 51 +++ function/int64_return_test.go | 48 ++ function/list_parameter.go | 110 +++++ function/list_parameter_test.go | 260 +++++++++++ function/list_return.go | 59 +++ function/list_return_test.go | 60 +++ function/map_parameter.go | 110 +++++ function/map_parameter_test.go | 260 +++++++++++ function/map_return.go | 59 +++ function/map_return_test.go | 60 +++ function/metadata.go | 20 + function/number_parameter.go | 103 +++++ function/number_parameter_test.go | 248 ++++++++++ function/number_return.go | 52 +++ function/number_return_test.go | 48 ++ function/object_parameter.go | 112 +++++ function/object_parameter_test.go | 268 +++++++++++ function/object_return.go | 55 +++ function/object_return_test.go | 68 +++ function/parameter.go | 40 ++ function/pointer_test.go | 8 + function/result_data.go | 63 +++ function/result_data_test.go | 86 ++++ function/return.go | 26 ++ function/run.go | 30 ++ function/set_parameter.go | 110 +++++ function/set_parameter_test.go | 260 +++++++++++ function/set_return.go | 59 +++ function/set_return_test.go | 60 +++ function/string_parameter.go | 104 +++++ function/string_parameter_test.go | 248 ++++++++++ function/string_return.go | 51 +++ function/string_return_test.go | 48 ++ internal/fromproto5/arguments_data.go | 152 ++++++ internal/fromproto5/arguments_data_test.go | 409 +++++++++++++++++ internal/fromproto5/callfunction.go | 32 ++ internal/fromproto5/callfunction_test.go | 100 ++++ internal/fromproto5/getfunctions.go | 23 + internal/fromproto5/getfunctions_test.go | 46 ++ internal/fromproto6/arguments_data.go | 148 ++++++ internal/fromproto6/arguments_data_test.go | 409 +++++++++++++++++ internal/fromproto6/callfunction.go | 32 ++ internal/fromproto6/callfunction_test.go | 100 ++++ internal/fromproto6/getfunctions.go | 23 + internal/fromproto6/getfunctions_test.go | 46 ++ internal/fwserver/server.go | 24 + internal/fwserver/server_callfunction.go | 56 +++ internal/fwserver/server_callfunction_test.go | 431 ++++++++++++++++++ internal/fwserver/server_functions.go | 194 ++++++++ internal/fwserver/server_getfunctions.go | 37 ++ internal/fwserver/server_getfunctions_test.go | 215 +++++++++ internal/fwserver/server_getmetadata.go | 14 + internal/fwserver/server_getmetadata_test.go | 131 ++++++ internal/fwserver/server_getproviderschema.go | 24 +- .../fwserver/server_getproviderschema_test.go | 225 ++++++++- internal/logging/keys.go | 3 + internal/proto5server/serve_test.go | 12 + internal/proto5server/server_callfunction.go | 50 ++ .../proto5server/server_callfunction_test.go | 275 +++++++++++ internal/proto5server/server_getfunctions.go | 27 ++ .../proto5server/server_getfunctions_test.go | 179 ++++++++ .../proto5server/server_getmetadata_test.go | 136 ++++++ .../server_getproviderschema_test.go | 174 +++++++ internal/proto6server/serve_test.go | 12 + internal/proto6server/server_callfunction.go | 50 ++ .../proto6server/server_callfunction_test.go | 275 +++++++++++ internal/proto6server/server_getfunctions.go | 27 ++ .../proto6server/server_getfunctions_test.go | 179 ++++++++ .../proto6server/server_getmetadata_test.go | 136 ++++++ .../server_getproviderschema_test.go | 174 +++++++ internal/testing/testprovider/function.go | 47 ++ .../testprovider/providerwithfunctions.go | 33 ++ internal/testing/testtypes/list.go | 25 +- internal/testing/testtypes/map.go | 25 +- internal/testing/testtypes/object.go | 25 +- internal/testing/testtypes/set.go | 25 +- internal/toproto5/callfunction.go | 30 ++ internal/toproto5/callfunction_test.go | 76 +++ internal/toproto5/diagnostics.go | 5 + internal/toproto5/diagnostics_test.go | 20 + internal/toproto5/function.go | 128 ++++++ internal/toproto5/function_test.go | 429 +++++++++++++++++ internal/toproto5/getfunctions.go | 30 ++ internal/toproto5/getfunctions_test.go | 239 ++++++++++ internal/toproto5/getmetadata.go | 9 +- internal/toproto5/getmetadata_test.go | 28 ++ internal/toproto5/getproviderschema.go | 9 +- internal/toproto5/getproviderschema_test.go | 284 ++++++++++++ internal/toproto5/pointer_test.go | 8 + internal/toproto6/callfunction.go | 30 ++ internal/toproto6/callfunction_test.go | 76 +++ internal/toproto6/diagnostics.go | 5 + internal/toproto6/diagnostics_test.go | 20 + internal/toproto6/function.go | 128 ++++++ internal/toproto6/function_test.go | 429 +++++++++++++++++ internal/toproto6/getfunctions.go | 30 ++ internal/toproto6/getfunctions_test.go | 239 ++++++++++ internal/toproto6/getmetadata.go | 9 +- internal/toproto6/getmetadata_test.go | 28 ++ internal/toproto6/getproviderschema.go | 9 +- internal/toproto6/getproviderschema_test.go | 284 ++++++++++++ internal/toproto6/pointer_test.go | 8 + provider/provider.go | 20 + website/data/plugin-framework-nav-data.json | 115 +++++ website/docs/plugin/framework/diagnostics.mdx | 44 +- .../plugin/framework/functions/concepts.mdx | 67 +++ .../framework/functions/documentation.mdx | 121 +++++ .../framework/functions/implementation.mdx | 313 +++++++++++++ .../docs/plugin/framework/functions/index.mdx | 33 ++ .../framework/functions/parameters/bool.mdx | 91 ++++ .../functions/parameters/float64.mdx | 97 ++++ .../framework/functions/parameters/index.mdx | 48 ++ .../framework/functions/parameters/int64.mdx | 97 ++++ .../framework/functions/parameters/list.mdx | 100 ++++ .../framework/functions/parameters/map.mdx | 103 +++++ .../framework/functions/parameters/number.mdx | 95 ++++ .../framework/functions/parameters/object.mdx | 118 +++++ .../framework/functions/parameters/set.mdx | 100 ++++ .../framework/functions/parameters/string.mdx | 91 ++++ .../framework/functions/returns/bool.mdx | 65 +++ .../framework/functions/returns/float64.mdx | 71 +++ .../framework/functions/returns/index.mdx | 48 ++ .../framework/functions/returns/int64.mdx | 71 +++ .../framework/functions/returns/list.mdx | 70 +++ .../framework/functions/returns/map.mdx | 73 +++ .../framework/functions/returns/number.mdx | 71 +++ .../framework/functions/returns/object.mdx | 82 ++++ .../framework/functions/returns/set.mdx | 70 +++ .../framework/functions/returns/string.mdx | 65 +++ .../plugin/framework/functions/testing.mdx | 231 ++++++++++ 157 files changed, 15542 insertions(+), 35 deletions(-) create mode 100644 .changes/unreleased/ENHANCEMENTS-20231211-125057.yaml create mode 100644 .changes/unreleased/ENHANCEMENTS-20231211-125151.yaml create mode 100644 .changes/unreleased/ENHANCEMENTS-20231211-125843.yaml create mode 100644 .changes/unreleased/FEATURES-20231211-125122.yaml create mode 100644 .changes/unreleased/NOTES-20231214-082931.yaml create mode 100644 diag/argument_error_diagnostic.go create mode 100644 diag/argument_warning_diagnostic.go create mode 100644 diag/with_function_argument.go create mode 100644 function/arguments_data.go create mode 100644 function/arguments_data_test.go create mode 100644 function/bool_parameter.go create mode 100644 function/bool_parameter_test.go create mode 100644 function/bool_return.go create mode 100644 function/bool_return_test.go create mode 100644 function/definition.go create mode 100644 function/definition_test.go create mode 100644 function/doc.go create mode 100644 function/float64_parameter.go create mode 100644 function/float64_parameter_test.go create mode 100644 function/float64_return.go create mode 100644 function/float64_return_test.go create mode 100644 function/function.go create mode 100644 function/int64_parameter.go create mode 100644 function/int64_parameter_test.go create mode 100644 function/int64_return.go create mode 100644 function/int64_return_test.go create mode 100644 function/list_parameter.go create mode 100644 function/list_parameter_test.go create mode 100644 function/list_return.go create mode 100644 function/list_return_test.go create mode 100644 function/map_parameter.go create mode 100644 function/map_parameter_test.go create mode 100644 function/map_return.go create mode 100644 function/map_return_test.go create mode 100644 function/metadata.go create mode 100644 function/number_parameter.go create mode 100644 function/number_parameter_test.go create mode 100644 function/number_return.go create mode 100644 function/number_return_test.go create mode 100644 function/object_parameter.go create mode 100644 function/object_parameter_test.go create mode 100644 function/object_return.go create mode 100644 function/object_return_test.go create mode 100644 function/parameter.go create mode 100644 function/pointer_test.go create mode 100644 function/result_data.go create mode 100644 function/result_data_test.go create mode 100644 function/return.go create mode 100644 function/run.go create mode 100644 function/set_parameter.go create mode 100644 function/set_parameter_test.go create mode 100644 function/set_return.go create mode 100644 function/set_return_test.go create mode 100644 function/string_parameter.go create mode 100644 function/string_parameter_test.go create mode 100644 function/string_return.go create mode 100644 function/string_return_test.go create mode 100644 internal/fromproto5/arguments_data.go create mode 100644 internal/fromproto5/arguments_data_test.go create mode 100644 internal/fromproto5/callfunction.go create mode 100644 internal/fromproto5/callfunction_test.go create mode 100644 internal/fromproto5/getfunctions.go create mode 100644 internal/fromproto5/getfunctions_test.go create mode 100644 internal/fromproto6/arguments_data.go create mode 100644 internal/fromproto6/arguments_data_test.go create mode 100644 internal/fromproto6/callfunction.go create mode 100644 internal/fromproto6/callfunction_test.go create mode 100644 internal/fromproto6/getfunctions.go create mode 100644 internal/fromproto6/getfunctions_test.go create mode 100644 internal/fwserver/server_callfunction.go create mode 100644 internal/fwserver/server_callfunction_test.go create mode 100644 internal/fwserver/server_functions.go create mode 100644 internal/fwserver/server_getfunctions.go create mode 100644 internal/fwserver/server_getfunctions_test.go create mode 100644 internal/proto5server/server_callfunction.go create mode 100644 internal/proto5server/server_callfunction_test.go create mode 100644 internal/proto5server/server_getfunctions.go create mode 100644 internal/proto5server/server_getfunctions_test.go create mode 100644 internal/proto6server/server_callfunction.go create mode 100644 internal/proto6server/server_callfunction_test.go create mode 100644 internal/proto6server/server_getfunctions.go create mode 100644 internal/proto6server/server_getfunctions_test.go create mode 100644 internal/testing/testprovider/function.go create mode 100644 internal/testing/testprovider/providerwithfunctions.go create mode 100644 internal/toproto5/callfunction.go create mode 100644 internal/toproto5/callfunction_test.go create mode 100644 internal/toproto5/function.go create mode 100644 internal/toproto5/function_test.go create mode 100644 internal/toproto5/getfunctions.go create mode 100644 internal/toproto5/getfunctions_test.go create mode 100644 internal/toproto5/pointer_test.go create mode 100644 internal/toproto6/callfunction.go create mode 100644 internal/toproto6/callfunction_test.go create mode 100644 internal/toproto6/function.go create mode 100644 internal/toproto6/function_test.go create mode 100644 internal/toproto6/getfunctions.go create mode 100644 internal/toproto6/getfunctions_test.go create mode 100644 internal/toproto6/pointer_test.go create mode 100644 website/docs/plugin/framework/functions/concepts.mdx create mode 100644 website/docs/plugin/framework/functions/documentation.mdx create mode 100644 website/docs/plugin/framework/functions/implementation.mdx create mode 100644 website/docs/plugin/framework/functions/index.mdx create mode 100644 website/docs/plugin/framework/functions/parameters/bool.mdx create mode 100644 website/docs/plugin/framework/functions/parameters/float64.mdx create mode 100644 website/docs/plugin/framework/functions/parameters/index.mdx create mode 100644 website/docs/plugin/framework/functions/parameters/int64.mdx create mode 100644 website/docs/plugin/framework/functions/parameters/list.mdx create mode 100644 website/docs/plugin/framework/functions/parameters/map.mdx create mode 100644 website/docs/plugin/framework/functions/parameters/number.mdx create mode 100644 website/docs/plugin/framework/functions/parameters/object.mdx create mode 100644 website/docs/plugin/framework/functions/parameters/set.mdx create mode 100644 website/docs/plugin/framework/functions/parameters/string.mdx create mode 100644 website/docs/plugin/framework/functions/returns/bool.mdx create mode 100644 website/docs/plugin/framework/functions/returns/float64.mdx create mode 100644 website/docs/plugin/framework/functions/returns/index.mdx create mode 100644 website/docs/plugin/framework/functions/returns/int64.mdx create mode 100644 website/docs/plugin/framework/functions/returns/list.mdx create mode 100644 website/docs/plugin/framework/functions/returns/map.mdx create mode 100644 website/docs/plugin/framework/functions/returns/number.mdx create mode 100644 website/docs/plugin/framework/functions/returns/object.mdx create mode 100644 website/docs/plugin/framework/functions/returns/set.mdx create mode 100644 website/docs/plugin/framework/functions/returns/string.mdx create mode 100644 website/docs/plugin/framework/functions/testing.mdx diff --git a/.changes/unreleased/ENHANCEMENTS-20231211-125057.yaml b/.changes/unreleased/ENHANCEMENTS-20231211-125057.yaml new file mode 100644 index 000000000..6ad662927 --- /dev/null +++ b/.changes/unreleased/ENHANCEMENTS-20231211-125057.yaml @@ -0,0 +1,6 @@ +kind: ENHANCEMENTS +body: 'diag: Added `NewArgumentErrorDiagnostic()` and `NewArgumentWarningDiagnostic()` + functions, which create diagnostics with the function argument position set' +time: 2023-12-11T12:50:57.570179-05:00 +custom: + Issue: "889" diff --git a/.changes/unreleased/ENHANCEMENTS-20231211-125151.yaml b/.changes/unreleased/ENHANCEMENTS-20231211-125151.yaml new file mode 100644 index 000000000..df5f8595d --- /dev/null +++ b/.changes/unreleased/ENHANCEMENTS-20231211-125151.yaml @@ -0,0 +1,6 @@ +kind: ENHANCEMENTS +body: 'provider: Added `ProviderWithFunctions` interface for implementing provider + defined functions' +time: 2023-12-11T12:51:51.441373-05:00 +custom: + Issue: "889" diff --git a/.changes/unreleased/ENHANCEMENTS-20231211-125843.yaml b/.changes/unreleased/ENHANCEMENTS-20231211-125843.yaml new file mode 100644 index 000000000..a7463672e --- /dev/null +++ b/.changes/unreleased/ENHANCEMENTS-20231211-125843.yaml @@ -0,0 +1,6 @@ +kind: ENHANCEMENTS +body: 'diag: Added `(Diagnostics).AddArgumentError()` and `(Diagnostics).AddArgumentWarning()` + methods for appending function argument diagnostics' +time: 2023-12-11T12:58:43.277177-05:00 +custom: + Issue: "889" diff --git a/.changes/unreleased/FEATURES-20231211-125122.yaml b/.changes/unreleased/FEATURES-20231211-125122.yaml new file mode 100644 index 000000000..17d7fb700 --- /dev/null +++ b/.changes/unreleased/FEATURES-20231211-125122.yaml @@ -0,0 +1,5 @@ +kind: FEATURES +body: 'function: New package for implementing provider defined functions' +time: 2023-12-11T12:51:22.409392-05:00 +custom: + Issue: "889" diff --git a/.changes/unreleased/NOTES-20231214-082931.yaml b/.changes/unreleased/NOTES-20231214-082931.yaml new file mode 100644 index 000000000..ce0647006 --- /dev/null +++ b/.changes/unreleased/NOTES-20231214-082931.yaml @@ -0,0 +1,6 @@ +kind: NOTES +body: Provider-defined function support is in technical preview and offered without + compatibility promises until Terraform 1.8 is generally available. +time: 2023-12-14T08:29:31.188561-05:00 +custom: + Issue: "889" diff --git a/diag/argument_error_diagnostic.go b/diag/argument_error_diagnostic.go new file mode 100644 index 000000000..1b2d2956e --- /dev/null +++ b/diag/argument_error_diagnostic.go @@ -0,0 +1,13 @@ +// 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 new file mode 100644 index 000000000..895ad9a66 --- /dev/null +++ b/diag/argument_warning_diagnostic.go @@ -0,0 +1,13 @@ +// 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 74af0143e..080800452 100644 --- a/diag/diagnostic.go +++ b/diag/diagnostic.go @@ -38,6 +38,21 @@ 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 3cd99cbf8..e092f9266 100644 --- a/diag/diagnostics.go +++ b/diag/diagnostics.go @@ -13,6 +13,18 @@ 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 82def641e..b34b24767 100644 --- a/diag/diagnostics_test.go +++ b/diag/diagnostics_test.go @@ -12,6 +12,130 @@ 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/with_function_argument.go b/diag/with_function_argument.go new file mode 100644 index 000000000..715b00732 --- /dev/null +++ b/diag/with_function_argument.go @@ -0,0 +1,54 @@ +// 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 new file mode 100644 index 000000000..9b75eaf05 --- /dev/null +++ b/function/arguments_data.go @@ -0,0 +1,179 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function + +import ( + "context" + "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" +) + +// ArgumentsData is the zero-based positional argument data sent by Terraform +// for a single function call. Use the Get method or GetArgument method in the +// Function type Run method to fetch the data. +// +// This data is automatically populated by the framework based on the function +// definition. For unit testing, use the NewArgumentsData function to manually +// create the data. +type ArgumentsData struct { + values []attr.Value +} + +// Equal returns true if all the underlying values are equivalent. +func (d ArgumentsData) Equal(o ArgumentsData) bool { + if len(d.values) != len(o.values) { + return false + } + + for index, value := range d.values { + if !value.Equal(o.values[index]) { + return false + } + } + + return true +} + +// Get retrieves all argument data and populates the targets with the values. +// All arguments must be present in the targets, including all parameters and an +// optional variadic parameter, otherwise an error diagnostic will be raised. +// Each target type must be acceptable for the data type in the parameter +// definition. +// +// Variadic parameter argument data must be consumed by a types.List or Go slice +// type with an element type appropriate for the parameter definition ([]T). The +// framework automatically populates this list 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 + + 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 + } + + 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)), + ) + + return diags + } + + for position, attrValue := range d.values { + target := targets[position] + + if fwreflect.IsGenericAttrValue(ctx, target) { + //nolint:forcetypeassert // Type assertion is guaranteed by the above `reflect.IsGenericAttrValue` function + *(target.(*attr.Value)) = attrValue + + continue + } + + 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"+ + "Position: %d\n"+ + "Error: %s", + attrValue, position, err), + ) + + continue + } + + reflectDiags := fwreflect.Into(ctx, attrValue.Type(ctx), tfValue, target, fwreflect.Options{}, path.Empty()) + + diags.Append(reflectDiags...) + } + + return diags +} + +// GetArgument retrieves the argument data found at the given zero-based +// position and populates the target with the value. The target type must be +// acceptable for the data type in the parameter definition. +// +// Variadic parameter argument data must be consumed by a types.List or Go slice +// type with an element type appropriate for the parameter definition ([]T) at +// the position after all parameters. The framework automatically populates this +// list 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 + + 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 + } + + 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 an ordered list of 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 + } + + attrValue := d.values[position] + + if fwreflect.IsGenericAttrValue(ctx, target) { + //nolint:forcetypeassert // Type assertion is guaranteed by the above `reflect.IsGenericAttrValue` function + *(target.(*attr.Value)) = attrValue + + return nil + } + + 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 + } + + reflectDiags := fwreflect.Into(ctx, attrValue.Type(ctx), tfValue, target, fwreflect.Options{}, path.Empty()) + + diags.Append(reflectDiags...) + + return diags +} + +// NewArgumentsData creates an ArgumentsData. This is only necessary for unit +// testing as the framework automatically creates this data. +func NewArgumentsData(values []attr.Value) ArgumentsData { + return ArgumentsData{ + values: values, + } +} diff --git a/function/arguments_data_test.go b/function/arguments_data_test.go new file mode 100644 index 000000000..bb9718118 --- /dev/null +++ b/function/arguments_data_test.go @@ -0,0 +1,353 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +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" +) + +func TestArgumentsDataEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + argumentsData function.ArgumentsData + other function.ArgumentsData + expected bool + }{ + "zero-zero": { + argumentsData: function.ArgumentsData{}, + other: function.ArgumentsData{}, + expected: true, + }, + "nil-nil": { + argumentsData: function.NewArgumentsData(nil), + other: function.NewArgumentsData(nil), + expected: true, + }, + "empty-empty": { + argumentsData: function.NewArgumentsData([]attr.Value{}), + other: function.NewArgumentsData([]attr.Value{}), + expected: true, + }, + "equal": { + argumentsData: function.NewArgumentsData([]attr.Value{ + basetypes.NewStringValue("test"), + }), + other: function.NewArgumentsData([]attr.Value{ + basetypes.NewStringValue("test"), + }), + expected: true, + }, + "different-types": { + argumentsData: function.NewArgumentsData([]attr.Value{ + basetypes.NewStringValue("test"), + }), + other: function.NewArgumentsData([]attr.Value{ + basetypes.NewBoolValue(true), + }), + expected: false, + }, + "different-values": { + argumentsData: function.NewArgumentsData([]attr.Value{ + basetypes.NewStringValue("test1"), + }), + other: function.NewArgumentsData([]attr.Value{ + basetypes.NewStringValue("test2"), + }), + expected: false, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.argumentsData.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestArgumentsDataGet(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + argumentsData function.ArgumentsData + targets []any + expected []any + expectedDiagnotics diag.Diagnostics + }{ + "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.", + ), + }, + }, + "invalid-targets-too-few": { + argumentsData: function.NewArgumentsData([]attr.Value{ + basetypes.NewBoolNull(), + basetypes.NewBoolNull(), + }), + 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", + ), + }, + }, + "invalid-targets-too-many": { + argumentsData: function.NewArgumentsData([]attr.Value{ + basetypes.NewBoolNull(), + }), + 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", + ), + }, + }, + "invalid-target": { + argumentsData: function.NewArgumentsData([]attr.Value{ + basetypes.NewBoolValue(true), + }), + 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{}, + }, + ), + }, + }, + "attr-value": { + argumentsData: function.NewArgumentsData([]attr.Value{ + basetypes.NewBoolNull(), + basetypes.NewInt64Unknown(), + basetypes.NewStringValue("test"), + }), + targets: []any{ + new(attr.Value), + new(attr.Value), + new(attr.Value), + }, + expected: []any{ + pointer(attr.Value(basetypes.NewBoolNull())), + pointer(attr.Value(basetypes.NewInt64Unknown())), + pointer(attr.Value(basetypes.NewStringValue("test"))), + }, + }, + "framework-type": { + argumentsData: function.NewArgumentsData([]attr.Value{ + basetypes.NewBoolNull(), + basetypes.NewInt64Unknown(), + basetypes.NewStringValue("test"), + }), + targets: []any{ + new(basetypes.BoolValue), + new(basetypes.Int64Value), + new(basetypes.StringValue), + }, + expected: []any{ + pointer(basetypes.NewBoolNull()), + pointer(basetypes.NewInt64Unknown()), + pointer(basetypes.NewStringValue("test")), + }, + }, + "reflection": { + argumentsData: function.NewArgumentsData([]attr.Value{ + basetypes.NewBoolNull(), + basetypes.NewStringValue("test"), + }), + targets: []any{ + new(*bool), + new(string), + }, + expected: []any{ + pointer((*bool)(nil)), + pointer("test"), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + diags := testCase.argumentsData.Get(context.Background(), testCase.targets...) + + // Prevent awkwardness with comparing pointers in []any + options := cmp.Options{ + cmp.Transformer("BoolValue", func(v *basetypes.BoolValue) basetypes.BoolValue { + return *v + }), + cmp.Transformer("Int64Value", func(v *basetypes.Int64Value) basetypes.Int64Value { + return *v + }), + cmp.Transformer("StringValue", func(v *basetypes.StringValue) basetypes.StringValue { + return *v + }), + } + + if diff := cmp.Diff(testCase.targets, testCase.expected, options...); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiagnotics); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} + +func TestArgumentsDataGetArgument(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + argumentsData function.ArgumentsData + position int + target any + expected any + expectedDiagnotics diag.Diagnostics + }{ + "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.", + ), + }, + }, + "invalid-position": { + argumentsData: function.NewArgumentsData([]attr.Value{ + basetypes.NewBoolNull(), + }), + 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 an ordered list of 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{ + basetypes.NewBoolValue(true), + }), + 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{}, + }, + ), + }, + }, + "attr-value": { + argumentsData: function.NewArgumentsData([]attr.Value{ + basetypes.NewBoolNull(), + }), + position: 0, + target: new(attr.Value), + expected: pointer(attr.Value(basetypes.NewBoolNull())), + }, + "framework-type": { + argumentsData: function.NewArgumentsData([]attr.Value{ + basetypes.NewBoolNull(), + }), + position: 0, + target: new(basetypes.BoolValue), + expected: pointer(basetypes.NewBoolNull()), + }, + "reflection": { + argumentsData: function.NewArgumentsData([]attr.Value{ + basetypes.NewBoolNull(), + }), + position: 0, + target: new(*bool), + expected: pointer((*bool)(nil)), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + diags := testCase.argumentsData.GetArgument(context.Background(), testCase.position, testCase.target) + + // Prevent awkwardness with comparing empty interface pointers + options := cmp.Options{ + cmp.Transformer("BoolValue", func(v *basetypes.BoolValue) basetypes.BoolValue { + return *v + }), + cmp.Transformer("StringValue", func(v *basetypes.StringValue) basetypes.StringValue { + return *v + }), + } + + if diff := cmp.Diff(testCase.target, testCase.expected, options...); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiagnotics); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} diff --git a/function/bool_parameter.go b/function/bool_parameter.go new file mode 100644 index 000000000..02910f518 --- /dev/null +++ b/function/bool_parameter.go @@ -0,0 +1,108 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function + +import ( + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var _ Parameter = BoolParameter{} + +// BoolParameter represents a function parameter that is a boolean. +// +// When retrieving the argument value for this parameter: +// +// - If CustomType is set, use its associated value type. +// - If AllowUnknownValues is enabled, you must use the [types.Bool] value +// type. +// - If AllowNullValue is enabled, you must use [types.Bool] or *bool +// value types. +// - Otherwise, use [types.Bool] or *bool, or bool value types. +// +// Terraform configurations set this parameter's argument data using expressions +// that return a bool or directly via true/false keywords. +type BoolParameter struct { + // AllowNullValue when enabled denotes that a null argument value can be + // passed to the function. When disabled, Terraform returns an error if the + // argument value is null. + // + // Enabling this requires reading argument values as *bool or [types.Bool]. + AllowNullValue bool + + // AllowUnknownValues when enabled denotes that an unknown argument value + // can be passed to the function. When disabled, Terraform skips the + // function call entirely and assumes an unknown value result from the + // function. + // + // Enabling this requires reading argument values as [types.Bool]. + AllowUnknownValues bool + + // CustomType enables the use of a custom data type in place of the + // default [basetypes.BoolType]. When retrieving data, the + // [basetypes.BoolValuable] implementation associated with this custom + // type must be used in place of [types.Bool]. + CustomType basetypes.BoolTypable + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this parameter is, + // what it is for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this parameter is, what it is for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // Name is a short usage name for the parameter, such as "data". This name + // is used in documentation, such as generating a function signature, + // however its usage may be extended in the future. If no name is provided, + // this will default to "param". + // + // This must be a valid Terraform identifier, such as starting with an + // alphabetical character and followed by alphanumeric or underscore + // characters. + Name string +} + +// GetAllowNullValue returns if the parameter accepts a null value. +func (p BoolParameter) GetAllowNullValue() bool { + return p.AllowNullValue +} + +// GetAllowUnknownValues returns if the parameter accepts an unknown value. +func (p BoolParameter) GetAllowUnknownValues() bool { + return p.AllowUnknownValues +} + +// GetDescription returns the parameter plaintext description. +func (p BoolParameter) GetDescription() string { + return p.Description +} + +// GetMarkdownDescription returns the parameter Markdown description. +func (p BoolParameter) GetMarkdownDescription() string { + return p.MarkdownDescription +} + +// GetName returns the parameter name. +func (p BoolParameter) GetName() string { + if p.Name != "" { + return p.Name + } + + return DefaultParameterName +} + +// GetType returns the parameter data type. +func (p BoolParameter) GetType() attr.Type { + if p.CustomType != nil { + return p.CustomType + } + + return basetypes.BoolType{} +} diff --git a/function/bool_parameter_test.go b/function/bool_parameter_test.go new file mode 100644 index 000000000..322c821c4 --- /dev/null +++ b/function/bool_parameter_test.go @@ -0,0 +1,248 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +func TestBoolParameterGetAllowNullValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.BoolParameter + expected bool + }{ + "unset": { + parameter: function.BoolParameter{}, + expected: false, + }, + "AllowNullValue-false": { + parameter: function.BoolParameter{ + AllowNullValue: false, + }, + expected: false, + }, + "AllowNullValue-true": { + parameter: function.BoolParameter{ + AllowNullValue: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetAllowNullValue() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolParameterGetAllowUnknownValues(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.BoolParameter + expected bool + }{ + "unset": { + parameter: function.BoolParameter{}, + expected: false, + }, + "AllowUnknownValues-false": { + parameter: function.BoolParameter{ + AllowUnknownValues: false, + }, + expected: false, + }, + "AllowUnknownValues-true": { + parameter: function.BoolParameter{ + AllowUnknownValues: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetAllowUnknownValues() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolParameterGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.BoolParameter + expected string + }{ + "unset": { + parameter: function.BoolParameter{}, + expected: "", + }, + "Description-empty": { + parameter: function.BoolParameter{ + Description: "", + }, + expected: "", + }, + "Description-nonempty": { + parameter: function.BoolParameter{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolParameterGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.BoolParameter + expected string + }{ + "unset": { + parameter: function.BoolParameter{}, + expected: "", + }, + "MarkdownDescription-empty": { + parameter: function.BoolParameter{ + MarkdownDescription: "", + }, + expected: "", + }, + "MarkdownDescription-nonempty": { + parameter: function.BoolParameter{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolParameterGetName(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.BoolParameter + expected string + }{ + "unset": { + parameter: function.BoolParameter{}, + expected: function.DefaultParameterName, + }, + "Name-empty": { + parameter: function.BoolParameter{ + Name: "", + }, + expected: function.DefaultParameterName, + }, + "Name-nonempty": { + parameter: function.BoolParameter{ + Name: "test", + }, + expected: "test", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetName() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolParameterGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.BoolParameter + expected attr.Type + }{ + "unset": { + parameter: function.BoolParameter{}, + expected: basetypes.BoolType{}, + }, + "CustomType": { + parameter: function.BoolParameter{ + CustomType: testtypes.BoolType{}, + }, + expected: testtypes.BoolType{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/function/bool_return.go b/function/bool_return.go new file mode 100644 index 000000000..9ef1d138f --- /dev/null +++ b/function/bool_return.go @@ -0,0 +1,51 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var _ Return = BoolReturn{} + +// BoolReturn represents a function return that is a boolean. +// +// When setting the value for this return: +// +// - If CustomType is set, use its associated value type. +// - Otherwise, use [types.Bool], *bool, or bool. +type BoolReturn struct { + // CustomType enables the use of a custom data type in place of the + // default [basetypes.BoolType]. When setting data, the + // [basetypes.BoolValuable] implementation associated with this custom + // type must be used in place of [types.Bool]. + CustomType basetypes.BoolTypable +} + +// GetType returns the return data type. +func (r BoolReturn) GetType() attr.Type { + if r.CustomType != nil { + return r.CustomType + } + + return basetypes.BoolType{} +} + +// NewResultData returns a new result data based on the type. +func (r BoolReturn) NewResultData(ctx context.Context) (ResultData, diag.Diagnostics) { + value := basetypes.NewBoolUnknown() + + if r.CustomType == nil { + return NewResultData(value), nil + } + + valuable, diags := r.CustomType.ValueFromBool(ctx, value) + + return NewResultData(valuable), diags +} diff --git a/function/bool_return_test.go b/function/bool_return_test.go new file mode 100644 index 000000000..5d27e7429 --- /dev/null +++ b/function/bool_return_test.go @@ -0,0 +1,48 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +func TestBoolReturnGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.BoolReturn + expected attr.Type + }{ + "unset": { + parameter: function.BoolReturn{}, + expected: basetypes.BoolType{}, + }, + "CustomType": { + parameter: function.BoolReturn{ + CustomType: testtypes.BoolType{}, + }, + expected: testtypes.BoolType{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/function/definition.go b/function/definition.go new file mode 100644 index 000000000..af14b2bfa --- /dev/null +++ b/function/definition.go @@ -0,0 +1,130 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/diag" +) + +// Definition is a function definition. Always set at least the Result field. +// +// NOTE: Provider-defined function support is in technical preview and offered +// without compatibility promises until Terraform 1.8 is generally available. +type Definition struct { + // Parameters is the ordered list of function parameters and their + // associated data types. + Parameters []Parameter + + // VariadicParameter is an optional final parameter which can accept zero or + // more arguments when the function is called. The argument data is sent as + // an ordered list of the associated data type. + VariadicParameter Parameter + + // Return is the function call response data type. + Return Return + + // Summary is a short description of the function, preferably a single + // sentence. Use the Description field for longer documentation about the + // function and its implementation. + Summary string + + // Description is the longer documentation for usage, such as editor + // integrations, to give practitioners more information about the purpose of + // the function and how its logic is implemented. It should be plaintext + // formatted. + Description string + + // MarkdownDescription is the longer documentation for usage, such as a + // registry, to give practitioners more information about the purpose of the + // function and how its logic is implemented. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this function. The warning diagnostic + // summary is automatically set to "Function Deprecated" along with + // configuration source file and line information. + DeprecationMessage string +} + +// Parameter returns the Parameter for a given argument position. This may be +// from the Parameters field or, if defined, the VariadicParameter field. An +// error diagnostic is raised if the position is outside the expected arguments. +func (d Definition) Parameter(ctx context.Context, position int) (Parameter, diag.Diagnostics) { + if d.VariadicParameter != nil && position >= len(d.Parameters) { + return d.VariadicParameter, nil + } + + if len(d.Parameters) == 0 { + return nil, diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Parameter Position for Definition", + "When determining the parameter for the given argument position, an invalid value was given. "+ + "This is always an issue in the provider code and should be reported to the provider developers.\n\n"+ + "Function does not implement parameters.\n"+ + fmt.Sprintf("Given position: %d", position), + ), + } + } + + if position >= len(d.Parameters) { + return nil, diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Parameter Position for Definition", + "When determining the parameter for the given argument position, an invalid value was given. "+ + "This is always an issue in the provider code and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("Max argument position: %d\n", len(d.Parameters)-1)+ + fmt.Sprintf("Given position: %d", position), + ), + } + } + + return d.Parameters[position], nil +} + +// ValidateImplementation contains logic for validating the provider-defined +// implementation of the definition to prevent unexpected errors or panics. This +// logic runs during the GetProviderSchema RPC, or via provider-defined unit +// testing, and should never include false positives. +func (d Definition) ValidateImplementation(ctx context.Context) diag.Diagnostics { + var diags diag.Diagnostics + + if d.Return == nil { + diags.AddError( + "Invalid Function Definition", + "When validating the function definition, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "Definition Return field is undefined", + ) + } else if d.Return.GetType() == nil { + diags.AddError( + "Invalid Function Definition", + "When validating the function definition, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "Definition return data type is undefined", + ) + } + + return diags +} + +// DefinitionRequest represents a request for the Function to return its +// definition, such as its ordered parameters and result. An instance of this +// request struct is supplied as an argument to the Function type Definition +// method. +type DefinitionRequest struct{} + +// DefinitionResponse represents a response to a DefinitionRequest. An instance +// of this response struct is supplied as an argument to the Function type +// Definition method. Always set at least the Definition field. +type DefinitionResponse struct { + // Definition is the function definition. + Definition Definition + + // Diagnostics report errors or warnings related to defining the function. + // An empty slice indicates success, with no warnings or errors generated. + Diagnostics diag.Diagnostics +} diff --git a/function/definition_test.go b/function/definition_test.go new file mode 100644 index 000000000..4aae5bf77 --- /dev/null +++ b/function/definition_test.go @@ -0,0 +1,186 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/function" +) + +func TestDefinitionParameter(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + definition function.Definition + position int + expected function.Parameter + expectedDiagnostics diag.Diagnostics + }{ + "none": { + definition: function.Definition{ + // no Parameters or VariadicParameter + }, + position: 0, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Parameter Position for Definition", + "When determining the parameter for the given argument position, an invalid value was given. "+ + "This is always an issue in the provider code and should be reported to the provider developers.\n\n"+ + "Function does not implement parameters.\n"+ + "Given position: 0", + ), + }, + }, + "parameters-first": { + definition: function.Definition{ + Parameters: []function.Parameter{ + function.BoolParameter{}, + function.Int64Parameter{}, + function.StringParameter{}, + }, + }, + position: 0, + expected: function.BoolParameter{}, + }, + "parameters-last": { + definition: function.Definition{ + Parameters: []function.Parameter{ + function.BoolParameter{}, + function.Int64Parameter{}, + function.StringParameter{}, + }, + }, + position: 2, + expected: function.StringParameter{}, + }, + "parameters-middle": { + definition: function.Definition{ + Parameters: []function.Parameter{ + function.BoolParameter{}, + function.Int64Parameter{}, + function.StringParameter{}, + }, + }, + position: 1, + expected: function.Int64Parameter{}, + }, + "parameters-only": { + definition: function.Definition{ + Parameters: []function.Parameter{ + function.BoolParameter{}, + }, + }, + position: 0, + expected: function.BoolParameter{}, + }, + "parameters-over": { + definition: function.Definition{ + Parameters: []function.Parameter{ + function.BoolParameter{}, + }, + }, + position: 1, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Parameter Position for Definition", + "When determining the parameter for the given argument position, an invalid value was given. "+ + "This is always an issue in the provider code and should be reported to the provider developers.\n\n"+ + "Max argument position: 0\n"+ + "Given position: 1", + ), + }, + }, + "variadicparameter-and-parameters-select-parameter": { + definition: function.Definition{ + Parameters: []function.Parameter{ + function.BoolParameter{}, + }, + VariadicParameter: function.StringParameter{}, + }, + position: 0, + expected: function.BoolParameter{}, + }, + "variadicparameter-and-parameters-select-variadicparameter": { + definition: function.Definition{ + Parameters: []function.Parameter{ + function.BoolParameter{}, + }, + VariadicParameter: function.StringParameter{}, + }, + position: 1, + expected: function.StringParameter{}, + }, + "variadicparameter-only": { + definition: function.Definition{ + VariadicParameter: function.StringParameter{}, + }, + position: 0, + expected: function.StringParameter{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := testCase.definition.Parameter(context.Background(), testCase.position) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiagnostics); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} + +func TestDefinitionValidateImplementation(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + definition function.Definition + expected diag.Diagnostics + }{ + "valid": { + definition: function.Definition{ + Return: function.StringReturn{}, + }, + }, + "result-missing": { + definition: function.Definition{}, + expected: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Function Definition", + "When validating the function definition, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "Definition Return field is undefined", + ), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.definition.ValidateImplementation(context.Background()) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/function/doc.go b/function/doc.go new file mode 100644 index 000000000..419b408b5 --- /dev/null +++ b/function/doc.go @@ -0,0 +1,18 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package function contains all interfaces, request types, and response +// types for a Terraform Provider function implementation. +// +// In Terraform, a function is a concept which enables provider developers +// to offer practitioners a pure function call in their configuration. Functions +// are defined by a function name, such as "parse_xyz", a definition +// representing the ordered list of parameters with associated data types and +// a result data type, and the function logic. +// +// The main starting point for implementations in this package is the +// [Function] type which represents an instance of a function that has its own +// 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. +package function diff --git a/function/float64_parameter.go b/function/float64_parameter.go new file mode 100644 index 000000000..b0b7bdb8a --- /dev/null +++ b/function/float64_parameter.go @@ -0,0 +1,105 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function + +import ( + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var _ Parameter = Float64Parameter{} + +// Float64Parameter represents a function parameter that is a 64-bit floating +// point number. +// +// When retrieving the argument value for this parameter: +// +// - If CustomType is set, use its associated value type. +// - If AllowUnknownValues is enabled, you must use the [types.Float64] value +// type. +// - If AllowNullValue is enabled, you must use [types.Float64] or *float64 +// value types. +// - Otherwise, use [types.Float64] or *float64, or float64 value types. +// +// Terraform configurations set this parameter's argument data using expressions +// that return a number or directly via numeric syntax. +type Float64Parameter struct { + // AllowNullValue when enabled denotes that a null argument value can be + // passed to the function. When disabled, Terraform returns an error if the + // argument value is null. + AllowNullValue bool + + // AllowUnknownValues when enabled denotes that an unknown argument value + // can be passed to the function. When disabled, Terraform skips the + // function call entirely and assumes an unknown value result from the + // function. + AllowUnknownValues bool + + // CustomType enables the use of a custom data type in place of the + // default [basetypes.Float64Type]. When retrieving data, the + // [basetypes.Float64Valuable] implementation associated with this custom + // type must be used in place of [types.Float64]. + CustomType basetypes.Float64Typable + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this parameter is, + // what it is for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this parameter is, what it is for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // Name is a short usage name for the parameter, such as "data". This name + // is used in documentation, such as generating a function signature, + // however its usage may be extended in the future. If no name is provided, + // this will default to "param". + // + // This must be a valid Terraform identifier, such as starting with an + // alphabetical character and followed by alphanumeric or underscore + // characters. + Name string +} + +// GetAllowNullValue returns if the parameter accepts a null value. +func (p Float64Parameter) GetAllowNullValue() bool { + return p.AllowNullValue +} + +// GetAllowUnknownValues returns if the parameter accepts an unknown value. +func (p Float64Parameter) GetAllowUnknownValues() bool { + return p.AllowUnknownValues +} + +// GetDescription returns the parameter plaintext description. +func (p Float64Parameter) GetDescription() string { + return p.Description +} + +// GetMarkdownDescription returns the parameter Markdown description. +func (p Float64Parameter) GetMarkdownDescription() string { + return p.MarkdownDescription +} + +// GetName returns the parameter name. +func (p Float64Parameter) GetName() string { + if p.Name != "" { + return p.Name + } + + return DefaultParameterName +} + +// GetType returns the parameter data type. +func (p Float64Parameter) GetType() attr.Type { + if p.CustomType != nil { + return p.CustomType + } + + return basetypes.Float64Type{} +} diff --git a/function/float64_parameter_test.go b/function/float64_parameter_test.go new file mode 100644 index 000000000..6cd6eaedb --- /dev/null +++ b/function/float64_parameter_test.go @@ -0,0 +1,248 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +func TestFloat64ParameterGetAllowNullValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.Float64Parameter + expected bool + }{ + "unset": { + parameter: function.Float64Parameter{}, + expected: false, + }, + "AllowNullValue-false": { + parameter: function.Float64Parameter{ + AllowNullValue: false, + }, + expected: false, + }, + "AllowNullValue-true": { + parameter: function.Float64Parameter{ + AllowNullValue: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetAllowNullValue() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64ParameterGetAllowUnknownValues(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.Float64Parameter + expected bool + }{ + "unset": { + parameter: function.Float64Parameter{}, + expected: false, + }, + "AllowUnknownValues-false": { + parameter: function.Float64Parameter{ + AllowUnknownValues: false, + }, + expected: false, + }, + "AllowUnknownValues-true": { + parameter: function.Float64Parameter{ + AllowUnknownValues: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetAllowUnknownValues() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64ParameterGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.Float64Parameter + expected string + }{ + "unset": { + parameter: function.Float64Parameter{}, + expected: "", + }, + "Description-empty": { + parameter: function.Float64Parameter{ + Description: "", + }, + expected: "", + }, + "Description-nonempty": { + parameter: function.Float64Parameter{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64ParameterGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.Float64Parameter + expected string + }{ + "unset": { + parameter: function.Float64Parameter{}, + expected: "", + }, + "MarkdownDescription-empty": { + parameter: function.Float64Parameter{ + MarkdownDescription: "", + }, + expected: "", + }, + "MarkdownDescription-nonempty": { + parameter: function.Float64Parameter{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64ParameterGetName(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.Float64Parameter + expected string + }{ + "unset": { + parameter: function.Float64Parameter{}, + expected: function.DefaultParameterName, + }, + "Name-empty": { + parameter: function.Float64Parameter{ + Name: "", + }, + expected: function.DefaultParameterName, + }, + "Name-nonempty": { + parameter: function.Float64Parameter{ + Name: "test", + }, + expected: "test", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetName() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64ParameterGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.Float64Parameter + expected attr.Type + }{ + "unset": { + parameter: function.Float64Parameter{}, + expected: basetypes.Float64Type{}, + }, + "CustomType": { + parameter: function.Float64Parameter{ + CustomType: testtypes.Float64TypeWithSemanticEquals{}, + }, + expected: testtypes.Float64TypeWithSemanticEquals{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/function/float64_return.go b/function/float64_return.go new file mode 100644 index 000000000..aa204d054 --- /dev/null +++ b/function/float64_return.go @@ -0,0 +1,52 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var _ Return = Float64Return{} + +// Float64Return represents a function return that is a 64-bit floating point +// number. +// +// When setting the value for this return: +// +// - If CustomType is set, use its associated value type. +// - Otherwise, use [types.Float64], *float64, or float64. +type Float64Return struct { + // CustomType enables the use of a custom data type in place of the + // default [basetypes.Float64Type]. When setting data, the + // [basetypes.Float64Valuable] implementation associated with this custom + // type must be used in place of [types.Float64]. + CustomType basetypes.Float64Typable +} + +// GetType returns the return data type. +func (r Float64Return) GetType() attr.Type { + if r.CustomType != nil { + return r.CustomType + } + + return basetypes.Float64Type{} +} + +// NewResultData returns a new result data based on the type. +func (r Float64Return) NewResultData(ctx context.Context) (ResultData, diag.Diagnostics) { + value := basetypes.NewFloat64Unknown() + + if r.CustomType == nil { + return NewResultData(value), nil + } + + valuable, diags := r.CustomType.ValueFromFloat64(ctx, value) + + return NewResultData(valuable), diags +} diff --git a/function/float64_return_test.go b/function/float64_return_test.go new file mode 100644 index 000000000..8b48732c4 --- /dev/null +++ b/function/float64_return_test.go @@ -0,0 +1,48 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +func TestFloat64ReturnGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.Float64Return + expected attr.Type + }{ + "unset": { + parameter: function.Float64Return{}, + expected: basetypes.Float64Type{}, + }, + "CustomType": { + parameter: function.Float64Return{ + CustomType: testtypes.Float64TypeWithSemanticEquals{}, + }, + expected: testtypes.Float64TypeWithSemanticEquals{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/function/function.go b/function/function.go new file mode 100644 index 000000000..0baf80188 --- /dev/null +++ b/function/function.go @@ -0,0 +1,27 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function + +import ( + "context" +) + +// Function represents an instance of a function. This is the core interface +// that all functions must implement. +// +// NOTE: Provider-defined function support is in technical preview and offered +// without compatibility promises until Terraform 1.8 is generally available. +type Function interface { + // Metadata should return the name of the function, such as parse_xyz. + Metadata(context.Context, MetadataRequest, *MetadataResponse) + + // Definition should return the definition for the function. + Definition(context.Context, DefinitionRequest, *DefinitionResponse) + + // Run should return the result of the function logic. It is called when + // Terraform reaches a function call in the configuration. Argument data + // values should be read from the [RunRequest] and the result value set in + // the [RunResponse]. + Run(context.Context, RunRequest, *RunResponse) +} diff --git a/function/int64_parameter.go b/function/int64_parameter.go new file mode 100644 index 000000000..6a4d7abb7 --- /dev/null +++ b/function/int64_parameter.go @@ -0,0 +1,104 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function + +import ( + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var _ Parameter = Int64Parameter{} + +// Int64Parameter represents a function parameter that is a 64-bit integer. +// +// When retrieving the argument value for this parameter: +// +// - If CustomType is set, use its associated value type. +// - If AllowUnknownValues is enabled, you must use the [types.Int64] value +// type. +// - If AllowNullValue is enabled, you must use [types.Int64] or *int64 +// value types. +// - Otherwise, use [types.Int64] or *int64, or int64 value types. +// +// Terraform configurations set this parameter's argument data using expressions +// that return a number or directly via numeric syntax. +type Int64Parameter struct { + // AllowNullValue when enabled denotes that a null argument value can be + // passed to the function. When disabled, Terraform returns an error if the + // argument value is null. + AllowNullValue bool + + // AllowUnknownValues when enabled denotes that an unknown argument value + // can be passed to the function. When disabled, Terraform skips the + // function call entirely and assumes an unknown value result from the + // function. + AllowUnknownValues bool + + // CustomType enables the use of a custom data type in place of the + // default [basetypes.Int64Type]. When retrieving data, the + // [basetypes.Int64Valuable] implementation associated with this custom + // type must be used in place of [types.Int64]. + CustomType basetypes.Int64Typable + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this parameter is, + // what it is for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this parameter is, what it is for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // Name is a short usage name for the parameter, such as "data". This name + // is used in documentation, such as generating a function signature, + // however its usage may be extended in the future. If no name is provided, + // this will default to "param". + // + // This must be a valid Terraform identifier, such as starting with an + // alphabetical character and followed by alphanumeric or underscore + // characters. + Name string +} + +// GetAllowNullValue returns if the parameter accepts a null value. +func (p Int64Parameter) GetAllowNullValue() bool { + return p.AllowNullValue +} + +// GetAllowUnknownValues returns if the parameter accepts an unknown value. +func (p Int64Parameter) GetAllowUnknownValues() bool { + return p.AllowUnknownValues +} + +// GetDescription returns the parameter plaintext description. +func (p Int64Parameter) GetDescription() string { + return p.Description +} + +// GetMarkdownDescription returns the parameter Markdown description. +func (p Int64Parameter) GetMarkdownDescription() string { + return p.MarkdownDescription +} + +// GetName returns the parameter name. +func (p Int64Parameter) GetName() string { + if p.Name != "" { + return p.Name + } + + return DefaultParameterName +} + +// GetType returns the parameter data type. +func (p Int64Parameter) GetType() attr.Type { + if p.CustomType != nil { + return p.CustomType + } + + return basetypes.Int64Type{} +} diff --git a/function/int64_parameter_test.go b/function/int64_parameter_test.go new file mode 100644 index 000000000..4651270e2 --- /dev/null +++ b/function/int64_parameter_test.go @@ -0,0 +1,248 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +func TestInt64ParameterGetAllowNullValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.Int64Parameter + expected bool + }{ + "unset": { + parameter: function.Int64Parameter{}, + expected: false, + }, + "AllowNullValue-false": { + parameter: function.Int64Parameter{ + AllowNullValue: false, + }, + expected: false, + }, + "AllowNullValue-true": { + parameter: function.Int64Parameter{ + AllowNullValue: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetAllowNullValue() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64ParameterGetAllowUnknownValues(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.Int64Parameter + expected bool + }{ + "unset": { + parameter: function.Int64Parameter{}, + expected: false, + }, + "AllowUnknownValues-false": { + parameter: function.Int64Parameter{ + AllowUnknownValues: false, + }, + expected: false, + }, + "AllowUnknownValues-true": { + parameter: function.Int64Parameter{ + AllowUnknownValues: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetAllowUnknownValues() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64ParameterGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.Int64Parameter + expected string + }{ + "unset": { + parameter: function.Int64Parameter{}, + expected: "", + }, + "Description-empty": { + parameter: function.Int64Parameter{ + Description: "", + }, + expected: "", + }, + "Description-nonempty": { + parameter: function.Int64Parameter{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64ParameterGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.Int64Parameter + expected string + }{ + "unset": { + parameter: function.Int64Parameter{}, + expected: "", + }, + "MarkdownDescription-empty": { + parameter: function.Int64Parameter{ + MarkdownDescription: "", + }, + expected: "", + }, + "MarkdownDescription-nonempty": { + parameter: function.Int64Parameter{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64ParameterGetName(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.Int64Parameter + expected string + }{ + "unset": { + parameter: function.Int64Parameter{}, + expected: function.DefaultParameterName, + }, + "Name-empty": { + parameter: function.Int64Parameter{ + Name: "", + }, + expected: function.DefaultParameterName, + }, + "Name-nonempty": { + parameter: function.Int64Parameter{ + Name: "test", + }, + expected: "test", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetName() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64ParameterGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.Int64Parameter + expected attr.Type + }{ + "unset": { + parameter: function.Int64Parameter{}, + expected: basetypes.Int64Type{}, + }, + "CustomType": { + parameter: function.Int64Parameter{ + CustomType: testtypes.Int64TypeWithSemanticEquals{}, + }, + expected: testtypes.Int64TypeWithSemanticEquals{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/function/int64_return.go b/function/int64_return.go new file mode 100644 index 000000000..e634a5d1f --- /dev/null +++ b/function/int64_return.go @@ -0,0 +1,51 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var _ Return = Int64Return{} + +// Int64Return represents a function return that is a 64-bit integer number. +// +// When setting the value for this return: +// +// - If CustomType is set, use its associated value type. +// - Otherwise, use [types.Int64], *int64, or int64. +type Int64Return struct { + // CustomType enables the use of a custom data type in place of the + // default [basetypes.Int64Type]. When setting data, the + // [basetypes.Int64Valuable] implementation associated with this custom + // type must be used in place of [types.Int64]. + CustomType basetypes.Int64Typable +} + +// GetType returns the return data type. +func (r Int64Return) GetType() attr.Type { + if r.CustomType != nil { + return r.CustomType + } + + return basetypes.Int64Type{} +} + +// NewResultData returns a new result data based on the type. +func (r Int64Return) NewResultData(ctx context.Context) (ResultData, diag.Diagnostics) { + value := basetypes.NewInt64Unknown() + + if r.CustomType == nil { + return NewResultData(value), nil + } + + valuable, diags := r.CustomType.ValueFromInt64(ctx, value) + + return NewResultData(valuable), diags +} diff --git a/function/int64_return_test.go b/function/int64_return_test.go new file mode 100644 index 000000000..7f15e3006 --- /dev/null +++ b/function/int64_return_test.go @@ -0,0 +1,48 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +func TestInt64ReturnGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.Int64Return + expected attr.Type + }{ + "unset": { + parameter: function.Int64Return{}, + expected: basetypes.Int64Type{}, + }, + "CustomType": { + parameter: function.Int64Return{ + CustomType: testtypes.Int64TypeWithSemanticEquals{}, + }, + expected: testtypes.Int64TypeWithSemanticEquals{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/function/list_parameter.go b/function/list_parameter.go new file mode 100644 index 000000000..006e3750e --- /dev/null +++ b/function/list_parameter.go @@ -0,0 +1,110 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function + +import ( + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var _ Parameter = ListParameter{} + +// ListParameter represents a function parameter that is an ordered list of a +// single element type. Either the ElementType or CustomType field must be set. +// +// When retrieving the argument value for this parameter: +// +// - If CustomType is set, use its associated value type. +// - If AllowUnknownValues is enabled, you must use the [types.List] value +// type. +// - Otherwise, use [types.List] or any Go slice value types compatible with +// the element type. +// +// Terraform configurations set this parameter's argument data using expressions +// that return a list or directly via list ("[...]") syntax. +type ListParameter struct { + // ElementType is the type for all elements of the list. This field must be + // set. + ElementType attr.Type + + // AllowNullValue when enabled denotes that a null argument value can be + // passed to the function. When disabled, Terraform returns an error if the + // argument value is null. + AllowNullValue bool + + // AllowUnknownValues when enabled denotes that an unknown argument value + // can be passed to the function. When disabled, Terraform skips the + // function call entirely and assumes an unknown value result from the + // function. + AllowUnknownValues bool + + // CustomType enables the use of a custom data type in place of the + // default [basetypes.ListType]. When retrieving data, the + // [basetypes.ListValuable] implementation associated with this custom + // type must be used in place of [types.List]. + CustomType basetypes.ListTypable + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this parameter is, + // what it is for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this parameter is, what it is for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // Name is a short usage name for the parameter, such as "data". This name + // is used in documentation, such as generating a function signature, + // however its usage may be extended in the future. If no name is provided, + // this will default to "param". + // + // This must be a valid Terraform identifier, such as starting with an + // alphabetical character and followed by alphanumeric or underscore + // characters. + Name string +} + +// GetAllowNullValue returns if the parameter accepts a null value. +func (p ListParameter) GetAllowNullValue() bool { + return p.AllowNullValue +} + +// GetAllowUnknownValues returns if the parameter accepts an unknown value. +func (p ListParameter) GetAllowUnknownValues() bool { + return p.AllowUnknownValues +} + +// GetDescription returns the parameter plaintext description. +func (p ListParameter) GetDescription() string { + return p.Description +} + +// GetMarkdownDescription returns the parameter Markdown description. +func (p ListParameter) GetMarkdownDescription() string { + return p.MarkdownDescription +} + +// GetName returns the parameter name. +func (p ListParameter) GetName() string { + if p.Name != "" { + return p.Name + } + + return DefaultParameterName +} + +// GetType returns the parameter data type. +func (p ListParameter) GetType() attr.Type { + if p.CustomType != nil { + return p.CustomType + } + + return basetypes.ListType{ + ElemType: p.ElementType, + } +} diff --git a/function/list_parameter_test.go b/function/list_parameter_test.go new file mode 100644 index 000000000..cf83fa09d --- /dev/null +++ b/function/list_parameter_test.go @@ -0,0 +1,260 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +func TestListParameterGetAllowNullValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.ListParameter + expected bool + }{ + "unset": { + parameter: function.ListParameter{}, + expected: false, + }, + "AllowNullValue-false": { + parameter: function.ListParameter{ + AllowNullValue: false, + }, + expected: false, + }, + "AllowNullValue-true": { + parameter: function.ListParameter{ + AllowNullValue: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetAllowNullValue() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListParameterGetAllowUnknownValues(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.ListParameter + expected bool + }{ + "unset": { + parameter: function.ListParameter{}, + expected: false, + }, + "AllowUnknownValues-false": { + parameter: function.ListParameter{ + AllowUnknownValues: false, + }, + expected: false, + }, + "AllowUnknownValues-true": { + parameter: function.ListParameter{ + AllowUnknownValues: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetAllowUnknownValues() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListParameterGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.ListParameter + expected string + }{ + "unset": { + parameter: function.ListParameter{}, + expected: "", + }, + "Description-empty": { + parameter: function.ListParameter{ + Description: "", + }, + expected: "", + }, + "Description-nonempty": { + parameter: function.ListParameter{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListParameterGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.ListParameter + expected string + }{ + "unset": { + parameter: function.ListParameter{}, + expected: "", + }, + "MarkdownDescription-empty": { + parameter: function.ListParameter{ + MarkdownDescription: "", + }, + expected: "", + }, + "MarkdownDescription-nonempty": { + parameter: function.ListParameter{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListParameterGetName(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.ListParameter + expected string + }{ + "unset": { + parameter: function.ListParameter{}, + expected: function.DefaultParameterName, + }, + "Name-empty": { + parameter: function.ListParameter{ + Name: "", + }, + expected: function.DefaultParameterName, + }, + "Name-nonempty": { + parameter: function.ListParameter{ + Name: "test", + }, + expected: "test", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetName() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListParameterGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.ListParameter + expected attr.Type + }{ + "ElementType": { + parameter: function.ListParameter{ + ElementType: basetypes.StringType{}, + }, + expected: basetypes.ListType{ + ElemType: basetypes.StringType{}, + }, + }, + "CustomType": { + parameter: function.ListParameter{ + CustomType: testtypes.ListType{ + ListType: basetypes.ListType{ + ElemType: basetypes.StringType{}, + }, + }, + }, + expected: testtypes.ListType{ + ListType: basetypes.ListType{ + ElemType: basetypes.StringType{}, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/function/list_return.go b/function/list_return.go new file mode 100644 index 000000000..582ddf88f --- /dev/null +++ b/function/list_return.go @@ -0,0 +1,59 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var _ Return = ListReturn{} + +// ListReturn represents a function return that is an ordered collection of a +// single element type. Either the ElementType or CustomType field must be set. +// +// When setting the value for this return: +// +// - If CustomType is set, use its associated value type. +// - Otherwise, use [types.List] or a Go slice value type compatible with the +// element type. +type ListReturn struct { + // ElementType is the type for all elements of the list. This field must be + // set. + ElementType attr.Type + + // CustomType enables the use of a custom data type in place of the + // default [basetypes.ListType]. When setting data, the + // [basetypes.ListValuable] implementation associated with this custom + // type must be used in place of [types.List]. + CustomType basetypes.ListTypable +} + +// GetType returns the return data type. +func (r ListReturn) GetType() attr.Type { + if r.CustomType != nil { + return r.CustomType + } + + return basetypes.ListType{ + ElemType: r.ElementType, + } +} + +// NewResultData returns a new result data based on the type. +func (r ListReturn) NewResultData(ctx context.Context) (ResultData, diag.Diagnostics) { + value := basetypes.NewListUnknown(r.ElementType) + + if r.CustomType == nil { + return NewResultData(value), nil + } + + valuable, diags := r.CustomType.ValueFromList(ctx, value) + + return NewResultData(valuable), diags +} diff --git a/function/list_return_test.go b/function/list_return_test.go new file mode 100644 index 000000000..4839975ec --- /dev/null +++ b/function/list_return_test.go @@ -0,0 +1,60 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +func TestListReturnGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.ListReturn + expected attr.Type + }{ + "ElementType": { + parameter: function.ListReturn{ + ElementType: basetypes.StringType{}, + }, + expected: basetypes.ListType{ + ElemType: basetypes.StringType{}, + }, + }, + "CustomType": { + parameter: function.ListReturn{ + CustomType: testtypes.ListType{ + ListType: basetypes.ListType{ + ElemType: basetypes.StringType{}, + }, + }, + }, + expected: testtypes.ListType{ + ListType: basetypes.ListType{ + ElemType: basetypes.StringType{}, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/function/map_parameter.go b/function/map_parameter.go new file mode 100644 index 000000000..fdde26d4d --- /dev/null +++ b/function/map_parameter.go @@ -0,0 +1,110 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function + +import ( + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var _ Parameter = MapParameter{} + +// MapParameter represents a function parameter that is a mapping of a single +// element type. Either the ElementType or CustomType field must be set. +// +// When retrieving the argument value for this parameter: +// +// - If CustomType is set, use its associated value type. +// - If AllowUnknownValues is enabled, you must use the [types.Map] value +// type. +// - Otherwise, use [types.Map] or any Go map value types compatible with +// the element type. +// +// Terraform configurations set this parameter's argument data using expressions +// that return a map or directly via map ("{...}") syntax. +type MapParameter struct { + // ElementType is the type for all elements of the map. This field must be + // set. + ElementType attr.Type + + // AllowNullValue when enabled denotes that a null argument value can be + // passed to the function. When disabled, Terraform returns an error if the + // argument value is null. + AllowNullValue bool + + // AllowUnknownValues when enabled denotes that an unknown argument value + // can be passed to the function. When disabled, Terraform skips the + // function call entirely and assumes an unknown value result from the + // function. + AllowUnknownValues bool + + // CustomType enables the use of a custom data type in place of the + // default [basetypes.MapType]. When retrieving data, the + // [basetypes.MapValuable] implementation associated with this custom + // type must be used in place of [types.Map]. + CustomType basetypes.MapTypable + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this parameter is, + // what it is for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this parameter is, what it is for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // Name is a short usage name for the parameter, such as "data". This name + // is used in documentation, such as generating a function signature, + // however its usage may be extended in the future. If no name is provided, + // this will default to "param". + // + // This must be a valid Terraform identifier, such as starting with an + // alphabetical character and followed by alphanumeric or underscore + // characters. + Name string +} + +// GetAllowNullValue returns if the parameter accepts a null value. +func (p MapParameter) GetAllowNullValue() bool { + return p.AllowNullValue +} + +// GetAllowUnknownValues returns if the parameter accepts an unknown value. +func (p MapParameter) GetAllowUnknownValues() bool { + return p.AllowUnknownValues +} + +// GetDescription returns the parameter plaintext description. +func (p MapParameter) GetDescription() string { + return p.Description +} + +// GetMarkdownDescription returns the parameter Markdown description. +func (p MapParameter) GetMarkdownDescription() string { + return p.MarkdownDescription +} + +// GetName returns the parameter name. +func (p MapParameter) GetName() string { + if p.Name != "" { + return p.Name + } + + return DefaultParameterName +} + +// GetType returns the parameter data type. +func (p MapParameter) GetType() attr.Type { + if p.CustomType != nil { + return p.CustomType + } + + return basetypes.MapType{ + ElemType: p.ElementType, + } +} diff --git a/function/map_parameter_test.go b/function/map_parameter_test.go new file mode 100644 index 000000000..092fe5880 --- /dev/null +++ b/function/map_parameter_test.go @@ -0,0 +1,260 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +func TestMapParameterGetAllowNullValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.MapParameter + expected bool + }{ + "unset": { + parameter: function.MapParameter{}, + expected: false, + }, + "AllowNullValue-false": { + parameter: function.MapParameter{ + AllowNullValue: false, + }, + expected: false, + }, + "AllowNullValue-true": { + parameter: function.MapParameter{ + AllowNullValue: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetAllowNullValue() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapParameterGetAllowUnknownValues(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.MapParameter + expected bool + }{ + "unset": { + parameter: function.MapParameter{}, + expected: false, + }, + "AllowUnknownValues-false": { + parameter: function.MapParameter{ + AllowUnknownValues: false, + }, + expected: false, + }, + "AllowUnknownValues-true": { + parameter: function.MapParameter{ + AllowUnknownValues: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetAllowUnknownValues() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapParameterGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.MapParameter + expected string + }{ + "unset": { + parameter: function.MapParameter{}, + expected: "", + }, + "Description-empty": { + parameter: function.MapParameter{ + Description: "", + }, + expected: "", + }, + "Description-nonempty": { + parameter: function.MapParameter{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapParameterGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.MapParameter + expected string + }{ + "unset": { + parameter: function.MapParameter{}, + expected: "", + }, + "MarkdownDescription-empty": { + parameter: function.MapParameter{ + MarkdownDescription: "", + }, + expected: "", + }, + "MarkdownDescription-nonempty": { + parameter: function.MapParameter{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapParameterGetName(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.MapParameter + expected string + }{ + "unset": { + parameter: function.MapParameter{}, + expected: function.DefaultParameterName, + }, + "Name-empty": { + parameter: function.MapParameter{ + Name: "", + }, + expected: function.DefaultParameterName, + }, + "Name-nonempty": { + parameter: function.MapParameter{ + Name: "test", + }, + expected: "test", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetName() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapParameterGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.MapParameter + expected attr.Type + }{ + "ElementType": { + parameter: function.MapParameter{ + ElementType: basetypes.StringType{}, + }, + expected: basetypes.MapType{ + ElemType: basetypes.StringType{}, + }, + }, + "CustomType": { + parameter: function.MapParameter{ + CustomType: testtypes.MapType{ + MapType: basetypes.MapType{ + ElemType: basetypes.StringType{}, + }, + }, + }, + expected: testtypes.MapType{ + MapType: basetypes.MapType{ + ElemType: basetypes.StringType{}, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/function/map_return.go b/function/map_return.go new file mode 100644 index 000000000..f12b595f1 --- /dev/null +++ b/function/map_return.go @@ -0,0 +1,59 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var _ Return = MapReturn{} + +// MapReturn represents a function return that is an ordered collect of a +// single element type. Either the ElementType or CustomType field must be set. +// +// When setting the value for this return: +// +// - If CustomType is set, use its associated value type. +// - Otherwise, use [types.Map] or a Go map value type compatible with the +// element type. +type MapReturn struct { + // ElementType is the type for all elements of the map. This field must be + // set. + ElementType attr.Type + + // CustomType enables the use of a custom data type in place of the + // default [basetypes.MapType]. When setting data, the + // [basetypes.MapValuable] implementation associated with this custom + // type must be used in place of [types.Map]. + CustomType basetypes.MapTypable +} + +// GetType returns the return data type. +func (r MapReturn) GetType() attr.Type { + if r.CustomType != nil { + return r.CustomType + } + + return basetypes.MapType{ + ElemType: r.ElementType, + } +} + +// NewResultData returns a new result data based on the type. +func (r MapReturn) NewResultData(ctx context.Context) (ResultData, diag.Diagnostics) { + value := basetypes.NewMapUnknown(r.ElementType) + + if r.CustomType == nil { + return NewResultData(value), nil + } + + valuable, diags := r.CustomType.ValueFromMap(ctx, value) + + return NewResultData(valuable), diags +} diff --git a/function/map_return_test.go b/function/map_return_test.go new file mode 100644 index 000000000..071607ec7 --- /dev/null +++ b/function/map_return_test.go @@ -0,0 +1,60 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +func TestMapReturnGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.MapReturn + expected attr.Type + }{ + "ElementType": { + parameter: function.MapReturn{ + ElementType: basetypes.StringType{}, + }, + expected: basetypes.MapType{ + ElemType: basetypes.StringType{}, + }, + }, + "CustomType": { + parameter: function.MapReturn{ + CustomType: testtypes.MapType{ + MapType: basetypes.MapType{ + ElemType: basetypes.StringType{}, + }, + }, + }, + expected: testtypes.MapType{ + MapType: basetypes.MapType{ + ElemType: basetypes.StringType{}, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/function/metadata.go b/function/metadata.go new file mode 100644 index 000000000..a0f277afb --- /dev/null +++ b/function/metadata.go @@ -0,0 +1,20 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function + +// MetadataRequest represents a request for the Function to return metadata, +// such as its name. An instance of this request struct is supplied as an +// argument to the Function type Metadata method. +type MetadataRequest struct{} + +// MetadataResponse represents a response to a MetadataRequest. An +// instance of this response struct is supplied as an argument to the +// Function type Metadata method. +type MetadataResponse struct { + // Name should be the function name, such as parse_xyz. Unlike data sources + // and managed resources, the provider name and an underscore should not be + // included as the Terraform configuration syntax for provider function + // calls already include the provider name. + Name string +} diff --git a/function/number_parameter.go b/function/number_parameter.go new file mode 100644 index 000000000..05575cfc7 --- /dev/null +++ b/function/number_parameter.go @@ -0,0 +1,103 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function + +import ( + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var _ Parameter = NumberParameter{} + +// NumberParameter represents a function parameter that is a 512-bit arbitrary +// precision number. +// +// When retrieving the argument value for this parameter: +// +// - If CustomType is set, use its associated value type. +// - If AllowUnknownValues is enabled, you must use the [types.Number] value +// type. +// - Otherwise, use [types.Number] or *big.Float value types. +// +// Terraform configurations set this parameter's argument data using expressions +// that return a number or directly via numeric syntax. +type NumberParameter struct { + // AllowNullValue when enabled denotes that a null argument value can be + // passed to the function. When disabled, Terraform returns an error if the + // argument value is null. + AllowNullValue bool + + // AllowUnknownValues when enabled denotes that an unknown argument value + // can be passed to the function. When disabled, Terraform skips the + // function call entirely and assumes an unknown value result from the + // function. + AllowUnknownValues bool + + // CustomType enables the use of a custom data type in place of the + // default [basetypes.NumberType]. When retrieving data, the + // [basetypes.NumberValuable] implementation associated with this custom + // type must be used in place of [types.Number]. + CustomType basetypes.NumberTypable + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this parameter is, + // what it is for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this parameter is, what it is for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // Name is a short usage name for the parameter, such as "data". This name + // is used in documentation, such as generating a function signature, + // however its usage may be extended in the future. If no name is provided, + // this will default to "param". + // + // This must be a valid Terraform identifier, such as starting with an + // alphabetical character and followed by alphanumeric or underscore + // characters. + Name string +} + +// GetAllowNullValue returns if the parameter accepts a null value. +func (p NumberParameter) GetAllowNullValue() bool { + return p.AllowNullValue +} + +// GetAllowUnknownValues returns if the parameter accepts an unknown value. +func (p NumberParameter) GetAllowUnknownValues() bool { + return p.AllowUnknownValues +} + +// GetDescription returns the parameter plaintext description. +func (p NumberParameter) GetDescription() string { + return p.Description +} + +// GetMarkdownDescription returns the parameter Markdown description. +func (p NumberParameter) GetMarkdownDescription() string { + return p.MarkdownDescription +} + +// GetName returns the parameter name. +func (p NumberParameter) GetName() string { + if p.Name != "" { + return p.Name + } + + return DefaultParameterName +} + +// GetType returns the parameter data type. +func (p NumberParameter) GetType() attr.Type { + if p.CustomType != nil { + return p.CustomType + } + + return basetypes.NumberType{} +} diff --git a/function/number_parameter_test.go b/function/number_parameter_test.go new file mode 100644 index 000000000..2847dc974 --- /dev/null +++ b/function/number_parameter_test.go @@ -0,0 +1,248 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +func TestNumberParameterGetAllowNullValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.NumberParameter + expected bool + }{ + "unset": { + parameter: function.NumberParameter{}, + expected: false, + }, + "AllowNullValue-false": { + parameter: function.NumberParameter{ + AllowNullValue: false, + }, + expected: false, + }, + "AllowNullValue-true": { + parameter: function.NumberParameter{ + AllowNullValue: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetAllowNullValue() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberParameterGetAllowUnknownValues(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.NumberParameter + expected bool + }{ + "unset": { + parameter: function.NumberParameter{}, + expected: false, + }, + "AllowUnknownValues-false": { + parameter: function.NumberParameter{ + AllowUnknownValues: false, + }, + expected: false, + }, + "AllowUnknownValues-true": { + parameter: function.NumberParameter{ + AllowUnknownValues: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetAllowUnknownValues() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberParameterGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.NumberParameter + expected string + }{ + "unset": { + parameter: function.NumberParameter{}, + expected: "", + }, + "Description-empty": { + parameter: function.NumberParameter{ + Description: "", + }, + expected: "", + }, + "Description-nonempty": { + parameter: function.NumberParameter{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberParameterGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.NumberParameter + expected string + }{ + "unset": { + parameter: function.NumberParameter{}, + expected: "", + }, + "MarkdownDescription-empty": { + parameter: function.NumberParameter{ + MarkdownDescription: "", + }, + expected: "", + }, + "MarkdownDescription-nonempty": { + parameter: function.NumberParameter{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberParameterGetName(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.NumberParameter + expected string + }{ + "unset": { + parameter: function.NumberParameter{}, + expected: function.DefaultParameterName, + }, + "Name-empty": { + parameter: function.NumberParameter{ + Name: "", + }, + expected: function.DefaultParameterName, + }, + "Name-nonempty": { + parameter: function.NumberParameter{ + Name: "test", + }, + expected: "test", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetName() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberParameterGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.NumberParameter + expected attr.Type + }{ + "unset": { + parameter: function.NumberParameter{}, + expected: basetypes.NumberType{}, + }, + "CustomType": { + parameter: function.NumberParameter{ + CustomType: testtypes.NumberType{}, + }, + expected: testtypes.NumberType{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/function/number_return.go b/function/number_return.go new file mode 100644 index 000000000..514997c9b --- /dev/null +++ b/function/number_return.go @@ -0,0 +1,52 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var _ Return = NumberReturn{} + +// NumberReturn represents a function return that is a 512-bit arbitrary +// precision number. +// +// When setting the value for this return: +// +// - If CustomType is set, use its associated value type. +// - Otherwise, use [types.Number] or *big.Float. +type NumberReturn struct { + // CustomType enables the use of a custom data type in place of the + // default [basetypes.NumberType]. When setting data, the + // [basetypes.NumberValuable] implementation associated with this custom + // type must be used in place of [types.Number]. + CustomType basetypes.NumberTypable +} + +// GetType returns the return data type. +func (r NumberReturn) GetType() attr.Type { + if r.CustomType != nil { + return r.CustomType + } + + return basetypes.NumberType{} +} + +// NewResultData returns a new result data based on the type. +func (r NumberReturn) NewResultData(ctx context.Context) (ResultData, diag.Diagnostics) { + value := basetypes.NewNumberUnknown() + + if r.CustomType == nil { + return NewResultData(value), nil + } + + valuable, diags := r.CustomType.ValueFromNumber(ctx, value) + + return NewResultData(valuable), diags +} diff --git a/function/number_return_test.go b/function/number_return_test.go new file mode 100644 index 000000000..870548470 --- /dev/null +++ b/function/number_return_test.go @@ -0,0 +1,48 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +func TestNumberReturnGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.NumberReturn + expected attr.Type + }{ + "unset": { + parameter: function.NumberReturn{}, + expected: basetypes.NumberType{}, + }, + "CustomType": { + parameter: function.NumberReturn{ + CustomType: testtypes.NumberType{}, + }, + expected: testtypes.NumberType{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/function/object_parameter.go b/function/object_parameter.go new file mode 100644 index 000000000..b51cf8095 --- /dev/null +++ b/function/object_parameter.go @@ -0,0 +1,112 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function + +import ( + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var _ Parameter = ObjectParameter{} + +// ObjectParameter represents a function parameter that is a mapping of +// defined attribute names to values. Either the AttributeTypes or CustomType +// field must be set. +// +// When retrieving the argument value for this parameter: +// +// - If CustomType is set, use its associated value type. +// - If AllowUnknownValues is enabled, you must use the [types.Object] value +// type. +// - If AllowNullValue is enabled, you must use the [types.Object] or a +// compatible Go *struct value type. +// - Otherwise, use [types.Object] or compatible *struct/struct value types. +// +// Terraform configurations set this parameter's argument data using expressions +// that return an object or directly via object ("{...}") syntax. +type ObjectParameter struct { + // AttributeTypes is the mapping of underlying attribute names to attribute + // types. This field must be set. + AttributeTypes map[string]attr.Type + + // AllowNullValue when enabled denotes that a null argument value can be + // passed to the function. When disabled, Terraform returns an error if the + // argument value is null. + AllowNullValue bool + + // AllowUnknownValues when enabled denotes that an unknown argument value + // can be passed to the function. When disabled, Terraform skips the + // function call entirely and assumes an unknown value result from the + // function. + AllowUnknownValues bool + + // CustomType enables the use of a custom data type in place of the + // default [basetypes.ObjectType]. When retrieving data, the + // [basetypes.ObjectValuable] implementation associated with this custom + // type must be used in place of [types.Object]. + CustomType basetypes.ObjectTypable + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this parameter is, + // what it is for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this parameter is, what it is for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // Name is a short usage name for the parameter, such as "data". This name + // is used in documentation, such as generating a function signature, + // however its usage may be extended in the future. If no name is provided, + // this will default to "param". + // + // This must be a valid Terraform identifier, such as starting with an + // alphabetical character and followed by alphanumeric or underscore + // characters. + Name string +} + +// GetAllowNullValue returns if the parameter accepts a null value. +func (p ObjectParameter) GetAllowNullValue() bool { + return p.AllowNullValue +} + +// GetAllowUnknownValues returns if the parameter accepts an unknown value. +func (p ObjectParameter) GetAllowUnknownValues() bool { + return p.AllowUnknownValues +} + +// GetDescription returns the parameter plaintext description. +func (p ObjectParameter) GetDescription() string { + return p.Description +} + +// GetMarkdownDescription returns the parameter Markdown description. +func (p ObjectParameter) GetMarkdownDescription() string { + return p.MarkdownDescription +} + +// GetName returns the parameter name. +func (p ObjectParameter) GetName() string { + if p.Name != "" { + return p.Name + } + + return DefaultParameterName +} + +// GetType returns the parameter data type. +func (p ObjectParameter) GetType() attr.Type { + if p.CustomType != nil { + return p.CustomType + } + + return basetypes.ObjectType{ + AttrTypes: p.AttributeTypes, + } +} diff --git a/function/object_parameter_test.go b/function/object_parameter_test.go new file mode 100644 index 000000000..fce67ae0c --- /dev/null +++ b/function/object_parameter_test.go @@ -0,0 +1,268 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +func TestObjectParameterGetAllowNullValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.ObjectParameter + expected bool + }{ + "unset": { + parameter: function.ObjectParameter{}, + expected: false, + }, + "AllowNullValue-false": { + parameter: function.ObjectParameter{ + AllowNullValue: false, + }, + expected: false, + }, + "AllowNullValue-true": { + parameter: function.ObjectParameter{ + AllowNullValue: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetAllowNullValue() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectParameterGetAllowUnknownValues(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.ObjectParameter + expected bool + }{ + "unset": { + parameter: function.ObjectParameter{}, + expected: false, + }, + "AllowUnknownValues-false": { + parameter: function.ObjectParameter{ + AllowUnknownValues: false, + }, + expected: false, + }, + "AllowUnknownValues-true": { + parameter: function.ObjectParameter{ + AllowUnknownValues: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetAllowUnknownValues() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectParameterGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.ObjectParameter + expected string + }{ + "unset": { + parameter: function.ObjectParameter{}, + expected: "", + }, + "Description-empty": { + parameter: function.ObjectParameter{ + Description: "", + }, + expected: "", + }, + "Description-nonempty": { + parameter: function.ObjectParameter{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectParameterGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.ObjectParameter + expected string + }{ + "unset": { + parameter: function.ObjectParameter{}, + expected: "", + }, + "MarkdownDescription-empty": { + parameter: function.ObjectParameter{ + MarkdownDescription: "", + }, + expected: "", + }, + "MarkdownDescription-nonempty": { + parameter: function.ObjectParameter{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectParameterGetName(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.ObjectParameter + expected string + }{ + "unset": { + parameter: function.ObjectParameter{}, + expected: function.DefaultParameterName, + }, + "Name-empty": { + parameter: function.ObjectParameter{ + Name: "", + }, + expected: function.DefaultParameterName, + }, + "Name-nonempty": { + parameter: function.ObjectParameter{ + Name: "test", + }, + expected: "test", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetName() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectParameterGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.ObjectParameter + expected attr.Type + }{ + "ElementType": { + parameter: function.ObjectParameter{ + AttributeTypes: map[string]attr.Type{ + "test": basetypes.StringType{}, + }, + }, + expected: basetypes.ObjectType{ + AttrTypes: map[string]attr.Type{ + "test": basetypes.StringType{}, + }, + }, + }, + "CustomType": { + parameter: function.ObjectParameter{ + CustomType: testtypes.ObjectType{ + ObjectType: basetypes.ObjectType{ + AttrTypes: map[string]attr.Type{ + "test": basetypes.StringType{}, + }, + }, + }, + }, + expected: testtypes.ObjectType{ + ObjectType: basetypes.ObjectType{ + AttrTypes: map[string]attr.Type{ + "test": basetypes.StringType{}, + }, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/function/object_return.go b/function/object_return.go new file mode 100644 index 000000000..312f49b9b --- /dev/null +++ b/function/object_return.go @@ -0,0 +1,55 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var _ Return = ObjectReturn{} + +// ObjectReturn represents a function return that is mapping of defined +// attribute names to values. When setting the value for this return, use +// [types.Object] or a compatible Go struct as the value type unless the +// CustomType field is set. The AttributeTypes field must be set. +type ObjectReturn struct { + // AttributeTypes is the mapping of underlying attribute names to attribute + // types. This field must be set. + AttributeTypes map[string]attr.Type + + // CustomType enables the use of a custom data type in place of the + // default [basetypes.ObjectType]. When setting data, the + // [basetypes.ObjectValuable] implementation associated with this custom + // type must be used in place of [types.Object]. + CustomType basetypes.ObjectTypable +} + +// GetType returns the return data type. +func (r ObjectReturn) GetType() attr.Type { + if r.CustomType != nil { + return r.CustomType + } + + return basetypes.ObjectType{ + AttrTypes: r.AttributeTypes, + } +} + +// NewResultData returns a new result data based on the type. +func (r ObjectReturn) NewResultData(ctx context.Context) (ResultData, diag.Diagnostics) { + value := basetypes.NewObjectUnknown(r.AttributeTypes) + + if r.CustomType == nil { + return NewResultData(value), nil + } + + valuable, diags := r.CustomType.ValueFromObject(ctx, value) + + return NewResultData(valuable), diags +} diff --git a/function/object_return_test.go b/function/object_return_test.go new file mode 100644 index 000000000..04c23e70f --- /dev/null +++ b/function/object_return_test.go @@ -0,0 +1,68 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +func TestObjectReturnGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.ObjectReturn + expected attr.Type + }{ + "ElementType": { + parameter: function.ObjectReturn{ + AttributeTypes: map[string]attr.Type{ + "test": basetypes.StringType{}, + }, + }, + expected: basetypes.ObjectType{ + AttrTypes: map[string]attr.Type{ + "test": basetypes.StringType{}, + }, + }, + }, + "CustomType": { + parameter: function.ObjectReturn{ + CustomType: testtypes.ObjectType{ + ObjectType: basetypes.ObjectType{ + AttrTypes: map[string]attr.Type{ + "test": basetypes.StringType{}, + }, + }, + }, + }, + expected: testtypes.ObjectType{ + ObjectType: basetypes.ObjectType{ + AttrTypes: map[string]attr.Type{ + "test": basetypes.StringType{}, + }, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/function/parameter.go b/function/parameter.go new file mode 100644 index 000000000..4a7c89ff1 --- /dev/null +++ b/function/parameter.go @@ -0,0 +1,40 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function + +import ( + "github.com/hashicorp/terraform-plugin-framework/attr" +) + +// DefaultParameterName is the name given to parameters which do not declare +// a name. Use this to prevent Terraform errors for missing names. +const DefaultParameterName = "param" + +// Parameter is the interface for defining function parameters. +type Parameter interface { + // GetAllowNullValue should return if the parameter accepts a null value. + GetAllowNullValue() bool + + // GetAllowUnknownValues should return if the parameter accepts an unknown + // value. + GetAllowUnknownValues() bool + + // GetDescription should return the plaintext documentation for the + // parameter. + GetDescription() string + + // GetMarkdownDescription should return the Markdown documentation for the + // parameter. + GetMarkdownDescription() string + + // GetName should return a usage name for the parameter. Parameters are + // positional, so this name has no meaning except documentation. + GetName() string + + // GetType should return the data type for the parameter, which determines + // what data type Terraform requires for configurations setting the argument + // during a function call and the argument data type received by the + // Function type Run method. + GetType() attr.Type +} diff --git a/function/pointer_test.go b/function/pointer_test.go new file mode 100644 index 000000000..df51db150 --- /dev/null +++ b/function/pointer_test.go @@ -0,0 +1,8 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function_test + +func pointer[T any](value T) *T { + return &value +} diff --git a/function/result_data.go b/function/result_data.go new file mode 100644 index 000000000..4cfad6a7d --- /dev/null +++ b/function/result_data.go @@ -0,0 +1,63 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function + +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" +) + +// ResultData is the response data sent to Terraform for a single function call. +// Use the Set method in the Function type Run method to set the result data. +// +// For unit testing, use the NewResultData function to manually create the data +// for comparison. +type ResultData struct { + value attr.Value +} + +// Equal returns true if the value is equivalent. +func (d ResultData) Equal(o ResultData) bool { + if d.value == nil { + return o.value == nil + } + + return d.value.Equal(o.value) +} + +// 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 + + reflectValue, reflectDiags := fwreflect.FromValue(ctx, d.value.Type(ctx), value, path.Empty()) + + diags.Append(reflectDiags...) + + if diags.HasError() { + return diags + } + + d.value = reflectValue + + return diags +} + +// Value returns the saved value. +func (d ResultData) Value() attr.Value { + return d.value +} + +// NewResultData creates a ResultData. This is only necessary for unit testing +// as the framework automatically creates this data for the Function type Run +// method. +func NewResultData(value attr.Value) ResultData { + return ResultData{ + value: value, + } +} diff --git a/function/result_data_test.go b/function/result_data_test.go new file mode 100644 index 000000000..984a374c0 --- /dev/null +++ b/function/result_data_test.go @@ -0,0 +1,86 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function_test + +import ( + "context" + "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" +) + +func TestResultDataSet(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + resultData function.ResultData + value any + expected attr.Value + expectedDiagnostics diag.Diagnostics + }{ + "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)", + ), + }, + }, + "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: ", + ), + }, + }, + "framework-type": { + resultData: function.NewResultData(basetypes.NewBoolUnknown()), + value: basetypes.NewBoolValue(true), + expected: basetypes.NewBoolValue(true), + }, + "reflection": { + resultData: function.NewResultData(basetypes.NewBoolUnknown()), + value: true, + expected: basetypes.NewBoolValue(true), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + diags := 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 != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} diff --git a/function/return.go b/function/return.go new file mode 100644 index 000000000..ed7779df8 --- /dev/null +++ b/function/return.go @@ -0,0 +1,26 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function + +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. +type Return interface { + // GetType should return the data type for the return, which determines + // what data type Terraform requires for configurations receiving the + // response of a function call and the return data type required from the + // Function type Run method. + GetType() attr.Type + + // NewResultData should return a new ResultData with an unknown value (or + // 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) +} diff --git a/function/run.go b/function/run.go new file mode 100644 index 000000000..5c8f064af --- /dev/null +++ b/function/run.go @@ -0,0 +1,30 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +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. +type RunRequest struct { + // Arguments is the data sent from Terraform. Use the ArgumentsData type + // GetArgument method to retrieve each positional argument. + Arguments ArgumentsData +} + +// 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 + + // 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 + // the ResultData type Set method to save the data. + Result ResultData +} diff --git a/function/set_parameter.go b/function/set_parameter.go new file mode 100644 index 000000000..a028d2109 --- /dev/null +++ b/function/set_parameter.go @@ -0,0 +1,110 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function + +import ( + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var _ Parameter = SetParameter{} + +// SetParameter represents a function parameter that is an unordered set of a +// single element type. Either the ElementType or CustomType field must be set. +// +// When retrieving the argument value for this parameter: +// +// - If CustomType is set, use its associated value type. +// - If AllowUnknownValues is enabled, you must use the [types.Set] value +// type. +// - Otherwise, use [types.Set] or any Go slice value types compatible with +// the element type. +// +// Terraform configurations set this parameter's argument data using expressions +// that return a set or directly via set ("[...]") syntax. +type SetParameter struct { + // ElementType is the type for all elements of the set. This field must be + // set. + ElementType attr.Type + + // AllowNullValue when enabled denotes that a null argument value can be + // passed to the function. When disabled, Terraform returns an error if the + // argument value is null. + AllowNullValue bool + + // AllowUnknownValues when enabled denotes that an unknown argument value + // can be passed to the function. When disabled, Terraform skips the + // function call entirely and assumes an unknown value result from the + // function. + AllowUnknownValues bool + + // CustomType enables the use of a custom data type in place of the + // default [basetypes.SetType]. When retrieving data, the + // [basetypes.SetValuable] implementation associated with this custom + // type must be used in place of [types.Set]. + CustomType basetypes.SetTypable + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this parameter is, + // what it is for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this parameter is, what it is for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // Name is a short usage name for the parameter, such as "data". This name + // is used in documentation, such as generating a function signature, + // however its usage may be extended in the future. If no name is provided, + // this will default to "param". + // + // This must be a valid Terraform identifier, such as starting with an + // alphabetical character and followed by alphanumeric or underscore + // characters. + Name string +} + +// GetAllowNullValue returns if the parameter accepts a null value. +func (p SetParameter) GetAllowNullValue() bool { + return p.AllowNullValue +} + +// GetAllowUnknownValues returns if the parameter accepts an unknown value. +func (p SetParameter) GetAllowUnknownValues() bool { + return p.AllowUnknownValues +} + +// GetDescription returns the parameter plaintext description. +func (p SetParameter) GetDescription() string { + return p.Description +} + +// GetMarkdownDescription returns the parameter Markdown description. +func (p SetParameter) GetMarkdownDescription() string { + return p.MarkdownDescription +} + +// GetName returns the parameter name. +func (p SetParameter) GetName() string { + if p.Name != "" { + return p.Name + } + + return DefaultParameterName +} + +// GetType returns the parameter data type. +func (p SetParameter) GetType() attr.Type { + if p.CustomType != nil { + return p.CustomType + } + + return basetypes.SetType{ + ElemType: p.ElementType, + } +} diff --git a/function/set_parameter_test.go b/function/set_parameter_test.go new file mode 100644 index 000000000..1328119f3 --- /dev/null +++ b/function/set_parameter_test.go @@ -0,0 +1,260 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +func TestSetParameterGetAllowNullValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.SetParameter + expected bool + }{ + "unset": { + parameter: function.SetParameter{}, + expected: false, + }, + "AllowNullValue-false": { + parameter: function.SetParameter{ + AllowNullValue: false, + }, + expected: false, + }, + "AllowNullValue-true": { + parameter: function.SetParameter{ + AllowNullValue: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetAllowNullValue() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetParameterGetAllowUnknownValues(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.SetParameter + expected bool + }{ + "unset": { + parameter: function.SetParameter{}, + expected: false, + }, + "AllowUnknownValues-false": { + parameter: function.SetParameter{ + AllowUnknownValues: false, + }, + expected: false, + }, + "AllowUnknownValues-true": { + parameter: function.SetParameter{ + AllowUnknownValues: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetAllowUnknownValues() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetParameterGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.SetParameter + expected string + }{ + "unset": { + parameter: function.SetParameter{}, + expected: "", + }, + "Description-empty": { + parameter: function.SetParameter{ + Description: "", + }, + expected: "", + }, + "Description-nonempty": { + parameter: function.SetParameter{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetParameterGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.SetParameter + expected string + }{ + "unset": { + parameter: function.SetParameter{}, + expected: "", + }, + "MarkdownDescription-empty": { + parameter: function.SetParameter{ + MarkdownDescription: "", + }, + expected: "", + }, + "MarkdownDescription-nonempty": { + parameter: function.SetParameter{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetParameterGetName(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.SetParameter + expected string + }{ + "unset": { + parameter: function.SetParameter{}, + expected: function.DefaultParameterName, + }, + "Name-empty": { + parameter: function.SetParameter{ + Name: "", + }, + expected: function.DefaultParameterName, + }, + "Name-nonempty": { + parameter: function.SetParameter{ + Name: "test", + }, + expected: "test", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetName() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetParameterGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.SetParameter + expected attr.Type + }{ + "ElementType": { + parameter: function.SetParameter{ + ElementType: basetypes.StringType{}, + }, + expected: basetypes.SetType{ + ElemType: basetypes.StringType{}, + }, + }, + "CustomType": { + parameter: function.SetParameter{ + CustomType: testtypes.SetType{ + SetType: basetypes.SetType{ + ElemType: basetypes.StringType{}, + }, + }, + }, + expected: testtypes.SetType{ + SetType: basetypes.SetType{ + ElemType: basetypes.StringType{}, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/function/set_return.go b/function/set_return.go new file mode 100644 index 000000000..b2c9f3236 --- /dev/null +++ b/function/set_return.go @@ -0,0 +1,59 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var _ Return = SetReturn{} + +// SetReturn represents a function return that is an unordered collection of a +// single element type. Either the ElementType or CustomType field must be set. +// +// When setting the value for this return: +// +// - If CustomType is set, use its associated value type. +// - Otherwise, use [types.Set] or a Go slice value type compatible with the +// element type. +type SetReturn struct { + // ElementType is the type for all elements of the set. This field must be + // set. + ElementType attr.Type + + // CustomType enables the use of a custom data type in place of the + // default [basetypes.SetType]. When setting data, the + // [basetypes.SetValuable] implementation associated with this custom + // type must be used in place of [types.Set]. + CustomType basetypes.SetTypable +} + +// GetType returns the return data type. +func (r SetReturn) GetType() attr.Type { + if r.CustomType != nil { + return r.CustomType + } + + return basetypes.SetType{ + ElemType: r.ElementType, + } +} + +// NewResultData returns a new result data based on the type. +func (r SetReturn) NewResultData(ctx context.Context) (ResultData, diag.Diagnostics) { + value := basetypes.NewSetUnknown(r.ElementType) + + if r.CustomType == nil { + return NewResultData(value), nil + } + + valuable, diags := r.CustomType.ValueFromSet(ctx, value) + + return NewResultData(valuable), diags +} diff --git a/function/set_return_test.go b/function/set_return_test.go new file mode 100644 index 000000000..b53102fd1 --- /dev/null +++ b/function/set_return_test.go @@ -0,0 +1,60 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +func TestSetReturnGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.SetReturn + expected attr.Type + }{ + "ElementType": { + parameter: function.SetReturn{ + ElementType: basetypes.StringType{}, + }, + expected: basetypes.SetType{ + ElemType: basetypes.StringType{}, + }, + }, + "CustomType": { + parameter: function.SetReturn{ + CustomType: testtypes.SetType{ + SetType: basetypes.SetType{ + ElemType: basetypes.StringType{}, + }, + }, + }, + expected: testtypes.SetType{ + SetType: basetypes.SetType{ + ElemType: basetypes.StringType{}, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/function/string_parameter.go b/function/string_parameter.go new file mode 100644 index 000000000..b8a3454d7 --- /dev/null +++ b/function/string_parameter.go @@ -0,0 +1,104 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function + +import ( + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var _ Parameter = StringParameter{} + +// StringParameter represents a function parameter that is a string. +// +// When retrieving the argument value for this parameter: +// +// - If CustomType is set, use its associated value type. +// - If AllowUnknownValues is enabled, you must use the [types.String] value +// type. +// - If AllowNullValue is enabled, you must use [types.String] or *string +// value types. +// - Otherwise, use [types.String] or *string, or string value types. +// +// Terraform configurations set this parameter's argument data using expressions +// that return a string or directly via double quote ("value") syntax. +type StringParameter struct { + // AllowNullValue when enabled denotes that a null argument value can be + // passed to the function. When disabled, Terraform returns an error if the + // argument value is null. + AllowNullValue bool + + // AllowUnknownValues when enabled denotes that an unknown argument value + // can be passed to the function. When disabled, Terraform skips the + // function call entirely and assumes an unknown value result from the + // function. + AllowUnknownValues bool + + // CustomType enables the use of a custom data type in place of the + // default [basetypes.StringType]. When retrieving data, the + // [basetypes.StringValuable] implementation associated with this custom + // type must be used in place of [types.String]. + CustomType basetypes.StringTypable + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this parameter is, + // what it is for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this parameter is, what it is for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // Name is a short usage name for the parameter, such as "data". This name + // is used in documentation, such as generating a function signature, + // however its usage may be extended in the future. If no name is provided, + // this will default to "param". + // + // This must be a valid Terraform identifier, such as starting with an + // alphabetical character and followed by alphanumeric or underscore + // characters. + Name string +} + +// GetAllowNullValue returns if the parameter accepts a null value. +func (p StringParameter) GetAllowNullValue() bool { + return p.AllowNullValue +} + +// GetAllowUnknownValues returns if the parameter accepts an unknown value. +func (p StringParameter) GetAllowUnknownValues() bool { + return p.AllowUnknownValues +} + +// GetDescription returns the parameter plaintext description. +func (p StringParameter) GetDescription() string { + return p.Description +} + +// GetMarkdownDescription returns the parameter Markdown description. +func (p StringParameter) GetMarkdownDescription() string { + return p.MarkdownDescription +} + +// GetName returns the parameter name. +func (p StringParameter) GetName() string { + if p.Name != "" { + return p.Name + } + + return DefaultParameterName +} + +// GetType returns the parameter data type. +func (p StringParameter) GetType() attr.Type { + if p.CustomType != nil { + return p.CustomType + } + + return basetypes.StringType{} +} diff --git a/function/string_parameter_test.go b/function/string_parameter_test.go new file mode 100644 index 000000000..2602fa525 --- /dev/null +++ b/function/string_parameter_test.go @@ -0,0 +1,248 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +func TestStringParameterGetAllowNullValue(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.StringParameter + expected bool + }{ + "unset": { + parameter: function.StringParameter{}, + expected: false, + }, + "AllowNullValue-false": { + parameter: function.StringParameter{ + AllowNullValue: false, + }, + expected: false, + }, + "AllowNullValue-true": { + parameter: function.StringParameter{ + AllowNullValue: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetAllowNullValue() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringParameterGetAllowUnknownValues(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.StringParameter + expected bool + }{ + "unset": { + parameter: function.StringParameter{}, + expected: false, + }, + "AllowUnknownValues-false": { + parameter: function.StringParameter{ + AllowUnknownValues: false, + }, + expected: false, + }, + "AllowUnknownValues-true": { + parameter: function.StringParameter{ + AllowUnknownValues: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetAllowUnknownValues() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringParameterGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.StringParameter + expected string + }{ + "unset": { + parameter: function.StringParameter{}, + expected: "", + }, + "Description-empty": { + parameter: function.StringParameter{ + Description: "", + }, + expected: "", + }, + "Description-nonempty": { + parameter: function.StringParameter{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringParameterGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.StringParameter + expected string + }{ + "unset": { + parameter: function.StringParameter{}, + expected: "", + }, + "MarkdownDescription-empty": { + parameter: function.StringParameter{ + MarkdownDescription: "", + }, + expected: "", + }, + "MarkdownDescription-nonempty": { + parameter: function.StringParameter{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringParameterGetName(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.StringParameter + expected string + }{ + "unset": { + parameter: function.StringParameter{}, + expected: function.DefaultParameterName, + }, + "Name-empty": { + parameter: function.StringParameter{ + Name: "", + }, + expected: function.DefaultParameterName, + }, + "Name-nonempty": { + parameter: function.StringParameter{ + Name: "test", + }, + expected: "test", + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetName() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringParameterGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.StringParameter + expected attr.Type + }{ + "unset": { + parameter: function.StringParameter{}, + expected: basetypes.StringType{}, + }, + "CustomType": { + parameter: function.StringParameter{ + CustomType: testtypes.StringType{}, + }, + expected: testtypes.StringType{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/function/string_return.go b/function/string_return.go new file mode 100644 index 000000000..bf5f63e13 --- /dev/null +++ b/function/string_return.go @@ -0,0 +1,51 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisifies the desired interfaces. +var _ Return = StringReturn{} + +// StringReturn represents a function return that is a string. +// +// When setting the value for this return: +// +// - If CustomType is set, use its associated value type. +// - Otherwise, use [types.String], *string, or string. +type StringReturn struct { + // CustomType enables the use of a custom data type in place of the + // default [basetypes.StringType]. When setting data, the + // [basetypes.StringValuable] implementation associated with this custom + // type must be used in place of [types.String]. + CustomType basetypes.StringTypable +} + +// GetType returns the return data type. +func (r StringReturn) GetType() attr.Type { + if r.CustomType != nil { + return r.CustomType + } + + return basetypes.StringType{} +} + +// NewResultData returns a new result data based on the type. +func (r StringReturn) NewResultData(ctx context.Context) (ResultData, diag.Diagnostics) { + value := basetypes.NewStringUnknown() + + if r.CustomType == nil { + return NewResultData(value), nil + } + + valuable, diags := r.CustomType.ValueFromString(ctx, value) + + return NewResultData(valuable), diags +} diff --git a/function/string_return_test.go b/function/string_return_test.go new file mode 100644 index 000000000..5e0db2260 --- /dev/null +++ b/function/string_return_test.go @@ -0,0 +1,48 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package function_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +func TestStringReturnGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + parameter function.StringReturn + expected attr.Type + }{ + "unset": { + parameter: function.StringReturn{}, + expected: basetypes.StringType{}, + }, + "CustomType": { + parameter: function.StringReturn{ + CustomType: testtypes.StringType{}, + }, + expected: testtypes.StringType{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.parameter.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/fromproto5/arguments_data.go b/internal/fromproto5/arguments_data.go new file mode 100644 index 000000000..57c43c6b0 --- /dev/null +++ b/internal/fromproto5/arguments_data.go @@ -0,0 +1,152 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto5 + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/attr/xattr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// ArgumentsData returns the ArgumentsData for a given []*tfprotov5.DynamicValue +// and function.Definition. +func ArgumentsData(ctx context.Context, arguments []*tfprotov5.DynamicValue, definition function.Definition) (function.ArgumentsData, diag.Diagnostics) { + if definition.VariadicParameter == nil && len(arguments) != len(definition.Parameters) { + return function.NewArgumentsData(nil), diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unexpected Function Arguments Data", + "The provider received an unexpected number of function arguments from Terraform for the given function definition. "+ + "This is always an issue in terraform-plugin-framework or Terraform itself and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("Expected function arguments: %d\n", len(definition.Parameters))+ + fmt.Sprintf("Given function arguments: %d", len(arguments)), + ), + } + } + + // Expect at least all parameters to have corresponding arguments. Variadic + // parameter might have 0 to n arguments, which is why it is not checked in + // this case. + if len(arguments) < len(definition.Parameters) { + return function.NewArgumentsData(nil), diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unexpected Function Arguments Data", + "The provider received an unexpected number of function arguments from Terraform for the given function definition. "+ + "This is always an issue in terraform-plugin-framework or Terraform itself and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("Expected minimum function arguments: %d\n", len(definition.Parameters))+ + fmt.Sprintf("Given function arguments: %d", len(arguments)), + ), + } + } + + if definition.VariadicParameter == nil && len(arguments) == 0 { + return function.NewArgumentsData(nil), nil + } + + // Variadic values are collected as a separate list to ease developer usage. + argumentValues := make([]attr.Value, 0, len(definition.Parameters)) + variadicValues := make([]attr.Value, 0, len(arguments)-len(definition.Parameters)) + var diags diag.Diagnostics + + for position, argument := range arguments { + parameter, parameterDiags := definition.Parameter(ctx, position) + + diags.Append(parameterDiags...) + + if diags.HasError() { + return function.NewArgumentsData(nil), diags + } + + parameterType := parameter.GetType() + + if parameterType == nil { + diags.AddError( + "Unable to Convert Function Argument", + "An unexpected error was encountered when converting the function argument from 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"+ + "Please report this to the provider developer:\n\n"+ + fmt.Sprintf("Parameter type missing at position %d", position), + ) + + return function.NewArgumentsData(nil), diags + } + + tfValue, err := argument.Unmarshal(parameterType.TerraformType(ctx)) + + if err != nil { + diags.AddError( + "Unable to Convert Function Argument", + "An unexpected error was encountered when converting the function argument from 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"+ + "Please report this to the provider developer:\n\n"+ + fmt.Sprintf("Unable to unmarshal DynamicValue at position %d: %s", position, err), + ) + + return function.NewArgumentsData(nil), diags + } + + attrValue, err := parameterType.ValueFromTerraform(ctx, tfValue) + + if err != nil { + diags.AddError( + "Unable to Convert Function Argument", + "An unexpected error was encountered when converting the function argument from the protocol type. "+ + "Please report this to the provider developer:\n\n"+ + fmt.Sprintf("Unable to convert tftypes to framework type at position %d: %s", position, err), + ) + + return function.NewArgumentsData(nil), diags + } + + // This is intentionally below the attr.Value conversion so it can be + // updated for any new type system validation interfaces. Note that the + // original xattr.TypeWithValidation interface must set a path.Path, + // which will always be incorrect in the context of functions. + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/589 + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/893 + if attrTypeWithValidate, ok := parameterType.(xattr.TypeWithValidate); ok { + logging.FrameworkTrace(ctx, "Parameter type implements TypeWithValidate") + logging.FrameworkTrace(ctx, "Calling provider defined Type Validate") + diags.Append(attrTypeWithValidate.Validate(ctx, tfValue, path.Empty())...) + logging.FrameworkTrace(ctx, "Called provider defined Type Validate") + + if diags.HasError() { + continue + } + } + + if definition.VariadicParameter != nil && position >= len(definition.Parameters) { + variadicValues = append(variadicValues, attrValue) + + continue + } + + argumentValues = append(argumentValues, attrValue) + } + + if definition.VariadicParameter != nil { + variadicValue, variadicValueDiags := basetypes.NewListValue(definition.VariadicParameter.GetType(), variadicValues) + + diags.Append(variadicValueDiags...) + + if diags.HasError() { + return function.NewArgumentsData(argumentValues), diags + } + + argumentValues = append(argumentValues, variadicValue) + } + + if diags.HasError() { + return function.NewArgumentsData(nil), diags + } + + return function.NewArgumentsData(argumentValues), diags +} diff --git a/internal/fromproto5/arguments_data_test.go b/internal/fromproto5/arguments_data_test.go new file mode 100644 index 000000000..73b45b7db --- /dev/null +++ b/internal/fromproto5/arguments_data_test.go @@ -0,0 +1,409 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto5_test + +import ( + "context" + "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/internal/fromproto5" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestArgumentsData(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input []*tfprotov5.DynamicValue + definition function.Definition + expected function.ArgumentsData + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + definition: function.Definition{}, + expected: function.ArgumentsData{}, + }, + "empty": { + input: []*tfprotov5.DynamicValue{}, + definition: function.Definition{}, + expected: function.ArgumentsData{}, + }, + "mismatched-arguments-too-few-arguments": { + input: []*tfprotov5.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Bool, nil)), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.BoolParameter{}, + function.BoolParameter{}, + }, + }, + expected: function.ArgumentsData{}, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unexpected Function Arguments Data", + "The provider received an unexpected number of function arguments from Terraform for the given function definition. "+ + "This is always an issue in terraform-plugin-framework or Terraform itself and should be reported to the provider developers.\n\n"+ + "Expected function arguments: 2\n"+ + "Given function arguments: 1", + ), + }, + }, + "mismatched-arguments-too-many-arguments": { + input: []*tfprotov5.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Bool, nil)), + DynamicValueMust(tftypes.NewValue(tftypes.Bool, nil)), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.BoolParameter{}, + }, + }, + expected: function.ArgumentsData{}, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unexpected Function Arguments Data", + "The provider received an unexpected number of function arguments from Terraform for the given function definition. "+ + "This is always an issue in terraform-plugin-framework or Terraform itself and should be reported to the provider developers.\n\n"+ + "Expected function arguments: 1\n"+ + "Given function arguments: 2", + ), + }, + }, + "mismatched-arguments-type": { + input: []*tfprotov5.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Bool, true)), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.StringParameter{}, + }, + }, + expected: function.ArgumentsData{}, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Function Argument", + "An unexpected error was encountered when converting the function argument from 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"+ + "Please report this to the provider developer:\n\n"+ + "Unable to unmarshal DynamicValue at position 0: error decoding string: msgpack: invalid code=c3 decoding string/bytes length", + ), + }, + }, + "parameters-zero": { + input: []*tfprotov5.DynamicValue{}, + definition: function.Definition{}, + expected: function.NewArgumentsData(nil), + }, + "parameters-one": { + input: []*tfprotov5.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Bool, true)), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.BoolParameter{}, + }, + }, + expected: function.NewArgumentsData([]attr.Value{ + basetypes.NewBoolValue(true), + }), + }, + "parameters-one-CustomType": { + input: []*tfprotov5.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Bool, true)), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.BoolParameter{ + CustomType: testtypes.BoolType{}, + }, + }, + }, + expected: function.NewArgumentsData([]attr.Value{ + testtypes.Bool{ + Bool: basetypes.NewBoolValue(true), + }, + }), + }, + "parameters-one-TypeWithValidation-error": { + input: []*tfprotov5.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Bool, true)), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.BoolParameter{ + CustomType: testtypes.BoolTypeWithValidateError{}, + }, + }, + }, + expected: function.NewArgumentsData(nil), + expectedDiagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic(path.Empty(), "Error Diagnostic", "This is an error."), + }, + }, + "parameters-one-TypeWithValidation-warning": { + input: []*tfprotov5.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Bool, true)), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.BoolParameter{ + CustomType: testtypes.BoolTypeWithValidateWarning{}, + }, + }, + }, + expected: function.NewArgumentsData([]attr.Value{ + testtypes.Bool{ + Bool: basetypes.NewBoolValue(true), + }, + }), + expectedDiagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic(path.Empty(), "Warning Diagnostic", "This is a warning."), + }, + }, + "parameters-one-variadicparameter-zero": { + input: []*tfprotov5.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Bool, true)), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.BoolParameter{}, + }, + VariadicParameter: function.StringParameter{}, + }, + expected: function.NewArgumentsData([]attr.Value{ + basetypes.NewBoolValue(true), + basetypes.NewListValueMust( + basetypes.StringType{}, + []attr.Value{}, + ), + }), + }, + "parameters-one-variadicparameter-one": { + input: []*tfprotov5.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Bool, true)), + DynamicValueMust(tftypes.NewValue(tftypes.String, "varg-arg1")), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.BoolParameter{}, + }, + VariadicParameter: function.StringParameter{}, + }, + expected: function.NewArgumentsData([]attr.Value{ + basetypes.NewBoolValue(true), + basetypes.NewListValueMust( + basetypes.StringType{}, + []attr.Value{ + basetypes.NewStringValue("varg-arg1"), + }, + ), + }), + }, + "parameters-one-variadicparameter-multiple": { + input: []*tfprotov5.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Bool, true)), + DynamicValueMust(tftypes.NewValue(tftypes.String, "varg-arg1")), + DynamicValueMust(tftypes.NewValue(tftypes.String, "varg-arg2")), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.BoolParameter{}, + }, + VariadicParameter: function.StringParameter{}, + }, + expected: function.NewArgumentsData([]attr.Value{ + basetypes.NewBoolValue(true), + basetypes.NewListValueMust( + basetypes.StringType{}, + []attr.Value{ + basetypes.NewStringValue("varg-arg1"), + basetypes.NewStringValue("varg-arg2"), + }, + ), + }), + }, + "parameters-multiple": { + input: []*tfprotov5.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Bool, true)), + DynamicValueMust(tftypes.NewValue(tftypes.Bool, false)), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.BoolParameter{}, + function.BoolParameter{}, + }, + }, + expected: function.NewArgumentsData([]attr.Value{ + basetypes.NewBoolValue(true), + basetypes.NewBoolValue(false), + }), + }, + "parameters-multiple-variadicparameter-zero": { + input: []*tfprotov5.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Bool, true)), + DynamicValueMust(tftypes.NewValue(tftypes.Bool, false)), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.BoolParameter{}, + function.BoolParameter{}, + }, + VariadicParameter: function.StringParameter{}, + }, + expected: function.NewArgumentsData([]attr.Value{ + basetypes.NewBoolValue(true), + basetypes.NewBoolValue(false), + basetypes.NewListValueMust( + basetypes.StringType{}, + []attr.Value{}, + ), + }), + }, + "parameters-multiple-variadicparameter-one": { + input: []*tfprotov5.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Bool, true)), + DynamicValueMust(tftypes.NewValue(tftypes.Bool, false)), + DynamicValueMust(tftypes.NewValue(tftypes.String, "varg-arg2")), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.BoolParameter{}, + function.BoolParameter{}, + }, + VariadicParameter: function.StringParameter{}, + }, + expected: function.NewArgumentsData([]attr.Value{ + basetypes.NewBoolValue(true), + basetypes.NewBoolValue(false), + basetypes.NewListValueMust( + basetypes.StringType{}, + []attr.Value{ + basetypes.NewStringValue("varg-arg2"), + }, + ), + }), + }, + "parameters-multiple-variadicparameter-multiple": { + input: []*tfprotov5.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Bool, true)), + DynamicValueMust(tftypes.NewValue(tftypes.Bool, false)), + DynamicValueMust(tftypes.NewValue(tftypes.String, "varg-arg2")), + DynamicValueMust(tftypes.NewValue(tftypes.String, "varg-arg3")), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.BoolParameter{}, + function.BoolParameter{}, + }, + VariadicParameter: function.StringParameter{}, + }, + expected: function.NewArgumentsData([]attr.Value{ + basetypes.NewBoolValue(true), + basetypes.NewBoolValue(false), + basetypes.NewListValueMust( + basetypes.StringType{}, + []attr.Value{ + basetypes.NewStringValue("varg-arg2"), + basetypes.NewStringValue("varg-arg3"), + }, + ), + }), + }, + "variadicparameter-zero": { + input: []*tfprotov5.DynamicValue{}, + definition: function.Definition{ + VariadicParameter: function.StringParameter{}, + }, + expected: function.NewArgumentsData([]attr.Value{ + basetypes.NewListValueMust( + basetypes.StringType{}, + []attr.Value{}, + ), + }), + }, + "variadicparameter-one": { + input: []*tfprotov5.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.String, "varg-arg0")), + }, + definition: function.Definition{ + VariadicParameter: function.StringParameter{}, + }, + expected: function.NewArgumentsData([]attr.Value{ + basetypes.NewListValueMust( + basetypes.StringType{}, + []attr.Value{ + basetypes.NewStringValue("varg-arg0"), + }, + ), + }), + }, + "variadicparameter-one-CustomType": { + input: []*tfprotov5.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.String, "varg-arg0")), + }, + definition: function.Definition{ + VariadicParameter: function.StringParameter{ + CustomType: testtypes.StringType{}, + }, + }, + expected: function.NewArgumentsData([]attr.Value{ + basetypes.NewListValueMust( + testtypes.StringType{}, + []attr.Value{ + testtypes.String{ + CreatedBy: testtypes.StringType{}, + InternalString: basetypes.NewStringValue("varg-arg0"), + }, + }, + ), + }), + }, + "variadicparameter-multiple": { + input: []*tfprotov5.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.String, "varg-arg0")), + DynamicValueMust(tftypes.NewValue(tftypes.String, "varg-arg1")), + }, + definition: function.Definition{ + VariadicParameter: function.StringParameter{}, + }, + expected: function.NewArgumentsData([]attr.Value{ + basetypes.NewListValueMust( + basetypes.StringType{}, + []attr.Value{ + basetypes.NewStringValue("varg-arg0"), + basetypes.NewStringValue("varg-arg1"), + }, + ), + }), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := fromproto5.ArgumentsData(context.Background(), testCase.input, testCase.definition) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiagnostics); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} diff --git a/internal/fromproto5/callfunction.go b/internal/fromproto5/callfunction.go new file mode 100644 index 000000000..4bcd44fe0 --- /dev/null +++ b/internal/fromproto5/callfunction.go @@ -0,0 +1,32 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto5 + +import ( + "context" + + "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-go/tfprotov5" +) + +// CallFunctionRequest returns the *fwserver.CallFunctionRequest +// equivalent of a *tfprotov5.CallFunctionRequest. +func CallFunctionRequest(ctx context.Context, proto *tfprotov5.CallFunctionRequest, function function.Function, functionDefinition function.Definition) (*fwserver.CallFunctionRequest, diag.Diagnostics) { + if proto == nil { + return nil, nil + } + + fw := &fwserver.CallFunctionRequest{ + Function: function, + FunctionDefinition: functionDefinition, + } + + arguments, diags := ArgumentsData(ctx, proto.Arguments, functionDefinition) + + fw.Arguments = arguments + + return fw, diags +} diff --git a/internal/fromproto5/callfunction_test.go b/internal/fromproto5/callfunction_test.go new file mode 100644 index 000000000..b8984cd9c --- /dev/null +++ b/internal/fromproto5/callfunction_test.go @@ -0,0 +1,100 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto5_test + +import ( + "context" + "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/internal/fromproto5" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestCallFunctionRequest(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input *tfprotov5.CallFunctionRequest + function function.Function + functionDefinition function.Definition + expected *fwserver.CallFunctionRequest + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + expected: nil, + }, + "arguments": { + input: &tfprotov5.CallFunctionRequest{ + Arguments: []*tfprotov5.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Bool, nil)), + DynamicValueMust(tftypes.NewValue(tftypes.Number, tftypes.UnknownValue)), + DynamicValueMust(tftypes.NewValue(tftypes.String, "arg2")), + }, + Name: "testfunction", + }, + functionDefinition: function.Definition{ + Parameters: []function.Parameter{ + function.BoolParameter{}, + function.Int64Parameter{}, + function.StringParameter{}, + }, + Return: function.StringReturn{}, + }, + expected: &fwserver.CallFunctionRequest{ + Arguments: function.NewArgumentsData([]attr.Value{ + basetypes.NewBoolNull(), + basetypes.NewInt64Unknown(), + basetypes.NewStringValue("arg2"), + }), + FunctionDefinition: function.Definition{ + Parameters: []function.Parameter{ + function.BoolParameter{}, + function.Int64Parameter{}, + function.StringParameter{}, + }, + Return: function.StringReturn{}, + }, + }, + }, + "name": { + input: &tfprotov5.CallFunctionRequest{ + Name: "testfunction", + }, + functionDefinition: function.Definition{ + Return: function.StringReturn{}, + }, + expected: &fwserver.CallFunctionRequest{ + FunctionDefinition: function.Definition{ + Return: function.StringReturn{}, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := fromproto5.CallFunctionRequest(context.Background(), testCase.input, testCase.function, testCase.functionDefinition) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiagnostics); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} diff --git a/internal/fromproto5/getfunctions.go b/internal/fromproto5/getfunctions.go new file mode 100644 index 000000000..45adedaba --- /dev/null +++ b/internal/fromproto5/getfunctions.go @@ -0,0 +1,23 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto5 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// GetFunctionsRequest returns the *fwserver.GetFunctionsRequest +// equivalent of a *tfprotov5.GetFunctionsRequest. +func GetFunctionsRequest(ctx context.Context, proto *tfprotov5.GetFunctionsRequest) *fwserver.GetFunctionsRequest { + if proto == nil { + return nil + } + + fw := &fwserver.GetFunctionsRequest{} + + return fw +} diff --git a/internal/fromproto5/getfunctions_test.go b/internal/fromproto5/getfunctions_test.go new file mode 100644 index 000000000..62d42aab8 --- /dev/null +++ b/internal/fromproto5/getfunctions_test.go @@ -0,0 +1,46 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto5_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto5" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +func TestGetFunctionsRequest(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input *tfprotov5.GetFunctionsRequest + expected *fwserver.GetFunctionsRequest + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &tfprotov5.GetFunctionsRequest{}, + expected: &fwserver.GetFunctionsRequest{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := fromproto5.GetFunctionsRequest(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/fromproto6/arguments_data.go b/internal/fromproto6/arguments_data.go new file mode 100644 index 000000000..a41b1ce7d --- /dev/null +++ b/internal/fromproto6/arguments_data.go @@ -0,0 +1,148 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto6 + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/attr/xattr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// ArgumentsData returns the ArgumentsData for a given []*tfprotov6.DynamicValue +// and function.Definition. +func ArgumentsData(ctx context.Context, arguments []*tfprotov6.DynamicValue, definition function.Definition) (function.ArgumentsData, diag.Diagnostics) { + if definition.VariadicParameter == nil && len(arguments) != len(definition.Parameters) { + return function.NewArgumentsData(nil), diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unexpected Function Arguments Data", + "The provider received an unexpected number of function arguments from Terraform for the given function definition. "+ + "This is always an issue in terraform-plugin-framework or Terraform itself and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("Expected function arguments: %d\n", len(definition.Parameters))+ + fmt.Sprintf("Given function arguments: %d", len(arguments)), + ), + } + } + + // Expect at least all parameters to have corresponding arguments. Variadic + // parameter might have 0 to n arguments, which is why it is not checked in + // this case. + if len(arguments) < len(definition.Parameters) { + return function.NewArgumentsData(nil), diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unexpected Function Arguments Data", + "The provider received an unexpected number of function arguments from Terraform for the given function definition. "+ + "This is always an issue in terraform-plugin-framework or Terraform itself and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("Expected minimum function arguments: %d\n", len(definition.Parameters))+ + fmt.Sprintf("Given function arguments: %d", len(arguments)), + ), + } + } + + if definition.VariadicParameter == nil && len(arguments) == 0 { + return function.NewArgumentsData(nil), nil + } + + // Variadic values are collected as a separate list to ease developer usage. + argumentValues := make([]attr.Value, 0, len(definition.Parameters)) + variadicValues := make([]attr.Value, 0, len(arguments)-len(definition.Parameters)) + var diags diag.Diagnostics + + for position, argument := range arguments { + parameter, parameterDiags := definition.Parameter(ctx, position) + + diags.Append(parameterDiags...) + + if diags.HasError() { + return function.NewArgumentsData(nil), diags + } + + parameterType := parameter.GetType() + + if parameterType == nil { + diags.AddError( + "Unable to Convert Function Argument", + "An unexpected error was encountered when converting the function argument from 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"+ + "Please report this to the provider developer:\n\n"+ + fmt.Sprintf("Parameter type missing at position %d", position), + ) + + return function.NewArgumentsData(nil), diags + } + + tfValue, err := argument.Unmarshal(parameterType.TerraformType(ctx)) + + if err != nil { + diags.AddError( + "Unable to Convert Function Argument", + "An unexpected error was encountered when converting the function argument from 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"+ + "Please report this to the provider developer:\n\n"+ + fmt.Sprintf("Unable to unmarshal DynamicValue at position %d: %s", position, err), + ) + + return function.NewArgumentsData(nil), diags + } + + attrValue, err := parameterType.ValueFromTerraform(ctx, tfValue) + + if err != nil { + diags.AddError( + "Unable to Convert Function Argument", + "An unexpected error was encountered when converting the function argument from the protocol type. "+ + "Please report this to the provider developer:\n\n"+ + fmt.Sprintf("Unable to convert tftypes to framework type at position %d: %s", position, err), + ) + + return function.NewArgumentsData(nil), diags + } + + // This is intentionally below the attr.Value conversion so it can be + // updated for any new type system validation interfaces. Note that the + // original xattr.TypeWithValidation interface must set a path.Path, + // which will always be incorrect in the context of functions. + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/589 + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/893 + if attrTypeWithValidate, ok := parameterType.(xattr.TypeWithValidate); ok { + logging.FrameworkTrace(ctx, "Parameter type implements TypeWithValidate") + logging.FrameworkTrace(ctx, "Calling provider defined Type Validate") + diags.Append(attrTypeWithValidate.Validate(ctx, tfValue, path.Empty())...) + logging.FrameworkTrace(ctx, "Called provider defined Type Validate") + + if diags.HasError() { + continue + } + } + + if definition.VariadicParameter != nil && position >= len(definition.Parameters) { + variadicValues = append(variadicValues, attrValue) + + continue + } + + argumentValues = append(argumentValues, attrValue) + } + + if definition.VariadicParameter != nil { + variadicValue, variadicValueDiags := basetypes.NewListValue(definition.VariadicParameter.GetType(), variadicValues) + + diags.Append(variadicValueDiags...) + + if diags.HasError() { + return function.NewArgumentsData(argumentValues), diags + } + + argumentValues = append(argumentValues, variadicValue) + } + + return function.NewArgumentsData(argumentValues), diags +} diff --git a/internal/fromproto6/arguments_data_test.go b/internal/fromproto6/arguments_data_test.go new file mode 100644 index 000000000..c88f97b49 --- /dev/null +++ b/internal/fromproto6/arguments_data_test.go @@ -0,0 +1,409 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto6_test + +import ( + "context" + "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/internal/fromproto6" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestArgumentsData(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input []*tfprotov6.DynamicValue + definition function.Definition + expected function.ArgumentsData + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + definition: function.Definition{}, + expected: function.ArgumentsData{}, + }, + "empty": { + input: []*tfprotov6.DynamicValue{}, + definition: function.Definition{}, + expected: function.ArgumentsData{}, + }, + "mismatched-arguments-too-few-arguments": { + input: []*tfprotov6.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Bool, nil)), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.BoolParameter{}, + function.BoolParameter{}, + }, + }, + expected: function.ArgumentsData{}, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unexpected Function Arguments Data", + "The provider received an unexpected number of function arguments from Terraform for the given function definition. "+ + "This is always an issue in terraform-plugin-framework or Terraform itself and should be reported to the provider developers.\n\n"+ + "Expected function arguments: 2\n"+ + "Given function arguments: 1", + ), + }, + }, + "mismatched-arguments-too-many-arguments": { + input: []*tfprotov6.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Bool, nil)), + DynamicValueMust(tftypes.NewValue(tftypes.Bool, nil)), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.BoolParameter{}, + }, + }, + expected: function.ArgumentsData{}, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unexpected Function Arguments Data", + "The provider received an unexpected number of function arguments from Terraform for the given function definition. "+ + "This is always an issue in terraform-plugin-framework or Terraform itself and should be reported to the provider developers.\n\n"+ + "Expected function arguments: 1\n"+ + "Given function arguments: 2", + ), + }, + }, + "mismatched-arguments-type": { + input: []*tfprotov6.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Bool, true)), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.StringParameter{}, + }, + }, + expected: function.ArgumentsData{}, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Function Argument", + "An unexpected error was encountered when converting the function argument from 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"+ + "Please report this to the provider developer:\n\n"+ + "Unable to unmarshal DynamicValue at position 0: error decoding string: msgpack: invalid code=c3 decoding string/bytes length", + ), + }, + }, + "parameters-zero": { + input: []*tfprotov6.DynamicValue{}, + definition: function.Definition{}, + expected: function.NewArgumentsData(nil), + }, + "parameters-one": { + input: []*tfprotov6.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Bool, true)), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.BoolParameter{}, + }, + }, + expected: function.NewArgumentsData([]attr.Value{ + basetypes.NewBoolValue(true), + }), + }, + "parameters-one-CustomType": { + input: []*tfprotov6.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Bool, true)), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.BoolParameter{ + CustomType: testtypes.BoolType{}, + }, + }, + }, + expected: function.NewArgumentsData([]attr.Value{ + testtypes.Bool{ + Bool: basetypes.NewBoolValue(true), + }, + }), + }, + "parameters-one-TypeWithValidation-error": { + input: []*tfprotov6.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Bool, true)), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.BoolParameter{ + CustomType: testtypes.BoolTypeWithValidateError{}, + }, + }, + }, + expected: function.NewArgumentsData(nil), + expectedDiagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic(path.Empty(), "Error Diagnostic", "This is an error."), + }, + }, + "parameters-one-TypeWithValidation-warning": { + input: []*tfprotov6.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Bool, true)), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.BoolParameter{ + CustomType: testtypes.BoolTypeWithValidateWarning{}, + }, + }, + }, + expected: function.NewArgumentsData([]attr.Value{ + testtypes.Bool{ + Bool: basetypes.NewBoolValue(true), + }, + }), + expectedDiagnostics: diag.Diagnostics{ + diag.NewAttributeWarningDiagnostic(path.Empty(), "Warning Diagnostic", "This is a warning."), + }, + }, + "parameters-one-variadicparameter-zero": { + input: []*tfprotov6.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Bool, true)), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.BoolParameter{}, + }, + VariadicParameter: function.StringParameter{}, + }, + expected: function.NewArgumentsData([]attr.Value{ + basetypes.NewBoolValue(true), + basetypes.NewListValueMust( + basetypes.StringType{}, + []attr.Value{}, + ), + }), + }, + "parameters-one-variadicparameter-one": { + input: []*tfprotov6.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Bool, true)), + DynamicValueMust(tftypes.NewValue(tftypes.String, "varg-arg1")), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.BoolParameter{}, + }, + VariadicParameter: function.StringParameter{}, + }, + expected: function.NewArgumentsData([]attr.Value{ + basetypes.NewBoolValue(true), + basetypes.NewListValueMust( + basetypes.StringType{}, + []attr.Value{ + basetypes.NewStringValue("varg-arg1"), + }, + ), + }), + }, + "parameters-one-variadicparameter-multiple": { + input: []*tfprotov6.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Bool, true)), + DynamicValueMust(tftypes.NewValue(tftypes.String, "varg-arg1")), + DynamicValueMust(tftypes.NewValue(tftypes.String, "varg-arg2")), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.BoolParameter{}, + }, + VariadicParameter: function.StringParameter{}, + }, + expected: function.NewArgumentsData([]attr.Value{ + basetypes.NewBoolValue(true), + basetypes.NewListValueMust( + basetypes.StringType{}, + []attr.Value{ + basetypes.NewStringValue("varg-arg1"), + basetypes.NewStringValue("varg-arg2"), + }, + ), + }), + }, + "parameters-multiple": { + input: []*tfprotov6.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Bool, true)), + DynamicValueMust(tftypes.NewValue(tftypes.Bool, false)), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.BoolParameter{}, + function.BoolParameter{}, + }, + }, + expected: function.NewArgumentsData([]attr.Value{ + basetypes.NewBoolValue(true), + basetypes.NewBoolValue(false), + }), + }, + "parameters-multiple-variadicparameter-zero": { + input: []*tfprotov6.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Bool, true)), + DynamicValueMust(tftypes.NewValue(tftypes.Bool, false)), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.BoolParameter{}, + function.BoolParameter{}, + }, + VariadicParameter: function.StringParameter{}, + }, + expected: function.NewArgumentsData([]attr.Value{ + basetypes.NewBoolValue(true), + basetypes.NewBoolValue(false), + basetypes.NewListValueMust( + basetypes.StringType{}, + []attr.Value{}, + ), + }), + }, + "parameters-multiple-variadicparameter-one": { + input: []*tfprotov6.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Bool, true)), + DynamicValueMust(tftypes.NewValue(tftypes.Bool, false)), + DynamicValueMust(tftypes.NewValue(tftypes.String, "varg-arg2")), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.BoolParameter{}, + function.BoolParameter{}, + }, + VariadicParameter: function.StringParameter{}, + }, + expected: function.NewArgumentsData([]attr.Value{ + basetypes.NewBoolValue(true), + basetypes.NewBoolValue(false), + basetypes.NewListValueMust( + basetypes.StringType{}, + []attr.Value{ + basetypes.NewStringValue("varg-arg2"), + }, + ), + }), + }, + "parameters-multiple-variadicparameter-multiple": { + input: []*tfprotov6.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Bool, true)), + DynamicValueMust(tftypes.NewValue(tftypes.Bool, false)), + DynamicValueMust(tftypes.NewValue(tftypes.String, "varg-arg2")), + DynamicValueMust(tftypes.NewValue(tftypes.String, "varg-arg3")), + }, + definition: function.Definition{ + Parameters: []function.Parameter{ + function.BoolParameter{}, + function.BoolParameter{}, + }, + VariadicParameter: function.StringParameter{}, + }, + expected: function.NewArgumentsData([]attr.Value{ + basetypes.NewBoolValue(true), + basetypes.NewBoolValue(false), + basetypes.NewListValueMust( + basetypes.StringType{}, + []attr.Value{ + basetypes.NewStringValue("varg-arg2"), + basetypes.NewStringValue("varg-arg3"), + }, + ), + }), + }, + "variadicparameter-zero": { + input: []*tfprotov6.DynamicValue{}, + definition: function.Definition{ + VariadicParameter: function.StringParameter{}, + }, + expected: function.NewArgumentsData([]attr.Value{ + basetypes.NewListValueMust( + basetypes.StringType{}, + []attr.Value{}, + ), + }), + }, + "variadicparameter-one": { + input: []*tfprotov6.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.String, "varg-arg0")), + }, + definition: function.Definition{ + VariadicParameter: function.StringParameter{}, + }, + expected: function.NewArgumentsData([]attr.Value{ + basetypes.NewListValueMust( + basetypes.StringType{}, + []attr.Value{ + basetypes.NewStringValue("varg-arg0"), + }, + ), + }), + }, + "variadicparameter-one-CustomType": { + input: []*tfprotov6.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.String, "varg-arg0")), + }, + definition: function.Definition{ + VariadicParameter: function.StringParameter{ + CustomType: testtypes.StringType{}, + }, + }, + expected: function.NewArgumentsData([]attr.Value{ + basetypes.NewListValueMust( + testtypes.StringType{}, + []attr.Value{ + testtypes.String{ + CreatedBy: testtypes.StringType{}, + InternalString: basetypes.NewStringValue("varg-arg0"), + }, + }, + ), + }), + }, + "variadicparameter-multiple": { + input: []*tfprotov6.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.String, "varg-arg0")), + DynamicValueMust(tftypes.NewValue(tftypes.String, "varg-arg1")), + }, + definition: function.Definition{ + VariadicParameter: function.StringParameter{}, + }, + expected: function.NewArgumentsData([]attr.Value{ + basetypes.NewListValueMust( + basetypes.StringType{}, + []attr.Value{ + basetypes.NewStringValue("varg-arg0"), + basetypes.NewStringValue("varg-arg1"), + }, + ), + }), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := fromproto6.ArgumentsData(context.Background(), testCase.input, testCase.definition) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiagnostics); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} diff --git a/internal/fromproto6/callfunction.go b/internal/fromproto6/callfunction.go new file mode 100644 index 000000000..4c1511845 --- /dev/null +++ b/internal/fromproto6/callfunction.go @@ -0,0 +1,32 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto6 + +import ( + "context" + + "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-go/tfprotov6" +) + +// CallFunctionRequest returns the *fwserver.CallFunctionRequest +// equivalent of a *tfprotov6.CallFunctionRequest. +func CallFunctionRequest(ctx context.Context, proto *tfprotov6.CallFunctionRequest, function function.Function, functionDefinition function.Definition) (*fwserver.CallFunctionRequest, diag.Diagnostics) { + if proto == nil { + return nil, nil + } + + fw := &fwserver.CallFunctionRequest{ + Function: function, + FunctionDefinition: functionDefinition, + } + + arguments, diags := ArgumentsData(ctx, proto.Arguments, functionDefinition) + + fw.Arguments = arguments + + return fw, diags +} diff --git a/internal/fromproto6/callfunction_test.go b/internal/fromproto6/callfunction_test.go new file mode 100644 index 000000000..c54a111bd --- /dev/null +++ b/internal/fromproto6/callfunction_test.go @@ -0,0 +1,100 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto6_test + +import ( + "context" + "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/internal/fromproto6" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestCallFunctionRequest(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input *tfprotov6.CallFunctionRequest + function function.Function + functionDefinition function.Definition + expected *fwserver.CallFunctionRequest + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + expected: nil, + }, + "arguments": { + input: &tfprotov6.CallFunctionRequest{ + Arguments: []*tfprotov6.DynamicValue{ + DynamicValueMust(tftypes.NewValue(tftypes.Bool, nil)), + DynamicValueMust(tftypes.NewValue(tftypes.Number, tftypes.UnknownValue)), + DynamicValueMust(tftypes.NewValue(tftypes.String, "arg2")), + }, + Name: "testfunction", + }, + functionDefinition: function.Definition{ + Parameters: []function.Parameter{ + function.BoolParameter{}, + function.Int64Parameter{}, + function.StringParameter{}, + }, + Return: function.StringReturn{}, + }, + expected: &fwserver.CallFunctionRequest{ + Arguments: function.NewArgumentsData([]attr.Value{ + basetypes.NewBoolNull(), + basetypes.NewInt64Unknown(), + basetypes.NewStringValue("arg2"), + }), + FunctionDefinition: function.Definition{ + Parameters: []function.Parameter{ + function.BoolParameter{}, + function.Int64Parameter{}, + function.StringParameter{}, + }, + Return: function.StringReturn{}, + }, + }, + }, + "name": { + input: &tfprotov6.CallFunctionRequest{ + Name: "testfunction", + }, + functionDefinition: function.Definition{ + Return: function.StringReturn{}, + }, + expected: &fwserver.CallFunctionRequest{ + FunctionDefinition: function.Definition{ + Return: function.StringReturn{}, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := fromproto6.CallFunctionRequest(context.Background(), testCase.input, testCase.function, testCase.functionDefinition) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiagnostics); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} diff --git a/internal/fromproto6/getfunctions.go b/internal/fromproto6/getfunctions.go new file mode 100644 index 000000000..40c225358 --- /dev/null +++ b/internal/fromproto6/getfunctions.go @@ -0,0 +1,23 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto6 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// GetFunctionsRequest returns the *fwserver.GetFunctionsRequest +// equivalent of a *tfprotov6.GetFunctionsRequest. +func GetFunctionsRequest(ctx context.Context, proto *tfprotov6.GetFunctionsRequest) *fwserver.GetFunctionsRequest { + if proto == nil { + return nil + } + + fw := &fwserver.GetFunctionsRequest{} + + return fw +} diff --git a/internal/fromproto6/getfunctions_test.go b/internal/fromproto6/getfunctions_test.go new file mode 100644 index 000000000..9269a889b --- /dev/null +++ b/internal/fromproto6/getfunctions_test.go @@ -0,0 +1,46 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto6_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +func TestGetFunctionsRequest(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input *tfprotov6.GetFunctionsRequest + expected *fwserver.GetFunctionsRequest + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &tfprotov6.GetFunctionsRequest{}, + expected: &fwserver.GetFunctionsRequest{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := fromproto6.GetFunctionsRequest(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/fwserver/server.go b/internal/fwserver/server.go index 261579c68..40fa631f8 100644 --- a/internal/fwserver/server.go +++ b/internal/fwserver/server.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/logging" "github.com/hashicorp/terraform-plugin-framework/provider" @@ -55,6 +56,29 @@ type Server struct { // access from race conditions. dataSourceTypesMutex sync.Mutex + // functionDefinitions is the cached Function Definitions for RPCs that need to + // convert data from the protocol. If not found, it will be fetched from the + // Function.Definition() method. + functionDefinitions map[string]function.Definition + + // functionDefinitionsMutex is a mutex to protect concurrent functionDefinitions + // access from race conditions. + functionDefinitionsMutex sync.RWMutex + + // functionFuncs is the cached Function functions for RPCs that need to + // access functions. If not found, it will be fetched from the + // Provider.Functions() method. + functionFuncs map[string]func() function.Function + + // functionFuncsDiags is the cached Diagnostics obtained while populating + // functionFuncs. This is to ensure any warnings or errors are also + // returned appropriately when fetching functionFuncs. + functionFuncsDiags diag.Diagnostics + + // functionFuncsMutex is a mutex to protect concurrent functionFuncs + // access from race conditions. + functionFuncsMutex sync.Mutex + // providerSchema is the cached Provider Schema for RPCs that need to // convert configuration data from the protocol. If not found, it will be // fetched from the Provider.GetSchema() method. diff --git a/internal/fwserver/server_callfunction.go b/internal/fwserver/server_callfunction.go new file mode 100644 index 000000000..36a19545d --- /dev/null +++ b/internal/fwserver/server_callfunction.go @@ -0,0 +1,56 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +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" +) + +// CallFunctionRequest is the framework server request for the +// CallFunction RPC. +type CallFunctionRequest struct { + Arguments function.ArgumentsData + Function function.Function + FunctionDefinition function.Definition +} + +// CallFunctionResponse is the framework server response for the +// CallFunction RPC. +type CallFunctionResponse struct { + Diagnostics diag.Diagnostics + Result function.ResultData +} + +// CallFunction implements the framework server CallFunction RPC. +func (s *Server) CallFunction(ctx context.Context, req *CallFunctionRequest, resp *CallFunctionResponse) { + if req == nil { + return + } + + resultData, diags := req.FunctionDefinition.Return.NewResultData(ctx) + + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + runReq := function.RunRequest{ + Arguments: req.Arguments, + } + runResp := function.RunResponse{ + Result: resultData, + } + + logging.FrameworkTrace(ctx, "Calling provider defined Function Run") + req.Function.Run(ctx, runReq, &runResp) + logging.FrameworkTrace(ctx, "Called provider defined Function Run") + + resp.Diagnostics = runResp.Diagnostics + resp.Result = runResp.Result +} diff --git a/internal/fwserver/server_callfunction_test.go b/internal/fwserver/server_callfunction_test.go new file mode 100644 index 000000000..c56d371f1 --- /dev/null +++ b/internal/fwserver/server_callfunction_test.go @@ -0,0 +1,431 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver_test + +import ( + "context" + "fmt" + "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/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +func TestServerCallFunction(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + server *fwserver.Server + request *fwserver.CallFunctionRequest + expectedResponse *fwserver.CallFunctionResponse + }{ + "request-nil": { + server: &fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{}, + }, + expectedResponse: &fwserver.CallFunctionResponse{}, + }, + "request-arguments-get": { + server: &fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{}, + }, + request: &fwserver.CallFunctionRequest{ + Arguments: function.NewArgumentsData([]attr.Value{ + basetypes.NewBoolNull(), + basetypes.NewInt64Unknown(), + basetypes.NewStringValue("arg2"), + }), + Function: &testprovider.Function{ + RunMethod: func(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + var arg0 basetypes.BoolValue + var arg1 basetypes.Int64Value + var arg2 basetypes.StringValue + + resp.Diagnostics.Append(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), + ) + } + + if !arg1.Equal(expectedArg1) { + resp.Diagnostics.AddError( + "Unexpected Argument 1 Difference", + fmt.Sprintf("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.Diagnostics.Append(resp.Result.Set(ctx, basetypes.NewStringValue("result"))...) + }, + }, + FunctionDefinition: function.Definition{ + Return: function.StringReturn{}, + }, + }, + expectedResponse: &fwserver.CallFunctionResponse{ + Diagnostics: nil, + Result: function.NewResultData(basetypes.NewStringValue("result")), + }, + }, + "request-arguments-get-reflection": { + server: &fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{}, + }, + request: &fwserver.CallFunctionRequest{ + Arguments: function.NewArgumentsData([]attr.Value{ + basetypes.NewStringValue("arg0"), + basetypes.NewStringNull(), + }), + Function: &testprovider.Function{ + RunMethod: func(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + var arg0 string + var arg1 *string + + resp.Diagnostics.Append(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), + ) + } + + if arg1 != nil { + resp.Diagnostics.AddError( + "Unexpected Argument 1 Difference", + fmt.Sprintf("got: %s, expected: nil", *arg1), + ) + } + + resp.Diagnostics.Append(resp.Result.Set(ctx, basetypes.NewStringValue("result"))...) + }, + }, + FunctionDefinition: function.Definition{ + Return: function.StringReturn{}, + }, + }, + expectedResponse: &fwserver.CallFunctionResponse{ + Diagnostics: nil, + Result: function.NewResultData(basetypes.NewStringValue("result")), + }, + }, + "request-arguments-get-variadic": { + server: &fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{}, + }, + request: &fwserver.CallFunctionRequest{ + Arguments: function.NewArgumentsData([]attr.Value{ + basetypes.NewStringValue("arg0"), + basetypes.NewListValueMust( + basetypes.StringType{}, + []attr.Value{ + basetypes.NewStringValue("arg1-element0"), + basetypes.NewStringValue("arg1-element1"), + }, + ), + }), + Function: &testprovider.Function{ + RunMethod: func(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + var arg0 basetypes.StringValue + var arg1 basetypes.ListValue + + resp.Diagnostics.Append(req.Arguments.Get(ctx, &arg0, &arg1)...) + + expectedArg0 := basetypes.NewStringValue("arg0") + expectedArg1 := basetypes.NewListValueMust( + basetypes.StringType{}, + []attr.Value{ + basetypes.NewStringValue("arg1-element0"), + basetypes.NewStringValue("arg1-element1"), + }, + ) + + if !arg0.Equal(expectedArg0) { + resp.Diagnostics.AddError( + "Unexpected Argument 0 Difference", + fmt.Sprintf("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.Diagnostics.Append(resp.Result.Set(ctx, basetypes.NewStringValue("result"))...) + }, + }, + FunctionDefinition: function.Definition{ + Return: function.StringReturn{}, + }, + }, + expectedResponse: &fwserver.CallFunctionResponse{ + Diagnostics: nil, + Result: function.NewResultData(basetypes.NewStringValue("result")), + }, + }, + "request-arguments-getargument": { + server: &fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{}, + }, + request: &fwserver.CallFunctionRequest{ + Arguments: function.NewArgumentsData([]attr.Value{ + basetypes.NewBoolNull(), + basetypes.NewInt64Unknown(), + basetypes.NewStringValue("arg2"), + }), + Function: &testprovider.Function{ + RunMethod: func(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + var arg0 basetypes.BoolValue + 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)...) + + 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), + ) + } + + if !arg1.Equal(expectedArg1) { + resp.Diagnostics.AddError( + "Unexpected Argument 1 Difference", + fmt.Sprintf("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.Diagnostics.Append(resp.Result.Set(ctx, basetypes.NewStringValue("result"))...) + }, + }, + FunctionDefinition: function.Definition{ + Return: function.StringReturn{}, + }, + }, + expectedResponse: &fwserver.CallFunctionResponse{ + Diagnostics: nil, + Result: function.NewResultData(basetypes.NewStringValue("result")), + }, + }, + "request-arguments-getargument-reflection": { + server: &fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{}, + }, + request: &fwserver.CallFunctionRequest{ + Arguments: function.NewArgumentsData([]attr.Value{ + basetypes.NewStringValue("arg0"), + basetypes.NewStringNull(), + }), + Function: &testprovider.Function{ + RunMethod: func(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + var arg0 string + var arg1 *string + + resp.Diagnostics.Append(req.Arguments.GetArgument(ctx, 0, &arg0)...) + resp.Diagnostics.Append(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), + ) + } + + if arg1 != nil { + resp.Diagnostics.AddError( + "Unexpected Argument 1 Difference", + fmt.Sprintf("got: %s, expected: nil", *arg1), + ) + } + + resp.Diagnostics.Append(resp.Result.Set(ctx, basetypes.NewStringValue("result"))...) + }, + }, + FunctionDefinition: function.Definition{ + Return: function.StringReturn{}, + }, + }, + expectedResponse: &fwserver.CallFunctionResponse{ + Diagnostics: nil, + Result: function.NewResultData(basetypes.NewStringValue("result")), + }, + }, + "request-arguments-getargument-variadic": { + server: &fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{}, + }, + request: &fwserver.CallFunctionRequest{ + Arguments: function.NewArgumentsData([]attr.Value{ + basetypes.NewStringValue("arg0"), + basetypes.NewListValueMust( + basetypes.StringType{}, + []attr.Value{ + basetypes.NewStringValue("arg1-element0"), + basetypes.NewStringValue("arg1-element1"), + }, + ), + }), + Function: &testprovider.Function{ + RunMethod: func(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + var arg0 basetypes.StringValue + var arg1 basetypes.ListValue + + resp.Diagnostics.Append(req.Arguments.GetArgument(ctx, 0, &arg0)...) + resp.Diagnostics.Append(req.Arguments.GetArgument(ctx, 1, &arg1)...) + + expectedArg0 := basetypes.NewStringValue("arg0") + expectedArg1 := basetypes.NewListValueMust( + basetypes.StringType{}, + []attr.Value{ + basetypes.NewStringValue("arg1-element0"), + basetypes.NewStringValue("arg1-element1"), + }, + ) + + if !arg0.Equal(expectedArg0) { + resp.Diagnostics.AddError( + "Unexpected Argument 0 Difference", + fmt.Sprintf("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.Diagnostics.Append(resp.Result.Set(ctx, basetypes.NewStringValue("result"))...) + }, + }, + FunctionDefinition: function.Definition{ + Return: function.StringReturn{}, + }, + }, + expectedResponse: &fwserver.CallFunctionResponse{ + Diagnostics: nil, + Result: function.NewResultData(basetypes.NewStringValue("result")), + }, + }, + "response-diagnostics": { + server: &fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{}, + }, + request: &fwserver.CallFunctionRequest{ + 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") + }, + }, + FunctionDefinition: function.Definition{ + Return: function.StringReturn{}, + }, + }, + expectedResponse: &fwserver.CallFunctionResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("warning summary", "warning detail"), + diag.NewErrorDiagnostic("error summary", "error detail"), + }, + Result: function.NewResultData(basetypes.NewStringUnknown()), + }, + }, + "response-result": { + server: &fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{}, + }, + request: &fwserver.CallFunctionRequest{ + 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"))...) + }, + }, + FunctionDefinition: function.Definition{ + Return: function.StringReturn{}, + }, + }, + expectedResponse: &fwserver.CallFunctionResponse{ + Diagnostics: nil, + Result: function.NewResultData(basetypes.NewStringValue("result")), + }, + }, + "response-result-reflection": { + server: &fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{}, + }, + request: &fwserver.CallFunctionRequest{ + 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")...) + }, + }, + FunctionDefinition: function.Definition{ + Return: function.StringReturn{}, + }, + }, + expectedResponse: &fwserver.CallFunctionResponse{ + Diagnostics: nil, + Result: function.NewResultData(basetypes.NewStringValue("result")), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + response := &fwserver.CallFunctionResponse{} + testCase.server.CallFunction(context.Background(), testCase.request, response) + + if diff := cmp.Diff(response, testCase.expectedResponse); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/fwserver/server_functions.go b/internal/fwserver/server_functions.go new file mode 100644 index 000000000..6d02ad879 --- /dev/null +++ b/internal/fwserver/server_functions.go @@ -0,0 +1,194 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/provider" +) + +// Function returns the Function for a given name. +func (s *Server) Function(ctx context.Context, name string) (function.Function, diag.Diagnostics) { + functionFuncs, diags := s.FunctionFuncs(ctx) + + functionFunc, ok := functionFuncs[name] + + if !ok { + diags.AddError( + "Function Not Found", + fmt.Sprintf("No function named %q was found in the provider.", name), + ) + + return nil, diags + } + + return functionFunc(), diags +} + +// 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) { + s.functionDefinitionsMutex.RLock() + functionDefinition, ok := s.functionDefinitions[name] + s.functionDefinitionsMutex.RUnlock() + + if ok { + return functionDefinition, nil + } + + var diags diag.Diagnostics + + functionImpl, functionDiags := s.Function(ctx, name) + + diags.Append(functionDiags...) + + if diags.HasError() { + return function.Definition{}, diags + } + + definitionReq := function.DefinitionRequest{} + definitionResp := function.DefinitionResponse{} + + logging.FrameworkTrace(ctx, "Calling provider defined Function Definition method", map[string]interface{}{logging.KeyFunctionName: name}) + functionImpl.Definition(ctx, definitionReq, &definitionResp) + logging.FrameworkTrace(ctx, "Called provider defined Function Definition method", map[string]interface{}{logging.KeyFunctionName: name}) + + diags.Append(definitionResp.Diagnostics...) + + if diags.HasError() { + return definitionResp.Definition, diags + } + + s.functionDefinitionsMutex.Lock() + + if s.functionDefinitions == nil { + s.functionDefinitions = make(map[string]function.Definition) + } + + s.functionDefinitions[name] = definitionResp.Definition + + s.functionDefinitionsMutex.Unlock() + + return definitionResp.Definition, diags +} + +// FunctionDefinitions returns a map of Function Definitions for the +// GetProviderSchema RPC without caching since not all definitions are +// guaranteed to be necessary for later provider operations. The definition +// implementations are also validated. +func (s *Server) FunctionDefinitions(ctx context.Context) (map[string]function.Definition, diag.Diagnostics) { + functionDefinitions := make(map[string]function.Definition) + + functionFuncs, diags := s.FunctionFuncs(ctx) + + for name, functionFunc := range functionFuncs { + functionImpl := functionFunc() + + definitionReq := function.DefinitionRequest{} + definitionResp := function.DefinitionResponse{} + + logging.FrameworkTrace(ctx, "Calling provider defined Function Definition", map[string]interface{}{logging.KeyFunctionName: name}) + functionImpl.Definition(ctx, definitionReq, &definitionResp) + logging.FrameworkTrace(ctx, "Called provider defined Function Definition", map[string]interface{}{logging.KeyFunctionName: name}) + + diags.Append(definitionResp.Diagnostics...) + + if definitionResp.Diagnostics.HasError() { + continue + } + + validateDiags := definitionResp.Definition.ValidateImplementation(ctx) + + diags.Append(validateDiags...) + + if validateDiags.HasError() { + continue + } + + functionDefinitions[name] = definitionResp.Definition + } + + return functionDefinitions, diags +} + +// FunctionFuncs returns a map of Function functions. The results are cached +// on first use. +func (s *Server) FunctionFuncs(ctx context.Context) (map[string]func() function.Function, diag.Diagnostics) { + logging.FrameworkTrace(ctx, "Checking FunctionTypes lock") + s.functionFuncsMutex.Lock() + defer s.functionFuncsMutex.Unlock() + + if s.functionFuncs != nil { + return s.functionFuncs, s.functionFuncsDiags + } + + s.functionFuncs = make(map[string]func() function.Function) + + provider, ok := s.Provider.(provider.ProviderWithFunctions) + + if !ok { + // Only function-specific RPCs should return diagnostics about the + // provider not implementing functions or missing functions. + return s.functionFuncs, s.functionFuncsDiags + } + + logging.FrameworkTrace(ctx, "Calling provider defined Provider Functions") + functionFuncs := provider.Functions(ctx) + logging.FrameworkTrace(ctx, "Called provider defined Provider Functions") + + for _, functionFunc := range functionFuncs { + functionImpl := functionFunc() + + metadataReq := function.MetadataRequest{} + metadataResp := function.MetadataResponse{} + + functionImpl.Metadata(ctx, metadataReq, &metadataResp) + + if metadataResp.Name == "" { + s.functionFuncsDiags.AddError( + "Function Name Missing", + fmt.Sprintf("The %T Function returned an empty string from the Metadata method. ", functionImpl)+ + "This is always an issue with the provider and should be reported to the provider developers.", + ) + continue + } + + logging.FrameworkTrace(ctx, "Found function", map[string]interface{}{logging.KeyFunctionName: metadataResp.Name}) + + if _, ok := s.functionFuncs[metadataResp.Name]; ok { + s.functionFuncsDiags.AddError( + "Duplicate Function Name Defined", + fmt.Sprintf("The %s function name was returned for multiple functions. ", metadataResp.Name)+ + "Function names must be unique. "+ + "This is always an issue with the provider and should be reported to the provider developers.", + ) + continue + } + + s.functionFuncs[metadataResp.Name] = functionFunc + } + + return s.functionFuncs, s.functionFuncsDiags +} + +// FunctionMetadatas returns a slice of FunctionMetadata for the GetMetadata +// RPC. +func (s *Server) FunctionMetadatas(ctx context.Context) ([]FunctionMetadata, diag.Diagnostics) { + functionFuncs, diags := s.FunctionFuncs(ctx) + + functionMetadatas := make([]FunctionMetadata, 0, len(functionFuncs)) + + for name := range functionFuncs { + functionMetadatas = append(functionMetadatas, FunctionMetadata{ + Name: name, + }) + } + + return functionMetadatas, diags +} diff --git a/internal/fwserver/server_getfunctions.go b/internal/fwserver/server_getfunctions.go new file mode 100644 index 000000000..e2567be84 --- /dev/null +++ b/internal/fwserver/server_getfunctions.go @@ -0,0 +1,37 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/function" +) + +// GetFunctionsRequest is the framework server request for the +// GetFunctions RPC. +type GetFunctionsRequest struct{} + +// GetFunctionsResponse is the framework server response for the +// GetFunctions RPC. +type GetFunctionsResponse struct { + FunctionDefinitions map[string]function.Definition + Diagnostics diag.Diagnostics +} + +// GetFunctions implements the framework server GetFunctions RPC. +func (s *Server) GetFunctions(ctx context.Context, req *GetFunctionsRequest, resp *GetFunctionsResponse) { + resp.FunctionDefinitions = map[string]function.Definition{} + + functionDefinitions, diags := s.FunctionDefinitions(ctx) + + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + resp.FunctionDefinitions = functionDefinitions +} diff --git a/internal/fwserver/server_getfunctions_test.go b/internal/fwserver/server_getfunctions_test.go new file mode 100644 index 000000000..8aae0b625 --- /dev/null +++ b/internal/fwserver/server_getfunctions_test.go @@ -0,0 +1,215 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver_test + +import ( + "context" + "testing" + + "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/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" +) + +func TestServerGetFunctions(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + server *fwserver.Server + request *fwserver.GetFunctionsRequest + expectedResponse *fwserver.GetFunctionsResponse + }{ + "empty-provider": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + expectedResponse: &fwserver.GetFunctionsResponse{ + FunctionDefinitions: map[string]function.Definition{}, + }, + }, + "functiondefinitions": { + server: &fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{ + FunctionsMethod: func(_ context.Context) []func() function.Function { + return []func() function.Function{ + func() function.Function { + return &testprovider.Function{ + DefinitionMethod: func(_ context.Context, _ function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Return: function.StringReturn{}, + } + }, + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "function1" + }, + } + }, + func() function.Function { + return &testprovider.Function{ + DefinitionMethod: func(_ context.Context, _ function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Return: function.StringReturn{}, + } + }, + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "function2" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetFunctionsRequest{}, + expectedResponse: &fwserver.GetFunctionsResponse{ + FunctionDefinitions: map[string]function.Definition{ + "function1": { + Return: function.StringReturn{}, + }, + "function2": { + Return: function.StringReturn{}, + }, + }, + }, + }, + "functiondefinitions-invalid-definition": { + server: &fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{ + FunctionsMethod: func(_ context.Context) []func() function.Function { + return []func() function.Function{ + func() function.Function { + return &testprovider.Function{ + DefinitionMethod: func(_ context.Context, _ function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Return: nil, // intentional + } + }, + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "function1" + }, + } + }, + func() function.Function { + return &testprovider.Function{ + DefinitionMethod: func(_ context.Context, _ function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Return: function.StringReturn{}, + } + }, + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "function2" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetFunctionsRequest{}, + expectedResponse: &fwserver.GetFunctionsResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Function Definition", + "When validating the function definition, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "Definition Return field is undefined", + ), + }, + FunctionDefinitions: map[string]function.Definition{}, + }, + }, + "functiondefinitions-duplicate-type-name": { + server: &fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{ + FunctionsMethod: func(_ context.Context) []func() function.Function { + return []func() function.Function{ + func() function.Function { + return &testprovider.Function{ + DefinitionMethod: func(_ context.Context, _ function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Return: function.StringReturn{}, + } + }, + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "testfunction" // intentionally duplicate + }, + } + }, + func() function.Function { + return &testprovider.Function{ + DefinitionMethod: func(_ context.Context, _ function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Return: function.StringReturn{}, + } + }, + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "testfunction" // intentionally duplicate + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetFunctionsRequest{}, + expectedResponse: &fwserver.GetFunctionsResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Duplicate Function Name Defined", + "The testfunction function name was returned for multiple functions. "+ + "Function names must be unique. "+ + "This is always an issue with the provider and should be reported to the provider developers.", + ), + }, + FunctionDefinitions: map[string]function.Definition{}, + }, + }, + "functiondefinitions-empty-name": { + server: &fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{ + FunctionsMethod: func(_ context.Context) []func() function.Function { + return []func() function.Function{ + func() function.Function { + return &testprovider.Function{ + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "" // intentionally empty + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetFunctionsRequest{}, + expectedResponse: &fwserver.GetFunctionsResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Function Name Missing", + "The *testprovider.Function Function returned an empty string from the Metadata method. "+ + "This is always an issue with the provider and should be reported to the provider developers.", + ), + }, + FunctionDefinitions: map[string]function.Definition{}, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + response := &fwserver.GetFunctionsResponse{} + testCase.server.GetFunctions(context.Background(), testCase.request, response) + + if diff := cmp.Diff(response, testCase.expectedResponse); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/fwserver/server_getmetadata.go b/internal/fwserver/server_getmetadata.go index 703fccd40..ebd0728a9 100644 --- a/internal/fwserver/server_getmetadata.go +++ b/internal/fwserver/server_getmetadata.go @@ -18,6 +18,7 @@ type GetMetadataRequest struct{} type GetMetadataResponse struct { DataSources []DataSourceMetadata Diagnostics diag.Diagnostics + Functions []FunctionMetadata Resources []ResourceMetadata ServerCapabilities *ServerCapabilities } @@ -29,6 +30,13 @@ type DataSourceMetadata struct { TypeName string } +// FunctionMetadata is the framework server equivalent of the +// tfprotov5.FunctionMetadata and tfprotov6.FunctionMetadata types. +type FunctionMetadata struct { + // Name is the name of the function. + Name string +} + // ResourceMetadata is the framework server equivalent of the // tfprotov5.ResourceMetadata and tfprotov6.ResourceMetadata types. type ResourceMetadata struct { @@ -39,6 +47,7 @@ type ResourceMetadata struct { // GetMetadata implements the framework server GetMetadata RPC. func (s *Server) GetMetadata(ctx context.Context, req *GetMetadataRequest, resp *GetMetadataResponse) { resp.DataSources = []DataSourceMetadata{} + resp.Functions = []FunctionMetadata{} resp.Resources = []ResourceMetadata{} resp.ServerCapabilities = s.ServerCapabilities() @@ -46,6 +55,10 @@ func (s *Server) GetMetadata(ctx context.Context, req *GetMetadataRequest, resp resp.Diagnostics.Append(diags...) + functionMetadatas, diags := s.FunctionMetadatas(ctx) + + resp.Diagnostics.Append(diags...) + resourceMetadatas, diags := s.ResourceMetadatas(ctx) resp.Diagnostics.Append(diags...) @@ -55,5 +68,6 @@ func (s *Server) GetMetadata(ctx context.Context, req *GetMetadataRequest, resp } resp.DataSources = datasourceMetadatas + resp.Functions = functionMetadatas resp.Resources = resourceMetadatas } diff --git a/internal/fwserver/server_getmetadata_test.go b/internal/fwserver/server_getmetadata_test.go index 7ba504723..653fb1655 100644 --- a/internal/fwserver/server_getmetadata_test.go +++ b/internal/fwserver/server_getmetadata_test.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/datasource" "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" "github.com/hashicorp/terraform-plugin-framework/provider" @@ -32,6 +33,7 @@ func TestServerGetMetadata(t *testing.T) { }, expectedResponse: &fwserver.GetMetadataResponse{ DataSources: []fwserver.DataSourceMetadata{}, + Functions: []fwserver.FunctionMetadata{}, Resources: []fwserver.ResourceMetadata{}, ServerCapabilities: &fwserver.ServerCapabilities{ GetProviderSchemaOptional: true, @@ -72,6 +74,7 @@ func TestServerGetMetadata(t *testing.T) { TypeName: "test_data_source2", }, }, + Functions: []fwserver.FunctionMetadata{}, Resources: []fwserver.ResourceMetadata{}, ServerCapabilities: &fwserver.ServerCapabilities{ GetProviderSchemaOptional: true, @@ -113,6 +116,7 @@ func TestServerGetMetadata(t *testing.T) { "This is always an issue with the provider and should be reported to the provider developers.", ), }, + Functions: []fwserver.FunctionMetadata{}, Resources: []fwserver.ResourceMetadata{}, ServerCapabilities: &fwserver.ServerCapabilities{ GetProviderSchemaOptional: true, @@ -146,6 +150,7 @@ func TestServerGetMetadata(t *testing.T) { "This is always an issue with the provider and should be reported to the provider developers.", ), }, + Functions: []fwserver.FunctionMetadata{}, Resources: []fwserver.ResourceMetadata{}, ServerCapabilities: &fwserver.ServerCapabilities{ GetProviderSchemaOptional: true, @@ -179,6 +184,124 @@ func TestServerGetMetadata(t *testing.T) { TypeName: "testprovidertype_data_source", }, }, + Functions: []fwserver.FunctionMetadata{}, + Resources: []fwserver.ResourceMetadata{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "functions": { + server: &fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{ + FunctionsMethod: func(_ context.Context) []func() function.Function { + return []func() function.Function{ + func() function.Function { + return &testprovider.Function{ + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "function1" + }, + } + }, + func() function.Function { + return &testprovider.Function{ + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "function2" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetMetadataRequest{}, + expectedResponse: &fwserver.GetMetadataResponse{ + DataSources: []fwserver.DataSourceMetadata{}, + Functions: []fwserver.FunctionMetadata{ + { + Name: "function1", + }, + { + Name: "function2", + }, + }, + Resources: []fwserver.ResourceMetadata{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "functions-duplicate-type-name": { + server: &fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{ + FunctionsMethod: func(_ context.Context) []func() function.Function { + return []func() function.Function{ + func() function.Function { + return &testprovider.Function{ + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "testfunction" // intentionally duplicate + }, + } + }, + func() function.Function { + return &testprovider.Function{ + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "testfunction" // intentionally duplicate + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetMetadataRequest{}, + expectedResponse: &fwserver.GetMetadataResponse{ + DataSources: []fwserver.DataSourceMetadata{}, + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Duplicate Function Name Defined", + "The testfunction function name was returned for multiple functions. "+ + "Function names must be unique. "+ + "This is always an issue with the provider and should be reported to the provider developers.", + ), + }, + Functions: []fwserver.FunctionMetadata{}, + Resources: []fwserver.ResourceMetadata{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "functions-empty-type-name": { + server: &fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{ + FunctionsMethod: func(_ context.Context) []func() function.Function { + return []func() function.Function{ + func() function.Function { + return &testprovider.Function{ + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "" // intentionally empty + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetMetadataRequest{}, + expectedResponse: &fwserver.GetMetadataResponse{ + DataSources: []fwserver.DataSourceMetadata{}, + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Function Name Missing", + "The *testprovider.Function Function returned an empty string from the Metadata method. "+ + "This is always an issue with the provider and should be reported to the provider developers.", + ), + }, + Functions: []fwserver.FunctionMetadata{}, Resources: []fwserver.ResourceMetadata{}, ServerCapabilities: &fwserver.ServerCapabilities{ GetProviderSchemaOptional: true, @@ -212,6 +335,7 @@ func TestServerGetMetadata(t *testing.T) { request: &fwserver.GetMetadataRequest{}, expectedResponse: &fwserver.GetMetadataResponse{ DataSources: []fwserver.DataSourceMetadata{}, + Functions: []fwserver.FunctionMetadata{}, Resources: []fwserver.ResourceMetadata{ { TypeName: "test_resource1", @@ -260,6 +384,7 @@ func TestServerGetMetadata(t *testing.T) { "This is always an issue with the provider and should be reported to the provider developers.", ), }, + Functions: []fwserver.FunctionMetadata{}, Resources: []fwserver.ResourceMetadata{}, ServerCapabilities: &fwserver.ServerCapabilities{ GetProviderSchemaOptional: true, @@ -293,6 +418,7 @@ func TestServerGetMetadata(t *testing.T) { "This is always an issue with the provider and should be reported to the provider developers.", ), }, + Functions: []fwserver.FunctionMetadata{}, Resources: []fwserver.ResourceMetadata{}, ServerCapabilities: &fwserver.ServerCapabilities{ GetProviderSchemaOptional: true, @@ -322,6 +448,7 @@ func TestServerGetMetadata(t *testing.T) { request: &fwserver.GetMetadataRequest{}, expectedResponse: &fwserver.GetMetadataResponse{ DataSources: []fwserver.DataSourceMetadata{}, + Functions: []fwserver.FunctionMetadata{}, Resources: []fwserver.ResourceMetadata{ { TypeName: "testprovidertype_resource", @@ -349,6 +476,10 @@ func TestServerGetMetadata(t *testing.T) { return response.DataSources[i].TypeName < response.DataSources[j].TypeName }) + sort.Slice(response.Functions, func(i int, j int) bool { + return response.Functions[i].Name < response.Functions[j].Name + }) + sort.Slice(response.Resources, func(i int, j int) bool { return response.Resources[i].TypeName < response.Resources[j].TypeName }) diff --git a/internal/fwserver/server_getproviderschema.go b/internal/fwserver/server_getproviderschema.go index b2acc82dd..afcca8352 100644 --- a/internal/fwserver/server_getproviderschema.go +++ b/internal/fwserver/server_getproviderschema.go @@ -7,6 +7,7 @@ import ( "context" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" ) @@ -17,12 +18,13 @@ type GetProviderSchemaRequest struct{} // GetProviderSchemaResponse is the framework server response for the // GetProviderSchema RPC. type GetProviderSchemaResponse struct { - ServerCapabilities *ServerCapabilities - Provider fwschema.Schema - ProviderMeta fwschema.Schema - ResourceSchemas map[string]fwschema.Schema - DataSourceSchemas map[string]fwschema.Schema - Diagnostics diag.Diagnostics + ServerCapabilities *ServerCapabilities + Provider fwschema.Schema + ProviderMeta fwschema.Schema + ResourceSchemas map[string]fwschema.Schema + DataSourceSchemas map[string]fwschema.Schema + FunctionDefinitions map[string]function.Definition + Diagnostics diag.Diagnostics } // GetProviderSchema implements the framework server GetProviderSchema RPC. @@ -68,4 +70,14 @@ func (s *Server) GetProviderSchema(ctx context.Context, req *GetProviderSchemaRe } resp.DataSourceSchemas = dataSourceSchemas + + functions, diags := s.FunctionDefinitions(ctx) + + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + resp.FunctionDefinitions = functions } diff --git a/internal/fwserver/server_getproviderschema_test.go b/internal/fwserver/server_getproviderschema_test.go index b956d47c4..b4abd0a43 100644 --- a/internal/fwserver/server_getproviderschema_test.go +++ b/internal/fwserver/server_getproviderschema_test.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/datasource" datasourceschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" @@ -35,9 +36,10 @@ func TestServerGetProviderSchema(t *testing.T) { Provider: &testprovider.Provider{}, }, expectedResponse: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{}, - Provider: providerschema.Schema{}, - ResourceSchemas: map[string]fwschema.Schema{}, + DataSourceSchemas: map[string]fwschema.Schema{}, + FunctionDefinitions: map[string]function.Definition{}, + Provider: providerschema.Schema{}, + ResourceSchemas: map[string]fwschema.Schema{}, ServerCapabilities: &fwserver.ServerCapabilities{ GetProviderSchemaOptional: true, PlanDestroy: true, @@ -103,8 +105,9 @@ func TestServerGetProviderSchema(t *testing.T) { }, }, }, - Provider: providerschema.Schema{}, - ResourceSchemas: map[string]fwschema.Schema{}, + FunctionDefinitions: map[string]function.Definition{}, + Provider: providerschema.Schema{}, + ResourceSchemas: map[string]fwschema.Schema{}, ServerCapabilities: &fwserver.ServerCapabilities{ GetProviderSchemaOptional: true, PlanDestroy: true, @@ -304,6 +307,59 @@ func TestServerGetProviderSchema(t *testing.T) { }, }, }, + FunctionDefinitions: map[string]function.Definition{}, + Provider: providerschema.Schema{}, + ResourceSchemas: map[string]fwschema.Schema{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "functiondefinitions": { + server: &fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{ + FunctionsMethod: func(_ context.Context) []func() function.Function { + return []func() function.Function{ + func() function.Function { + return &testprovider.Function{ + DefinitionMethod: func(_ context.Context, _ function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Return: function.StringReturn{}, + } + }, + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "function1" + }, + } + }, + func() function.Function { + return &testprovider.Function{ + DefinitionMethod: func(_ context.Context, _ function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Return: function.StringReturn{}, + } + }, + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "function2" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetProviderSchemaRequest{}, + expectedResponse: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{}, + FunctionDefinitions: map[string]function.Definition{ + "function1": { + Return: function.StringReturn{}, + }, + "function2": { + Return: function.StringReturn{}, + }, + }, Provider: providerschema.Schema{}, ResourceSchemas: map[string]fwschema.Schema{}, ServerCapabilities: &fwserver.ServerCapabilities{ @@ -312,6 +368,147 @@ func TestServerGetProviderSchema(t *testing.T) { }, }, }, + "functiondefinitions-invalid-definition": { + server: &fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{ + FunctionsMethod: func(_ context.Context) []func() function.Function { + return []func() function.Function{ + func() function.Function { + return &testprovider.Function{ + DefinitionMethod: func(_ context.Context, _ function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Return: nil, // intentional + } + }, + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "function1" + }, + } + }, + func() function.Function { + return &testprovider.Function{ + DefinitionMethod: func(_ context.Context, _ function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Return: function.StringReturn{}, + } + }, + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "function2" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetProviderSchemaRequest{}, + expectedResponse: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{}, + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Function Definition", + "When validating the function definition, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "Definition Return field is undefined", + ), + }, + FunctionDefinitions: nil, + Provider: providerschema.Schema{}, + ResourceSchemas: map[string]fwschema.Schema{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "functiondefinitions-duplicate-type-name": { + server: &fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{ + FunctionsMethod: func(_ context.Context) []func() function.Function { + return []func() function.Function{ + func() function.Function { + return &testprovider.Function{ + DefinitionMethod: func(_ context.Context, _ function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Return: function.StringReturn{}, + } + }, + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "testfunction" // intentionally duplicate + }, + } + }, + func() function.Function { + return &testprovider.Function{ + DefinitionMethod: func(_ context.Context, _ function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Return: function.StringReturn{}, + } + }, + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "testfunction" // intentionally duplicate + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetProviderSchemaRequest{}, + expectedResponse: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{}, + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Duplicate Function Name Defined", + "The testfunction function name was returned for multiple functions. "+ + "Function names must be unique. "+ + "This is always an issue with the provider and should be reported to the provider developers.", + ), + }, + FunctionDefinitions: nil, + Provider: providerschema.Schema{}, + ResourceSchemas: map[string]fwschema.Schema{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "functiondefinitions-empty-name": { + server: &fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{ + FunctionsMethod: func(_ context.Context) []func() function.Function { + return []func() function.Function{ + func() function.Function { + return &testprovider.Function{ + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "" // intentionally empty + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetProviderSchemaRequest{}, + expectedResponse: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{}, + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Function Name Missing", + "The *testprovider.Function Function returned an empty string from the Metadata method. "+ + "This is always an issue with the provider and should be reported to the provider developers.", + ), + }, + FunctionDefinitions: nil, + Provider: providerschema.Schema{}, + ResourceSchemas: map[string]fwschema.Schema{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, "provider": { server: &fwserver.Server{ Provider: &testprovider.Provider{ @@ -328,7 +525,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &fwserver.GetProviderSchemaRequest{}, expectedResponse: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{}, + DataSourceSchemas: map[string]fwschema.Schema{}, + FunctionDefinitions: map[string]function.Definition{}, Provider: providerschema.Schema{ Attributes: map[string]providerschema.Attribute{ "test": providerschema.StringAttribute{ @@ -391,8 +589,9 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &fwserver.GetProviderSchemaRequest{}, expectedResponse: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{}, - Provider: providerschema.Schema{}, + DataSourceSchemas: map[string]fwschema.Schema{}, + FunctionDefinitions: map[string]function.Definition{}, + Provider: providerschema.Schema{}, ProviderMeta: metaschema.Schema{ Attributes: map[string]metaschema.Attribute{ "test": metaschema.StringAttribute{ @@ -483,8 +682,9 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &fwserver.GetProviderSchemaRequest{}, expectedResponse: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{}, - Provider: providerschema.Schema{}, + DataSourceSchemas: map[string]fwschema.Schema{}, + FunctionDefinitions: map[string]function.Definition{}, + Provider: providerschema.Schema{}, ResourceSchemas: map[string]fwschema.Schema{ "test_resource1": resourceschema.Schema{ Attributes: map[string]resourceschema.Attribute{ @@ -690,8 +890,9 @@ func TestServerGetProviderSchema(t *testing.T) { }, request: &fwserver.GetProviderSchemaRequest{}, expectedResponse: &fwserver.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]fwschema.Schema{}, - Provider: providerschema.Schema{}, + DataSourceSchemas: map[string]fwschema.Schema{}, + FunctionDefinitions: map[string]function.Definition{}, + Provider: providerschema.Schema{}, ResourceSchemas: map[string]fwschema.Schema{ "testprovidertype_resource": resourceschema.Schema{ Attributes: map[string]resourceschema.Attribute{ diff --git a/internal/logging/keys.go b/internal/logging/keys.go index f2100afc8..7c68d0f13 100644 --- a/internal/logging/keys.go +++ b/internal/logging/keys.go @@ -25,6 +25,9 @@ const ( // Underlying Go error string when logging an error. KeyError = "error" + // The name of function being operated on, such as "parse_xyz" + KeyFunctionName = "tf_function_name" + // The type of resource being operated on, such as "random_pet" KeyResourceType = "tf_resource_type" diff --git a/internal/proto5server/serve_test.go b/internal/proto5server/serve_test.go index bc5d94d1a..dc60839ef 100644 --- a/internal/proto5server/serve_test.go +++ b/internal/proto5server/serve_test.go @@ -55,6 +55,18 @@ func TestServerCancelInFlightContexts(t *testing.T) { // canceled, or we have an error reported } +func testNewSingleValueDynamicValue(t *testing.T, argumentValue tftypes.Value) *tfprotov5.DynamicValue { + t.Helper() + + dynamicValue, err := tfprotov5.NewDynamicValue(argumentValue.Type(), argumentValue) + + if err != nil { + t.Fatalf("unable to create DynamicValue: %s", err) + } + + return &dynamicValue +} + func testNewDynamicValue(t *testing.T, schemaType tftypes.Type, schemaValue map[string]tftypes.Value) *tfprotov5.DynamicValue { t.Helper() diff --git a/internal/proto5server/server_callfunction.go b/internal/proto5server/server_callfunction.go new file mode 100644 index 000000000..ad4a4478a --- /dev/null +++ b/internal/proto5server/server_callfunction.go @@ -0,0 +1,50 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto5server + +import ( + "context" + + "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. +func (s *Server) CallFunction(ctx context.Context, protoReq *tfprotov5.CallFunctionRequest) (*tfprotov5.CallFunctionResponse, error) { + ctx = s.registerContext(ctx) + ctx = logging.InitContext(ctx) + + fwResp := &fwserver.CallFunctionResponse{} + + function, diags := s.FrameworkServer.Function(ctx, protoReq.Name) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.CallFunctionResponse(ctx, fwResp), nil + } + + functionDefinition, diags := s.FrameworkServer.FunctionDefinition(ctx, protoReq.Name) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.CallFunctionResponse(ctx, fwResp), nil + } + + fwReq, diags := fromproto5.CallFunctionRequest(ctx, protoReq, function, functionDefinition) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.CallFunctionResponse(ctx, fwResp), nil + } + + s.FrameworkServer.CallFunction(ctx, fwReq, fwResp) + + return toproto5.CallFunctionResponse(ctx, fwResp), nil +} diff --git a/internal/proto5server/server_callfunction_test.go b/internal/proto5server/server_callfunction_test.go new file mode 100644 index 000000000..273008754 --- /dev/null +++ b/internal/proto5server/server_callfunction_test.go @@ -0,0 +1,275 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto5server + +import ( + "context" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "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) { + t.Parallel() + + testCases := map[string]struct { + server *Server + request *tfprotov5.CallFunctionRequest + expectedError error + expectedResponse *tfprotov5.CallFunctionResponse + }{ + "request-arguments": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{ + FunctionsMethod: func(ctx context.Context) []func() function.Function { + return []func() function.Function{ + func() function.Function { + return &testprovider.Function{ + MetadataMethod: func(ctx context.Context, req function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "testfunction" + }, + DefinitionMethod: func(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Parameters: []function.Parameter{ + function.BoolParameter{}, + function.Int64Parameter{}, + function.StringParameter{}, + }, + Return: function.StringReturn{}, + } + }, + RunMethod: func(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + var arg0 basetypes.BoolValue + var arg1 basetypes.Int64Value + var arg2 basetypes.StringValue + + resp.Diagnostics.Append(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), + ) + } + + if !arg1.Equal(expectedArg1) { + resp.Diagnostics.AddError( + "Unexpected Argument 1 Difference", + fmt.Sprintf("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.Diagnostics.Append(resp.Result.Set(ctx, basetypes.NewStringValue("result"))...) + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.CallFunctionRequest{ + Arguments: []*tfprotov5.DynamicValue{ + testNewSingleValueDynamicValue(t, tftypes.NewValue(tftypes.Bool, nil)), + testNewSingleValueDynamicValue(t, tftypes.NewValue(tftypes.Number, tftypes.UnknownValue)), + testNewSingleValueDynamicValue(t, tftypes.NewValue(tftypes.String, "arg2")), + }, + Name: "testfunction", + }, + expectedResponse: &tfprotov5.CallFunctionResponse{ + Result: testNewSingleValueDynamicValue(t, tftypes.NewValue(tftypes.String, "result")), + }, + }, + "request-arguments-variadic": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{ + FunctionsMethod: func(ctx context.Context) []func() function.Function { + return []func() function.Function{ + func() function.Function { + return &testprovider.Function{ + MetadataMethod: func(ctx context.Context, req function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "testfunction" + }, + DefinitionMethod: func(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Parameters: []function.Parameter{ + function.StringParameter{}, + }, + VariadicParameter: function.StringParameter{}, + Return: function.StringReturn{}, + } + }, + RunMethod: func(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + var arg0 basetypes.StringValue + var arg1 basetypes.ListValue + + resp.Diagnostics.Append(req.Arguments.Get(ctx, &arg0, &arg1)...) + + expectedArg0 := basetypes.NewStringValue("arg0") + expectedArg1 := basetypes.NewListValueMust( + basetypes.StringType{}, + []attr.Value{ + basetypes.NewStringValue("varg-arg1"), + basetypes.NewStringValue("varg-arg2"), + }, + ) + + if !arg0.Equal(expectedArg0) { + resp.Diagnostics.AddError( + "Unexpected Argument 0 Difference", + fmt.Sprintf("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.Diagnostics.Append(resp.Result.Set(ctx, basetypes.NewStringValue("result"))...) + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.CallFunctionRequest{ + Arguments: []*tfprotov5.DynamicValue{ + testNewSingleValueDynamicValue(t, tftypes.NewValue(tftypes.String, "arg0")), + testNewSingleValueDynamicValue(t, tftypes.NewValue(tftypes.String, "varg-arg1")), + testNewSingleValueDynamicValue(t, tftypes.NewValue(tftypes.String, "varg-arg2")), + }, + Name: "testfunction", + }, + expectedResponse: &tfprotov5.CallFunctionResponse{ + Result: testNewSingleValueDynamicValue(t, tftypes.NewValue(tftypes.String, "result")), + }, + }, + "response-diagnostics": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{ + FunctionsMethod: func(ctx context.Context) []func() function.Function { + return []func() function.Function{ + func() function.Function { + return &testprovider.Function{ + MetadataMethod: func(ctx context.Context, req function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "testfunction" + }, + DefinitionMethod: func(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Return: function.StringReturn{}, + } + }, + 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"))...) + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.CallFunctionRequest{ + Arguments: []*tfprotov5.DynamicValue{}, + 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", + }, + }, + Result: testNewSingleValueDynamicValue(t, tftypes.NewValue(tftypes.String, "result")), + }, + }, + "response-result": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{ + FunctionsMethod: func(ctx context.Context) []func() function.Function { + return []func() function.Function{ + func() function.Function { + return &testprovider.Function{ + MetadataMethod: func(ctx context.Context, req function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "testfunction" + }, + DefinitionMethod: func(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Return: function.StringReturn{}, + } + }, + RunMethod: func(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + resp.Diagnostics.Append(resp.Result.Set(ctx, basetypes.NewStringValue("result"))...) + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.CallFunctionRequest{ + Arguments: []*tfprotov5.DynamicValue{}, + Name: "testfunction", + }, + expectedResponse: &tfprotov5.CallFunctionResponse{ + Result: testNewSingleValueDynamicValue(t, tftypes.NewValue(tftypes.String, "result")), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.server.CallFunction(context.Background(), testCase.request) + + if diff := cmp.Diff(testCase.expectedError, err); diff != "" { + t.Errorf("unexpected error difference: %s", diff) + } + + if diff := cmp.Diff(testCase.expectedResponse, got); diff != "" { + t.Errorf("unexpected response difference: %s", diff) + } + }) + } +} diff --git a/internal/proto5server/server_getfunctions.go b/internal/proto5server/server_getfunctions.go new file mode 100644 index 000000000..25c93c5c0 --- /dev/null +++ b/internal/proto5server/server_getfunctions.go @@ -0,0 +1,27 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto5server + +import ( + "context" + + "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" +) + +// GetFunctions satisfies the tfprotov5.ProviderServer interface. +func (s *Server) GetFunctions(ctx context.Context, protoReq *tfprotov5.GetFunctionsRequest) (*tfprotov5.GetFunctionsResponse, error) { + ctx = s.registerContext(ctx) + ctx = logging.InitContext(ctx) + + fwReq := fromproto5.GetFunctionsRequest(ctx, protoReq) + fwResp := &fwserver.GetFunctionsResponse{} + + s.FrameworkServer.GetFunctions(ctx, fwReq, fwResp) + + return toproto5.GetFunctionsResponse(ctx, fwResp), nil +} diff --git a/internal/proto5server/server_getfunctions_test.go b/internal/proto5server/server_getfunctions_test.go new file mode 100644 index 000000000..07060e83f --- /dev/null +++ b/internal/proto5server/server_getfunctions_test.go @@ -0,0 +1,179 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto5server + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "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-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestServerGetFunctions(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + server *Server + request *tfprotov5.GetFunctionsRequest + expectedError error + expectedResponse *tfprotov5.GetFunctionsResponse + }{ + "functions": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{ + FunctionsMethod: func(_ context.Context) []func() function.Function { + return []func() function.Function{ + func() function.Function { + return &testprovider.Function{ + DefinitionMethod: func(_ context.Context, _ function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Return: function.StringReturn{}, + } + }, + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "function1" + }, + } + }, + func() function.Function { + return &testprovider.Function{ + DefinitionMethod: func(_ context.Context, _ function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Return: function.StringReturn{}, + } + }, + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "function2" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.GetFunctionsRequest{}, + expectedResponse: &tfprotov5.GetFunctionsResponse{ + Functions: map[string]*tfprotov5.Function{ + "function1": { + Parameters: []*tfprotov5.FunctionParameter{}, + Return: &tfprotov5.FunctionReturn{ + Type: tftypes.String, + }, + }, + "function2": { + Parameters: []*tfprotov5.FunctionParameter{}, + Return: &tfprotov5.FunctionReturn{ + Type: tftypes.String, + }, + }, + }, + }, + }, + "functions-duplicate-type-name": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{ + FunctionsMethod: func(_ context.Context) []func() function.Function { + return []func() function.Function{ + func() function.Function { + return &testprovider.Function{ + DefinitionMethod: func(_ context.Context, _ function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Return: function.StringReturn{}, + } + }, + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "testfunction" // intentionally duplicate + }, + } + }, + func() function.Function { + return &testprovider.Function{ + DefinitionMethod: func(_ context.Context, _ function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Return: function.StringReturn{}, + } + }, + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "testfunction" // intentionally duplicate + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.GetFunctionsRequest{}, + expectedResponse: &tfprotov5.GetFunctionsResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Duplicate Function Name Defined", + Detail: "The testfunction function name was returned for multiple functions. " + + "Function names must be unique. " + + "This is always an issue with the provider and should be reported to the provider developers.", + }, + }, + Functions: map[string]*tfprotov5.Function{}, + }, + }, + "functions-empty-name": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{ + FunctionsMethod: func(_ context.Context) []func() function.Function { + return []func() function.Function{ + func() function.Function { + return &testprovider.Function{ + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "" // intentionally empty + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.GetFunctionsRequest{}, + expectedResponse: &tfprotov5.GetFunctionsResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Function Name Missing", + Detail: "The *testprovider.Function Function returned an empty string from the Metadata method. " + + "This is always an issue with the provider and should be reported to the provider developers.", + }, + }, + Functions: map[string]*tfprotov5.Function{}, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.server.GetFunctions(context.Background(), new(tfprotov5.GetFunctionsRequest)) + + if diff := cmp.Diff(testCase.expectedError, err); diff != "" { + t.Errorf("unexpected error difference: %s", diff) + } + + if diff := cmp.Diff(testCase.expectedResponse, got); diff != "" { + t.Errorf("unexpected response difference: %s", diff) + } + }) + } +} diff --git a/internal/proto5server/server_getmetadata_test.go b/internal/proto5server/server_getmetadata_test.go index a6b090759..decc62739 100644 --- a/internal/proto5server/server_getmetadata_test.go +++ b/internal/proto5server/server_getmetadata_test.go @@ -10,6 +10,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/datasource" + "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/resource" @@ -60,6 +61,7 @@ func TestServerGetMetadata(t *testing.T) { TypeName: "test_data_source2", }, }, + Functions: []tfprotov5.FunctionMetadata{}, Resources: []tfprotov5.ResourceMetadata{}, ServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, @@ -104,6 +106,7 @@ func TestServerGetMetadata(t *testing.T) { "This is always an issue with the provider and should be reported to the provider developers.", }, }, + Functions: []tfprotov5.FunctionMetadata{}, Resources: []tfprotov5.ResourceMetadata{}, ServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, @@ -140,6 +143,132 @@ func TestServerGetMetadata(t *testing.T) { "This is always an issue with the provider and should be reported to the provider developers.", }, }, + Functions: []tfprotov5.FunctionMetadata{}, + Resources: []tfprotov5.ResourceMetadata{}, + ServerCapabilities: &tfprotov5.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "functions": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{ + FunctionsMethod: func(_ context.Context) []func() function.Function { + return []func() function.Function{ + func() function.Function { + return &testprovider.Function{ + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "function1" + }, + } + }, + func() function.Function { + return &testprovider.Function{ + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "function2" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.GetMetadataRequest{}, + expectedResponse: &tfprotov5.GetMetadataResponse{ + DataSources: []tfprotov5.DataSourceMetadata{}, + Functions: []tfprotov5.FunctionMetadata{ + { + Name: "function1", + }, + { + Name: "function2", + }, + }, + Resources: []tfprotov5.ResourceMetadata{}, + ServerCapabilities: &tfprotov5.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "functions-duplicate-type-name": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{ + FunctionsMethod: func(_ context.Context) []func() function.Function { + return []func() function.Function{ + func() function.Function { + return &testprovider.Function{ + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "testfunction" // intentionally duplicate + }, + } + }, + func() function.Function { + return &testprovider.Function{ + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "testfunction" // intentionally duplicate + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.GetMetadataRequest{}, + expectedResponse: &tfprotov5.GetMetadataResponse{ + DataSources: []tfprotov5.DataSourceMetadata{}, + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Duplicate Function Name Defined", + Detail: "The testfunction function name was returned for multiple functions. " + + "Function names must be unique. " + + "This is always an issue with the provider and should be reported to the provider developers.", + }, + }, + Functions: []tfprotov5.FunctionMetadata{}, + Resources: []tfprotov5.ResourceMetadata{}, + ServerCapabilities: &tfprotov5.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "functions-empty-type-name": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{ + FunctionsMethod: func(_ context.Context) []func() function.Function { + return []func() function.Function{ + func() function.Function { + return &testprovider.Function{ + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "" // intentionally empty + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.GetMetadataRequest{}, + expectedResponse: &tfprotov5.GetMetadataResponse{ + DataSources: []tfprotov5.DataSourceMetadata{}, + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Function Name Missing", + Detail: "The *testprovider.Function Function returned an empty string from the Metadata method. " + + "This is always an issue with the provider and should be reported to the provider developers.", + }, + }, + Functions: []tfprotov5.FunctionMetadata{}, Resources: []tfprotov5.ResourceMetadata{}, ServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, @@ -175,6 +304,7 @@ func TestServerGetMetadata(t *testing.T) { request: &tfprotov5.GetMetadataRequest{}, expectedResponse: &tfprotov5.GetMetadataResponse{ DataSources: []tfprotov5.DataSourceMetadata{}, + Functions: []tfprotov5.FunctionMetadata{}, Resources: []tfprotov5.ResourceMetadata{ { TypeName: "test_resource1", @@ -226,6 +356,7 @@ func TestServerGetMetadata(t *testing.T) { "This is always an issue with the provider and should be reported to the provider developers.", }, }, + Functions: []tfprotov5.FunctionMetadata{}, Resources: []tfprotov5.ResourceMetadata{}, ServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, @@ -262,6 +393,7 @@ func TestServerGetMetadata(t *testing.T) { "This is always an issue with the provider and should be reported to the provider developers.", }, }, + Functions: []tfprotov5.FunctionMetadata{}, Resources: []tfprotov5.ResourceMetadata{}, ServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, @@ -288,6 +420,10 @@ func TestServerGetMetadata(t *testing.T) { return got.DataSources[i].TypeName < got.DataSources[j].TypeName }) + sort.Slice(got.Functions, func(i int, j int) bool { + return got.Functions[i].Name < got.Functions[j].Name + }) + sort.Slice(got.Resources, func(i int, j int) bool { return got.Resources[i].TypeName < got.Resources[j].TypeName }) diff --git a/internal/proto5server/server_getproviderschema_test.go b/internal/proto5server/server_getproviderschema_test.go index 2c2f15cb8..29b33024a 100644 --- a/internal/proto5server/server_getproviderschema_test.go +++ b/internal/proto5server/server_getproviderschema_test.go @@ -11,6 +11,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/datasource" datasourceschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/logging" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" @@ -102,6 +103,7 @@ func TestServerGetProviderSchema(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{}, }, @@ -167,6 +169,7 @@ func TestServerGetProviderSchema(t *testing.T) { "This is always an issue with the provider and should be reported to the provider developers.", }, }, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{}, }, @@ -206,6 +209,167 @@ func TestServerGetProviderSchema(t *testing.T) { "This is always an issue with the provider and should be reported to the provider developers.", }, }, + Functions: map[string]*tfprotov5.Function{}, + Provider: &tfprotov5.Schema{ + Block: &tfprotov5.SchemaBlock{}, + }, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + ServerCapabilities: &tfprotov5.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "functions": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{ + FunctionsMethod: func(_ context.Context) []func() function.Function { + return []func() function.Function{ + func() function.Function { + return &testprovider.Function{ + DefinitionMethod: func(_ context.Context, _ function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Return: function.StringReturn{}, + } + }, + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "function1" + }, + } + }, + func() function.Function { + return &testprovider.Function{ + DefinitionMethod: func(_ context.Context, _ function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Return: function.StringReturn{}, + } + }, + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "function2" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.GetProviderSchemaRequest{}, + expectedResponse: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{ + "function1": { + Parameters: []*tfprotov5.FunctionParameter{}, + Return: &tfprotov5.FunctionReturn{ + Type: tftypes.String, + }, + }, + "function2": { + Parameters: []*tfprotov5.FunctionParameter{}, + Return: &tfprotov5.FunctionReturn{ + Type: tftypes.String, + }, + }, + }, + Provider: &tfprotov5.Schema{ + Block: &tfprotov5.SchemaBlock{}, + }, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + ServerCapabilities: &tfprotov5.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "functions-duplicate-type-name": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{ + FunctionsMethod: func(_ context.Context) []func() function.Function { + return []func() function.Function{ + func() function.Function { + return &testprovider.Function{ + DefinitionMethod: func(_ context.Context, _ function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Return: function.StringReturn{}, + } + }, + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "testfunction" // intentionally duplicate + }, + } + }, + func() function.Function { + return &testprovider.Function{ + DefinitionMethod: func(_ context.Context, _ function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Return: function.StringReturn{}, + } + }, + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "testfunction" // intentionally duplicate + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.GetProviderSchemaRequest{}, + expectedResponse: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Duplicate Function Name Defined", + Detail: "The testfunction function name was returned for multiple functions. " + + "Function names must be unique. " + + "This is always an issue with the provider and should be reported to the provider developers.", + }, + }, + Functions: map[string]*tfprotov5.Function{}, + Provider: &tfprotov5.Schema{ + Block: &tfprotov5.SchemaBlock{}, + }, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + ServerCapabilities: &tfprotov5.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "functions-empty-name": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{ + FunctionsMethod: func(_ context.Context) []func() function.Function { + return []func() function.Function{ + func() function.Function { + return &testprovider.Function{ + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "" // intentionally empty + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.GetProviderSchemaRequest{}, + expectedResponse: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Function Name Missing", + Detail: "The *testprovider.Function Function returned an empty string from the Metadata method. " + + "This is always an issue with the provider and should be reported to the provider developers.", + }, + }, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{}, }, @@ -235,6 +399,7 @@ func TestServerGetProviderSchema(t *testing.T) { request: &tfprotov5.GetProviderSchemaRequest{}, expectedResponse: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -273,6 +438,7 @@ func TestServerGetProviderSchema(t *testing.T) { request: &tfprotov5.GetProviderSchemaRequest{}, expectedResponse: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{}, }, @@ -340,6 +506,7 @@ func TestServerGetProviderSchema(t *testing.T) { request: &tfprotov5.GetProviderSchemaRequest{}, expectedResponse: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{}, }, @@ -428,6 +595,7 @@ func TestServerGetProviderSchema(t *testing.T) { "This is always an issue with the provider and should be reported to the provider developers.", }, }, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{}, }, @@ -467,6 +635,7 @@ func TestServerGetProviderSchema(t *testing.T) { "This is always an issue with the provider and should be reported to the provider developers.", }, }, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{}, }, @@ -600,6 +769,11 @@ func TestServerGetProviderSchema_logging(t *testing.T) { "@message": "Called provider defined Provider DataSources", "@module": "sdk.framework", }, + { + "@level": "trace", + "@message": "Checking FunctionTypes lock", + "@module": "sdk.framework", + }, } if diff := cmp.Diff(entries, expectedEntries); diff != "" { diff --git a/internal/proto6server/serve_test.go b/internal/proto6server/serve_test.go index 2bb1a0df4..c80ca583c 100644 --- a/internal/proto6server/serve_test.go +++ b/internal/proto6server/serve_test.go @@ -54,6 +54,18 @@ func TestServerCancelInFlightContexts(t *testing.T) { // canceled, or we have an error reported } +func testNewSingleValueDynamicValue(t *testing.T, argumentValue tftypes.Value) *tfprotov6.DynamicValue { + t.Helper() + + dynamicValue, err := tfprotov6.NewDynamicValue(argumentValue.Type(), argumentValue) + + if err != nil { + t.Fatalf("unable to create DynamicValue: %s", err) + } + + return &dynamicValue +} + func testNewDynamicValue(t *testing.T, schemaType tftypes.Type, schemaValue map[string]tftypes.Value) *tfprotov6.DynamicValue { t.Helper() diff --git a/internal/proto6server/server_callfunction.go b/internal/proto6server/server_callfunction.go new file mode 100644 index 000000000..8b216d240 --- /dev/null +++ b/internal/proto6server/server_callfunction.go @@ -0,0 +1,50 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto6server + +import ( + "context" + + "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. +func (s *Server) CallFunction(ctx context.Context, protoReq *tfprotov6.CallFunctionRequest) (*tfprotov6.CallFunctionResponse, error) { + ctx = s.registerContext(ctx) + ctx = logging.InitContext(ctx) + + fwResp := &fwserver.CallFunctionResponse{} + + function, diags := s.FrameworkServer.Function(ctx, protoReq.Name) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.CallFunctionResponse(ctx, fwResp), nil + } + + functionDefinition, diags := s.FrameworkServer.FunctionDefinition(ctx, protoReq.Name) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.CallFunctionResponse(ctx, fwResp), nil + } + + fwReq, diags := fromproto6.CallFunctionRequest(ctx, protoReq, function, functionDefinition) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.CallFunctionResponse(ctx, fwResp), nil + } + + s.FrameworkServer.CallFunction(ctx, fwReq, fwResp) + + return toproto6.CallFunctionResponse(ctx, fwResp), nil +} diff --git a/internal/proto6server/server_callfunction_test.go b/internal/proto6server/server_callfunction_test.go new file mode 100644 index 000000000..aae148006 --- /dev/null +++ b/internal/proto6server/server_callfunction_test.go @@ -0,0 +1,275 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto6server + +import ( + "context" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "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) { + t.Parallel() + + testCases := map[string]struct { + server *Server + request *tfprotov6.CallFunctionRequest + expectedError error + expectedResponse *tfprotov6.CallFunctionResponse + }{ + "request-arguments": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{ + FunctionsMethod: func(ctx context.Context) []func() function.Function { + return []func() function.Function{ + func() function.Function { + return &testprovider.Function{ + MetadataMethod: func(ctx context.Context, req function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "testfunction" + }, + DefinitionMethod: func(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Parameters: []function.Parameter{ + function.BoolParameter{}, + function.Int64Parameter{}, + function.StringParameter{}, + }, + Return: function.StringReturn{}, + } + }, + RunMethod: func(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + var arg0 basetypes.BoolValue + var arg1 basetypes.Int64Value + var arg2 basetypes.StringValue + + resp.Diagnostics.Append(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), + ) + } + + if !arg1.Equal(expectedArg1) { + resp.Diagnostics.AddError( + "Unexpected Argument 1 Difference", + fmt.Sprintf("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.Diagnostics.Append(resp.Result.Set(ctx, basetypes.NewStringValue("result"))...) + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.CallFunctionRequest{ + Arguments: []*tfprotov6.DynamicValue{ + testNewSingleValueDynamicValue(t, tftypes.NewValue(tftypes.Bool, nil)), + testNewSingleValueDynamicValue(t, tftypes.NewValue(tftypes.Number, tftypes.UnknownValue)), + testNewSingleValueDynamicValue(t, tftypes.NewValue(tftypes.String, "arg2")), + }, + Name: "testfunction", + }, + expectedResponse: &tfprotov6.CallFunctionResponse{ + Result: testNewSingleValueDynamicValue(t, tftypes.NewValue(tftypes.String, "result")), + }, + }, + "request-arguments-variadic": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{ + FunctionsMethod: func(ctx context.Context) []func() function.Function { + return []func() function.Function{ + func() function.Function { + return &testprovider.Function{ + MetadataMethod: func(ctx context.Context, req function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "testfunction" + }, + DefinitionMethod: func(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Parameters: []function.Parameter{ + function.StringParameter{}, + }, + VariadicParameter: function.StringParameter{}, + Return: function.StringReturn{}, + } + }, + RunMethod: func(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + var arg0 basetypes.StringValue + var arg1 basetypes.ListValue + + resp.Diagnostics.Append(req.Arguments.Get(ctx, &arg0, &arg1)...) + + expectedArg0 := basetypes.NewStringValue("arg0") + expectedArg1 := basetypes.NewListValueMust( + basetypes.StringType{}, + []attr.Value{ + basetypes.NewStringValue("varg-arg1"), + basetypes.NewStringValue("varg-arg2"), + }, + ) + + if !arg0.Equal(expectedArg0) { + resp.Diagnostics.AddError( + "Unexpected Argument 0 Difference", + fmt.Sprintf("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.Diagnostics.Append(resp.Result.Set(ctx, basetypes.NewStringValue("result"))...) + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.CallFunctionRequest{ + Arguments: []*tfprotov6.DynamicValue{ + testNewSingleValueDynamicValue(t, tftypes.NewValue(tftypes.String, "arg0")), + testNewSingleValueDynamicValue(t, tftypes.NewValue(tftypes.String, "varg-arg1")), + testNewSingleValueDynamicValue(t, tftypes.NewValue(tftypes.String, "varg-arg2")), + }, + Name: "testfunction", + }, + expectedResponse: &tfprotov6.CallFunctionResponse{ + Result: testNewSingleValueDynamicValue(t, tftypes.NewValue(tftypes.String, "result")), + }, + }, + "response-diagnostics": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{ + FunctionsMethod: func(ctx context.Context) []func() function.Function { + return []func() function.Function{ + func() function.Function { + return &testprovider.Function{ + MetadataMethod: func(ctx context.Context, req function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "testfunction" + }, + DefinitionMethod: func(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Return: function.StringReturn{}, + } + }, + 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"))...) + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.CallFunctionRequest{ + Arguments: []*tfprotov6.DynamicValue{}, + 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", + }, + }, + Result: testNewSingleValueDynamicValue(t, tftypes.NewValue(tftypes.String, "result")), + }, + }, + "response-result": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{ + FunctionsMethod: func(ctx context.Context) []func() function.Function { + return []func() function.Function{ + func() function.Function { + return &testprovider.Function{ + MetadataMethod: func(ctx context.Context, req function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "testfunction" + }, + DefinitionMethod: func(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Return: function.StringReturn{}, + } + }, + RunMethod: func(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + resp.Diagnostics.Append(resp.Result.Set(ctx, basetypes.NewStringValue("result"))...) + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.CallFunctionRequest{ + Arguments: []*tfprotov6.DynamicValue{}, + Name: "testfunction", + }, + expectedResponse: &tfprotov6.CallFunctionResponse{ + Result: testNewSingleValueDynamicValue(t, tftypes.NewValue(tftypes.String, "result")), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.server.CallFunction(context.Background(), testCase.request) + + if diff := cmp.Diff(testCase.expectedError, err); diff != "" { + t.Errorf("unexpected error difference: %s", diff) + } + + if diff := cmp.Diff(testCase.expectedResponse, got); diff != "" { + t.Errorf("unexpected response difference: %s", diff) + } + }) + } +} diff --git a/internal/proto6server/server_getfunctions.go b/internal/proto6server/server_getfunctions.go new file mode 100644 index 000000000..6201672c3 --- /dev/null +++ b/internal/proto6server/server_getfunctions.go @@ -0,0 +1,27 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto6server + +import ( + "context" + + "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" +) + +// GetFunctions satisfies the tfprotov6.ProviderServer interface. +func (s *Server) GetFunctions(ctx context.Context, protoReq *tfprotov6.GetFunctionsRequest) (*tfprotov6.GetFunctionsResponse, error) { + ctx = s.registerContext(ctx) + ctx = logging.InitContext(ctx) + + fwReq := fromproto6.GetFunctionsRequest(ctx, protoReq) + fwResp := &fwserver.GetFunctionsResponse{} + + s.FrameworkServer.GetFunctions(ctx, fwReq, fwResp) + + return toproto6.GetFunctionsResponse(ctx, fwResp), nil +} diff --git a/internal/proto6server/server_getfunctions_test.go b/internal/proto6server/server_getfunctions_test.go new file mode 100644 index 000000000..f503f7ef3 --- /dev/null +++ b/internal/proto6server/server_getfunctions_test.go @@ -0,0 +1,179 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto6server + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "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-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestServerGetFunctions(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + server *Server + request *tfprotov6.GetFunctionsRequest + expectedError error + expectedResponse *tfprotov6.GetFunctionsResponse + }{ + "functions": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{ + FunctionsMethod: func(_ context.Context) []func() function.Function { + return []func() function.Function{ + func() function.Function { + return &testprovider.Function{ + DefinitionMethod: func(_ context.Context, _ function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Return: function.StringReturn{}, + } + }, + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "function1" + }, + } + }, + func() function.Function { + return &testprovider.Function{ + DefinitionMethod: func(_ context.Context, _ function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Return: function.StringReturn{}, + } + }, + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "function2" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.GetFunctionsRequest{}, + expectedResponse: &tfprotov6.GetFunctionsResponse{ + Functions: map[string]*tfprotov6.Function{ + "function1": { + Parameters: []*tfprotov6.FunctionParameter{}, + Return: &tfprotov6.FunctionReturn{ + Type: tftypes.String, + }, + }, + "function2": { + Parameters: []*tfprotov6.FunctionParameter{}, + Return: &tfprotov6.FunctionReturn{ + Type: tftypes.String, + }, + }, + }, + }, + }, + "functions-duplicate-type-name": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{ + FunctionsMethod: func(_ context.Context) []func() function.Function { + return []func() function.Function{ + func() function.Function { + return &testprovider.Function{ + DefinitionMethod: func(_ context.Context, _ function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Return: function.StringReturn{}, + } + }, + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "testfunction" // intentionally duplicate + }, + } + }, + func() function.Function { + return &testprovider.Function{ + DefinitionMethod: func(_ context.Context, _ function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Return: function.StringReturn{}, + } + }, + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "testfunction" // intentionally duplicate + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.GetFunctionsRequest{}, + expectedResponse: &tfprotov6.GetFunctionsResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Duplicate Function Name Defined", + Detail: "The testfunction function name was returned for multiple functions. " + + "Function names must be unique. " + + "This is always an issue with the provider and should be reported to the provider developers.", + }, + }, + Functions: map[string]*tfprotov6.Function{}, + }, + }, + "functions-empty-name": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{ + FunctionsMethod: func(_ context.Context) []func() function.Function { + return []func() function.Function{ + func() function.Function { + return &testprovider.Function{ + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "" // intentionally empty + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.GetFunctionsRequest{}, + expectedResponse: &tfprotov6.GetFunctionsResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Function Name Missing", + Detail: "The *testprovider.Function Function returned an empty string from the Metadata method. " + + "This is always an issue with the provider and should be reported to the provider developers.", + }, + }, + Functions: map[string]*tfprotov6.Function{}, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.server.GetFunctions(context.Background(), new(tfprotov6.GetFunctionsRequest)) + + if diff := cmp.Diff(testCase.expectedError, err); diff != "" { + t.Errorf("unexpected error difference: %s", diff) + } + + if diff := cmp.Diff(testCase.expectedResponse, got); diff != "" { + t.Errorf("unexpected response difference: %s", diff) + } + }) + } +} diff --git a/internal/proto6server/server_getmetadata_test.go b/internal/proto6server/server_getmetadata_test.go index 1c65ab30b..35dc1a4c7 100644 --- a/internal/proto6server/server_getmetadata_test.go +++ b/internal/proto6server/server_getmetadata_test.go @@ -10,6 +10,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/datasource" + "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/resource" @@ -60,6 +61,7 @@ func TestServerGetMetadata(t *testing.T) { TypeName: "test_data_source2", }, }, + Functions: []tfprotov6.FunctionMetadata{}, Resources: []tfprotov6.ResourceMetadata{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, @@ -104,6 +106,7 @@ func TestServerGetMetadata(t *testing.T) { "This is always an issue with the provider and should be reported to the provider developers.", }, }, + Functions: []tfprotov6.FunctionMetadata{}, Resources: []tfprotov6.ResourceMetadata{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, @@ -140,6 +143,132 @@ func TestServerGetMetadata(t *testing.T) { "This is always an issue with the provider and should be reported to the provider developers.", }, }, + Functions: []tfprotov6.FunctionMetadata{}, + Resources: []tfprotov6.ResourceMetadata{}, + ServerCapabilities: &tfprotov6.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "functions": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{ + FunctionsMethod: func(_ context.Context) []func() function.Function { + return []func() function.Function{ + func() function.Function { + return &testprovider.Function{ + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "function1" + }, + } + }, + func() function.Function { + return &testprovider.Function{ + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "function2" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.GetMetadataRequest{}, + expectedResponse: &tfprotov6.GetMetadataResponse{ + DataSources: []tfprotov6.DataSourceMetadata{}, + Functions: []tfprotov6.FunctionMetadata{ + { + Name: "function1", + }, + { + Name: "function2", + }, + }, + Resources: []tfprotov6.ResourceMetadata{}, + ServerCapabilities: &tfprotov6.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "functions-duplicate-type-name": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{ + FunctionsMethod: func(_ context.Context) []func() function.Function { + return []func() function.Function{ + func() function.Function { + return &testprovider.Function{ + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "testfunction" // intentionally duplicate + }, + } + }, + func() function.Function { + return &testprovider.Function{ + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "testfunction" // intentionally duplicate + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.GetMetadataRequest{}, + expectedResponse: &tfprotov6.GetMetadataResponse{ + DataSources: []tfprotov6.DataSourceMetadata{}, + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Duplicate Function Name Defined", + Detail: "The testfunction function name was returned for multiple functions. " + + "Function names must be unique. " + + "This is always an issue with the provider and should be reported to the provider developers.", + }, + }, + Functions: []tfprotov6.FunctionMetadata{}, + Resources: []tfprotov6.ResourceMetadata{}, + ServerCapabilities: &tfprotov6.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "functions-empty-type-name": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{ + FunctionsMethod: func(_ context.Context) []func() function.Function { + return []func() function.Function{ + func() function.Function { + return &testprovider.Function{ + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "" // intentionally empty + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.GetMetadataRequest{}, + expectedResponse: &tfprotov6.GetMetadataResponse{ + DataSources: []tfprotov6.DataSourceMetadata{}, + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Function Name Missing", + Detail: "The *testprovider.Function Function returned an empty string from the Metadata method. " + + "This is always an issue with the provider and should be reported to the provider developers.", + }, + }, + Functions: []tfprotov6.FunctionMetadata{}, Resources: []tfprotov6.ResourceMetadata{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, @@ -175,6 +304,7 @@ func TestServerGetMetadata(t *testing.T) { request: &tfprotov6.GetMetadataRequest{}, expectedResponse: &tfprotov6.GetMetadataResponse{ DataSources: []tfprotov6.DataSourceMetadata{}, + Functions: []tfprotov6.FunctionMetadata{}, Resources: []tfprotov6.ResourceMetadata{ { TypeName: "test_resource1", @@ -226,6 +356,7 @@ func TestServerGetMetadata(t *testing.T) { "This is always an issue with the provider and should be reported to the provider developers.", }, }, + Functions: []tfprotov6.FunctionMetadata{}, Resources: []tfprotov6.ResourceMetadata{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, @@ -262,6 +393,7 @@ func TestServerGetMetadata(t *testing.T) { "This is always an issue with the provider and should be reported to the provider developers.", }, }, + Functions: []tfprotov6.FunctionMetadata{}, Resources: []tfprotov6.ResourceMetadata{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, @@ -288,6 +420,10 @@ func TestServerGetMetadata(t *testing.T) { return got.DataSources[i].TypeName < got.DataSources[j].TypeName }) + sort.Slice(got.Functions, func(i int, j int) bool { + return got.Functions[i].Name < got.Functions[j].Name + }) + sort.Slice(got.Resources, func(i int, j int) bool { return got.Resources[i].TypeName < got.Resources[j].TypeName }) diff --git a/internal/proto6server/server_getproviderschema_test.go b/internal/proto6server/server_getproviderschema_test.go index 989d339ad..fc5958f25 100644 --- a/internal/proto6server/server_getproviderschema_test.go +++ b/internal/proto6server/server_getproviderschema_test.go @@ -11,6 +11,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/datasource" datasourceschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/logging" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" @@ -102,6 +103,7 @@ func TestServerGetProviderSchema(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{}, }, @@ -167,6 +169,7 @@ func TestServerGetProviderSchema(t *testing.T) { "This is always an issue with the provider and should be reported to the provider developers.", }, }, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{}, }, @@ -206,6 +209,167 @@ func TestServerGetProviderSchema(t *testing.T) { "This is always an issue with the provider and should be reported to the provider developers.", }, }, + Functions: map[string]*tfprotov6.Function{}, + Provider: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{}, + }, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + ServerCapabilities: &tfprotov6.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "functions": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{ + FunctionsMethod: func(_ context.Context) []func() function.Function { + return []func() function.Function{ + func() function.Function { + return &testprovider.Function{ + DefinitionMethod: func(_ context.Context, _ function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Return: function.StringReturn{}, + } + }, + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "function1" + }, + } + }, + func() function.Function { + return &testprovider.Function{ + DefinitionMethod: func(_ context.Context, _ function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Return: function.StringReturn{}, + } + }, + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "function2" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.GetProviderSchemaRequest{}, + expectedResponse: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{ + "function1": { + Parameters: []*tfprotov6.FunctionParameter{}, + Return: &tfprotov6.FunctionReturn{ + Type: tftypes.String, + }, + }, + "function2": { + Parameters: []*tfprotov6.FunctionParameter{}, + Return: &tfprotov6.FunctionReturn{ + Type: tftypes.String, + }, + }, + }, + Provider: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{}, + }, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + ServerCapabilities: &tfprotov6.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "functions-duplicate-type-name": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{ + FunctionsMethod: func(_ context.Context) []func() function.Function { + return []func() function.Function{ + func() function.Function { + return &testprovider.Function{ + DefinitionMethod: func(_ context.Context, _ function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Return: function.StringReturn{}, + } + }, + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "testfunction" // intentionally duplicate + }, + } + }, + func() function.Function { + return &testprovider.Function{ + DefinitionMethod: func(_ context.Context, _ function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Return: function.StringReturn{}, + } + }, + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "testfunction" // intentionally duplicate + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.GetProviderSchemaRequest{}, + expectedResponse: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Duplicate Function Name Defined", + Detail: "The testfunction function name was returned for multiple functions. " + + "Function names must be unique. " + + "This is always an issue with the provider and should be reported to the provider developers.", + }, + }, + Functions: map[string]*tfprotov6.Function{}, + Provider: &tfprotov6.Schema{ + Block: &tfprotov6.SchemaBlock{}, + }, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + ServerCapabilities: &tfprotov6.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "functions-empty-name": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.ProviderWithFunctions{ + FunctionsMethod: func(_ context.Context) []func() function.Function { + return []func() function.Function{ + func() function.Function { + return &testprovider.Function{ + MetadataMethod: func(_ context.Context, _ function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "" // intentionally empty + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.GetProviderSchemaRequest{}, + expectedResponse: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Function Name Missing", + Detail: "The *testprovider.Function Function returned an empty string from the Metadata method. " + + "This is always an issue with the provider and should be reported to the provider developers.", + }, + }, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{}, }, @@ -235,6 +399,7 @@ func TestServerGetProviderSchema(t *testing.T) { request: &tfprotov6.GetProviderSchemaRequest{}, expectedResponse: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -273,6 +438,7 @@ func TestServerGetProviderSchema(t *testing.T) { request: &tfprotov6.GetProviderSchemaRequest{}, expectedResponse: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{}, }, @@ -340,6 +506,7 @@ func TestServerGetProviderSchema(t *testing.T) { request: &tfprotov6.GetProviderSchemaRequest{}, expectedResponse: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{}, }, @@ -428,6 +595,7 @@ func TestServerGetProviderSchema(t *testing.T) { "This is always an issue with the provider and should be reported to the provider developers.", }, }, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{}, }, @@ -467,6 +635,7 @@ func TestServerGetProviderSchema(t *testing.T) { "This is always an issue with the provider and should be reported to the provider developers.", }, }, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{}, }, @@ -600,6 +769,11 @@ func TestServerGetProviderSchema_logging(t *testing.T) { "@message": "Called provider defined Provider DataSources", "@module": "sdk.framework", }, + { + "@level": "trace", + "@message": "Checking FunctionTypes lock", + "@module": "sdk.framework", + }, } if diff := cmp.Diff(entries, expectedEntries); diff != "" { diff --git a/internal/testing/testprovider/function.go b/internal/testing/testprovider/function.go new file mode 100644 index 000000000..2a6d4a9aa --- /dev/null +++ b/internal/testing/testprovider/function.go @@ -0,0 +1,47 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/function" +) + +var _ function.Function = &Function{} + +// Declarative function.Function for unit testing. +type Function struct { + // Function interface methods + DefinitionMethod func(context.Context, function.DefinitionRequest, *function.DefinitionResponse) + MetadataMethod func(context.Context, function.MetadataRequest, *function.MetadataResponse) + RunMethod func(context.Context, function.RunRequest, *function.RunResponse) +} + +// Definition satisfies the function.Function interface. +func (d *Function) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + if d.DefinitionMethod == nil { + return + } + + d.DefinitionMethod(ctx, req, resp) +} + +// Metadata satisfies the function.Function interface. +func (d *Function) Metadata(ctx context.Context, req function.MetadataRequest, resp *function.MetadataResponse) { + if d.MetadataMethod == nil { + return + } + + d.MetadataMethod(ctx, req, resp) +} + +// Run satisfies the function.Function interface. +func (d *Function) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + if d.RunMethod == nil { + return + } + + d.RunMethod(ctx, req, resp) +} diff --git a/internal/testing/testprovider/providerwithfunctions.go b/internal/testing/testprovider/providerwithfunctions.go new file mode 100644 index 000000000..9bfd3d53c --- /dev/null +++ b/internal/testing/testprovider/providerwithfunctions.go @@ -0,0 +1,33 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/provider" +) + +var ( + _ provider.Provider = &ProviderWithFunctions{} + _ provider.ProviderWithFunctions = &ProviderWithFunctions{} +) + +// Declarative provider.ProviderWithFunctions for unit testing. +type ProviderWithFunctions struct { + *Provider + + // ProviderWithFunctions interface methods + FunctionsMethod func(context.Context) []func() function.Function +} + +// Functions satisfies the provider.ProviderWithFunctions interface. +func (p *ProviderWithFunctions) Functions(ctx context.Context) []func() function.Function { + if p.FunctionsMethod == nil { + return nil + } + + return p.FunctionsMethod(ctx) +} diff --git a/internal/testing/testtypes/list.go b/internal/testing/testtypes/list.go index bc081e97a..0f7ee28a3 100644 --- a/internal/testing/testtypes/list.go +++ b/internal/testing/testtypes/list.go @@ -3,7 +3,10 @@ package testtypes -import "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +import ( + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) var ( _ basetypes.ListTypable = ListType{} @@ -14,6 +17,26 @@ type ListType struct { basetypes.ListType } +func (t ListType) Equal(o attr.Type) bool { + other, ok := o.(ListType) + + if !ok { + return false + } + + return t.ListType.Equal(other.ListType) +} + type ListValue struct { basetypes.ListValue } + +func (v ListValue) Equal(o attr.Value) bool { + other, ok := o.(ListValue) + + if !ok { + return false + } + + return v.ListValue.Equal(other.ListValue) +} diff --git a/internal/testing/testtypes/map.go b/internal/testing/testtypes/map.go index 17e131dab..72a9ae85c 100644 --- a/internal/testing/testtypes/map.go +++ b/internal/testing/testtypes/map.go @@ -3,7 +3,10 @@ package testtypes -import "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +import ( + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) var ( _ basetypes.MapTypable = MapType{} @@ -14,6 +17,26 @@ type MapType struct { basetypes.MapType } +func (t MapType) Equal(o attr.Type) bool { + other, ok := o.(MapType) + + if !ok { + return false + } + + return t.MapType.Equal(other.MapType) +} + type MapValue struct { basetypes.MapValue } + +func (v MapValue) Equal(o attr.Value) bool { + other, ok := o.(MapValue) + + if !ok { + return false + } + + return v.MapValue.Equal(other.MapValue) +} diff --git a/internal/testing/testtypes/object.go b/internal/testing/testtypes/object.go index 03f2a7ce9..b52998ef6 100644 --- a/internal/testing/testtypes/object.go +++ b/internal/testing/testtypes/object.go @@ -3,7 +3,10 @@ package testtypes -import "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +import ( + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) var ( _ basetypes.ObjectTypable = ObjectType{} @@ -14,6 +17,26 @@ type ObjectType struct { basetypes.ObjectType } +func (t ObjectType) Equal(o attr.Type) bool { + other, ok := o.(ObjectType) + + if !ok { + return false + } + + return t.ObjectType.Equal(other.ObjectType) +} + type ObjectValue struct { basetypes.ObjectValue } + +func (v ObjectValue) Equal(o attr.Value) bool { + other, ok := o.(ObjectValue) + + if !ok { + return false + } + + return v.ObjectValue.Equal(other.ObjectValue) +} diff --git a/internal/testing/testtypes/set.go b/internal/testing/testtypes/set.go index 21d138cc7..ff795226a 100644 --- a/internal/testing/testtypes/set.go +++ b/internal/testing/testtypes/set.go @@ -3,7 +3,10 @@ package testtypes -import "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +import ( + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) var ( _ basetypes.SetTypable = SetType{} @@ -14,6 +17,26 @@ type SetType struct { basetypes.SetType } +func (t SetType) Equal(o attr.Type) bool { + other, ok := o.(SetType) + + if !ok { + return false + } + + return t.SetType.Equal(other.SetType) +} + type SetValue struct { basetypes.SetValue } + +func (v SetValue) Equal(o attr.Value) bool { + other, ok := o.(SetValue) + + if !ok { + return false + } + + return v.SetValue.Equal(other.SetValue) +} diff --git a/internal/toproto5/callfunction.go b/internal/toproto5/callfunction.go new file mode 100644 index 000000000..c1a6f89ea --- /dev/null +++ b/internal/toproto5/callfunction.go @@ -0,0 +1,30 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// CallFunctionResponse returns the *tfprotov5.CallFunctionResponse +// equivalent of a *fwserver.CallFunctionResponse. +func CallFunctionResponse(ctx context.Context, fw *fwserver.CallFunctionResponse) *tfprotov5.CallFunctionResponse { + if fw == nil { + return nil + } + + proto := &tfprotov5.CallFunctionResponse{ + Diagnostics: Diagnostics(ctx, fw.Diagnostics), + } + + result, diags := FunctionResultData(ctx, fw.Result) + + proto.Diagnostics = append(proto.Diagnostics, Diagnostics(ctx, diags)...) + proto.Result = result + + return proto +} diff --git a/internal/toproto5/callfunction_test.go b/internal/toproto5/callfunction_test.go new file mode 100644 index 000000000..9714f77d3 --- /dev/null +++ b/internal/toproto5/callfunction_test.go @@ -0,0 +1,76 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5_test + +import ( + "context" + "testing" + + "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/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) { + t.Parallel() + + testCases := map[string]struct { + input *fwserver.CallFunctionResponse + expected *tfprotov5.CallFunctionResponse + }{ + "nil": { + input: nil, + expected: nil, + }, + "diagnostics": { + input: &fwserver.CallFunctionResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("warning summary", "warning detail"), + diag.NewErrorDiagnostic("error summary", "error detail"), + }, + }, + expected: &tfprotov5.CallFunctionResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "warning summary", + Detail: "warning detail", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "error summary", + Detail: "error detail", + }, + }, + }, + }, + "result": { + input: &fwserver.CallFunctionResponse{ + Result: function.NewResultData(basetypes.NewBoolValue(true)), + }, + expected: &tfprotov5.CallFunctionResponse{ + Result: DynamicValueMust(tftypes.NewValue(tftypes.Bool, true)), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto5.CallFunctionResponse(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/toproto5/diagnostics.go b/internal/toproto5/diagnostics.go index d32952a7f..93b0f39f0 100644 --- a/internal/toproto5/diagnostics.go +++ b/internal/toproto5/diagnostics.go @@ -34,6 +34,11 @@ 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 ad25fddd3..fabf775ae 100644 --- a/internal/toproto5/diagnostics_test.go +++ b/internal/toproto5/diagnostics_test.go @@ -92,6 +92,26 @@ 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 new file mode 100644 index 000000000..e9f0c3934 --- /dev/null +++ b/internal/toproto5/function.go @@ -0,0 +1,128 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5 + +import ( + "context" + + "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-go/tfprotov5" +) + +// Function returns the *tfprotov5.Function for a function.Definition. +func Function(ctx context.Context, fw function.Definition) *tfprotov5.Function { + proto := &tfprotov5.Function{ + DeprecationMessage: fw.DeprecationMessage, + Parameters: make([]*tfprotov5.FunctionParameter, 0, len(fw.Parameters)), + Return: FunctionReturn(ctx, fw.Return), + Summary: fw.Summary, + VariadicParameter: FunctionParameter(ctx, fw.VariadicParameter), + } + + if fw.MarkdownDescription != "" { + proto.Description = fw.MarkdownDescription + proto.DescriptionKind = tfprotov5.StringKindMarkdown + } else if fw.Description != "" { + proto.Description = fw.Description + proto.DescriptionKind = tfprotov5.StringKindPlain + } + + for _, fwParameter := range fw.Parameters { + proto.Parameters = append(proto.Parameters, FunctionParameter(ctx, fwParameter)) + } + + return proto +} + +// FunctionParameter returns the *tfprotov5.FunctionParameter for a +// function.Parameter. +func FunctionParameter(ctx context.Context, fw function.Parameter) *tfprotov5.FunctionParameter { + if fw == nil { + return nil + } + + proto := &tfprotov5.FunctionParameter{ + AllowNullValue: fw.GetAllowNullValue(), + AllowUnknownValues: fw.GetAllowUnknownValues(), + Name: fw.GetName(), + Type: fw.GetType().TerraformType(ctx), + } + + if fw.GetMarkdownDescription() != "" { + proto.Description = fw.GetMarkdownDescription() + proto.DescriptionKind = tfprotov5.StringKindMarkdown + } else if fw.GetDescription() != "" { + proto.Description = fw.GetDescription() + proto.DescriptionKind = tfprotov5.StringKindPlain + } + + return proto +} + +// FunctionMetadata returns the tfprotov5.FunctionMetadata for a +// fwserver.FunctionMetadata. +func FunctionMetadata(ctx context.Context, fw fwserver.FunctionMetadata) tfprotov5.FunctionMetadata { + proto := tfprotov5.FunctionMetadata{ + Name: fw.Name, + } + + return proto +} + +// FunctionReturn returns the *tfprotov5.FunctionReturn for a +// function.Return. +func FunctionReturn(ctx context.Context, fw function.Return) *tfprotov5.FunctionReturn { + if fw == nil { + return nil + } + + proto := &tfprotov5.FunctionReturn{ + Type: fw.GetType().TerraformType(ctx), + } + + return proto +} + +// 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 + + attrValue := data.Value() + + if attrValue == nil { + return nil, nil + } + + tfType := attrValue.Type(ctx).TerraformType(ctx) + 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 + } + + 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 + } + + return &dynamicValue, nil +} diff --git a/internal/toproto5/function_test.go b/internal/toproto5/function_test.go new file mode 100644 index 000000000..7f6b9f414 --- /dev/null +++ b/internal/toproto5/function_test.go @@ -0,0 +1,429 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5_test + +import ( + "context" + "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/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) { + t.Parallel() + + testCases := map[string]struct { + fw function.Definition + expected *tfprotov5.Function + }{ + "deprecationmessage": { + fw: function.Definition{ + DeprecationMessage: "test deprecation message", + Return: function.StringReturn{}, + }, + expected: &tfprotov5.Function{ + DeprecationMessage: "test deprecation message", + Parameters: []*tfprotov5.FunctionParameter{}, + Return: &tfprotov5.FunctionReturn{ + Type: tftypes.String, + }, + }, + }, + "description": { + fw: function.Definition{ + Description: "test description", + Return: function.StringReturn{}, + }, + expected: &tfprotov5.Function{ + Description: "test description", + DescriptionKind: tfprotov5.StringKindPlain, + Parameters: []*tfprotov5.FunctionParameter{}, + Return: &tfprotov5.FunctionReturn{ + Type: tftypes.String, + }, + }, + }, + "description-markdown": { + fw: function.Definition{ + MarkdownDescription: "test description", + Return: function.StringReturn{}, + }, + expected: &tfprotov5.Function{ + Description: "test description", + DescriptionKind: tfprotov5.StringKindMarkdown, + Parameters: []*tfprotov5.FunctionParameter{}, + Return: &tfprotov5.FunctionReturn{ + Type: tftypes.String, + }, + }, + }, + "parameters": { + fw: function.Definition{ + Parameters: []function.Parameter{ + function.BoolParameter{}, + function.Int64Parameter{}, + function.StringParameter{}, + }, + Return: function.StringReturn{}, + }, + expected: &tfprotov5.Function{ + Parameters: []*tfprotov5.FunctionParameter{ + { + Name: function.DefaultParameterName, + Type: tftypes.Bool, + }, + { + Name: function.DefaultParameterName, + Type: tftypes.Number, + }, + { + Name: function.DefaultParameterName, + Type: tftypes.String, + }, + }, + Return: &tfprotov5.FunctionReturn{ + Type: tftypes.String, + }, + }, + }, + "result": { + fw: function.Definition{ + Return: function.StringReturn{}, + }, + expected: &tfprotov5.Function{ + Parameters: []*tfprotov5.FunctionParameter{}, + Return: &tfprotov5.FunctionReturn{ + Type: tftypes.String, + }, + }, + }, + "summary": { + fw: function.Definition{ + Return: function.StringReturn{}, + Summary: "test summary", + }, + expected: &tfprotov5.Function{ + Parameters: []*tfprotov5.FunctionParameter{}, + Return: &tfprotov5.FunctionReturn{ + Type: tftypes.String, + }, + Summary: "test summary", + }, + }, + "variadicparameter": { + fw: function.Definition{ + Return: function.StringReturn{}, + VariadicParameter: function.StringParameter{}, + }, + expected: &tfprotov5.Function{ + Parameters: []*tfprotov5.FunctionParameter{}, + Return: &tfprotov5.FunctionReturn{ + Type: tftypes.String, + }, + VariadicParameter: &tfprotov5.FunctionParameter{ + Name: function.DefaultParameterName, + Type: tftypes.String, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto5.Function(context.Background(), testCase.fw) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFunctionMetadata(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + fw fwserver.FunctionMetadata + expected tfprotov5.FunctionMetadata + }{ + "name": { + fw: fwserver.FunctionMetadata{ + Name: "test", + }, + expected: tfprotov5.FunctionMetadata{ + Name: "test", + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto5.FunctionMetadata(context.Background(), testCase.fw) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFunctionParameter(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + fw function.Parameter + expected *tfprotov5.FunctionParameter + }{ + "nil": { + fw: nil, + expected: nil, + }, + "allownullvalue": { + fw: function.BoolParameter{ + AllowNullValue: true, + }, + expected: &tfprotov5.FunctionParameter{ + AllowNullValue: true, + Name: function.DefaultParameterName, + Type: tftypes.Bool, + }, + }, + "allowunknownvalues": { + fw: function.BoolParameter{ + AllowUnknownValues: true, + }, + expected: &tfprotov5.FunctionParameter{ + AllowUnknownValues: true, + Name: function.DefaultParameterName, + Type: tftypes.Bool, + }, + }, + "description": { + fw: function.BoolParameter{ + Description: "test description", + }, + expected: &tfprotov5.FunctionParameter{ + Description: "test description", + DescriptionKind: tfprotov5.StringKindPlain, + Name: function.DefaultParameterName, + Type: tftypes.Bool, + }, + }, + "description-markdown": { + fw: function.BoolParameter{ + MarkdownDescription: "test description", + }, + expected: &tfprotov5.FunctionParameter{ + Description: "test description", + DescriptionKind: tfprotov5.StringKindMarkdown, + Name: function.DefaultParameterName, + Type: tftypes.Bool, + }, + }, + "name": { + fw: function.BoolParameter{ + Name: "test", + }, + expected: &tfprotov5.FunctionParameter{ + Name: "test", + Type: tftypes.Bool, + }, + }, + "type-bool": { + fw: function.BoolParameter{}, + expected: &tfprotov5.FunctionParameter{ + Name: function.DefaultParameterName, + Type: tftypes.Bool, + }, + }, + "type-float64": { + fw: function.Float64Parameter{}, + expected: &tfprotov5.FunctionParameter{ + Name: function.DefaultParameterName, + Type: tftypes.Number, + }, + }, + "type-int64": { + fw: function.Int64Parameter{}, + expected: &tfprotov5.FunctionParameter{ + Name: function.DefaultParameterName, + Type: tftypes.Number, + }, + }, + "type-list": { + fw: function.ListParameter{ + ElementType: basetypes.StringType{}, + }, + expected: &tfprotov5.FunctionParameter{ + Name: function.DefaultParameterName, + Type: tftypes.List{ + ElementType: tftypes.String, + }, + }, + }, + "type-map": { + fw: function.MapParameter{ + ElementType: basetypes.StringType{}, + }, + expected: &tfprotov5.FunctionParameter{ + Name: function.DefaultParameterName, + Type: tftypes.Map{ + ElementType: tftypes.String, + }, + }, + }, + "type-number": { + fw: function.NumberParameter{}, + expected: &tfprotov5.FunctionParameter{ + Name: function.DefaultParameterName, + Type: tftypes.Number, + }, + }, + "type-object": { + fw: function.ObjectParameter{ + AttributeTypes: map[string]attr.Type{ + "bool": basetypes.BoolType{}, + "int64": basetypes.Int64Type{}, + "string": basetypes.StringType{}, + }, + }, + expected: &tfprotov5.FunctionParameter{ + Name: function.DefaultParameterName, + Type: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "bool": tftypes.Bool, + "int64": tftypes.Number, + "string": tftypes.String, + }, + }, + }, + }, + "type-set": { + fw: function.SetParameter{ + ElementType: basetypes.StringType{}, + }, + expected: &tfprotov5.FunctionParameter{ + Name: function.DefaultParameterName, + Type: tftypes.Set{ + ElementType: tftypes.String, + }, + }, + }, + "type-string": { + fw: function.StringParameter{}, + expected: &tfprotov5.FunctionParameter{ + Name: function.DefaultParameterName, + Type: tftypes.String, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto5.FunctionParameter(context.Background(), testCase.fw) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFunctionReturn(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + fw function.Return + expected *tfprotov5.FunctionReturn + }{ + "nil": { + fw: nil, + expected: nil, + }, + "type-string": { + fw: function.StringReturn{}, + expected: &tfprotov5.FunctionReturn{ + Type: tftypes.String, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto5.FunctionReturn(context.Background(), testCase.fw) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFunctionResultData(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + fw function.ResultData + expected *tfprotov5.DynamicValue + expectedDiagnostics diag.Diagnostics + }{ + "empty": { + fw: function.ResultData{}, + expected: nil, + expectedDiagnostics: nil, + }, + "value-nil": { + fw: function.NewResultData(nil), + expected: nil, + expectedDiagnostics: nil, + }, + "value": { + fw: function.NewResultData(basetypes.NewBoolValue(true)), + expected: DynamicValueMust(tftypes.NewValue(tftypes.Bool, true)), + expectedDiagnostics: nil, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := toproto5.FunctionResultData(context.Background(), testCase.fw) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiagnostics); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} diff --git a/internal/toproto5/getfunctions.go b/internal/toproto5/getfunctions.go new file mode 100644 index 000000000..1fa61a3f0 --- /dev/null +++ b/internal/toproto5/getfunctions.go @@ -0,0 +1,30 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// GetFunctionsResponse returns the *tfprotov5.GetFunctionsResponse +// equivalent of a *fwserver.GetFunctionsResponse. +func GetFunctionsResponse(ctx context.Context, fw *fwserver.GetFunctionsResponse) *tfprotov5.GetFunctionsResponse { + if fw == nil { + return nil + } + + proto := &tfprotov5.GetFunctionsResponse{ + Diagnostics: Diagnostics(ctx, fw.Diagnostics), + Functions: make(map[string]*tfprotov5.Function, len(fw.FunctionDefinitions)), + } + + for name, functionDefinition := range fw.FunctionDefinitions { + proto.Functions[name] = Function(ctx, functionDefinition) + } + + return proto +} diff --git a/internal/toproto5/getfunctions_test.go b/internal/toproto5/getfunctions_test.go new file mode 100644 index 000000000..524566d9e --- /dev/null +++ b/internal/toproto5/getfunctions_test.go @@ -0,0 +1,239 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5_test + +import ( + "context" + "testing" + + "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/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestGetFunctionsResponse(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input *fwserver.GetFunctionsResponse + expected *tfprotov5.GetFunctionsResponse + }{ + "nil": { + input: nil, + expected: nil, + }, + "diagnostics": { + input: &fwserver.GetFunctionsResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("warning summary", "warning detail"), + diag.NewErrorDiagnostic("error summary", "error detail"), + }, + }, + expected: &tfprotov5.GetFunctionsResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "warning summary", + Detail: "warning detail", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "error summary", + Detail: "error detail", + }, + }, + Functions: map[string]*tfprotov5.Function{}, + }, + }, + "functions": { + input: &fwserver.GetFunctionsResponse{ + FunctionDefinitions: map[string]function.Definition{ + "testfunction1": { + Return: function.StringReturn{}, + }, + "testfunction2": { + Return: function.StringReturn{}, + }, + }, + }, + expected: &tfprotov5.GetFunctionsResponse{ + Functions: map[string]*tfprotov5.Function{ + "testfunction1": { + Parameters: []*tfprotov5.FunctionParameter{}, + Return: &tfprotov5.FunctionReturn{ + Type: tftypes.String, + }, + }, + "testfunction2": { + Parameters: []*tfprotov5.FunctionParameter{}, + Return: &tfprotov5.FunctionReturn{ + Type: tftypes.String, + }, + }, + }, + }, + }, + "functions-deprecationmessage": { + input: &fwserver.GetFunctionsResponse{ + FunctionDefinitions: map[string]function.Definition{ + "testfunction": { + DeprecationMessage: "test deprecation message", + Return: function.StringReturn{}, + }, + }, + }, + expected: &tfprotov5.GetFunctionsResponse{ + Functions: map[string]*tfprotov5.Function{ + "testfunction": { + DeprecationMessage: "test deprecation message", + Parameters: []*tfprotov5.FunctionParameter{}, + Return: &tfprotov5.FunctionReturn{ + Type: tftypes.String, + }, + }, + }, + }, + }, + "functions-description": { + input: &fwserver.GetFunctionsResponse{ + FunctionDefinitions: map[string]function.Definition{ + "testfunction": { + Description: "test description", + Return: function.StringReturn{}, + }, + }, + }, + expected: &tfprotov5.GetFunctionsResponse{ + Functions: map[string]*tfprotov5.Function{ + "testfunction": { + Description: "test description", + Parameters: []*tfprotov5.FunctionParameter{}, + Return: &tfprotov5.FunctionReturn{ + Type: tftypes.String, + }, + }, + }, + }, + }, + "functions-parameters": { + input: &fwserver.GetFunctionsResponse{ + FunctionDefinitions: map[string]function.Definition{ + "testfunction": { + Parameters: []function.Parameter{ + function.BoolParameter{}, + function.Int64Parameter{}, + function.StringParameter{}, + }, + Return: function.StringReturn{}, + }, + }, + }, + expected: &tfprotov5.GetFunctionsResponse{ + Functions: map[string]*tfprotov5.Function{ + "testfunction": { + Parameters: []*tfprotov5.FunctionParameter{ + { + Name: function.DefaultParameterName, + Type: tftypes.Bool, + }, + { + Name: function.DefaultParameterName, + Type: tftypes.Number, + }, + { + Name: function.DefaultParameterName, + Type: tftypes.String, + }, + }, + Return: &tfprotov5.FunctionReturn{ + Type: tftypes.String, + }, + }, + }, + }, + }, + "functions-result": { + input: &fwserver.GetFunctionsResponse{ + FunctionDefinitions: map[string]function.Definition{ + "testfunction": { + Return: function.StringReturn{}, + }, + }, + }, + expected: &tfprotov5.GetFunctionsResponse{ + Functions: map[string]*tfprotov5.Function{ + "testfunction": { + Parameters: []*tfprotov5.FunctionParameter{}, + Return: &tfprotov5.FunctionReturn{ + Type: tftypes.String, + }, + }, + }, + }, + }, + "functions-summary": { + input: &fwserver.GetFunctionsResponse{ + FunctionDefinitions: map[string]function.Definition{ + "testfunction": { + Return: function.StringReturn{}, + Summary: "test summary", + }, + }, + }, + expected: &tfprotov5.GetFunctionsResponse{ + Functions: map[string]*tfprotov5.Function{ + "testfunction": { + Parameters: []*tfprotov5.FunctionParameter{}, + Return: &tfprotov5.FunctionReturn{ + Type: tftypes.String, + }, + Summary: "test summary", + }, + }, + }, + }, + "functions-variadicparameter": { + input: &fwserver.GetFunctionsResponse{ + FunctionDefinitions: map[string]function.Definition{ + "testfunction": { + Return: function.StringReturn{}, + VariadicParameter: function.StringParameter{}, + }, + }, + }, + expected: &tfprotov5.GetFunctionsResponse{ + Functions: map[string]*tfprotov5.Function{ + "testfunction": { + Parameters: []*tfprotov5.FunctionParameter{}, + Return: &tfprotov5.FunctionReturn{ + Type: tftypes.String, + }, + VariadicParameter: &tfprotov5.FunctionParameter{ + Name: function.DefaultParameterName, + Type: tftypes.String, + }, + }, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto5.GetFunctionsResponse(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/toproto5/getmetadata.go b/internal/toproto5/getmetadata.go index 2162635c8..9c1892d8a 100644 --- a/internal/toproto5/getmetadata.go +++ b/internal/toproto5/getmetadata.go @@ -18,9 +18,10 @@ func GetMetadataResponse(ctx context.Context, fw *fwserver.GetMetadataResponse) } protov6 := &tfprotov5.GetMetadataResponse{ - DataSources: []tfprotov5.DataSourceMetadata{}, + DataSources: make([]tfprotov5.DataSourceMetadata, 0, len(fw.DataSources)), Diagnostics: Diagnostics(ctx, fw.Diagnostics), - Resources: []tfprotov5.ResourceMetadata{}, + Functions: make([]tfprotov5.FunctionMetadata, 0, len(fw.Functions)), + Resources: make([]tfprotov5.ResourceMetadata, 0, len(fw.Resources)), ServerCapabilities: ServerCapabilities(ctx, fw.ServerCapabilities), } @@ -28,6 +29,10 @@ func GetMetadataResponse(ctx context.Context, fw *fwserver.GetMetadataResponse) protov6.DataSources = append(protov6.DataSources, DataSourceMetadata(ctx, datasource)) } + for _, function := range fw.Functions { + protov6.Functions = append(protov6.Functions, FunctionMetadata(ctx, function)) + } + for _, resource := range fw.Resources { protov6.Resources = append(protov6.Resources, ResourceMetadata(ctx, resource)) } diff --git a/internal/toproto5/getmetadata_test.go b/internal/toproto5/getmetadata_test.go index d363d260e..07001c939 100644 --- a/internal/toproto5/getmetadata_test.go +++ b/internal/toproto5/getmetadata_test.go @@ -45,6 +45,7 @@ func TestGetMetadataResponse(t *testing.T) { TypeName: "test_data_source_2", }, }, + Functions: []tfprotov5.FunctionMetadata{}, Resources: []tfprotov5.ResourceMetadata{}, }, }, @@ -70,6 +71,31 @@ func TestGetMetadataResponse(t *testing.T) { "This is always an issue with the provider and should be reported to the provider developers.", }, }, + Functions: []tfprotov5.FunctionMetadata{}, + Resources: []tfprotov5.ResourceMetadata{}, + }, + }, + "functions": { + input: &fwserver.GetMetadataResponse{ + Functions: []fwserver.FunctionMetadata{ + { + Name: "function1", + }, + { + Name: "function2", + }, + }, + }, + expected: &tfprotov5.GetMetadataResponse{ + DataSources: []tfprotov5.DataSourceMetadata{}, + Functions: []tfprotov5.FunctionMetadata{ + { + Name: "function1", + }, + { + Name: "function2", + }, + }, Resources: []tfprotov5.ResourceMetadata{}, }, }, @@ -86,6 +112,7 @@ func TestGetMetadataResponse(t *testing.T) { }, expected: &tfprotov5.GetMetadataResponse{ DataSources: []tfprotov5.DataSourceMetadata{}, + Functions: []tfprotov5.FunctionMetadata{}, Resources: []tfprotov5.ResourceMetadata{ { TypeName: "test_resource_1", @@ -105,6 +132,7 @@ func TestGetMetadataResponse(t *testing.T) { }, expected: &tfprotov5.GetMetadataResponse{ DataSources: []tfprotov5.DataSourceMetadata{}, + Functions: []tfprotov5.FunctionMetadata{}, Resources: []tfprotov5.ResourceMetadata{}, ServerCapabilities: &tfprotov5.ServerCapabilities{ GetProviderSchemaOptional: true, diff --git a/internal/toproto5/getproviderschema.go b/internal/toproto5/getproviderschema.go index 6a5998273..1fec486ae 100644 --- a/internal/toproto5/getproviderschema.go +++ b/internal/toproto5/getproviderschema.go @@ -18,9 +18,10 @@ func GetProviderSchemaResponse(ctx context.Context, fw *fwserver.GetProviderSche } protov5 := &tfprotov5.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov5.Schema{}, + DataSourceSchemas: make(map[string]*tfprotov5.Schema, len(fw.DataSourceSchemas)), Diagnostics: Diagnostics(ctx, fw.Diagnostics), - ResourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: make(map[string]*tfprotov5.Function, len(fw.FunctionDefinitions)), + ResourceSchemas: make(map[string]*tfprotov5.Schema, len(fw.ResourceSchemas)), ServerCapabilities: ServerCapabilities(ctx, fw.ServerCapabilities), } @@ -64,6 +65,10 @@ func GetProviderSchemaResponse(ctx context.Context, fw *fwserver.GetProviderSche } } + for name, functionDefinition := range fw.FunctionDefinitions { + protov5.Functions[name] = Function(ctx, functionDefinition) + } + for resourceType, resourceSchema := range fw.ResourceSchemas { protov5.ResourceSchemas[resourceType], err = Schema(ctx, resourceSchema) diff --git a/internal/toproto5/getproviderschema_test.go b/internal/toproto5/getproviderschema_test.go index fc15e9082..ddce83357 100644 --- a/internal/toproto5/getproviderschema_test.go +++ b/internal/toproto5/getproviderschema_test.go @@ -10,6 +10,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/attr" datasourceschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" @@ -80,6 +81,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, @@ -109,6 +111,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, @@ -140,6 +143,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, @@ -169,6 +173,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, @@ -200,6 +205,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, @@ -229,6 +235,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, @@ -260,6 +267,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, @@ -289,6 +297,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, @@ -318,6 +327,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, @@ -347,6 +357,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, @@ -383,6 +394,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, @@ -416,6 +428,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { Detail: "The schema for the data source \"test_data_source\" couldn't be converted into a usable type. This is always a problem with the provider. Please report the following to the provider developer:\n\nAttributeName(\"test_attribute\"): protocol version 5 cannot have Attributes set", }, }, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, @@ -456,6 +469,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, @@ -488,6 +502,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, @@ -521,6 +536,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { Detail: "The schema for the data source \"test_data_source\" couldn't be converted into a usable type. This is always a problem with the provider. Please report the following to the provider developer:\n\nAttributeName(\"test_attribute\"): protocol version 5 cannot have Attributes set", }, }, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, @@ -553,6 +569,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, @@ -582,6 +599,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, @@ -618,6 +636,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, @@ -651,6 +670,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { Detail: "The schema for the data source \"test_data_source\" couldn't be converted into a usable type. This is always a problem with the provider. Please report the following to the provider developer:\n\nAttributeName(\"test_attribute\"): protocol version 5 cannot have Attributes set", }, }, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, @@ -691,6 +711,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, @@ -727,6 +748,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, @@ -759,6 +781,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, @@ -790,6 +813,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { Detail: "The schema for the data source \"test_data_source\" couldn't be converted into a usable type. This is always a problem with the provider. Please report the following to the provider developer:\n\nAttributeName(\"test_attribute\"): protocol version 5 cannot have Attributes set", }, }, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, @@ -819,6 +843,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, @@ -862,6 +887,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, @@ -905,6 +931,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, @@ -946,6 +973,192 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov5.Function{}, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "functions": { + input: &fwserver.GetProviderSchemaResponse{ + FunctionDefinitions: map[string]function.Definition{ + "testfunction1": { + Return: function.StringReturn{}, + }, + "testfunction2": { + Return: function.StringReturn{}, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{ + "testfunction1": { + Parameters: []*tfprotov5.FunctionParameter{}, + Return: &tfprotov5.FunctionReturn{ + Type: tftypes.String, + }, + }, + "testfunction2": { + Parameters: []*tfprotov5.FunctionParameter{}, + Return: &tfprotov5.FunctionReturn{ + Type: tftypes.String, + }, + }, + }, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "functions-deprecationmessage": { + input: &fwserver.GetProviderSchemaResponse{ + FunctionDefinitions: map[string]function.Definition{ + "testfunction": { + DeprecationMessage: "test deprecation message", + Return: function.StringReturn{}, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{ + "testfunction": { + DeprecationMessage: "test deprecation message", + Parameters: []*tfprotov5.FunctionParameter{}, + Return: &tfprotov5.FunctionReturn{ + Type: tftypes.String, + }, + }, + }, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "functions-description": { + input: &fwserver.GetProviderSchemaResponse{ + FunctionDefinitions: map[string]function.Definition{ + "testfunction": { + Description: "test description", + Return: function.StringReturn{}, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{ + "testfunction": { + Description: "test description", + Parameters: []*tfprotov5.FunctionParameter{}, + Return: &tfprotov5.FunctionReturn{ + Type: tftypes.String, + }, + }, + }, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "functions-parameters": { + input: &fwserver.GetProviderSchemaResponse{ + FunctionDefinitions: map[string]function.Definition{ + "testfunction": { + Parameters: []function.Parameter{ + function.BoolParameter{}, + function.Int64Parameter{}, + function.StringParameter{}, + }, + Return: function.StringReturn{}, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{ + "testfunction": { + Parameters: []*tfprotov5.FunctionParameter{ + { + Name: function.DefaultParameterName, + Type: tftypes.Bool, + }, + { + Name: function.DefaultParameterName, + Type: tftypes.Number, + }, + { + Name: function.DefaultParameterName, + Type: tftypes.String, + }, + }, + Return: &tfprotov5.FunctionReturn{ + Type: tftypes.String, + }, + }, + }, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "functions-result": { + input: &fwserver.GetProviderSchemaResponse{ + FunctionDefinitions: map[string]function.Definition{ + "testfunction": { + Return: function.StringReturn{}, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{ + "testfunction": { + Parameters: []*tfprotov5.FunctionParameter{}, + Return: &tfprotov5.FunctionReturn{ + Type: tftypes.String, + }, + }, + }, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "functions-summary": { + input: &fwserver.GetProviderSchemaResponse{ + FunctionDefinitions: map[string]function.Definition{ + "testfunction": { + Return: function.StringReturn{}, + Summary: "test summary", + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{ + "testfunction": { + Parameters: []*tfprotov5.FunctionParameter{}, + Return: &tfprotov5.FunctionReturn{ + Type: tftypes.String, + }, + Summary: "test summary", + }, + }, + ResourceSchemas: map[string]*tfprotov5.Schema{}, + }, + }, + "functions-variadicparameter": { + input: &fwserver.GetProviderSchemaResponse{ + FunctionDefinitions: map[string]function.Definition{ + "testfunction": { + Return: function.StringReturn{}, + VariadicParameter: function.StringParameter{}, + }, + }, + }, + expected: &tfprotov5.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{ + "testfunction": { + Parameters: []*tfprotov5.FunctionParameter{}, + Return: &tfprotov5.FunctionReturn{ + Type: tftypes.String, + }, + VariadicParameter: &tfprotov5.FunctionParameter{ + Name: function.DefaultParameterName, + Type: tftypes.String, + }, + }, + }, ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, @@ -962,6 +1175,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -989,6 +1203,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1015,6 +1230,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1042,6 +1258,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1069,6 +1286,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1095,6 +1313,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1121,6 +1340,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1150,6 +1370,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1194,6 +1415,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { Detail: "The provider schema couldn't be converted into a usable type. This is always a problem with the provider. Please report the following to the provider developer:\n\nAttributeName(\"test_attribute\"): protocol version 5 cannot have Attributes set", }, }, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, @@ -1214,6 +1436,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1247,6 +1470,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1289,6 +1513,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { Detail: "The provider schema couldn't be converted into a usable type. This is always a problem with the provider. Please report the following to the provider developer:\n\nAttributeName(\"test_attribute\"): protocol version 5 cannot have Attributes set", }, }, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, @@ -1305,6 +1530,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1333,6 +1559,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1362,6 +1589,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1406,6 +1634,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { Detail: "The provider schema couldn't be converted into a usable type. This is always a problem with the provider. Please report the following to the provider developer:\n\nAttributeName(\"test_attribute\"): protocol version 5 cannot have Attributes set", }, }, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, @@ -1426,6 +1655,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1461,6 +1691,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1492,6 +1723,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1532,6 +1764,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { Detail: "The provider schema couldn't be converted into a usable type. This is always a problem with the provider. Please report the following to the provider developer:\n\nAttributeName(\"test_attribute\"): protocol version 5 cannot have Attributes set", }, }, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, @@ -1547,6 +1780,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1579,6 +1813,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ BlockTypes: []*tfprotov5.SchemaNestedBlock{ @@ -1619,6 +1854,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ BlockTypes: []*tfprotov5.SchemaNestedBlock{ @@ -1657,6 +1893,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, Provider: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ BlockTypes: []*tfprotov5.SchemaNestedBlock{ @@ -1691,6 +1928,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ProviderMeta: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1717,6 +1955,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ProviderMeta: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1743,6 +1982,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ProviderMeta: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1769,6 +2009,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ProviderMeta: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1795,6 +2036,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ProviderMeta: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1824,6 +2066,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ProviderMeta: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1868,6 +2111,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { Detail: "The provider_meta schema couldn't be converted into a usable type. This is always a problem with the provider. Please report the following to the provider developer:\n\nAttributeName(\"test_attribute\"): protocol version 5 cannot have Attributes set", }, }, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, @@ -1905,6 +2149,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, @@ -1921,6 +2166,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ProviderMeta: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -1963,6 +2209,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { Detail: "The provider_meta schema couldn't be converted into a usable type. This is always a problem with the provider. Please report the following to the provider developer:\n\nAttributeName(\"test_attribute\"): protocol version 5 cannot have Attributes set", }, }, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, @@ -1979,6 +2226,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ProviderMeta: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -2007,6 +2255,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ProviderMeta: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -2036,6 +2285,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ProviderMeta: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -2080,6 +2330,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { Detail: "The provider_meta schema couldn't be converted into a usable type. This is always a problem with the provider. Please report the following to the provider developer:\n\nAttributeName(\"test_attribute\"): protocol version 5 cannot have Attributes set", }, }, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, @@ -2100,6 +2351,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ProviderMeta: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -2135,6 +2387,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ProviderMeta: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -2166,6 +2419,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ProviderMeta: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -2206,6 +2460,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { Detail: "The provider_meta schema couldn't be converted into a usable type. This is always a problem with the provider. Please report the following to the provider developer:\n\nAttributeName(\"test_attribute\"): protocol version 5 cannot have Attributes set", }, }, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{}, }, }, @@ -2221,6 +2476,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ProviderMeta: &tfprotov5.Schema{ Block: &tfprotov5.SchemaBlock{ Attributes: []*tfprotov5.SchemaAttribute{ @@ -2256,6 +2512,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource_1": { Block: &tfprotov5.SchemaBlock{ @@ -2296,6 +2553,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -2326,6 +2584,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -2356,6 +2615,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -2386,6 +2646,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -2416,6 +2677,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -2446,6 +2708,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -2476,6 +2739,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -2505,6 +2769,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -2534,6 +2799,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -2566,6 +2832,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -2613,6 +2880,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { Detail: "The schema for the resource \"test_resource\" couldn't be converted into a usable type. This is always a problem with the provider. Please report the following to the provider developer:\n\nAttributeName(\"test_attribute\"): protocol version 5 cannot have Attributes set", }, }, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": nil, }, @@ -2637,6 +2905,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -2673,6 +2942,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -2718,6 +2988,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { Detail: "The schema for the resource \"test_resource\" couldn't be converted into a usable type. This is always a problem with the provider. Please report the following to the provider developer:\n\nAttributeName(\"test_attribute\"): protocol version 5 cannot have Attributes set", }, }, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": nil, }, @@ -2738,6 +3009,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -2769,6 +3041,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -2801,6 +3074,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -2848,6 +3122,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { Detail: "The schema for the resource \"test_resource\" couldn't be converted into a usable type. This is always a problem with the provider. Please report the following to the provider developer:\n\nAttributeName(\"test_attribute\"): protocol version 5 cannot have Attributes set", }, }, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": nil, }, @@ -2872,6 +3147,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -2910,6 +3186,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -2944,6 +3221,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -2987,6 +3265,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { Detail: "The schema for the resource \"test_resource\" couldn't be converted into a usable type. This is always a problem with the provider. Please report the following to the provider developer:\n\nAttributeName(\"test_attribute\"): protocol version 5 cannot have Attributes set", }, }, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": nil, }, @@ -3006,6 +3285,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -3041,6 +3321,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -3084,6 +3365,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -3125,6 +3407,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{ @@ -3158,6 +3441,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov5.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov5.Schema{}, + Functions: map[string]*tfprotov5.Function{}, ResourceSchemas: map[string]*tfprotov5.Schema{ "test_resource": { Block: &tfprotov5.SchemaBlock{}, diff --git a/internal/toproto5/pointer_test.go b/internal/toproto5/pointer_test.go new file mode 100644 index 000000000..84cfbd6a1 --- /dev/null +++ b/internal/toproto5/pointer_test.go @@ -0,0 +1,8 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5_test + +func pointer[T any](value T) *T { + return &value +} diff --git a/internal/toproto6/callfunction.go b/internal/toproto6/callfunction.go new file mode 100644 index 000000000..0eba3ee8d --- /dev/null +++ b/internal/toproto6/callfunction.go @@ -0,0 +1,30 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// CallFunctionResponse returns the *tfprotov6.CallFunctionResponse +// equivalent of a *fwserver.CallFunctionResponse. +func CallFunctionResponse(ctx context.Context, fw *fwserver.CallFunctionResponse) *tfprotov6.CallFunctionResponse { + if fw == nil { + return nil + } + + proto := &tfprotov6.CallFunctionResponse{ + Diagnostics: Diagnostics(ctx, fw.Diagnostics), + } + + result, diags := FunctionResultData(ctx, fw.Result) + + proto.Diagnostics = append(proto.Diagnostics, Diagnostics(ctx, diags)...) + proto.Result = result + + return proto +} diff --git a/internal/toproto6/callfunction_test.go b/internal/toproto6/callfunction_test.go new file mode 100644 index 000000000..34078fa41 --- /dev/null +++ b/internal/toproto6/callfunction_test.go @@ -0,0 +1,76 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6_test + +import ( + "context" + "testing" + + "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/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) { + t.Parallel() + + testCases := map[string]struct { + input *fwserver.CallFunctionResponse + expected *tfprotov6.CallFunctionResponse + }{ + "nil": { + input: nil, + expected: nil, + }, + "diagnostics": { + input: &fwserver.CallFunctionResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("warning summary", "warning detail"), + diag.NewErrorDiagnostic("error summary", "error detail"), + }, + }, + expected: &tfprotov6.CallFunctionResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "warning summary", + Detail: "warning detail", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "error summary", + Detail: "error detail", + }, + }, + }, + }, + "result": { + input: &fwserver.CallFunctionResponse{ + Result: function.NewResultData(basetypes.NewBoolValue(true)), + }, + expected: &tfprotov6.CallFunctionResponse{ + Result: DynamicValueMust(tftypes.NewValue(tftypes.Bool, true)), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto6.CallFunctionResponse(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/toproto6/diagnostics.go b/internal/toproto6/diagnostics.go index f66e1b569..ee258e144 100644 --- a/internal/toproto6/diagnostics.go +++ b/internal/toproto6/diagnostics.go @@ -34,6 +34,11 @@ 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 8b35b83bc..08f9c15f8 100644 --- a/internal/toproto6/diagnostics_test.go +++ b/internal/toproto6/diagnostics_test.go @@ -92,6 +92,26 @@ 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 new file mode 100644 index 000000000..243f8fd2f --- /dev/null +++ b/internal/toproto6/function.go @@ -0,0 +1,128 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6 + +import ( + "context" + + "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-go/tfprotov6" +) + +// Function returns the *tfprotov6.Function for a function.Definition. +func Function(ctx context.Context, fw function.Definition) *tfprotov6.Function { + proto := &tfprotov6.Function{ + DeprecationMessage: fw.DeprecationMessage, + Parameters: make([]*tfprotov6.FunctionParameter, 0, len(fw.Parameters)), + Return: FunctionReturn(ctx, fw.Return), + Summary: fw.Summary, + VariadicParameter: FunctionParameter(ctx, fw.VariadicParameter), + } + + if fw.MarkdownDescription != "" { + proto.Description = fw.MarkdownDescription + proto.DescriptionKind = tfprotov6.StringKindMarkdown + } else if fw.Description != "" { + proto.Description = fw.Description + proto.DescriptionKind = tfprotov6.StringKindPlain + } + + for _, fwParameter := range fw.Parameters { + proto.Parameters = append(proto.Parameters, FunctionParameter(ctx, fwParameter)) + } + + return proto +} + +// FunctionParameter returns the *tfprotov6.FunctionParameter for a +// function.Parameter. +func FunctionParameter(ctx context.Context, fw function.Parameter) *tfprotov6.FunctionParameter { + if fw == nil { + return nil + } + + proto := &tfprotov6.FunctionParameter{ + AllowNullValue: fw.GetAllowNullValue(), + AllowUnknownValues: fw.GetAllowUnknownValues(), + Name: fw.GetName(), + Type: fw.GetType().TerraformType(ctx), + } + + if fw.GetMarkdownDescription() != "" { + proto.Description = fw.GetMarkdownDescription() + proto.DescriptionKind = tfprotov6.StringKindMarkdown + } else if fw.GetDescription() != "" { + proto.Description = fw.GetDescription() + proto.DescriptionKind = tfprotov6.StringKindPlain + } + + return proto +} + +// FunctionMetadata returns the tfprotov6.FunctionMetadata for a +// fwserver.FunctionMetadata. +func FunctionMetadata(ctx context.Context, fw fwserver.FunctionMetadata) tfprotov6.FunctionMetadata { + proto := tfprotov6.FunctionMetadata{ + Name: fw.Name, + } + + return proto +} + +// FunctionReturn returns the *tfprotov6.FunctionReturn for a +// function.Return. +func FunctionReturn(ctx context.Context, fw function.Return) *tfprotov6.FunctionReturn { + if fw == nil { + return nil + } + + proto := &tfprotov6.FunctionReturn{ + Type: fw.GetType().TerraformType(ctx), + } + + return proto +} + +// 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 + + attrValue := data.Value() + + if attrValue == nil { + return nil, nil + } + + tfType := attrValue.Type(ctx).TerraformType(ctx) + 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 + } + + 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 + } + + return &dynamicValue, nil +} diff --git a/internal/toproto6/function_test.go b/internal/toproto6/function_test.go new file mode 100644 index 000000000..82fe4bf0b --- /dev/null +++ b/internal/toproto6/function_test.go @@ -0,0 +1,429 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6_test + +import ( + "context" + "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/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) { + t.Parallel() + + testCases := map[string]struct { + fw function.Definition + expected *tfprotov6.Function + }{ + "deprecationmessage": { + fw: function.Definition{ + DeprecationMessage: "test deprecation message", + Return: function.StringReturn{}, + }, + expected: &tfprotov6.Function{ + DeprecationMessage: "test deprecation message", + Parameters: []*tfprotov6.FunctionParameter{}, + Return: &tfprotov6.FunctionReturn{ + Type: tftypes.String, + }, + }, + }, + "description": { + fw: function.Definition{ + Description: "test description", + Return: function.StringReturn{}, + }, + expected: &tfprotov6.Function{ + Description: "test description", + DescriptionKind: tfprotov6.StringKindPlain, + Parameters: []*tfprotov6.FunctionParameter{}, + Return: &tfprotov6.FunctionReturn{ + Type: tftypes.String, + }, + }, + }, + "description-markdown": { + fw: function.Definition{ + MarkdownDescription: "test description", + Return: function.StringReturn{}, + }, + expected: &tfprotov6.Function{ + Description: "test description", + DescriptionKind: tfprotov6.StringKindMarkdown, + Parameters: []*tfprotov6.FunctionParameter{}, + Return: &tfprotov6.FunctionReturn{ + Type: tftypes.String, + }, + }, + }, + "parameters": { + fw: function.Definition{ + Parameters: []function.Parameter{ + function.BoolParameter{}, + function.Int64Parameter{}, + function.StringParameter{}, + }, + Return: function.StringReturn{}, + }, + expected: &tfprotov6.Function{ + Parameters: []*tfprotov6.FunctionParameter{ + { + Name: function.DefaultParameterName, + Type: tftypes.Bool, + }, + { + Name: function.DefaultParameterName, + Type: tftypes.Number, + }, + { + Name: function.DefaultParameterName, + Type: tftypes.String, + }, + }, + Return: &tfprotov6.FunctionReturn{ + Type: tftypes.String, + }, + }, + }, + "result": { + fw: function.Definition{ + Return: function.StringReturn{}, + }, + expected: &tfprotov6.Function{ + Parameters: []*tfprotov6.FunctionParameter{}, + Return: &tfprotov6.FunctionReturn{ + Type: tftypes.String, + }, + }, + }, + "summary": { + fw: function.Definition{ + Return: function.StringReturn{}, + Summary: "test summary", + }, + expected: &tfprotov6.Function{ + Parameters: []*tfprotov6.FunctionParameter{}, + Return: &tfprotov6.FunctionReturn{ + Type: tftypes.String, + }, + Summary: "test summary", + }, + }, + "variadicparameter": { + fw: function.Definition{ + Return: function.StringReturn{}, + VariadicParameter: function.StringParameter{}, + }, + expected: &tfprotov6.Function{ + Parameters: []*tfprotov6.FunctionParameter{}, + Return: &tfprotov6.FunctionReturn{ + Type: tftypes.String, + }, + VariadicParameter: &tfprotov6.FunctionParameter{ + Name: function.DefaultParameterName, + Type: tftypes.String, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto6.Function(context.Background(), testCase.fw) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFunctionMetadata(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + fw fwserver.FunctionMetadata + expected tfprotov6.FunctionMetadata + }{ + "name": { + fw: fwserver.FunctionMetadata{ + Name: "test", + }, + expected: tfprotov6.FunctionMetadata{ + Name: "test", + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto6.FunctionMetadata(context.Background(), testCase.fw) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFunctionParameter(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + fw function.Parameter + expected *tfprotov6.FunctionParameter + }{ + "nil": { + fw: nil, + expected: nil, + }, + "allownullvalue": { + fw: function.BoolParameter{ + AllowNullValue: true, + }, + expected: &tfprotov6.FunctionParameter{ + AllowNullValue: true, + Name: function.DefaultParameterName, + Type: tftypes.Bool, + }, + }, + "allowunknownvalue": { + fw: function.BoolParameter{ + AllowUnknownValues: true, + }, + expected: &tfprotov6.FunctionParameter{ + AllowUnknownValues: true, + Name: function.DefaultParameterName, + Type: tftypes.Bool, + }, + }, + "description": { + fw: function.BoolParameter{ + Description: "test description", + }, + expected: &tfprotov6.FunctionParameter{ + Description: "test description", + DescriptionKind: tfprotov6.StringKindPlain, + Name: function.DefaultParameterName, + Type: tftypes.Bool, + }, + }, + "description-markdown": { + fw: function.BoolParameter{ + MarkdownDescription: "test description", + }, + expected: &tfprotov6.FunctionParameter{ + Description: "test description", + DescriptionKind: tfprotov6.StringKindMarkdown, + Name: function.DefaultParameterName, + Type: tftypes.Bool, + }, + }, + "name": { + fw: function.BoolParameter{ + Name: "test", + }, + expected: &tfprotov6.FunctionParameter{ + Name: "test", + Type: tftypes.Bool, + }, + }, + "type-bool": { + fw: function.BoolParameter{}, + expected: &tfprotov6.FunctionParameter{ + Name: function.DefaultParameterName, + Type: tftypes.Bool, + }, + }, + "type-float64": { + fw: function.Float64Parameter{}, + expected: &tfprotov6.FunctionParameter{ + Name: function.DefaultParameterName, + Type: tftypes.Number, + }, + }, + "type-int64": { + fw: function.Int64Parameter{}, + expected: &tfprotov6.FunctionParameter{ + Name: function.DefaultParameterName, + Type: tftypes.Number, + }, + }, + "type-list": { + fw: function.ListParameter{ + ElementType: basetypes.StringType{}, + }, + expected: &tfprotov6.FunctionParameter{ + Name: function.DefaultParameterName, + Type: tftypes.List{ + ElementType: tftypes.String, + }, + }, + }, + "type-map": { + fw: function.MapParameter{ + ElementType: basetypes.StringType{}, + }, + expected: &tfprotov6.FunctionParameter{ + Name: function.DefaultParameterName, + Type: tftypes.Map{ + ElementType: tftypes.String, + }, + }, + }, + "type-number": { + fw: function.NumberParameter{}, + expected: &tfprotov6.FunctionParameter{ + Name: function.DefaultParameterName, + Type: tftypes.Number, + }, + }, + "type-object": { + fw: function.ObjectParameter{ + AttributeTypes: map[string]attr.Type{ + "bool": basetypes.BoolType{}, + "int64": basetypes.Int64Type{}, + "string": basetypes.StringType{}, + }, + }, + expected: &tfprotov6.FunctionParameter{ + Name: function.DefaultParameterName, + Type: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "bool": tftypes.Bool, + "int64": tftypes.Number, + "string": tftypes.String, + }, + }, + }, + }, + "type-set": { + fw: function.SetParameter{ + ElementType: basetypes.StringType{}, + }, + expected: &tfprotov6.FunctionParameter{ + Name: function.DefaultParameterName, + Type: tftypes.Set{ + ElementType: tftypes.String, + }, + }, + }, + "type-string": { + fw: function.StringParameter{}, + expected: &tfprotov6.FunctionParameter{ + Name: function.DefaultParameterName, + Type: tftypes.String, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto6.FunctionParameter(context.Background(), testCase.fw) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFunctionReturn(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + fw function.Return + expected *tfprotov6.FunctionReturn + }{ + "nil": { + fw: nil, + expected: nil, + }, + "type-string": { + fw: function.StringReturn{}, + expected: &tfprotov6.FunctionReturn{ + Type: tftypes.String, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto6.FunctionReturn(context.Background(), testCase.fw) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFunctionResultData(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + fw function.ResultData + expected *tfprotov6.DynamicValue + expectedDiagnostics diag.Diagnostics + }{ + "empty": { + fw: function.ResultData{}, + expected: nil, + expectedDiagnostics: nil, + }, + "value-nil": { + fw: function.NewResultData(nil), + expected: nil, + expectedDiagnostics: nil, + }, + "value": { + fw: function.NewResultData(basetypes.NewBoolValue(true)), + expected: DynamicValueMust(tftypes.NewValue(tftypes.Bool, true)), + expectedDiagnostics: nil, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := toproto6.FunctionResultData(context.Background(), testCase.fw) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiagnostics); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} diff --git a/internal/toproto6/getfunctions.go b/internal/toproto6/getfunctions.go new file mode 100644 index 000000000..30cc7bde3 --- /dev/null +++ b/internal/toproto6/getfunctions.go @@ -0,0 +1,30 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// GetFunctionsResponse returns the *tfprotov6.GetFunctionsResponse +// equivalent of a *fwserver.GetFunctionsResponse. +func GetFunctionsResponse(ctx context.Context, fw *fwserver.GetFunctionsResponse) *tfprotov6.GetFunctionsResponse { + if fw == nil { + return nil + } + + proto := &tfprotov6.GetFunctionsResponse{ + Diagnostics: Diagnostics(ctx, fw.Diagnostics), + Functions: make(map[string]*tfprotov6.Function, len(fw.FunctionDefinitions)), + } + + for name, functionDefinition := range fw.FunctionDefinitions { + proto.Functions[name] = Function(ctx, functionDefinition) + } + + return proto +} diff --git a/internal/toproto6/getfunctions_test.go b/internal/toproto6/getfunctions_test.go new file mode 100644 index 000000000..789eab18c --- /dev/null +++ b/internal/toproto6/getfunctions_test.go @@ -0,0 +1,239 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6_test + +import ( + "context" + "testing" + + "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/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestGetFunctionsResponse(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input *fwserver.GetFunctionsResponse + expected *tfprotov6.GetFunctionsResponse + }{ + "nil": { + input: nil, + expected: nil, + }, + "diagnostics": { + input: &fwserver.GetFunctionsResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("warning summary", "warning detail"), + diag.NewErrorDiagnostic("error summary", "error detail"), + }, + }, + expected: &tfprotov6.GetFunctionsResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "warning summary", + Detail: "warning detail", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "error summary", + Detail: "error detail", + }, + }, + Functions: map[string]*tfprotov6.Function{}, + }, + }, + "functions": { + input: &fwserver.GetFunctionsResponse{ + FunctionDefinitions: map[string]function.Definition{ + "testfunction1": { + Return: function.StringReturn{}, + }, + "testfunction2": { + Return: function.StringReturn{}, + }, + }, + }, + expected: &tfprotov6.GetFunctionsResponse{ + Functions: map[string]*tfprotov6.Function{ + "testfunction1": { + Parameters: []*tfprotov6.FunctionParameter{}, + Return: &tfprotov6.FunctionReturn{ + Type: tftypes.String, + }, + }, + "testfunction2": { + Parameters: []*tfprotov6.FunctionParameter{}, + Return: &tfprotov6.FunctionReturn{ + Type: tftypes.String, + }, + }, + }, + }, + }, + "functions-deprecationmessage": { + input: &fwserver.GetFunctionsResponse{ + FunctionDefinitions: map[string]function.Definition{ + "testfunction": { + DeprecationMessage: "test deprecation message", + Return: function.StringReturn{}, + }, + }, + }, + expected: &tfprotov6.GetFunctionsResponse{ + Functions: map[string]*tfprotov6.Function{ + "testfunction": { + DeprecationMessage: "test deprecation message", + Parameters: []*tfprotov6.FunctionParameter{}, + Return: &tfprotov6.FunctionReturn{ + Type: tftypes.String, + }, + }, + }, + }, + }, + "functions-description": { + input: &fwserver.GetFunctionsResponse{ + FunctionDefinitions: map[string]function.Definition{ + "testfunction": { + Description: "test description", + Return: function.StringReturn{}, + }, + }, + }, + expected: &tfprotov6.GetFunctionsResponse{ + Functions: map[string]*tfprotov6.Function{ + "testfunction": { + Description: "test description", + Parameters: []*tfprotov6.FunctionParameter{}, + Return: &tfprotov6.FunctionReturn{ + Type: tftypes.String, + }, + }, + }, + }, + }, + "functions-parameters": { + input: &fwserver.GetFunctionsResponse{ + FunctionDefinitions: map[string]function.Definition{ + "testfunction": { + Parameters: []function.Parameter{ + function.BoolParameter{}, + function.Int64Parameter{}, + function.StringParameter{}, + }, + Return: function.StringReturn{}, + }, + }, + }, + expected: &tfprotov6.GetFunctionsResponse{ + Functions: map[string]*tfprotov6.Function{ + "testfunction": { + Parameters: []*tfprotov6.FunctionParameter{ + { + Name: function.DefaultParameterName, + Type: tftypes.Bool, + }, + { + Name: function.DefaultParameterName, + Type: tftypes.Number, + }, + { + Name: function.DefaultParameterName, + Type: tftypes.String, + }, + }, + Return: &tfprotov6.FunctionReturn{ + Type: tftypes.String, + }, + }, + }, + }, + }, + "functions-result": { + input: &fwserver.GetFunctionsResponse{ + FunctionDefinitions: map[string]function.Definition{ + "testfunction": { + Return: function.StringReturn{}, + }, + }, + }, + expected: &tfprotov6.GetFunctionsResponse{ + Functions: map[string]*tfprotov6.Function{ + "testfunction": { + Parameters: []*tfprotov6.FunctionParameter{}, + Return: &tfprotov6.FunctionReturn{ + Type: tftypes.String, + }, + }, + }, + }, + }, + "functions-summary": { + input: &fwserver.GetFunctionsResponse{ + FunctionDefinitions: map[string]function.Definition{ + "testfunction": { + Return: function.StringReturn{}, + Summary: "test summary", + }, + }, + }, + expected: &tfprotov6.GetFunctionsResponse{ + Functions: map[string]*tfprotov6.Function{ + "testfunction": { + Parameters: []*tfprotov6.FunctionParameter{}, + Return: &tfprotov6.FunctionReturn{ + Type: tftypes.String, + }, + Summary: "test summary", + }, + }, + }, + }, + "functions-variadicparameter": { + input: &fwserver.GetFunctionsResponse{ + FunctionDefinitions: map[string]function.Definition{ + "testfunction": { + Return: function.StringReturn{}, + VariadicParameter: function.StringParameter{}, + }, + }, + }, + expected: &tfprotov6.GetFunctionsResponse{ + Functions: map[string]*tfprotov6.Function{ + "testfunction": { + Parameters: []*tfprotov6.FunctionParameter{}, + Return: &tfprotov6.FunctionReturn{ + Type: tftypes.String, + }, + VariadicParameter: &tfprotov6.FunctionParameter{ + Name: function.DefaultParameterName, + Type: tftypes.String, + }, + }, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto6.GetFunctionsResponse(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/toproto6/getmetadata.go b/internal/toproto6/getmetadata.go index 87e1af5d3..0924f3c9f 100644 --- a/internal/toproto6/getmetadata.go +++ b/internal/toproto6/getmetadata.go @@ -18,9 +18,10 @@ func GetMetadataResponse(ctx context.Context, fw *fwserver.GetMetadataResponse) } protov6 := &tfprotov6.GetMetadataResponse{ - DataSources: []tfprotov6.DataSourceMetadata{}, + DataSources: make([]tfprotov6.DataSourceMetadata, 0, len(fw.DataSources)), Diagnostics: Diagnostics(ctx, fw.Diagnostics), - Resources: []tfprotov6.ResourceMetadata{}, + Functions: make([]tfprotov6.FunctionMetadata, 0, len(fw.Functions)), + Resources: make([]tfprotov6.ResourceMetadata, 0, len(fw.Resources)), ServerCapabilities: ServerCapabilities(ctx, fw.ServerCapabilities), } @@ -28,6 +29,10 @@ func GetMetadataResponse(ctx context.Context, fw *fwserver.GetMetadataResponse) protov6.DataSources = append(protov6.DataSources, DataSourceMetadata(ctx, datasource)) } + for _, function := range fw.Functions { + protov6.Functions = append(protov6.Functions, FunctionMetadata(ctx, function)) + } + for _, resource := range fw.Resources { protov6.Resources = append(protov6.Resources, ResourceMetadata(ctx, resource)) } diff --git a/internal/toproto6/getmetadata_test.go b/internal/toproto6/getmetadata_test.go index af0654095..5c0590500 100644 --- a/internal/toproto6/getmetadata_test.go +++ b/internal/toproto6/getmetadata_test.go @@ -45,6 +45,7 @@ func TestGetMetadataResponse(t *testing.T) { TypeName: "test_data_source_2", }, }, + Functions: []tfprotov6.FunctionMetadata{}, Resources: []tfprotov6.ResourceMetadata{}, }, }, @@ -70,6 +71,31 @@ func TestGetMetadataResponse(t *testing.T) { "This is always an issue with the provider and should be reported to the provider developers.", }, }, + Functions: []tfprotov6.FunctionMetadata{}, + Resources: []tfprotov6.ResourceMetadata{}, + }, + }, + "functions": { + input: &fwserver.GetMetadataResponse{ + Functions: []fwserver.FunctionMetadata{ + { + Name: "function1", + }, + { + Name: "function2", + }, + }, + }, + expected: &tfprotov6.GetMetadataResponse{ + DataSources: []tfprotov6.DataSourceMetadata{}, + Functions: []tfprotov6.FunctionMetadata{ + { + Name: "function1", + }, + { + Name: "function2", + }, + }, Resources: []tfprotov6.ResourceMetadata{}, }, }, @@ -86,6 +112,7 @@ func TestGetMetadataResponse(t *testing.T) { }, expected: &tfprotov6.GetMetadataResponse{ DataSources: []tfprotov6.DataSourceMetadata{}, + Functions: []tfprotov6.FunctionMetadata{}, Resources: []tfprotov6.ResourceMetadata{ { TypeName: "test_resource_1", @@ -105,6 +132,7 @@ func TestGetMetadataResponse(t *testing.T) { }, expected: &tfprotov6.GetMetadataResponse{ DataSources: []tfprotov6.DataSourceMetadata{}, + Functions: []tfprotov6.FunctionMetadata{}, Resources: []tfprotov6.ResourceMetadata{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ GetProviderSchemaOptional: true, diff --git a/internal/toproto6/getproviderschema.go b/internal/toproto6/getproviderschema.go index 54ddac41b..ee221abbf 100644 --- a/internal/toproto6/getproviderschema.go +++ b/internal/toproto6/getproviderschema.go @@ -18,9 +18,10 @@ func GetProviderSchemaResponse(ctx context.Context, fw *fwserver.GetProviderSche } protov6 := &tfprotov6.GetProviderSchemaResponse{ - DataSourceSchemas: map[string]*tfprotov6.Schema{}, + DataSourceSchemas: make(map[string]*tfprotov6.Schema, len(fw.DataSourceSchemas)), Diagnostics: Diagnostics(ctx, fw.Diagnostics), - ResourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: make(map[string]*tfprotov6.Function, len(fw.FunctionDefinitions)), + ResourceSchemas: make(map[string]*tfprotov6.Schema, len(fw.ResourceSchemas)), ServerCapabilities: ServerCapabilities(ctx, fw.ServerCapabilities), } @@ -64,6 +65,10 @@ func GetProviderSchemaResponse(ctx context.Context, fw *fwserver.GetProviderSche } } + for name, functionDefinition := range fw.FunctionDefinitions { + protov6.Functions[name] = Function(ctx, functionDefinition) + } + for resourceType, resourceSchema := range fw.ResourceSchemas { protov6.ResourceSchemas[resourceType], err = Schema(ctx, resourceSchema) diff --git a/internal/toproto6/getproviderschema_test.go b/internal/toproto6/getproviderschema_test.go index e31aae1c4..20cd7df10 100644 --- a/internal/toproto6/getproviderschema_test.go +++ b/internal/toproto6/getproviderschema_test.go @@ -10,6 +10,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/attr" datasourceschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" @@ -80,6 +81,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, @@ -109,6 +111,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, @@ -140,6 +143,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, @@ -169,6 +173,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, @@ -200,6 +205,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, @@ -229,6 +235,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, @@ -260,6 +267,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, @@ -289,6 +297,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, @@ -318,6 +327,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, @@ -347,6 +357,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, @@ -383,6 +394,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, @@ -428,6 +440,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, @@ -468,6 +481,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, @@ -500,6 +514,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, @@ -545,6 +560,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, @@ -577,6 +593,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, @@ -606,6 +623,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, @@ -642,6 +660,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, @@ -687,6 +706,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, @@ -727,6 +747,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, @@ -763,6 +784,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, @@ -795,6 +817,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, @@ -838,6 +861,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, @@ -867,6 +891,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, @@ -910,6 +935,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, @@ -953,6 +979,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, @@ -994,6 +1021,192 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, }, }, + Functions: map[string]*tfprotov6.Function{}, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "functions": { + input: &fwserver.GetProviderSchemaResponse{ + FunctionDefinitions: map[string]function.Definition{ + "testfunction1": { + Return: function.StringReturn{}, + }, + "testfunction2": { + Return: function.StringReturn{}, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{ + "testfunction1": { + Parameters: []*tfprotov6.FunctionParameter{}, + Return: &tfprotov6.FunctionReturn{ + Type: tftypes.String, + }, + }, + "testfunction2": { + Parameters: []*tfprotov6.FunctionParameter{}, + Return: &tfprotov6.FunctionReturn{ + Type: tftypes.String, + }, + }, + }, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "functions-deprecationmessage": { + input: &fwserver.GetProviderSchemaResponse{ + FunctionDefinitions: map[string]function.Definition{ + "testfunction": { + DeprecationMessage: "test deprecation message", + Return: function.StringReturn{}, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{ + "testfunction": { + DeprecationMessage: "test deprecation message", + Parameters: []*tfprotov6.FunctionParameter{}, + Return: &tfprotov6.FunctionReturn{ + Type: tftypes.String, + }, + }, + }, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "functions-description": { + input: &fwserver.GetProviderSchemaResponse{ + FunctionDefinitions: map[string]function.Definition{ + "testfunction": { + Description: "test description", + Return: function.StringReturn{}, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{ + "testfunction": { + Description: "test description", + Parameters: []*tfprotov6.FunctionParameter{}, + Return: &tfprotov6.FunctionReturn{ + Type: tftypes.String, + }, + }, + }, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "functions-parameters": { + input: &fwserver.GetProviderSchemaResponse{ + FunctionDefinitions: map[string]function.Definition{ + "testfunction": { + Parameters: []function.Parameter{ + function.BoolParameter{}, + function.Int64Parameter{}, + function.StringParameter{}, + }, + Return: function.StringReturn{}, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{ + "testfunction": { + Parameters: []*tfprotov6.FunctionParameter{ + { + Name: function.DefaultParameterName, + Type: tftypes.Bool, + }, + { + Name: function.DefaultParameterName, + Type: tftypes.Number, + }, + { + Name: function.DefaultParameterName, + Type: tftypes.String, + }, + }, + Return: &tfprotov6.FunctionReturn{ + Type: tftypes.String, + }, + }, + }, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "functions-result": { + input: &fwserver.GetProviderSchemaResponse{ + FunctionDefinitions: map[string]function.Definition{ + "testfunction": { + Return: function.StringReturn{}, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{ + "testfunction": { + Parameters: []*tfprotov6.FunctionParameter{}, + Return: &tfprotov6.FunctionReturn{ + Type: tftypes.String, + }, + }, + }, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "functions-summary": { + input: &fwserver.GetProviderSchemaResponse{ + FunctionDefinitions: map[string]function.Definition{ + "testfunction": { + Return: function.StringReturn{}, + Summary: "test summary", + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{ + "testfunction": { + Parameters: []*tfprotov6.FunctionParameter{}, + Return: &tfprotov6.FunctionReturn{ + Type: tftypes.String, + }, + Summary: "test summary", + }, + }, + ResourceSchemas: map[string]*tfprotov6.Schema{}, + }, + }, + "functions-variadicparameter": { + input: &fwserver.GetProviderSchemaResponse{ + FunctionDefinitions: map[string]function.Definition{ + "testfunction": { + Return: function.StringReturn{}, + VariadicParameter: function.StringParameter{}, + }, + }, + }, + expected: &tfprotov6.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{ + "testfunction": { + Parameters: []*tfprotov6.FunctionParameter{}, + Return: &tfprotov6.FunctionReturn{ + Type: tftypes.String, + }, + VariadicParameter: &tfprotov6.FunctionParameter{ + Name: function.DefaultParameterName, + Type: tftypes.String, + }, + }, + }, ResourceSchemas: map[string]*tfprotov6.Schema{}, }, }, @@ -1010,6 +1223,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1037,6 +1251,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1063,6 +1278,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1090,6 +1306,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1117,6 +1334,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1143,6 +1361,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1169,6 +1388,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1198,6 +1418,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1235,6 +1456,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1275,6 +1497,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1308,6 +1531,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1343,6 +1567,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1379,6 +1604,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1407,6 +1633,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1436,6 +1663,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1473,6 +1701,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1513,6 +1742,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1548,6 +1778,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1579,6 +1810,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1612,6 +1844,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1647,6 +1880,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1679,6 +1913,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ BlockTypes: []*tfprotov6.SchemaNestedBlock{ @@ -1719,6 +1954,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ BlockTypes: []*tfprotov6.SchemaNestedBlock{ @@ -1757,6 +1993,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, Provider: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ BlockTypes: []*tfprotov6.SchemaNestedBlock{ @@ -1791,6 +2028,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1817,6 +2055,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1843,6 +2082,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1869,6 +2109,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1895,6 +2136,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1924,6 +2166,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -1961,6 +2204,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2001,6 +2245,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2034,6 +2279,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2069,6 +2315,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2105,6 +2352,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2133,6 +2381,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2162,6 +2411,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2199,6 +2449,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2239,6 +2490,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2274,6 +2526,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2305,6 +2558,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2338,6 +2592,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2373,6 +2628,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ProviderMeta: &tfprotov6.Schema{ Block: &tfprotov6.SchemaBlock{ Attributes: []*tfprotov6.SchemaAttribute{ @@ -2408,6 +2664,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource_1": { Block: &tfprotov6.SchemaBlock{ @@ -2448,6 +2705,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -2478,6 +2736,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -2508,6 +2767,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -2538,6 +2798,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -2568,6 +2829,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -2598,6 +2860,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -2628,6 +2891,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -2657,6 +2921,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -2686,6 +2951,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -2718,6 +2984,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -2758,6 +3025,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -2801,6 +3069,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -2837,6 +3106,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -2875,6 +3145,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -2914,6 +3185,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -2945,6 +3217,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -2977,6 +3250,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -3017,6 +3291,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -3060,6 +3335,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -3098,6 +3374,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -3132,6 +3409,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -3168,6 +3446,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -3206,6 +3485,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -3241,6 +3521,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -3284,6 +3565,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -3325,6 +3607,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{ @@ -3358,6 +3641,7 @@ func TestGetProviderSchemaResponse(t *testing.T) { }, expected: &tfprotov6.GetProviderSchemaResponse{ DataSourceSchemas: map[string]*tfprotov6.Schema{}, + Functions: map[string]*tfprotov6.Function{}, ResourceSchemas: map[string]*tfprotov6.Schema{ "test_resource": { Block: &tfprotov6.SchemaBlock{}, diff --git a/internal/toproto6/pointer_test.go b/internal/toproto6/pointer_test.go new file mode 100644 index 000000000..acd79e238 --- /dev/null +++ b/internal/toproto6/pointer_test.go @@ -0,0 +1,8 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6_test + +func pointer[T any](value T) *T { + return &value +} diff --git a/provider/provider.go b/provider/provider.go index 8805f99b1..f96aaa1f9 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -7,6 +7,7 @@ import ( "context" "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/resource" ) @@ -16,6 +17,7 @@ import ( // // - Validation: Schema-based or entire configuration // via ProviderWithConfigValidators or ProviderWithValidateConfig. +// - Functions: ProviderWithFunctions // - Meta Schema: ProviderWithMetaSchema type Provider interface { // Metadata should return the metadata for the provider, such as @@ -68,6 +70,24 @@ type ProviderWithConfigValidators interface { ConfigValidators(context.Context) []ConfigValidator } +// ProviderWithFunctions is an interface type that extends Provider to +// include provider defined functions for usage in practitioner configurations. +// +// Provider-defined functions are supported in Terraform version 1.8 and later. +// +// NOTE: Provider-defined function support is in technical preview and offered +// without compatibility promises until Terraform 1.8 is generally available. +type ProviderWithFunctions interface { + Provider + + // Functions returns a slice of functions to instantiate each Function + // implementation. + // + // The function name is determined by the Function implementing its Metadata + // method. All functions must have unique names. + Functions(context.Context) []func() function.Function +} + // ProviderWithMetaSchema is a provider with a provider meta schema, which // is configured by practitioners via the provider_meta configuration block // and the configuration data is included with certain data source and resource diff --git a/website/data/plugin-framework-nav-data.json b/website/data/plugin-framework-nav-data.json index b8c97bad9..1bdc7baa1 100644 --- a/website/data/plugin-framework-nav-data.json +++ b/website/data/plugin-framework-nav-data.json @@ -118,6 +118,121 @@ } ] }, + { + "title": "Functions", + "routes": [ + { + "title": "Overview", + "path": "functions" + }, + { + "title": "Concepts", + "path": "functions/concepts" + }, + { + "title": "Implementation", + "path": "functions/implementation" + }, + { + "title": "Parameters", + "routes": [ + { + "title": "Overview", + "path": "functions/parameters" + }, + { + "title": "Bool", + "path": "functions/parameters/bool" + }, + { + "title": "Float64", + "path": "functions/parameters/float64" + }, + { + "title": "Int64", + "path": "functions/parameters/int64" + }, + { + "title": "List", + "path": "functions/parameters/list" + }, + { + "title": "Map", + "path": "functions/parameters/map" + }, + { + "title": "Number", + "path": "functions/parameters/number" + }, + { + "title": "Object", + "path": "functions/parameters/object" + }, + { + "title": "Set", + "path": "functions/parameters/set" + }, + { + "title": "String", + "path": "functions/parameters/string" + } + ] + }, + { + "title": "Returns", + "routes": [ + { + "title": "Overview", + "path": "functions/returns" + }, + { + "title": "Bool", + "path": "functions/returns/bool" + }, + { + "title": "Float64", + "path": "functions/returns/float64" + }, + { + "title": "Int64", + "path": "functions/returns/int64" + }, + { + "title": "List", + "path": "functions/returns/list" + }, + { + "title": "Map", + "path": "functions/returns/map" + }, + { + "title": "Number", + "path": "functions/returns/number" + }, + { + "title": "Object", + "path": "functions/returns/object" + }, + { + "title": "Set", + "path": "functions/returns/set" + }, + { + "title": "String", + "path": "functions/returns/string" + } + ] + }, + { + "title": "Testing", + "path": "functions/testing" + }, + { + "title": "Documentation", + "path": "functions/documentation" + } + ] + }, { "title": "Handling Data", "routes": [ diff --git a/website/docs/plugin/framework/diagnostics.mdx b/website/docs/plugin/framework/diagnostics.mdx index 8b20b2acc..2d4f17318 100644 --- a/website/docs/plugin/framework/diagnostics.mdx +++ b/website/docs/plugin/framework/diagnostics.mdx @@ -83,6 +83,12 @@ future release.". error or warning. Only diagnostics that pertain to a whole attribute or a specific attribute value will include this information. +### Argument + +`Argument` identifies the specific function argument position that caused the +error or warning. Only diagnostics that pertain to a function argument will +include this information. + ## How Errors Affect State **Returning an error diagnostic does not stop the state from being updated**. @@ -245,6 +251,23 @@ 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. @@ -253,10 +276,12 @@ The [`diag` package](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-fr | Function | Description | |---|---| -| [`diag.NewAttributeErrorDiagnostic()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/diag#NewAttributeErrorDiagnostic) | Create a new error diagnostic with a [path](/terraform/plugin/framework/handling-data/paths). |. -| [`diag.NewAttributeWarningDiagnostic()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/diag#NewAttributeWarningDiagnostic) | Create a new warning diagnostic with a [path](/terraform/plugin/framework/handling-data/paths). |. -| [`diag.NewErrorDiagnostic()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/diag#NewErrorDiagnostic) | Create a new error diagnostic without a [path](/terraform/plugin/framework/handling-data/paths). |. -| [`diag.NewWarningDiagnostic()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/diag#NewWarningDiagnostic) | Create a new warning diagnostic without a [path](/terraform/plugin/framework/handling-data/paths). |. +| [`diag.NewArgumentErrorDiagnostic()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/diag#NewArgumentErrorDiagnostic) | Create a new error diagnostic with a function argument position. | +| [`diag.NewArgumentWarningDiagnostic()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/diag#NewArgumentWarningDiagnostic) | Create a new warning diagnostic with a function argument position. | +| [`diag.NewAttributeErrorDiagnostic()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/diag#NewAttributeErrorDiagnostic) | Create a new error diagnostic with a [path](/terraform/plugin/framework/handling-data/paths). | +| [`diag.NewAttributeWarningDiagnostic()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/diag#NewAttributeWarningDiagnostic) | Create a new warning diagnostic with a [path](/terraform/plugin/framework/handling-data/paths). | +| [`diag.NewErrorDiagnostic()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/diag#NewErrorDiagnostic) | Create a new error diagnostic without a [path](/terraform/plugin/framework/handling-data/paths). | +| [`diag.NewWarningDiagnostic()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/diag#NewWarningDiagnostic) | Create a new warning diagnostic without a [path](/terraform/plugin/framework/handling-data/paths). | In this example, the provider code is setup to always convert `error` returns from the API SDK to a consistent error diagnostic. @@ -304,7 +329,7 @@ type Diagnostic interface { } ``` -To also include attribute path information, the [`diag.DiagnosticWithPath` interface](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/diag#DiagnosticWithPath) can be implemented with the additional `Path()` method: +To include attribute path information, the [`diag.DiagnosticWithPath` interface](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/diag#DiagnosticWithPath) can be implemented with the additional `Path()` method: ```go type DiagnosticWithPath interface { @@ -313,6 +338,15 @@ type DiagnosticWithPath interface { } ``` +To include function argument information, the [`diag.DiagnosticWithFunctionArgument` interface](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/diag#DiagnosticWithFunctionArgument) can be implemented with the additional `FunctionArgument()` method: + +```go +type DiagnosticWithFunctionArgument interface { + Diagnostic + FunctionArgument() int +} +``` + In this example, a custom diagnostic type stores an underlying `error` that caused the diagnostic: ```go diff --git a/website/docs/plugin/framework/functions/concepts.mdx b/website/docs/plugin/framework/functions/concepts.mdx new file mode 100644 index 000000000..a933a4dfa --- /dev/null +++ b/website/docs/plugin/framework/functions/concepts.mdx @@ -0,0 +1,67 @@ +--- +page_title: 'Plugin Development - Framework: Function Concepts' +description: >- + Terraform concepts for provider-defined functions. +--- + +# Function Concepts + +This page describes Terraform concepts relating to provider-defined functions within framework-based provider code. Provider-defined functions are supported in Terraform 1.8 and later. The [What is Terraform](/terraform/intro), [Terraform language](/terraform/language), and [Plugin Development](/terraform/plugin) documentation covers more general concepts behind Terraform's workflow, its configuration, and how it interacts with providers. + +## Purpose + +The purpose of provider-defined functions is to encapsulate offline, computational logic beyond Terraform's built-in functions to simplify practitioner configurations. Terraform expects that provider-defined functions are implemented without side-effects and as pure functions where given the same input data that they always return the same output. Refer to [HashiCorp Provider Design Principles](/terraform/plugin/best-practices/hashicorp-provider-design-principles) for additional best practice details. + +Example use cases include: + +* Transforming existing data, such as merging complex data structures using a specific algorithm or converting between encodings. +* Parsing combined data into individual, referenceable components, such as taking an Amazon Resource Name (ARN) and returning an object of region, account identifier, etc. attributes. +* Building combined data from individual components, such as returning an Amazon Resource Name (ARN) based on given region, account identifier, etc. data. +* Static data lookups when there is no remote system query available, such as returning a data value typically necessary for a practitioner configuration. + +Differences from other provider-defined concepts include: + +* [Data Sources](/terraform/plugin/framework/data-sources): Intended to perform online or provider configuration dependent data lookup, which participate in Terraform's operational graph. +* [Resources](/terraform/plugin/framework/resources): Intended to manage the full lifecycle (create, update, destroy) of a remote system component, which participate in Terraform's operational graph. + +## Terminology + +There are two main components of provider-defined functions: + +* **Definition**: Defines the expected input and output data along with documentation descriptions. +* **Call**: When a practioner configuration causes a function's logic to be run. + +Within a function definition the components are: + +* **Parameters**: An ordered list of definitions for input data. + * **Variadic Parameter**: An optional, final parameter which accepts zero, one, or multiple parts of input data. +* **Return**: The definition for output data. + +Similar to many programming languages, when the function is called, the terminology for the data is slightly different than the terminology for the definition. + +* **Arguments**: Positionally ordered data based on the definitions of the parameters. +* **Result**: Data based on the definition of the return. + +## Implementation Overview + +For each provider listed as a [required provider](/terraform/language/providers/requirements), Terraform will query the provider for its function definitions. If a configuration attempts to call a provider-defined function without listing the provider as required, Terraform will return an error. + +Terraform will typically call functions before other provider concepts are evaluated. This includes before provider configuration being evaluated, which the framework enforces by not exposing provider configuration data to function implementations. + +### Naming + +Terraform requires that function names must be valid [identifiers](/terraform/language/syntax/configuration#identifiers). + +### Argument Handling + +Terraform will statically validate that the number and types of arguments in a configuration match the definitions of parameters, otherwise returning an error. + +If a null value is given as an argument, without individual parameter definition opt-in, Terraform will return an error. If an unknown value is given as an argument, without individual parameter definition opt-in, Terraform will skip calling the provider logic entirely and set the function result to an unknown value matching the return type. + +### Result Handling + +Terraform will statically validate that the return type is appropriately used in consuming configuration, otherwise returning an error. + +Function logic must always set the result to the return type, otherwise Terraform will return an error. + +Function logic can only set the result to an unknown value if there is a parameter that opted into unknown value handling and an unknown value argument was received for one of those parameters. diff --git a/website/docs/plugin/framework/functions/documentation.mdx b/website/docs/plugin/framework/functions/documentation.mdx new file mode 100644 index 000000000..94c38ee3a --- /dev/null +++ b/website/docs/plugin/framework/functions/documentation.mdx @@ -0,0 +1,121 @@ +--- +page_title: 'Plugin Development - Framework: Document Functions' +description: >- + How to document provider-defined functions. +--- + +# Document Functions + +When a function is [implemented](/terraform/plugin/framework/functions/implementation), ensure the function is discoverable by practitioners with usage information. + +There are two main components for function documentation: + +* [Implementation-Based Documentation](#implementation-based-documentation): Exposes function documentation to Terraform and downstream tooling, such as practitioner configuration editor integrations. +* [Registry-Based Documentation](#registry-based-documentation): Exposes function documentation to the [Terraform Registry](https://registry.terraform.io) when the [provider is published](/terraform/registry/providers/publishing), making it displayed and discoverable on the web. + +## Implementation-Based Documentation + +Add documentation directly inside the [function definition](/terraform/plugin/framework/functions/implementation#definition-method). All implementation-based documentation is passed to Terraform, which downstream tooling such as pracitioner configuration editor integrations will automatically display. + +### Definition + +The [`function.Definition` type](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#Definition) implements the following fields: + +| Field Name | Description | +|---|---| +| `Summary` | A short description of the function and its return, preferably a single sentence. | +| `Description` | Longer documentation about the function, its return, and pertinent implementation details in plaintext format. | +| `MarkdownDescription` | Longer documentation about the function, its return, and pertinent implementation details in Markdown format. | + +If there are no description formatting differences, set only one of `Description` or `MarkdownDescription`. When Terraform has not sent a preference for the description formatting, the framework will return `MarkdownDescription` if both are defined. + +In this example, the function definition sets summary and description documentation: + +```go +func (f *CidrContainsIpFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other fields ... + Summary: "Check if a network CIDR contains an IP", + Description: "Returns a boolean whether a RFC4632 CIDR contains an IP address", + } +} +``` + +### Parameters + +Each [parameter type](/terraform/plugin/framework/functions/parameters), whether in the definition `Parameters` or `VariadicParameter` field, implements the following fields: + +| Field Name | Description | +|---|---| +| `Name` | Single word or abbreviation of parameter for function signature generation, defaults to `param`. | +| `Description` | Documentation about the parameter and its expected values in plaintext format. | +| `MarkdownDescription` | Documentation about the parameter and its expected values in Markdown format. | + +The name is only for documentation purposes and helpful when there is a need to disambiguate between multiple parameters, such as the words `cidr` and `ip` in a generated function signature like `cidr_contains_ip(cidr string, ip string) bool`. + +If there are no description formatting differences, set only one of `Description` or `MarkdownDescription`. When Terraform has not sent a preference for the description formatting, the framework will return `MarkdownDescription` if both are defined. + +In this example, the function parameters set name and description documentation: + +```go +func (f *CidrContainsIpFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other fields ... + Parameters: []function.Parameter{ + function.StringParameter{ + Name: "cidr", + Description: "RFC4632 CIDR to check whether it contains the given IP address", + }, + function.StringParameter{ + Name: "ip", + Description: "IP address to check whether its contained in the RFC4632 CIDR", + }, + }, + } +} +``` + +## Registry-Based Documentation + +Add Markdown documentation files in conventional provider codebase locations before [publishing](/terraform/registry/providers/publishing) to the [Terraform Registry](https://registry.terraform.io). The documentation is displayed and discoverable on the web. These files can be manually created or automatically generated using tooling such as [`terraform-plugin-docs`](https://github.com/hashicorp/terraform-plugin-docs). + +The [Registry provider documentation](/terraform/registry/providers/docs) covers the overall requirements, conventional file layout details, and how to enable additional features such as sub-categories for the navigation sidebar. Function documentation for most providers is expected under the `docs/functions/` directory with a file named after the function and with the extension `.md`. Older providers using the legacy file layout use `website/docs/functions/` and `.html.md`. + +Functions are conventionally documented with the following: + +* Description +* Example Usage +* Signature +* Arguments + +In this example, a `docs/functions/contains_ip.md` file (either manually or automatically created) will be displayed in the Terraform Registry after provider publishing: + +``````plain +--- +page_title: contains_ip Function - terraform-provider-cidr +description: |- + Returns a boolean whether a RFC4632 CIDR contains an IP address. +--- + +# Function: contains_ip + +Returns a boolean whether a RFC4632 CIDR contains an IP address. + +## Example Usage + +```terraform +# result: true +provider::cidr::contains_ip("10.0.0.0/8", "10.0.0.1") +``` + +## Signature + +```text +contains_ip(cidr string, ip string) bool +``` + +## Arguments + +1. `cidr` (String) RFC4632 CIDR to check whether it contains the given IP address. +2. `ip` (String) IP address to check whether its contained in the RFC4632 CIDR. +`````` diff --git a/website/docs/plugin/framework/functions/implementation.mdx b/website/docs/plugin/framework/functions/implementation.mdx new file mode 100644 index 000000000..9fb430a75 --- /dev/null +++ b/website/docs/plugin/framework/functions/implementation.mdx @@ -0,0 +1,313 @@ +--- +page_title: 'Plugin Development - Framework: Implement Functions' +description: >- + How to implement provider-defined functions in the provider development framework. +--- + +# Implement Functions + + + +Provider-defined function support is in technical preview and offered without compatibility promises until Terraform 1.8 is generally available. + + + +The framework supports implementing functions based on Terraform's [concepts for provider-defined functions](/terraform/plugin/framework/functions/concepts). It is recommended to understand those concepts before implementing a function since the terminology is used throughout this page and there are details that simplify function handling as compared to other provider concepts. + +The main code components of a function implementation are: + +* [Defining the function](#define-function-type) including its name, expected data types, descriptions, and logic. +* [Adding the function to the provider](#add-function-to-provider) so it is accessible by Terraform and practitioners. + +Once the code is implemented, it is always recommended to also add: + +* [Testing](/terraform/plugin/framework/functions/testing) to ensure expected function behaviors. +* [Documentation](/terraform/plugin/framework/functions/documentation) to ensure the function is discoverable by practitioners with usage information. + +## Define Function Type + +Implement the [`function.Function` interface](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#Function). Each of the methods is described in more detail below. + +In this example, a function named `echo` is defined, which takes a string argument and returns that value as the result: + +```go +import ( + "github.com/hashicorp/terraform-plugin-framework/function" +) + +// Ensure the implementation satisfies the desired interfaces. +var _ function.Function = &EchoFunction{} + +type EchoFunction struct {} + +func (f *EchoFunction) Metadata(ctx context.Context, req function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "echo" +} + +func (f *EchoFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Summary: "Echo a string", + Description: "Given a string value, returns the same value.", + + Parameters: []function.Parameter{ + function.StringParameter{ + Name: "input", + Description: "Value to echo", + }, + }, + Return: function.StringReturn{}, + } +} + +func (f *EchoFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + var input string + + // Read Terraform argument data into the variable + resp.Diagnostics.Append(req.Arguments.Get(ctx, &input)...) + + // Set the result to the same data + resp.Diagnostics.Append(resp.Result.Set(ctx, &input)...) +} +``` + +### Metadata Method + +The [`function.Function` interface `Metadata` method](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#Function.Metadata) defines the function name as it would appear in Terraform configurations. Unlike resources and data sources, this name should **NOT** include the provider name as the configuration language syntax for calling functions will separately include the provider name. Refer to [naming](/terraform/plugin/best-practices/naming) for additional best practice details. + +In this example, the function name is set to `example`: + +```go +// With the function.Function implementation +func (f *ExampleFunction) Metadata(ctx context.Context, req function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "example" +} +``` + +### Definition Method + +The [`function.Function` interface `Definition` method](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#Function.Definition) defines the parameters, return, and various descriptions for documentation of the function. + +In this example, the function definition includes one string parameter, a string return, and descriptions for documentation: + +```go +func (f *ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Summary: "Echo a string", + Description: "Given a string value, returns the same value.", + + Parameters: []function.Parameter{ + function.StringParameter{ + Description: "Value to echo", + Name: "input", + }, + }, + Return: function.StringReturn{}, + } +} +``` + +#### Return + +The `Return` field must be defined as all functions must return a result. This influences how the [Run method](#run-method) must set the result data. Refer to the [returns](/terraform/plugin/framework/functions/returns) documentation for details about all available types and how to handle data with each type. + +#### Parameters + +There may be zero or more parameters, which are defined with the `Parameters` field. They are ordered, which influences how practitioners call the function in their configurations and how the [Run method](#run-method) must read the argument data. Refer to the [parameters](/terraform/plugin/framework/functions/parameters) documentation for details about all available types and how to handle data with each type. + +An optional `VariadicParameter` field enables a final variadic parameter which accepts zero, one, or more values of the same type. It may be optionally combined with `Parameters`, meaning it represents the any argument data after the final parameter. When reading argument data, a `VariadicParameter` is represented as an ordered list of the parameter type, where the list has zero or more elements to match the given arguments. + +By default, Terraform will not pass null or unknown values to the provider logic when a function is called. Within each parameter, use the `AllowNullValue` and/or `AllowUnknownValues` fields to explicitly allow those kinds of values. Enabling `AllowNullValue` requires using a pointer type or [framework type](/terraform/plugin/framework/handling-data/types) when reading argument data. Enabling `AllowUnknownValues` requires using a [framework type](/terraform/plugin/framework/handling-data/types) when reading argument data. + +#### Documentation + +The [function documentation](/terraform/plugin/framework/functions/documentation) page describes how to implement documentation so it is available to Terraform, downstream tooling such as practitioner configuration editor integrations, and in the [Terraform Registry](https://registry.terraform.io). + +#### Deprecation + +If a function is being deprecated, such as for future removal, the `DeprecationMessage` field should be set. The message should be actionable for practitioners, such as telling them what to do with their configuration instead of calling this function. + +### Run Method + +The [`function.Function` interface `Run` method](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#Function.Run) defines the logic that is invoked when Terraform calls the function. Only argument data is provided when a function is called. Refer to [HashiCorp Provider Design Principles](/terraform/plugin/best-practices/hashicorp-provider-design-principles) for additional best practice details. + +Implement the `Run` method by: + +1. Creating variables for argument data, based on the parameter definitions. Refer to the [parameters](/terraform/plugin/framework/functions/parameters) documentation for details about all available parameter types and how to handle data with each type. +1. Reading argument data from the [`function.RunRequest.Arguments` field](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#RunRequest.Arguments). +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). + +### 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. + +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: + +```go +func (f *ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other fields ... + Parameters: []function.Parameter{ + function.BoolParameter{}, + function.StringParameter{}, + }, + } +} + +func (f *ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + var boolArg bool + var stringArg string + + resp.Diagnostics.Append(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. + +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: + +```go +func (f *ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other fields ... + Parameters: []function.Parameter{ + function.BoolParameter{}, + function.StringParameter{}, + }, + } +} + +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)...) + + // ... other logic ... +} +``` + +#### Reading Variadic Parameter Argument Data + +The optional `VariadicParameter` field in a function definition enables a final variadic parameter which accepts zero, one, or more values of the same type. It may be optionally combined with `Parameters`, meaning it represents the argument data after the final parameter. When reading argument data, a `VariadicParameter` is represented as an ordered list of the parameter type, where the list has zero or more elements to match the given arguments. + +Use either the [framework list type](/terraform/plugin/framework/handling-data/types/list) or a Go slice of an appropriate type to match the variadic parameter `[]T`. + +In this example, there is a boolean parameter and string variadic parameter, where the variadic parameter argument data is always fetched as a list: + +```go +func (f *ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other fields ... + Parameters: []function.Parameter{ + function.BoolParameter{}, + }, + VariadicParameter: function.StringParameter{}, + } +} + +func (f *ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + var boolArg bool + var stringVarg []string + + resp.Diagnostics.Append(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 list element index to ensure the diagnostic 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: + +```go +func (f *ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other fields ... + Parameters: []function.Parameter{ + function.BoolParameter{}, + function.Int64Parameter{}, + }, + VariadicParameter: function.StringParameter{}, + } +} + +func (f *ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + var boolArg bool + var int64Arg int64 + var stringVarg []string + + resp.Diagnostics.Append(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") + } + + // ... other logic ... +} +``` + +### Setting Result Data + +The framework supports setting a result value into the [`function.RunResponse.Result` field](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#RunResponse.Result), which is of the [`function.ResultData` type](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#ResultData). The result value must match the return type, otherwise the framework or Terraform will return an error. + +In this example, the return is defined as a string and a string value is set: + +```go +func (f *ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other fields ... + Return: function.StringReturn{}, + } +} + +func (f *ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + // ... other logic ... + + // 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)...) +} +``` + +## Add Function to Provider + +Functions become available to practitioners when they are included in the [provider](/terraform/plugin/framework/providers) implementation via the [`provider.ProviderWithFunctions` interface `Functions` method](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/provider#ProviderWithFunctions.Functions). + +In this example, the `EchoFunction` type, which implements the `function.Function` interface, is added to the provider implementation: + +```go +// With the provider.Provider implementation +func (p *ExampleCloudProvider) Functions(_ context.Context) []func() function.Function { + return []func() function.Function{ + func() function.Function { + return &EchoFunction{}, + }, + } +} +``` + +To simplify provider implementations, a named function can be created with the function implementation. + +In this example, the `EchoFunction` code includes an additional `NewEchoFunction` function, which simplifies the provider implementation: + +```go +// With the provider.Provider implementation +func (p *ExampleCloudProvider) Functions(_ context.Context) []func() function.Function { + return []func() function.Function{ + NewEchoFunction, + } +} + +// With the function.Function implementation +func NewEchoFunction() function.Function { + return &EchoFunction{} +} +``` diff --git a/website/docs/plugin/framework/functions/index.mdx b/website/docs/plugin/framework/functions/index.mdx new file mode 100644 index 000000000..4814e65ee --- /dev/null +++ b/website/docs/plugin/framework/functions/index.mdx @@ -0,0 +1,33 @@ +--- +page_title: 'Plugin Development - Framework: Functions' +description: >- + How to build functions in the provider development framework. Provider-defined + functions expose logic beyond Terraform's built-in functions and simplify + practitioner configurations. +--- + +# Functions + + + +Provider-defined function support is in technical preview and offered without compatibility promises until Terraform 1.8 is generally available. + + + +Functions are an abstraction that allow providers to expose computational logic beyond Terraform's [built-in functions](/terraform/language/functions) and simplify practitioner configurations. Provider-defined functions are supported in Terraform 1.8 and later. + +## Concepts + +Learn about Terraform's [concepts](/terraform/plugin/framework/functions/concepts) for provider-defined functions, such as intended purpose, example use cases, and terminology. The framework's implementation details, such as naming, are based on these concepts. + +## Implementation + +Learn about how to [implement code](/terraform/plugin/framework/functions/implementation) for a provider-defined function in the framework. + +## Testing + +Learn about how to ensure a provider-defined function implementation works as expected via [unit testing and acceptance testing](/terraform/plugin/framework/functions/testing). + +## Documentation + +Learn about how to [document](/terraform/plugin/framework/functions/documentation) a provider-defined function implementation so practitioners can discover and use the function. diff --git a/website/docs/plugin/framework/functions/parameters/bool.mdx b/website/docs/plugin/framework/functions/parameters/bool.mdx new file mode 100644 index 000000000..6f8147045 --- /dev/null +++ b/website/docs/plugin/framework/functions/parameters/bool.mdx @@ -0,0 +1,91 @@ +--- +page_title: 'Plugin Development - Framework: Bool Function Parameter' +description: >- + Learn the bool function parameter type in the provider development framework. +--- + +# Bool Function Parameter + +Bool function parameters expect a boolean true or false value from a practitioner configuration. Values are accessible in function logic by the Go built-in `bool` type, Go built-in `*bool` type, or the [framework bool type](/terraform/plugin/framework/handling-data/types/bool). + +In this Terraform configuration example, a bool parameter is set to the value `true`: + +```hcl +provider::example::example(true) +``` + +## Function Definition + +Use the [`function.BoolParameter` type](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#BoolParameter) in the [function definition](/terraform/plugin/framework/functions/implementation#definition-method) to accept a bool value. + +In this example, a function definition includes a first position bool parameter: + +```go +func (f ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other Definition fields ... + Parameters: []function.Parameter{ + function.BoolParameter{ + // ... potentially other BoolParameter fields ... + }, + }, + } +} +``` + +If the bool value should be the element type of a [collection parameter type](/terraform/plugin/framework/functions/parameters#collection-parameter-types), set the `ElementType` field according to the [framework bool type](/terraform/plugin/framework/handling-data/types/bool). Refer to the collection parameter type documentation for additional details. + +If the bool value should be a value type of an [object parameter type](/terraform/plugin/framework/functions/parameters#object-parameter-type), set the `AttributeTypes` map value according to the [framework bool type](/terraform/plugin/framework/handling-data/types/bool). Refer to the object parameter type documentation for additional details. + +### Allow Null Values + +By default, Terraform will not pass null values to the function logic. Use the `AllowNullValue` field to explicitly allow null values, if there is a meaningful distinction that should occur in function logic. Enabling `AllowNullValue` requires using a Go pointer type or [framework bool type](/terraform/plugin/framework/handling-data/types/bool) when reading argument data. + +### Allow Unknown Values + +By default, Terraform will not pass unknown values to the function logic. Use the `AllowUnknownValues` field to explicitly allow unknown values, if there is a meaningful distinction that should occur in function logic. Enabling `AllowUnknownValues` requires using a [framework bool type](/terraform/plugin/framework/handling-data/types/bool) when reading argument data. + +### Custom Types + +You may want to build your own data value and type implementations to allow your provider to combine validation and other behaviors into a reusable bundle. This helps avoid duplication and ensures consistency. These implementations use the `CustomType` field in the parameter type. + +Refer to [Custom Types](/terraform/plugin/framework/handling-data/types/custom) for further details on creating provider-defined types and values. + +### Documentation + +Refer to [function documentation](/terraform/plugin/framework/functions/documentation) for information about the `Name`, `Description`, and `MarkdownDescription` fields available. + +## Reading Argument Data + +The [function implementation](/terraform/plugin/framework/functions/implementation) documentation covers the general methods for reading function argument data in function logic. + +When retrieving the argument value for this parameter: + +* If `CustomType` is set, use its associated value type. +* If `AllowUnknownValues` is enabled, you must use the [framework bool type](/terraform/plugin/framework/handling-data/types/bool). +* If `AllowNullValue` is enabled, you must use the Go built-in `*bool` type or [framework bool type](/terraform/plugin/framework/handling-data/types/bool). +* Otherwise, use the Go built-in `bool` type, Go built-in `*bool` type, or [framework bool type](/terraform/plugin/framework/handling-data/types/bool). + +In this example, a function defines a single bool parameter and accesses its argument value: + +```go +func (f ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other Definition fields ... + Parameters: []function.Parameter{ + function.BoolParameter{}, + }, + } +} + +func (f ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + var boolArg bool + // 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)...) + + // 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 new file mode 100644 index 000000000..be8ebf464 --- /dev/null +++ b/website/docs/plugin/framework/functions/parameters/float64.mdx @@ -0,0 +1,97 @@ +--- +page_title: 'Plugin Development - Framework: Float64 Function Parameter' +description: >- + Learn the float64 function parameter type in the provider development framework. +--- + +# Float64 Function Parameter + + + +Use [Int64 Parameter](/terraform/plugin/framework/functions/parameters/int64) for 64-bit integer numbers. Use [Number Parameter](/terraform/plugin/framework/functions/parameters/number) for arbitrary precision numbers. + + + +Float64 function parameters expect a 64-bit floating point number value from a practitioner configuration. Values are accessible in function logic by the Go built-in `float64` type, Go built-in `*float64` type, or the [framework float64 type](/terraform/plugin/framework/handling-data/types/float64). + +In this Terraform configuration example, a float64 parameter is set to the value `1.23`: + +```hcl +provider::example::example(1.23) +``` + +## Function Definition + +Use the [`function.Float64Parameter` type](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#Float64Parameter) in the [function definition](/terraform/plugin/framework/functions/implementation#definition-method) to accept a float64 value. + +In this example, a function definition includes a first position float64 parameter: + +```go +func (f ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other Definition fields ... + Parameters: []function.Parameter{ + function.Float64Parameter{ + // ... potentially other Float64Parameter fields ... + }, + }, + } +} +``` + +If the float64 value should be the element type of a [collection parameter type](/terraform/plugin/framework/functions/parameters#collection-parameter-types), set the `ElementType` field according to the [framework float64 type](/terraform/plugin/framework/handling-data/types/float64). Refer to the collection parameter type documentation for additional details. + +If the float64 value should be a value type of an [object parameter type](/terraform/plugin/framework/functions/parameters#object-parameter-type), set the `AttributeTypes` map value according to the [framework float64 type](/terraform/plugin/framework/handling-data/types/float64). Refer to the object parameter type documentation for additional details. + +### Allow Null Values + +By default, Terraform will not pass null values to the function logic. Use the `AllowNullValue` field to explicitly allow null values, if there is a meaningful distinction that should occur in function logic. Enabling `AllowNullValue` requires using a Go pointer type or [framework float64 type](/terraform/plugin/framework/handling-data/types/float64) when reading argument data. + +### Allow Unknown Values + +By default, Terraform will not pass unknown values to the function logic. Use the `AllowUnknownValues` field to explicitly allow unknown values, if there is a meaningful distinction that should occur in function logic. Enabling `AllowUnknownValues` requires using a [framework float64 type](/terraform/plugin/framework/handling-data/types/float64) when reading argument data. + +### Custom Types + +You may want to build your own data value and type implementations to allow your provider to combine validation and other behaviors into a reusable bundle. This helps avoid duplication and ensures consistency. These implementations use the `CustomType` field in the parameter type. + +Refer to [Custom Types](/terraform/plugin/framework/handling-data/types/custom) for further details on creating provider-defined types and values. + +### Documentation + +Refer to [function documentation](/terraform/plugin/framework/functions/documentation) for information about the `Name`, `Description`, and `MarkdownDescription` fields available. + +## Reading Argument Data + +The [function implementation](/terraform/plugin/framework/functions/implementation) documentation covers the general methods for reading function argument data in function logic. + +When retrieving the argument value for this parameter: + +* If `CustomType` is set, use its associated value type. +* If `AllowUnknownValues` is enabled, you must use the [framework float64 type](/terraform/plugin/framework/handling-data/types/float64). +* If `AllowNullValue` is enabled, you must use the Go built-in `*float64` type or [framework float64 type](/terraform/plugin/framework/handling-data/types/float64). +* Otherwise, use the Go built-in `float64` type, Go built-in `*float64` type, or [framework float64 type](/terraform/plugin/framework/handling-data/types/float64). + +In this example, a function defines a single float64 parameter and accesses its argument value: + +```go +func (f ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other Definition fields ... + Parameters: []function.Parameter{ + function.Float64Parameter{}, + }, + } +} + +func (f ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + var float64Arg float64 + // 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)...) + + // float64Arg is now populated + // ... other logic ... +} +``` diff --git a/website/docs/plugin/framework/functions/parameters/index.mdx b/website/docs/plugin/framework/functions/parameters/index.mdx new file mode 100644 index 000000000..694fb5b4b --- /dev/null +++ b/website/docs/plugin/framework/functions/parameters/index.mdx @@ -0,0 +1,48 @@ +--- +page_title: 'Plugin Development - Framework: Function Parameters' +description: >- + Learn the function parameter types in the provider development framework. + Parameters are positional data arguments in a function definition. +--- + +# Parameters + +Parameters in [function definitions](/terraform/plugin/framework/functions/implementation#definition-method) describes how data values are passed to the function logic. Every parameter type has an associated [value type](/terraform/plugin/framework/handling-data/types), although this data handling is simplified for function implementations over other provider concepts, such as resource implementations. + +## Available Parameter Types + +Function definitions support the following parameter types: + +- [Primitive](#primitive-parameter-types): Parameter that accepts a single value, such as a boolean, number, or string. +- [Collection](#collection-parameter-types): Parameter that accepts multiple values of a single element type, such as a list, map, or set. +- [Object](#object-parameter-type): Parameter that accepts a structure of explicit attribute names. + +### Primitive Parameter Types + +Parameter types that accepts a single data value, such as a boolean, number, or string. + +| Parameter Type | Use Case | +|----------------|----------| +| [Bool](/terraform/plugin/framework/parameters/bool) | Boolean true or false | +| [Float64](/terraform/plugin/framework/parameters/float64) | 64-bit floating point number | +| [Int64](/terraform/plugin/framework/parameters/int64) | 64-bit integer number | +| [Number](/terraform/plugin/framework/parameters/number) | Arbitrary precision (generally over 64-bit, up to 512-bit) number | +| [String](/terraform/plugin/framework/parameters/string) | Collection of UTF-8 encoded characters | + +#### Collection Parameter Types + +Parameter types that accepts multiple values of a single element type, such as a list, map, or set. + +| Parameter Type | Use Case | +|----------------|----------| +| [List](/terraform/plugin/framework/parameters/list) | Ordered collection of single element type | +| [Map](/terraform/plugin/framework/parameters/map) | Mapping of arbitrary string keys to values of single element type | +| [Set](/terraform/plugin/framework/parameters/set) | Unordered, unique collection of single element type | + +#### Object Parameter Type + +Parameter type that accepts a structure of explicit attribute names. + +| Parameter Type | Use Case | +|----------------|----------| +| [Object](/terraform/plugin/framework/parameters/object) | Single structure mapping explicit attribute names | diff --git a/website/docs/plugin/framework/functions/parameters/int64.mdx b/website/docs/plugin/framework/functions/parameters/int64.mdx new file mode 100644 index 000000000..dd116b373 --- /dev/null +++ b/website/docs/plugin/framework/functions/parameters/int64.mdx @@ -0,0 +1,97 @@ +--- +page_title: 'Plugin Development - Framework: Int64 Function Parameter' +description: >- + Learn the int64 function parameter type in the provider development framework. +--- + +# Int64 Function Parameter + + + +Use [Float64 Parameter](/terraform/plugin/framework/functions/parameters/float64) for 64-bit floating point numbers. Use [Number Parameter](/terraform/plugin/framework/functions/parameters/number) for arbitrary precision numbers. + + + +Int64 function parameters expect a 64-bit integer number value from a practitioner configuration. Values are accessible in function logic by the Go built-in `int64` type, Go built-in `*int64` type, or the [framework int64 type](/terraform/plugin/framework/handling-data/types/int64). + +In this Terraform configuration example, a int64 parameter is set to the value `123`: + +```hcl +provider::example::example(123) +``` + +## Function Definition + +Use the [`function.Int64Parameter` type](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#Int64Parameter) in the [function definition](/terraform/plugin/framework/functions/implementation#definition-method) to accept a int64 value. + +In this example, a function definition includes a first position int64 parameter: + +```go +func (f ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other Definition fields ... + Parameters: []function.Parameter{ + function.Int64Parameter{ + // ... potentially other Int64Parameter fields ... + }, + }, + } +} +``` + +If the int64 value should be the element type of a [collection parameter type](/terraform/plugin/framework/functions/parameters#collection-parameter-types), set the `ElementType` field according to the [framework int64 type](/terraform/plugin/framework/handling-data/types/int64). Refer to the collection parameter type documentation for additional details. + +If the int64 value should be a value type of an [object parameter type](/terraform/plugin/framework/functions/parameters#object-parameter-type), set the `AttributeTypes` map value according to the [framework int64 type](/terraform/plugin/framework/handling-data/types/int64). Refer to the object parameter type documentation for additional details. + +### Allow Null Values + +By default, Terraform will not pass null values to the function logic. Use the `AllowNullValue` field to explicitly allow null values, if there is a meaningful distinction that should occur in function logic. Enabling `AllowNullValue` requires using a Go pointer type or [framework int64 type](/terraform/plugin/framework/handling-data/types/int64) when reading argument data. + +### Allow Unknown Values + +By default, Terraform will not pass unknown values to the function logic. Use the `AllowUnknownValues` field to explicitly allow unknown values, if there is a meaningful distinction that should occur in function logic. Enabling `AllowUnknownValues` requires using a [framework int64 type](/terraform/plugin/framework/handling-data/types/int64) when reading argument data. + +### Custom Types + +You may want to build your own data value and type implementations to allow your provider to combine validation and other behaviors into a reusable bundle. This helps avoid duplication and ensures consistency. These implementations use the `CustomType` field in the parameter type. + +Refer to [Custom Types](/terraform/plugin/framework/handling-data/types/custom) for further details on creating provider-defined types and values. + +### Documentation + +Refer to [function documentation](/terraform/plugin/framework/functions/documentation) for information about the `Name`, `Description`, and `MarkdownDescription` fields available. + +## Reading Argument Data + +The [function implementation](/terraform/plugin/framework/functions/implementation) documentation covers the general methods for reading function argument data in function logic. + +When retrieving the argument value for this parameter: + +* If `CustomType` is set, use its associated value type. +* If `AllowUnknownValues` is enabled, you must use the [framework int64 type](/terraform/plugin/framework/handling-data/types/int64). +* If `AllowNullValue` is enabled, you must use the Go built-in `*int64` type or [framework int64 type](/terraform/plugin/framework/handling-data/types/int64). +* Otherwise, use the Go built-in `int64` type, Go built-in `*int64` type, or [framework int64 type](/terraform/plugin/framework/handling-data/types/int64). + +In this example, a function defines a single int64 parameter and accesses its argument value: + +```go +func (f ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other Definition fields ... + Parameters: []function.Parameter{ + function.Int64Parameter{}, + }, + } +} + +func (f ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + var int64Arg int64 + // 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)...) + + // 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 new file mode 100644 index 000000000..3cec58e83 --- /dev/null +++ b/website/docs/plugin/framework/functions/parameters/list.mdx @@ -0,0 +1,100 @@ +--- +page_title: 'Plugin Development - Framework: List Function Parameter' +description: >- + Learn the list function parameter type in the provider development framework. +--- + +# List Function Parameter + +List function parameters expect an ordered collection of single element type value from a practitioner configuration. Values are accessible in function logic by a Go slice of an appropriate pointer type to match the element type `[]*T` or the [framework list type](/terraform/plugin/framework/handling-data/types/list). + +In this Terraform configuration example, a list of string parameter is set to the ordered collection values `one` and `two`: + +```hcl +provider::example::example(["one", "two"]) +``` + +## Function Definition + +Use the [`function.ListParameter` type](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#ListParameter) in the [function definition](/terraform/plugin/framework/functions/implementation#definition-method) to accept a list value. + +The `ElementType` field must be defined, which represents the single [framework value type](/terraform/plugin/framework/handling-data/types) of every element of the list. An element type may itself contain further collection or object types, if necessary. + +In this example, a function definition includes a first position list of string parameter: + +```go +func (f ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other Definition fields ... + Parameters: []function.Parameter{ + function.ListParameter{ + ElementType: types.StringType, + // ... potentially other ListParameter fields ... + }, + }, + } +} +``` + +If the list value should be the element type of another [collection parameter type](/terraform/plugin/framework/functions/parameters#collection-parameter-types), set the `ElementType` field according to the [framework list type](/terraform/plugin/framework/handling-data/types/list). Refer to the collection parameter type documentation for additional details. + +If the list value should be a value type of an [object parameter type](/terraform/plugin/framework/functions/parameters#object-parameter-type), set the `AttributeTypes` map value according to the [framework list type](/terraform/plugin/framework/handling-data/types/list). Refer to the object parameter type documentation for additional details. + +### Allow Null Values + + + +A known list value with null element values will always be sent to the function logic, regardless of the `AllowNullValue` setting. Data handling must always account for this situation. + + + +By default, Terraform will not pass null values to the function logic. Use the `AllowNullValue` field to explicitly allow null values, if there is a meaningful distinction that should occur in function logic. Enabling `AllowNullValue` requires no changes when reading argument data. + +### Allow Unknown Values + +By default, Terraform will not pass unknown values to the function logic. Use the `AllowUnknownValues` field to explicitly allow unknown values, if there is a meaningful distinction that should occur in function logic. Enabling `AllowUnknownValues` requires using a [framework list type](/terraform/plugin/framework/handling-data/types/list) when reading argument data. + +### Custom Types + +You may want to build your own data value and type implementations to allow your provider to combine validation and other behaviors into a reusable bundle. This helps avoid duplication and ensures consistency. These implementations use the `CustomType` field in the parameter type. + +Refer to [Custom Types](/terraform/plugin/framework/handling-data/types/custom) for further details on creating provider-defined types and values. + +### Documentation + +Refer to [function documentation](/terraform/plugin/framework/functions/documentation) for information about the `Name`, `Description`, and `MarkdownDescription` fields available. + +## Reading Argument Data + +The [function implementation](/terraform/plugin/framework/functions/implementation) documentation covers the general methods for reading function argument data in function logic. + +When retrieving the argument value for this parameter: + +* If `CustomType` is set, use its associated value type. +* If `AllowUnknownValues` is enabled, you must use the [framework list type](/terraform/plugin/framework/handling-data/types/list). +* Otherwise, use the Go slice of an appropriate pointer type to match the element type `[]*T` or [framework list type](/terraform/plugin/framework/handling-data/types/list). + +In this example, a function defines a single list of string parameter and accesses its argument value: + +```go +func (f ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other Definition fields ... + Parameters: []function.Parameter{ + function.ListParameter{ + ElementType: types.StringType, + }, + }, + } +} + +func (f ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + var listArg []*string // Go nil equals Terraform null + // var listArg types.List // e.g. with AllowUnknownValues + + resp.Diagnostics.Append(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 new file mode 100644 index 000000000..3c34463d2 --- /dev/null +++ b/website/docs/plugin/framework/functions/parameters/map.mdx @@ -0,0 +1,103 @@ +--- +page_title: 'Plugin Development - Framework: Map Function Parameter' +description: >- + Learn the map function parameter type in the provider development framework. +--- + +# Map Function Parameter + +Map function parameters expect a mapping of arbitrary string keys to values of single element type from a practitioner configuration. Values are accessible in function logic by a Go map of string keys to values of an appropriate pointer type to match the element type `map[string]*T` or the [framework map type](/terraform/plugin/framework/handling-data/types/map). + +In this Terraform configuration example, a map of string parameter is set to the mapped values of `"key1"` to `"value1"` and `"key2"` to `"value2"`: + +```hcl +provider::example::example({ + "key1" = "value1", + "key2" = "value2", +}) +``` + +## Function Definition + +Use the [`function.MapParameter` type](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#MapParameter) in the [function definition](/terraform/plugin/framework/functions/implementation#definition-method) to accept a map value. + +The `ElementType` field must be defined, which represents the single [framework value type](/terraform/plugin/framework/handling-data/types) of every element of the map. An element type may itself contain further collection or object types, if necessary. + +In this example, a function definition includes a first position map of string parameter: + +```go +func (f ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other Definition fields ... + Parameters: []function.Parameter{ + function.MapParameter{ + ElementType: types.StringType, + // ... potentially other MapParameter fields ... + }, + }, + } +} +``` + +If the map value should be the element type of another [collection parameter type](/terraform/plugin/framework/functions/parameters#collection-parameter-types), set the `ElementType` field according to the [framework map type](/terraform/plugin/framework/handling-data/types/map). Refer to the collection parameter type documentation for additional details. + +If the map value should be a value type of an [object parameter type](/terraform/plugin/framework/functions/parameters#object-parameter-type), set the `AttributeTypes` map value according to the [framework map type](/terraform/plugin/framework/handling-data/types/map). Refer to the object parameter type documentation for additional details. + +### Allow Null Values + + + +A known map value with null element values will always be sent to the function logic, regardless of the `AllowNullValue` setting. Data handling must always account for this situation. + + + +By default, Terraform will not pass null values to the function logic. Use the `AllowNullValue` field to explicitly allow null values, if there is a meaningful distinction that should occur in function logic. Enabling `AllowNullValue` requires no changes when reading argument data. + +### Allow Unknown Values + +By default, Terraform will not pass unknown values to the function logic. Use the `AllowUnknownValues` field to explicitly allow unknown values, if there is a meaningful distinction that should occur in function logic. Enabling `AllowUnknownValues` requires using a [framework map type](/terraform/plugin/framework/handling-data/types/map) when reading argument data. + +### Custom Types + +You may want to build your own data value and type implementations to allow your provider to combine validation and other behaviors into a reusable bundle. This helps avoid duplication and ensures consistency. These implementations use the `CustomType` field in the parameter type. + +Refer to [Custom Types](/terraform/plugin/framework/handling-data/types/custom) for further details on creating provider-defined types and values. + +### Documentation + +Refer to [function documentation](/terraform/plugin/framework/functions/documentation) for information about the `Name`, `Description`, and `MarkdownDescription` fields available. + +## Reading Argument Data + +The [function implementation](/terraform/plugin/framework/functions/implementation) documentation covers the general methods for reading function argument data in function logic. + +When retrieving the argument value for this parameter: + +* If `CustomType` is set, use its associated value type. +* If `AllowUnknownValues` is enabled, you must use the [framework map type](/terraform/plugin/framework/handling-data/types/map). +* Otherwise, use the Go map of string keys to values of an appropriate pointer type to match the element type `map[string]*T` or [framework map type](/terraform/plugin/framework/handling-data/types/map). + +In this example, a function defines a single map of string parameter and accesses its argument value: + +```go +func (f ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other Definition fields ... + Parameters: []function.Parameter{ + function.MapParameter{ + ElementType: types.StringType, + }, + }, + } +} + +func (f ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + 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)...) + + // 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 new file mode 100644 index 000000000..c8a0801e6 --- /dev/null +++ b/website/docs/plugin/framework/functions/parameters/number.mdx @@ -0,0 +1,95 @@ +--- +page_title: 'Plugin Development - Framework: Number Function Parameter' +description: >- + Learn the number function parameter type in the provider development framework. +--- + +# Number Function Parameter + + + +Use [Float64 Parameter](/terraform/plugin/framework/functions/parameters/float64) for 64-bit floating point numbers. Use [Int64 Parameter](/terraform/plugin/framework/functions/parameters/int64) for 64-bit integer numbers. + + + +Number function parameters expect an arbitrary precision (generally over 64-bit, up to 512-bit) number value from a practitioner configuration. Values are accessible in function logic by the Go built-in `*big.Float` type or the [framework number type](/terraform/plugin/framework/handling-data/types/number). + +In this Terraform configuration example, a number parameter is set to the value greater than 64 bits: + +```hcl +provider::example::example(pow(2, 64) + 1) +``` + +## Function Definition + +Use the [`function.NumberParameter` type](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#NumberParameter) in the [function definition](/terraform/plugin/framework/functions/implementation#definition-method) to accept a number value. + +In this example, a function definition includes a first position number parameter: + +```go +func (f ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other Definition fields ... + Parameters: []function.Parameter{ + function.NumberParameter{ + // ... potentially other NumberParameter fields ... + }, + }, + } +} +``` + +If the number value should be the element type of a [collection parameter type](/terraform/plugin/framework/functions/parameters#collection-parameter-types), set the `ElementType` field according to the [framework number type](/terraform/plugin/framework/handling-data/types/number). Refer to the collection parameter type documentation for additional details. + +If the number value should be a value type of an [object parameter type](/terraform/plugin/framework/functions/parameters#object-parameter-type), set the `AttributeTypes` map value according to the [framework number type](/terraform/plugin/framework/handling-data/types/number). Refer to the object parameter type documentation for additional details. + +### Allow Null Values + +By default, Terraform will not pass null values to the function logic. Use the `AllowNullValue` field to explicitly allow null values, if there is a meaningful distinction that should occur in function logic. Enabling `AllowNullValue` requires using a Go pointer type or [framework number type](/terraform/plugin/framework/handling-data/types/number) when reading argument data. + +### Allow Unknown Values + +By default, Terraform will not pass unknown values to the function logic. Use the `AllowUnknownValues` field to explicitly allow unknown values, if there is a meaningful distinction that should occur in function logic. Enabling `AllowUnknownValues` requires using a [framework number type](/terraform/plugin/framework/handling-data/types/number) when reading argument data. + +### Custom Types + +You may want to build your own data value and type implementations to allow your provider to combine validation and other behaviors into a reusable bundle. This helps avoid duplication and ensures consistency. These implementations use the `CustomType` field in the parameter type. + +Refer to [Custom Types](/terraform/plugin/framework/handling-data/types/custom) for further details on creating provider-defined types and values. + +### Documentation + +Refer to [function documentation](/terraform/plugin/framework/functions/documentation) for information about the `Name`, `Description`, and `MarkdownDescription` fields available. + +## Reading Argument Data + +The [function implementation](/terraform/plugin/framework/functions/implementation) documentation covers the general methods for reading function argument data in function logic. + +When retrieving the argument value for this parameter: + +* If `CustomType` is set, use its associated value type. +* If `AllowUnknownValues` is enabled, you must use the [framework number type](/terraform/plugin/framework/handling-data/types/number). +* Otherwise, use the Go built-in `*big.Float` type or [framework number type](/terraform/plugin/framework/handling-data/types/number). + +In this example, a function defines a single number parameter and accesses its argument value: + +```go +func (f ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other Definition fields ... + Parameters: []function.Parameter{ + function.NumberParameter{}, + }, + } +} + +func (f ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + var numberArg *big.Float + // var numberArg types.Number // e.g. with AllowUnknownValues or AllowNullValue + + resp.Diagnostics.Append(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 new file mode 100644 index 000000000..0ae535021 --- /dev/null +++ b/website/docs/plugin/framework/functions/parameters/object.mdx @@ -0,0 +1,118 @@ +--- +page_title: 'Plugin Development - Framework: Object Function Parameter' +description: >- + Learn the object function parameter type in the provider development framework. +--- + +# Object Function Parameter + +Object function parameters expect a single structure mapping explicit attribute names to type definitions from a practitioner configuration. Values are accessible in function logic by a Go structure type annotated with `tfsdk` field tags or the [framework object type](/terraform/plugin/framework/handling-data/types/object). + +In this Terraform configuration example, a object parameter is set to the mapped values of `attr1` to `"value1"` and `attr2` to `123`: + +```hcl +provider::example::example({ + attr1 = "value1", + attr2 = 123, +}) +``` + +## Function Definition + +Use the [`function.ObjectParameter` type](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#ObjectParameter) in the [function definition](/terraform/plugin/framework/functions/implementation#definition-method) to accept an object value. + +The `AttributeTypes` field must be defined, which represents a mapping of attribute names to [framework value types](/terraform/plugin/framework/handling-data/types). An attribute type may itself contain further collection or object types, if necessary. + +In this example, a function definition includes a first position object parameter: + +```go +func (f ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other Definition fields ... + Parameters: []function.Parameter{ + function.ObjectParameter{ + AttributeTypes: map[string]attr.Value{ + "attr1": types.StringType, + "attr2": types.Int64Type, + }, + // ... potentially other ObjectParameter fields ... + }, + }, + } +} +``` + +If the map value should be the element type of another [collection parameter type](/terraform/plugin/framework/functions/parameters#collection-parameter-types), set the `ElementType` field according to the [framework object type](/terraform/plugin/framework/handling-data/types/object). Refer to the collection parameter type documentation for additional details. + +If the map value should be a value type of an [object parameter type](/terraform/plugin/framework/functions/parameters#object-parameter-type), set the `AttributeTypes` map value according to the [framework object type](/terraform/plugin/framework/handling-data/types/object). Refer to the object parameter type documentation for additional details. + +### Allow Null Values + + + +A known object value with null attribute values will always be sent to the function logic, regardless of the `AllowNullValue` setting. Data handling must always account for this situation. + + + +By default, Terraform will not pass null values to the function logic. Use the `AllowNullValue` field to explicitly allow null values, if there is a meaningful distinction that should occur in function logic. Enabling `AllowNullValue` requires no changes when reading argument data. + +### Allow Unknown Values + +By default, Terraform will not pass unknown values to the function logic. Use the `AllowUnknownValues` field to explicitly allow unknown values, if there is a meaningful distinction that should occur in function logic. Enabling `AllowUnknownValues` requires using a [framework object type](/terraform/plugin/framework/handling-data/types/object) when reading argument data. + +### Custom Types + +You may want to build your own data value and type implementations to allow your provider to combine validation and other behaviors into a reusable bundle. This helps avoid duplication and ensures consistency. These implementations use the `CustomType` field in the parameter type. + +Refer to [Custom Types](/terraform/plugin/framework/handling-data/types/custom) for further details on creating provider-defined types and values. + +### Documentation + +Refer to [function documentation](/terraform/plugin/framework/functions/documentation) for information about the `Name`, `Description`, and `MarkdownDescription` fields available. + +## Reading Argument Data + +The [function implementation](/terraform/plugin/framework/functions/implementation) documentation covers the general methods for reading function argument data in function logic. + +When retrieving the argument value for this parameter: + +* If `CustomType` is set, use its associated value type. +* If `AllowUnknownValues` is enabled, you must use the [framework object type](/terraform/plugin/framework/handling-data/types/object). +* If `AllowNullValue` is enabled, you must use a pointer to the Go structure type annotated with `tfsdk` field tags or the [framework object type](/terraform/plugin/framework/handling-data/types/object). +* Otherwise, use the Go structure type annotated with `tfsdk` field tags or [framework object type](/terraform/plugin/framework/handling-data/types/object). + +In this example, a function defines a single object parameter and accesses its argument value: + +```go +func (f ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other Definition fields ... + Parameters: []function.Parameter{ + function.ObjectParameter{ + AttributeTypes: map[string]attr.Value{ + "attr1": types.StringType, + "attr2": types.Int64Type, + }, + }, + }, + } +} + +func (f ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + var objectArg struct{ + Attr1 *string `tfsdk:"attr1"` + Attr2 *int64 `tfsdk:"attr2"` + } + // e.g. with AllowNullValues + // var objectArg *struct{ + // Attr1 *string `tfsdk:"attr1"` + // Attr2 *int64 `tfsdk:"attr2"` + // } + // var objectArg types.Object // e.g. with AllowUnknownValues or AllowNullValues + + resp.Diagnostics.Append(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 new file mode 100644 index 000000000..2e4bc5fe9 --- /dev/null +++ b/website/docs/plugin/framework/functions/parameters/set.mdx @@ -0,0 +1,100 @@ +--- +page_title: 'Plugin Development - Framework: Set Function Parameter' +description: >- + Learn the set function parameter type in the provider development framework. +--- + +# Set Function Parameter + +Set function parameters expect an unordered, unique collection of single element type value from a practitioner configuration. Values are accessible in function logic by a Go slice of an appropriate pointer type to match the element type `[]*T` or the [framework set type](/terraform/plugin/framework/handling-data/types/set). + +In this Terraform configuration example, a set of string parameter is set to the collection values `one` and `two`: + +```hcl +provider::example::example(["one", "two"]) +``` + +## Function Definition + +Use the [`function.SetParameter` type](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#SetParameter) in the [function definition](/terraform/plugin/framework/functions/implementation#definition-method) to accept a set value. + +The `ElementType` field must be defined, which represents the single [framework value type](/terraform/plugin/framework/handling-data/types) of every element of the set. An element type may itself contain further collection or object types, if necessary. + +In this example, a function definition includes a first position set of string parameter: + +```go +func (f ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other Definition fields ... + Parameters: []function.Parameter{ + function.SetParameter{ + ElementType: types.StringType, + // ... potentially other SetParameter fields ... + }, + }, + } +} +``` + +If the set value should be the element type of another [collection parameter type](/terraform/plugin/framework/functions/parameters#collection-parameter-types), set the `ElementType` field according to the [framework set type](/terraform/plugin/framework/handling-data/types/set). Refer to the collection parameter type documentation for additional details. + +If the set value should be a value type of an [object parameter type](/terraform/plugin/framework/functions/parameters#object-parameter-type), set the `AttributeTypes` map value according to the [framework set type](/terraform/plugin/framework/handling-data/types/set). Refer to the object parameter type documentation for additional details. + +### Allow Null Values + + + +A known set value with null element values will always be sent to the function logic, regardless of the `AllowNullValue` setting. Data handling must always account for this situation. + + + +By default, Terraform will not pass null values to the function logic. Use the `AllowNullValue` field to explicitly allow null values, if there is a meaningful distinction that should occur in function logic. Enabling `AllowNullValue` requires no changes when reading argument data. + +### Allow Unknown Values + +By default, Terraform will not pass unknown values to the function logic. Use the `AllowUnknownValues` field to explicitly allow unknown values, if there is a meaningful distinction that should occur in function logic. Enabling `AllowUnknownValues` requires using a [framework set type](/terraform/plugin/framework/handling-data/types/set) when reading argument data. + +### Custom Types + +You may want to build your own data value and type implementations to allow your provider to combine validation and other behaviors into a reusable bundle. This helps avoid duplication and ensures consistency. These implementations use the `CustomType` field in the parameter type. + +Refer to [Custom Types](/terraform/plugin/framework/handling-data/types/custom) for further details on creating provider-defined types and values. + +### Documentation + +Refer to [function documentation](/terraform/plugin/framework/functions/documentation) for information about the `Name`, `Description`, and `MarkdownDescription` fields available. + +## Reading Argument Data + +The [function implementation](/terraform/plugin/framework/functions/implementation) documentation covers the general methods for reading function argument data in function logic. + +When retrieving the argument value for this parameter: + +* If `CustomType` is set, use its associated value type. +* If `AllowUnknownValues` is enabled, you must use the [framework set type](/terraform/plugin/framework/handling-data/types/set). +* Otherwise, use the Go slice of an appropriate pointer type to match the element type `[]*T` or [framework set type](/terraform/plugin/framework/handling-data/types/set). + +In this example, a function defines a single set of string parameter and accesses its argument value: + +```go +func (f ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other Definition fields ... + Parameters: []function.Parameter{ + function.SetParameter{ + ElementType: types.StringType, + }, + }, + } +} + +func (f ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + var setArg []*string // Go nil equals Terraform null + // var setArg types.Set // e.g. with AllowUnknownValues + + resp.Diagnostics.Append(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 new file mode 100644 index 000000000..67ea3aa73 --- /dev/null +++ b/website/docs/plugin/framework/functions/parameters/string.mdx @@ -0,0 +1,91 @@ +--- +page_title: 'Plugin Development - Framework: String Function Parameter' +description: >- + Learn the string function parameter type in the provider development framework. +--- + +# String Function Parameter + +String function parameters expect a collection of UTF-8 encoded bytes from a practitioner configuration. Values are accessible in function logic by the Go built-in `string` type, Go built-in `*string` type, or the [framework string type](/terraform/plugin/framework/handling-data/types/string). + +In this Terraform configuration example, a string parameter is set to the value `"hello world"`: + +```hcl +provider::example::example("hello world") +``` + +## Function Definition + +Use the [`function.StringParameter` type](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#StringParameter) in the [function definition](/terraform/plugin/framework/functions/implementation#definition-method) to accept a string value. + +In this example, a function definition includes a first position string parameter: + +```go +func (f ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other Definition fields ... + Parameters: []function.Parameter{ + function.StringParameter{ + // ... potentially other StringParameter fields ... + }, + }, + } +} +``` + +If the string value should be the element type of a [collection parameter type](/terraform/plugin/framework/functions/parameters#collection-parameter-types), set the `ElementType` field according to the [framework string type](/terraform/plugin/framework/handling-data/types/string). Refer to the collection parameter type documentation for additional details. + +If the string value should be a value type of an [object parameter type](/terraform/plugin/framework/functions/parameters#object-parameter-type), set the `AttributeTypes` map value according to the [framework string type](/terraform/plugin/framework/handling-data/types/string). Refer to the object parameter type documentation for additional details. + +### Allow Null Values + +By default, Terraform will not pass null values to the function logic. Use the `AllowNullValue` field to explicitly allow null values, if there is a meaningful distinction that should occur in function logic. Enabling `AllowNullValue` requires using a Go pointer type or [framework string type](/terraform/plugin/framework/handling-data/types/string) when reading argument data. + +### Allow Unknown Values + +By default, Terraform will not pass unknown values to the function logic. Use the `AllowUnknownValues` field to explicitly allow unknown values, if there is a meaningful distinction that should occur in function logic. Enabling `AllowUnknownValues` requires using a [framework string type](/terraform/plugin/framework/handling-data/types/string) when reading argument data. + +### Custom Types + +You may want to build your own data value and type implementations to allow your provider to combine validation and other behaviors into a reusable bundle. This helps avoid duplication and ensures consistency. These implementations use the `CustomType` field in the parameter type. + +Refer to [Custom Types](/terraform/plugin/framework/handling-data/types/custom) for further details on creating provider-defined types and values. + +### Documentation + +Refer to [function documentation](/terraform/plugin/framework/functions/documentation) for information about the `Name`, `Description`, and `MarkdownDescription` fields available. + +## Reading Argument Data + +The [function implementation](/terraform/plugin/framework/functions/implementation) documentation covers the general methods for reading function argument data in function logic. + +When retrieving the argument value for this parameter: + +* If `CustomType` is set, use its associated value type. +* If `AllowUnknownValues` is enabled, you must use the [framework string type](/terraform/plugin/framework/handling-data/types/string). +* If `AllowNullValue` is enabled, you must use the Go built-in `*string` type or [framework string type](/terraform/plugin/framework/handling-data/types/string). +* Otherwise, use the Go built-in `string` type, Go built-in `*string` type, or [framework string type](/terraform/plugin/framework/handling-data/types/string). + +In this example, a function defines a single string parameter and accesses its argument value: + +```go +func (f ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other Definition fields ... + Parameters: []function.Parameter{ + function.StringParameter{}, + }, + } +} + +func (f ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + var stringArg string + // 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)...) + + // 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 new file mode 100644 index 000000000..8b4cd5085 --- /dev/null +++ b/website/docs/plugin/framework/functions/returns/bool.mdx @@ -0,0 +1,65 @@ +--- +page_title: 'Plugin Development - Framework: Bool Function Return' +description: >- + Learn the bool function return type in the provider development framework. +--- + +# Bool Function Return + +Bool function return expects a boolean true or false value from function logic. Set values in function logic with the Go built-in `bool` type, Go built-in `*bool` type, or the [framework bool type](/terraform/plugin/framework/handling-data/types/bool). + +## Function Definition + +Use the [`function.BoolReturn` type](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#BoolReturn) in the [function definition](/terraform/plugin/framework/functions/implementation#definition-method). + +In this example, a function definition includes a bool return: + +```go +func (f ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other Definition fields ... + Return: function.BoolReturn{ + // ... potentially other BoolReturn fields ... + }, + } +} +``` + +### Custom Types + +You may want to build your own data value and type implementations to allow your provider to combine validation and other behaviors into a reusable bundle. This helps avoid duplication and ensures consistency. These implementations use the `CustomType` field in the return type. + +Refer to [Custom Types](/terraform/plugin/framework/handling-data/types/custom) for further details on creating provider-defined types and values. + +### Documentation + +Return documentation is expected in the top-level function documentation. Refer to [function documentation](/terraform/plugin/framework/functions/documentation) for information about the `Summary`, `Description`, and `MarkdownDescription` fields available. + +## Setting Return Data + +The [function implementation](/terraform/plugin/framework/functions/implementation) documentation covers the general methods for setting function return data in function logic. + +When setting the value for this return: + +* If `CustomType` is set, use its associated value type. +* Otherwise, use the Go built-in `bool` type, Go built-in `*bool` type, or [framework bool type](/terraform/plugin/framework/handling-data/types/bool). + +In this example, a function defines a bool return and sets its value: + +```go +func (f ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other Definition fields ... + Return: function.BoolReturn{}, + } +} + +func (f ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + // ... other logic ... + + // hardcoded value for example brevity + result := true + + resp.Diagnostics.Append(resp.Result.Set(ctx, &result)...) +} +``` diff --git a/website/docs/plugin/framework/functions/returns/float64.mdx b/website/docs/plugin/framework/functions/returns/float64.mdx new file mode 100644 index 000000000..564c63823 --- /dev/null +++ b/website/docs/plugin/framework/functions/returns/float64.mdx @@ -0,0 +1,71 @@ +--- +page_title: 'Plugin Development - Framework: Float64 Function Return' +description: >- + Learn the float64 function return type in the provider development framework. +--- + +# Float64 Function Return + + + +Use [Int64 Return](/terraform/plugin/framework/functions/returns/int64) for 64-bit integer numbers. Use [Number Return](/terraform/plugin/framework/functions/returns/number) for arbitrary precision numbers. + + + +Float64 function return expects a 64-bit floating point number value from function logic. Set values in function logic with the Go built-in `float64` type, Go built-in `*float64` type, or the [framework float64 type](/terraform/plugin/framework/handling-data/types/float64). + +## Function Definition + +Use the [`function.Float64Return` type](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#Float64Return) in the [function definition](/terraform/plugin/framework/functions/implementation#definition-method). + +In this example, a function definition includes a float64 return: + +```go +func (f ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other Definition fields ... + Return: function.Float64Return{ + // ... potentially other Float64Return fields ... + }, + } +} +``` + +### Custom Types + +You may want to build your own data value and type implementations to allow your provider to combine validation and other behaviors into a reusable bundle. This helps avoid duplication and ensures consistency. These implementations use the `CustomType` field in the return type. + +Refer to [Custom Types](/terraform/plugin/framework/handling-data/types/custom) for further details on creating provider-defined types and values. + +### Documentation + +Return documentation is expected in the top-level function documentation. Refer to [function documentation](/terraform/plugin/framework/functions/documentation) for information about the `Summary`, `Description`, and `MarkdownDescription` fields available. + +## Setting Return Data + +The [function implementation](/terraform/plugin/framework/functions/implementation) documentation covers the general methods for setting function return data in function logic. + +When setting the value for this return: + +* If `CustomType` is set, use its associated value type. +* Otherwise, use the Go built-in `float64` type, Go built-in `*float64` type, or [framework float64 type](/terraform/plugin/framework/handling-data/types/float64). + +In this example, a function defines a float64 return and sets its value: + +```go +func (f ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other Definition fields ... + Return: function.Float64Return{}, + } +} + +func (f ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + // ... other logic ... + + // hardcoded value for example brevity + result := 1.23 + + resp.Diagnostics.Append(resp.Result.Set(ctx, &result)...) +} +``` diff --git a/website/docs/plugin/framework/functions/returns/index.mdx b/website/docs/plugin/framework/functions/returns/index.mdx new file mode 100644 index 000000000..4d1402289 --- /dev/null +++ b/website/docs/plugin/framework/functions/returns/index.mdx @@ -0,0 +1,48 @@ +--- +page_title: 'Plugin Development - Framework: Function Returns' +description: >- + Learn the function return types in the provider development framework. + A return describes the output data in a function definition. +--- + +# Returns + +A return in a [function definition](/terraform/plugin/framework/functions/implementation#definition-method) describes the result data value from function logic. Every return type has an associated [value type](/terraform/plugin/framework/handling-data/types), although this data handling is simplified for function implementations over other provider concepts, such as resource implementations. + +## Available Return Types + +Function definitions support the following return types: + +- [Primitive](#primitive-return-types): Return that expects a single value, such as a boolean, number, or string. +- [Collection](#collection-return-types): Return that expects multiple values of a single element type, such as a list, map, or set. +- [Object](#object-return-type): Return that expects a structure of explicit attribute names. + +### Primitive Return Types + +Return types that expect a single data value, such as a boolean, number, or string. + +| Return Type | Use Case | +|----------------|----------| +| [Bool](/terraform/plugin/framework/returns/bool) | Boolean true or false | +| [Float64](/terraform/plugin/framework/returns/float64) | 64-bit floating point number | +| [Int64](/terraform/plugin/framework/returns/int64) | 64-bit integer number | +| [Number](/terraform/plugin/framework/returns/number) | Arbitrary precision (generally over 64-bit, up to 512-bit) number | +| [String](/terraform/plugin/framework/returns/string) | Collection of UTF-8 encoded characters | + +#### Collection Return Types + +Return types that expect multiple values of a single element type, such as a list, map, or set. + +| Return Type | Use Case | +|----------------|----------| +| [List](/terraform/plugin/framework/returns/list) | Ordered collection of single element type | +| [Map](/terraform/plugin/framework/returns/map) | Mapping of arbitrary string keys to values of single element type | +| [Set](/terraform/plugin/framework/returns/set) | Unordered, unique collection of single element type | + +#### Object Return Type + +Return type that expects a structure of explicit attribute names. + +| Return Type | Use Case | +|----------------|----------| +| [Object](/terraform/plugin/framework/returns/object) | Single structure mapping explicit attribute names | diff --git a/website/docs/plugin/framework/functions/returns/int64.mdx b/website/docs/plugin/framework/functions/returns/int64.mdx new file mode 100644 index 000000000..4c9f94b84 --- /dev/null +++ b/website/docs/plugin/framework/functions/returns/int64.mdx @@ -0,0 +1,71 @@ +--- +page_title: 'Plugin Development - Framework: Int64 Function Return' +description: >- + Learn the int64 function return type in the provider development framework. +--- + +# Int64 Function Return + + + +Use [Float64 Return](/terraform/plugin/framework/functions/returns/float64) for 64-bit floating point numbers. Use [Number Return](/terraform/plugin/framework/functions/returns/number) for arbitrary precision numbers. + + + +Int64 function return expects a 64-bit integer number value from function logic. Set values in function logic with the Go built-in `int64` type, Go built-in `*int64` type, or the [framework int64 type](/terraform/plugin/framework/handling-data/types/int64). + +## Function Definition + +Use the [`function.Int64Return` type](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#Int64Return) in the [function definition](/terraform/plugin/framework/functions/implementation#definition-method). + +In this example, a function definition includes a int64 return: + +```go +func (f ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other Definition fields ... + Return: function.Int64Return{ + // ... potentially other Int64Return fields ... + }, + } +} +``` + +### Custom Types + +You may want to build your own data value and type implementations to allow your provider to combine validation and other behaviors into a reusable bundle. This helps avoid duplication and ensures consistency. These implementations use the `CustomType` field in the return type. + +Refer to [Custom Types](/terraform/plugin/framework/handling-data/types/custom) for further details on creating provider-defined types and values. + +### Documentation + +Return documentation is expected in the top-level function documentation. Refer to [function documentation](/terraform/plugin/framework/functions/documentation) for information about the `Summary`, `Description`, and `MarkdownDescription` fields available. + +## Setting Return Data + +The [function implementation](/terraform/plugin/framework/functions/implementation) documentation covers the general methods for setting function return data in function logic. + +When setting the value for this return: + +* If `CustomType` is set, use its associated value type. +* Otherwise, use the Go built-in `int64` type, Go built-in `*int64` type, or [framework int64 type](/terraform/plugin/framework/handling-data/types/int64). + +In this example, a function defines a int64 return and sets its value: + +```go +func (f ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other Definition fields ... + Return: function.Int64Return{}, + } +} + +func (f ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + // ... other logic ... + + // hardcoded value for example brevity + result := 123 + + resp.Diagnostics.Append(resp.Result.Set(ctx, &result)...) +} +``` diff --git a/website/docs/plugin/framework/functions/returns/list.mdx b/website/docs/plugin/framework/functions/returns/list.mdx new file mode 100644 index 000000000..8b203d9da --- /dev/null +++ b/website/docs/plugin/framework/functions/returns/list.mdx @@ -0,0 +1,70 @@ +--- +page_title: 'Plugin Development - Framework: List Function Return' +description: >- + Learn the list function return type in the provider development framework. +--- + +# List Function Return + +List function return expects an ordered collection of single element type value from function logic. Set values in function logic with a Go slice of an appropriate type to match the element type `[]T` or the [framework list type](/terraform/plugin/framework/handling-data/types/list). + +## Function Definition + +Use the [`function.ListReturn` type](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#ListReturn) in the [function definition](/terraform/plugin/framework/functions/implementation#definition-method). + +The `ElementType` field must be defined, which represents the single [framework value type](/terraform/plugin/framework/handling-data/types) of every element of the list. An element type may itself contain further collection or object types, if necessary. + +In this example, a function definition includes a list of string return: + +```go +func (f ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other Definition fields ... + Return: function.ListReturn{ + ElementType: types.StringType, + // ... potentially other ListReturn fields ... + }, + } +} +``` + +### Custom Types + +You may want to build your own data value and type implementations to allow your provider to combine validation and other behaviors into a reusable bundle. This helps avoid duplication and ensures consistency. These implementations use the `CustomType` field in the return type. + +Refer to [Custom Types](/terraform/plugin/framework/handling-data/types/custom) for further details on creating provider-defined types and values. + +### Documentation + +Return documentation is expected in the top-level function documentation. Refer to [function documentation](/terraform/plugin/framework/functions/documentation) for information about the `Summary`, `Description`, and `MarkdownDescription` fields available. + +## Setting Return Data + +The [function implementation](/terraform/plugin/framework/functions/implementation) documentation covers the general methods for setting function return data in function logic. + +When setting the value for this return: + +* If `CustomType` is set, use its associated value type. +* Otherwise, use a Go slice of an appropriate type to match the element type `[]T` or [framework list type](/terraform/plugin/framework/handling-data/types/list). + +In this example, a function defines a list of string return and sets its value: + +```go +func (f ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other Definition fields ... + Return: function.ListReturn{ + ElementType: types.StringType, + }, + } +} + +func (f ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + // ... other logic ... + + // hardcoded value for example brevity + result := []string{"one", "two"} + + resp.Diagnostics.Append(resp.Result.Set(ctx, &result)...) +} +``` diff --git a/website/docs/plugin/framework/functions/returns/map.mdx b/website/docs/plugin/framework/functions/returns/map.mdx new file mode 100644 index 000000000..968b15f03 --- /dev/null +++ b/website/docs/plugin/framework/functions/returns/map.mdx @@ -0,0 +1,73 @@ +--- +page_title: 'Plugin Development - Framework: Map Function Return' +description: >- + Learn the map function return type in the provider development framework. +--- + +# Map Function Return + +Map function return expects a mapping of arbitrary string keys to values of single element type from function logic. Set values in function logic with a Go map of string keys to values of an appropriate type to match the element type `map[string]T` or the [framework map type](/terraform/plugin/framework/handling-data/types/map). + +## Function Definition + +Use the [`function.MapReturn` type](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#MapReturn) in the [function definition](/terraform/plugin/framework/functions/implementation#definition-method). + +The `ElementType` field must be defined, which represents the single [framework value type](/terraform/plugin/framework/handling-data/types) of every element of the map. An element type may itself contain further collection or object types, if necessary. + +In this example, a function definition includes a map of string return: + +```go +func (f ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other Definition fields ... + Return: function.MapReturn{ + ElementType: types.StringType, + // ... potentially other MapReturn fields ... + }, + } +} +``` + +### Custom Types + +You may want to build your own data value and type implementations to allow your provider to combine validation and other behaviors into a reusable bundle. This helps avoid duplication and ensures consistency. These implementations use the `CustomType` field in the return type. + +Refer to [Custom Types](/terraform/plugin/framework/handling-data/types/custom) for further details on creating provider-defined types and values. + +### Documentation + +Return documentation is expected in the top-level function documentation. Refer to [function documentation](/terraform/plugin/framework/functions/documentation) for information about the `Summary`, `Description`, and `MarkdownDescription` fields available. + +## Setting Return Data + +The [function implementation](/terraform/plugin/framework/functions/implementation) documentation covers the general methods for setting function return data in function logic. + +When setting the value for this return: + +* If `CustomType` is set, use its associated value type. +* Otherwise, use a Go map of string keys to values of an appropriate type to match the element type `map[string]T` or [framework map type](/terraform/plugin/framework/handling-data/types/map). + +In this example, a function defines a map of string return and sets its value: + +```go +func (f ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other Definition fields ... + Return: function.MapReturn{ + ElementType: types.StringType, + }, + } +} + +func (f ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + // ... other logic ... + + // hardcoded value for example brevity + result := map[string]string{ + "key1": "value1", + "key2": "value2", + } + + resp.Diagnostics.Append(resp.Result.Set(ctx, &result)...) +} +``` diff --git a/website/docs/plugin/framework/functions/returns/number.mdx b/website/docs/plugin/framework/functions/returns/number.mdx new file mode 100644 index 000000000..17c52a05a --- /dev/null +++ b/website/docs/plugin/framework/functions/returns/number.mdx @@ -0,0 +1,71 @@ +--- +page_title: 'Plugin Development - Framework: Number Function Return' +description: >- + Learn the number function return type in the provider development framework. +--- + +# Number Function Return + + + +Use [Float64 Return](/terraform/plugin/framework/functions/returns/float64) for 64-bit floating point numbers. Use [Int64 Return](/terraform/plugin/framework/functions/returns/int64) for 64-bit integer numbers. + + + +Number function return expects an arbitrary precision (generally over 64-bit, up to 512-bit) number value from function logic. Set values in function logic with the Go built-in `*big.Float` type or the [framework number type](/terraform/plugin/framework/handling-data/types/number). + +## Function Definition + +Use the [`function.NumberReturn` type](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#NumberReturn) in the [function definition](/terraform/plugin/framework/functions/implementation#definition-method). + +In this example, a function definition includes a number return: + +```go +func (f ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other Definition fields ... + Return: function.NumberReturn{ + // ... potentially other NumberReturn fields ... + }, + } +} +``` + +### Custom Types + +You may want to build your own data value and type implementations to allow your provider to combine validation and other behaviors into a reusable bundle. This helps avoid duplication and ensures consistency. These implementations use the `CustomType` field in the return type. + +Refer to [Custom Types](/terraform/plugin/framework/handling-data/types/custom) for further details on creating provider-defined types and values. + +### Documentation + +Return documentation is expected in the top-level function documentation. Refer to [function documentation](/terraform/plugin/framework/functions/documentation) for information about the `Summary`, `Description`, and `MarkdownDescription` fields available. + +## Setting Return Data + +The [function implementation](/terraform/plugin/framework/functions/implementation) documentation covers the general methods for setting function return data in function logic. + +When setting the value for this return: + +* If `CustomType` is set, use its associated value type. +* Otherwise, use the Go built-in `*big.Float` type or [framework number type](/terraform/plugin/framework/handling-data/types/number). + +In this example, a function defines a number return and sets its value: + +```go +func (f ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other Definition fields ... + Return: function.NumberReturn{}, + } +} + +func (f ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + // ... other logic ... + + // hardcoded value for example brevity + result := big.NewFloat(1.23) + + resp.Diagnostics.Append(resp.Result.Set(ctx, &result)...) +} +``` diff --git a/website/docs/plugin/framework/functions/returns/object.mdx b/website/docs/plugin/framework/functions/returns/object.mdx new file mode 100644 index 000000000..de70109f1 --- /dev/null +++ b/website/docs/plugin/framework/functions/returns/object.mdx @@ -0,0 +1,82 @@ +--- +page_title: 'Plugin Development - Framework: Object Function Return' +description: >- + Learn the object function return type in the provider development framework. +--- + +# Object Function Return + +Object function return expects a single structure mapping explicit attribute names to type definitions from function logic. Set values in function logic with a Go structure type annotated with `tfsdk` field tags or the [framework map type](/terraform/plugin/framework/handling-data/types/map). + +## Function Definition + +Use the [`function.ObjectReturn` type](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#ObjectReturn) in the [function definition](/terraform/plugin/framework/functions/implementation#definition-method). + +The `AttributeTypes` field must be defined, which represents a mapping of attribute names to [framework value types](/terraform/plugin/framework/handling-data/types). An attribute type may itself contain further collection or object types, if necessary. + +In this example, a function definition includes an object return: + +```go +func (f ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other Definition fields ... + Return: function.ObjectReturn{ + AttributeTypes: map[string]attr.Value{ + "attr1": types.StringType, + "attr2": types.Int64Type, + }, + // ... potentially other ObjectReturn fields ... + }, + } +} +``` + +### Custom Types + +You may want to build your own data value and type implementations to allow your provider to combine validation and other behaviors into a reusable bundle. This helps avoid duplication and ensures consistency. These implementations use the `CustomType` field in the return type. + +Refer to [Custom Types](/terraform/plugin/framework/handling-data/types/custom) for further details on creating provider-defined types and values. + +### Documentation + +Return documentation is expected in the top-level function documentation. Refer to [function documentation](/terraform/plugin/framework/functions/documentation) for information about the `Summary`, `Description`, and `MarkdownDescription` fields available. + +## Setting Return Data + +The [function implementation](/terraform/plugin/framework/functions/implementation) documentation covers the general methods for setting function return data in function logic. + +When setting the value for this return: + +* If `CustomType` is set, use its associated value type. +* Otherwise, use a Go structure type annotated with `tfsdk` field tags or [framework map type](/terraform/plugin/framework/handling-data/types/map). + +In this example, a function defines a map of string return and sets its value: + +```go +func (f ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other Definition fields ... + Return: function.ObjectReturn{ + AttributeTypes: map[string]attr.Value{ + "attr1": types.StringType, + "attr2": types.Int64Type, + }, + }, + } +} + +func (f ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + // ... other logic ... + + // hardcoded structure type and value for example brevity + result := struct{ + Attr1 string `tfsdk:"attr1"` + Attr2 int64 `tfsdk:"attr2"` + }{ + Attr1: "value1", + Attr2: 123, + } + + resp.Diagnostics.Append(resp.Result.Set(ctx, &result)...) +} +``` diff --git a/website/docs/plugin/framework/functions/returns/set.mdx b/website/docs/plugin/framework/functions/returns/set.mdx new file mode 100644 index 000000000..af6dac7dd --- /dev/null +++ b/website/docs/plugin/framework/functions/returns/set.mdx @@ -0,0 +1,70 @@ +--- +page_title: 'Plugin Development - Framework: Set Function Return' +description: >- + Learn the set function return type in the provider development framework. +--- + +# Set Function Return + +Set function return expects an unordered, unique collection of single element type value from function logic. Set values in function logic with a Go slice of an appropriate type to match the element type `[]T` or the [framework set type](/terraform/plugin/framework/handling-data/types/set). + +## Function Definition + +Use the [`function.SetReturn` type](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#SetReturn) in the [function definition](/terraform/plugin/framework/functions/implementation#definition-method). + +The `ElementType` field must be defined, which represents the single [framework value type](/terraform/plugin/framework/handling-data/types) of every element of the set. An element type may itself contain further collection or object types, if necessary. + +In this example, a function definition includes a set of string return: + +```go +func (f ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other Definition fields ... + Return: function.SetReturn{ + ElementType: types.StringType, + // ... potentially other SetReturn fields ... + }, + } +} +``` + +### Custom Types + +You may want to build your own data value and type implementations to allow your provider to combine validation and other behaviors into a reusable bundle. This helps avoid duplication and ensures consistency. These implementations use the `CustomType` field in the return type. + +Refer to [Custom Types](/terraform/plugin/framework/handling-data/types/custom) for further details on creating provider-defined types and values. + +### Documentation + +Return documentation is expected in the top-level function documentation. Refer to [function documentation](/terraform/plugin/framework/functions/documentation) for information about the `Summary`, `Description`, and `MarkdownDescription` fields available. + +## Setting Return Data + +The [function implementation](/terraform/plugin/framework/functions/implementation) documentation covers the general methods for setting function return data in function logic. + +When setting the value for this return: + +* If `CustomType` is set, use its associated value type. +* Otherwise, use a Go slice of an appropriate type to match the element type `[]T` or [framework set type](/terraform/plugin/framework/handling-data/types/set). + +In this example, a function defines a set of string return and sets its value: + +```go +func (f ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other Definition fields ... + Return: function.SetReturn{ + ElementType: types.StringType, + }, + } +} + +func (f ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + // ... other logic ... + + // hardcoded value for example brevity + result := []string{"one", "two"} + + resp.Diagnostics.Append(resp.Result.Set(ctx, &result)...) +} +``` diff --git a/website/docs/plugin/framework/functions/returns/string.mdx b/website/docs/plugin/framework/functions/returns/string.mdx new file mode 100644 index 000000000..f5f4e8fe3 --- /dev/null +++ b/website/docs/plugin/framework/functions/returns/string.mdx @@ -0,0 +1,65 @@ +--- +page_title: 'Plugin Development - Framework: String Function Return' +description: >- + Learn the string function return type in the provider development framework. +--- + +# String Function Return + +String function return expects a collection of UTF-8 encoded bytes from function logic. Set values in function logic with the Go built-in `string` type, Go built-in `*string` type, or the [framework string type](/terraform/plugin/framework/handling-data/types/string). + +## Function Definition + +Use the [`function.StringReturn` type](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/function#StringReturn) in the [function definition](/terraform/plugin/framework/functions/implementation#definition-method). + +In this example, a function definition includes a string return: + +```go +func (f ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other Definition fields ... + Return: function.StringReturn{ + // ... potentially other StringReturn fields ... + }, + } +} +``` + +### Custom Types + +You may want to build your own data value and type implementations to allow your provider to combine validation and other behaviors into a reusable bundle. This helps avoid duplication and ensures consistency. These implementations use the `CustomType` field in the return type. + +Refer to [Custom Types](/terraform/plugin/framework/handling-data/types/custom) for further details on creating provider-defined types and values. + +### Documentation + +Return documentation is expected in the top-level function documentation. Refer to [function documentation](/terraform/plugin/framework/functions/documentation) for information about the `Summary`, `Description`, and `MarkdownDescription` fields available. + +## Setting Return Data + +The [function implementation](/terraform/plugin/framework/functions/implementation) documentation covers the general methods for setting function return data in function logic. + +When setting the value for this return: + +* If `CustomType` is set, use its associated value type. +* Otherwise, use the Go built-in `string` type, Go built-in `*string` type, or [framework string type](/terraform/plugin/framework/handling-data/types/string). + +In this example, a function defines a string return and sets its value: + +```go +func (f ExampleFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + // ... other Definition fields ... + Return: function.StringReturn{}, + } +} + +func (f ExampleFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + // ... other logic ... + + // hardcoded value for example brevity + result := "example" + + resp.Diagnostics.Append(resp.Result.Set(ctx, &result)...) +} +``` diff --git a/website/docs/plugin/framework/functions/testing.mdx b/website/docs/plugin/framework/functions/testing.mdx new file mode 100644 index 000000000..2e20de799 --- /dev/null +++ b/website/docs/plugin/framework/functions/testing.mdx @@ -0,0 +1,231 @@ +--- +page_title: 'Plugin Development - Framework: Testing Functions' +description: >- + How to test provider-defined functions. +--- + +# Testing Functions + +When a function is [implemented](/terraform/plugin/framework/functions/implementation), ensure the function behaves as expected. Follow [recommendations](#recommendations) to cover how practitioner configurations may call the function. + +There are two methodologies for testing provider-defined functions: + +* [Acceptance Testing](#acceptance-testing): Verify implementation using real Terraform configurations and commands. +* [Unit Testing](#unit-testing): Verify implementation using with Terraform and framework implementation details. + +Similar to other provider concepts, many provider developers prefer acceptance testing over unit testing. Acceptance testing guarantees the function implementation works exactly as expected in real world use cases without trying to determine Terraform or framework implementation details. Unit testing details are provided, however, for function implementations which warrant a broad amount of input value testing, such as generic data handling functions or to perform [fuzzing](https://go.dev/security/fuzz/). + +Testing examples on this page are dependent on the example [echo function implementation](/terraform/plugin/framework/functions/implementation). + +## Recommendations + +Testing a provider-defined function should ensure at least the following behaviors are covered: + +* Known values return the expected results. +* 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. + +## Acceptance Testing + +Use the [plugin testing Go module](/terraform/plugin/testing) to implement real world testing with Terraform configurations and commands. The documentation for that Go module covers many more available testing features, however this section example gives a high level overview of how to start writing these tests. + +In this example, a `echo_function_test.go` file is created: + +```go +package provider_test + +import ( + "testing" + + "example.com/terraform-provider-example/internal/provider" + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestEchoFunction_Valid(t *testing.T) { + t.Parallel() + + resource.UnitTest(t, resource.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error) { + "example": providerserver.NewProtocol6WithError(provider.New()), + }, + Steps: []resource.TestStep{ + { + Config: ` +output "test" { + value = provider::example::echo("test-value") +} +`, + Check: resource.TestCheckOutput("test", "test-value"), + }, + }, + }) +} + +// The example implementation does not return any error diagnostics, however +// this acceptance test verifies how the function should behave if it did. +func TestEchoFunction_Invalid(t *testing.T) { + t.Parallel() + + resource.UnitTest(t, resource.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error) { + "example": providerserver.NewProtocol6WithError(provider.New()), + }, + Steps: []resource.TestStep{ + { + Config: ` +output "test" { + value = provider::example::echo("invalid") +} +`, + ExpectError: regexp.MustCompile(`error summary`), + }, + }, + }) +} + +// The example implementation does not enable AllowNullValue, however this +// acceptance test shows how to verify the behavior. +func TestEchoFunction_Null(t *testing.T) { + t.Parallel() + + resource.UnitTest(t, resource.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error) { + "example": providerserver.NewProtocol6WithError(provider.New()), + }, + Steps: []resource.TestStep{ + { + Config: ` +output "test" { + value = provider::example::echo(null) +} +`, + ExpectError: regexp.MustCompile(`Invalid Function Call`), + }, + }, + }) +} + +// The example implementation does not enable AllowUnknownValues, however this +// acceptance test shows how to verify the behavior. +func TestEchoFunction_Unknown(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error) { + "example": providerserver.NewProtocol6WithError(provider.New()), + }, + Steps: []resource.TestStep{ + { + Config: ` +terraform_data "test" { + input = "test-value" +} + +output "test" { + value = provider::example::echo(terraform_data.test.output) +} +`, + Check: resource.TestCheckOutput("test", "test-value"), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownOutputValue("test"), + }, + }, + }, + }, + }) +} +``` + +## Unit Testing + +Use the [`function.NewArgumentsData()` function](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/functions#NewArgumentsData) and [`function.NewResultData()` function](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/functions#NewResultData) as part of implementing a [Go test](https://go.dev/doc/tutorial/add-a-test). + +In this example, a `echo_function_test.go` file is created: + +```go +package provider_test + +import ( + "context" + "testing" + + "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" +) + +func TestEchoFunctionRun(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + request function.RunRequest + expected function.RunResponse + }{ + // The example implementation uses the Go built-in string type, however + // if AllowNullValue was enabled and *string or types.String was used, + // this test case shows how the function would be expected to behave. + "null": { + request: function.RunRequest{ + Arguments: function.NewArgumentsData(types.StringNull()), + }, + Expected: function.RunResponse{ + Result: function.NewResultData(types.StringNull()), + }, + }, + // The example implementation uses the Go built-in string type, however + // if AllowUnknownValues was enabled and types.String was used, + // this test case shows how the function would be expected to behave. + "unknown": { + request: function.RunRequest{ + Arguments: function.NewArgumentsData(types.StringUnknown()), + }, + Expected: function.RunResponse{ + Result: function.NewResultData(types.StringUnknown()), + }, + }, + "value-valid": { + request: function.RunRequest{ + Arguments: function.NewArgumentsData(types.StringValue("test-value")), + }, + Expected: function.RunResponse{ + Result: function.NewResultData(types.StringValue("test-value")), + }, + }, + // The example implementation does not return any diagnostics, however + // this test case shows how the function would be expected to behave if + // it did. + "value-invalid": { + request: function.RunRequest{ + Arguments: function.NewArgumentsData(types.StringValue()), + }, + Expected: function.RunResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewArgumentErrorDiagnostic(0, "error summary", "error detail"), + }, + Result: function.NewResultData(types.StringUnknown()), + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := function.RunResponse{} + + provider.EchoFunction{}.Run(context.Background(), testCase.request, &got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} +```