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
5 changes: 2 additions & 3 deletions .github/scripts/split_spec.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@ foascli versions -s openapi-foas.json -o ./openapi/v2/versions.json --env "${tar

echo "Running FOAS CLI split command with the following --env=${target_env:?} and -o=./openapi/v2/openapi.json"

foascli split -s openapi-foas.json --env "${target_env:?}" -o ./openapi/v2/openapi.json
foascli split -s openapi-foas.json --env "${target_env:?}" -o ./openapi/v2/openapi.json --format all
mv -f "openapi-foas.json" "./openapi/v2.json"

foascli split -s openapi-foas.yaml --env "${target_env:?}" -o ./openapi/v2/openapi.yaml
mv -f "openapi-foas.yaml" "./openapi/v2.yaml"

# Create folder if it does not exist
mkdir -p ./openapi/v2/private

Expand Down
4 changes: 2 additions & 2 deletions tools/cli/internal/cli/merge/merge.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ func (o *Opts) PreRunE(_ []string) error {
return fmt.Errorf("output file must be either a JSON or YAML file, got %s", o.outputPath)
}

if o.format != "json" && o.format != "yaml" {
return fmt.Errorf("output format must be either 'json' or 'yaml', got %s", o.format)
if err := openapi.ValidateFormat(o.format); err != nil {
return err
}

m, err := openapi.NewOasDiff(o.basePath, o.excludePrivatePaths)
Expand Down
2 changes: 1 addition & 1 deletion tools/cli/internal/cli/merge/merge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ func TestInvalidFormat_PreRun(t *testing.T) {

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

func TestInvalidPath_PreRun(t *testing.T) {
Expand Down
24 changes: 13 additions & 11 deletions tools/cli/internal/cli/split/split.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,20 @@ func (o *Opts) saveVersionedOas(oas *openapi3.T, version string) error {
path = o.outputPath
}

path = strings.Replace(path, "."+o.format, fmt.Sprintf("-%s.%s", version, o.format), 1)
path = getVersionPath(path, version)
return openapi.Save(path, oas, o.format, o.fs)
}

// getVersionPath replaces file path with version.
// Example: 'path/path.to.file/file.<json|yaml|any>' to 'path/path.to.file/file-version.<json|yaml|any>'.
func getVersionPath(path, version string) string {
extIndex := strings.LastIndex(path, ".")
if extIndex == -1 {
return fmt.Sprintf("%s-%s", path, version)
}
return fmt.Sprintf("%s-%s%s", path[:extIndex], version, path[extIndex:])
}

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)
Expand All @@ -103,15 +113,7 @@ func (o *Opts) PreRunE(_ []string) error {
return fmt.Errorf("output file must be either a JSON or YAML file, got %s", o.outputPath)
}

if o.format != openapi.JSON && o.format != openapi.YAML {
return fmt.Errorf("output format must be either 'json' or 'yaml', got %s", o.format)
}

if strings.Contains(o.basePath, openapi.DotYAML) {
o.format = openapi.YAML
}

return nil
return openapi.ValidateFormat(o.format)
}

// Builder builds the split command with the following signature:
Expand All @@ -136,7 +138,7 @@ func Builder() *cobra.Command {
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().StringVarP(&opts.format, flag.Format, flag.FormatShort, openapi.JSON, usage.Format)
cmd.Flags().StringVarP(&opts.format, flag.Format, flag.FormatShort, openapi.ALL, usage.Format)
cmd.Flags().StringVar(&opts.gitSha, flag.GitSha, "", usage.GitSha)

_ = cmd.MarkFlagRequired(flag.Output)
Expand Down
2 changes: 1 addition & 1 deletion tools/cli/internal/cli/split/split_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ func TestInvalidFormat_PreRun(t *testing.T) {

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

func TestInvalidPath_PreRun(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion tools/cli/internal/cli/usage/usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const (
Base = "Base OAS. The command will merge other OASes into it."
External = "OASes that will be merged into the base OAS."
Output = "File name or path where the command will store the output."
Format = "Output format. Supported values are 'json' and 'yaml'."
Format = "Output format. Supported values are 'json', 'yaml' or 'all' which will generate one file for each supported format."
Versions = "Boolean flag that defines wether to split the OAS into multiple versions."
VersionsChangelog = "List of versions to consider when generating the changelog. (Format: YYYY-MM-DD)"
Spec = "Path to the OAS file."
Expand Down
24 changes: 21 additions & 3 deletions tools/cli/internal/openapi/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,27 +29,34 @@ import (
const (
JSON = "json"
YAML = "yaml"
ALL = "all"
Copy link
Collaborator

Choose a reason for hiding this comment

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

[No need to address the comment]. Something to consider in the future is tohave the --format flag as a list so that we can provide multiple formats. The drawback of having ALL are:

  • it is not clear what ALL traslate to by looking at the command but we force users to run --help
  • if we add support for another format (not likely), ALL will start generating other format too 🤔

I think is fine to have all since we are the user of this cli and it is not likely we will support other formats

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ack, if we add more formats we can defintely revisit! or keep supporitng the all to generate all formats

DotYAML = ".yaml"
DotJSON = ".json"
)

// SaveToFile saves the content to a file in the specified format.
// If format is empty, it saves the content in both JSON and YAML formats.
// If format is empty or set to 'all', it saves the content in both JSON and YAML formats.
func SaveToFile[T any](path, format string, content T, fs afero.Fs) error {
data, err := SerializeToJSON(content)
if err != nil {
return err
}

if format == JSON || format == "" {
if format == ALL || format == "" {
// strip . format from path
path = strings.TrimSuffix(path, DotJSON)
path = strings.TrimSuffix(path, DotYAML)
}

if format == JSON || format == "" || format == ALL {
jsonPath := newPathWithExtension(path, JSON)
if errJSON := afero.WriteFile(fs, jsonPath, data, 0o600); errJSON != nil {
return errJSON
}
log.Printf("\nFile was saved in '%s'.\n\n", jsonPath)
}

if format == YAML || format == "" {
if format == YAML || format == "" || format == ALL {
dataYAML, err := SerializeToYAML(data)
if err != nil {
return err
Expand Down Expand Up @@ -118,6 +125,17 @@ func SerializeToYAML(data []byte) ([]byte, error) {
return yamlData, nil
}

// Save saves the OpenAPI document to a file in the specified format. This is important for public
// OpenAPI documents as it ensures to follow the order of the Spec object.
func Save(path string, oas *openapi3.T, format string, fs afero.Fs) error {
return SaveToFile(path, format, newSpec(oas), fs)
}

// ValidateFormat validates the format of files supported.
func ValidateFormat(format string) error {
if format != JSON && format != YAML && format != ALL {
return fmt.Errorf("format must be either 'json', 'yaml' or 'all', got '%s'", format)
}

return nil
}
112 changes: 88 additions & 24 deletions tools/cli/internal/openapi/file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"testing"

"github.com/getkin/kin-openapi/openapi3"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand All @@ -31,35 +32,15 @@ func TestNewArrayBytesFromOAS(t *testing.T) {
expected string
}{
{
name: "JSON with HTML characters",
spec: &Spec{
Paths: openapi3.NewPaths(
openapi3.WithPath(
"/api/atlas/v2/groups/{groupId}/databaseUsers/{databaseName}/{username}",
&openapi3.PathItem{
Delete: &openapi3.Operation{
Description: "<test>&</test>",
},
},
)),
},
name: "JSON with HTML characters",
spec: getTestSpecWithHTMLChars(),
path: "test.json",
format: "json",
expected: "<test>&</test>",
},
{
name: "YAML with HTML characters",
spec: &Spec{
Paths: openapi3.NewPaths(
openapi3.WithPath(
"/api/atlas/v2/groups/{groupId}/databaseUsers/{databaseName}/{username}",
&openapi3.PathItem{
Delete: &openapi3.Operation{
Description: "<test>&</test>",
},
},
)),
},
name: "YAML with HTML characters",
spec: getTestSpecWithHTMLChars(),
path: "test.yaml",
format: "yaml",
expected: "<test>&</test>",
Expand Down Expand Up @@ -109,3 +90,86 @@ func TestNewArrayBytesFromOAS(t *testing.T) {
})
}
}

func TestSaveToFileFormats(t *testing.T) {
tests := []struct {
name string
spec *Spec
path string
format string
expected string
}{
{
name: "JSON with HTML characters",
spec: getTestSpecWithHTMLChars(),
path: "test.json",
format: "json",
expected: "<test>&</test>",
},
{
name: "YAML with HTML characters",
spec: getTestSpecWithHTMLChars(),

path: "test.yaml",
format: "yaml",
expected: "<test>&</test>",
},
{
name: "all with HTML characters",
spec: getTestSpecWithHTMLChars(),
path: "test.yaml",
format: "all",
expected: "<test>&</test>",
},
{
name: "empty format with HTML characters",
spec: getTestSpecWithHTMLChars(),
path: "test.yaml",
format: "",
expected: "<test>&</test>",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fs := afero.NewMemMapFs()
err := SaveToFile(tt.path, tt.format, tt.spec, fs)
require.NoError(t, err)

data, err := afero.ReadFile(fs, tt.path)
require.NoError(t, err)
assert.Contains(t, string(data), tt.expected)
})
}
}

func TestSaveToFile_All(t *testing.T) {
fs := afero.NewMemMapFs()
err := SaveToFile("test.yaml", "all", getTestSpecWithHTMLChars(), fs)
require.NoError(t, err)

// read yaml file
data, err := afero.ReadFile(fs, "test.yaml")
require.NoError(t, err)
assert.Contains(t, string(data), "<test>&</test>")

// read json file
data, err = afero.ReadFile(fs, "test.json")
require.NoError(t, err)
assert.Contains(t, string(data), "<test>&</test>")
}

func getTestSpecWithHTMLChars() *Spec {
return &Spec{
Paths: openapi3.NewPaths(
openapi3.WithPath(
"/api/atlas/v2/groups/{groupId}/databaseUsers/{databaseName}/{username}",
&openapi3.PathItem{
Delete: &openapi3.Operation{
Description: "<test>&</test>",
},
},
),
),
}
}
40 changes: 40 additions & 0 deletions tools/cli/test/e2e/cli/split_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,46 @@ func versionInFuture(t *testing.T, version string) bool {
return v.Date().After(time.Now())
}

func TestSplitVersionsFilteredOASes_All(t *testing.T) {
cliPath := NewBin(t)
env := "dev"
folder := env
base := getInputPath(t, "filtered", "json", folder)
jsonOutputPath := getOutputFolder(t, folder) + "/filtered-dev-output.json"
cmd := exec.Command(cliPath,
"split",
"-s",
base,
"-o",
jsonOutputPath,
"--env",
"dev",
"--format",
"all",
)

var o, e bytes.Buffer
cmd.Stdout = &o
cmd.Stderr = &e
require.NoError(t, cmd.Run(), e.String())

versions := getVersions(t, cliPath, base, folder)
for _, version := range versions {
if slices.Contains(skipVersions, version) {
continue
}
if env == "prod" && !versionInFuture(t, version) {
continue
}
fmt.Printf("Validating version: %s\n", version)
noExtensionOutputPath := strings.Replace(jsonOutputPath, ".json", "", 1)
versionedOutputPath := noExtensionOutputPath + "-" + version + ".json"
ValidateVersionedSpec(t, NewValidAtlasSpecPath(t, version, folder), versionedOutputPath)
versionedOutputPath = noExtensionOutputPath + "-" + version + ".yaml"
ValidateVersionedSpec(t, NewValidAtlasSpecPath(t, version, folder), versionedOutputPath)
}
}

func TestSplitVersionsForOASWithExternalReferences(t *testing.T) {
folder := "dev"
cliPath := NewBin(t)
Expand Down
Loading