| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package jsonfunction | |
| import ( | |
| "encoding/json" | |
| "fmt" | |
| "testing" | |
| "github.com/google/go-cmp/cmp" | |
| "github.com/hashicorp/terraform/internal/providers" | |
| "github.com/zclconf/go-cty/cty" | |
| "github.com/zclconf/go-cty/cty/function" | |
| ) | |
| func TestMarshal(t *testing.T) { | |
| tests := []struct { | |
| Name string | |
| Functions map[string]function.Function | |
| ProviderFunctions map[string]providers.FunctionDecl | |
| Want string | |
| WantErr string | |
| }{ | |
| { | |
| Name: "minimal function", | |
| Functions: map[string]function.Function{ | |
| "fun": function.New(&function.Spec{ | |
| Type: function.StaticReturnType(cty.Bool), | |
| }), | |
| }, | |
| ProviderFunctions: map[string]providers.FunctionDecl{ | |
| "fun": { | |
| ReturnType: cty.Bool, | |
| }, | |
| }, | |
| Want: `{"format_version":"1.0","function_signatures":{"fun":{"return_type":"bool"}}}`, | |
| }, | |
| { | |
| Name: "function with description", | |
| Functions: map[string]function.Function{ | |
| "fun": function.New(&function.Spec{ | |
| Description: "`timestamp` returns a UTC timestamp string.", | |
| Type: function.StaticReturnType(cty.String), | |
| }), | |
| }, | |
| ProviderFunctions: map[string]providers.FunctionDecl{ | |
| "fun": { | |
| Description: "`timestamp` returns a UTC timestamp string.", | |
| ReturnType: cty.String, | |
| }, | |
| }, | |
| Want: "{\"format_version\":\"1.0\",\"function_signatures\":{\"fun\":{\"description\":\"`timestamp` returns a UTC timestamp string.\",\"return_type\":\"string\"}}}", | |
| }, | |
| { | |
| Name: "function with parameters", | |
| Functions: map[string]function.Function{ | |
| "fun": function.New(&function.Spec{ | |
| Params: []function.Parameter{ | |
| { | |
| Name: "timestamp", | |
| Description: "timestamp text", | |
| Type: cty.String, | |
| }, | |
| { | |
| Name: "duration", | |
| Description: "duration text", | |
| Type: cty.String, | |
| }, | |
| }, | |
| Type: function.StaticReturnType(cty.String), | |
| }), | |
| }, | |
| ProviderFunctions: map[string]providers.FunctionDecl{ | |
| "fun": { | |
| Parameters: []providers.FunctionParam{ | |
| { | |
| Name: "timestamp", | |
| Description: "timestamp text", | |
| Type: cty.String, | |
| }, | |
| { | |
| Name: "duration", | |
| Description: "duration text", | |
| Type: cty.String, | |
| }, | |
| }, | |
| ReturnType: cty.String, | |
| }, | |
| }, | |
| Want: `{"format_version":"1.0","function_signatures":{"fun":{"return_type":"string","parameters":[{"name":"timestamp","description":"timestamp text","type":"string"},{"name":"duration","description":"duration text","type":"string"}]}}}`, | |
| }, | |
| { | |
| Name: "function with variadic parameter", | |
| Functions: map[string]function.Function{ | |
| "fun": function.New(&function.Spec{ | |
| VarParam: &function.Parameter{ | |
| Name: "default", | |
| Description: "default description", | |
| Type: cty.DynamicPseudoType, | |
| AllowUnknown: true, | |
| AllowDynamicType: true, | |
| AllowNull: true, | |
| AllowMarked: true, | |
| }, | |
| Type: function.StaticReturnType(cty.DynamicPseudoType), | |
| }), | |
| }, | |
| ProviderFunctions: map[string]providers.FunctionDecl{ | |
| "fun": { | |
| VariadicParameter: &providers.FunctionParam{ | |
| Name: "default", | |
| Description: "default description", | |
| Type: cty.DynamicPseudoType, | |
| AllowUnknownValues: true, | |
| AllowNullValue: true, | |
| }, | |
| ReturnType: cty.DynamicPseudoType, | |
| }, | |
| }, | |
| Want: `{"format_version":"1.0","function_signatures":{"fun":{"return_type":"dynamic","variadic_parameter":{"name":"default","description":"default description","is_nullable":true,"type":"dynamic"}}}}`, | |
| }, | |
| { | |
| Name: "function with list types", | |
| Functions: map[string]function.Function{ | |
| "fun": function.New(&function.Spec{ | |
| Params: []function.Parameter{ | |
| { | |
| Name: "list", | |
| Type: cty.List(cty.String), | |
| }, | |
| }, | |
| Type: function.StaticReturnType(cty.List(cty.String)), | |
| }), | |
| }, | |
| ProviderFunctions: map[string]providers.FunctionDecl{ | |
| "fun": { | |
| Parameters: []providers.FunctionParam{ | |
| { | |
| Name: "list", | |
| Type: cty.List(cty.String), | |
| }, | |
| }, | |
| ReturnType: cty.List(cty.String), | |
| }, | |
| }, | |
| Want: `{"format_version":"1.0","function_signatures":{"fun":{"return_type":["list","string"],"parameters":[{"name":"list","type":["list","string"]}]}}}`, | |
| }, | |
| { | |
| Name: "returns diagnostics on failure", | |
| Functions: map[string]function.Function{ | |
| "fun": function.New(&function.Spec{ | |
| Params: []function.Parameter{}, | |
| Type: func(args []cty.Value) (ret cty.Type, err error) { | |
| return cty.DynamicPseudoType, fmt.Errorf("error") | |
| }, | |
| }), | |
| }, | |
| WantErr: "Failed to serialize function \"fun\": error", | |
| }, | |
| } | |
| for i, test := range tests { | |
| t.Run(fmt.Sprintf("%d-%s", i, test.Name), func(t *testing.T) { | |
| got, diags := Marshal(test.Functions) | |
| if test.WantErr != "" { | |
| if !diags.HasErrors() { | |
| t.Fatal("expected error, got none") | |
| } | |
| if diags.Err().Error() != test.WantErr { | |
| t.Fatalf("expected error %q, got %q", test.WantErr, diags.Err()) | |
| } | |
| } else { | |
| if diags.HasErrors() { | |
| t.Fatal(diags) | |
| } | |
| if diff := cmp.Diff(test.Want, string(got)); diff != "" { | |
| t.Fatalf("mismatch of function signature: %s", diff) | |
| } | |
| } | |
| if test.ProviderFunctions != nil { | |
| // Provider functions should marshal identically to cty | |
| // functions, without the wrapping object. | |
| got := MarshalProviderFunctions(test.ProviderFunctions) | |
| gotBytes, err := json.Marshal(got) | |
| if err != nil { | |
| // these should never error | |
| t.Fatal("Marshal of ProviderFunctions failed:", err) | |
| } | |
| var want functions | |
| err = json.Unmarshal([]byte(test.Want), &want) | |
| if err != nil { | |
| // these should never error | |
| t.Fatal("Unmarshal of Want failed:", err) | |
| } | |
| wantBytes, err := json.Marshal(want.Signatures) | |
| if err != nil { | |
| // these should never error | |
| t.Fatal("Marshal of Want.Signatures failed:", err) | |
| } | |
| if diff := cmp.Diff(string(wantBytes), string(gotBytes)); diff != "" { | |
| t.Fatalf("mismatch of function signature: %s", diff) | |
| } | |
| } | |
| }) | |
| } | |
| } |