Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce metadata functions command #32487

Merged
merged 15 commits into from
Feb 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,18 @@ func initCommands(
}, nil
},

"metadata": func() (cli.Command, error) {
return &command.MetadataCommand{
Meta: meta,
}, nil
},

"metadata functions": func() (cli.Command, error) {
return &command.MetadataFunctionsCommand{
Meta: meta,
}, nil
},

"output": func() (cli.Command, error) {
return &command.OutputCommand{
Meta: meta,
Expand Down
145 changes: 145 additions & 0 deletions internal/command/jsonfunction/function.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package jsonfunction

import (
"encoding/json"
"fmt"

"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
)

// FormatVersion represents the version of the json format and will be
// incremented for any change to this format that requires changes to a
// consuming parser.
const FormatVersion = "1.0"

// functions is the top-level object returned when exporting function signatures
type functions struct {
FormatVersion string `json:"format_version"`
Signatures map[string]*FunctionSignature `json:"function_signatures,omitempty"`
}

// FunctionSignature represents a function signature.
type FunctionSignature struct {
// Description is an optional human-readable description
// of the function
Description string `json:"description,omitempty"`

// ReturnTypes is the ctyjson representation of the function's
// return types based on supplying all parameters using
// dynamic types. Functions can have dynamic return types.
ReturnType cty.Type `json:"return_type"`

// Parameters describes the function's fixed positional parameters.
Parameters []*parameter `json:"parameters,omitempty"`

// VariadicParameter describes the function's variadic
// parameters, if any are supported.
VariadicParameter *parameter `json:"variadic_parameter,omitempty"`
}

func newFunctions() *functions {
signatures := make(map[string]*FunctionSignature)
return &functions{
FormatVersion: FormatVersion,
Signatures: signatures,
}
}

func Marshal(f map[string]function.Function) ([]byte, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
signatures := newFunctions()

for name, v := range f {
if name == "can" {
signatures.Signatures[name] = marshalCan(v)
} else if name == "try" {
signatures.Signatures[name] = marshalTry(v)
} else {
signature, err := marshalFunction(v)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
fmt.Sprintf("Failed to serialize function %q", name),
err.Error(),
))
}
signatures.Signatures[name] = signature
}
}

if diags.HasErrors() {
return nil, diags
}

ret, err := json.Marshal(signatures)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to serialize functions",
err.Error(),
))
return nil, diags
}
return ret, nil
}

func marshalFunction(f function.Function) (*FunctionSignature, error) {
var err error
var vp *parameter
if f.VarParam() != nil {
vp = marshalParameter(f.VarParam())
}

var p []*parameter
if len(f.Params()) > 0 {
p = marshalParameters(f.Params())
}

r, err := getReturnType(f)
if err != nil {
return nil, err
}

return &FunctionSignature{
Description: f.Description(),
ReturnType: r,
Parameters: p,
VariadicParameter: vp,
}, nil
}

// marshalTry returns a static function signature for the try function.
// We need this exception because the function implementation uses capsule
// types that we can't marshal.
func marshalTry(try function.Function) *FunctionSignature {
return &FunctionSignature{
Description: try.Description(),
ReturnType: cty.DynamicPseudoType,
VariadicParameter: &parameter{
Name: try.VarParam().Name,
Description: try.VarParam().Description,
IsNullable: try.VarParam().AllowNull,
Type: cty.DynamicPseudoType,
},
}
}

// marshalCan returns a static function signature for the can function.
// We need this exception because the function implementation uses capsule
// types that we can't marshal.
func marshalCan(can function.Function) *FunctionSignature {
return &FunctionSignature{
Description: can.Description(),
ReturnType: cty.Bool,
Parameters: []*parameter{
{
Name: can.Params()[0].Name,
Description: can.Params()[0].Description,
IsNullable: can.Params()[0].AllowNull,
Type: cty.DynamicPseudoType,
},
},
}
}
134 changes: 134 additions & 0 deletions internal/command/jsonfunction/function_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package jsonfunction

import (
"fmt"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/zclconf/go-cty-debug/ctydebug"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
)

func TestMarshal(t *testing.T) {
tests := []struct {
Name string
Input map[string]function.Function
Want string
WantErr string
}{
{
"minimal function",
map[string]function.Function{
"fun": function.New(&function.Spec{
Type: function.StaticReturnType(cty.Bool),
}),
},
`{"format_version":"1.0","function_signatures":{"fun":{"return_type":"bool"}}}`,
"",
},
{
"function with description",
map[string]function.Function{
"fun": function.New(&function.Spec{
Description: "`timestamp` returns a UTC timestamp string.",
Type: function.StaticReturnType(cty.String),
}),
},
"{\"format_version\":\"1.0\",\"function_signatures\":{\"fun\":{\"description\":\"`timestamp` returns a UTC timestamp string.\",\"return_type\":\"string\"}}}",
"",
},
{
"function with parameters",
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),
}),
},
`{"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"}]}}}`,
"",
},
{
"function with variadic parameter",
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),
}),
},
`{"format_version":"1.0","function_signatures":{"fun":{"return_type":"dynamic","variadic_parameter":{"name":"default","description":"default description","is_nullable":true,"type":"dynamic"}}}}`,
"",
},
{
"function with list types",
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)),
}),
},
`{"format_version":"1.0","function_signatures":{"fun":{"return_type":["list","string"],"parameters":[{"name":"list","type":["list","string"]}]}}}`,
"",
},
{
"returns diagnostics on failure",
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")
},
}),
},
"",
"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.Input)
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), ctydebug.CmpOptions); diff != "" {
t.Fatalf("mismatch of function signature: %s", diff)
}
}
})
}
}
kmoe marked this conversation as resolved.
Show resolved Hide resolved
43 changes: 43 additions & 0 deletions internal/command/jsonfunction/parameter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package jsonfunction

import (
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
)

// parameter represents a parameter to a function.
type parameter struct {
// Name is an optional name for the argument.
Name string `json:"name,omitempty"`

// Description is an optional human-readable description
// of the argument
Description string `json:"description,omitempty"`

// IsNullable is true if null is acceptable value for the argument
IsNullable bool `json:"is_nullable,omitempty"`

// A type that any argument for this parameter must conform to.
Type cty.Type `json:"type"`
}

func marshalParameter(p *function.Parameter) *parameter {
if p == nil {
return &parameter{}
}

return &parameter{
Name: p.Name,
Description: p.Description,
IsNullable: p.AllowNull,
Type: p.Type,
}
}

func marshalParameters(parameters []function.Parameter) []*parameter {
ret := make([]*parameter, len(parameters))
for k, p := range parameters {
ret[k] = marshalParameter(&p)
}
return ret
}
Loading