Adds provider function to tofu scheme/metadata commands (#1753)

Signed-off-by: Andrew Hayes <andrew.hayes@harness.io>
This commit is contained in:
Andrew Hayes 2024-07-01 16:41:27 +01:00 committed by GitHub
parent edc654c1de
commit 7e706fa1a7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 434 additions and 5 deletions

View File

@ -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
}

View File

@ -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)))
}
})
}
}

View File

@ -24,9 +24,10 @@ type Providers struct {
} }
type Provider struct { type Provider struct {
Provider *Schema `json:"provider,omitempty"` Provider *Schema `json:"provider,omitempty"`
ResourceSchemas map[string]*Schema `json:"resource_schemas,omitempty"` ResourceSchemas map[string]*Schema `json:"resource_schemas,omitempty"`
DataSourceSchemas map[string]*Schema `json:"data_source_schemas,omitempty"` DataSourceSchemas map[string]*Schema `json:"data_source_schemas,omitempty"`
Functions map[string]*Function `json:"functions,omitempty"`
} }
func newProviders() *Providers { func newProviders() *Providers {
@ -61,5 +62,6 @@ func marshalProvider(tps providers.ProviderSchema) *Provider {
Provider: marshalSchema(tps.Provider), Provider: marshalSchema(tps.Provider),
ResourceSchemas: marshalSchemas(tps.ResourceTypes), ResourceSchemas: marshalSchemas(tps.ResourceTypes),
DataSourceSchemas: marshalSchemas(tps.DataSources), DataSourceSchemas: marshalSchemas(tps.DataSources),
Functions: marshalFunctions(tps.Functions),
} }
} }

View File

@ -28,6 +28,7 @@ func TestMarshalProvider(t *testing.T) {
Provider: &Schema{}, Provider: &Schema{},
ResourceSchemas: map[string]*Schema{}, ResourceSchemas: map[string]*Schema{},
DataSourceSchemas: 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{},
} }
} }

View File

@ -114,6 +114,7 @@ type providerSchema struct {
Provider interface{} `json:"provider,omitempty"` Provider interface{} `json:"provider,omitempty"`
ResourceSchemas map[string]interface{} `json:"resource_schemas,omitempty"` ResourceSchemas map[string]interface{} `json:"resource_schemas,omitempty"`
DataSourceSchemas map[string]interface{} `json:"data_source_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 // 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),
},
},
},
} }
} }

View File

@ -54,7 +54,29 @@
"description_kind": "plain" "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"
]
}
}
} }
} }
} }
} }

View File

@ -54,7 +54,29 @@
"description_kind": "plain" "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"
]
}
}
} }
} }
} }
} }