diff --git a/README.md b/README.md index 0c9b3dd..4311564 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ go get -u github.com/gofunky/tuplip docker pull gofunky/tuplip ``` -## Using tulip +## Using tuplip ### Using the binary diff --git a/cmd/tuplip/exec.go b/cmd/tuplip/build.go similarity index 97% rename from cmd/tuplip/exec.go rename to cmd/tuplip/build.go index a269c0f..b1e1407 100644 --- a/cmd/tuplip/exec.go +++ b/cmd/tuplip/build.go @@ -69,7 +69,8 @@ func (c *BuildCommand) Synopsis() string { // execute reads the input and prints the output of tuplip. func execute(tuplip tupliplib.Tuplip) int { reader := bufio.NewReader(os.Stdin) - tuplipStream := tuplip.FromReader(reader) + tuplipSource := tuplip.FromReader(reader) + tuplipStream := tuplipSource.Build() lineSplit := func(input string) string { return fmt.Sprintln(input) } diff --git a/pkg/tupliplib/lib.go b/pkg/tupliplib/lib.go index d6330a6..cc108b6 100644 --- a/pkg/tupliplib/lib.go +++ b/pkg/tupliplib/lib.go @@ -1,31 +1,12 @@ package tupliplib import ( - "errors" - "fmt" - "github.com/blang/semver" "github.com/deckarep/golang-set" "github.com/gofunky/automi/emitters" "github.com/gofunky/automi/stream" "io" - "sort" - "strings" ) -// Tuplip contains the parameters for the Docker tag generation. -type Tuplip struct { - // ExcludeMajor excludes the major versions from the result set. - ExcludeMajor bool - // ExcludeMinor excludes the minor versions from the result set. - ExcludeMinor bool - // ExcludeBase excludes the base alias without version suffix from the result set. - ExcludeBase bool - // AddLatest adds an additional 'latest' tag to the result set. - AddLatest bool - // Separator to split the separate tag vector aliases. The default separator is single space. - Separator string -} - // VersionSeparator is the separator that separates the alias form the semantic version. const VersionSeparator = ":" @@ -42,185 +23,52 @@ const DockerTagSeparator = "-" // VectorSeparator is the default tag vector separator. const VectorSeparator = " " -// buildTag parses a semantic version with the given version digits. Optionally, prefix an alias tag. -func (t Tuplip) buildTag(withBase bool, alias string, versionDigits ...uint64) (string, error) { - var builder strings.Builder - if withBase { - _, err := builder.WriteString(alias) - if err != nil { - return "", err - } - } - for n, digit := range versionDigits { - if n > 0 { - _, err := builder.WriteString(".") - if err != nil { - return "", err - } - } - _, err := builder.WriteString(fmt.Sprint(digit)) - if err != nil { - return "", err - } - } - return builder.String(), nil -} - -// buildVersionSet parses all possible shortened version representations from a semantic version object. -func (t Tuplip) buildVersionSet(withBase bool, alias string, isShort bool, version semver.Version) (result mapset.Set, - err error) { - - result = mapset.NewSet() - if withBase && !t.ExcludeBase { - result.Add(alias) - } - if isShort { - if !t.ExcludeMajor { - newTag, err := t.buildTag(withBase, alias, version.Minor) - if err != nil { - return nil, err - } - result.Add(newTag) - } - newTag, err := t.buildTag(withBase, alias, version.Minor, version.Patch) - if err != nil { - return nil, err - } - result.Add(newTag) - } else { - if !t.ExcludeMajor { - newTag, err := t.buildTag(withBase, alias, version.Major) - if err != nil { - return nil, err - } - result.Add(newTag) - } - if !t.ExcludeMinor { - newTag, err := t.buildTag(withBase, alias, version.Major, version.Minor) - if err != nil { - return nil, err - } - result.Add(newTag) - } - newTag, err := t.buildTag(withBase, alias, version.Major, version.Minor, version.Patch) - if err != nil { - return nil, err - } - result.Add(newTag) - } - return result, nil -} - -// splitVersion takes a parsed semantic version string, builds a semantic version object and generates all possible -// shortened version strings from it. -func (t Tuplip) splitVersion(inputTag string) (result mapset.Set, err error) { - if strings.Contains(inputTag, VersionSeparator) { - dependency := strings.SplitN(inputTag, VersionSeparator, 2) - dependencyAlias := dependency[0] - var dependencyVersionText = dependency[1] - versionIsShort := strings.Count(dependencyVersionText, VersionDot) == 1 - if versionIsShort { - dependencyVersionText = "0." + dependencyVersionText - } - dependencyVersion, err := semver.Make(dependencyVersionText) - if err != nil { - return nil, err - } - withBase := dependencyAlias != WildcardDependency - return t.buildVersionSet(withBase, dependencyAlias, versionIsShort, dependencyVersion) - } else { - return mapset.NewSetWith(inputTag), nil - } +// Tuplip contains the parameters for the Docker tag generation. +type Tuplip struct { + // ExcludeMajor excludes the major versions from the result set. + ExcludeMajor bool + // ExcludeMinor excludes the minor versions from the result set. + ExcludeMinor bool + // ExcludeBase excludes the base alias without version suffix from the result set. + ExcludeBase bool + // AddLatest adds an additional 'latest' tag to the result set. + AddLatest bool + // Separator to split the separate tag vector aliases. The default separator is single space. + Separator string } -// nonEmpty marks if a string is not empty. -func (t Tuplip) nonEmpty(input string) bool { - return input != "" +// tuplipSource is the intermediarily-built Tuplip stream containing only the source parsing steps. +type tuplipSource struct { + tuplip *Tuplip + stream *stream.Stream } -// splitBySeparator separates the input string by the chosen character and trims superfluous spaces. -func (t Tuplip) splitBySeparator(input string) (result []string) { +// check performs post-construction checks. +func (t *Tuplip) check() { if t.Separator == "" { t.Separator = VectorSeparator } - result = strings.Split(input, t.Separator) - for i, el := range result { - result[i] = strings.TrimSpace(el) - } - return -} - -// packInSet packs a set as subset into a new set. -func (t Tuplip) packInSet(subSet mapset.Set) (result mapset.Set) { - return mapset.NewSetWith(subSet) } -// mergeSets merges the second given set into the first one. -func (t Tuplip) mergeSets(left mapset.Set, right mapset.Set) (result mapset.Set) { - return left.Union(right) -} - -// power build a power of the given set. -func (t Tuplip) power(inputSet mapset.Set) mapset.Set { - return inputSet.PowerSet() -} - -// failOnEmpty returns an error if the given power set is empty (i.e, has cardinality < 2). -func (t Tuplip) failOnEmpty(inputSet mapset.Set) (mapset.Set, error) { - if inputSet.Cardinality() <= 1 { - return nil, errors.New("no input tags could be detected") - } - return inputSet, nil -} - -// join joins all subtags (i.e., elements of the given set) to all possible representations by building a cartesian -// product of them. The subtags are separated by the given Docker separator. The subtags are ordered alphabetically -// to ensure that a root tag vector (i.e., a tag without an alias) is mentioned before alias tags. -func (t Tuplip) join(inputSet mapset.Set) (result mapset.Set) { - result = mapset.NewSet() - inputSlice := inputSet.ToSlice() - subTagSlice := make(SortedSet, len(inputSlice)) - for i, subTag := range inputSlice { - subTagSlice[i] = subTag.(mapset.Set) - } - sort.Sort(subTagSlice) - for _, subTag := range subTagSlice { - subTagSet := subTag.(mapset.Set) - if result.Cardinality() == 0 { - result = subTagSet - } else { - productSet := subTagSet.CartesianProduct(result) - result = mapset.NewSet() - for item := range productSet.Iter() { - pair := item.(mapset.OrderedPair) - concatPair := fmt.Sprintf("%s%s%s", pair.First, DockerTagSeparator, pair.Second) - result.Add(concatPair) - } - } - } - return result -} - -// addLatestTag adds an additional latest tag if requested in *Tuplip. -func (t Tuplip) addLatestTag(inputSet mapset.Set) mapset.Set { - if t.AddLatest { - inputSet.Add(mapset.NewSet(mapset.NewSet("latest"))) - } - return inputSet +// FromReader builds a tuplip source from a pipe reader. +func (t *Tuplip) FromReader(src io.Reader) *tuplipSource { + t.check() + stm := stream.New(emitters.Scanner(src, nil)) + stm.FlatMap(t.splitBySeparator) + stm.Filter(t.nonEmpty) + return &tuplipSource{t, stm} } -// FromReader builds a tuplip stream from a io.Reader as scanner. The returned stream has no configured sink. -func (t Tuplip) FromReader(src io.Reader) *stream.Stream { - iStream := stream.New(emitters.Scanner(src, nil)) - iStream.FlatMap(t.splitBySeparator) - iStream.Filter(t.nonEmpty) - iStream.Map(t.splitVersion) - iStream.Map(t.packInSet) - iStream.Reduce(mapset.NewSet(), t.mergeSets) - iStream.Map(t.power) - iStream.Map(t.addLatestTag) - iStream.FlatMap(t.failOnEmpty) - iStream.FlatMap(t.join) - iStream.Filter(t.nonEmpty) - return iStream +// Build builds a tuplip stream from a io.Reader as scanner. The returned stream has no configured sink. +func (s *tuplipSource) Build() *stream.Stream { + stm := s.stream + stm.Map(s.tuplip.splitVersion) + stm.Map(s.tuplip.packInSet) + stm.Reduce(mapset.NewSet(), s.tuplip.mergeSets) + stm.Map(s.tuplip.power) + stm.Map(s.tuplip.addLatestTag) + stm.FlatMap(s.tuplip.failOnEmpty) + stm.FlatMap(s.tuplip.join) + stm.Filter(s.tuplip.nonEmpty) + return stm } diff --git a/pkg/tupliplib/lib_test.go b/pkg/tupliplib/lib_test.go index c91fd6e..facf798 100644 --- a/pkg/tupliplib/lib_test.go +++ b/pkg/tupliplib/lib_test.go @@ -2,16 +2,14 @@ package tupliplib import ( "github.com/deckarep/golang-set" - "reflect" "strings" "testing" "time" - "github.com/blang/semver" "github.com/gofunky/automi/collectors" ) -func TestTuplipStream_FromReader(t *testing.T) { +func TestTuplipStream_BuildFromReader(t *testing.T) { type args struct { input []string } @@ -102,7 +100,8 @@ func TestTuplipStream_FromReader(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { src := strings.NewReader(strings.Join(tt.args.input, " ")) - tStream := tt.t.FromReader(src) + tuplipSrc := tt.t.FromReader(src) + tStream := tuplipSrc.Build() collector := collectors.Slice() tStream.Into(collector) select { @@ -126,507 +125,3 @@ func TestTuplipStream_FromReader(t *testing.T) { }) } } - -func TestTuplip_buildTag(t *testing.T) { - type args struct { - withBase bool - alias string - versionDigits []uint64 - } - tests := []struct { - name string - t Tuplip - args args - want string - wantErr bool - }{ - { - name: "With Base And 3 Digits", - args: args{ - withBase: true, - alias: "alias", - versionDigits: []uint64{1, 0, 0}, - }, - want: "alias1.0.0", - }, - { - name: "With Base And 2 Digits", - args: args{ - withBase: true, - alias: "alias", - versionDigits: []uint64{1, 0}, - }, - want: "alias1.0", - }, - { - name: "With Base And 1 Digit", - args: args{ - withBase: true, - alias: "alias", - versionDigits: []uint64{1}, - }, - want: "alias1", - }, - { - name: "Without Base And 3 Digits", - t: Tuplip{}, - args: args{ - versionDigits: []uint64{2, 0, 0}, - }, - want: "2.0.0", - }, - { - name: "Without Base And 2 Digits", - t: Tuplip{}, - args: args{ - versionDigits: []uint64{2, 0}, - }, - want: "2.0", - }, - { - name: "Without Base And 1 Digit", - t: Tuplip{}, - args: args{ - versionDigits: []uint64{2}, - }, - want: "2", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := tt.t.buildTag(tt.args.withBase, tt.args.alias, tt.args.versionDigits...) - if (err != nil) != tt.wantErr { - t.Errorf("Tuplip.buildTag() error = %v, wantErr %v", err, tt.wantErr) - return - } - if got != tt.want { - t.Errorf("Tuplip.buildTag() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestTuplip_parseVersions(t *testing.T) { - type args struct { - withBase bool - alias string - isShort bool - version semver.Version - } - tests := []struct { - name string - t Tuplip - args args - wantResult []string - wantErr bool - }{ - { - name: "Short With Base", - args: args{ - withBase: true, - isShort: true, - alias: "alias", - version: semver.Version{Minor: 1}, - }, - wantResult: []string{"alias", "alias1", "alias1.0"}, - }, - { - name: "Long With Base", - args: args{ - withBase: true, - alias: "alias", - version: semver.Version{Major: 1}, - }, - wantResult: []string{"alias", "alias1", "alias1.0", "alias1.0.0"}, - }, - { - name: "Short Without Base", - args: args{ - withBase: false, - isShort: true, - version: semver.Version{Minor: 1}, - }, - wantResult: []string{"1", "1.0"}, - }, - { - name: "Long Without Base", - args: args{ - withBase: false, - version: semver.Version{Major: 1}, - }, - wantResult: []string{"1", "1.0", "1.0.0"}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotResult, err := tt.t.buildVersionSet(tt.args.withBase, tt.args.alias, tt.args.isShort, tt.args.version) - if (err != nil) != tt.wantErr { - t.Errorf("Tuplip.buildVersionSet() error = %v, wantErr %v", err, tt.wantErr) - return - } - wantSet := mapset.NewSet() - for _, e := range tt.wantResult { - wantSet.Add(e) - } - if !reflect.DeepEqual(gotResult, wantSet) { - t.Errorf("Tuplip.buildVersionSet() = %v, want %v", gotResult, wantSet) - } - }) - } -} - -func TestTuplip_splitVersion(t *testing.T) { - type args struct { - inputTag string - } - tests := []struct { - name string - t Tuplip - args args - wantResult []string - wantErr bool - }{ - { - name: "Short With Base", - args: args{ - inputTag: "alias:1.0", - }, - wantResult: []string{"alias", "alias1", "alias1.0"}, - }, - { - name: "Long With Base", - args: args{ - inputTag: "alias:1.0.0", - }, - wantResult: []string{"alias", "alias1", "alias1.0", "alias1.0.0"}, - }, - { - name: "Short Without Base", - args: args{ - inputTag: "_:1.0", - }, - wantResult: []string{"1", "1.0"}, - }, - { - name: "Long Without Base", - args: args{ - inputTag: "_:1.0.0", - }, - wantResult: []string{"1", "1.0", "1.0.0"}, - }, - { - name: "Invalid Version", - args: args{ - inputTag: "_:invalid.stuff", - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotResult, err := tt.t.splitVersion(tt.args.inputTag) - if (err != nil) != tt.wantErr { - t.Errorf("Tuplip.splitVersion() error = %v, wantErr %v", err, tt.wantErr) - return - } - if err == nil { - wantSet := mapset.NewSet() - for _, e := range tt.wantResult { - wantSet.Add(e) - } - if !reflect.DeepEqual(gotResult, wantSet) { - t.Errorf("Tuplip.splitVersion() = %v, want %v", gotResult, wantSet) - } - } - }) - } -} - -func TestTuplip_nonEmpty(t *testing.T) { - type args struct { - input string - } - tests := []struct { - name string - t Tuplip - args args - want bool - }{ - { - name: "Empty String", - args: args{""}, - want: false, - }, - { - name: "Nonempty String", - args: args{"foo"}, - want: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := tt.t.nonEmpty(tt.args.input); got != tt.want { - t.Errorf("Tuplip.nonEmpty() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestTuplip_splitBySeparator(t *testing.T) { - type args struct { - input string - } - tests := []struct { - name string - t Tuplip - args args - wantResult []string - }{ - { - name: "Empty Split", - args: args{""}, - wantResult: []string{""}, - }, - { - name: "Unary Split", - args: args{"foo"}, - wantResult: []string{"foo"}, - }, - { - name: "Split Tuple", - args: args{"foo boo hoo"}, - wantResult: []string{"foo", "boo", "hoo"}, - }, - { - name: "Split Tuple With Different Separator", - t: Tuplip{Separator: ","}, - args: args{"foo, boo,hoo"}, - wantResult: []string{"foo", "boo", "hoo"}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if gotResult := tt.t.splitBySeparator(tt.args.input); !reflect.DeepEqual(gotResult, tt.wantResult) { - t.Errorf("Tuplip.splitBySeparator() = %v, want %v", gotResult, tt.wantResult) - } - }) - } -} - -func TestTuplip_packInSet(t *testing.T) { - type args struct { - subSet mapset.Set - } - tests := []struct { - name string - t Tuplip - args args - wantResult mapset.Set - }{ - { - name: "Empty Set", - args: args{mapset.NewSet()}, - wantResult: mapset.NewSetWith(mapset.NewSet()), - }, - { - name: "Unary Set", - args: args{mapset.NewSet("foo")}, - wantResult: mapset.NewSetWith(mapset.NewSet("foo")), - }, - { - name: "Tuple Set", - args: args{mapset.NewSet("foo", "boo")}, - wantResult: mapset.NewSetWith(mapset.NewSet("foo", "boo")), - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if gotResult := tt.t.packInSet(tt.args.subSet); !reflect.DeepEqual(gotResult.ToSlice(), - tt.wantResult.ToSlice()) { - t.Errorf("Tuplip.packInSet() = %v, want %v", gotResult, tt.wantResult) - } - }) - } -} - -func TestTuplip_mergeSets(t *testing.T) { - type args struct { - left mapset.Set - right mapset.Set - } - tests := []struct { - name string - t Tuplip - args args - wantResult mapset.Set - }{ - { - name: "Merge Empty Sets", - args: args{mapset.NewSet(), mapset.NewSet()}, - wantResult: mapset.NewSet(), - }, - { - name: "Merge Empty Set With Nonempty Set", - args: args{mapset.NewSet(), mapset.NewSet("foo")}, - wantResult: mapset.NewSet("foo"), - }, - { - name: "Merge Two Nonempty Sets", - args: args{mapset.NewSet("foo", "boo"), mapset.NewSet("hoo")}, - wantResult: mapset.NewSet("foo", "boo", "hoo"), - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if gotResult := tt.t.mergeSets(tt.args.left, tt.args.right); !reflect.DeepEqual(gotResult, tt.wantResult) { - t.Errorf("Tuplip.mergeSets() = %v, want %v", gotResult, tt.wantResult) - } - }) - } -} - -func TestTuplip_join(t *testing.T) { - type args struct { - inputSet mapset.Set - } - tests := []struct { - name string - t Tuplip - args args - wantResult mapset.Set - }{ - { - name: "Empty Set", - args: args{mapset.NewSet()}, - wantResult: mapset.NewSet(), - }, - { - name: "Unary Set", - args: args{mapset.NewSet(mapset.NewSet("alias"))}, - wantResult: mapset.NewSet("alias"), - }, - { - name: "Binary Set", - args: args{mapset.NewSet(mapset.NewSet("alias"), mapset.NewSet("foo"))}, - wantResult: mapset.NewSet("alias-foo"), - }, - { - name: "Cartesian Product Check", - args: args{mapset.NewSet(mapset.NewSet("alias", "alias2"), mapset.NewSet("foo", "boo"))}, - wantResult: mapset.NewSet("alias-foo", "alias-boo", "alias2-foo", "alias2-boo"), - }, - { - name: "Cartesian Product Check With Base Version", - args: args{mapset.NewSet(mapset.NewSet("1.0", "1.0.0"), mapset.NewSet("foo", "boo"))}, - wantResult: mapset.NewSet("1.0-foo", "1.0-boo", "1.0.0-foo", "1.0.0-boo"), - }, - { - name: "Tertiary Set", - args: args{mapset.NewSet(mapset.NewSet("alias"), mapset.NewSet("foo"), mapset.NewSet("boo"))}, - wantResult: mapset.NewSet("alias-boo-foo"), - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if gotResult := tt.t.join(tt.args.inputSet); !reflect.DeepEqual(gotResult, tt.wantResult) { - t.Errorf("Tuplip.join() = %v, want %v", gotResult, tt.wantResult) - } - }) - } -} - -func TestTuplip_power(t *testing.T) { - type args struct { - inputSet mapset.Set - } - tests := []struct { - name string - t Tuplip - args args - want []mapset.Set - }{ - { - name: "Empty Set", - args: args{mapset.NewSet()}, - want: []mapset.Set{ - mapset.NewSet(), - }, - }, - { - name: "Unary Set", - args: args{mapset.NewSet("alias")}, - want: []mapset.Set{ - mapset.NewSet(), - mapset.NewSet("alias"), - }, - }, - { - name: "Binary Set", - args: args{mapset.NewSet("alias", "foo")}, - want: []mapset.Set{ - mapset.NewSet(), - mapset.NewSet("alias"), - mapset.NewSet("foo"), - mapset.NewSet("alias", "foo"), - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := tt.t.power(tt.args.inputSet).ToSlice() - for _, tagSet := range tt.want { - var found bool - for _, val := range got { - if tagSet.Equal(val.(mapset.Set)) { - found = true - } - } - if !found { - t.Errorf("Tuplip.power() = %v, want %v, missing %v", got, tt.want, tagSet) - } - } - }) - } -} - -func TestTuplip_failOnEmpty(t *testing.T) { - nonemptySet := mapset.NewSet(mapset.NewSet(), mapset.NewSet("alias")) - type args struct { - inputSet mapset.Set - } - tests := []struct { - name string - t Tuplip - args args - want mapset.Set - wantErr bool - }{ - { - name: "Empty Set", - args: args{mapset.NewSet()}, - wantErr: true, - }, - { - name: "Empty Power Set", - args: args{mapset.NewSet(mapset.NewSet())}, - wantErr: true, - }, - { - name: "Nonempty Power Set", - args: args{nonemptySet}, - want: nonemptySet, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := tt.t.failOnEmpty(tt.args.inputSet) - if (err != nil) != tt.wantErr { - t.Errorf("Tuplip.failOnEmpty() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("Tuplip.failOnEmpty() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/pkg/tupliplib/members.go b/pkg/tupliplib/members.go new file mode 100644 index 0000000..503b930 --- /dev/null +++ b/pkg/tupliplib/members.go @@ -0,0 +1,174 @@ +package tupliplib + +import ( + "errors" + "fmt" + "github.com/blang/semver" + "github.com/deckarep/golang-set" + "sort" + "strings" +) + +// buildTag parses a semantic version with the given version digits. Optionally, prefix an alias tag. +func (t Tuplip) buildTag(withBase bool, alias string, versionDigits ...uint64) (string, error) { + var builder strings.Builder + if withBase { + _, err := builder.WriteString(alias) + if err != nil { + return "", err + } + } + for n, digit := range versionDigits { + if n > 0 { + _, err := builder.WriteString(".") + if err != nil { + return "", err + } + } + _, err := builder.WriteString(fmt.Sprint(digit)) + if err != nil { + return "", err + } + } + return builder.String(), nil +} + +// buildVersionSet parses all possible shortened version representations from a semantic version object. +func (t Tuplip) buildVersionSet(withBase bool, alias string, isShort bool, version semver.Version) (result mapset.Set, + err error) { + + result = mapset.NewSet() + if withBase && !t.ExcludeBase { + result.Add(alias) + } + if isShort { + if !t.ExcludeMajor { + newTag, err := t.buildTag(withBase, alias, version.Minor) + if err != nil { + return nil, err + } + result.Add(newTag) + } + newTag, err := t.buildTag(withBase, alias, version.Minor, version.Patch) + if err != nil { + return nil, err + } + result.Add(newTag) + } else { + if !t.ExcludeMajor { + newTag, err := t.buildTag(withBase, alias, version.Major) + if err != nil { + return nil, err + } + result.Add(newTag) + } + if !t.ExcludeMinor { + newTag, err := t.buildTag(withBase, alias, version.Major, version.Minor) + if err != nil { + return nil, err + } + result.Add(newTag) + } + newTag, err := t.buildTag(withBase, alias, version.Major, version.Minor, version.Patch) + if err != nil { + return nil, err + } + result.Add(newTag) + } + return result, nil +} + +// splitVersion takes a parsed semantic version string, builds a semantic version object and generates all possible +// shortened version strings from it. +func (t Tuplip) splitVersion(inputTag string) (result mapset.Set, err error) { + if strings.Contains(inputTag, VersionSeparator) { + dependency := strings.SplitN(inputTag, VersionSeparator, 2) + dependencyAlias := dependency[0] + var dependencyVersionText = dependency[1] + versionIsShort := strings.Count(dependencyVersionText, VersionDot) == 1 + if versionIsShort { + dependencyVersionText = "0." + dependencyVersionText + } + dependencyVersion, err := semver.Make(dependencyVersionText) + if err != nil { + return nil, err + } + withBase := dependencyAlias != WildcardDependency + return t.buildVersionSet(withBase, dependencyAlias, versionIsShort, dependencyVersion) + } else { + return mapset.NewSetWith(inputTag), nil + } +} + +// nonEmpty marks if a string is not empty. +func (t Tuplip) nonEmpty(input string) bool { + return input != "" +} + +// splitBySeparator separates the input string by the chosen character and trims superfluous spaces. +func (t Tuplip) splitBySeparator(input string) (result []string) { + result = strings.Split(input, t.Separator) + for i, el := range result { + result[i] = strings.TrimSpace(el) + } + return +} + +// packInSet packs a set as subset into a new set. +func (t Tuplip) packInSet(subSet mapset.Set) (result mapset.Set) { + return mapset.NewSetWith(subSet) +} + +// mergeSets merges the second given set into the first one. +func (t Tuplip) mergeSets(left mapset.Set, right mapset.Set) (result mapset.Set) { + return left.Union(right) +} + +// power build a power of the given set. +func (t Tuplip) power(inputSet mapset.Set) mapset.Set { + return inputSet.PowerSet() +} + +// failOnEmpty returns an error if the given power set is empty (i.e, has cardinality < 2). +func (t Tuplip) failOnEmpty(inputSet mapset.Set) (mapset.Set, error) { + if inputSet.Cardinality() <= 1 { + return nil, errors.New("no input tags could be detected") + } + return inputSet, nil +} + +// join joins all subtags (i.e., elements of the given set) to all possible representations by building a cartesian +// product of them. The subtags are separated by the given Docker separator. The subtags are ordered alphabetically +// to ensure that a root tag vector (i.e., a tag without an alias) is mentioned before alias tags. +func (t Tuplip) join(inputSet mapset.Set) (result mapset.Set) { + result = mapset.NewSet() + inputSlice := inputSet.ToSlice() + subTagSlice := make(SortedSet, len(inputSlice)) + for i, subTag := range inputSlice { + subTagSlice[i] = subTag.(mapset.Set) + } + sort.Sort(subTagSlice) + for _, subTag := range subTagSlice { + subTagSet := subTag.(mapset.Set) + if result.Cardinality() == 0 { + result = subTagSet + } else { + productSet := subTagSet.CartesianProduct(result) + result = mapset.NewSet() + for item := range productSet.Iter() { + pair := item.(mapset.OrderedPair) + concatPair := fmt.Sprintf("%s%s%s", pair.First, DockerTagSeparator, pair.Second) + result.Add(concatPair) + } + } + } + return result +} + +// addLatestTag adds an additional latest tag if requested in *Tuplip. +func (t Tuplip) addLatestTag(inputSet mapset.Set) mapset.Set { + if t.AddLatest { + inputSet.Add(mapset.NewSet(mapset.NewSet("latest"))) + } + return inputSet +} diff --git a/pkg/tupliplib/members_test.go b/pkg/tupliplib/members_test.go new file mode 100644 index 0000000..48832b8 --- /dev/null +++ b/pkg/tupliplib/members_test.go @@ -0,0 +1,512 @@ +package tupliplib + +import ( + "github.com/blang/semver" + "github.com/deckarep/golang-set" + "reflect" + "testing" +) + +func TestTuplip_buildTag(t *testing.T) { + type args struct { + withBase bool + alias string + versionDigits []uint64 + } + tests := []struct { + name string + t Tuplip + args args + want string + wantErr bool + }{ + { + name: "With Base And 3 Digits", + args: args{ + withBase: true, + alias: "alias", + versionDigits: []uint64{1, 0, 0}, + }, + want: "alias1.0.0", + }, + { + name: "With Base And 2 Digits", + args: args{ + withBase: true, + alias: "alias", + versionDigits: []uint64{1, 0}, + }, + want: "alias1.0", + }, + { + name: "With Base And 1 Digit", + args: args{ + withBase: true, + alias: "alias", + versionDigits: []uint64{1}, + }, + want: "alias1", + }, + { + name: "Without Base And 3 Digits", + t: Tuplip{}, + args: args{ + versionDigits: []uint64{2, 0, 0}, + }, + want: "2.0.0", + }, + { + name: "Without Base And 2 Digits", + t: Tuplip{}, + args: args{ + versionDigits: []uint64{2, 0}, + }, + want: "2.0", + }, + { + name: "Without Base And 1 Digit", + t: Tuplip{}, + args: args{ + versionDigits: []uint64{2}, + }, + want: "2", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.t.buildTag(tt.args.withBase, tt.args.alias, tt.args.versionDigits...) + if (err != nil) != tt.wantErr { + t.Errorf("Tuplip.buildTag() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("Tuplip.buildTag() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestTuplip_parseVersions(t *testing.T) { + type args struct { + withBase bool + alias string + isShort bool + version semver.Version + } + tests := []struct { + name string + t Tuplip + args args + wantResult []string + wantErr bool + }{ + { + name: "Short With Base", + args: args{ + withBase: true, + isShort: true, + alias: "alias", + version: semver.Version{Minor: 1}, + }, + wantResult: []string{"alias", "alias1", "alias1.0"}, + }, + { + name: "Long With Base", + args: args{ + withBase: true, + alias: "alias", + version: semver.Version{Major: 1}, + }, + wantResult: []string{"alias", "alias1", "alias1.0", "alias1.0.0"}, + }, + { + name: "Short Without Base", + args: args{ + withBase: false, + isShort: true, + version: semver.Version{Minor: 1}, + }, + wantResult: []string{"1", "1.0"}, + }, + { + name: "Long Without Base", + args: args{ + withBase: false, + version: semver.Version{Major: 1}, + }, + wantResult: []string{"1", "1.0", "1.0.0"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotResult, err := tt.t.buildVersionSet(tt.args.withBase, tt.args.alias, tt.args.isShort, tt.args.version) + if (err != nil) != tt.wantErr { + t.Errorf("Tuplip.buildVersionSet() error = %v, wantErr %v", err, tt.wantErr) + return + } + wantSet := mapset.NewSet() + for _, e := range tt.wantResult { + wantSet.Add(e) + } + if !reflect.DeepEqual(gotResult, wantSet) { + t.Errorf("Tuplip.buildVersionSet() = %v, want %v", gotResult, wantSet) + } + }) + } +} + +func TestTuplip_splitVersion(t *testing.T) { + type args struct { + inputTag string + } + tests := []struct { + name string + t Tuplip + args args + wantResult []string + wantErr bool + }{ + { + name: "Short With Base", + args: args{ + inputTag: "alias:1.0", + }, + wantResult: []string{"alias", "alias1", "alias1.0"}, + }, + { + name: "Long With Base", + args: args{ + inputTag: "alias:1.0.0", + }, + wantResult: []string{"alias", "alias1", "alias1.0", "alias1.0.0"}, + }, + { + name: "Short Without Base", + args: args{ + inputTag: "_:1.0", + }, + wantResult: []string{"1", "1.0"}, + }, + { + name: "Long Without Base", + args: args{ + inputTag: "_:1.0.0", + }, + wantResult: []string{"1", "1.0", "1.0.0"}, + }, + { + name: "Invalid Version", + args: args{ + inputTag: "_:invalid.stuff", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotResult, err := tt.t.splitVersion(tt.args.inputTag) + if (err != nil) != tt.wantErr { + t.Errorf("Tuplip.splitVersion() error = %v, wantErr %v", err, tt.wantErr) + return + } + if err == nil { + wantSet := mapset.NewSet() + for _, e := range tt.wantResult { + wantSet.Add(e) + } + if !reflect.DeepEqual(gotResult, wantSet) { + t.Errorf("Tuplip.splitVersion() = %v, want %v", gotResult, wantSet) + } + } + }) + } +} + +func TestTuplip_nonEmpty(t *testing.T) { + type args struct { + input string + } + tests := []struct { + name string + t Tuplip + args args + want bool + }{ + { + name: "Empty String", + args: args{""}, + want: false, + }, + { + name: "Nonempty String", + args: args{"foo"}, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.t.nonEmpty(tt.args.input); got != tt.want { + t.Errorf("Tuplip.nonEmpty() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestTuplip_splitBySeparator(t *testing.T) { + type args struct { + input string + } + tests := []struct { + name string + t Tuplip + args args + wantResult []string + }{ + { + name: "Empty Split", + args: args{""}, + wantResult: []string{""}, + }, + { + name: "Unary Split", + args: args{"foo"}, + wantResult: []string{"foo"}, + }, + { + name: "Split Tuple", + args: args{"foo boo hoo"}, + wantResult: []string{"foo", "boo", "hoo"}, + }, + { + name: "Split Tuple With Different Separator", + t: Tuplip{Separator: ","}, + args: args{"foo, boo,hoo"}, + wantResult: []string{"foo", "boo", "hoo"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if gotResult := tt.t.splitBySeparator(tt.args.input); !reflect.DeepEqual(gotResult, tt.wantResult) { + t.Errorf("Tuplip.splitBySeparator() = %v, want %v", gotResult, tt.wantResult) + } + }) + } +} + +func TestTuplip_packInSet(t *testing.T) { + type args struct { + subSet mapset.Set + } + tests := []struct { + name string + t Tuplip + args args + wantResult mapset.Set + }{ + { + name: "Empty Set", + args: args{mapset.NewSet()}, + wantResult: mapset.NewSetWith(mapset.NewSet()), + }, + { + name: "Unary Set", + args: args{mapset.NewSet("foo")}, + wantResult: mapset.NewSetWith(mapset.NewSet("foo")), + }, + { + name: "Tuple Set", + args: args{mapset.NewSet("foo", "boo")}, + wantResult: mapset.NewSetWith(mapset.NewSet("foo", "boo")), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if gotResult := tt.t.packInSet(tt.args.subSet); !reflect.DeepEqual(gotResult.ToSlice(), + tt.wantResult.ToSlice()) { + t.Errorf("Tuplip.packInSet() = %v, want %v", gotResult, tt.wantResult) + } + }) + } +} + +func TestTuplip_mergeSets(t *testing.T) { + type args struct { + left mapset.Set + right mapset.Set + } + tests := []struct { + name string + t Tuplip + args args + wantResult mapset.Set + }{ + { + name: "Merge Empty Sets", + args: args{mapset.NewSet(), mapset.NewSet()}, + wantResult: mapset.NewSet(), + }, + { + name: "Merge Empty Set With Nonempty Set", + args: args{mapset.NewSet(), mapset.NewSet("foo")}, + wantResult: mapset.NewSet("foo"), + }, + { + name: "Merge Two Nonempty Sets", + args: args{mapset.NewSet("foo", "boo"), mapset.NewSet("hoo")}, + wantResult: mapset.NewSet("foo", "boo", "hoo"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if gotResult := tt.t.mergeSets(tt.args.left, tt.args.right); !reflect.DeepEqual(gotResult, tt.wantResult) { + t.Errorf("Tuplip.mergeSets() = %v, want %v", gotResult, tt.wantResult) + } + }) + } +} + +func TestTuplip_join(t *testing.T) { + type args struct { + inputSet mapset.Set + } + tests := []struct { + name string + t Tuplip + args args + wantResult mapset.Set + }{ + { + name: "Empty Set", + args: args{mapset.NewSet()}, + wantResult: mapset.NewSet(), + }, + { + name: "Unary Set", + args: args{mapset.NewSet(mapset.NewSet("alias"))}, + wantResult: mapset.NewSet("alias"), + }, + { + name: "Binary Set", + args: args{mapset.NewSet(mapset.NewSet("alias"), mapset.NewSet("foo"))}, + wantResult: mapset.NewSet("alias-foo"), + }, + { + name: "Cartesian Product Check", + args: args{mapset.NewSet(mapset.NewSet("alias", "alias2"), mapset.NewSet("foo", "boo"))}, + wantResult: mapset.NewSet("alias-foo", "alias-boo", "alias2-foo", "alias2-boo"), + }, + { + name: "Cartesian Product Check With Base Version", + args: args{mapset.NewSet(mapset.NewSet("1.0", "1.0.0"), mapset.NewSet("foo", "boo"))}, + wantResult: mapset.NewSet("1.0-foo", "1.0-boo", "1.0.0-foo", "1.0.0-boo"), + }, + { + name: "Tertiary Set", + args: args{mapset.NewSet(mapset.NewSet("alias"), mapset.NewSet("foo"), mapset.NewSet("boo"))}, + wantResult: mapset.NewSet("alias-boo-foo"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if gotResult := tt.t.join(tt.args.inputSet); !reflect.DeepEqual(gotResult, tt.wantResult) { + t.Errorf("Tuplip.join() = %v, want %v", gotResult, tt.wantResult) + } + }) + } +} + +func TestTuplip_power(t *testing.T) { + type args struct { + inputSet mapset.Set + } + tests := []struct { + name string + t Tuplip + args args + want []mapset.Set + }{ + { + name: "Empty Set", + args: args{mapset.NewSet()}, + want: []mapset.Set{ + mapset.NewSet(), + }, + }, + { + name: "Unary Set", + args: args{mapset.NewSet("alias")}, + want: []mapset.Set{ + mapset.NewSet(), + mapset.NewSet("alias"), + }, + }, + { + name: "Binary Set", + args: args{mapset.NewSet("alias", "foo")}, + want: []mapset.Set{ + mapset.NewSet(), + mapset.NewSet("alias"), + mapset.NewSet("foo"), + mapset.NewSet("alias", "foo"), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.t.power(tt.args.inputSet).ToSlice() + for _, tagSet := range tt.want { + var found bool + for _, val := range got { + if tagSet.Equal(val.(mapset.Set)) { + found = true + } + } + if !found { + t.Errorf("Tuplip.power() = %v, want %v, missing %v", got, tt.want, tagSet) + } + } + }) + } +} + +func TestTuplip_failOnEmpty(t *testing.T) { + nonemptySet := mapset.NewSet(mapset.NewSet(), mapset.NewSet("alias")) + type args struct { + inputSet mapset.Set + } + tests := []struct { + name string + t Tuplip + args args + want mapset.Set + wantErr bool + }{ + { + name: "Empty Set", + args: args{mapset.NewSet()}, + wantErr: true, + }, + { + name: "Empty Power Set", + args: args{mapset.NewSet(mapset.NewSet())}, + wantErr: true, + }, + { + name: "Nonempty Power Set", + args: args{nonemptySet}, + want: nonemptySet, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.t.failOnEmpty(tt.args.inputSet) + if (err != nil) != tt.wantErr { + t.Errorf("Tuplip.failOnEmpty() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Tuplip.failOnEmpty() = %v, want %v", got, tt.want) + } + }) + } +}