Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion tools/cli/internal/cli/changelog/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ func (o *Opts) newOutputFilePath(fileName string) string {
return fileName
}

// Builder builds the merge command with the following signature:
// CreateBuilder builds the merge command with the following signature:
// changelog create -b path_folder -r path_folder --dry-run
func CreateBuilder() *cobra.Command {
opts := &Opts{
Expand Down
2 changes: 2 additions & 0 deletions tools/cli/internal/cli/root/openapi/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/mongodb/openapi/tools/cli/internal/cli/changelog"
"github.com/mongodb/openapi/tools/cli/internal/cli/merge"
"github.com/mongodb/openapi/tools/cli/internal/cli/split"
"github.com/mongodb/openapi/tools/cli/internal/cli/sunset"
"github.com/mongodb/openapi/tools/cli/internal/cli/versions"
"github.com/mongodb/openapi/tools/cli/internal/version"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -59,6 +60,7 @@ func Builder() *cobra.Command {
versions.Builder(),
changelog.Builder(),
breakingchanges.Builder(),
sunset.Builder(),
)
return rootCmd
}
Expand Down
2 changes: 1 addition & 1 deletion tools/cli/internal/cli/split/split.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func (o *Opts) Run() error {
}

func (o *Opts) filter(oas *openapi3.T, version string) (result *openapi3.T, err error) {
log.Printf("Filtering OpenAPI document by version %s", version)
log.Printf("Filtering OpenAPI document by version '%s'", version)
apiVersion, err := apiversion.New(apiversion.WithVersion(version))
if err != nil {
return nil, err
Expand Down
103 changes: 103 additions & 0 deletions tools/cli/internal/cli/sunset/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// 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 sunset

import (
"encoding/json"
"fmt"
"strings"

"github.com/mongodb/openapi/tools/cli/internal/cli/flag"
"github.com/mongodb/openapi/tools/cli/internal/cli/usage"
"github.com/mongodb/openapi/tools/cli/internal/openapi"
"github.com/spf13/afero"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
)

type ListOpts struct {
fs afero.Fs
basePath string
outputPath string
format string
}

func (o *ListOpts) Run() error {
loader := openapi.NewOpenAPI3()
specInfo, err := loader.CreateOpenAPISpecFromPath(o.basePath)
if err != nil {
return err
}

bytes, err := o.newSunsetListBytes(openapi.NewSunsetListFromSpec(specInfo))
if err != nil {
return err
}
if o.outputPath != "" {
return afero.WriteFile(o.fs, o.outputPath, bytes, 0o600)
}

fmt.Println(string(bytes))
return nil
}

func (o *ListOpts) newSunsetListBytes(versions []*openapi.Sunset) ([]byte, error) {
data, err := json.MarshalIndent(versions, "", " ")
if err != nil {
return nil, err
}

if format := strings.ToLower(o.format); format == "json" {
return data, nil
}

var jsonData interface{}
if mErr := json.Unmarshal(data, &jsonData); mErr != nil {
return nil, mErr
}

yamlData, err := yaml.Marshal(jsonData)
if err != nil {
return nil, err
}

return yamlData, nil
}

// ListBuilder builds the merge command with the following signature:
// changelog create -b path_folder -r path_folder --dry-run
func ListBuilder() *cobra.Command {
opts := &ListOpts{
fs: afero.NewOsFs(),
}

cmd := &cobra.Command{
Use: "list -s spec.json -o json",
Short: "List API endpoints with a Sunset date for a given OpenAPI spec.",
Aliases: []string{"ls"},
Args: cobra.NoArgs,
RunE: func(_ *cobra.Command, _ []string) error {
return opts.Run()
},
}

cmd.Flags().StringVarP(&opts.basePath, flag.Spec, flag.SpecShort, "", usage.Spec)
cmd.Flags().StringVarP(&opts.outputPath, flag.Output, flag.OutputShort, "", usage.Output)
cmd.Flags().StringVarP(&opts.format, flag.Format, flag.FormatShort, "json", usage.Format)

_ = cmd.MarkFlagRequired(flag.Spec)

return cmd
}
41 changes: 41 additions & 0 deletions tools/cli/internal/cli/sunset/list_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// 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 sunset

import (
"testing"

"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
)

func TestList_Run(t *testing.T) {
fs := afero.NewMemMapFs()
opts := &ListOpts{
basePath: "../../../test/data/base_spec.json",
outputPath: "foas.json",
fs: fs,
}

if err := opts.Run(); err != nil {
t.Fatalf("Run() unexpected error: %v", err)
}

b, err := afero.ReadFile(fs, opts.outputPath)
if err != nil {
t.Fatalf("ReadFile() unexpected error: %v", err)
}
assert.NotEmpty(t, b)
}
33 changes: 33 additions & 0 deletions tools/cli/internal/cli/sunset/sunset.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// 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 sunset

import (
"github.com/spf13/cobra"
)

func Builder() *cobra.Command {
cmd := &cobra.Command{
Use: "sunset",
Short: "Manage the Sunset API for the OpenAPI spec.",
Annotations: map[string]string{
"toc": "true",
},
}

cmd.AddCommand(ListBuilder())

return cmd
}
30 changes: 30 additions & 0 deletions tools/cli/internal/cli/sunset/sunset_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// 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 sunset

import (
"testing"

"github.com/mongodb/openapi/tools/cli/internal/test"
)

func TestBuilder(t *testing.T) {
test.CmdValidator(
t,
Builder(),
1,
[]string{},
)
}
102 changes: 102 additions & 0 deletions tools/cli/internal/openapi/sunset.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// 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 openapi

import (
"github.com/getkin/kin-openapi/openapi3"
"github.com/tufin/oasdiff/load"
)

const (
sunsetExtensionName = "x-sunset"
apiVersionExtensionName = "x-xgen-version"
teamExtensionName = "x-xgen-owner-team"
)

type Sunset struct {
Operation string `json:"http_method" yaml:"http_method"`
Path string `json:"path" yaml:"path"`
Version string `json:"version" yaml:"version"`
SunsetDate string `json:"sunset_date" yaml:"sunset_date"`
Team string `json:"team" yaml:"team"`
}

func NewSunsetListFromSpec(spec *load.SpecInfo) []*Sunset {
var sunsets []*Sunset
paths := spec.Spec.Paths

for path, pathBody := range paths.Map() {
for operationName, operationBody := range pathBody.Operations() {
teamName := newTeamNameFromOperation(operationBody)
extensions := newExtensionsFrom2xxResponse(operationBody.Responses.Map())
if extensions == nil {
continue
}

apiVersion, ok := extensions[apiVersionExtensionName]
if !ok {
continue
}

sunsetExt, ok := extensions[sunsetExtensionName]
if !ok {
continue
}

sunset := Sunset{
Operation: operationName,
Path: path,
SunsetDate: sunsetExt.(string),
Version: apiVersion.(string),
Team: teamName,
}

sunsets = append(sunsets, &sunset)
}
}

return sunsets
}

func newTeamNameFromOperation(op *openapi3.Operation) string {
if value, ok := op.Extensions[teamExtensionName]; ok {
return value.(string)
}
return ""
}

func newExtensionsFrom2xxResponse(responsesMap map[string]*openapi3.ResponseRef) map[string]any {
if val, ok := responsesMap["200"]; ok {
return newExtensionsFromContent(val.Value.Content)
}
if val, ok := responsesMap["201"]; ok {
return newExtensionsFromContent(val.Value.Content)
}
if val, ok := responsesMap["202"]; ok {
return newExtensionsFromContent(val.Value.Content)
}
if val, ok := responsesMap["204"]; ok {
return newExtensionsFromContent(val.Value.Content)
}

return nil
}

func newExtensionsFromContent(content openapi3.Content) map[string]any {
for _, v := range content {
return v.Extensions
}
return nil
}
Loading
Loading