From 765af4745b635987e95475dc7d4c6762005c66db Mon Sep 17 00:00:00 2001 From: Vicken Simonian Date: Wed, 4 Sep 2019 12:11:53 -0700 Subject: [PATCH] lang/funcs: Add sortsemver function Reference: #22688 To support sorting a list of strings in ascending semantic versioning order using the pre-existing 'blang/semver' package dependency. --- lang/funcs/string.go | 50 +++++++++++ lang/funcs/string_test.go | 85 +++++++++++++++++++ lang/functions.go | 1 + lang/functions_test.go | 13 +++ .../functions/sortsemver.html.md | 30 +++++++ website/layouts/functions.erb | 4 + 6 files changed, 183 insertions(+) create mode 100644 website/docs/configuration/functions/sortsemver.html.md diff --git a/lang/funcs/string.go b/lang/funcs/string.go index ab6da72778ec..e8679613e018 100644 --- a/lang/funcs/string.go +++ b/lang/funcs/string.go @@ -1,9 +1,11 @@ package funcs import ( + "fmt" "regexp" "strings" + "github.com/blang/semver" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/function" ) @@ -46,8 +48,56 @@ var ReplaceFunc = function.New(&function.Spec{ }, }) +var SortSemVerFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "list", + Type: cty.List(cty.String), + }, + }, + Type: function.StaticReturnType(cty.List(cty.String)), + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + listVal := args[0] + + if !listVal.IsWhollyKnown() { + // If some of the element values aren't known yet then we + // can't yet preduct the order of the result. + return cty.UnknownVal(retType), nil + } + if listVal.LengthInt() == 0 { // Easy path + return listVal, nil + } + + list := make([]semver.Version, 0, listVal.LengthInt()) + for it := listVal.ElementIterator(); it.Next(); { + iv, v := it.Element() + if v.IsNull() { + return cty.UnknownVal(retType), fmt.Errorf("given list element %s is null; a null string cannot be sorted", iv.AsBigFloat().String()) + } + s, err := semver.Parse(v.AsString()) + if err != nil { + return cty.UnknownVal(retType), fmt.Errorf("given list element %s is not parseable as a semantic version", iv.AsBigFloat().String()) + } + list = append(list, s) + } + + semver.Sort(list) + retVals := make([]cty.Value, len(list)) + for i, s := range list { + retVals[i] = cty.StringVal(s.String()) + } + return cty.ListVal(retVals), nil + }, +}) + // Replace searches a given string for another given substring, // and replaces all occurences with a given replacement string. func Replace(str, substr, replace cty.Value) (cty.Value, error) { return ReplaceFunc.Call([]cty.Value{str, substr, replace}) } + +// SortSemVer re-orders the elements of a given list of strings so that they are +// in ascending semantic versioning order. +func SortSemVer(list cty.Value) (cty.Value, error) { + return SortSemVerFunc.Call([]cty.Value{list}) +} diff --git a/lang/funcs/string_test.go b/lang/funcs/string_test.go index 7b44a2762402..f1ef1e298311 100644 --- a/lang/funcs/string_test.go +++ b/lang/funcs/string_test.go @@ -71,3 +71,88 @@ func TestReplace(t *testing.T) { }) } } + +func TestSortSemVer(t *testing.T) { + tests := []struct { + List cty.Value + Want cty.Value + Err bool + }{ + { + cty.ListValEmpty(cty.String), + cty.ListValEmpty(cty.String), + false, + }, + { + cty.ListVal([]cty.Value{ + cty.StringVal("banana"), + }), + cty.UnknownVal(cty.List(cty.String)), + true, + }, + { + cty.ListVal([]cty.Value{ + cty.StringVal("banana"), + cty.StringVal("apple"), + }), + cty.UnknownVal(cty.List(cty.String)), + true, + }, + { + cty.ListVal([]cty.Value{ + cty.StringVal("1.2.3"), + cty.StringVal("1.0.0"), + cty.StringVal("1.3.0"), + cty.StringVal("2.0.0"), + cty.StringVal("0.4.2"), + }), + cty.ListVal([]cty.Value{ + cty.StringVal("0.4.2"), + cty.StringVal("1.0.0"), + cty.StringVal("1.2.3"), + cty.StringVal("1.3.0"), + cty.StringVal("2.0.0"), + }), + false, + }, + { + cty.UnknownVal(cty.List(cty.String)), + cty.UnknownVal(cty.List(cty.String)), + false, + }, + { + cty.ListVal([]cty.Value{ + cty.UnknownVal(cty.String), + }), + cty.UnknownVal(cty.List(cty.String)), + false, + }, + { + cty.ListVal([]cty.Value{ + cty.UnknownVal(cty.String), + cty.StringVal("1.0"), + }), + cty.UnknownVal(cty.List(cty.String)), + false, + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("SortSemVer(%#v)", test.List), func(t *testing.T) { + got, err := SortSemVer(test.List) + + if test.Err { + if err == nil { + t.Fatal("succeeded; want error") + } + return + } else if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !got.RawEquals(test.Want) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) + } + }) + } +} diff --git a/lang/functions.go b/lang/functions.go index b4cc2d72e57c..af07936702ba 100644 --- a/lang/functions.go +++ b/lang/functions.go @@ -108,6 +108,7 @@ func (s *Scope) Functions() map[string]function.Function { "signum": stdlib.SignumFunc, "slice": stdlib.SliceFunc, "sort": stdlib.SortFunc, + "sortsemver": funcs.SortSemVerFunc, "split": stdlib.SplitFunc, "strrev": stdlib.ReverseFunc, "substr": stdlib.SubstrFunc, diff --git a/lang/functions_test.go b/lang/functions_test.go index d27a487af9fb..4f0740233066 100644 --- a/lang/functions_test.go +++ b/lang/functions_test.go @@ -762,6 +762,19 @@ func TestFunctions(t *testing.T) { }, }, + "sortsemver": { + { + `sortsemver(["2.0.0", "1.0.0", "0.1.0", "0.0.1", "1.0.0-1"])`, + cty.ListVal([]cty.Value{ + cty.StringVal("0.0.1"), + cty.StringVal("0.1.0"), + cty.StringVal("1.0.0-1"), + cty.StringVal("1.0.0"), + cty.StringVal("2.0.0"), + }), + }, + }, + "split": { { `split(" ", "Hello World")`, diff --git a/website/docs/configuration/functions/sortsemver.html.md b/website/docs/configuration/functions/sortsemver.html.md new file mode 100644 index 000000000000..3881d31e4f6e --- /dev/null +++ b/website/docs/configuration/functions/sortsemver.html.md @@ -0,0 +1,30 @@ +--- +layout: "functions" +page_title: "sortsemver - Functions - Configuration Language" +sidebar_current: "docs-funcs-collection-sortsemver" +description: |- + The sortsemver function takes a list of strings and returns a new list with + those strings sorted in semantic versioning order. +--- + +# `sortsemver` Function + +-> **Note:** This page is about Terraform 0.12 and later. For Terraform 0.11 and +earlier, see +[0.11 Configuration Language: Interpolation Syntax](../../configuration-0-11/interpolation.html). + +`sortsemver` takes a list of strings and returns a new list with + those strings sorted in semantic versioning order. A valid string version is + described by the v2.0.0 specification found at https://semver.org/. + +## Examples + +``` +> sortsemver(["1.0.0", "1.2.4", "1.4.0-5", "1.2.3"]) +[ + "1.0.0", + "1.2.3", + "1.2.4", + "1.4.0-5", +] +``` diff --git a/website/layouts/functions.erb b/website/layouts/functions.erb index d0ecd70a50a7..c74824ecf29f 100644 --- a/website/layouts/functions.erb +++ b/website/layouts/functions.erb @@ -238,6 +238,10 @@ sort +
  • + sortsemver +
  • +
  • sum