| // Copyright (c) HashiCorp, Inc. |
| // 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) |
| } |
| } |
| }) |
| } |
| } |