diff --git a/.github/scripts/split_spec.sh b/.github/scripts/split_spec.sh index 153bd2361a..3daa40ca52 100755 --- a/.github/scripts/split_spec.sh +++ b/.github/scripts/split_spec.sh @@ -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 diff --git a/tools/cli/internal/cli/merge/merge.go b/tools/cli/internal/cli/merge/merge.go index 44ba523df0..6c55ca5054 100644 --- a/tools/cli/internal/cli/merge/merge.go +++ b/tools/cli/internal/cli/merge/merge.go @@ -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) diff --git a/tools/cli/internal/cli/merge/merge_test.go b/tools/cli/internal/cli/merge/merge_test.go index 69e5d0d0e6..e7fc4700d2 100644 --- a/tools/cli/internal/cli/merge/merge_test.go +++ b/tools/cli/internal/cli/merge/merge_test.go @@ -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) { diff --git a/tools/cli/internal/cli/split/split.go b/tools/cli/internal/cli/split/split.go index df6e697fab..9cb9424554 100644 --- a/tools/cli/internal/cli/split/split.go +++ b/tools/cli/internal/cli/split/split.go @@ -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.' to 'path/path.to.file/file-version.'. +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) @@ -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: @@ -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) diff --git a/tools/cli/internal/cli/split/split_test.go b/tools/cli/internal/cli/split/split_test.go index 0597fd9305..9ca6371739 100644 --- a/tools/cli/internal/cli/split/split_test.go +++ b/tools/cli/internal/cli/split/split_test.go @@ -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) { diff --git a/tools/cli/internal/cli/usage/usage.go b/tools/cli/internal/cli/usage/usage.go index 8dc166a582..5918454f07 100644 --- a/tools/cli/internal/cli/usage/usage.go +++ b/tools/cli/internal/cli/usage/usage.go @@ -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." diff --git a/tools/cli/internal/openapi/file.go b/tools/cli/internal/openapi/file.go index edb6daf304..9c84b0d34c 100644 --- a/tools/cli/internal/openapi/file.go +++ b/tools/cli/internal/openapi/file.go @@ -29,19 +29,26 @@ import ( const ( JSON = "json" YAML = "yaml" + ALL = "all" 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 @@ -49,7 +56,7 @@ func SaveToFile[T any](path, format string, content T, fs afero.Fs) error { 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 @@ -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 +} diff --git a/tools/cli/internal/openapi/file_test.go b/tools/cli/internal/openapi/file_test.go index e239599064..e81f445ce2 100644 --- a/tools/cli/internal/openapi/file_test.go +++ b/tools/cli/internal/openapi/file_test.go @@ -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" ) @@ -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: "&", - }, - }, - )), - }, + name: "JSON with HTML characters", + spec: getTestSpecWithHTMLChars(), path: "test.json", format: "json", expected: "&", }, { - 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: "&", - }, - }, - )), - }, + name: "YAML with HTML characters", + spec: getTestSpecWithHTMLChars(), path: "test.yaml", format: "yaml", expected: "&", @@ -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: "&", + }, + { + name: "YAML with HTML characters", + spec: getTestSpecWithHTMLChars(), + + path: "test.yaml", + format: "yaml", + expected: "&", + }, + { + name: "all with HTML characters", + spec: getTestSpecWithHTMLChars(), + path: "test.yaml", + format: "all", + expected: "&", + }, + { + name: "empty format with HTML characters", + spec: getTestSpecWithHTMLChars(), + path: "test.yaml", + format: "", + expected: "&", + }, + } + + 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), "&") + + // read json file + data, err = afero.ReadFile(fs, "test.json") + require.NoError(t, err) + assert.Contains(t, string(data), "&") +} + +func getTestSpecWithHTMLChars() *Spec { + return &Spec{ + Paths: openapi3.NewPaths( + openapi3.WithPath( + "/api/atlas/v2/groups/{groupId}/databaseUsers/{databaseName}/{username}", + &openapi3.PathItem{ + Delete: &openapi3.Operation{ + Description: "&", + }, + }, + ), + ), + } +} diff --git a/tools/cli/test/e2e/cli/split_test.go b/tools/cli/test/e2e/cli/split_test.go index 07a95146fb..dc6eec02d9 100755 --- a/tools/cli/test/e2e/cli/split_test.go +++ b/tools/cli/test/e2e/cli/split_test.go @@ -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)