diff --git a/internal/command/jsonprovider/function.go b/internal/command/jsonprovider/function.go new file mode 100644 index 0000000000..b2140da6f4 --- /dev/null +++ b/internal/command/jsonprovider/function.go @@ -0,0 +1,114 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package jsonprovider + +import ( + "github.com/opentofu/opentofu/internal/providers" + "github.com/zclconf/go-cty/cty" +) + +const ( + mapTypeName = "map" + listTypeName = "list" + setTypeName = "set" + tupleTypeName = "tuple" +) + +// Function is the top-level object returned when exporting function schemas +type Function struct { + Description string `json:"description"` + Summary string `json:"summary"` + ReturnType any `json:"return_type"` + Parameters []*FunctionParam `json:"parameters,omitempty"` + VariadicParameter *FunctionParam `json:"variadic_parameter,omitempty"` +} + +// FunctionParam is the object for wrapping the functions parameters and return types +type FunctionParam struct { + Name string `json:"name"` + Description string `json:"description"` + Type any `json:"type"` + IsNullable *bool `json:"is_nullable,omitempty"` +} + +func marshalReturnType(returnType cty.Type) any { + switch { + case returnType.IsObjectType(): + return []any{ + returnType.FriendlyName(), + returnType.AttributeTypes(), + } + case returnType.IsListType(): + return []any{ + listTypeName, + returnType.ListElementType(), + } + case returnType.IsMapType(): + return []any{ + mapTypeName, + returnType.MapElementType(), + } + case returnType.IsSetType(): + return []any{ + setTypeName, + returnType.SetElementType(), + } + case returnType.IsTupleType(): + return []any{ + tupleTypeName, + returnType.TupleElementTypes(), + } + default: + return returnType.FriendlyName() + } +} + +func marshalParameter(parameter providers.FunctionParameterSpec) *FunctionParam { + var output FunctionParam + output.Description = parameter.Description + output.Name = parameter.Name + output.Type = marshalReturnType(parameter.Type) + + if parameter.AllowNullValue { + isNullable := true + output.IsNullable = &isNullable + } + + return &output +} + +func marshalParameters(parameters []providers.FunctionParameterSpec) []*FunctionParam { + output := make([]*FunctionParam, 0, len(parameters)) + for _, parameter := range parameters { + output = append(output, marshalParameter(parameter)) + } + + return output +} + +func marshalFunction(function providers.FunctionSpec) *Function { + var output Function + output.Description = function.Description + output.Summary = function.Summary + output.ReturnType = marshalReturnType(function.Return) + output.Parameters = marshalParameters(function.Parameters) + if function.VariadicParameter != nil { + output.VariadicParameter = marshalParameter(*function.VariadicParameter) + } + + return &output +} + +func marshalFunctions(functions map[string]providers.FunctionSpec) map[string]*Function { + if functions == nil { + return map[string]*Function{} + } + output := make(map[string]*Function, len(functions)) + for k, v := range functions { + output[k] = marshalFunction(v) + } + return output +} diff --git a/internal/command/jsonprovider/function_test.go b/internal/command/jsonprovider/function_test.go new file mode 100644 index 0000000000..754c823087 --- /dev/null +++ b/internal/command/jsonprovider/function_test.go @@ -0,0 +1,250 @@ +// Copyright (c) The OpenTofu Authors +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2023 HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package jsonprovider + +import ( + "encoding/json" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/opentofu/opentofu/internal/providers" + "github.com/zclconf/go-cty/cty" +) + +func TestMarshalReturnType(t *testing.T) { + type testcase struct { + Arg cty.Type + Expected any + } + + tests := map[string]testcase{ + "string": { + Arg: cty.String, + Expected: "string", + }, + "number": { + Arg: cty.Number, + Expected: "number", + }, + "bool": { + Arg: cty.Bool, + Expected: "bool", + }, + "object": { + Arg: cty.Object(map[string]cty.Type{"number_type": cty.Number}), + Expected: []any{ + string("object"), + map[string]cty.Type{"number_type": cty.Number}, + }, + }, + "map": { + Arg: cty.Map(cty.String), + Expected: []any{ + string("map"), + cty.String, + }, + }, + "list": { + Arg: cty.List(cty.Bool), + Expected: []any{ + string("list"), + cty.Bool, + }, + }, + "set": { + Arg: cty.Set(cty.Number), + Expected: []any{ + string("set"), + cty.Number, + }, + }, + "tuple": { + Arg: cty.Tuple([]cty.Type{cty.String}), + Expected: []any{ + string("tuple"), + []any{cty.String}, + }, + }, + } + + for tn, tc := range tests { + t.Run(tn, func(t *testing.T) { + actual := marshalReturnType(tc.Arg) + + // to avoid the nightmare of comparing cty primitve types we can marshal them to json and compare that + actualJSON, _ := json.Marshal(actual) + expectedJSON, _ := json.Marshal(tc.Expected) + if !cmp.Equal(actualJSON, expectedJSON) { + t.Fatalf("values don't match:\n %v\n", cmp.Diff(string(actualJSON), string(expectedJSON))) + } + }) + } +} + +func TestMarshalParameter(t *testing.T) { + // used so can make a pointer to it + trueBoolVal := true + + type testcase struct { + Arg providers.FunctionParameterSpec + Expected FunctionParam + } + + tests := map[string]testcase{ + "basic": { + Arg: providers.FunctionParameterSpec{ + Description: "basic string func", + Type: cty.String, + }, + Expected: FunctionParam{ + Description: "basic string func", + Type: cty.String, + }, + }, + "nullable": { + Arg: providers.FunctionParameterSpec{ + Description: "nullable number func", + Type: cty.Number, + AllowNullValue: trueBoolVal, + }, + Expected: FunctionParam{ + Description: "nullable number func", + Type: cty.Number, + IsNullable: &trueBoolVal, + }, + }, + } + + for tn, tc := range tests { + t.Run(tn, func(t *testing.T) { + actual := marshalParameter(tc.Arg) + + // to avoid the nightmare of comparing cty primitve types we can marshal them to json and compare that + actualJSON, _ := json.Marshal(actual) + expectedJSON, _ := json.Marshal(tc.Expected) + if !cmp.Equal(actualJSON, expectedJSON) { + t.Fatalf("values don't match:\n %v\n", cmp.Diff(string(actualJSON), string(expectedJSON))) + } + }) + } +} + +func TestMarshalParameters(t *testing.T) { + type testcase struct { + Arg []providers.FunctionParameterSpec + Expected []FunctionParam + } + + tests := map[string]testcase{ + "basic": { + Arg: []providers.FunctionParameterSpec{{ + Description: "basic string func", + Type: cty.String, + }}, + Expected: []FunctionParam{{ + Description: "basic string func", + Type: cty.String, + }}, + }, + } + + for tn, tc := range tests { + t.Run(tn, func(t *testing.T) { + actual := marshalParameters(tc.Arg) + + // to avoid the nightmare of comparing cty primitve types we can marshal them to json and compare that + actualJSON, _ := json.Marshal(actual) + expectedJSON, _ := json.Marshal(tc.Expected) + if !cmp.Equal(actualJSON, expectedJSON) { + t.Fatalf("values don't match:\n %v\n", cmp.Diff(string(actualJSON), string(expectedJSON))) + } + }) + } +} + +func TestMarshalFunction(t *testing.T) { + type testcase struct { + Arg providers.FunctionSpec + Expected Function + } + + tests := map[string]testcase{ + "basic": { + Arg: providers.FunctionSpec{ + Description: "basic string func", + Return: cty.String, + }, + Expected: Function{ + Description: "basic string func", + ReturnType: cty.String, + }, + }, + "variadic": { + Arg: providers.FunctionSpec{ + Description: "basic string func", + Return: cty.String, + VariadicParameter: &providers.FunctionParameterSpec{ + Description: "basic string func", + Type: cty.String, + }, + }, + Expected: Function{ + Description: "basic string func", + ReturnType: cty.String, + VariadicParameter: &FunctionParam{ + Description: "basic string func", + Type: cty.String, + }, + }, + }, + } + + for tn, tc := range tests { + t.Run(tn, func(t *testing.T) { + actual := marshalFunction(tc.Arg) + + // to avoid the nightmare of comparing cty primitve types we can marshal them to json and compare that + actualJSON, _ := json.Marshal(actual) + expectedJSON, _ := json.Marshal(tc.Expected) + if !cmp.Equal(actualJSON, expectedJSON) { + t.Fatalf("values don't match:\n %v\n", cmp.Diff(string(actualJSON), string(expectedJSON))) + } + }) + } +} + +func TestMarshalFunctions(t *testing.T) { + type testcase struct { + Arg map[string]providers.FunctionSpec + Expected map[string]Function + } + + tests := map[string]testcase{ + "basic": { + Arg: map[string]providers.FunctionSpec{"basic_func": { + Description: "basic string func", + Return: cty.String, + }}, + Expected: map[string]Function{"basic_func": { + Description: "basic string func", + ReturnType: cty.String, + }}, + }, + } + + for tn, tc := range tests { + t.Run(tn, func(t *testing.T) { + actual := marshalFunctions(tc.Arg) + + // to avoid the nightmare of comparing cty primitve types we can marshal them to json and compare that + actualJSON, _ := json.Marshal(actual) + expectedJSON, _ := json.Marshal(tc.Expected) + if !cmp.Equal(actualJSON, expectedJSON) { + t.Fatalf("values don't match:\n %v\n", cmp.Diff(string(actualJSON), string(expectedJSON))) + } + }) + } +} diff --git a/internal/command/jsonprovider/provider.go b/internal/command/jsonprovider/provider.go index dc28d8c129..e3c2190005 100644 --- a/internal/command/jsonprovider/provider.go +++ b/internal/command/jsonprovider/provider.go @@ -24,9 +24,10 @@ type Providers struct { } type Provider struct { - Provider *Schema `json:"provider,omitempty"` - ResourceSchemas map[string]*Schema `json:"resource_schemas,omitempty"` - DataSourceSchemas map[string]*Schema `json:"data_source_schemas,omitempty"` + Provider *Schema `json:"provider,omitempty"` + ResourceSchemas map[string]*Schema `json:"resource_schemas,omitempty"` + DataSourceSchemas map[string]*Schema `json:"data_source_schemas,omitempty"` + Functions map[string]*Function `json:"functions,omitempty"` } func newProviders() *Providers { @@ -61,5 +62,6 @@ func marshalProvider(tps providers.ProviderSchema) *Provider { Provider: marshalSchema(tps.Provider), ResourceSchemas: marshalSchemas(tps.ResourceTypes), DataSourceSchemas: marshalSchemas(tps.DataSources), + Functions: marshalFunctions(tps.Functions), } } diff --git a/internal/command/jsonprovider/provider_test.go b/internal/command/jsonprovider/provider_test.go index 5634446ccd..b73aeb6a9e 100644 --- a/internal/command/jsonprovider/provider_test.go +++ b/internal/command/jsonprovider/provider_test.go @@ -28,6 +28,7 @@ func TestMarshalProvider(t *testing.T) { Provider: &Schema{}, ResourceSchemas: map[string]*Schema{}, DataSourceSchemas: map[string]*Schema{}, + Functions: map[string]*Function{}, }, }, { @@ -146,6 +147,7 @@ func TestMarshalProvider(t *testing.T) { }, }, }, + Functions: map[string]*Function{}, }, }, } @@ -223,5 +225,6 @@ func testProvider() providers.ProviderSchema { }, }, }, + Functions: map[string]providers.FunctionSpec{}, } } diff --git a/internal/command/providers_schema_test.go b/internal/command/providers_schema_test.go index f6dcf922fa..e0eb128f85 100644 --- a/internal/command/providers_schema_test.go +++ b/internal/command/providers_schema_test.go @@ -114,6 +114,7 @@ type providerSchema struct { Provider interface{} `json:"provider,omitempty"` ResourceSchemas map[string]interface{} `json:"resource_schemas,omitempty"` DataSourceSchemas map[string]interface{} `json:"data_source_schemas,omitempty"` + Functions map[string]interface{} `json:"functions,omitempty"` } // testProvider returns a mock provider that is configured for basic @@ -155,5 +156,20 @@ func providersSchemaFixtureSchema() *providers.GetProviderSchemaResponse { }, }, }, + Functions: map[string]providers.FunctionSpec{ + "test_func": { + Description: "a basic string function", + Return: cty.String, + Summary: "test", + Parameters: []providers.FunctionParameterSpec{{ + Name: "input", + Type: cty.Number, + }}, + VariadicParameter: &providers.FunctionParameterSpec{ + Name: "variadic_input", + Type: cty.List(cty.Bool), + }, + }, + }, } } diff --git a/internal/command/testdata/providers-schema/basic/output.json b/internal/command/testdata/providers-schema/basic/output.json index 59caf0dce5..579e49e174 100644 --- a/internal/command/testdata/providers-schema/basic/output.json +++ b/internal/command/testdata/providers-schema/basic/output.json @@ -54,7 +54,29 @@ "description_kind": "plain" } } + }, + "functions": { + "test_func": { + "description": "a basic string function", + "summary": "test", + "return_type": "string", + "parameters": [ + { + "name": "input", + "description": "", + "type": "number" + } + ], + "variadic_parameter": { + "name": "variadic_input", + "description": "", + "type": [ + "list", + "bool" + ] + } + } } } } -} +} \ No newline at end of file diff --git a/internal/command/testdata/providers-schema/required/output.json b/internal/command/testdata/providers-schema/required/output.json index 59caf0dce5..579e49e174 100644 --- a/internal/command/testdata/providers-schema/required/output.json +++ b/internal/command/testdata/providers-schema/required/output.json @@ -54,7 +54,29 @@ "description_kind": "plain" } } + }, + "functions": { + "test_func": { + "description": "a basic string function", + "summary": "test", + "return_type": "string", + "parameters": [ + { + "name": "input", + "description": "", + "type": "number" + } + ], + "variadic_parameter": { + "name": "variadic_input", + "description": "", + "type": [ + "list", + "bool" + ] + } + } } } } -} +} \ No newline at end of file