diff --git a/README.md b/README.md index 109fa2b..949ed6d 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A library for handling a superset of semantic versioning in golang. ## Documentation and examples -See the godoc here: https://godoc.org/go.bug.st/relaxed-semver +See the godoc here: ## Semantic versioning specification followed in this library @@ -39,20 +39,22 @@ To parse a `RelaxedVersion` you can use the `ParseRelaxed` function. Dependency version matching can be specified via version constraints, which might be a version range or an exact version. -The following operators are supported: - -| | | -| -------- | ------------------------ | -| `=` | equal to | -| `>` | greater than | -| `>=` | greater than or equal to | -| `<` | less than | -| `<=` | less than or equal to | -| `^` | compatible-with | -| `!` | NOT | -| `&&` | AND | -| `\|\|` | OR | -| `(`, `)` | constraint group | +The following range operators are supported: + +| Operator | Meaning | +| ----------- | ------------------------ | +| `^` or None | compatible-with | +| `=` | equal to | +| `>` | greater than | +| `>=` | greater than or equal to | +| `<` | less than | +| `<=` | less than or equal to | +| `!` | NOT | +| `&&` | AND | +| `\|\|` | OR | +| `(`, `)` | constraint group | + +In a constraint if a version is written without the operator the "compatible-with" operator is applied. ### Examples @@ -82,6 +84,8 @@ constraints conditions would match as follows: | `<1.0.0 \|\| >2.0.0` | `0.1.0`, `0.1.1`, `0.2.0`, `2.0.5`, `2.0.6`, `2.1.0`, `3.0.0` | | `(>0.1.0 && <2.0.0) \|\| >2.0.5` | `0.1.1`, `0.2.0`, `1.0.0`, `2.0.6`, `2.1.0`, `3.0.0` | | `^2.0.5` | `2.0.5`, `2.0.6`, `2.1.0` | +| `2.0.5` | `2.0.5`, `2.0.6`, `2.1.0` (same as `^2.0.5`) | +| `2.0.5 && !=2.0.6` | `2.0.5`, `2.1.0` (same as `^2.0.5 && !=2.0.6`) | | `^0.1.0` | `0.1.0`, `0.1.1` | ## Json parsable diff --git a/constraints.go b/constraints.go index ce882ae..4c4ccf7 100644 --- a/constraints.go +++ b/constraints.go @@ -53,11 +53,11 @@ func ParseConstraint(in string) (Constraint, error) { n := peek() if !isIdentifier(n) && !isVersionSeparator(n) { if start == curr { - return nil, fmt.Errorf("invalid version") + return nil, fmt.Errorf("invalid version: unexpected char '%c'", rune(n)) } return Parse(in[start:curr]) } - curr++ + next() } } @@ -66,36 +66,41 @@ func ParseConstraint(in string) (Constraint, error) { terminal = func() (Constraint, error) { skipSpace() - switch next() { + switch peek() { case '!': + next() expr, err := terminal() if err != nil { return nil, err } return &Not{expr}, nil case '(': + next() expr, err := constraint() if err != nil { return nil, err } skipSpace() if c := next(); c != ')' { - return nil, fmt.Errorf("unexpected char at: %s", in[curr-1:]) + return nil, fmt.Errorf("unexpected char: %c", rune(c)) } return expr, nil case '=': + next() v, err := version() if err != nil { return nil, err } return &Equals{v}, nil case '^': + next() v, err := version() if err != nil { return nil, err } return &CompatibleWith{v}, nil case '>': + next() if peek() == '=' { next() v, err := version() @@ -111,6 +116,7 @@ func ParseConstraint(in string) (Constraint, error) { return &GreaterThan{v}, nil } case '<': + next() if peek() == '=' { next() v, err := version() @@ -126,7 +132,11 @@ func ParseConstraint(in string) (Constraint, error) { return &LessThan{v}, nil } default: - return nil, fmt.Errorf("unexpected char at: %s", in[curr-1:]) + v, err := version() + if err != nil { + return nil, err + } + return &CompatibleWith{v}, nil } } diff --git a/constraints_test.go b/constraints_test.go index cf6adab..b73e1ce 100644 --- a/constraints_test.go +++ b/constraints_test.go @@ -99,9 +99,17 @@ func TestConstraintsParser(t *testing.T) { good := []goodStringTest{ {"", ""}, // always true {"=1.3.0", "=1.3.0"}, + {"!=1.3.0", "!(=1.3.0)"}, + {" !=1.3.0", "!(=1.3.0)"}, + {"! =1.3.0", "!(=1.3.0)"}, + {"1.3.0", "^1.3.0"}, + {"!1.0.0", "!(^1.0.0)"}, {" =1.3.0 ", "=1.3.0"}, + {" 1.3.0 ", "^1.3.0"}, {"=1.3.0 ", "=1.3.0"}, + {"1.3.0 ", "^1.3.0"}, {" =1.3.0", "=1.3.0"}, + {" 1.3.0", "^1.3.0"}, {">=1.3.0", ">=1.3.0"}, {">1.3.0", ">1.3.0"}, {"<=1.3.0", "<=1.3.0"}, @@ -111,16 +119,37 @@ func TestConstraintsParser(t *testing.T) { {"^1.3.0 ", "^1.3.0"}, {" ^1.3.0 ", "^1.3.0"}, {"(=1.4.0)", "=1.4.0"}, + {"(1.4.0)", "^1.4.0"}, {"!(=1.4.0)", "!(=1.4.0)"}, + {"!(1.4.0)", "!(^1.4.0)"}, {"!(((=1.4.0)))", "!(=1.4.0)"}, + {"!(((1.4.0)))", "!(^1.4.0)"}, {"=1.2.4 && =1.3.0", "(=1.2.4 && =1.3.0)"}, {"=1.2.4 && ^1.3.0", "(=1.2.4 && ^1.3.0)"}, + {"1.2.4 && 1.3.0", "(^1.2.4 && ^1.3.0)"}, {"=1.2.4 && =1.3.0 && =1.2.0", "(=1.2.4 && =1.3.0 && =1.2.0)"}, + {"1.2.4 && 1.3.0 && 1.2.0", "(^1.2.4 && ^1.3.0 && ^1.2.0)"}, {"=1.2.4 && =1.3.0 || =1.2.0", "((=1.2.4 && =1.3.0) || =1.2.0)"}, + {"1.2.4 && 1.3.0 || 1.2.0", "((^1.2.4 && ^1.3.0) || ^1.2.0)"}, {"=1.2.4 || =1.3.0 && =1.2.0", "(=1.2.4 || (=1.3.0 && =1.2.0))"}, {"(=1.2.4 || =1.3.0) && =1.2.0", "((=1.2.4 || =1.3.0) && =1.2.0)"}, + {"(1.2.4 || 1.3.0) && 1.2.0", "((^1.2.4 || ^1.3.0) && ^1.2.0)"}, {"(=1.2.4 || !>1.3.0) && =1.2.0", "((=1.2.4 || !(>1.3.0)) && =1.2.0)"}, + {"(1.2.4 || !>1.3.0) && 1.2.0", "((^1.2.4 || !(>1.3.0)) && ^1.2.0)"}, {"!(=1.2.4 || >1.3.0) && =1.2.0", "(!(=1.2.4 || >1.3.0) && =1.2.0)"}, + {"!(1.2.4 || >1.3.0) && 1.2.0", "(!(^1.2.4 || >1.3.0) && ^1.2.0)"}, + {">1.0.0 && 2.0.0", "(>1.0.0 && ^2.0.0)"}, + {">1.0.0 && =2.0.0", "(>1.0.0 && =2.0.0)"}, + {">1.0.0 || 2.0.0", "(>1.0.0 || ^2.0.0)"}, + {">1.0.0 || =2.0.0", "(>1.0.0 || =2.0.0)"}, + {"(>1.0.0) || 2.0.0", "(>1.0.0 || ^2.0.0)"}, + {"(>1.0.0) || =2.0.0", "(>1.0.0 || =2.0.0)"}, + {">1.0.0 || (2.0.0)", "(>1.0.0 || ^2.0.0)"}, + {">1.0.0 || (=2.0.0)", "(>1.0.0 || =2.0.0)"}, + {"!>1.0.0 || (=2.0.0)", "(!(>1.0.0) || =2.0.0)"}, + {"!(>1.0.0 || =2.0.0)", "!(>1.0.0 || =2.0.0)"}, + {"((>1.0.0) || (2.0.0))", "(>1.0.0 || ^2.0.0)"}, + {"((>1.0.0) || (=2.0.0))", "(>1.0.0 || =2.0.0)"}, } for i, test := range good { in := test.In @@ -134,8 +163,8 @@ func TestConstraintsParser(t *testing.T) { } bad := []string{ - "1.0.0", "= 1.0.0", + "!= 1.3.9", ">= 1.0.0", "> 1.0.0", "<= 1.0.0", @@ -144,19 +173,21 @@ func TestConstraintsParser(t *testing.T) { ">1.0.0 =2.0.0", ">1.0.0 &", "^1.1.1.1", - "!1.0.0", - ">1.0.0 && 2.0.0", ">1.0.0 | =2.0.0", "(>1.0.0 | =2.0.0)", "(>1.0.0 || =2.0.0", - ">1.0.0 || 2.0.0", + "!1.0.0.0", + "!1.0.0 && !1.0.0.0", + "(!1.0.0 && !1.0.0", + "!1.0.0 || !1.0.0.0", + "(!1.0.0 || !1.0.0", } for i, s := range bad { in := s t.Run(fmt.Sprintf("BadString%03d", i), func(t *testing.T) { p, err := ParseConstraint(in) - require.Nil(t, p) - require.Error(t, err) + require.Nil(t, p, "parsing: %s", in) + require.Error(t, err, "parsing: %s", in) fmt.Printf("'%s' parse error: %s\n", in, err) }) }