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 }