github.com/blend/go-sdk@v1.20240719.1/semver/constraint.go (about)

     1  /*
     2  
     3  Copyright (c) 2024 - Present. Blend Labs, Inc. All rights reserved
     4  Use of this source code is governed by a MIT license that can be found in the LICENSE file.
     5  
     6  */
     7  
     8  package semver
     9  
    10  import (
    11  	"fmt"
    12  	"reflect"
    13  	"regexp"
    14  	"strings"
    15  )
    16  
    17  // Constraint represents a single constraint for a version, such as
    18  // ">= 1.0".
    19  type Constraint struct {
    20  	f        constraintFunc
    21  	check    *Version
    22  	original string
    23  }
    24  
    25  // Constraints is a slice of constraints. We make a custom type so that
    26  // we can add methods to it.
    27  type Constraints []*Constraint
    28  
    29  type constraintFunc func(v, c *Version) bool
    30  
    31  var constraintOperators map[string]constraintFunc
    32  
    33  var constraintRegexp *regexp.Regexp
    34  
    35  func init() {
    36  	constraintOperators = map[string]constraintFunc{
    37  		"":   constraintEqual,
    38  		"=":  constraintEqual,
    39  		"!=": constraintNotEqual,
    40  		">":  constraintGreaterThan,
    41  		"<":  constraintLessThan,
    42  		">=": constraintGreaterThanEqual,
    43  		"<=": constraintLessThanEqual,
    44  		"~>": constraintPessimistic,
    45  	}
    46  
    47  	ops := make([]string, 0, len(constraintOperators))
    48  	for k := range constraintOperators {
    49  		ops = append(ops, regexp.QuoteMeta(k))
    50  	}
    51  
    52  	constraintRegexp = regexp.MustCompile(fmt.Sprintf(
    53  		`^\s*(%s)\s*(%s)\s*$`,
    54  		strings.Join(ops, "|"),
    55  		VersionRegexpRaw))
    56  }
    57  
    58  // NewConstraint will parse one or more constraints from the given
    59  // constraint string. The string must be a comma-separated list of
    60  // constraints.
    61  func NewConstraint(v string) (Constraints, error) {
    62  	vs := strings.Split(v, ",")
    63  	result := make([]*Constraint, len(vs))
    64  	for i, single := range vs {
    65  		c, err := parseSingle(single)
    66  		if err != nil {
    67  			return nil, err
    68  		}
    69  
    70  		result[i] = c
    71  	}
    72  
    73  	return Constraints(result), nil
    74  }
    75  
    76  // Check tests if a version satisfies all the constraints.
    77  func (cs Constraints) Check(v *Version) bool {
    78  	for _, c := range cs {
    79  		if !c.Check(v) {
    80  			return false
    81  		}
    82  	}
    83  
    84  	return true
    85  }
    86  
    87  // Returns the string format of the constraints
    88  func (cs Constraints) String() string {
    89  	csStr := make([]string, len(cs))
    90  	for i, c := range cs {
    91  		csStr[i] = c.String()
    92  	}
    93  
    94  	return strings.Join(csStr, ",")
    95  }
    96  
    97  // Check tests if a constraint is validated by the given version.
    98  func (c *Constraint) Check(v *Version) bool {
    99  	return c.f(v, c.check)
   100  }
   101  
   102  // String returns the original string.
   103  func (c *Constraint) String() string {
   104  	return c.original
   105  }
   106  
   107  func parseSingle(v string) (*Constraint, error) {
   108  	matches := constraintRegexp.FindStringSubmatch(v)
   109  	if matches == nil {
   110  		return nil, fmt.Errorf("malformed constraint: %s", v)
   111  	}
   112  
   113  	check, err := NewVersion(matches[2])
   114  	if err != nil {
   115  		return nil, err
   116  	}
   117  
   118  	return &Constraint{
   119  		f:        constraintOperators[matches[1]],
   120  		check:    check,
   121  		original: v,
   122  	}, nil
   123  }
   124  
   125  func prereleaseCheck(v, c *Version) bool {
   126  	switch vPre, cPre := v.Prerelease() != "", c.Prerelease() != ""; {
   127  	case cPre && vPre:
   128  		// A constraint with a pre-release can only match a pre-release version
   129  		// with the same base segments.
   130  		return reflect.DeepEqual(c.Segments64(), v.Segments64())
   131  
   132  	case !cPre && vPre:
   133  		// A constraint without a pre-release can only match a version without a
   134  		// pre-release.
   135  		return false
   136  
   137  	case cPre && !vPre:
   138  		// OK, except with the pessimistic operator
   139  	case !cPre && !vPre:
   140  		// OK
   141  	}
   142  	return true
   143  }
   144  
   145  //-------------------------------------------------------------------
   146  // Constraint functions
   147  //-------------------------------------------------------------------
   148  
   149  func constraintEqual(v, c *Version) bool {
   150  	return v.Equal(c)
   151  }
   152  
   153  func constraintNotEqual(v, c *Version) bool {
   154  	return !v.Equal(c)
   155  }
   156  
   157  func constraintGreaterThan(v, c *Version) bool {
   158  	return prereleaseCheck(v, c) && v.Compare(c) == 1
   159  }
   160  
   161  func constraintLessThan(v, c *Version) bool {
   162  	return prereleaseCheck(v, c) && v.Compare(c) == -1
   163  }
   164  
   165  func constraintGreaterThanEqual(v, c *Version) bool {
   166  	return prereleaseCheck(v, c) && v.Compare(c) >= 0
   167  }
   168  
   169  func constraintLessThanEqual(v, c *Version) bool {
   170  	return prereleaseCheck(v, c) && v.Compare(c) <= 0
   171  }
   172  
   173  func constraintPessimistic(v, c *Version) bool {
   174  	// Using a pessimistic constraint with a pre-release, restricts versions to pre-releases
   175  	if !prereleaseCheck(v, c) || (c.Prerelease() != "" && v.Prerelease() == "") {
   176  		return false
   177  	}
   178  
   179  	// If the version being checked is naturally less than the constraint, then there
   180  	// is no way for the version to be valid against the constraint
   181  	if v.LessThan(c) {
   182  		return false
   183  	}
   184  	// We'll use this more than once, so grab the length now so it's a little cleaner
   185  	// to write the later checks
   186  	cs := len(c.segments)
   187  
   188  	// If the version being checked has less specificity than the constraint, then there
   189  	// is no way for the version to be valid against the constraint
   190  	if cs > len(v.segments) {
   191  		return false
   192  	}
   193  
   194  	// Check the segments in the constraint against those in the version. If the version
   195  	// being checked, at any point, does not have the same values in each index of the
   196  	// constraints segments, then it cannot be valid against the constraint.
   197  	for i := 0; i < c.si-1; i++ {
   198  		if v.segments[i] != c.segments[i] {
   199  			return false
   200  		}
   201  	}
   202  
   203  	// Check the last part of the segment in the constraint. If the version segment at
   204  	// this index is less than the constraints segment at this index, then it cannot
   205  	// be valid against the constraint
   206  	return c.segments[cs-1] <= v.segments[cs-1]
   207  }