github.com/snyk/vervet/v6@v6.2.4/version.go (about)

     1  // Package vervet supports opinionated API versioning tools.
     2  package vervet
     3  
     4  import (
     5  	"fmt"
     6  	"os"
     7  	"sort"
     8  	"strings"
     9  	"time"
    10  )
    11  
    12  var timeNow = time.Now
    13  
    14  // Version defines an API version. API versions may be dates of the form
    15  // "YYYY-mm-dd", or stability tags "beta", "experimental".
    16  type Version struct {
    17  	Date      time.Time
    18  	Stability Stability
    19  }
    20  
    21  // DateString returns the string representation of the version date in
    22  // YYYY-mm-dd form.
    23  func (v Version) DateString() string {
    24  	return v.Date.Format("2006-01-02")
    25  }
    26  
    27  // String returns the string representation of the version in
    28  // YYYY-mm-dd~Stability form. This method will panic if the value is empty.
    29  func (v Version) String() string {
    30  	d := v.Date.Format("2006-01-02")
    31  	if v.Stability != StabilityGA {
    32  		return d + "~" + v.Stability.String()
    33  	}
    34  	return d
    35  }
    36  
    37  // AddDays returns the version corresponding to adding the given number of days
    38  // to the version date.
    39  func (v Version) AddDays(days int) Version {
    40  	return Version{
    41  		Date:      v.Date.AddDate(0, 0, days),
    42  		Stability: v.Stability,
    43  	}
    44  }
    45  
    46  // Stability defines the stability level of the version.
    47  type Stability int
    48  
    49  const (
    50  	stabilityUndefined Stability = iota
    51  
    52  	// StabilityWIP means the API is a work-in-progress and not yet ready.
    53  	StabilityWIP Stability = iota
    54  
    55  	// StabilityExperimental means the API is experimental and still subject to
    56  	// drastic change.
    57  	StabilityExperimental Stability = iota
    58  
    59  	// StabilityBeta means the API is becoming more stable, but may undergo some
    60  	// final changes before being released.
    61  	StabilityBeta Stability = iota
    62  
    63  	// StabilityGA means the API has been released and will not change.
    64  	StabilityGA Stability = iota
    65  
    66  	numStabilityLevels = iota
    67  )
    68  
    69  // String returns a string representation of the stability level. This method
    70  // will panic if the value is empty.
    71  func (s Stability) String() string {
    72  	switch s {
    73  	case StabilityWIP:
    74  		return "wip"
    75  	case StabilityExperimental:
    76  		return "experimental"
    77  	case StabilityBeta:
    78  		return "beta"
    79  	case StabilityGA:
    80  		return "ga"
    81  	default:
    82  		panic(fmt.Sprintf("invalid stability (%d)", int(s)))
    83  	}
    84  }
    85  
    86  // ParseVersion parses a version string into a Version type, returning an error
    87  // if the string is invalid.
    88  func ParseVersion(s string) (Version, error) {
    89  	parts := strings.Split(s, "~")
    90  	if len(parts) < 1 {
    91  		return Version{}, fmt.Errorf("invalid version %q", s)
    92  	}
    93  	d, err := time.ParseInLocation("2006-01-02", parts[0], time.UTC)
    94  	if err != nil {
    95  		return Version{}, fmt.Errorf("invalid version %q", s)
    96  	}
    97  	stab := StabilityGA
    98  	if len(parts) > 1 {
    99  		stab, err = ParseStability(parts[1])
   100  		if err != nil {
   101  			return Version{}, err
   102  		}
   103  	}
   104  	return Version{Date: d.UTC(), Stability: stab}, nil
   105  }
   106  
   107  // MustParseVersion parses a version string into a Version type, panicking if
   108  // the string is invalid.
   109  func MustParseVersion(s string) Version {
   110  	v, err := ParseVersion(s)
   111  	if err != nil {
   112  		panic(err)
   113  	}
   114  	return v
   115  }
   116  
   117  // ParseStability parses a stability string into a Stability type, returning an
   118  // error if the string is invalid.
   119  func ParseStability(s string) (Stability, error) {
   120  	switch s {
   121  	case "wip":
   122  		return StabilityWIP, nil
   123  	case "experimental":
   124  		return StabilityExperimental, nil
   125  	case "beta":
   126  		return StabilityBeta, nil
   127  	case "ga":
   128  		return StabilityGA, nil
   129  	default:
   130  		return stabilityUndefined, fmt.Errorf("invalid stability %q", s)
   131  	}
   132  }
   133  
   134  // MustParseStability parses a stability string into a Stability type,
   135  // panicking if the string is invalid.
   136  func MustParseStability(s string) Stability {
   137  	stab, err := ParseStability(s)
   138  	if err != nil {
   139  		panic(err)
   140  	}
   141  	return stab
   142  }
   143  
   144  // Compare returns -1 if the given stability level is less than, 0 if equal to,
   145  // and 1 if greater than the caller target stability level.
   146  func (s Stability) Compare(sr Stability) int {
   147  	if s < sr {
   148  		return -1
   149  	} else if s > sr {
   150  		return 1
   151  	}
   152  	return 0
   153  }
   154  
   155  func (s Stability) Resolvable() []Stability {
   156  	// We do not route to WIP paths unless explicitly requested
   157  	switch s {
   158  	case StabilityExperimental:
   159  		return []Stability{StabilityExperimental}
   160  	case StabilityBeta:
   161  		return []Stability{StabilityExperimental, StabilityBeta}
   162  	case StabilityGA:
   163  		return []Stability{StabilityExperimental, StabilityBeta, StabilityGA}
   164  	}
   165  	return []Stability{s}
   166  }
   167  
   168  // Compare returns -1 if the given version is less than, 0 if equal to, and 1
   169  // if greater than the caller target version.
   170  func (v Version) Compare(vr Version) int {
   171  	dateCmp, stabilityCmp := v.compareDateStability(&vr)
   172  	if dateCmp != 0 {
   173  		return dateCmp
   174  	}
   175  	return stabilityCmp
   176  }
   177  
   178  // DeprecatedBy returns true if the given version deprecates the caller target
   179  // version.
   180  func (v Version) DeprecatedBy(vr Version) bool {
   181  	dateCmp, stabilityCmp := v.compareDateStability(&vr)
   182  	// A version is deprecated by a newer version of equal or greater stability.
   183  	return dateCmp == -1 && stabilityCmp <= 0
   184  }
   185  
   186  const (
   187  	// SunsetWIP is the duration past deprecation after which a work-in-progress version may be sunset.
   188  	SunsetWIP = 0
   189  
   190  	// SunsetExperimental is the duration past deprecation after which an experimental version may be sunset.
   191  	SunsetExperimental = 24 * time.Hour
   192  
   193  	// SunsetBeta is the duration past deprecation after which a beta version may be sunset.
   194  	SunsetBeta = 91 * 24 * time.Hour
   195  
   196  	// SunsetGA is the duration past deprecation after which a GA version may be sunset.
   197  	SunsetGA = 181 * 24 * time.Hour
   198  )
   199  
   200  // Sunset returns, given a potentially deprecating version, the eligible sunset
   201  // date and whether the caller target version would actually be deprecated and
   202  // sunset by the given version.
   203  func (v Version) Sunset(vr Version) (time.Time, bool) {
   204  	if !v.DeprecatedBy(vr) {
   205  		return time.Time{}, false
   206  	}
   207  	switch v.Stability {
   208  	case StabilityWIP:
   209  		return vr.Date.Add(SunsetWIP), true
   210  	case StabilityExperimental:
   211  		return vr.Date.Add(SunsetExperimental), true
   212  	case StabilityBeta:
   213  		return vr.Date.Add(SunsetBeta), true
   214  	case StabilityGA:
   215  		return vr.Date.Add(SunsetGA), true
   216  	default:
   217  		return time.Time{}, false
   218  	}
   219  }
   220  
   221  // compareDateStability returns the comparison of both the date and stability
   222  // between two versions. Used internally where these need to be evaluated
   223  // independently, such as when searching for the best matching version.
   224  func (v *Version) compareDateStability(vr *Version) (int, int) {
   225  	dateCmp := 0
   226  	if v.Date.Before(vr.Date) {
   227  		dateCmp = -1
   228  	} else if v.Date.After(vr.Date) {
   229  		dateCmp = 1
   230  	}
   231  	stabilityCmp := v.Stability.Compare(vr.Stability)
   232  	return dateCmp, stabilityCmp
   233  }
   234  
   235  // VersionDateStrings returns a slice of distinct version date strings for a
   236  // slice of Versions. Consecutive duplicate dates are removed.
   237  func VersionDateStrings(vs []Version) []string {
   238  	var result []string
   239  	for i := range vs {
   240  		ds := vs[i].DateString()
   241  		if len(result) == 0 || result[len(result)-1] != ds {
   242  			result = append(result, ds)
   243  		}
   244  	}
   245  	return result
   246  }
   247  
   248  // VersionSlice is a sortable slice of Versions.
   249  type VersionSlice []Version
   250  
   251  // VersionIndex provides a search over versions, resolving which version is in
   252  // effect for a given date and stability level.
   253  type VersionIndex struct {
   254  	effectiveVersions []effectiveVersion
   255  	versions          VersionSlice
   256  }
   257  
   258  type effectiveVersion struct {
   259  	date        time.Time
   260  	stabilities [numStabilityLevels]time.Time
   261  }
   262  
   263  // NewVersionIndex returns a new VersionIndex of the given versions. The given
   264  // VersionSlice will be sorted.
   265  func NewVersionIndex(vs VersionSlice) (vi VersionIndex) {
   266  	sort.Sort(vs)
   267  	vi.versions = make(VersionSlice, len(vs))
   268  	copy(vi.versions, vs)
   269  
   270  	evIndex := -1
   271  	currentStabilities := [numStabilityLevels]time.Time{}
   272  	for i := range vi.versions {
   273  		if evIndex == -1 || !vi.effectiveVersions[evIndex].date.Equal(vi.versions[i].Date) {
   274  			vi.effectiveVersions = append(vi.effectiveVersions, effectiveVersion{
   275  				date:        vi.versions[i].Date,
   276  				stabilities: currentStabilities,
   277  			})
   278  			evIndex++
   279  		}
   280  		vi.effectiveVersions[evIndex].stabilities[vi.versions[i].Stability] = vi.versions[i].Date
   281  		currentStabilities[vi.versions[i].Stability] = vi.versions[i].Date
   282  	}
   283  	return vi
   284  }
   285  
   286  // Deprecates returns the version that deprecates the given version in the
   287  // slice.
   288  func (vi *VersionIndex) Deprecates(q Version) (Version, bool) {
   289  	match, err := vi.resolveIndex(q.Date)
   290  	if err == ErrNoMatchingVersion {
   291  		return Version{}, false
   292  	}
   293  	if err != nil {
   294  		panic(err)
   295  	}
   296  	for i := match + 1; i < len(vi.effectiveVersions); i++ {
   297  		for stab := q.Stability; stab < numStabilityLevels; stab++ {
   298  			if stabDate := vi.effectiveVersions[i].stabilities[stab]; stabDate.After(q.Date) {
   299  				return Version{
   300  					Date:      vi.effectiveVersions[i].date,
   301  					Stability: stab,
   302  				}, true
   303  			}
   304  		}
   305  	}
   306  	return Version{}, false
   307  }
   308  
   309  // Resolve returns the released version effective on the query version date at
   310  // the given version stability. Returns ErrNoMatchingVersion if no version matches.
   311  //
   312  // Resolve should be used on a collection of already "compiled" or
   313  // "collated" API versions.
   314  func (vi *VersionIndex) Resolve(query Version) (Version, error) {
   315  	i, err := vi.resolveIndex(query.Date)
   316  	if err != nil {
   317  		return Version{}, err
   318  	}
   319  	for stab := query.Stability; stab < numStabilityLevels; stab++ {
   320  		if stabDate := vi.effectiveVersions[i].stabilities[stab]; !stabDate.IsZero() {
   321  			return Version{Date: stabDate, Stability: stab}, nil
   322  		}
   323  	}
   324  	return Version{}, ErrNoMatchingVersion
   325  }
   326  
   327  // Versions returns each Version defined.
   328  func (vi *VersionIndex) Versions() VersionSlice {
   329  	vs := make(VersionSlice, len(vi.versions))
   330  	copy(vs, vi.versions)
   331  	return vs
   332  }
   333  
   334  // resolveIndex performs a binary search on the stability versions in effect on
   335  // the query date.
   336  func (vi *VersionIndex) resolveIndex(query time.Time) (int, error) {
   337  	if len(vi.effectiveVersions) == 0 || vi.effectiveVersions[0].date.After(query) {
   338  		return -1, ErrNoMatchingVersion
   339  	}
   340  	lower, curr, upper := 0, len(vi.effectiveVersions)/2, len(vi.effectiveVersions)
   341  	for lower < upper-1 {
   342  		if vi.effectiveVersions[curr].date.After(query) {
   343  			upper = curr
   344  		} else {
   345  			lower = curr
   346  		}
   347  		curr = lower + (upper-lower)/2
   348  	}
   349  	return lower, nil
   350  }
   351  
   352  // ResolveForBuild returns the most stable version effective on the query
   353  // version date with respect to the given version stability. Returns
   354  // ErrNoMatchingVersion if no version matches.
   355  //
   356  // Use ResolveForBuild when resolving version deprecation and effective releases
   357  // _within a single resource_ during the "compilation" or "collation" process.
   358  func (vi *VersionIndex) ResolveForBuild(query Version) (Version, error) {
   359  	i, err := vi.resolveIndex(query.Date)
   360  	if err != nil {
   361  		return Version{}, err
   362  	}
   363  	var matchDate time.Time
   364  	var matchStab Stability
   365  	for stab := query.Stability; stab < numStabilityLevels; stab++ {
   366  		stabDate := vi.effectiveVersions[i].stabilities[stab]
   367  		if !stabDate.IsZero() && !stabDate.Before(matchDate) && !stabDate.After(query.Date) {
   368  			matchDate, matchStab = stabDate, stab
   369  		}
   370  	}
   371  	if matchDate.IsZero() {
   372  		return Version{}, ErrNoMatchingVersion
   373  	}
   374  	return Version{Date: matchDate, Stability: matchStab}, nil
   375  }
   376  
   377  // Len implements sort.Interface.
   378  func (vs VersionSlice) Len() int { return len(vs) }
   379  
   380  // Less implements sort.Interface.
   381  func (vs VersionSlice) Less(i, j int) bool {
   382  	return vs[i].Compare(vs[j]) < 0
   383  }
   384  
   385  // Swap implements sort.Interface.
   386  func (vs VersionSlice) Swap(i, j int) { vs[i], vs[j] = vs[j], vs[i] }
   387  
   388  // Strings returns a slice of string versions.
   389  func (vs VersionSlice) Strings() []string {
   390  	s := make([]string, len(vs))
   391  	for i := range vs {
   392  		s[i] = vs[i].String()
   393  	}
   394  	return s
   395  }
   396  
   397  // Lifecycle defines the release lifecycle.
   398  type Lifecycle int
   399  
   400  const (
   401  	lifecycleUndefined Lifecycle = iota
   402  
   403  	// LifecycleUnreleased means the version has not been released yet.
   404  	LifecycleUnreleased Lifecycle = iota
   405  
   406  	// LifecycleReleased means the version is released.
   407  	LifecycleReleased Lifecycle = iota
   408  
   409  	// LifecycleDeprecated means the version is deprecated.
   410  	LifecycleDeprecated Lifecycle = iota
   411  
   412  	// LifecycleSunset means the version is eligible to be sunset.
   413  	LifecycleSunset Lifecycle = iota
   414  
   415  	// ExperimentalTTL is the duration after which experimental releases expire
   416  	// and should be considered sunset.
   417  	ExperimentalTTL = 90 * 24 * time.Hour
   418  )
   419  
   420  // ParseLifecycle parses a lifecycle string into a Lifecycle type, returning an
   421  // error if the string is invalid.
   422  func ParseLifecycle(s string) (Lifecycle, error) {
   423  	switch s {
   424  	case "released":
   425  		return LifecycleReleased, nil
   426  	case "deprecated":
   427  		return LifecycleDeprecated, nil
   428  	case "sunset":
   429  		return LifecycleSunset, nil
   430  	default:
   431  		return lifecycleUndefined, fmt.Errorf("invalid lifecycle %q", s)
   432  	}
   433  }
   434  
   435  // String returns a string representation of the lifecycle stage. This method
   436  // will panic if the value is empty.
   437  func (l Lifecycle) String() string {
   438  	switch l {
   439  	case LifecycleReleased:
   440  		return "released"
   441  	case LifecycleDeprecated:
   442  		return "deprecated"
   443  	case LifecycleSunset:
   444  		return "sunset"
   445  	default:
   446  		panic(fmt.Sprintf("invalid lifecycle (%d)", int(l)))
   447  	}
   448  }
   449  
   450  func (l Lifecycle) Valid() bool {
   451  	switch l {
   452  	case LifecycleReleased, LifecycleDeprecated, LifecycleSunset:
   453  		return true
   454  	default:
   455  		return false
   456  	}
   457  }
   458  
   459  // LifecycleAt returns the Lifecycle of the version at the given time. If the
   460  // time is the zero value (time.Time{}), then the following are used to
   461  // determine the reference time:
   462  //
   463  // If VERVET_LIFECYCLE_AT is set to an ISO date string of the form YYYY-mm-dd,
   464  // this date is used as the reference time for deprecation, at midnight UTC.
   465  //
   466  // Otherwise `time.Now().UTC()` is used for the reference time.
   467  //
   468  // The current time is always used for determining whether a version is unreleased.
   469  func (v *Version) LifecycleAt(t time.Time) Lifecycle {
   470  	if t.IsZero() {
   471  		t = defaultLifecycleAt()
   472  	}
   473  	deprecationDelta := t.Sub(v.Date)
   474  	releaseDelta := timeNow().UTC().Sub(v.Date)
   475  	if releaseDelta < 0 {
   476  		return LifecycleUnreleased
   477  	}
   478  	if v.Stability.Compare(StabilityExperimental) <= 0 {
   479  		if v.Stability == StabilityWIP {
   480  			return LifecycleSunset
   481  		}
   482  		// experimental
   483  		if deprecationDelta > ExperimentalTTL {
   484  			return LifecycleSunset
   485  		}
   486  		return LifecycleDeprecated
   487  	}
   488  	return LifecycleReleased
   489  }
   490  
   491  func defaultLifecycleAt() time.Time {
   492  	if dateStr := os.Getenv("VERVET_LIFECYCLE_AT"); dateStr != "" {
   493  		if t, err := time.ParseInLocation("2006-01-02", dateStr, time.UTC); err == nil {
   494  			return t
   495  		}
   496  	}
   497  	return timeNow().UTC()
   498  }