github.com/w3security/vervet/v5@v5.3.1-0.20230618081846-5bd9b5d799dc/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  // Compare returns -1 if the given version is less than, 0 if equal to, and 1
   156  // if greater than the caller target version.
   157  func (v Version) Compare(vr Version) int {
   158  	dateCmp, stabilityCmp := v.compareDateStability(&vr)
   159  	if dateCmp != 0 {
   160  		return dateCmp
   161  	}
   162  	return stabilityCmp
   163  }
   164  
   165  // DeprecatedBy returns true if the given version deprecates the caller target
   166  // version.
   167  func (v Version) DeprecatedBy(vr Version) bool {
   168  	dateCmp, stabilityCmp := v.compareDateStability(&vr)
   169  	// A version is deprecated by a newer version of equal or greater stability.
   170  	return dateCmp == -1 && stabilityCmp <= 0
   171  }
   172  
   173  const (
   174  	// SunsetWIP is the duration past deprecation after which a work-in-progress version may be sunset.
   175  	SunsetWIP = 0
   176  
   177  	// SunsetExperimental is the duration past deprecation after which an experimental version may be sunset.
   178  	SunsetExperimental = 24 * time.Hour
   179  
   180  	// SunsetBeta is the duration past deprecation after which a beta version may be sunset.
   181  	SunsetBeta = 91 * 24 * time.Hour
   182  
   183  	// SunsetGA is the duration past deprecation after which a GA version may be sunset.
   184  	SunsetGA = 181 * 24 * time.Hour
   185  )
   186  
   187  // Sunset returns, given a potentially deprecating version, the eligible sunset
   188  // date and whether the caller target version would actually be deprecated and
   189  // sunset by the given version.
   190  func (v Version) Sunset(vr Version) (time.Time, bool) {
   191  	if !v.DeprecatedBy(vr) {
   192  		return time.Time{}, false
   193  	}
   194  	switch v.Stability {
   195  	case StabilityWIP:
   196  		return vr.Date.Add(SunsetWIP), true
   197  	case StabilityExperimental:
   198  		return vr.Date.Add(SunsetExperimental), true
   199  	case StabilityBeta:
   200  		return vr.Date.Add(SunsetBeta), true
   201  	case StabilityGA:
   202  		return vr.Date.Add(SunsetGA), true
   203  	default:
   204  		return time.Time{}, false
   205  	}
   206  }
   207  
   208  // compareDateStability returns the comparison of both the date and stability
   209  // between two versions. Used internally where these need to be evaluated
   210  // independently, such as when searching for the best matching version.
   211  func (v *Version) compareDateStability(vr *Version) (int, int) {
   212  	dateCmp := 0
   213  	if v.Date.Before(vr.Date) {
   214  		dateCmp = -1
   215  	} else if v.Date.After(vr.Date) {
   216  		dateCmp = 1
   217  	}
   218  	stabilityCmp := v.Stability.Compare(vr.Stability)
   219  	return dateCmp, stabilityCmp
   220  }
   221  
   222  // VersionDateStrings returns a slice of distinct version date strings for a
   223  // slice of Versions. Consecutive duplicate dates are removed.
   224  func VersionDateStrings(vs []Version) []string {
   225  	var result []string
   226  	for i := range vs {
   227  		ds := vs[i].DateString()
   228  		if len(result) == 0 || result[len(result)-1] != ds {
   229  			result = append(result, ds)
   230  		}
   231  	}
   232  	return result
   233  }
   234  
   235  // VersionSlice is a sortable slice of Versions.
   236  type VersionSlice []Version
   237  
   238  // VersionIndex provides a search over versions, resolving which version is in
   239  // effect for a given date and stability level.
   240  type VersionIndex struct {
   241  	versions []effectiveVersion
   242  }
   243  
   244  type effectiveVersion struct {
   245  	date        time.Time
   246  	stabilities [numStabilityLevels]time.Time
   247  }
   248  
   249  // NewVersionIndex returns a new VersionIndex of the given versions. The given
   250  // VersionSlice will be sorted.
   251  func NewVersionIndex(vs VersionSlice) (vi VersionIndex) {
   252  	sort.Sort(vs)
   253  	evIndex := -1
   254  	currentStabilities := [numStabilityLevels]time.Time{}
   255  	for i := range vs {
   256  		if evIndex == -1 || !vi.versions[evIndex].date.Equal(vs[i].Date) {
   257  			vi.versions = append(vi.versions, effectiveVersion{
   258  				date:        vs[i].Date,
   259  				stabilities: currentStabilities,
   260  			})
   261  			evIndex++
   262  		}
   263  		vi.versions[evIndex].stabilities[vs[i].Stability] = vs[i].Date
   264  		currentStabilities[vs[i].Stability] = vs[i].Date
   265  	}
   266  	return vi
   267  }
   268  
   269  // resolveIndex performs a binary search on the stability versions in effect on
   270  // the query date.
   271  func (vi *VersionIndex) resolveIndex(query time.Time) (int, error) {
   272  	if len(vi.versions) == 0 || vi.versions[0].date.After(query) {
   273  		return -1, ErrNoMatchingVersion
   274  	}
   275  	lower, curr, upper := 0, len(vi.versions)/2, len(vi.versions)
   276  	for lower < upper-1 {
   277  		if vi.versions[curr].date.After(query) {
   278  			upper = curr
   279  		} else {
   280  			lower = curr
   281  		}
   282  		curr = lower + (upper-lower)/2
   283  	}
   284  	return lower, nil
   285  }
   286  
   287  // Resolve returns the released version effective on the query version date at
   288  // the given version stability. Returns ErrNoMatchingVersion if no version matches.
   289  //
   290  // Resolve should be used on a collection of already "compiled" or
   291  // "collated" API versions.
   292  func (vi *VersionIndex) Resolve(query Version) (Version, error) {
   293  	i, err := vi.resolveIndex(query.Date)
   294  	if err != nil {
   295  		return Version{}, err
   296  	}
   297  	for stab := query.Stability; stab < numStabilityLevels; stab++ {
   298  		if stabDate := vi.versions[i].stabilities[stab]; !stabDate.IsZero() {
   299  			return Version{Date: stabDate, Stability: stab}, nil
   300  		}
   301  	}
   302  	return Version{}, ErrNoMatchingVersion
   303  }
   304  
   305  // resolveForBuild returns the most stable version effective on the query
   306  // version date with respect to the given version stability. Returns
   307  // ErrNoMatchingVersion if no version matches.
   308  //
   309  // Use resolveForBuild when resolving version deprecation and effective releases
   310  // _within a single resource_ during the "compilation" or "collation" process.
   311  func (vi *VersionIndex) resolveForBuild(query Version) (Version, error) {
   312  	i, err := vi.resolveIndex(query.Date)
   313  	if err != nil {
   314  		return Version{}, err
   315  	}
   316  	var matchDate time.Time
   317  	var matchStab Stability
   318  	for stab := query.Stability; stab < numStabilityLevels; stab++ {
   319  		stabDate := vi.versions[i].stabilities[stab]
   320  		if !stabDate.IsZero() && !stabDate.Before(matchDate) && !stabDate.After(query.Date) {
   321  			matchDate, matchStab = stabDate, stab
   322  		}
   323  	}
   324  	if matchDate.IsZero() {
   325  		return Version{}, ErrNoMatchingVersion
   326  	}
   327  	return Version{Date: matchDate, Stability: matchStab}, nil
   328  }
   329  
   330  // Deprecates returns the version that deprecates the given version in the
   331  // slice.
   332  func (vi *VersionIndex) Deprecates(q Version) (Version, bool) {
   333  	match, err := vi.resolveIndex(q.Date)
   334  	if err == ErrNoMatchingVersion {
   335  		return Version{}, false
   336  	}
   337  	if err != nil {
   338  		panic(err)
   339  	}
   340  	for i := match + 1; i < len(vi.versions); i++ {
   341  		for stab := q.Stability; stab < numStabilityLevels; stab++ {
   342  			if stabDate := vi.versions[i].stabilities[stab]; stabDate.After(q.Date) {
   343  				return Version{
   344  					Date:      vi.versions[i].date,
   345  					Stability: stab,
   346  				}, true
   347  			}
   348  		}
   349  	}
   350  	return Version{}, false
   351  }
   352  
   353  // Len implements sort.Interface.
   354  func (vs VersionSlice) Len() int { return len(vs) }
   355  
   356  // Less implements sort.Interface.
   357  func (vs VersionSlice) Less(i, j int) bool {
   358  	return vs[i].Compare(vs[j]) < 0
   359  }
   360  
   361  // Swap implements sort.Interface.
   362  func (vs VersionSlice) Swap(i, j int) { vs[i], vs[j] = vs[j], vs[i] }
   363  
   364  // Strings returns a slice of string versions.
   365  func (vs VersionSlice) Strings() []string {
   366  	s := make([]string, len(vs))
   367  	for i := range vs {
   368  		s[i] = vs[i].String()
   369  	}
   370  	return s
   371  }
   372  
   373  // Lifecycle defines the release lifecycle.
   374  type Lifecycle int
   375  
   376  const (
   377  	lifecycleUndefined Lifecycle = iota
   378  
   379  	// LifecycleUnreleased means the version has not been released yet.
   380  	LifecycleUnreleased Lifecycle = iota
   381  
   382  	// LifecycleReleased means the version is released.
   383  	LifecycleReleased Lifecycle = iota
   384  
   385  	// LifecycleDeprecated means the version is deprecated.
   386  	LifecycleDeprecated Lifecycle = iota
   387  
   388  	// LifecycleSunset means the version is eligible to be sunset.
   389  	LifecycleSunset Lifecycle = iota
   390  
   391  	// ExperimentalTTL is the duration after which experimental releases expire
   392  	// and should be considered sunset.
   393  	ExperimentalTTL = 90 * 24 * time.Hour
   394  )
   395  
   396  // ParseLifecycle parses a lifecycle string into a Lifecycle type, returning an
   397  // error if the string is invalid.
   398  func ParseLifecycle(s string) (Lifecycle, error) {
   399  	switch s {
   400  	case "released":
   401  		return LifecycleReleased, nil
   402  	case "deprecated":
   403  		return LifecycleDeprecated, nil
   404  	case "sunset":
   405  		return LifecycleSunset, nil
   406  	default:
   407  		return lifecycleUndefined, fmt.Errorf("invalid lifecycle %q", s)
   408  	}
   409  }
   410  
   411  // String returns a string representation of the lifecycle stage. This method
   412  // will panic if the value is empty.
   413  func (l Lifecycle) String() string {
   414  	switch l {
   415  	case LifecycleReleased:
   416  		return "released"
   417  	case LifecycleDeprecated:
   418  		return "deprecated"
   419  	case LifecycleSunset:
   420  		return "sunset"
   421  	default:
   422  		panic(fmt.Sprintf("invalid lifecycle (%d)", int(l)))
   423  	}
   424  }
   425  
   426  func (l Lifecycle) Valid() bool {
   427  	switch l {
   428  	case LifecycleReleased, LifecycleDeprecated, LifecycleSunset:
   429  		return true
   430  	default:
   431  		return false
   432  	}
   433  }
   434  
   435  // LifecycleAt returns the Lifecycle of the version at the given time. If the
   436  // time is the zero value (time.Time{}), then the following are used to
   437  // determine the reference time:
   438  //
   439  // If VERVET_LIFECYCLE_AT is set to an ISO date string of the form YYYY-mm-dd,
   440  // this date is used as the reference time for deprecation, at midnight UTC.
   441  //
   442  // Otherwise `time.Now().UTC()` is used for the reference time.
   443  //
   444  // The current time is always used for determining whether a version is unreleased.
   445  func (v *Version) LifecycleAt(t time.Time) Lifecycle {
   446  	if t.IsZero() {
   447  		t = defaultLifecycleAt()
   448  	}
   449  	deprecationDelta := t.Sub(v.Date)
   450  	releaseDelta := timeNow().UTC().Sub(v.Date)
   451  	if releaseDelta < 0 {
   452  		return LifecycleUnreleased
   453  	}
   454  	if v.Stability.Compare(StabilityExperimental) <= 0 {
   455  		if v.Stability == StabilityWIP {
   456  			return LifecycleSunset
   457  		}
   458  		// experimental
   459  		if deprecationDelta > ExperimentalTTL {
   460  			return LifecycleSunset
   461  		}
   462  		return LifecycleDeprecated
   463  	}
   464  	return LifecycleReleased
   465  }
   466  
   467  func defaultLifecycleAt() time.Time {
   468  	if dateStr := os.Getenv("VERVET_LIFECYCLE_AT"); dateStr != "" {
   469  		if t, err := time.ParseInLocation("2006-01-02", dateStr, time.UTC); err == nil {
   470  			return t
   471  		}
   472  	}
   473  	return timeNow().UTC()
   474  }