147 lines
4.2 KiB
Go
147 lines
4.2 KiB
Go
package sdk
|
|
|
|
import (
|
|
"dappco.re/go/core"
|
|
coreerr "dappco.re/go/core/log"
|
|
"github.com/oasdiff/kin-openapi/openapi3"
|
|
"github.com/oasdiff/oasdiff/checker"
|
|
"github.com/oasdiff/oasdiff/diff"
|
|
"github.com/oasdiff/oasdiff/load"
|
|
)
|
|
|
|
// DiffResult holds the result of comparing two OpenAPI specs.
|
|
//
|
|
// result, err := sdk.Diff("docs/openapi.v1.yaml", "docs/openapi.yaml")
|
|
type DiffResult struct {
|
|
// Breaking is true if breaking changes were detected.
|
|
Breaking bool
|
|
// Changes is the list of breaking changes.
|
|
Changes []string
|
|
// HasWarnings is true if warning-level changes were detected.
|
|
HasWarnings bool
|
|
// Warnings is the list of warning-level changes.
|
|
Warnings []string
|
|
// Summary is a human-readable summary.
|
|
Summary string
|
|
}
|
|
|
|
// DiffOptions controls the change levels included in the diff result.
|
|
type DiffOptions struct {
|
|
// MinimumLevel selects the lowest severity to include.
|
|
// Defaults to checker.ERR to preserve breaking-only behaviour.
|
|
MinimumLevel checker.Level
|
|
}
|
|
|
|
// Diff compares two OpenAPI specs and detects breaking changes.
|
|
//
|
|
// result, err := sdk.Diff("docs/openapi.v1.yaml", "docs/openapi.yaml")
|
|
func Diff(basePath, revisionPath string) (*DiffResult, error) {
|
|
return DiffWithOptions(basePath, revisionPath, DiffOptions{MinimumLevel: checker.ERR})
|
|
}
|
|
|
|
// DiffWithOptions compares two OpenAPI specs and includes changes at or above
|
|
// the requested severity level.
|
|
func DiffWithOptions(basePath, revisionPath string, opts DiffOptions) (*DiffResult, error) {
|
|
loader := openapi3.NewLoader()
|
|
loader.IsExternalRefsAllowed = true
|
|
|
|
// Load specs
|
|
baseSpec, err := load.NewSpecInfo(loader, load.NewSource(basePath))
|
|
if err != nil {
|
|
return nil, coreerr.E("sdk.Diff", "failed to load base spec", err)
|
|
}
|
|
|
|
revSpec, err := load.NewSpecInfo(loader, load.NewSource(revisionPath))
|
|
if err != nil {
|
|
return nil, coreerr.E("sdk.Diff", "failed to load revision spec", err)
|
|
}
|
|
|
|
// Compute diff with operations sources map for better error reporting
|
|
diffResult, operationsSources, err := diff.GetWithOperationsSourcesMap(diff.NewConfig(), baseSpec, revSpec)
|
|
if err != nil {
|
|
return nil, coreerr.E("sdk.Diff", "failed to compute diff", err)
|
|
}
|
|
|
|
// Check for breaking changes
|
|
config := checker.NewConfig(checker.GetAllChecks())
|
|
changes := checker.CheckBackwardCompatibilityUntilLevel(
|
|
config,
|
|
diffResult,
|
|
operationsSources,
|
|
resolveDiffLevel(opts.MinimumLevel),
|
|
)
|
|
|
|
// Build result
|
|
result := &DiffResult{
|
|
Breaking: len(changes) > 0 && changes.HasLevelOrHigher(checker.ERR),
|
|
Changes: make([]string, 0, len(changes)),
|
|
Warnings: make([]string, 0, len(changes)),
|
|
}
|
|
|
|
localizer := checker.NewDefaultLocalizer()
|
|
for _, change := range changes {
|
|
// GetUncolorizedText uses US spelling — upstream oasdiff API.
|
|
text := change.GetUncolorizedText(localizer)
|
|
switch change.GetLevel() {
|
|
case checker.ERR:
|
|
result.Changes = append(result.Changes, text)
|
|
case checker.WARN:
|
|
result.HasWarnings = true
|
|
result.Warnings = append(result.Warnings, text)
|
|
}
|
|
}
|
|
|
|
result.Summary = diffSummary(result, resolveDiffLevel(opts.MinimumLevel))
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// DiffExitCode returns the exit code for CI integration.
|
|
// 0 = no breaking changes, 1 = breaking changes, 2 = error.
|
|
//
|
|
// os.Exit(sdk.DiffExitCode(sdk.Diff("old.yaml", "new.yaml")))
|
|
func DiffExitCode(result *DiffResult, err error) int {
|
|
if err != nil {
|
|
return 2
|
|
}
|
|
if result == nil {
|
|
return 2
|
|
}
|
|
if result.Breaking {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func resolveDiffLevel(level checker.Level) checker.Level {
|
|
switch level {
|
|
case checker.WARN, checker.INFO, checker.ERR:
|
|
return level
|
|
default:
|
|
return checker.ERR
|
|
}
|
|
}
|
|
|
|
func diffSummary(result *DiffResult, level checker.Level) string {
|
|
if result == nil {
|
|
return "No breaking changes"
|
|
}
|
|
|
|
if level == checker.ERR {
|
|
if result.Breaking {
|
|
return core.Sprintf("%d breaking change(s) detected", len(result.Changes))
|
|
}
|
|
return "No breaking changes"
|
|
}
|
|
|
|
switch {
|
|
case result.Breaking && result.HasWarnings:
|
|
return core.Sprintf("%d breaking change(s), %d warning(s) detected", len(result.Changes), len(result.Warnings))
|
|
case result.Breaking:
|
|
return core.Sprintf("%d breaking change(s) detected", len(result.Changes))
|
|
case result.HasWarnings:
|
|
return core.Sprintf("%d warning(s) detected", len(result.Warnings))
|
|
default:
|
|
return "No warnings or breaking changes"
|
|
}
|
|
}
|