differ

package
v1.9.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 21, 2025 License: MIT Imports: 6 Imported by: 0

Documentation

Overview

Package differ provides OpenAPI specification comparison and breaking change detection.

Overview

The differ package enables comparison of OpenAPI specifications to identify differences, categorize changes, and detect breaking API changes. It supports both OAS 2.0 and OAS 3.x documents.

Usage

The package provides two API styles:

  1. Package-level convenience functions for simple, one-off operations
  2. Struct-based API for reusable instances with custom configuration

Diff Modes

The differ supports two operational modes:

  • ModeSimple: Reports all semantic differences without categorization
  • ModeBreaking: Categorizes changes by severity and identifies breaking changes

Change Categories

Changes are categorized by the part of the specification that changed:

  • CategoryEndpoint: Path/endpoint changes
  • CategoryOperation: HTTP operation changes
  • CategoryParameter: Parameter changes
  • CategoryRequestBody: Request body changes
  • CategoryResponse: Response changes
  • CategorySchema: Schema/definition changes
  • CategorySecurity: Security scheme changes
  • CategoryServer: Server/host changes
  • CategoryInfo: Metadata changes

Severity Levels

In ModeBreaking, changes are assigned severity levels:

  • SeverityCritical: Critical breaking changes (removed endpoints, operations)
  • SeverityError: Breaking changes (removed required parameters, type changes)
  • SeverityWarning: Potentially problematic changes (deprecated operations, new required fields)
  • SeverityInfo: Non-breaking changes (additions, relaxed constraints)

Example (Simple Diff)

package main

import (
	"fmt"
	"log"

	"github.com/erraggy/oastools/differ"
)

func main() {
	// Simple diff using convenience function
	result, err := differ.Diff("api-v1.yaml", "api-v2.yaml")
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Found %d changes\n", len(result.Changes))
	for _, change := range result.Changes {
		fmt.Println(change.String())
	}
}

Example (Breaking Change Detection)

package main

import (
	"fmt"
	"log"

	"github.com/erraggy/oastools/differ"
)

func main() {
	// Create differ with breaking mode
	d := differ.New()
	d.Mode = differ.ModeBreaking
	d.IncludeInfo = true

	result, err := d.Diff("api-v1.yaml", "api-v2.yaml")
	if err != nil {
		log.Fatal(err)
	}

	if result.HasBreakingChanges {
		fmt.Printf("⚠️  Found %d breaking change(s)!\n", result.BreakingCount)
	}

	fmt.Printf("Summary: %d breaking, %d warnings, %d info\n",
		result.BreakingCount, result.WarningCount, result.InfoCount)

	// Print changes grouped by severity
	for _, change := range result.Changes {
		fmt.Println(change.String())
	}
}

Example (Reusable Differ Instance)

package main

import (
	"fmt"
	"log"

	"github.com/erraggy/oastools/differ"
)

func main() {
	// Create a reusable differ instance
	d := differ.New()
	d.Mode = differ.ModeBreaking
	d.IncludeInfo = false // Skip informational changes

	// Compare multiple spec pairs with same configuration
	pairs := []struct{ old, new string }{
		{"api-v1.yaml", "api-v2.yaml"},
		{"api-v2.yaml", "api-v3.yaml"},
		{"api-v3.yaml", "api-v4.yaml"},
	}

	for _, pair := range pairs {
		result, err := d.Diff(pair.old, pair.new)
		if err != nil {
			log.Printf("Error comparing %s to %s: %v", pair.old, pair.new, err)
			continue
		}

		fmt.Printf("\n%s → %s:\n", pair.old, pair.new)
		if result.HasBreakingChanges {
			fmt.Printf("  ⚠️  %d breaking changes\n", result.BreakingCount)
		} else {
			fmt.Println("  ✓ No breaking changes")
		}
	}
}

Working with Parsed Documents

For efficiency when documents are already parsed, use DiffParsed:

package main

import (
	"fmt"
	"log"

	"github.com/erraggy/oastools/differ"
	"github.com/erraggy/oastools/parser"
)

func main() {
	// Parse documents once
	source, err := parser.Parse("api-v1.yaml", false, true)
	if err != nil {
		log.Fatal(err)
	}

	target, err := parser.Parse("api-v2.yaml", false, true)
	if err != nil {
		log.Fatal(err)
	}

	// Compare parsed documents
	result, err := differ.DiffParsed(*source, *target)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Found %d changes\n", len(result.Changes))
}

Change Analysis

The Change struct provides detailed information about each difference:

for _, change := range result.Changes {
	fmt.Printf("Path: %s\n", change.Path)
	fmt.Printf("Type: %s\n", change.Type)
	fmt.Printf("Category: %s\n", change.Category)
	fmt.Printf("Severity: %s\n", change.Severity)
	fmt.Printf("Message: %s\n", change.Message)

	if change.OldValue != nil {
		fmt.Printf("Old value: %v\n", change.OldValue)
	}
	if change.NewValue != nil {
		fmt.Printf("New value: %v\n", change.NewValue)
	}
}

Breaking Change Examples

Common breaking changes detected in ModeBreaking:

  • Removed endpoints or operations (SeverityCritical)
  • Removed required parameters (SeverityCritical)
  • Changed parameter types (SeverityError)
  • Made optional parameters required (SeverityError)
  • Removed enum values (SeverityError)
  • Removed success response codes (SeverityError)
  • Removed schemas (SeverityError)
  • Changed authentication requirements (SeverityError)

Non-Breaking Change Examples

Common non-breaking changes in ModeBreaking:

  • Added endpoints or operations (SeverityInfo)
  • Added optional parameters (SeverityInfo)
  • Made required parameters optional (SeverityInfo)
  • Added enum values (SeverityInfo)
  • Added response codes (SeverityInfo)
  • Documentation updates (SeverityInfo)

Version Compatibility

The differ works with:

  • OAS 2.0 (Swagger) documents
  • OAS 3.0.x documents
  • OAS 3.1.x documents
  • OAS 3.2.x documents
  • Cross-version comparisons (with limitations)

When comparing documents of different OAS versions (e.g., 2.0 vs 3.0), the diff is limited to common elements present in both versions.

Example

Example demonstrates basic diff usage with the convenience function

package main

import (
	"fmt"
	"log"

	"github.com/erraggy/oastools/differ"
)

func main() {
	// Compare two OpenAPI specifications
	result, err := differ.Diff("../testdata/petstore-v1.yaml", "../testdata/petstore-v2.yaml")
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Found %d changes\n", len(result.Changes))
	fmt.Printf("Source version: %s\n", result.SourceVersion)
	fmt.Printf("Target version: %s\n", result.TargetVersion)
}
Example (Breaking)

Example_breaking demonstrates breaking change detection

package main

import (
	"fmt"
	"log"

	"github.com/erraggy/oastools/differ"
)

func main() {
	d := differ.New()
	d.Mode = differ.ModeBreaking
	d.IncludeInfo = true

	result, err := d.Diff("../testdata/petstore-v1.yaml", "../testdata/petstore-v2.yaml")
	if err != nil {
		log.Fatal(err)
	}

	if result.HasBreakingChanges {
		fmt.Printf("⚠️  Found %d breaking change(s)\n", result.BreakingCount)
	} else {
		fmt.Println("✓ No breaking changes detected")
	}

	fmt.Printf("Summary: %d breaking, %d warnings, %d info\n",
		result.BreakingCount, result.WarningCount, result.InfoCount)
}
Example (ChangeAnalysis)

Example_changeAnalysis demonstrates detailed change analysis

package main

import (
	"fmt"
	"log"

	"github.com/erraggy/oastools/differ"
)

func main() {
	d := differ.New()
	d.Mode = differ.ModeBreaking

	result, err := d.Diff("../testdata/petstore-v1.yaml", "../testdata/petstore-v2.yaml")
	if err != nil {
		log.Fatal(err)
	}

	// Group changes by category
	categories := make(map[differ.ChangeCategory]int)
	for _, change := range result.Changes {
		categories[change.Category]++
	}

	fmt.Println("Changes by category:")
	for category, count := range categories {
		fmt.Printf("  %s: %d\n", category, count)
	}
}
Example (FilterBySeverity)

Example_filterBySeverity demonstrates filtering changes by severity

package main

import (
	"fmt"
	"log"

	"github.com/erraggy/oastools/differ"
)

func main() {
	d := differ.New()
	d.Mode = differ.ModeBreaking
	d.IncludeInfo = false // Exclude info-level changes

	result, err := d.Diff("../testdata/petstore-v1.yaml", "../testdata/petstore-v2.yaml")
	if err != nil {
		log.Fatal(err)
	}

	// Only breaking changes and warnings remain
	fmt.Printf("Breaking and warnings only: %d changes\n", len(result.Changes))
	fmt.Printf("Breaking: %d, Warnings: %d\n", result.BreakingCount, result.WarningCount)
}
Example (Parsed)

Example_parsed demonstrates comparing already-parsed documents

package main

import (
	"fmt"
	"log"

	"github.com/erraggy/oastools/differ"
	"github.com/erraggy/oastools/parser"
)

func main() {
	// Parse documents once
	source, err := parser.Parse("../testdata/petstore-v1.yaml", false, true)
	if err != nil {
		log.Fatal(err)
	}

	target, err := parser.Parse("../testdata/petstore-v2.yaml", false, true)
	if err != nil {
		log.Fatal(err)
	}

	// Compare parsed documents
	result, err := differ.DiffParsed(*source, *target)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Found %d changes between %s and %s\n",
		len(result.Changes), result.SourceVersion, result.TargetVersion)
}
Example (ReusableDiffer)

Example_reusableDiffer demonstrates creating a reusable differ instance

package main

import (
	"fmt"
	"log"

	"github.com/erraggy/oastools/differ"
)

func main() {
	// Create a reusable differ with specific configuration
	d := differ.New()
	d.Mode = differ.ModeBreaking
	d.IncludeInfo = false
	d.UserAgent = "my-api-tool/1.0"

	// Use the same differ for multiple comparisons
	specs := []struct{ old, new string }{
		{"../testdata/petstore-v1.yaml", "../testdata/petstore-v2.yaml"},
	}

	for _, spec := range specs {
		result, err := d.Diff(spec.old, spec.new)
		if err != nil {
			log.Printf("Error: %v", err)
			continue
		}

		fmt.Printf("%s → %s: ", spec.old, spec.new)
		if result.HasBreakingChanges {
			fmt.Printf("%d breaking\n", result.BreakingCount)
		} else {
			fmt.Println("compatible")
		}
	}
}
Example (Simple)

Example_simple demonstrates simple diff mode

package main

import (
	"fmt"
	"log"

	"github.com/erraggy/oastools/differ"
)

func main() {
	d := differ.New()
	d.Mode = differ.ModeSimple

	result, err := d.Diff("../testdata/petstore-v1.yaml", "../testdata/petstore-v2.yaml")
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Simple diff found %d changes\n", len(result.Changes))

	// Print first few changes
	for i, change := range result.Changes {
		if i >= 3 {
			break
		}
		fmt.Println(change.String())
	}
}

Index

Examples

Constants

View Source
const (
	// SeverityInfo indicates informational changes (additions, relaxed constraints)
	SeverityInfo = severity.SeverityInfo
	// SeverityWarning indicates potentially problematic changes
	SeverityWarning = severity.SeverityWarning
	// SeverityError indicates breaking changes (removed features, stricter constraints)
	SeverityError = severity.SeverityError
	// SeverityCritical indicates critical breaking changes (removed endpoints, operations)
	SeverityCritical = severity.SeverityCritical
)

Variables

This section is empty.

Functions

This section is empty.

Types

type Change

type Change struct {
	// Path is the JSON path to the changed element (e.g., "paths./pets.get")
	Path string
	// Type indicates if this is an addition, removal, or modification
	Type ChangeType
	// Category indicates which part of the spec was changed
	Category ChangeCategory
	// Severity indicates the impact level (only used in ModeBreaking)
	Severity Severity
	// OldValue is the value in the source document (nil for additions)
	OldValue any
	// NewValue is the value in the target document (nil for removals)
	NewValue any
	// Message is a human-readable description of the change
	Message string
}

Change represents a single difference between two OpenAPI specifications

func (Change) String

func (c Change) String() string

String returns a formatted string representation of the change

type ChangeCategory

type ChangeCategory string

ChangeCategory indicates which part of the spec was changed

const (
	// CategoryEndpoint indicates a path/endpoint change
	CategoryEndpoint ChangeCategory = "endpoint"
	// CategoryOperation indicates an HTTP operation change
	CategoryOperation ChangeCategory = "operation"
	// CategoryParameter indicates a parameter change
	CategoryParameter ChangeCategory = "parameter"
	// CategoryRequestBody indicates a request body change
	CategoryRequestBody ChangeCategory = "request_body"
	// CategoryResponse indicates a response change
	CategoryResponse ChangeCategory = "response"
	// CategorySchema indicates a schema/definition change
	CategorySchema ChangeCategory = "schema"
	// CategorySecurity indicates a security scheme change
	CategorySecurity ChangeCategory = "security"
	// CategoryServer indicates a server change
	CategoryServer ChangeCategory = "server"
	// CategoryInfo indicates metadata change (info, contact, license, etc.)
	CategoryInfo ChangeCategory = "info"
)

type ChangeType

type ChangeType string

ChangeType indicates whether a change is an addition, removal, or modification

const (
	// ChangeTypeAdded indicates a new element was added
	ChangeTypeAdded ChangeType = "added"
	// ChangeTypeRemoved indicates an element was removed
	ChangeTypeRemoved ChangeType = "removed"
	// ChangeTypeModified indicates an existing element was changed
	ChangeTypeModified ChangeType = "modified"
)

type DiffMode

type DiffMode int

DiffMode indicates the type of diff operation to perform

const (
	// ModeSimple reports all semantic differences between documents
	ModeSimple DiffMode = iota
	// ModeBreaking categorizes changes and identifies breaking API changes
	ModeBreaking
)

type DiffResult

type DiffResult struct {
	// SourceVersion is the source document's OAS version string
	SourceVersion string
	// SourceOASVersion is the enumerated source OAS version
	SourceOASVersion parser.OASVersion
	// TargetVersion is the target document's OAS version string
	TargetVersion string
	// TargetOASVersion is the enumerated target OAS version
	TargetOASVersion parser.OASVersion
	// Changes contains all detected changes
	Changes []Change
	// BreakingCount is the number of breaking changes (Critical + Error severity)
	BreakingCount int
	// WarningCount is the number of warnings
	WarningCount int
	// InfoCount is the number of informational changes
	InfoCount int
	// HasBreakingChanges is true if any breaking changes were detected
	HasBreakingChanges bool
}

DiffResult contains the results of comparing two OpenAPI specifications

func Diff

func Diff(sourcePath, targetPath string) (*DiffResult, error)

Diff is a convenience function that compares two OpenAPI specification files. It's equivalent to creating a Differ with New() and calling Diff().

For one-off diff operations, this function provides a simpler API. For comparing multiple files with the same configuration, create a Differ instance and reuse it.

Example:

result, err := differ.Diff("api-v1.yaml", "api-v2.yaml")
if err != nil {
    log.Fatal(err)
}
if result.HasBreakingChanges {
    // Handle breaking changes
}

func DiffParsed

func DiffParsed(source, target parser.ParseResult) (*DiffResult, error)

DiffParsed is a convenience function that compares two already-parsed OpenAPI specifications.

Example:

source, _ := parser.Parse("api-v1.yaml", false, true)
target, _ := parser.Parse("api-v2.yaml", false, true)
result, err := differ.DiffParsed(*source, *target)

type Differ

type Differ struct {
	// Mode determines the type of diff operation (Simple or Breaking)
	Mode DiffMode
	// IncludeInfo determines whether to include informational changes
	IncludeInfo bool
	// UserAgent is the User-Agent string used when fetching URLs
	// Defaults to "oastools" if not set
	UserAgent string
}

Differ handles OpenAPI specification comparison

func New

func New() *Differ

New creates a new Differ instance with default settings

func (*Differ) Diff

func (d *Differ) Diff(sourcePath, targetPath string) (*DiffResult, error)

Diff compares two OpenAPI specification files

func (*Differ) DiffParsed

func (d *Differ) DiffParsed(source, target parser.ParseResult) (*DiffResult, error)

DiffParsed compares two already-parsed OpenAPI specifications

type Severity

type Severity = severity.Severity

Severity indicates the severity level of a change

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL