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 }