package versions import ( "fmt" "github.com/apparentlymart/go-versions/versions/constraints" ) // ParseVersion attempts to parse the given string as a semantic version // specification, and returns the result if successful. // // If the given string is not parseable then an error is returned that is // suitable for display directly to a hypothetical end-user that provided this // version string, as long as they can read English. func ParseVersion(s string) (Version, error) { spec, err := constraints.ParseExactVersion(s) if err != nil { return Unspecified, err } return versionFromExactVersionSpec(spec), nil } // MustParseVersion is the same as ParseVersion except that it will panic // instead of returning an error. func MustParseVersion(s string) Version { v, err := ParseVersion(s) if err != nil { panic(err) } return v } // MeetingConstraints returns a version set that contains all of the versions // that meet the given constraints, specified using the Spec type from the // constraints package. // // The resulting Set has all pre-release versions excluded, except any that // are explicitly mentioned as exact selections. For example, the constraint // "2.0.0-beta1 || >2" contains 2.0.0-beta1 but not 2.0.0-beta2 or 3.0.0-beta1. // This additional constraint on pre-releases can be avoided by calling // MeetingConstraintsExact instead, at which point the caller can apply other // logic to deal with prereleases. // // This function expects an internally-consistent Spec like what would be // generated by that package's constraint parsers. Behavior is undefined -- // including the possibility of panics -- if specs are hand-created and the // expected invariants aren't met. func MeetingConstraints(spec constraints.Spec) Set { exact := MeetingConstraintsExact(spec) reqd := exact.AllRequested().List() set := Intersection(Released, exact) reqd = reqd.Filter(Prerelease) if len(reqd) != 0 { set = Union(Selection(reqd...), set) } return set } // MeetingConstraintsExact is like MeetingConstraints except that it doesn't // apply the extra rules to exclude pre-release versions that are not // explicitly requested. // // This means that given a constraint ">=1.0.0 <2.0.0" a hypothetical version // 2.0.0-beta1 _is_ in the returned set, because prerelease versions have // lower precedence than their corresponding release. // // A caller can use this to implement its own specialized handling of // pre-release versions by applying additional set operations to the result, // such as intersecting it with the predefined set versions.Released to // remove prerelease versions altogether. func MeetingConstraintsExact(spec constraints.Spec) Set { if spec == nil { return All } switch ts := spec.(type) { case constraints.VersionSpec: lowerBound, upperBound := ts.ConstraintBounds() switch lowerBound.Operator { case constraints.OpUnconstrained: return All case constraints.OpEqual: return Only(versionFromExactVersionSpec(lowerBound.Boundary)) default: return AtLeast( versionFromExactVersionSpec(lowerBound.Boundary), ).Intersection( OlderThan(versionFromExactVersionSpec(upperBound.Boundary))) } case constraints.SelectionSpec: lower := ts.Boundary.ConstrainToZero() if ts.Operator != constraints.OpEqual && ts.Operator != constraints.OpNotEqual { lower.Metadata = "" // metadata is only considered for exact matches } switch ts.Operator { case constraints.OpUnconstrained: // Degenerate case, but we'll allow it. return All case constraints.OpMatch: // The match operator uses the constraints implied by the // Boundary version spec as the specification. // Note that we discard "lower" in this case, because we do want // to match our metadata if it's specified. return MeetingConstraintsExact(ts.Boundary) case constraints.OpEqual, constraints.OpNotEqual: set := Only(versionFromExactVersionSpec(lower)) if ts.Operator == constraints.OpNotEqual { // We want everything _except_ what's in our set, then. set = All.Subtract(set) } return set case constraints.OpGreaterThan: return NewerThan(versionFromExactVersionSpec(lower)) case constraints.OpGreaterThanOrEqual: return AtLeast(versionFromExactVersionSpec(lower)) case constraints.OpLessThan: return OlderThan(versionFromExactVersionSpec(lower)) case constraints.OpLessThanOrEqual: return AtMost(versionFromExactVersionSpec(lower)) case constraints.OpGreaterThanOrEqualMinorOnly: upper := lower upper.Major.Num++ upper.Minor.Num = 0 upper.Patch.Num = 0 upper.Prerelease = "" return AtLeast( versionFromExactVersionSpec(lower), ).Intersection( OlderThan(versionFromExactVersionSpec(upper))) case constraints.OpGreaterThanOrEqualPatchOnly: upper := lower upper.Minor.Num++ upper.Patch.Num = 0 upper.Prerelease = "" return AtLeast( versionFromExactVersionSpec(lower), ).Intersection( OlderThan(versionFromExactVersionSpec(upper))) default: panic(fmt.Errorf("unsupported constraints.SelectionOp %s", ts.Operator)) } case constraints.UnionSpec: if len(ts) == 0 { return All } if len(ts) == 1 { return MeetingConstraintsExact(ts[0]) } union := make(setUnion, len(ts)) for i, subSpec := range ts { union[i] = MeetingConstraintsExact(subSpec).setI } return Set{setI: union} case constraints.IntersectionSpec: if len(ts) == 0 { return All } if len(ts) == 1 { return MeetingConstraintsExact(ts[0]) } intersection := make(setIntersection, len(ts)) for i, subSpec := range ts { intersection[i] = MeetingConstraintsExact(subSpec).setI } return Set{setI: intersection} default: // should never happen because the above cases are exhaustive for // all valid constraint implementations. panic(fmt.Errorf("unsupported constraints.Spec implementation %T", spec)) } } // MeetingConstraintsString attempts to parse the given spec as a constraints // string in our canonical format, which is most similar to the syntax used by // npm, Go's "dep" tool, Rust's "cargo", etc. // // This is a covenience wrapper around calling constraints.Parse and then // passing the result to MeetingConstraints. Call into the constraints package // yourself for access to the constraint tree. // // If unsuccessful, the error from the underlying parser is returned verbatim. // Parser errors are suitable for showing to an end-user in situations where // the given spec came from user input. func MeetingConstraintsString(spec string) (Set, error) { s, err := constraints.Parse(spec) if err != nil { return None, err } return MeetingConstraints(s), nil } // MeetingConstraintsStringRuby attempts to parse the given spec as a // "Ruby-style" version constraint string, and returns the set of versions // that match the constraint if successful. // // If unsuccessful, the error from the underlying parser is returned verbatim. // Parser errors are suitable for showing to an end-user in situations where // the given spec came from user input. // // "Ruby-style" here is not a promise of exact compatibility with rubygems // or any other Ruby tools. Rather, it refers to this parser using a syntax // that is intended to feel familiar to those who are familiar with rubygems // syntax. // // Constraints are parsed in "multi" mode, allowing multiple comma-separated // constraints that are combined with the Intersection operator. For more // control over the parsing process, use the constraints package API directly // and then call MeetingConstraints. func MeetingConstraintsStringRuby(spec string) (Set, error) { s, err := constraints.ParseRubyStyleMulti(spec) if err != nil { return None, err } return MeetingConstraints(s), nil } // MustMakeSet can be used to wrap any function that returns a set and an error // to make it panic if an error occurs and return the set otherwise. // // This is intended for tests and other situations where input is from // known-good constants. func MustMakeSet(set Set, err error) Set { if err != nil { panic(err) } return set } func versionFromExactVersionSpec(spec constraints.VersionSpec) Version { return Version{ Major: spec.Major.Num, Minor: spec.Minor.Num, Patch: spec.Patch.Num, Prerelease: VersionExtra(spec.Prerelease), Metadata: VersionExtra(spec.Metadata), } }