Skip to content

Commit

Permalink
function: Allow overriding function descriptions
Browse files Browse the repository at this point in the history
Callers may wish to use the functions implementations provided in stdlib
but need to customize the descriptions in some way. This new method allows
deriving a new function which has the same signature and behavior but
has different description strings.
  • Loading branch information
apparentlymart committed Oct 27, 2022
1 parent 0e3fb70 commit 4566e66
Show file tree
Hide file tree
Showing 2 changed files with 338 additions and 0 deletions.
54 changes: 54 additions & 0 deletions cty/function/function.go
Original file line number Diff line number Diff line change
Expand Up @@ -352,3 +352,57 @@ func (f Function) VarParam() *Parameter {
func (f Function) Description() string {
return f.spec.Description
}

// WithNewDescriptions returns a new function that has the same signature
// and implementation as the receiver but has the function description and
// the parameter descriptions replaced with those given in the arguments.
//
// All descriptions may be given as an empty string to specify that there
// should be no description at all.
//
// The paramDescs argument must match the number of parameters
// the reciever expects, or this function will panic. If the function has a
// VarParam then that counts as one parameter for the sake of this rule. The
// given descriptions will be assigned in order starting with the positional
// arguments in their declared order, followed by the variadic parameter if
// any.
//
// As a special case, WithNewDescriptions will accept a paramDescs which
// does not cover the reciever's variadic parameter (if any), so that it's
// possible to add a variadic parameter to a function which didn't previously
// have one without that being a breaking change for an existing caller using
// WithNewDescriptions against that function. In this case the base description
// of the variadic parameter will be preserved.
func (f Function) WithNewDescriptions(funcDesc string, paramDescs []string) Function {
retSpec := *f.spec // shallow copy of the reciever
retSpec.Description = funcDesc

retSpec.Params = make([]Parameter, len(f.spec.Params))
copy(retSpec.Params, f.spec.Params) // shallow copy of positional parameters
if f.spec.VarParam != nil {
retVarParam := *f.spec.VarParam // shallow copy of variadic parameter
retSpec.VarParam = &retVarParam
}

if retSpec.VarParam != nil {
if with, without := len(retSpec.Params)+1, len(retSpec.Params); len(paramDescs) != with && len(paramDescs) != without {
panic(fmt.Sprintf("paramDescs must have length of either %d or %d", with, without))
}
} else {
if want := len(retSpec.Params); len(paramDescs) != want {
panic(fmt.Sprintf("paramDescs must have length %d", want))
}
}

posParamDescs := paramDescs[:len(retSpec.Params)]
varParamDescs := paramDescs[len(retSpec.Params):] // guaranteed to be zero or one elements because of the rules above

for i, desc := range posParamDescs {
retSpec.Params[i].Description = desc
}
for _, desc := range varParamDescs {
retSpec.VarParam.Description = desc
}

return New(&retSpec)
}
284 changes: 284 additions & 0 deletions cty/function/function_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,290 @@ func TestReturnTypeForValues(t *testing.T) {
}
}

func TestFunctionWithNewDescriptions(t *testing.T) {
t.Run("no params", func(t *testing.T) {
f1 := New(&Spec{
Description: "old func",
Params: []Parameter{},
Type: stubType,
Impl: stubImpl,
})
f2 := f1.WithNewDescriptions(
"new func",
nil,
)

if got, want := f1.Description(), "old func"; got != want {
t.Errorf("wrong original func description\ngot: %s\nwant: %s", got, want)
}
if got, want := f2.Description(), "new func"; got != want {
t.Errorf("wrong updated func description\ngot: %s\nwant: %s", got, want)
}
})
t.Run("one pos param", func(t *testing.T) {
f1 := New(&Spec{
Description: "old func",
Params: []Parameter{
{
Name: "a",
Description: "old a",
},
},
Type: stubType,
Impl: stubImpl,
})
f2 := f1.WithNewDescriptions(
"new func",
[]string{"new a"},
)

if got, want := f1.Description(), "old func"; got != want {
t.Errorf("wrong original func description\ngot: %s\nwant: %s", got, want)
}
if got, want := f2.Description(), "new func"; got != want {
t.Errorf("wrong updated func description\ngot: %s\nwant: %s", got, want)
}

if got, want := len(f1.Params()), 1; got != want {
t.Fatalf("wrong original param count\ngot: %d\nwant: %d", got, want)
}
if got, want := len(f2.Params()), 1; got != want {
t.Fatalf("wrong updated param count\ngot: %d\nwant: %d", got, want)
}
if got, want := f1.Params()[0].Description, "old a"; got != want {
t.Errorf("wrong original param a description\ngot: %s\nwant: %s", got, want)
}
if got, want := f2.Params()[0].Description, "new a"; got != want {
t.Errorf("wrong updated param a description\ngot: %s\nwant: %s", got, want)
}
})
t.Run("two pos params", func(t *testing.T) {
f1 := New(&Spec{
Description: "old func",
Params: []Parameter{
{
Name: "a",
Description: "old a",
},
{
Name: "b",
Description: "old b",
},
},
Type: stubType,
Impl: stubImpl,
})
f2 := f1.WithNewDescriptions(
"new func",
[]string{"new a", "new b"},
)

if got, want := f1.Description(), "old func"; got != want {
t.Errorf("wrong original func description\ngot: %s\nwant: %s", got, want)
}
if got, want := f2.Description(), "new func"; got != want {
t.Errorf("wrong updated func description\ngot: %s\nwant: %s", got, want)
}

if got, want := len(f1.Params()), 2; got != want {
t.Fatalf("wrong original param count\ngot: %d\nwant: %d", got, want)
}
if got, want := len(f2.Params()), 2; got != want {
t.Fatalf("wrong updated param count\ngot: %d\nwant: %d", got, want)
}
if got, want := f1.Params()[0].Description, "old a"; got != want {
t.Errorf("wrong original param a description\ngot: %s\nwant: %s", got, want)
}
if got, want := f2.Params()[0].Description, "new a"; got != want {
t.Errorf("wrong updated param a description\ngot: %s\nwant: %s", got, want)
}
if got, want := f1.Params()[1].Description, "old b"; got != want {
t.Errorf("wrong original param b description\ngot: %s\nwant: %s", got, want)
}
if got, want := f2.Params()[1].Description, "new b"; got != want {
t.Errorf("wrong updated param b description\ngot: %s\nwant: %s", got, want)
}
})
t.Run("varparam overridden", func(t *testing.T) {
f1 := New(&Spec{
Description: "old func",
Params: []Parameter{
{
Name: "a",
Description: "old a",
},
},
VarParam: &Parameter{
Name: "b",
Description: "old b",
},
Type: stubType,
Impl: stubImpl,
})
f2 := f1.WithNewDescriptions(
"new func",
[]string{"new a", "new b"},
)

if got, want := f1.Description(), "old func"; got != want {
t.Errorf("wrong original func description\ngot: %s\nwant: %s", got, want)
}
if got, want := f2.Description(), "new func"; got != want {
t.Errorf("wrong updated func description\ngot: %s\nwant: %s", got, want)
}

if got, want := len(f1.Params()), 1; got != want {
t.Fatalf("wrong original param count\ngot: %d\nwant: %d", got, want)
}
if got, want := len(f2.Params()), 1; got != want {
t.Fatalf("wrong updated param count\ngot: %d\nwant: %d", got, want)
}
if got, want := f1.Params()[0].Description, "old a"; got != want {
t.Errorf("wrong original param a description\ngot: %s\nwant: %s", got, want)
}
if got, want := f2.Params()[0].Description, "new a"; got != want {
t.Errorf("wrong updated param a description\ngot: %s\nwant: %s", got, want)
}
if got, want := f1.VarParam().Description, "old b"; got != want {
t.Errorf("wrong original param b description\ngot: %s\nwant: %s", got, want)
}
if got, want := f2.VarParam().Description, "new b"; got != want {
t.Errorf("wrong updated param b description\ngot: %s\nwant: %s", got, want)
}
})
t.Run("varparam not overridden", func(t *testing.T) {
f1 := New(&Spec{
Description: "old func",
Params: []Parameter{
{
Name: "a",
Description: "old a",
},
},
VarParam: &Parameter{
Name: "b",
Description: "old b",
},
Type: stubType,
Impl: stubImpl,
})
f2 := f1.WithNewDescriptions(
"new func",
[]string{"new a"},
)

if got, want := f1.Description(), "old func"; got != want {
t.Errorf("wrong original func description\ngot: %s\nwant: %s", got, want)
}
if got, want := f2.Description(), "new func"; got != want {
t.Errorf("wrong updated func description\ngot: %s\nwant: %s", got, want)
}

if got, want := len(f1.Params()), 1; got != want {
t.Fatalf("wrong original param count\ngot: %d\nwant: %d", got, want)
}
if got, want := len(f2.Params()), 1; got != want {
t.Fatalf("wrong updated param count\ngot: %d\nwant: %d", got, want)
}
if got, want := f1.Params()[0].Description, "old a"; got != want {
t.Errorf("wrong original param a description\ngot: %s\nwant: %s", got, want)
}
if got, want := f2.Params()[0].Description, "new a"; got != want {
t.Errorf("wrong updated param a description\ngot: %s\nwant: %s", got, want)
}
if got, want := f1.VarParam().Description, "old b"; got != want {
t.Errorf("wrong original param b description\ngot: %s\nwant: %s", got, want)
}
if got, want := f2.VarParam().Description, "old b"; got != want {
// This is the one case where we allow the caller to leave one of
// the param descriptions unchanged, because we want to allow
// a function to grow a variadic parameter later without it being
// a breaking change for existing callers that might be overriding
// descriptions.
t.Errorf("wrong updated param b description\ngot: %s\nwant: %s", got, want)
}
})
t.Run("solo varparam overridden", func(t *testing.T) {
f1 := New(&Spec{
Description: "old func",
VarParam: &Parameter{
Name: "a",
Description: "old a",
},
Type: stubType,
Impl: stubImpl,
})
f2 := f1.WithNewDescriptions(
"new func",
[]string{"new a"},
)

if got, want := f1.Description(), "old func"; got != want {
t.Errorf("wrong original func description\ngot: %s\nwant: %s", got, want)
}
if got, want := f2.Description(), "new func"; got != want {
t.Errorf("wrong updated func description\ngot: %s\nwant: %s", got, want)
}

if got, want := len(f1.Params()), 0; got != want {
t.Fatalf("wrong original param count\ngot: %d\nwant: %d", got, want)
}
if got, want := len(f2.Params()), 0; got != want {
t.Fatalf("wrong updated param count\ngot: %d\nwant: %d", got, want)
}
if got, want := f1.VarParam().Description, "old a"; got != want {
t.Errorf("wrong original param b description\ngot: %s\nwant: %s", got, want)
}
if got, want := f2.VarParam().Description, "new a"; got != want {
t.Errorf("wrong updated param b description\ngot: %s\nwant: %s", got, want)
}
})
t.Run("solo varparam not overridden", func(t *testing.T) {
f1 := New(&Spec{
Description: "old func",
VarParam: &Parameter{
Name: "a",
Description: "old a",
},
Type: stubType,
Impl: stubImpl,
})
f2 := f1.WithNewDescriptions(
"new func",
nil,
)

if got, want := f1.Description(), "old func"; got != want {
t.Errorf("wrong original func description\ngot: %s\nwant: %s", got, want)
}
if got, want := f2.Description(), "new func"; got != want {
t.Errorf("wrong updated func description\ngot: %s\nwant: %s", got, want)
}

if got, want := len(f1.Params()), 0; got != want {
t.Fatalf("wrong original param count\ngot: %d\nwant: %d", got, want)
}
if got, want := len(f2.Params()), 0; got != want {
t.Fatalf("wrong updated param count\ngot: %d\nwant: %d", got, want)
}
if got, want := f1.VarParam().Description, "old a"; got != want {
t.Errorf("wrong original param b description\ngot: %s\nwant: %s", got, want)
}
if got, want := f2.VarParam().Description, "old a"; got != want {
// This is the one case where we allow the caller to leave one of
// the param descriptions unchanged, because we want to allow
// a function to grow a variadic parameter later without it being
// a breaking change for existing callers that might be overriding
// descriptions.
t.Errorf("wrong updated param b description\ngot: %s\nwant: %s", got, want)
}
})
}

func stubType([]cty.Value) (cty.Type, error) {
return cty.NilType, fmt.Errorf("should not be called")
}

func stubImpl([]cty.Value, cty.Type) (cty.Value, error) {
return cty.NilVal, fmt.Errorf("should not be called")
}

0 comments on commit 4566e66

Please sign in to comment.