Skip to content

Commit

Permalink
add attributes (#587)
Browse files Browse the repository at this point in the history
  • Loading branch information
reuvenharrison authored Jul 21, 2024
1 parent d2e44ea commit ca3ab5d
Show file tree
Hide file tree
Showing 20 changed files with 240 additions and 25 deletions.
20 changes: 20 additions & 0 deletions checker/api_change.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (

// ApiChange represnts a change in the Paths Section of an OpenAPI spec
type ApiChange struct {
CommonChange

Id string
Args []any
Comment string
Expand All @@ -38,7 +40,25 @@ func NewApiChange(id string, config *Config, args []any, comment string, operati
Operation: method,
Path: path,
Source: load.NewSource((*operationsSources)[operation]),
CommonChange: CommonChange{
Attributes: getAttributes(config, operation),
},
}
}

func getAttributes(config *Config, operation *openapi3.Operation) map[string]any {
result := map[string]any{}
for _, tag := range config.Attributes {
if val, ok := operation.Extensions[tag]; ok {
result[tag] = val
}
}

if len(result) == 0 {
return nil
}

return result
}

func (c ApiChange) GetSection() string {
Expand Down
76 changes: 76 additions & 0 deletions checker/attributes_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package checker_test

import (
"testing"

"github.com/stretchr/testify/require"
"github.com/tufin/oasdiff/checker"
"github.com/tufin/oasdiff/diff"
)

func TestBreaking_Attributes(t *testing.T) {
s1, err := open("../data/attributes/base.yaml")
require.NoError(t, err)

s2, err := open("../data/attributes/revision.yaml")
require.NoError(t, err)

d, osm, err := diff.GetWithOperationsSourcesMap(diff.NewConfig(), s1, s2)
require.NoError(t, err)
errs := checker.CheckBackwardCompatibility(allChecksConfig().WithAttributes([]string{"x-test"}), d, osm)
require.NotEmpty(t, errs)
require.Len(t, errs, 2)

require.Equal(t, map[string]any{"x-test": []any{"xyz", float64(456)}}, errs[0].GetAttributes())
require.Equal(t, map[string]any{"x-test": "abc"}, errs[1].GetAttributes())
}

func TestBreaking_AttributesNone(t *testing.T) {
s1, err := open("../data/attributes/base.yaml")
require.NoError(t, err)

s2, err := open("../data/attributes/revision.yaml")
require.NoError(t, err)

d, osm, err := diff.GetWithOperationsSourcesMap(diff.NewConfig(), s1, s2)
require.NoError(t, err)
errs := checker.CheckBackwardCompatibility(allChecksConfig().WithAttributes([]string{"x-other"}), d, osm)
require.NotEmpty(t, errs)
require.Len(t, errs, 2)

require.Empty(t, errs[0].GetAttributes())
require.Empty(t, errs[1].GetAttributes())
}

func TestBreaking_AttributesReverse(t *testing.T) {
s1, err := open("../data/attributes/revision.yaml")
require.NoError(t, err)

s2, err := open("../data/attributes/base.yaml")
require.NoError(t, err)

d, osm, err := diff.GetWithOperationsSourcesMap(diff.NewConfig(), s1, s2)
require.NoError(t, err)
errs := checker.CheckBackwardCompatibility(allChecksConfig().WithAttributes([]string{"x-test"}), d, osm)
require.NotEmpty(t, errs)
require.Len(t, errs, 2)

require.Equal(t, map[string]any{"x-test": []any{float64(123), float64(456)}}, errs[0].GetAttributes())
require.Equal(t, map[string]any{"x-test": float64(123)}, errs[1].GetAttributes())
}

func TestBreaking_AttributesTwo(t *testing.T) {
s1, err := open("../data/attributes/base.yaml")
require.NoError(t, err)

s2, err := open("../data/attributes/revision.yaml")
require.NoError(t, err)

d, osm, err := diff.GetWithOperationsSourcesMap(diff.NewConfig(), s1, s2)
require.NoError(t, err)
errs := checker.CheckBackwardCompatibility(allChecksConfig().WithAttributes([]string{"x-test", "x-test2"}), d, osm)
require.Len(t, errs, 2)

require.Equal(t, map[string]any{"x-test": []any{"xyz", float64(456)}}, errs[0].GetAttributes())
require.Equal(t, map[string]any{"x-test": "abc", "x-test2": "def"}, errs[1].GetAttributes())
}
9 changes: 9 additions & 0 deletions checker/change.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type Change interface {
GetOperationId() string
GetPath() string
GetSource() string
GetAttributes() map[string]any
GetSourceFile() string
GetSourceLine() int
GetSourceLineEnd() int
Expand All @@ -22,3 +23,11 @@ type Change interface {
SingleLineError(l Localizer, colorMode ColorMode) string
MultiLineError(l Localizer, colorMode ColorMode) string
}

type CommonChange struct {
Attributes map[string]any
}

func (c CommonChange) GetAttributes() map[string]any {
return c.Attributes
}
2 changes: 2 additions & 0 deletions checker/component_change.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (

// ComponentChange represnts a change in the Components Section: https://swagger.io/docs/specification/components/
type ComponentChange struct {
CommonChange

Id string
Args []any
Comment string
Expand Down
7 changes: 7 additions & 0 deletions checker/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ type Config struct {
MinSunsetBetaDays uint
MinSunsetStableDays uint
LogLevels map[string]Level
Attributes []string
}

const (
Expand Down Expand Up @@ -63,6 +64,12 @@ func (config *Config) WithChecks(checks BackwardCompatibilityChecks) *Config {
return config
}

// WithAttributes sets a list of attributes to be used.
func (config *Config) WithAttributes(attributes []string) *Config {
config.Attributes = attributes
return config
}

func (config *Config) getLogLevel(checkId string) Level {
level, ok := config.LogLevels[checkId]

Expand Down
2 changes: 2 additions & 0 deletions checker/security_change.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (

// SecurityChange represents a change in the Security Section (not to be confised with components/securitySchemes)
type SecurityChange struct {
CommonChange

Id string
Args []any
Comment string
Expand Down
23 changes: 23 additions & 0 deletions data/attributes/base.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
openapi: 3.0.1
info:
title: Test API
version: v1
paths:
/partner-api/test/some-method:
get:
x-test: 123
tags:
- Test
responses:
"200":
description: Success
/partner-api/test/another-method:
get:
x-test:
- 123
- 456
tags:
- Test
responses:
"200":
description: Success
24 changes: 24 additions & 0 deletions data/attributes/revision.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
openapi: 3.0.1
info:
title: Test API
version: v1
paths:
/partner-api/test/some-method:
get:
x-test: "abc"
x-test2: "def"
tags:
- Test
responses:
"201":
description: Success
/partner-api/test/another-method:
get:
x-test:
- "xyz"
- 456
tags:
- Test
responses:
"201":
description: Success
29 changes: 29 additions & 0 deletions docs/ATTRIBUTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
## Adding custom attributes to changelog entries basing on OpenAPI extension tags
Some people annotate their endpoints with OpenAPI Extension tags, for example:
```
/restapi/oauth/token:
post:
operationId: getToken
x-audience: Public
summary: ...
requestBody:
...
responses:
...
```

Oasdiff can add these attributes to the changelog in JSON or YAML formats as follows:

```
❯ oasdiff changelog base.yaml revision.yaml -f yaml --attributes x-audience
- id: new-optional-request-property
text: added the new optional request property ivr_pin
level: 1
operation: POST
operationId: getToken
path: /restapi/oauth/token
source: new-revision.yaml
section: paths
attributes:
x-audience: Public
```
18 changes: 8 additions & 10 deletions docs/BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ This method allows adding new entries to enums used in responses which is very u
In most cases the `x-extensible-enum` is similar to enum values, except it allows adding new entries in messages sent to the client (responses or callbacks).
If you don't use the `x-extensible-enum` in your OpenAPI specifications, nothing changes for you, but if you do, oasdiff will identify breaking changes related to `x-extensible-enum` parameters and properties.

### Localization
To display changes in other languages, use the `--lang` flag.
Currently English and Russian are supported.
[Please improve oasdiff by adding your own language](https://github.com/Tufin/oasdiff/issues/383).

### Customizing Severity Levels
Oasdiff allows you to change the default severity levels according to your needs.
For example, the default severity level of the `api-security-removed` check is `INFO`. You can verify this by running `oasdiff checks`.
Expand All @@ -113,17 +118,9 @@ Where the file `oasdiff-levels.txt` contains a single line:
api-security-removed err
```

[Here are some examples of breaking and non-breaking changes that oasdiff supports](BREAKING-CHANGES-EXAMPLES.md).
This document is automatically generated from oasdiff unit tests.

### Localization
To display changes in other languages, use the `--lang` flag.
Currently English and Russian are supported.
[Please improve oasdiff by adding your own language](https://github.com/Tufin/oasdiff/issues/383).

### Customizing Breaking Changes Checks
If you encounter a change that isn't considered breaking by oasdiff you may:
1. Check if the change is already available as an [optional check](#optional-checks).
If you encounter a change that isn't reported, you may:
1. Run `oasdiff changelog` to see if the check is available as an info-level check, and [customize the level as needed](#customizing-breaking-changes-checks).
2. Add a [custom check](CUSTOMIZING-CHECKS.md)

### Additional Options
Expand All @@ -133,6 +130,7 @@ If you encounter a change that isn't considered breaking by oasdiff you may:
- [Path parameter renaming](PATH-PARAM-RENAME.md)
- [Case-insensitive header comparison](HEADER-DIFF.md)
- [Comparing multiple specs](COMPOSED.md)
- [Adding OpenAPI Extensions to the changelog output](ATTRIBUTES.md)
- [Running from docker](DOCKER.md)
- [Embedding in your go program](GO.md)

Expand Down
22 changes: 12 additions & 10 deletions formatters/changes.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@ import (
)

type Change struct {
Id string `json:"id,omitempty" yaml:"id,omitempty"`
Text string `json:"text,omitempty" yaml:"text,omitempty"`
Comment string `json:"comment,omitempty" yaml:"comment,omitempty"`
Level checker.Level `json:"level" yaml:"level"`
Operation string `json:"operation,omitempty" yaml:"operation,omitempty"`
OperationId string `json:"operationId,omitempty" yaml:"operationId,omitempty"`
Path string `json:"path,omitempty" yaml:"path,omitempty"`
Source string `json:"source,omitempty" yaml:"source,omitempty"`
Section string `json:"section,omitempty" yaml:"section,omitempty"`
IsBreaking bool `json:"-" yaml:"-"`
Id string `json:"id,omitempty" yaml:"id,omitempty"`
Text string `json:"text,omitempty" yaml:"text,omitempty"`
Comment string `json:"comment,omitempty" yaml:"comment,omitempty"`
Level checker.Level `json:"level" yaml:"level"`
Operation string `json:"operation,omitempty" yaml:"operation,omitempty"`
OperationId string `json:"operationId,omitempty" yaml:"operationId,omitempty"`
Path string `json:"path,omitempty" yaml:"path,omitempty"`
Source string `json:"source,omitempty" yaml:"source,omitempty"`
Section string `json:"section,omitempty" yaml:"section,omitempty"`
IsBreaking bool `json:"-" yaml:"-"`
Attributes map[string]any `json:"attributes,omitempty" yaml:"attributes,omitempty"`
}

type Changes []Change
Expand All @@ -32,6 +33,7 @@ func NewChanges(originalChanges checker.Changes, l checker.Localizer) Changes {
OperationId: change.GetOperationId(),
Path: change.GetPath(),
Source: change.GetSource(),
Attributes: change.GetAttributes(),
}
}
return changes
Expand Down
2 changes: 1 addition & 1 deletion internal/changelog.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func getChangelog(flags Flags, stdout io.Writer, level checker.Level) (bool, *Re

errs, returnErr := filterIgnored(
checker.CheckBackwardCompatibilityUntilLevel(
checker.NewConfig(checker.GetAllChecks()).WithOptionalChecks(flags.getIncludeChecks()).WithSeverityLevels(severityLevels).WithDeprecation(flags.getDeprecationDaysBeta(), flags.getDeprecationDaysStable()),
checker.NewConfig(checker.GetAllChecks()).WithOptionalChecks(flags.getIncludeChecks()).WithSeverityLevels(severityLevels).WithDeprecation(flags.getDeprecationDaysBeta(), flags.getDeprecationDaysStable()).WithAttributes(flags.getAttributes()),
diffResult.diffReport,
diffResult.operationsSources,
level),
Expand Down
2 changes: 2 additions & 0 deletions internal/changelog_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
)

type ChangelogFlags struct {
CommonFlags

base *load.Source
revision *load.Source
composed bool
Expand Down
3 changes: 2 additions & 1 deletion internal/cmd_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,12 @@ func addCommonBreakingFlags(cmd *cobra.Command, flags Flags) {
enumWithOptions(cmd, newEnumValue(localizations.GetSupportedLanguages(), localizations.LangDefault, flags.refLang()), "lang", "l", "language for localized output")
cmd.PersistentFlags().StringVarP(flags.refErrIgnoreFile(), "err-ignore", "", "", "configuration file for ignoring errors")
cmd.PersistentFlags().StringVarP(flags.refWarnIgnoreFile(), "warn-ignore", "", "", "configuration file for ignoring warnings")
cmd.PersistentFlags().VarP(newEnumSliceValue(checker.GetOptionalRuleIds(), nil, flags.refIncludeChecks()), "include-checks", "i", "comma-separated list of optional checks")
cmd.PersistentFlags().VarP(newEnumSliceValue(checker.GetOptionalRuleIds(), nil, flags.refIncludeChecks()), "include-checks", "i", "optional checks")
hideFlag(cmd, "include-checks")
cmd.PersistentFlags().UintVarP(flags.refDeprecationDaysBeta(), "deprecation-days-beta", "", checker.DefaultBetaDeprecationDays, "min days required between deprecating a beta resource and removing it")
cmd.PersistentFlags().UintVarP(flags.refDeprecationDaysStable(), "deprecation-days-stable", "", checker.DefaultStableDeprecationDays, "min days required between deprecating a stable resource and removing it")
enumWithOptions(cmd, newEnumValue([]string{"auto", "always", "never"}, "auto", flags.refColor()), "color", "", "when to colorize textual output")
enumWithOptions(cmd, newEnumValue(formatters.SupportedFormatsByContentType(formatters.OutputChangelog), string(formatters.FormatText), flags.refFormat()), "format", "f", "output format")
cmd.PersistentFlags().StringVarP(flags.refSeverityLevelsFile(), "severity-levels", "", "", "configuration file for custom severity levels")
cmd.PersistentFlags().StringSliceVarP(flags.refAttributes(), "attributes", "", nil, "OpenAPI Extensions to include in json or yaml output")
}
2 changes: 1 addition & 1 deletion internal/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func getDiffCmd() *cobra.Command {
}

addCommonDiffFlags(&cmd, &flags)
enumWithOptions(&cmd, newEnumSliceValue(diff.ExcludeDiffOptions, nil, flags.refExcludeElements()), "exclude-elements", "e", "comma-separated list of elements to exclude")
enumWithOptions(&cmd, newEnumSliceValue(diff.ExcludeDiffOptions, nil, flags.refExcludeElements()), "exclude-elements", "e", "elements to exclude")
enumWithOptions(&cmd, newEnumValue(formatters.SupportedFormatsByContentType(formatters.OutputDiff), string(formatters.FormatYAML), &flags.format), "format", "f", "output format")
cmd.PersistentFlags().BoolVarP(&flags.failOnDiff, "fail-on-diff", "o", false, "exit with return code 1 when any change is found")

Expand Down
2 changes: 2 additions & 0 deletions internal/diff_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
)

type DiffFlags struct {
CommonFlags

base *load.Source
revision *load.Source
composed bool
Expand Down
2 changes: 1 addition & 1 deletion internal/enum_slice.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ func (s *enumSliceValue) Set(val string) error {
}

func (s *enumSliceValue) Type() string {
return "csv"
return "strings"
}

func (s *enumSliceValue) String() string {
Expand Down
Loading

0 comments on commit ca3ab5d

Please sign in to comment.