1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184
|
package constraints
import (
"fmt"
"strings"
)
// ParseRubyStyle parses a single selection constraint using a syntax similar
// to that used by rubygems and other Ruby tools.
//
// Exact compatibility with rubygems is not guaranteed; "ruby-style" here
// just means that users familiar with rubygems should find familiar the choice
// of operators and their meanings.
//
// ParseRubyStyle parses only a single specification, mimicking the usual
// rubygems approach of providing each selection as a separate string.
// The result can be combined with other results to create an IntersectionSpec
// that describes the effect of multiple such constraints.
func ParseRubyStyle(str string) (SelectionSpec, error) {
if strings.TrimSpace(str) == "" {
return SelectionSpec{}, fmt.Errorf("empty specification")
}
spec, remain, err := parseRubyStyle(str)
if err != nil {
return spec, err
}
if remain != "" {
remain = strings.TrimSpace(remain)
switch {
case remain == "":
return spec, fmt.Errorf("extraneous spaces at end of specification")
case strings.HasPrefix(remain, "v"):
// User seems to be trying to use a "v" prefix, like "v1.0.0"
return spec, fmt.Errorf(`a "v" prefix should not be used`)
case strings.HasPrefix(remain, "||") || strings.HasPrefix(remain, ","):
// User seems to be trying to specify multiple constraints
return spec, fmt.Errorf(`only one constraint may be specified`)
case strings.HasPrefix(remain, "-"):
// User seems to be trying to use npm-style range constraints
return spec, fmt.Errorf(`range constraints are not supported`)
default:
return spec, fmt.Errorf("invalid characters %q", remain)
}
}
return spec, nil
}
// ParseRubyStyleAll is a helper wrapper around ParseRubyStyle that accepts
// multiple selection strings and combines them together into a single
// IntersectionSpec.
func ParseRubyStyleAll(strs ...string) (IntersectionSpec, error) {
spec := make(IntersectionSpec, 0, len(strs))
for _, str := range strs {
subSpec, err := ParseRubyStyle(str)
if err != nil {
return nil, fmt.Errorf("invalid specification %q: %s", str, err)
}
spec = append(spec, subSpec)
}
return spec, nil
}
// ParseRubyStyleMulti is similar to ParseRubyStyle, but rather than parsing
// only a single selection specification it instead expects one or more
// comma-separated specifications, returning the result as an
// IntersectionSpec.
func ParseRubyStyleMulti(str string) (IntersectionSpec, error) {
var spec IntersectionSpec
remain := strings.TrimSpace(str)
for remain != "" {
if strings.TrimSpace(remain) == "" {
break
}
var subSpec SelectionSpec
var err error
var newRemain string
subSpec, newRemain, err = parseRubyStyle(remain)
consumed := remain[:len(remain)-len(newRemain)]
if err != nil {
return nil, fmt.Errorf("invalid specification %q: %s", consumed, err)
}
remain = strings.TrimSpace(newRemain)
if remain != "" {
if strings.HasPrefix(remain, "v") {
return nil, fmt.Errorf(`a "v" prefix should not be used`)
}
if !strings.HasPrefix(remain, ",") {
return nil, fmt.Errorf("missing comma after %q", consumed)
}
// Eat the separator comma
remain = strings.TrimSpace(remain[1:])
}
spec = append(spec, subSpec)
}
return spec, nil
}
// parseRubyStyle parses a ruby-style constraint from the prefix of the given
// string and returns the remaining unconsumed string for the caller to use
// for further processing.
func parseRubyStyle(str string) (SelectionSpec, string, error) {
raw, remain := scanConstraint(str)
var spec SelectionSpec
switch raw.op {
case "=", "":
spec.Operator = OpEqual
case "!=":
spec.Operator = OpNotEqual
case ">":
spec.Operator = OpGreaterThan
case ">=":
spec.Operator = OpGreaterThanOrEqual
case "<":
spec.Operator = OpLessThan
case "<=":
spec.Operator = OpLessThanOrEqual
case "~>":
// Ruby-style pessimistic can be either a minor-only or patch-only
// constraint, depending on how many digits were given.
switch raw.numCt {
case 3:
spec.Operator = OpGreaterThanOrEqualPatchOnly
default:
spec.Operator = OpGreaterThanOrEqualMinorOnly
}
case "=<":
return spec, remain, fmt.Errorf("invalid constraint operator %q; did you mean \"<=\"?", raw.op)
case "=>":
return spec, remain, fmt.Errorf("invalid constraint operator %q; did you mean \">=\"?", raw.op)
default:
return spec, remain, fmt.Errorf("invalid constraint operator %q", raw.op)
}
switch raw.sep {
case "":
// No separator is always okay. Although all of the examples in the
// rubygems docs show a space separator, the parser doesn't actually
// require it.
case " ":
if raw.op == "" {
return spec, remain, fmt.Errorf("extraneous spaces at start of specification")
}
default:
if raw.op == "" {
return spec, remain, fmt.Errorf("extraneous spaces at start of specification")
} else {
return spec, remain, fmt.Errorf("only one space is expected after the operator %q", raw.op)
}
}
if raw.numCt > 3 {
return spec, remain, fmt.Errorf("too many numbered portions; only three are allowed (major, minor, patch)")
}
// Ruby-style doesn't use explicit wildcards
for i, s := range raw.nums {
switch {
case isWildcardNum(s):
// Can't use wildcards in an exact specification
return spec, remain, fmt.Errorf("can't use wildcard for %s number; omit segments that should be unconstrained", rawNumNames[i])
}
}
if raw.pre != "" || raw.meta != "" {
// If either the prerelease or meta portions are set then any unconstrained
// segments are implied to be zero in order to guarantee constraint
// consistency.
for i, s := range raw.nums {
if s == "" {
raw.nums[i] = "0"
}
}
}
spec.Boundary = raw.VersionSpec()
return spec, remain, nil
}
|