From 652603095f37d89d5a8848049142324a12474244 Mon Sep 17 00:00:00 2001 From: Casey Marshall Date: Sun, 5 Dec 2021 11:07:53 -0600 Subject: [PATCH 1/2] feat: annotate each path with other release versions API documentation needs to represent a "changelog" per resource endpoint: a list of versions in which a change to a given resource endpoint was released. This change adds a list of other resource release versions in which a particular path was declared for such navigation purposes. Implements #78. --- resource.go | 39 +++++++++++++++---- .../output/2021-06-01~experimental/spec.json | 5 +++ .../output/2021-06-01~experimental/spec.yaml | 4 ++ .../output/2021-06-04~experimental/spec.json | 8 ++++ .../output/2021-06-04~experimental/spec.yaml | 6 +++ .../output/2021-06-07~experimental/spec.json | 8 ++++ .../output/2021-06-07~experimental/spec.yaml | 6 +++ testdata/output/2021-06-13~beta/spec.json | 8 ++++ testdata/output/2021-06-13~beta/spec.yaml | 6 +++ .../output/2021-06-13~experimental/spec.json | 11 ++++++ .../output/2021-06-13~experimental/spec.yaml | 8 ++++ version.go | 9 +++++ 12 files changed, 110 insertions(+), 8 deletions(-) diff --git a/resource.go b/resource.go index 27b24f6a..2a70f65e 100644 --- a/resource.go +++ b/resource.go @@ -21,6 +21,11 @@ const ( // ExtSnykApiVersion is used to annotate a path in a compiled OpenAPI spec with its resolved release version. ExtSnykApiVersion = "x-snyk-api-version" + + // ExtSnykApiReleases is used to annotate a path in a compiled OpenAPI spec + // with all the release versions containing a change in the path info. This + // is useful for navigating changes in a particular path across versions. + ExtSnykApiReleases = "x-snyk-api-releases" ) // Resource defines a specific version of a resource, corresponding to a @@ -139,8 +144,10 @@ func LoadResourceVersions(epPath string) (*ResourceVersions, error) { } func LoadResourceVersionsFileset(specYamls []string) (*ResourceVersions, error) { - var eps ResourceVersions + var resourceVersions ResourceVersions var err error + pathReleases := map[string]VersionSlice{} + for i := range specYamls { specYamls[i], err = filepath.Abs(specYamls[i]) if err != nil { @@ -148,22 +155,38 @@ func LoadResourceVersionsFileset(specYamls []string) (*ResourceVersions, error) } versionDir := filepath.Dir(specYamls[i]) versionBase := filepath.Base(versionDir) - ep, err := loadResource(specYamls[i], versionBase) + rc, err := loadResource(specYamls[i], versionBase) if err != nil { return nil, err } - if ep == nil { + if rc == nil { continue } - ep.sourcePrefix = specYamls[i] - err = ep.Validate(context.TODO()) + rc.sourcePrefix = specYamls[i] + err = rc.Validate(context.TODO()) if err != nil { return nil, err } - eps.versions = append(eps.versions, ep) + resourceVersions.versions = append(resourceVersions.versions, rc) + // Map of release versions per path + for path := range rc.Paths { + pathReleases[path] = append(pathReleases[path], rc.Version) + } + } + // Sort release versions per path + for _, releases := range pathReleases { + sort.Sort(releases) + } + // Sort the resources themselves by version + sort.Sort(resourceVersionSlice(resourceVersions.versions)) + // Annotate each path in each resource version with the other change + // versions affecting the path. This supports navigation across versions. + for _, rc := range resourceVersions.versions { + for path, pathInfo := range rc.Paths { + pathInfo.ExtensionProps.Extensions[ExtSnykApiReleases] = pathReleases[path].Strings() + } } - sort.Sort(resourceVersionSlice(eps.versions)) - return &eps, nil + return &resourceVersions, nil } // ExtensionString returns the string value of an OpenAPI extension. diff --git a/testdata/output/2021-06-01~experimental/spec.json b/testdata/output/2021-06-01~experimental/spec.json index 9296923e..1ea9de40 100644 --- a/testdata/output/2021-06-01~experimental/spec.json +++ b/testdata/output/2021-06-01~experimental/spec.json @@ -394,6 +394,11 @@ } } }, + "x-snyk-api-releases": [ + "2021-06-01~experimental", + "2021-06-07~experimental", + "2021-06-13~beta" + ], "x-snyk-api-resource": "hello-world", "x-snyk-api-version": "2021-06-01~experimental" }, diff --git a/testdata/output/2021-06-01~experimental/spec.yaml b/testdata/output/2021-06-01~experimental/spec.yaml index 35913520..7a4ed5e4 100644 --- a/testdata/output/2021-06-01~experimental/spec.yaml +++ b/testdata/output/2021-06-01~experimental/spec.yaml @@ -267,6 +267,10 @@ paths: $ref: '#/components/responses/404' "500": $ref: '#/components/responses/500' + x-snyk-api-releases: + - 2021-06-01~experimental + - 2021-06-07~experimental + - 2021-06-13~beta x-snyk-api-resource: hello-world x-snyk-api-version: 2021-06-01~experimental /openapi: diff --git a/testdata/output/2021-06-04~experimental/spec.json b/testdata/output/2021-06-04~experimental/spec.json index 0b23ed16..1b3c5c8d 100644 --- a/testdata/output/2021-06-04~experimental/spec.json +++ b/testdata/output/2021-06-04~experimental/spec.json @@ -456,6 +456,11 @@ } } }, + "x-snyk-api-releases": [ + "2021-06-01~experimental", + "2021-06-07~experimental", + "2021-06-13~beta" + ], "x-snyk-api-resource": "hello-world", "x-snyk-api-version": "2021-06-01~experimental" }, @@ -688,6 +693,9 @@ } } }, + "x-snyk-api-releases": [ + "2021-06-04~experimental" + ], "x-snyk-api-resource": "projects", "x-snyk-api-version": "2021-06-04~experimental" } diff --git a/testdata/output/2021-06-04~experimental/spec.yaml b/testdata/output/2021-06-04~experimental/spec.yaml index 92445c2a..8c46dda1 100644 --- a/testdata/output/2021-06-04~experimental/spec.yaml +++ b/testdata/output/2021-06-04~experimental/spec.yaml @@ -316,6 +316,10 @@ paths: $ref: '#/components/responses/404' "500": $ref: '#/components/responses/500' + x-snyk-api-releases: + - 2021-06-01~experimental + - 2021-06-07~experimental + - 2021-06-13~beta x-snyk-api-resource: hello-world x-snyk-api-version: 2021-06-01~experimental /openapi: @@ -465,6 +469,8 @@ paths: $ref: '#/components/responses/404' "500": $ref: '#/components/responses/500' + x-snyk-api-releases: + - 2021-06-04~experimental x-snyk-api-resource: projects x-snyk-api-version: 2021-06-04~experimental servers: diff --git a/testdata/output/2021-06-07~experimental/spec.json b/testdata/output/2021-06-07~experimental/spec.json index ee676d04..a57f3c98 100644 --- a/testdata/output/2021-06-07~experimental/spec.json +++ b/testdata/output/2021-06-07~experimental/spec.json @@ -456,6 +456,11 @@ } } }, + "x-snyk-api-releases": [ + "2021-06-01~experimental", + "2021-06-07~experimental", + "2021-06-13~beta" + ], "x-snyk-api-resource": "hello-world", "x-snyk-api-version": "2021-06-07~experimental" }, @@ -688,6 +693,9 @@ } } }, + "x-snyk-api-releases": [ + "2021-06-04~experimental" + ], "x-snyk-api-resource": "projects", "x-snyk-api-version": "2021-06-04~experimental" } diff --git a/testdata/output/2021-06-07~experimental/spec.yaml b/testdata/output/2021-06-07~experimental/spec.yaml index fc933a10..3addaa31 100644 --- a/testdata/output/2021-06-07~experimental/spec.yaml +++ b/testdata/output/2021-06-07~experimental/spec.yaml @@ -316,6 +316,10 @@ paths: $ref: '#/components/responses/404' "500": $ref: '#/components/responses/500' + x-snyk-api-releases: + - 2021-06-01~experimental + - 2021-06-07~experimental + - 2021-06-13~beta x-snyk-api-resource: hello-world x-snyk-api-version: 2021-06-07~experimental /openapi: @@ -465,6 +469,8 @@ paths: $ref: '#/components/responses/404' "500": $ref: '#/components/responses/500' + x-snyk-api-releases: + - 2021-06-04~experimental x-snyk-api-resource: projects x-snyk-api-version: 2021-06-04~experimental servers: diff --git a/testdata/output/2021-06-13~beta/spec.json b/testdata/output/2021-06-13~beta/spec.json index 9dad76f5..47c79763 100644 --- a/testdata/output/2021-06-13~beta/spec.json +++ b/testdata/output/2021-06-13~beta/spec.json @@ -413,6 +413,9 @@ } } }, + "x-snyk-api-releases": [ + "2021-06-13~beta" + ], "x-snyk-api-resource": "hello-world", "x-snyk-api-version": "2021-06-13~beta" }, @@ -490,6 +493,11 @@ } } }, + "x-snyk-api-releases": [ + "2021-06-01~experimental", + "2021-06-07~experimental", + "2021-06-13~beta" + ], "x-snyk-api-resource": "hello-world", "x-snyk-api-version": "2021-06-13~beta" }, diff --git a/testdata/output/2021-06-13~beta/spec.yaml b/testdata/output/2021-06-13~beta/spec.yaml index 735a9e6d..d2253802 100644 --- a/testdata/output/2021-06-13~beta/spec.yaml +++ b/testdata/output/2021-06-13~beta/spec.yaml @@ -280,6 +280,8 @@ paths: $ref: '#/components/responses/404' "500": $ref: '#/components/responses/500' + x-snyk-api-releases: + - 2021-06-13~beta x-snyk-api-resource: hello-world x-snyk-api-version: 2021-06-13~beta /examples/hello-world/{id}: @@ -329,6 +331,10 @@ paths: $ref: '#/components/responses/404' "500": $ref: '#/components/responses/500' + x-snyk-api-releases: + - 2021-06-01~experimental + - 2021-06-07~experimental + - 2021-06-13~beta x-snyk-api-resource: hello-world x-snyk-api-version: 2021-06-13~beta /openapi: diff --git a/testdata/output/2021-06-13~experimental/spec.json b/testdata/output/2021-06-13~experimental/spec.json index 8c515ace..b31c66ff 100644 --- a/testdata/output/2021-06-13~experimental/spec.json +++ b/testdata/output/2021-06-13~experimental/spec.json @@ -475,6 +475,9 @@ } } }, + "x-snyk-api-releases": [ + "2021-06-13~beta" + ], "x-snyk-api-resource": "hello-world", "x-snyk-api-version": "2021-06-13~beta" }, @@ -552,6 +555,11 @@ } } }, + "x-snyk-api-releases": [ + "2021-06-01~experimental", + "2021-06-07~experimental", + "2021-06-13~beta" + ], "x-snyk-api-resource": "hello-world", "x-snyk-api-version": "2021-06-13~beta" }, @@ -784,6 +792,9 @@ } } }, + "x-snyk-api-releases": [ + "2021-06-04~experimental" + ], "x-snyk-api-resource": "projects", "x-snyk-api-version": "2021-06-04~experimental" } diff --git a/testdata/output/2021-06-13~experimental/spec.yaml b/testdata/output/2021-06-13~experimental/spec.yaml index b47883ed..7755cde2 100644 --- a/testdata/output/2021-06-13~experimental/spec.yaml +++ b/testdata/output/2021-06-13~experimental/spec.yaml @@ -329,6 +329,8 @@ paths: $ref: '#/components/responses/404' "500": $ref: '#/components/responses/500' + x-snyk-api-releases: + - 2021-06-13~beta x-snyk-api-resource: hello-world x-snyk-api-version: 2021-06-13~beta /examples/hello-world/{id}: @@ -378,6 +380,10 @@ paths: $ref: '#/components/responses/404' "500": $ref: '#/components/responses/500' + x-snyk-api-releases: + - 2021-06-01~experimental + - 2021-06-07~experimental + - 2021-06-13~beta x-snyk-api-resource: hello-world x-snyk-api-version: 2021-06-13~beta /openapi: @@ -527,6 +533,8 @@ paths: $ref: '#/components/responses/404' "500": $ref: '#/components/responses/500' + x-snyk-api-releases: + - 2021-06-04~experimental x-snyk-api-resource: projects x-snyk-api-version: 2021-06-04~experimental servers: diff --git a/version.go b/version.go index 09a58eee..27a793a0 100644 --- a/version.go +++ b/version.go @@ -223,3 +223,12 @@ func (vs VersionSlice) Less(i, j int) bool { // Swap implements sort.Interface. func (vs VersionSlice) Swap(i, j int) { vs[i], vs[j] = vs[j], vs[i] } + +// Strings returns a slice of string versions +func (vs VersionSlice) Strings() []string { + s := make([]string, len(vs)) + for i := range vs { + s[i] = vs[i].String() + } + return s +} From 6f6cb548cfa5799d487e3971adf7d06f552404d7 Mon Sep 17 00:00:00 2001 From: Casey Marshall Date: Sun, 5 Dec 2021 14:28:16 -0600 Subject: [PATCH 2/2] feat: add version deprecation and sunset eligibility Annotate paths in a given resource version with the newer release version that deprecates it, and when the deprecated version may be sunset. This may be used in linting, middleware, and documentation. Implements #86. --- resource.go | 34 ++++++- .../output/2021-06-01~experimental/spec.json | 4 +- .../output/2021-06-01~experimental/spec.yaml | 2 + .../output/2021-06-04~experimental/spec.json | 4 +- .../output/2021-06-04~experimental/spec.yaml | 2 + .../output/2021-06-07~experimental/spec.json | 4 +- .../output/2021-06-07~experimental/spec.yaml | 2 + version.go | 94 ++++++++++++++++++- version_test.go | 83 ++++++++++++++++ 9 files changed, 220 insertions(+), 9 deletions(-) diff --git a/resource.go b/resource.go index 2a70f65e..299612ca 100644 --- a/resource.go +++ b/resource.go @@ -26,6 +26,17 @@ const ( // with all the release versions containing a change in the path info. This // is useful for navigating changes in a particular path across versions. ExtSnykApiReleases = "x-snyk-api-releases" + + // ExtSnykDeprecatedBy is used to annotate a path in a resource version + // spec with the subsequent version that deprecates it. This may be used + // by linters, service middleware and API documentation to indicate which + // version deprecates a given version. + ExtSnykDeprecatedBy = "x-snyk-deprecated-by" + + // ExtSnykSunsetEligible is used to annotate a path in a resource version + // spec which is deprecated, with the sunset eligible date: the date after + // which the resource version may be removed and no longer available. + ExtSnykSunsetEligible = "x-snyk-sunset-eligible" ) // Resource defines a specific version of a resource, corresponding to a @@ -43,10 +54,12 @@ type extensionNotFoundError struct { extension string } +// Error implements error. func (e *extensionNotFoundError) Error() string { return fmt.Sprintf("extension \"%s\" not found", e.extension) } +// Is returns whether an error matches this error instance. func (e *extensionNotFoundError) Is(err error) bool { _, ok := err.(*extensionNotFoundError) return ok @@ -72,6 +85,7 @@ type ResourceVersions struct { versions resourceVersionSlice } +// Name returns the resource name for a collection of resource versions. func (e *ResourceVersions) Name() string { for i := range e.versions { return e.versions[i].Name @@ -115,10 +129,15 @@ func (e *ResourceVersions) At(vs string) (*Resource, error) { type resourceVersionSlice []*Resource +// Less implements sort.Interface. func (e resourceVersionSlice) Less(i, j int) bool { return e[i].Version.Compare(&e[j].Version) < 0 } -func (e resourceVersionSlice) Len() int { return len(e) } + +// Len implements sort.Interface. +func (e resourceVersionSlice) Len() int { return len(e) } + +// Swap implements sort.Interface. func (e resourceVersionSlice) Swap(i, j int) { e[i], e[j] = e[j], e[i] } // LoadResourceVersions returns a ResourceVersions slice parsed from a @@ -143,6 +162,8 @@ func LoadResourceVersions(epPath string) (*ResourceVersions, error) { return LoadResourceVersionsFileset(specYamls) } +// LoadResourceVersionFileset returns a ResourceVersions slice parsed from the +// directory structure described above for LoadResourceVersions. func LoadResourceVersionsFileset(specYamls []string) (*ResourceVersions, error) { var resourceVersions ResourceVersions var err error @@ -183,7 +204,16 @@ func LoadResourceVersionsFileset(specYamls []string) (*ResourceVersions, error) // versions affecting the path. This supports navigation across versions. for _, rc := range resourceVersions.versions { for path, pathInfo := range rc.Paths { - pathInfo.ExtensionProps.Extensions[ExtSnykApiReleases] = pathReleases[path].Strings() + // Annotate path with other release versions available for this path + releases := pathReleases[path] + pathInfo.ExtensionProps.Extensions[ExtSnykApiReleases] = releases.Strings() + // Annotate path with deprecated-by and sunset information + if deprecatedBy, ok := releases.Deprecates(rc.Version); ok { + pathInfo.ExtensionProps.Extensions[ExtSnykDeprecatedBy] = deprecatedBy.String() + if sunset, ok := rc.Version.Sunset(deprecatedBy); ok { + pathInfo.ExtensionProps.Extensions[ExtSnykSunsetEligible] = sunset.Format("2006-01-02") + } + } } } return &resourceVersions, nil diff --git a/testdata/output/2021-06-01~experimental/spec.json b/testdata/output/2021-06-01~experimental/spec.json index 1ea9de40..0407c2fd 100644 --- a/testdata/output/2021-06-01~experimental/spec.json +++ b/testdata/output/2021-06-01~experimental/spec.json @@ -400,7 +400,9 @@ "2021-06-13~beta" ], "x-snyk-api-resource": "hello-world", - "x-snyk-api-version": "2021-06-01~experimental" + "x-snyk-api-version": "2021-06-01~experimental", + "x-snyk-deprecated-by": "2021-06-07~experimental", + "x-snyk-sunset-eligible": "2021-07-08" }, "/openapi": { "get": { diff --git a/testdata/output/2021-06-01~experimental/spec.yaml b/testdata/output/2021-06-01~experimental/spec.yaml index 7a4ed5e4..20bc84f1 100644 --- a/testdata/output/2021-06-01~experimental/spec.yaml +++ b/testdata/output/2021-06-01~experimental/spec.yaml @@ -273,6 +273,8 @@ paths: - 2021-06-13~beta x-snyk-api-resource: hello-world x-snyk-api-version: 2021-06-01~experimental + x-snyk-deprecated-by: 2021-06-07~experimental + x-snyk-sunset-eligible: "2021-07-08" /openapi: get: description: List available versions of OpenAPI specification diff --git a/testdata/output/2021-06-04~experimental/spec.json b/testdata/output/2021-06-04~experimental/spec.json index 1b3c5c8d..b747d16f 100644 --- a/testdata/output/2021-06-04~experimental/spec.json +++ b/testdata/output/2021-06-04~experimental/spec.json @@ -462,7 +462,9 @@ "2021-06-13~beta" ], "x-snyk-api-resource": "hello-world", - "x-snyk-api-version": "2021-06-01~experimental" + "x-snyk-api-version": "2021-06-01~experimental", + "x-snyk-deprecated-by": "2021-06-07~experimental", + "x-snyk-sunset-eligible": "2021-07-08" }, "/openapi": { "get": { diff --git a/testdata/output/2021-06-04~experimental/spec.yaml b/testdata/output/2021-06-04~experimental/spec.yaml index 8c46dda1..529c260f 100644 --- a/testdata/output/2021-06-04~experimental/spec.yaml +++ b/testdata/output/2021-06-04~experimental/spec.yaml @@ -322,6 +322,8 @@ paths: - 2021-06-13~beta x-snyk-api-resource: hello-world x-snyk-api-version: 2021-06-01~experimental + x-snyk-deprecated-by: 2021-06-07~experimental + x-snyk-sunset-eligible: "2021-07-08" /openapi: get: description: List available versions of OpenAPI specification diff --git a/testdata/output/2021-06-07~experimental/spec.json b/testdata/output/2021-06-07~experimental/spec.json index a57f3c98..324d6be1 100644 --- a/testdata/output/2021-06-07~experimental/spec.json +++ b/testdata/output/2021-06-07~experimental/spec.json @@ -462,7 +462,9 @@ "2021-06-13~beta" ], "x-snyk-api-resource": "hello-world", - "x-snyk-api-version": "2021-06-07~experimental" + "x-snyk-api-version": "2021-06-07~experimental", + "x-snyk-deprecated-by": "2021-06-13~beta", + "x-snyk-sunset-eligible": "2021-07-14" }, "/openapi": { "get": { diff --git a/testdata/output/2021-06-07~experimental/spec.yaml b/testdata/output/2021-06-07~experimental/spec.yaml index 3addaa31..d29c4296 100644 --- a/testdata/output/2021-06-07~experimental/spec.yaml +++ b/testdata/output/2021-06-07~experimental/spec.yaml @@ -322,6 +322,8 @@ paths: - 2021-06-13~beta x-snyk-api-resource: hello-world x-snyk-api-version: 2021-06-07~experimental + x-snyk-deprecated-by: 2021-06-13~beta + x-snyk-sunset-eligible: "2021-07-14" /openapi: get: description: List available versions of OpenAPI specification diff --git a/version.go b/version.go index 27a793a0..691ea66f 100644 --- a/version.go +++ b/version.go @@ -30,6 +30,15 @@ func (v *Version) String() string { return d } +// AddDays returns the version corresponding to adding the given number of days +// to the version date. +func (v *Version) AddDays(days int) Version { + return Version{ + Date: v.Date.AddDate(0, 0, days), + Stability: v.Stability, + } +} + // Stability defines the stability level of the version. type Stability int @@ -146,6 +155,48 @@ func (v *Version) Compare(vr *Version) int { return stabilityCmp } +// DeprecatedBy returns true if the given version deprecates the caller target +// version. +func (v *Version) DeprecatedBy(vr *Version) bool { + dateCmp, stabilityCmp := v.compareDateStability(vr) + // A version is deprecated by a newer version of equal or greater stability. + return dateCmp == -1 && stabilityCmp <= 0 +} + +const ( + // SunsetWIP is the duration past deprecation after which a work-in-progress version may be sunset. + SunsetWIP = 0 + + // SunsetExperimental is the duration past deprecation after which an experimental version may be sunset. + SunsetExperimental = 31 * 24 * time.Hour + + // SunsetBeta is the duration past deprecation after which a beta version may be sunset. + SunsetBeta = 91 * 24 * time.Hour + + // SunsetGA is the duration past deprecation after which a GA version may be sunset. + SunsetGA = 181 * 24 * time.Hour +) + +// Sunset returns, given a potentially deprecating version, the eligible sunset +// date and whether the caller target version would actually be deprecated and +// sunset by the given version. +func (v *Version) Sunset(vr *Version) (time.Time, bool) { + if !v.DeprecatedBy(vr) { + return time.Time{}, false + } + switch v.Stability { + case StabilityWIP: + return vr.Date.Add(SunsetWIP), true + case StabilityExperimental: + return vr.Date.Add(SunsetExperimental), true + case StabilityBeta: + return vr.Date.Add(SunsetBeta), true + case StabilityGA: + return vr.Date.Add(SunsetGA), true + } + return time.Time{}, false +} + // compareDateStability returns the comparison of both the date and stability // between two versions. Used internally where these need to be evaluated // independently, such as when searching for the best matching version. @@ -182,10 +233,24 @@ type VersionSlice []Version // This method requires that the VersionSlice has already been sorted with // sort.Sort, otherwise behavior is undefined. func (vs VersionSlice) Resolve(q Version) (*Version, error) { + i, err := vs.ResolveIndex(q) + if err != nil { + return nil, err + } + v := vs[i] + return &v, nil +} + +// ResolveIndex returns the slice index of the most recent Version in the slice +// with equal or greater stability. +// +// This method requires that the VersionSlice has already been sorted with +// sort.Sort, otherwise behavior is undefined. +func (vs VersionSlice) ResolveIndex(q Version) (int, error) { lower, curr, upper := 0, len(vs)/2, len(vs) if upper == 0 { // Nothing matches an empty slice. - return nil, ErrNoMatchingVersion + return -1, ErrNoMatchingVersion } for curr < upper && lower != upper-1 { dateCmp, stabilityCmp := vs[curr].compareDateStability(&q) @@ -207,10 +272,31 @@ func (vs VersionSlice) Resolve(q Version) (*Version, error) { // Did we find a match? dateCmp, stabilityCmp := vs[lower].compareDateStability(&q) if dateCmp <= 0 && stabilityCmp >= 0 { - v := &vs[lower] - return v, nil + return lower, nil + } + return -1, ErrNoMatchingVersion +} + +// Deprecates returns the version that deprecates the given version in the +// slice. +func (vs VersionSlice) Deprecates(q Version) (*Version, bool) { + match, err := vs.ResolveIndex(q) + if err == ErrNoMatchingVersion { + return nil, false + } else if err != nil { + panic(err) + } + for i := match + 1; i < len(vs); i++ { + dateCmp, stabilityCmp := vs[match].compareDateStability(&vs[i]) + if stabilityCmp > 0 { + continue + } + if dateCmp < 0 { + v := vs[i] + return &v, true + } } - return nil, ErrNoMatchingVersion + return nil, false } // Len implements sort.Interface. diff --git a/version_test.go b/version_test.go index fe0bcf6c..e6d249bc 100644 --- a/version_test.go +++ b/version_test.go @@ -3,6 +3,7 @@ package vervet_test import ( "sort" "testing" + "time" qt "github.com/frankban/quicktest" @@ -237,3 +238,85 @@ func TestVersionSliceResolveEmpty(t *testing.T) { _, err := VersionSlice{}.Resolve(MustParseVersion("2021-10-31")) c.Assert(err, qt.ErrorMatches, "no matching version") } + +func TestDeprecatedBy(t *testing.T) { + c := qt.New(t) + tests := []struct { + base, deprecatedBy string + result bool + }{{ + "2021-06-01", "2021-02-01", false, + }, { + "2021-06-01", "2021-02-01~experimental", false, + }, { + "2021-06-01", "2021-02-01~beta", false, + }, { + "2021-06-01", "2022-02-01~beta", false, + }, { + "2021-06-01", "2022-02-01~experimental", false, + }, { + "2021-06-01", "2021-06-01", false, + }, { + "2021-06-01", "2021-06-02", true, + }, { + "2021-06-01~experimental", "2021-06-02~beta", true, + }, { + "2021-06-01~beta", "2021-06-02", true, + }, { + "2021-06-01", "2021-06-01", false, + }} + for i, test := range tests { + c.Logf("test#%d: %#v", i, test) + base, deprecatedBy := MustParseVersion(test.base), MustParseVersion(test.deprecatedBy) + c.Assert(base.DeprecatedBy(&deprecatedBy), qt.Equals, test.result) + } +} + +func TestDeprecates(t *testing.T) { + c := qt.New(t) + versions := VersionSlice{ + MustParseVersion("2021-06-01~experimental"), + MustParseVersion("2021-06-07~beta"), + MustParseVersion("2021-07-01"), + MustParseVersion("2021-08-12~experimental"), + MustParseVersion("2021-09-16~beta"), + MustParseVersion("2021-10-31"), + } + sort.Sort(versions) + tests := []struct { + name string + target Version + deprecatedBy Version + isDeprecated bool + sunset time.Time + }{{ + name: "beta deprecates experimental", + target: MustParseVersion("2021-06-01~experimental"), + deprecatedBy: MustParseVersion("2021-06-07~beta"), + isDeprecated: true, + sunset: time.Date(2021, time.July, 8, 0, 0, 0, 0, time.UTC), + }, { + name: "ga deprecates beta", + target: MustParseVersion("2021-06-07~beta"), + deprecatedBy: MustParseVersion("2021-07-01"), + isDeprecated: true, + sunset: time.Date(2021, time.September, 30, 0, 0, 0, 0, time.UTC), + }, { + name: "ga deprecates ga", + target: MustParseVersion("2021-07-01"), + deprecatedBy: MustParseVersion("2021-10-31"), + isDeprecated: true, + sunset: time.Date(2022, time.April, 30, 0, 0, 0, 0, time.UTC), + }} + for i, test := range tests { + c.Logf("test#%d: %s", i, test.name) + deprecates, ok := versions.Deprecates(test.target) + c.Assert(ok, qt.Equals, test.isDeprecated) + if test.isDeprecated { + c.Assert(&test.deprecatedBy, qt.DeepEquals, deprecates) + sunset, ok := test.target.Sunset(deprecates) + c.Assert(ok, qt.IsTrue) + c.Assert(test.sunset, qt.Equals, sunset) + } + } +}