File: parse.go

package info (click to toggle)
golang-github-apparentlymart-go-versions 1.0.1-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, bullseye, sid
  • size: 292 kB
  • sloc: makefile: 2
file content (243 lines) | stat: -rw-r--r-- 8,377 bytes parent folder | download
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
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
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),
	}
}