Skip to content

Commit

Permalink
refactored library
Browse files Browse the repository at this point in the history
  • Loading branch information
matfax committed Jan 6, 2019
1 parent 83b8834 commit 7f69244
Show file tree
Hide file tree
Showing 6 changed files with 729 additions and 699 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ go get -u github.com/gofunky/tuplip
docker pull gofunky/tuplip
```

## Using tulip
## Using tuplip

### Using the binary

Expand Down
3 changes: 2 additions & 1 deletion cmd/tuplip/exec.go → cmd/tuplip/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
226 changes: 37 additions & 189 deletions pkg/tupliplib/lib.go
Original file line number Diff line number Diff line change
@@ -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 = ":"

Expand All @@ -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
}
Loading

0 comments on commit 7f69244

Please sign in to comment.