diff --git a/README.md b/README.md index 6502be54..cfecfa1c 100644 --- a/README.md +++ b/README.md @@ -376,6 +376,10 @@ For example, this is a JSON version of an emitted RuntimeContainer struct: * *`sha1 $string`*: Returns the hexadecimal representation of the SHA1 hash of `$string`. * *`split $string $sep`*: Splits `$string` into a slice of substrings delimited by `$sep`. Alias for [`strings.Split`](http://golang.org/pkg/strings/#Split) * *`splitN $string $sep $count`*: Splits `$string` into a slice of substrings delimited by `$sep`, with number of substrings returned determined by `$count`. Alias for [`strings.SplitN`](https://golang.org/pkg/strings/#SplitN) +* *`sortStringsAsc $strings`: Returns a slice of strings `$strings` sorted in ascending order. +* *`sortStringsDesc $strings`: Returns a slice of strings `$strings` sorted in descending (reverse) order. +* *`sortObjectsByKeysAsc $objects $fieldPath`: Returns the array `$objects`, sorted in ascending order based on the values of a field path expression `$fieldPath`. +* *`sortObjectsByKeysDesc $objects $fieldPath`: Returns the array `$objects`, sorted in descending (reverse) order based on the values of a field path expression `$fieldPath`. * *`trimPrefix $prefix $string`*: If `$prefix` is a prefix of `$string`, return `$string` with `$prefix` trimmed from the beginning. Otherwise, return `$string` unchanged. * *`trimSuffix $suffix $string`*: If `$suffix` is a suffix of `$string`, return `$string` with `$suffix` trimmed from the end. Otherwise, return `$string` unchanged. * *`trim $string`*: Removes whitespace from both sides of `$string`. diff --git a/internal/template/sort.go b/internal/template/sort.go new file mode 100644 index 00000000..e27f5658 --- /dev/null +++ b/internal/template/sort.go @@ -0,0 +1,90 @@ +package template + +import ( + "reflect" + "sort" +) + +// sortStrings returns a sorted array of strings in increasing order +func sortStringsAsc(values []string) []string { + sort.Strings(values) + return values +} + +// sortStringsDesc returns a sorted array of strings in decreasing order +func sortStringsDesc(values []string) []string { + sort.Sort(sort.Reverse(sort.StringSlice(values))) + return values +} + +type sortable interface { + sort.Interface + set(string, interface{}) error + get() []interface{} +} + +type sortableData struct { + data []interface{} +} + +func (s sortableData) get() []interface{} { + return s.data +} + +func (s sortableData) Len() int { return len(s.data) } + +func (s sortableData) Swap(i, j int) { s.data[i], s.data[j] = s.data[j], s.data[i] } + +type sortableByKey struct { + sortableData + key string +} + +func (s *sortableByKey) set(funcName string, entries interface{}) (err error) { + entriesVal, err := getArrayValues(funcName, entries) + if err != nil { + return + } + s.data = make([]interface{}, entriesVal.Len()) + for i := 0; i < entriesVal.Len(); i++ { + s.data[i] = reflect.Indirect(entriesVal.Index(i)).Interface() + } + return +} + +// method required to implement sort.Interface +func (s sortableByKey) Less(i, j int) bool { + values := map[int]string{i: "", j: ""} + for k := range values { + if v := reflect.ValueOf(deepGet(s.data[k], s.key)); v.Kind() != reflect.Invalid { + values[k] = v.Interface().(string) + } + } + return values[i] < values[j] +} + +// Generalized SortBy function +func generalizedSortBy(funcName string, entries interface{}, s sortable, reverse bool) (sorted []interface{}, err error) { + err = s.set(funcName, entries) + if err != nil { + return nil, err + } + if reverse { + sort.Stable(sort.Reverse(s)) + } else { + sort.Stable(s) + } + return s.get(), nil +} + +// sortObjectsByKeysAsc returns a sorted array of objects, sorted by object's key field in ascending order +func sortObjectsByKeysAsc(objs interface{}, key string) ([]interface{}, error) { + s := &sortableByKey{key: key} + return generalizedSortBy("sortObjsByKeys", objs, s, false) +} + +// sortObjectsByKeysDesc returns a sorted array of objects, sorted by object's key field in descending order +func sortObjectsByKeysDesc(objs interface{}, key string) ([]interface{}, error) { + s := &sortableByKey{key: key} + return generalizedSortBy("sortObjsByKey", objs, s, true) +} diff --git a/internal/template/sort_test.go b/internal/template/sort_test.go new file mode 100644 index 00000000..1a003713 --- /dev/null +++ b/internal/template/sort_test.go @@ -0,0 +1,102 @@ +package template + +import ( + "testing" + + "github.com/nginx-proxy/docker-gen/internal/context" + "github.com/stretchr/testify/assert" +) + +func TestSortStringsAsc(t *testing.T) { + strings := []string{"foo", "bar", "baz", "qux"} + expected := []string{"bar", "baz", "foo", "qux"} + assert.Equal(t, expected, sortStringsAsc(strings)) +} + +func TestSortStringsDesc(t *testing.T) { + strings := []string{"foo", "bar", "baz", "qux"} + expected := []string{"qux", "foo", "baz", "bar"} + assert.Equal(t, expected, sortStringsDesc(strings)) +} + +func TestSortObjectsByKeysAsc(t *testing.T) { + containers := []*context.RuntimeContainer{ + { + Env: map[string]string{ + "VIRTUAL_HOST": "bar.localhost", + }, + ID: "9", + }, + { + Env: map[string]string{ + "VIRTUAL_HOST": "foo.localhost", + }, + ID: "1", + }, + { + Env: map[string]string{ + "VIRTUAL_HOST": "baz.localhost", + }, + ID: "3", + }, + { + Env: map[string]string{}, + ID: "8", + }, + } + + sorted, err := sortObjectsByKeysAsc(containers, "ID") + + assert.NoError(t, err) + assert.Len(t, sorted, 4) + assert.Equal(t, "foo.localhost", sorted[0].(context.RuntimeContainer).Env["VIRTUAL_HOST"]) + assert.Equal(t, "9", sorted[3].(context.RuntimeContainer).ID) + + sorted, err = sortObjectsByKeysAsc(sorted, "Env.VIRTUAL_HOST") + + assert.NoError(t, err) + assert.Len(t, sorted, 4) + assert.Equal(t, "foo.localhost", sorted[3].(context.RuntimeContainer).Env["VIRTUAL_HOST"]) + assert.Equal(t, "8", sorted[0].(context.RuntimeContainer).ID) +} + +func TestSortObjectsByKeysDesc(t *testing.T) { + containers := []*context.RuntimeContainer{ + { + Env: map[string]string{ + "VIRTUAL_HOST": "bar.localhost", + }, + ID: "9", + }, + { + Env: map[string]string{ + "VIRTUAL_HOST": "foo.localhost", + }, + ID: "1", + }, + { + Env: map[string]string{ + "VIRTUAL_HOST": "baz.localhost", + }, + ID: "3", + }, + { + Env: map[string]string{}, + ID: "8", + }, + } + + sorted, err := sortObjectsByKeysDesc(containers, "ID") + + assert.NoError(t, err) + assert.Len(t, sorted, 4) + assert.Equal(t, "bar.localhost", sorted[0].(context.RuntimeContainer).Env["VIRTUAL_HOST"]) + assert.Equal(t, "1", sorted[3].(context.RuntimeContainer).ID) + + sorted, err = sortObjectsByKeysDesc(sorted, "Env.VIRTUAL_HOST") + + assert.NoError(t, err) + assert.Len(t, sorted, 4) + assert.Equal(t, "", sorted[3].(context.RuntimeContainer).Env["VIRTUAL_HOST"]) + assert.Equal(t, "1", sorted[0].(context.RuntimeContainer).ID) +} diff --git a/internal/template/template.go b/internal/template/template.go index fe1547d3..a6af7e15 100644 --- a/internal/template/template.go +++ b/internal/template/template.go @@ -67,6 +67,10 @@ func newTemplate(name string) *template.Template { "sha1": hashSha1, "split": strings.Split, "splitN": strings.SplitN, + "sortStringsAsc": sortStringsAsc, + "sortStringsDesc": sortStringsDesc, + "sortObjectsByKeysAsc": sortObjectsByKeysAsc, + "sortObjectsByKeysDesc": sortObjectsByKeysDesc, "trimPrefix": trimPrefix, "trimSuffix": trimSuffix, "trim": trim,