From 36582abe9e99e20b33902515f145116eda9d9f6d Mon Sep 17 00:00:00 2001 From: Reuven Date: Wed, 27 Sep 2023 11:37:06 +0300 Subject: [PATCH] read specs from stdin --- checker/checker_deprecation_test.go | 2 +- internal/breaking_changes.go | 4 +-- internal/changelog.go | 2 +- internal/diff.go | 7 ++++- internal/flatten.go | 2 +- internal/summary.go | 2 +- load/load.go | 5 ++++ load/load_test.go | 42 +++++++++++++++++++++++++++++ load/spec_info.go | 6 ----- load/spec_info_test.go | 42 +++++++++++++++++++++++++++++ 10 files changed, 101 insertions(+), 13 deletions(-) create mode 100644 load/spec_info_test.go diff --git a/checker/checker_deprecation_test.go b/checker/checker_deprecation_test.go index e32acd7c..2a265ce2 100644 --- a/checker/checker_deprecation_test.go +++ b/checker/checker_deprecation_test.go @@ -15,7 +15,7 @@ import ( ) func open(file string) (*load.SpecInfo, error) { - return load.LoadSpecInfoFromFile(openapi3.NewLoader(), file) + return load.LoadSpecInfo(openapi3.NewLoader(), file) } func getDeprecationFile(file string) string { diff --git a/internal/breaking_changes.go b/internal/breaking_changes.go index d995bb89..65f0c7b5 100644 --- a/internal/breaking_changes.go +++ b/internal/breaking_changes.go @@ -16,7 +16,7 @@ func getBreakingChangesCmd() *cobra.Command { Use: "breaking base revision [flags]", Short: "Display breaking changes", Long: `Display breaking changes between base and revision specs. -Base and revision can be a path to a file or a URL. +Base and revision can be a path to a file, a URL or '-' to read standard input. In 'composed' mode, base and revision can be a glob and oasdiff will compare matching endpoints between the two sets of files. `, Args: cobra.ExactArgs(2), @@ -58,7 +58,7 @@ In 'composed' mode, base and revision can be a glob and oasdiff will compare mat enumWithOptions(&cmd, newEnumValue([]string{LangEn, LangRu}, LangDefault, &flags.lang), "lang", "l", "language for localized output") cmd.PersistentFlags().StringVarP(&flags.errIgnoreFile, "err-ignore", "", "", "configuration file for ignoring errors") cmd.PersistentFlags().StringVarP(&flags.warnIgnoreFile, "warn-ignore", "", "", "configuration file for ignoring warnings") - enumWithOptions(&cmd, newEnumSliceValue(checker.GetOptionalChecks(), nil, &flags.includeChecks), "include-checks", "i", "comma-separated list of optional checks (run 'oasdiff checks' to see options)") + cmd.PersistentFlags().VarP(newEnumSliceValue(checker.GetOptionalChecks(), nil, &flags.includeChecks), "include-checks", "i", "comma-separated list of optional checks (run 'oasdiff checks' to see options)") cmd.PersistentFlags().IntVarP(&flags.deprecationDaysBeta, "deprecation-days-beta", "", checker.BetaDeprecationDays, "min days required between deprecating a beta resource and removing it") cmd.PersistentFlags().IntVarP(&flags.deprecationDaysStable, "deprecation-days-stable", "", checker.StableDeprecationDays, "min days required between deprecating a stable resource and removing it") diff --git a/internal/changelog.go b/internal/changelog.go index 7c237c6e..fd7f3022 100644 --- a/internal/changelog.go +++ b/internal/changelog.go @@ -18,7 +18,7 @@ func getChangelogCmd() *cobra.Command { Use: "changelog base revision [flags]", Short: "Display changelog", Long: `Display a changelog between base and revision specs. -Base and revision can be a path to a file or a URL. +Base and revision can be a path to a file, a URL or '-' to read standard input. In 'composed' mode, base and revision can be a glob and oasdiff will compare mathcing endpoints between the two sets of files. `, Args: cobra.ExactArgs(2), diff --git a/internal/diff.go b/internal/diff.go index e8eb4b23..64dbfce0 100644 --- a/internal/diff.go +++ b/internal/diff.go @@ -20,7 +20,7 @@ func getDiffCmd() *cobra.Command { Use: "diff base revision [flags]", Short: "Generate a diff report", Long: `Generate a diff report between base and revision specs. -Base and revision can be a path to a file or a URL. +Base and revision can be a path to a file, a URL or '-' to read standard input. In 'composed' mode, base and revision can be a glob and oasdiff will compare matching endpoints between the two sets of files. `, Args: cobra.ExactArgs(2), @@ -131,6 +131,11 @@ func normalDiff(loader load.Loader, flags Flags) (*diff.Diff, *diff.OperationsSo return nil, nil, getErrFailedToLoadSpec("revision", flags.getRevision(), err) } + if flags.getBase() == "-" && flags.getRevision() == "-" { + // io.ReadAll can only read stdin once, so in this edge case, we copy base into revision + s2.Spec = s1.Spec + } + if flags.getFlatten() { if err := mergeAllOf("base", []*load.SpecInfo{s1}); err != nil { return nil, nil, err diff --git a/internal/flatten.go b/internal/flatten.go index bdd575a6..95097027 100644 --- a/internal/flatten.go +++ b/internal/flatten.go @@ -17,7 +17,7 @@ func getFlattenCmd() *cobra.Command { Use: "flatten spec", Short: "Merge allOf", Long: `Display a flattened version of the given OpenAPI spec by merging all instances of allOf. -Spec can be a path to a file or a URL. +Spec can be a path to a file, a URL or '-' to read standard input. `, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { diff --git a/internal/summary.go b/internal/summary.go index 60c1b563..031c7ca2 100644 --- a/internal/summary.go +++ b/internal/summary.go @@ -15,7 +15,7 @@ func getSummaryCmd() *cobra.Command { Use: "summary base revision [flags]", Short: "Generate a diff summary", Long: `Display a summary of changes between base and revision specs. -Base and revision can be a path to a file or a URL. +Base and revision can be a path to a file, a URL or '-' to read standard input. In 'composed' mode, base and revision can be a glob and oasdiff will compare matching endpoints between the two sets of files. `, Args: cobra.ExactArgs(2), diff --git a/load/load.go b/load/load.go index 7f1fe159..e6e00afe 100644 --- a/load/load.go +++ b/load/load.go @@ -10,11 +10,16 @@ import ( type Loader interface { LoadFromURI(*url.URL) (*openapi3.T, error) LoadFromFile(string) (*openapi3.T, error) + LoadFromStdin() (*openapi3.T, error) } // From is a convenience function that opens an OpenAPI spec from a URL or a local path based on the format of the path parameter func From(loader Loader, path string) (*openapi3.T, error) { + if path == "-" { + return loader.LoadFromStdin() + } + uri, err := url.ParseRequestURI(path) if err == nil { return loadFromURI(loader, uri) diff --git a/load/load_test.go b/load/load_test.go index a72b8d60..10a06b6f 100644 --- a/load/load_test.go +++ b/load/load_test.go @@ -1,7 +1,9 @@ package load_test import ( + "log" "net/url" + "os" "testing" "github.com/getkin/kin-openapi/openapi3" @@ -19,6 +21,10 @@ func (mockLoader MockLoader) LoadFromURI(location *url.URL) (*openapi3.T, error) return openapi3.NewLoader().LoadFromFile(RelativeDataPath + location.Path) } +func (mockLoader MockLoader) LoadFromStdin() (*openapi3.T, error) { + return openapi3.NewLoader().LoadFromStdin() +} + type MockLoader struct{} func TestLoad_File(t *testing.T) { @@ -35,3 +41,39 @@ func TestLoad_URIError(t *testing.T) { _, err := load.From(MockLoader{}, "http://localhost/null") require.Error(t, err) } + +func TestLoad_Stdin(t *testing.T) { + content := []byte(`openapi: 3.0.1 +info: + title: Test API + version: v1 +paths: + /partner-api/test/some-method: + get: + responses: + "200": + description: Success +`) + + tmpfile, err := os.CreateTemp("", "example") + if err != nil { + log.Fatal(err) + } + + defer os.Remove(tmpfile.Name()) // clean up + + if _, err := tmpfile.Write(content); err != nil { + log.Fatal(err) + } + + if _, err := tmpfile.Seek(0, 0); err != nil { + log.Fatal(err) + } + + oldStdin := os.Stdin + defer func() { os.Stdin = oldStdin }() // Restore original Stdin + + os.Stdin = tmpfile + _, err = load.From(MockLoader{}, "-") + require.NoError(t, err) +} diff --git a/load/spec_info.go b/load/spec_info.go index 072ec001..c8609090 100644 --- a/load/spec_info.go +++ b/load/spec_info.go @@ -13,12 +13,6 @@ type SpecInfo struct { Spec *openapi3.T } -// LoadSpecInfoFromFile creates a SpecInfo from a local file path -func LoadSpecInfoFromFile(loader Loader, location string) (*SpecInfo, error) { - s, err := loader.LoadFromFile(location) - return &SpecInfo{Spec: s, Url: location}, err -} - // LoadSpecInfo creates a SpecInfo from a local file path or a URL func LoadSpecInfo(loader Loader, location string) (*SpecInfo, error) { s, err := From(loader, location) diff --git a/load/spec_info_test.go b/load/spec_info_test.go new file mode 100644 index 00000000..090627d9 --- /dev/null +++ b/load/spec_info_test.go @@ -0,0 +1,42 @@ +package load_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/tufin/oasdiff/load" +) + +func TestLoadSpecInfo(t *testing.T) { + _, err := load.LoadSpecInfo(MockLoader{}, "openapi-test1.yaml") + require.NoError(t, err) +} + +func TestLoadGlob_OK(t *testing.T) { + _, err := load.FromGlob(MockLoader{}, RelativeDataPath+"*.yaml") + require.NoError(t, err) +} + +func TestLoadGlob_InvalidSpec(t *testing.T) { + _, err := load.FromGlob(MockLoader{}, RelativeDataPath+"ignore-err-example.txt") + require.Error(t, err) + require.Equal(t, "error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go value of type openapi3.TBis", err.Error()) +} + +func TestLoadGlob_Invalid(t *testing.T) { + _, err := load.FromGlob(MockLoader{}, "[*") + require.Error(t, err) + require.Equal(t, "syntax error in pattern", err.Error()) +} + +func TestLoadGlob_URL(t *testing.T) { + _, err := load.FromGlob(MockLoader{}, "http://localhost/openapi-test1.yaml") + require.Error(t, err) + require.Equal(t, "no matching files (should be a glob, not a URL)", err.Error()) +} + +func TestLoadGlob_NoFiles(t *testing.T) { + _, err := load.FromGlob(MockLoader{}, RelativeDataPath+"*.xxx") + require.Error(t, err) + require.Equal(t, "no matching files", err.Error()) +}