Skip to content

Commit 3d3bd76

Browse files
erraggyclaude
andauthored
feat(differ): add OpenAPI specification diff and breaking change detection (#22)
* feat(differ): add OpenAPI specification diff and breaking change detection Add comprehensive differ package and CLI command for comparing OpenAPI specifications with two operational modes: simple semantic diff and breaking change detection. Features: - New differ package with Diff() and DiffParsed() convenience functions - Reusable Differ struct with configurable modes and options - Simple mode: reports all semantic differences without categorization - Breaking mode: categorizes changes by severity (Critical, Error, Warning, Info) - Change detection across all OAS elements: endpoints, operations, parameters, request bodies, responses, schemas, security, servers, and metadata - Support for OAS 2.0, 3.0.x, 3.1.x, 3.2.x with cross-version comparison - CLI command with --breaking and --no-info flags - Pretty-printed output with severity symbols and category grouping - Exit code 1 for breaking changes in breaking mode Breaking change detection includes: - Critical: Removed endpoints, operations, required parameters - Error: Incompatible type changes, removed enum values, removed success responses - Warning: Deprecated operations, server changes, added required fields - Info: Additions, relaxed constraints, documentation updates Implementation: - differ/differ.go: Core types and API - differ/simple.go: Simple semantic diff implementation - differ/breaking.go: Breaking change detection logic - differ/doc.go: Comprehensive package documentation - differ/example_test.go: Runnable examples for godoc - differ/differ_test.go: Full test coverage - CLI integration in cmd/oastools/main.go with help text - Test fixtures: petstore-v1.yaml and petstore-v2.yaml - Planning documentation in planning/differ.md All tests passing with make check. Code follows existing package patterns for consistency. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * docs: add differ package to CLAUDE.md documentation Update project overview and directory structure to include the new differ package. Addresses review feedback from PR #22. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent a10caa5 commit 3d3bd76

12 files changed

Lines changed: 3081 additions & 3 deletions

File tree

CLAUDE.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
88
- Validating OpenAPI specification files
99
- Parsing and analyzing OAS documents
1010
- Joining multiple OpenAPI specification documents
11+
- Converting between OAS versions
12+
- Comparing OAS documents and detecting breaking changes
1113

1214
## Specification References
1315

@@ -280,6 +282,12 @@ make clean
280282
- Best-effort conversion with transparent issue tracking
281283
- Package documentation in `doc.go` and examples in `example_test.go`
282284

285+
- **differ/** - Public diffing library for OpenAPI specifications
286+
- Logic for comparing OpenAPI specification files
287+
- Simple semantic diff and breaking change detection
288+
- Categorizes changes by severity (Critical, Error, Warning, Info)
289+
- Package documentation in `doc.go` and examples in `example_test.go`
290+
283291
- **internal/** - Internal packages with shared utilities (not part of public API)
284292
- **internal/httputil/** - HTTP-related validation constants and utilities
285293
- HTTP status code validation and RFC 9110 standards

cmd/oastools/main.go

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"path/filepath"
88

99
"github.com/erraggy/oastools/converter"
10+
"github.com/erraggy/oastools/differ"
1011
"github.com/erraggy/oastools/joiner"
1112
"github.com/erraggy/oastools/parser"
1213
"github.com/erraggy/oastools/validator"
@@ -38,6 +39,8 @@ func main() {
3839
handleJoin(os.Args[2:])
3940
case "convert":
4041
handleConvert(os.Args[2:])
42+
case "diff":
43+
handleDiff(os.Args[2:])
4144
default:
4245
fmt.Fprintf(os.Stderr, "Unknown command: %s\n\n", command)
4346
printUsage()
@@ -609,6 +612,167 @@ Notes:
609612
- Always validate converted documents before deployment`)
610613
}
611614

615+
func handleDiff(args []string) {
616+
// Parse flags
617+
var breaking bool
618+
var noInfo bool
619+
var sourcePath string
620+
var targetPath string
621+
622+
for i := 0; i < len(args); i++ {
623+
arg := args[i]
624+
switch arg {
625+
case "--breaking":
626+
breaking = true
627+
case "--no-info":
628+
noInfo = true
629+
case "-h", "--help":
630+
printDiffUsage()
631+
return
632+
default:
633+
if sourcePath == "" {
634+
sourcePath = arg
635+
} else if targetPath == "" {
636+
targetPath = arg
637+
} else {
638+
fmt.Fprintf(os.Stderr, "Error: unexpected argument '%s'\n", arg)
639+
os.Exit(1)
640+
}
641+
}
642+
}
643+
644+
if sourcePath == "" || targetPath == "" {
645+
fmt.Fprintf(os.Stderr, "Error: diff command requires two file paths or URLs\n\n")
646+
printDiffUsage()
647+
os.Exit(1)
648+
}
649+
650+
// Create differ with options
651+
d := differ.New()
652+
if breaking {
653+
d.Mode = differ.ModeBreaking
654+
} else {
655+
d.Mode = differ.ModeSimple
656+
}
657+
d.IncludeInfo = !noInfo
658+
d.UserAgent = fmt.Sprintf("oastools/%s", version)
659+
660+
// Diff the files
661+
result, err := d.Diff(sourcePath, targetPath)
662+
if err != nil {
663+
fmt.Fprintf(os.Stderr, "Error comparing specifications: %v\n", err)
664+
os.Exit(1)
665+
}
666+
667+
// Print results
668+
fmt.Printf("OpenAPI Specification Diff\n")
669+
fmt.Printf("==========================\n\n")
670+
fmt.Printf("Source: %s (%s)\n", sourcePath, result.SourceVersion)
671+
fmt.Printf("Target: %s (%s)\n\n", targetPath, result.TargetVersion)
672+
673+
if len(result.Changes) == 0 {
674+
fmt.Println("✓ No differences found - specifications are identical")
675+
return
676+
}
677+
678+
// Print changes grouped by category if in breaking mode
679+
if breaking {
680+
// Group changes by category
681+
categories := make(map[differ.ChangeCategory][]differ.Change)
682+
for _, change := range result.Changes {
683+
categories[change.Category] = append(categories[change.Category], change)
684+
}
685+
686+
// Print each category
687+
categoryOrder := []differ.ChangeCategory{
688+
differ.CategoryEndpoint,
689+
differ.CategoryOperation,
690+
differ.CategoryParameter,
691+
differ.CategoryRequestBody,
692+
differ.CategoryResponse,
693+
differ.CategorySchema,
694+
differ.CategorySecurity,
695+
differ.CategoryServer,
696+
differ.CategoryInfo,
697+
}
698+
699+
for _, category := range categoryOrder {
700+
changes := categories[category]
701+
if len(changes) == 0 {
702+
continue
703+
}
704+
705+
fmt.Printf("%s Changes (%d):\n", category, len(changes))
706+
for _, change := range changes {
707+
fmt.Printf(" %s\n", change.String())
708+
}
709+
fmt.Println()
710+
}
711+
712+
// Print summary
713+
fmt.Printf("Summary:\n")
714+
fmt.Printf(" Total changes: %d\n", len(result.Changes))
715+
if result.HasBreakingChanges {
716+
fmt.Printf(" ⚠️ Breaking changes: %d\n", result.BreakingCount)
717+
} else {
718+
fmt.Printf(" ✓ Breaking changes: 0\n")
719+
}
720+
fmt.Printf(" Warnings: %d\n", result.WarningCount)
721+
if d.IncludeInfo {
722+
fmt.Printf(" Info: %d\n", result.InfoCount)
723+
}
724+
725+
// Exit with error if breaking changes found
726+
if result.HasBreakingChanges {
727+
os.Exit(1)
728+
}
729+
} else {
730+
// Simple mode - just print all changes
731+
fmt.Printf("Changes (%d):\n", len(result.Changes))
732+
for _, change := range result.Changes {
733+
fmt.Printf(" %s\n", change.String())
734+
}
735+
}
736+
}
737+
738+
func printDiffUsage() {
739+
fmt.Println(`Usage: oastools diff [options] <source> <target>
740+
741+
Compare two OpenAPI specification files or URLs and report differences.
742+
743+
Options:
744+
--breaking Enable breaking change detection and categorization
745+
--no-info Exclude informational changes from output
746+
-h, --help Show this help message
747+
748+
Modes:
749+
Default (Simple):
750+
Reports all semantic differences between specifications without
751+
categorizing them by severity or breaking change impact.
752+
753+
--breaking (Breaking Change Detection):
754+
Categorizes changes by severity and identifies breaking API changes:
755+
- Critical: Removed endpoints or operations
756+
- Error: Removed required parameters, incompatible type changes
757+
- Warning: Deprecated operations, added required fields
758+
- Info: Additions, relaxed constraints, documentation updates
759+
760+
Examples:
761+
oastools diff api-v1.yaml api-v2.yaml
762+
oastools diff --breaking api-v1.yaml api-v2.yaml
763+
oastools diff --breaking --no-info old.yaml new.yaml
764+
oastools diff https://example.com/api/v1.yaml https://example.com/api/v2.yaml
765+
766+
Exit Status:
767+
0 No differences found (or no breaking changes in --breaking mode)
768+
1 Differences found (or breaking changes found in --breaking mode)
769+
770+
Notes:
771+
- Both specifications must be valid OpenAPI documents
772+
- Cross-version comparison (2.0 vs 3.x) is supported with limitations
773+
- Breaking change detection helps identify backward compatibility issues`)
774+
}
775+
612776
func printUsage() {
613777
fmt.Println(`oastools - OpenAPI Specification Tools
614778
@@ -618,6 +782,7 @@ Usage:
618782
Commands:
619783
validate Validate an OpenAPI specification file or URL
620784
convert Convert between OpenAPI specification versions
785+
diff Compare two OpenAPI specifications and detect changes
621786
join Join multiple OpenAPI specification files
622787
parse Parse and display an OpenAPI specification file or URL
623788
version Show version information
@@ -627,6 +792,7 @@ Examples:
627792
oastools validate openapi.yaml
628793
oastools validate https://example.com/api/openapi.yaml
629794
oastools convert -t 3.0.3 swagger.yaml -o openapi.yaml
795+
oastools diff --breaking api-v1.yaml api-v2.yaml
630796
oastools join -o merged.yaml base.yaml extensions.yaml
631797
oastools parse https://raw.githubusercontent.com/OAI/OpenAPI-Specification/main/examples/v3.0/petstore.yaml
632798

0 commit comments

Comments
 (0)