Skip to content

Commit

Permalink
lang/funcs: Add sortsemver function
Browse files Browse the repository at this point in the history
Reference: #22688
To support sorting a list of strings in ascending semantic versioning order
using the pre-existing 'blang/semver' package dependency.
  • Loading branch information
vsimon authored and Vicken Simonian committed Aug 8, 2020
1 parent 7bc75a1 commit 765af47
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 0 deletions.
50 changes: 50 additions & 0 deletions lang/funcs/string.go
Original file line number Diff line number Diff line change
@@ -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"
)
Expand Down Expand Up @@ -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})
}
85 changes: 85 additions & 0 deletions lang/funcs/string_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
}
}
1 change: 1 addition & 0 deletions lang/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
13 changes: 13 additions & 0 deletions lang/functions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")`,
Expand Down
30 changes: 30 additions & 0 deletions website/docs/configuration/functions/sortsemver.html.md
Original file line number Diff line number Diff line change
@@ -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",
]
```
4 changes: 4 additions & 0 deletions website/layouts/functions.erb
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,10 @@
<a href="/docs/configuration/functions/sort.html">sort</a>
</li>

<li>
<a href="/docs/configuration/functions/sortsemver.html">sortsemver</a>
</li>

<li>
<a href="/docs/configuration/functions/sum.html">sum</a>
</li>
Expand Down

0 comments on commit 765af47

Please sign in to comment.