blob: cf1b484c6198805f9b626ab4031c36ad59f3b35b [file] [log] [blame]
package gps
import (
"fmt"
"sort"
"github.com/Masterminds/semver"
)
var (
none = noneConstraint{}
any = anyConstraint{}
)
// A Constraint provides structured limitations on the versions that are
// admissible for a given project.
//
// As with Version, it has a private method because the gps's internal
// implementation of the problem is complete, and the system relies on type
// magic to operate.
type Constraint interface {
fmt.Stringer
// Matches indicates if the provided Version is allowed by the Constraint.
Matches(Version) bool
// MatchesAny indicates if the intersection of the Constraint with the
// provided Constraint would yield a Constraint that could allow *any*
// Version.
MatchesAny(Constraint) bool
// Intersect computes the intersection of the Constraint with the provided
// Constraint.
Intersect(Constraint) Constraint
_private()
}
func (semverConstraint) _private() {}
func (anyConstraint) _private() {}
func (noneConstraint) _private() {}
// NewSemverConstraint attempts to construct a semver Constraint object from the
// input string.
//
// If the input string cannot be made into a valid semver Constraint, an error
// is returned.
func NewSemverConstraint(body string) (Constraint, error) {
c, err := semver.NewConstraint(body)
if err != nil {
return nil, err
}
// If we got a simple semver.Version, simplify by returning our
// corresponding type
if sv, ok := c.(*semver.Version); ok {
return semVersion{sv: sv}, nil
}
return semverConstraint{c: c}, nil
}
type semverConstraint struct {
c semver.Constraint
}
func (c semverConstraint) String() string {
return c.c.String()
}
func (c semverConstraint) Matches(v Version) bool {
switch tv := v.(type) {
case versionTypeUnion:
for _, elem := range tv {
if c.Matches(elem) {
return true
}
}
case semVersion:
return c.c.Matches(tv.sv) == nil
case versionPair:
if tv2, ok := tv.v.(semVersion); ok {
return c.c.Matches(tv2.sv) == nil
}
}
return false
}
func (c semverConstraint) MatchesAny(c2 Constraint) bool {
return c.Intersect(c2) != none
}
func (c semverConstraint) Intersect(c2 Constraint) Constraint {
switch tc := c2.(type) {
case anyConstraint:
return c
case versionTypeUnion:
for _, elem := range tc {
if rc := c.Intersect(elem); rc != none {
return rc
}
}
case semverConstraint:
rc := c.c.Intersect(tc.c)
if !semver.IsNone(rc) {
return semverConstraint{c: rc}
}
case semVersion:
rc := c.c.Intersect(tc.sv)
if !semver.IsNone(rc) {
// If single version intersected with constraint, we know the result
// must be the single version, so just return it back out
return c2
}
case versionPair:
if tc2, ok := tc.v.(semVersion); ok {
rc := c.c.Intersect(tc2.sv)
if !semver.IsNone(rc) {
// same reasoning as previous case
return c2
}
}
}
return none
}
// IsAny indicates if the provided constraint is the wildcard "Any" constraint.
func IsAny(c Constraint) bool {
_, ok := c.(anyConstraint)
return ok
}
// Any returns a constraint that will match anything.
func Any() Constraint {
return anyConstraint{}
}
// anyConstraint is an unbounded constraint - it matches all other types of
// constraints. It mirrors the behavior of the semver package's any type.
type anyConstraint struct{}
func (anyConstraint) String() string {
return "*"
}
func (anyConstraint) Matches(Version) bool {
return true
}
func (anyConstraint) MatchesAny(Constraint) bool {
return true
}
func (anyConstraint) Intersect(c Constraint) Constraint {
return c
}
// noneConstraint is the empty set - it matches no versions. It mirrors the
// behavior of the semver package's none type.
type noneConstraint struct{}
func (noneConstraint) String() string {
return ""
}
func (noneConstraint) Matches(Version) bool {
return false
}
func (noneConstraint) MatchesAny(Constraint) bool {
return false
}
func (noneConstraint) Intersect(Constraint) Constraint {
return none
}
// A ProjectConstraint combines a ProjectIdentifier with a Constraint. It
// indicates that, if packages contained in the ProjectIdentifier enter the
// depgraph, they must do so at a version that is allowed by the Constraint.
type ProjectConstraint struct {
Ident ProjectIdentifier
Constraint Constraint
}
type workingConstraint struct {
Ident ProjectIdentifier
Constraint Constraint
overrNet, overrConstraint bool
}
type ProjectConstraints map[ProjectRoot]ProjectProperties
func pcSliceToMap(l []ProjectConstraint, r ...[]ProjectConstraint) ProjectConstraints {
final := make(ProjectConstraints)
for _, pc := range l {
final[pc.Ident.ProjectRoot] = ProjectProperties{
NetworkName: pc.Ident.NetworkName,
Constraint: pc.Constraint,
}
}
for _, pcs := range r {
for _, pc := range pcs {
if pp, exists := final[pc.Ident.ProjectRoot]; exists {
// Technically this should be done through a bridge for
// cross-version-type matching...but this is a one off for root and
// that's just ridiculous for this.
pp.Constraint = pp.Constraint.Intersect(pc.Constraint)
final[pc.Ident.ProjectRoot] = pp
} else {
final[pc.Ident.ProjectRoot] = ProjectProperties{
NetworkName: pc.Ident.NetworkName,
Constraint: pc.Constraint,
}
}
}
}
return final
}
func (m ProjectConstraints) asSortedSlice() []ProjectConstraint {
pcs := make([]ProjectConstraint, len(m))
k := 0
for pr, pp := range m {
pcs[k] = ProjectConstraint{
Ident: ProjectIdentifier{
ProjectRoot: pr,
NetworkName: pp.NetworkName,
},
Constraint: pp.Constraint,
}
k++
}
sort.Stable(sortedConstraints(pcs))
return pcs
}
// overrideAll treats the ProjectConstraints map as an override map, and applies
// overridden values to the input.
//
// A slice of workingConstraint is returned, allowing differentiation between
// values that were or were not overridden.
func (m ProjectConstraints) overrideAll(in []ProjectConstraint) (out []workingConstraint) {
out = make([]workingConstraint, len(in))
k := 0
for _, pc := range in {
out[k] = m.override(pc)
k++
}
return
}
// override replaces a single ProjectConstraint with a workingConstraint,
// overriding its values if a corresponding entry exists in the
// ProjectConstraints map.
func (m ProjectConstraints) override(pc ProjectConstraint) workingConstraint {
wc := workingConstraint{
Ident: pc.Ident,
Constraint: pc.Constraint,
}
if pp, has := m[pc.Ident.ProjectRoot]; has {
// The rule for overrides is that *any* non-zero value for the prop
// should be considered an override, even if it's equal to what's
// already there.
if pp.Constraint != nil {
wc.Constraint = pp.Constraint
wc.overrConstraint = true
}
if pp.NetworkName != "" {
wc.Ident.NetworkName = pp.NetworkName
wc.overrNet = true
}
}
return wc
}
type sortedConstraints []ProjectConstraint
func (s sortedConstraints) Len() int {
return len(s)
}
func (s sortedConstraints) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s sortedConstraints) Less(i, j int) bool {
return s[i].Ident.less(s[j].Ident)
}