Skip to content

Commit cc38548

Browse files
authored
CLOUDP-304430: allow multiple formats (#461)
1 parent 1b297b7 commit cc38548

File tree

9 files changed

+169
-46
lines changed

9 files changed

+169
-46
lines changed

.github/scripts/split_spec.sh

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,10 @@ foascli versions -s openapi-foas.json -o ./openapi/v2/versions.json --env "${tar
66

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

9-
foascli split -s openapi-foas.json --env "${target_env:?}" -o ./openapi/v2/openapi.json
9+
foascli split -s openapi-foas.json --env "${target_env:?}" -o ./openapi/v2/openapi.json --format all
1010
mv -f "openapi-foas.json" "./openapi/v2.json"
11-
12-
foascli split -s openapi-foas.yaml --env "${target_env:?}" -o ./openapi/v2/openapi.yaml
1311
mv -f "openapi-foas.yaml" "./openapi/v2.yaml"
12+
1413
# Create folder if it does not exist
1514
mkdir -p ./openapi/v2/private
1615

tools/cli/internal/cli/merge/merge.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@ func (o *Opts) PreRunE(_ []string) error {
7575
return fmt.Errorf("output file must be either a JSON or YAML file, got %s", o.outputPath)
7676
}
7777

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

8282
m, err := openapi.NewOasDiff(o.basePath, o.excludePrivatePaths)

tools/cli/internal/cli/merge/merge_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ func TestInvalidFormat_PreRun(t *testing.T) {
150150

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

156156
func TestInvalidPath_PreRun(t *testing.T) {

tools/cli/internal/cli/split/split.go

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,20 @@ func (o *Opts) saveVersionedOas(oas *openapi3.T, version string) error {
9090
path = o.outputPath
9191
}
9292

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

97+
// getVersionPath replaces file path with version.
98+
// Example: 'path/path.to.file/file.<json|yaml|any>' to 'path/path.to.file/file-version.<json|yaml|any>'.
99+
func getVersionPath(path, version string) string {
100+
extIndex := strings.LastIndex(path, ".")
101+
if extIndex == -1 {
102+
return fmt.Sprintf("%s-%s", path, version)
103+
}
104+
return fmt.Sprintf("%s-%s%s", path[:extIndex], version, path[extIndex:])
105+
}
106+
97107
func (o *Opts) PreRunE(_ []string) error {
98108
if o.basePath == "" {
99109
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 {
103113
return fmt.Errorf("output file must be either a JSON or YAML file, got %s", o.outputPath)
104114
}
105115

106-
if o.format != openapi.JSON && o.format != openapi.YAML {
107-
return fmt.Errorf("output format must be either 'json' or 'yaml', got %s", o.format)
108-
}
109-
110-
if strings.Contains(o.basePath, openapi.DotYAML) {
111-
o.format = openapi.YAML
112-
}
113-
114-
return nil
116+
return openapi.ValidateFormat(o.format)
115117
}
116118

117119
// Builder builds the split command with the following signature:
@@ -136,7 +138,7 @@ func Builder() *cobra.Command {
136138
cmd.Flags().StringVarP(&opts.basePath, flag.Spec, flag.SpecShort, "-", usage.Spec)
137139
cmd.Flags().StringVar(&opts.env, flag.Environment, "", usage.Environment)
138140
cmd.Flags().StringVarP(&opts.outputPath, flag.Output, flag.OutputShort, "", usage.Output)
139-
cmd.Flags().StringVarP(&opts.format, flag.Format, flag.FormatShort, openapi.JSON, usage.Format)
141+
cmd.Flags().StringVarP(&opts.format, flag.Format, flag.FormatShort, openapi.ALL, usage.Format)
140142
cmd.Flags().StringVar(&opts.gitSha, flag.GitSha, "", usage.GitSha)
141143

142144
_ = cmd.MarkFlagRequired(flag.Output)

tools/cli/internal/cli/split/split_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ func TestInvalidFormat_PreRun(t *testing.T) {
172172

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

178178
func TestInvalidPath_PreRun(t *testing.T) {

tools/cli/internal/cli/usage/usage.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const (
1818
Base = "Base OAS. The command will merge other OASes into it."
1919
External = "OASes that will be merged into the base OAS."
2020
Output = "File name or path where the command will store the output."
21-
Format = "Output format. Supported values are 'json' and 'yaml'."
21+
Format = "Output format. Supported values are 'json', 'yaml' or 'all' which will generate one file for each supported format."
2222
Versions = "Boolean flag that defines wether to split the OAS into multiple versions."
2323
VersionsChangelog = "List of versions to consider when generating the changelog. (Format: YYYY-MM-DD)"
2424
Spec = "Path to the OAS file."

tools/cli/internal/openapi/file.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,27 +29,34 @@ import (
2929
const (
3030
JSON = "json"
3131
YAML = "yaml"
32+
ALL = "all"
3233
DotYAML = ".yaml"
3334
DotJSON = ".json"
3435
)
3536

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

44-
if format == JSON || format == "" {
45+
if format == ALL || format == "" {
46+
// strip . format from path
47+
path = strings.TrimSuffix(path, DotJSON)
48+
path = strings.TrimSuffix(path, DotYAML)
49+
}
50+
51+
if format == JSON || format == "" || format == ALL {
4552
jsonPath := newPathWithExtension(path, JSON)
4653
if errJSON := afero.WriteFile(fs, jsonPath, data, 0o600); errJSON != nil {
4754
return errJSON
4855
}
4956
log.Printf("\nFile was saved in '%s'.\n\n", jsonPath)
5057
}
5158

52-
if format == YAML || format == "" {
59+
if format == YAML || format == "" || format == ALL {
5360
dataYAML, err := SerializeToYAML(data)
5461
if err != nil {
5562
return err
@@ -118,6 +125,17 @@ func SerializeToYAML(data []byte) ([]byte, error) {
118125
return yamlData, nil
119126
}
120127

128+
// Save saves the OpenAPI document to a file in the specified format. This is important for public
129+
// OpenAPI documents as it ensures to follow the order of the Spec object.
121130
func Save(path string, oas *openapi3.T, format string, fs afero.Fs) error {
122131
return SaveToFile(path, format, newSpec(oas), fs)
123132
}
133+
134+
// ValidateFormat validates the format of files supported.
135+
func ValidateFormat(format string) error {
136+
if format != JSON && format != YAML && format != ALL {
137+
return fmt.Errorf("format must be either 'json', 'yaml' or 'all', got '%s'", format)
138+
}
139+
140+
return nil
141+
}

tools/cli/internal/openapi/file_test.go

Lines changed: 88 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"testing"
1919

2020
"github.com/getkin/kin-openapi/openapi3"
21+
"github.com/spf13/afero"
2122
"github.com/stretchr/testify/assert"
2223
"github.com/stretchr/testify/require"
2324
)
@@ -31,35 +32,15 @@ func TestNewArrayBytesFromOAS(t *testing.T) {
3132
expected string
3233
}{
3334
{
34-
name: "JSON with HTML characters",
35-
spec: &Spec{
36-
Paths: openapi3.NewPaths(
37-
openapi3.WithPath(
38-
"/api/atlas/v2/groups/{groupId}/databaseUsers/{databaseName}/{username}",
39-
&openapi3.PathItem{
40-
Delete: &openapi3.Operation{
41-
Description: "<test>&</test>",
42-
},
43-
},
44-
)),
45-
},
35+
name: "JSON with HTML characters",
36+
spec: getTestSpecWithHTMLChars(),
4637
path: "test.json",
4738
format: "json",
4839
expected: "<test>&</test>",
4940
},
5041
{
51-
name: "YAML with HTML characters",
52-
spec: &Spec{
53-
Paths: openapi3.NewPaths(
54-
openapi3.WithPath(
55-
"/api/atlas/v2/groups/{groupId}/databaseUsers/{databaseName}/{username}",
56-
&openapi3.PathItem{
57-
Delete: &openapi3.Operation{
58-
Description: "<test>&</test>",
59-
},
60-
},
61-
)),
62-
},
42+
name: "YAML with HTML characters",
43+
spec: getTestSpecWithHTMLChars(),
6344
path: "test.yaml",
6445
format: "yaml",
6546
expected: "<test>&</test>",
@@ -109,3 +90,86 @@ func TestNewArrayBytesFromOAS(t *testing.T) {
10990
})
11091
}
11192
}
93+
94+
func TestSaveToFileFormats(t *testing.T) {
95+
tests := []struct {
96+
name string
97+
spec *Spec
98+
path string
99+
format string
100+
expected string
101+
}{
102+
{
103+
name: "JSON with HTML characters",
104+
spec: getTestSpecWithHTMLChars(),
105+
path: "test.json",
106+
format: "json",
107+
expected: "<test>&</test>",
108+
},
109+
{
110+
name: "YAML with HTML characters",
111+
spec: getTestSpecWithHTMLChars(),
112+
113+
path: "test.yaml",
114+
format: "yaml",
115+
expected: "<test>&</test>",
116+
},
117+
{
118+
name: "all with HTML characters",
119+
spec: getTestSpecWithHTMLChars(),
120+
path: "test.yaml",
121+
format: "all",
122+
expected: "<test>&</test>",
123+
},
124+
{
125+
name: "empty format with HTML characters",
126+
spec: getTestSpecWithHTMLChars(),
127+
path: "test.yaml",
128+
format: "",
129+
expected: "<test>&</test>",
130+
},
131+
}
132+
133+
for _, tt := range tests {
134+
t.Run(tt.name, func(t *testing.T) {
135+
fs := afero.NewMemMapFs()
136+
err := SaveToFile(tt.path, tt.format, tt.spec, fs)
137+
require.NoError(t, err)
138+
139+
data, err := afero.ReadFile(fs, tt.path)
140+
require.NoError(t, err)
141+
assert.Contains(t, string(data), tt.expected)
142+
})
143+
}
144+
}
145+
146+
func TestSaveToFile_All(t *testing.T) {
147+
fs := afero.NewMemMapFs()
148+
err := SaveToFile("test.yaml", "all", getTestSpecWithHTMLChars(), fs)
149+
require.NoError(t, err)
150+
151+
// read yaml file
152+
data, err := afero.ReadFile(fs, "test.yaml")
153+
require.NoError(t, err)
154+
assert.Contains(t, string(data), "<test>&</test>")
155+
156+
// read json file
157+
data, err = afero.ReadFile(fs, "test.json")
158+
require.NoError(t, err)
159+
assert.Contains(t, string(data), "<test>&</test>")
160+
}
161+
162+
func getTestSpecWithHTMLChars() *Spec {
163+
return &Spec{
164+
Paths: openapi3.NewPaths(
165+
openapi3.WithPath(
166+
"/api/atlas/v2/groups/{groupId}/databaseUsers/{databaseName}/{username}",
167+
&openapi3.PathItem{
168+
Delete: &openapi3.Operation{
169+
Description: "<test>&</test>",
170+
},
171+
},
172+
),
173+
),
174+
}
175+
}

tools/cli/test/e2e/cli/split_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,46 @@ func versionInFuture(t *testing.T, version string) bool {
112112
return v.Date().After(time.Now())
113113
}
114114

115+
func TestSplitVersionsFilteredOASes_All(t *testing.T) {
116+
cliPath := NewBin(t)
117+
env := "dev"
118+
folder := env
119+
base := getInputPath(t, "filtered", "json", folder)
120+
jsonOutputPath := getOutputFolder(t, folder) + "/filtered-dev-output.json"
121+
cmd := exec.Command(cliPath,
122+
"split",
123+
"-s",
124+
base,
125+
"-o",
126+
jsonOutputPath,
127+
"--env",
128+
"dev",
129+
"--format",
130+
"all",
131+
)
132+
133+
var o, e bytes.Buffer
134+
cmd.Stdout = &o
135+
cmd.Stderr = &e
136+
require.NoError(t, cmd.Run(), e.String())
137+
138+
versions := getVersions(t, cliPath, base, folder)
139+
for _, version := range versions {
140+
if slices.Contains(skipVersions, version) {
141+
continue
142+
}
143+
if env == "prod" && !versionInFuture(t, version) {
144+
continue
145+
}
146+
fmt.Printf("Validating version: %s\n", version)
147+
noExtensionOutputPath := strings.Replace(jsonOutputPath, ".json", "", 1)
148+
versionedOutputPath := noExtensionOutputPath + "-" + version + ".json"
149+
ValidateVersionedSpec(t, NewValidAtlasSpecPath(t, version, folder), versionedOutputPath)
150+
versionedOutputPath = noExtensionOutputPath + "-" + version + ".yaml"
151+
ValidateVersionedSpec(t, NewValidAtlasSpecPath(t, version, folder), versionedOutputPath)
152+
}
153+
}
154+
115155
func TestSplitVersionsForOASWithExternalReferences(t *testing.T) {
116156
folder := "dev"
117157
cliPath := NewBin(t)

0 commit comments

Comments
 (0)