Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
113 changes: 113 additions & 0 deletions tools/cli/internal/cli/filter/filter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Copyright 2024 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 (
"fmt"
"log"

"github.com/getkin/kin-openapi/openapi3"
"github.com/mongodb/openapi/tools/cli/internal/apiversion"
"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/mongodb/openapi/tools/cli/internal/openapi/filter"
"github.com/spf13/afero"
"github.com/spf13/cobra"
)

type Opts struct {
fs afero.Fs
basePath string
outputPath string
env string
version string
format string
}

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

var filteredOAS *openapi3.T
// If a version is provided, versioning filters will also be applied.
if o.version != "" {
filteredOAS, err = ByVersion(specInfo.Spec, o.version, o.env)
} else {
filters := filter.FiltersWithoutVersioning
metadata := filter.NewMetadata(nil, o.env)
filteredOAS, err = filter.ApplyFilters(specInfo.Spec, metadata, filters)
}

if err != nil {
return err
}

return openapi.Save(o.outputPath, filteredOAS, o.format, o.fs)
}

func ByVersion(oas *openapi3.T, version, env string) (result *openapi3.T, err error) {
log.Printf("Filtering OpenAPI document by version %q", version)
apiVersion, err := apiversion.New(apiversion.WithVersion(version))
if err != nil {
return nil, err
}

return filter.ApplyFilters(oas, filter.NewMetadata(apiVersion, env), filter.DefaultFilters)
}

func (o *Opts) PreRunE(_ []string) error {
if o.basePath == "" {
return fmt.Errorf("no OAS detected. Please, use the flag %s to include the base OAS", flag.Base)
}

return openapi.ValidateFormatAndOutput(o.format, o.outputPath)
}

// Builder builds the filter command with the following signature:
// filter -s oas -o output-oas.json.
func Builder() *cobra.Command {
opts := &Opts{
fs: afero.NewOsFs(),
}

cmd := &cobra.Command{
Use: "filter -s spec ",
Short: `Filter Open API specification removing hidden endpoints and extension metadata.
If a version is provided, versioning filters will also be applied.`,
Comment on lines +91 to +92
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we define a flag with the name of the filter to apply? I am wondering a flag would help to clarify what filters will be applied to the spec

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about it, but since we don't have a use case I would rather only give the two options for now, to avoid generating several different specs with different filters applied

Args: cobra.NoArgs,
PreRunE: func(_ *cobra.Command, args []string) error {
return opts.PreRunE(args)
},
RunE: func(_ *cobra.Command, _ []string) error {
return opts.Run()
},
}

cmd.Flags().StringVarP(&opts.basePath, flag.Spec, flag.SpecShort, "-", usage.Spec)
cmd.Flags().StringVar(&opts.env, flag.Environment, "", usage.Environment)
cmd.Flags().StringVarP(&opts.outputPath, flag.Output, flag.OutputShort, "", usage.Output)
cmd.Flags().StringVar(&opts.version, flag.Version, "", usage.Version)
cmd.Flags().StringVarP(&opts.format, flag.Format, flag.FormatShort, openapi.ALL, usage.Format)

// Required flags
_ = cmd.MarkFlagRequired(flag.Output)
_ = cmd.MarkFlagRequired(flag.Spec)
_ = cmd.MarkFlagRequired(flag.Environment)
return cmd
}
143 changes: 143 additions & 0 deletions tools/cli/internal/cli/filter/filter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// Copyright 2024 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 (
"net/url"
"testing"

"github.com/getkin/kin-openapi/openapi3"
"github.com/mongodb/openapi/tools/cli/internal/openapi"
"github.com/spf13/afero"
"github.com/stretchr/testify/require"
"github.com/tufin/oasdiff/load"
)

func TestSuccessfulFilter_Run(t *testing.T) {
fs := afero.NewMemMapFs()
t.Parallel()

opts := &Opts{
basePath: "../../../test/data/base_spec.json",
outputPath: "filtered-oas.yaml",
fs: fs,
env: "dev",
}

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

newSpec, err := loadRunResultOas(fs, opts.outputPath)
require.NoError(t, err)

// check all paths are kept
for _, pathItem := range newSpec.Spec.Paths.Map() {
for _, operation := range pathItem.Operations() {
// check extensions are removed at the operation level
require.Nil(t, operation.Extensions)
}
}
}

func TestSuccessfulFilterWithVersion_Run(t *testing.T) {
fs := afero.NewMemMapFs()
t.Parallel()

opts := &Opts{
basePath: "../../../test/data/base_spec.json",
outputPath: "filtered-oas.yaml",
fs: fs,
env: "dev",
version: "2023-01-01",
}

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

s, err := loadRunResultOas(fs, opts.outputPath)
require.NoError(t, err)
// assert /api/atlas/v2/groups/{groupId}:migrate does not exist in filtered spec as it is from a newer version
paths := s.Spec.Paths.Map()
require.Contains(t, paths, "/api/atlas/v2/groups")
require.NotContains(t, paths, "/api/atlas/v2/groups/{groupId}:migrate")
}

func TestOpts_PreRunE(t *testing.T) {
testCases := []struct {
wantErr require.ErrorAssertionFunc
basePath string
name string
}{
{
wantErr: require.Error,
name: "NoBasePath",
},
{
wantErr: require.NoError,
basePath: "../../../test/data/base_spec.json",
name: "Successful",
},
}

for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
o := &Opts{
basePath: tt.basePath,
format: "json",
}
tt.wantErr(t, o.PreRunE(nil))
})
}
}

func TestInvalidFormat_PreRun(t *testing.T) {
opts := &Opts{
outputPath: "foas.json",
basePath: "base.json",
format: "html",
}

err := opts.PreRunE(nil)
require.Error(t, err)
require.EqualError(t, err, "format must be either 'json', 'yaml' or 'all', got 'html'")
}

func TestInvalidPath_PreRun(t *testing.T) {
opts := &Opts{
outputPath: "foas.html",
basePath: "base.json",
format: "all",
}

err := opts.PreRunE(nil)
require.Error(t, err)
require.EqualError(t, err, "output file must be either a JSON or YAML file, got foas.html")
}

func loadRunResultOas(fs afero.Fs, fileName string) (*load.SpecInfo, error) {
oas := openapi.NewOpenAPI3()
oas.Loader.ReadFromURIFunc = func(_ *openapi3.Loader, _ *url.URL) ([]byte, error) {
f, err := fs.OpenFile(fileName, 0, 0)
if err != nil {
return nil, err
}
defer f.Close()
return afero.ReadAll(f)
}

return oas.CreateOpenAPISpecFromPath(fileName)
}
2 changes: 2 additions & 0 deletions tools/cli/internal/cli/flag/flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,6 @@ const (
To = "to"
StabilityLevel = "stability-level"
StabilityLevelShort = "l"
Version = "version"
VersionShort = "v"
)
7 changes: 1 addition & 6 deletions tools/cli/internal/cli/merge/merge.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ package merge
import (
"encoding/json"
"fmt"
"strings"

"github.com/mongodb/openapi/tools/cli/internal/cli/flag"
"github.com/mongodb/openapi/tools/cli/internal/cli/usage"
Expand Down Expand Up @@ -71,11 +70,7 @@ func (o *Opts) PreRunE(_ []string) error {
return fmt.Errorf("no external OAS detected. Please, use the flag %s to include at least one OAS", flag.External)
}

if o.outputPath != "" && !strings.Contains(o.outputPath, ".json") && !strings.Contains(o.outputPath, ".yaml") {
return fmt.Errorf("output file must be either a JSON or YAML file, got %s", o.outputPath)
}

if err := openapi.ValidateFormat(o.format); err != nil {
if err := openapi.ValidateFormatAndOutput(o.format, o.outputPath); err != nil {
return err
}

Expand Down
1 change: 1 addition & 0 deletions tools/cli/internal/cli/merge/merge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ func TestInvalidPath_PreRun(t *testing.T) {
outputPath: "foas.html",
externalPaths: externalPaths,
basePath: "base.json",
format: "json",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
format: "json",
jsonFormat: "json",

}

err := opts.PreRunE(nil)
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 @@ -20,6 +20,7 @@ import (

"github.com/mongodb/openapi/tools/cli/internal/cli/breakingchanges"
"github.com/mongodb/openapi/tools/cli/internal/cli/changelog"
"github.com/mongodb/openapi/tools/cli/internal/cli/filter"
"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"
Expand Down Expand Up @@ -61,6 +62,7 @@ func Builder() *cobra.Command {
changelog.Builder(),
breakingchanges.Builder(),
sunset.Builder(),
filter.Builder(),
)
return rootCmd
}
Expand Down
21 changes: 3 additions & 18 deletions tools/cli/internal/cli/split/split.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,10 @@ import (
"strings"

"github.com/getkin/kin-openapi/openapi3"
"github.com/mongodb/openapi/tools/cli/internal/apiversion"
"github.com/mongodb/openapi/tools/cli/internal/cli/filter"
"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/mongodb/openapi/tools/cli/internal/openapi/filter"
"github.com/spf13/afero"
"github.com/spf13/cobra"
)
Expand All @@ -51,7 +50,7 @@ func (o *Opts) Run() error {
}

for _, version := range versions {
filteredOAS, err := o.filter(specInfo.Spec, version)
filteredOAS, err := filter.ByVersion(specInfo.Spec, version, o.env)
if err != nil {
return err
}
Expand All @@ -74,16 +73,6 @@ func (o *Opts) Run() error {
return nil
}

func (o *Opts) filter(oas *openapi3.T, version string) (result *openapi3.T, err error) {
log.Printf("Filtering OpenAPI document by version %q", version)
apiVersion, err := apiversion.New(apiversion.WithVersion(version))
if err != nil {
return nil, err
}

return filter.ApplyFilters(oas, filter.NewMetadata(apiVersion, o.env), filter.DefaultFilters)
}

func (o *Opts) saveVersionedOas(oas *openapi3.T, version string) error {
path := o.basePath
if o.outputPath != "" {
Expand All @@ -109,11 +98,7 @@ func (o *Opts) PreRunE(_ []string) error {
return fmt.Errorf("no OAS detected. Please, use the flag %s to include the base OAS", flag.Base)
}

if o.outputPath != "" && !strings.Contains(o.outputPath, openapi.DotJSON) && !strings.Contains(o.outputPath, openapi.DotYAML) {
return fmt.Errorf("output file must be either a JSON or YAML file, got %s", o.outputPath)
}

return openapi.ValidateFormat(o.format)
return openapi.ValidateFormatAndOutput(o.format, o.outputPath)
}

// Builder builds the split command with the following signature:
Expand Down
Loading
Loading