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 re-ordering the elements of a given list of strings so that
the elements matching a given version constraint are returned in
precedence order.
  • Loading branch information
vsimon authored and Vicken Simonian committed Feb 20, 2021
1 parent 4e345b6 commit aaab3a1
Show file tree
Hide file tree
Showing 7 changed files with 292 additions and 1 deletion.
2 changes: 1 addition & 1 deletion backend/remote/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ func (b *Remote) Configure(obj cty.Value) tfdiags.Diagnostics {
// a remote backend API and to get the version constraints.
service, constraints, err := b.discover(serviceID)

// First check any contraints we might have received.
// First check any constraints we might have received.
if constraints != nil {
diags = diags.Append(b.checkConstraints(constraints))
if diags.HasErrors() {
Expand Down
69 changes: 69 additions & 0 deletions lang/funcs/string.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package funcs

import (
"fmt"
"regexp"
"sort"
"strings"

"github.com/hashicorp/go-version"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
)
Expand Down Expand Up @@ -46,8 +49,74 @@ var ReplaceFunc = function.New(&function.Spec{
},
})

// SortSemVerFunc constructs a function that takes a version constraint string
// and a list of semantic version strings and returns the versions matching that
// constraint in precedence order.
var SortSemVerFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "constraint",
Type: cty.String,
},
{
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) {
constStr := args[0].AsString()
listVal := args[1]

// Create the constraints to check against.
constraints := version.Constraints{}
if strings.TrimSpace(constStr) != "" {
var err error
constraints, err = version.NewConstraint(constStr)
if err != nil {
return cty.UnknownVal(retType), err
}
}

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([]*version.Version, 0, listVal.LengthInt())
for it := listVal.ElementIterator(); it.Next(); {
iv, v := it.Element()
version, err := version.NewSemver(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())
}
if constraints.Check(version) {
list = append(list, version)
}
}

sort.Stable(version.Collection(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 the
// elements matching a given version constraint are returned in precedence
// order.
func SortSemVer(constraint cty.Value, list cty.Value) (cty.Value, error) {
return SortSemVerFunc.Call([]cty.Value{constraint, list})
}
181 changes: 181 additions & 0 deletions lang/funcs/string_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,184 @@ func TestReplace(t *testing.T) {
})
}
}

func TestSortSemVer(t *testing.T) {
tests := []struct {
Constraint cty.Value
List cty.Value
Want cty.Value
Err bool
}{
{
cty.StringVal(""),
cty.ListValEmpty(cty.String),
cty.ListValEmpty(cty.String),
false,
},
{
cty.StringVal(""),
cty.ListVal([]cty.Value{
cty.StringVal("banana"),
}),
cty.UnknownVal(cty.List(cty.String)),
true,
},
{
cty.StringVal(""),
cty.ListVal([]cty.Value{
cty.StringVal("banana"),
cty.StringVal("apple"),
}),
cty.UnknownVal(cty.List(cty.String)),
true,
},
{
cty.StringVal(""),
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.StringVal(""),
cty.UnknownVal(cty.List(cty.String)),
cty.UnknownVal(cty.List(cty.String)),
false,
},
{
cty.StringVal(""),
cty.ListVal([]cty.Value{
cty.UnknownVal(cty.String),
}),
cty.UnknownVal(cty.List(cty.String)),
false,
},
{
cty.StringVal(""),
cty.ListVal([]cty.Value{
cty.UnknownVal(cty.String),
cty.StringVal("1.0"),
}),
cty.UnknownVal(cty.List(cty.String)),
false,
},
{
cty.StringVal("~>"),
cty.ListVal([]cty.Value{
cty.StringVal("1.0"),
}),
cty.ListVal([]cty.Value{
cty.StringVal("1.0"),
}),
true,
},
{
cty.StringVal("~> 1.0.0"),
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("1.0.0"),
}),
false,
},
{
cty.StringVal("~> 1.0"),
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("1.0.0"),
cty.StringVal("1.2.3"),
cty.StringVal("1.3.0"),
}),
false,
},
{
cty.StringVal("~> 1"),
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("1.0.0"),
cty.StringVal("1.2.3"),
cty.StringVal("1.3.0"),
cty.StringVal("2.0.0"),
}),
false,
},
{
cty.StringVal(">= 1.3"),
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("1.3.0"),
cty.StringVal("2.0.0"),
}),
false,
},
{
cty.StringVal("< 1.3.0"),
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"),
}),
false,
},
}

for _, test := range tests {
t.Run(fmt.Sprintf("SortSemVer(%#v)", test.List), func(t *testing.T) {
got, err := SortSemVer(test.Constraint, 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 @@ -112,6 +112,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
9 changes: 9 additions & 0 deletions lang/functions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,15 @@ func TestFunctions(t *testing.T) {
},
},

"sortsemver": {
{
`sortsemver("~> 1.0.0", ["2.0.0", "1.0.0", "0.1.0", "0.0.1", "1.0.0-1"])`,
cty.ListVal([]cty.Value{
cty.StringVal("1.0.0"),
}),
},
},

"split": {
{
`split(" ", "Hello World")`,
Expand Down
27 changes: 27 additions & 0 deletions website/docs/language/functions/sortsemver.html.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
layout: "functions"
page_title: "sortsemver - Functions - Configuration Language"
sidebar_current: "docs-funcs-collection-sortsemver"
description: |-
The sortsemver function takes a version constraint string and a list of
semantic version strings and returns the versions matching that constraint in
precedence order.
---

# `sortsemver` Function

`sortsemver` takes a [version constraint string](/docs/language/expressions/version-constraints.html)
and a list of semantic version strings and returns the versions matching that
constraint in precedence order. A valid semantic version string is described
by the v2.0.0 specification. An empty version constraint string will
successfully match all versions.

## Examples

```
> sortsemver("~> 1.2.0", ["1.0.0", "1.2.4", "1.4.0-5", "1.2.3"])
[
"1.2.3",
"1.2.4",
]
```
4 changes: 4 additions & 0 deletions website/layouts/language.erb
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,10 @@
<a href="/docs/language/functions/sort.html">sort</a>
</li>

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

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

0 comments on commit aaab3a1

Please sign in to comment.