diff --git a/tools/cli/internal/openapi/filter/filter.go b/tools/cli/internal/openapi/filter/filter.go index 3a18988072..f6c238f3fc 100644 --- a/tools/cli/internal/openapi/filter/filter.go +++ b/tools/cli/internal/openapi/filter/filter.go @@ -11,6 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. + package filter import ( @@ -77,6 +78,7 @@ func DefaultFilters(oas *openapi3.T, metadata *Metadata) []Filter { &HiddenEnvsFilter{oas: oas, metadata: metadata}, &TagsFilter{oas: oas}, &OperationsFilter{oas: oas}, + &SunsetFilter{oas: oas}, } } diff --git a/tools/cli/internal/openapi/filter/filter_test.go b/tools/cli/internal/openapi/filter/filter_test.go index 89b86d0029..ba437684eb 100644 --- a/tools/cli/internal/openapi/filter/filter_test.go +++ b/tools/cli/internal/openapi/filter/filter_test.go @@ -112,7 +112,7 @@ func TestDefaultFilters(t *testing.T) { metadata := &Metadata{} filters := DefaultFilters(doc, metadata) - assert.Len(t, filters, 7) + assert.Len(t, filters, 8) } func TestFiltersWithoutVersioning(t *testing.T) { diff --git a/tools/cli/internal/openapi/filter/operations.go b/tools/cli/internal/openapi/filter/operations.go index 65b3760ba5..5d90152b0c 100644 --- a/tools/cli/internal/openapi/filter/operations.go +++ b/tools/cli/internal/openapi/filter/operations.go @@ -11,6 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. + package filter import ( diff --git a/tools/cli/internal/openapi/filter/sunset.go b/tools/cli/internal/openapi/filter/sunset.go new file mode 100644 index 0000000000..ae13930f3b --- /dev/null +++ b/tools/cli/internal/openapi/filter/sunset.go @@ -0,0 +1,66 @@ +// Copyright 2025 MongoDB Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package filter + +import ( + "strings" + + "github.com/getkin/kin-openapi/openapi3" +) + +const sunsetToBeDecided = "9999-12-31" + +// SunsetFilter removes the sunsetToBeDecided from the openapi specification. +type SunsetFilter struct { + oas *openapi3.T +} + +func (*SunsetFilter) ValidateMetadata() error { + return nil +} + +func (f *SunsetFilter) Apply() error { + if f.oas.Paths == nil { + return nil + } + + for _, pathItem := range f.oas.Paths.Map() { + for _, operation := range pathItem.Operations() { + if operation.Responses == nil { + continue + } + + applyOnOperation(operation) + } + } + return nil +} + +func applyOnOperation(op *openapi3.Operation) { + for key, response := range op.Responses.Map() { + if !strings.HasPrefix(key, "20") { + continue + } + + for _, content := range response.Value.Content { + if v, ok := content.Extensions["x-sunset"]; ok { + if v != sunsetToBeDecided { + continue + } + delete(content.Extensions, "x-sunset") + } + } + } +} diff --git a/tools/cli/internal/openapi/filter/sunset_test.go b/tools/cli/internal/openapi/filter/sunset_test.go new file mode 100644 index 0000000000..3ef1e67c23 --- /dev/null +++ b/tools/cli/internal/openapi/filter/sunset_test.go @@ -0,0 +1,337 @@ +// Copyright 2025 MongoDB Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package filter + +import ( + "testing" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/stretchr/testify/require" +) + +func TestSunsetFilter_Apply(t *testing.T) { + testCases := []struct { + name string + initSpec *openapi3.T + wantedSpec *openapi3.T + }{ + { + name: "Remove x-sunset when value is 9999-12-31", + initSpec: &openapi3.T{ + Paths: openapi3.NewPaths(openapi3.WithPath("test", &openapi3.PathItem{ + Get: &openapi3.Operation{ + OperationID: "testOperationID", + Summary: "testSummary", + Responses: openapi3.NewResponses(openapi3.WithName("200", &openapi3.Response{ + Content: openapi3.Content{ + "application/vnd.atlas.2025-01-01+json": { + Schema: &openapi3.SchemaRef{ + Ref: "#/components/schemas/PaginatedAppUserView", + }, + Extensions: map[string]any{ + "x-gen-version": "2025-01-01", + }, + }, + "application/vnd.atlas.2024-01-01+json": { + Schema: &openapi3.SchemaRef{ + Ref: "#/components/schemas/PaginatedAppUserView", + }, + Extensions: map[string]any{ + "x-gen-version": "2024-01-01", + "x-sunset": "2024-12-31", + }, + }, + "application/vnd.atlas.2023-01-01+json": { + Schema: &openapi3.SchemaRef{ + Ref: "#/components/schemas/PaginatedAppUserView", + }, + Extensions: map[string]any{ + "x-gen-version": "2023-01-01", + "x-sunset": "9999-12-31", + }, + }, + }, + })), + }, + })), + }, + wantedSpec: &openapi3.T{ + Paths: openapi3.NewPaths(openapi3.WithPath("test", &openapi3.PathItem{ + Get: &openapi3.Operation{ + OperationID: "testOperationID", + Summary: "testSummary", + Responses: openapi3.NewResponses(openapi3.WithName("200", &openapi3.Response{ + Content: openapi3.Content{ + "application/vnd.atlas.2025-01-01+json": { + Schema: &openapi3.SchemaRef{ + Ref: "#/components/schemas/PaginatedAppUserView", + }, + Extensions: map[string]any{ + "x-gen-version": "2025-01-01", + }, + }, + "application/vnd.atlas.2024-01-01+json": { + Schema: &openapi3.SchemaRef{ + Ref: "#/components/schemas/PaginatedAppUserView", + }, + Extensions: map[string]any{ + "x-gen-version": "2024-01-01", + "x-sunset": "2024-12-31", + }, + }, + "application/vnd.atlas.2023-01-01+json": { + Schema: &openapi3.SchemaRef{ + Ref: "#/components/schemas/PaginatedAppUserView", + }, + Extensions: map[string]any{ + "x-gen-version": "2023-01-01", + }, + }, + }, + })), + }, + })), + }, + }, + { + name: "Remove x-sunset when value is 9999-12-31 for a 204", + initSpec: &openapi3.T{ + Paths: openapi3.NewPaths(openapi3.WithPath("test", &openapi3.PathItem{ + Get: &openapi3.Operation{ + OperationID: "testOperationID", + Summary: "testSummary", + Responses: openapi3.NewResponses(openapi3.WithName("204", &openapi3.Response{ + Content: openapi3.Content{ + "application/vnd.atlas.2025-01-01+json": { + Schema: &openapi3.SchemaRef{ + Ref: "#/components/schemas/PaginatedAppUserView", + }, + Extensions: map[string]any{ + "x-gen-version": "2025-01-01", + }, + }, + "application/vnd.atlas.2024-01-01+json": { + Schema: &openapi3.SchemaRef{ + Ref: "#/components/schemas/PaginatedAppUserView", + }, + Extensions: map[string]any{ + "x-gen-version": "2024-01-01", + "x-sunset": "2024-12-31", + }, + }, + "application/vnd.atlas.2023-01-01+json": { + Schema: &openapi3.SchemaRef{ + Ref: "#/components/schemas/PaginatedAppUserView", + }, + Extensions: map[string]any{ + "x-gen-version": "2023-01-01", + "x-sunset": "9999-12-31", + }, + }, + }, + })), + }, + })), + }, + wantedSpec: &openapi3.T{ + Paths: openapi3.NewPaths(openapi3.WithPath("test", &openapi3.PathItem{ + Get: &openapi3.Operation{ + OperationID: "testOperationID", + Summary: "testSummary", + Responses: openapi3.NewResponses(openapi3.WithName("204", &openapi3.Response{ + Content: openapi3.Content{ + "application/vnd.atlas.2025-01-01+json": { + Schema: &openapi3.SchemaRef{ + Ref: "#/components/schemas/PaginatedAppUserView", + }, + Extensions: map[string]any{ + "x-gen-version": "2025-01-01", + }, + }, + "application/vnd.atlas.2024-01-01+json": { + Schema: &openapi3.SchemaRef{ + Ref: "#/components/schemas/PaginatedAppUserView", + }, + Extensions: map[string]any{ + "x-gen-version": "2024-01-01", + "x-sunset": "2024-12-31", + }, + }, + "application/vnd.atlas.2023-01-01+json": { + Schema: &openapi3.SchemaRef{ + Ref: "#/components/schemas/PaginatedAppUserView", + }, + Extensions: map[string]any{ + "x-gen-version": "2023-01-01", + }, + }, + }, + })), + }, + })), + }, + }, + { + name: "Keep x-sunset when value is different", + initSpec: &openapi3.T{ + Paths: openapi3.NewPaths(openapi3.WithPath("test", &openapi3.PathItem{ + Get: &openapi3.Operation{ + OperationID: "testOperationID", + Summary: "testSummary", + Responses: openapi3.NewResponses(openapi3.WithName("200", &openapi3.Response{ + Content: openapi3.Content{ + "application/vnd.atlas.2025-01-01+json": { + Schema: &openapi3.SchemaRef{ + Ref: "#/components/schemas/PaginatedAppUserView", + }, + Extensions: map[string]any{ + "x-gen-version": "2025-01-01", + }, + }, + "application/vnd.atlas.2024-01-01+json": { + Schema: &openapi3.SchemaRef{ + Ref: "#/components/schemas/PaginatedAppUserView", + }, + Extensions: map[string]any{ + "x-gen-version": "2024-01-01", + "x-sunset": "2024-12-31", + }, + }, + }, + })), + }, + })), + }, + wantedSpec: &openapi3.T{ + Paths: openapi3.NewPaths(openapi3.WithPath("test", &openapi3.PathItem{ + Get: &openapi3.Operation{ + OperationID: "testOperationID", + Summary: "testSummary", + Responses: openapi3.NewResponses(openapi3.WithName("200", &openapi3.Response{ + Content: openapi3.Content{ + "application/vnd.atlas.2025-01-01+json": { + Schema: &openapi3.SchemaRef{ + Ref: "#/components/schemas/PaginatedAppUserView", + }, + Extensions: map[string]any{ + "x-gen-version": "2025-01-01", + }, + }, + "application/vnd.atlas.2024-01-01+json": { + Schema: &openapi3.SchemaRef{ + Ref: "#/components/schemas/PaginatedAppUserView", + }, + Extensions: map[string]any{ + "x-gen-version": "2024-01-01", + "x-sunset": "2024-12-31", + }, + }, + }, + })), + }, + })), + }, + }, + { + name: "Ignore non-2xx responses", + initSpec: &openapi3.T{ + Paths: openapi3.NewPaths(openapi3.WithPath("test", &openapi3.PathItem{ + Get: &openapi3.Operation{ + OperationID: "testOperationID", + Summary: "testSummary", + Responses: openapi3.NewResponses(openapi3.WithName("404", &openapi3.Response{ + Content: openapi3.Content{ + "application/vnd.atlas.2025-01-01+json": { + Schema: &openapi3.SchemaRef{ + Ref: "#/components/schemas/PaginatedAppUserView", + }, + Extensions: map[string]any{ + "x-gen-version": "2025-01-01", + }, + }, + "application/vnd.atlas.2024-01-01+json": { + Schema: &openapi3.SchemaRef{ + Ref: "#/components/schemas/PaginatedAppUserView", + }, + Extensions: map[string]any{ + "x-gen-version": "2024-01-01", + "x-sunset": "2024-12-31", + }, + }, + "application/vnd.atlas.2023-01-01+json": { + Schema: &openapi3.SchemaRef{ + Ref: "#/components/schemas/PaginatedAppUserView", + }, + Extensions: map[string]any{ + "x-gen-version": "2023-01-01", + "x-sunset": "9999-12-31", + }, + }, + }, + })), + }, + })), + }, + wantedSpec: &openapi3.T{ + Paths: openapi3.NewPaths(openapi3.WithPath("test", &openapi3.PathItem{ + Get: &openapi3.Operation{ + OperationID: "testOperationID", + Summary: "testSummary", + Responses: openapi3.NewResponses(openapi3.WithName("404", &openapi3.Response{ + Content: openapi3.Content{ + "application/vnd.atlas.2025-01-01+json": { + Schema: &openapi3.SchemaRef{ + Ref: "#/components/schemas/PaginatedAppUserView", + }, + Extensions: map[string]any{ + "x-gen-version": "2025-01-01", + }, + }, + "application/vnd.atlas.2024-01-01+json": { + Schema: &openapi3.SchemaRef{ + Ref: "#/components/schemas/PaginatedAppUserView", + }, + Extensions: map[string]any{ + "x-gen-version": "2024-01-01", + "x-sunset": "2024-12-31", + }, + }, + "application/vnd.atlas.2023-01-01+json": { + Schema: &openapi3.SchemaRef{ + Ref: "#/components/schemas/PaginatedAppUserView", + }, + Extensions: map[string]any{ + "x-gen-version": "2023-01-01", + "x-sunset": "9999-12-31", + }, + }, + }, + })), + }, + })), + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + f := &SunsetFilter{ + oas: tc.initSpec, + } + + require.NoError(t, f.Apply()) + require.EqualValues(t, tc.wantedSpec, f.oas) + }) + } +}