Documentation
¶
Overview ¶
Package joiner provides OpenAPI Specification (OAS) joining functionality.
This package enables merging multiple OpenAPI specification documents into a single unified document. It supports all OAS versions from 2.0 through 3.2.0 and provides flexible collision resolution strategies for handling conflicts between documents.
Supported Versions ¶
The joiner supports combining documents of the same major version:
- OAS 2.0 (Swagger) documents can be joined with other 2.0 documents
- All OAS 3.x versions (3.0.x, 3.1.x, 3.2.x) can be joined together
The resulting document will use the OpenAPI version declared in the first input document. While minor version mismatches (e.g., 3.0.3 + 3.1.0) are allowed, users should verify the joined document is valid for its declared version, as features from newer versions may be incompatible.
See the OpenAPI Specification references:
- OAS 2.0: https://spec.openapis.org/oas/v2.0.html
- OAS 3.0.x: https://spec.openapis.org/oas/v3.0.0.html
- OAS 3.1.x: https://spec.openapis.org/oas/v3.1.0.html
- OAS 3.2.0: https://spec.openapis.org/oas/v3.2.0.html
Features ¶
- Flexible collision resolution with configurable strategies
- Support for all major OpenAPI components (paths, schemas, parameters, etc.)
- Array merging for servers, security requirements, and tags
- Tag deduplication by name
- Detailed collision reporting and warnings
- Version compatibility validation
Collision Strategies ¶
When joining documents, the joiner handles collisions using configurable strategies:
- StrategyAcceptLeft: Keep the value from the first document (default for schemas/components)
- StrategyAcceptRight: Keep the value from the last document (overwrite)
- StrategyFailOnCollision: Return an error on any collision (default for all)
- StrategyFailOnPaths: Fail only on path collisions, allow schema collisions
Different strategies can be set globally or for specific component types:
- PathStrategy: Controls collision handling for API paths and webhooks
- SchemaStrategy: Controls collision handling for schemas/definitions
- ComponentStrategy: Controls collision handling for other components (parameters, responses, examples, request bodies, headers, security schemes, links, callbacks)
Security Considerations ¶
The joiner implements several security protections:
File overwrite protection: Validates that the output path does not overwrite any input files
Restrictive permissions: Output files are created with 0600 permissions (owner read/write only) to protect potentially sensitive API specifications
Validation: All input documents are validated before joining to prevent combining invalid or malformed specifications
Resource limits: Inherits MaxCachedDocuments limit from the parser (default: 1000) to prevent memory exhaustion
Basic Usage ¶
config := joiner.DefaultConfig()
config.PathStrategy = joiner.StrategyFailOnCollision
config.SchemaStrategy = joiner.StrategyAcceptLeft
j := joiner.New(config)
result, err := j.Join([]string{"api-base.yaml", "api-extensions.yaml"})
if err != nil {
log.Fatalf("Join failed: %v", err)
}
err = j.WriteResult(result, "merged-api.yaml")
if err != nil {
log.Fatalf("Failed to write result: %v", err)
}
Advanced Usage ¶
For more control over the joining process:
config := joiner.JoinerConfig{
DefaultStrategy: joiner.StrategyFailOnCollision,
PathStrategy: joiner.StrategyFailOnPaths,
SchemaStrategy: joiner.StrategyAcceptLeft,
ComponentStrategy: joiner.StrategyAcceptRight,
DeduplicateTags: true,
MergeArrays: true,
}
j := joiner.New(config)
result, err := j.Join([]string{"base.yaml", "ext1.yaml", "ext2.yaml"})
if err != nil {
log.Fatalf("Join failed: %v", err)
}
// Check for warnings
if len(result.Warnings) > 0 {
for _, warning := range result.Warnings {
log.Printf("Warning: %s", warning)
}
}
// Report collision statistics
if result.CollisionCount > 0 {
log.Printf("Resolved %d collisions", result.CollisionCount)
}
Limitations ¶
- Cross-version joining: Cannot join OAS 2.0 documents with OAS 3.x documents
- Info object: The info section from the first document is used; subsequent info sections are ignored
- External references: $ref values in components are preserved as-is; the joiner does not resolve or merge referenced content across documents
- OpenAPI extensions: Extension fields (x-*) are merged like other fields, but custom merging logic for extensions is not supported
Performance Notes ¶
The joiner performs full validation of all input documents before joining, which provides safety but may impact performance for large documents. For better performance:
- Pre-validate documents if possible
- Use StrategyAcceptLeft or StrategyAcceptRight for schemas/components to allow collisions without failing
- Disable array merging (MergeArrays: false) if not needed
- Disable tag deduplication (DeduplicateTags: false) if not needed
Example ¶
Example demonstrates basic usage of the joiner to combine two OpenAPI specifications.
package main
import (
"fmt"
"log"
"os"
"path/filepath"
"github.com/erraggy/oastools/joiner"
)
func main() {
// Create a temporary output file path
outputPath := filepath.Join(os.TempDir(), "joined-example.yaml")
defer func() { _ = os.Remove(outputPath) }()
// Use default configuration
config := joiner.DefaultConfig()
j := joiner.New(config)
// Join two specification files
result, err := j.Join([]string{
"../testdata/join-base-3.0.yaml",
"../testdata/join-extension-3.0.yaml",
})
if err != nil {
log.Fatalf("failed to join: %v", err)
}
// Write the result
err = j.WriteResult(result, outputPath)
if err != nil {
log.Fatalf("failed to write result: %v", err)
}
fmt.Printf("Version: %s\n", result.Version)
fmt.Printf("Warnings: %d\n", len(result.Warnings))
}
Output: Version: 3.0.3 Warnings: 0
Example (AcceptLeft) ¶
Example_acceptLeft demonstrates using accept-left strategy to prefer the first document's values.
package main
import (
"fmt"
"log"
"os"
"path/filepath"
"github.com/erraggy/oastools/joiner"
)
func main() {
outputPath := filepath.Join(os.TempDir(), "joined-left.yaml")
defer func() { _ = os.Remove(outputPath) }()
config := joiner.DefaultConfig()
// Accept values from the first (left) document when collisions occur
config.PathStrategy = joiner.StrategyAcceptLeft
config.SchemaStrategy = joiner.StrategyAcceptLeft
config.ComponentStrategy = joiner.StrategyAcceptLeft
j := joiner.New(config)
result, err := j.Join([]string{
"../testdata/join-base-3.0.yaml",
"../testdata/join-extension-3.0.yaml",
})
if err != nil {
log.Fatalf("failed to join: %v", err)
}
err = j.WriteResult(result, outputPath)
if err != nil {
log.Fatalf("failed to write result: %v", err)
}
fmt.Printf("Strategy: accept-left\n")
fmt.Printf("Version: %s\n", result.Version)
}
Output: Strategy: accept-left Version: 3.0.3
Example (AcceptRight) ¶
Example_acceptRight demonstrates using accept-right strategy to prefer the last document's values.
package main
import (
"fmt"
"log"
"os"
"path/filepath"
"github.com/erraggy/oastools/joiner"
)
func main() {
outputPath := filepath.Join(os.TempDir(), "joined-right.yaml")
defer func() { _ = os.Remove(outputPath) }()
config := joiner.DefaultConfig()
// Accept values from the last (right) document when collisions occur (overwrite)
config.PathStrategy = joiner.StrategyAcceptRight
config.SchemaStrategy = joiner.StrategyAcceptRight
config.ComponentStrategy = joiner.StrategyAcceptRight
j := joiner.New(config)
result, err := j.Join([]string{
"../testdata/join-base-3.0.yaml",
"../testdata/join-extension-3.0.yaml",
})
if err != nil {
log.Fatalf("failed to join: %v", err)
}
err = j.WriteResult(result, outputPath)
if err != nil {
log.Fatalf("failed to write result: %v", err)
}
fmt.Printf("Strategy: accept-right\n")
fmt.Printf("Version: %s\n", result.Version)
}
Output: Strategy: accept-right Version: 3.0.3
Example (ArrayMerging) ¶
Example_arrayMerging demonstrates array merging behavior.
package main
import (
"fmt"
"log"
"os"
"path/filepath"
"github.com/erraggy/oastools/joiner"
)
func main() {
outputPath := filepath.Join(os.TempDir(), "joined-arrays.yaml")
defer func() { _ = os.Remove(outputPath) }()
// Enable array merging (default)
config := joiner.DefaultConfig()
config.MergeArrays = true
config.SchemaStrategy = joiner.StrategyAcceptLeft
j := joiner.New(config)
result, err := j.Join([]string{
"../testdata/join-base-3.0.yaml",
"../testdata/join-extension-3.0.yaml",
})
if err != nil {
log.Fatalf("failed to join: %v", err)
}
err = j.WriteResult(result, outputPath)
if err != nil {
log.Fatalf("failed to write result: %v", err)
}
fmt.Println("Arrays merged successfully")
}
Output: Arrays merged successfully
Example (CollisionError) ¶
Example_collisionError demonstrates handling collision errors.
package main
import (
"fmt"
"log"
"os"
"path/filepath"
"github.com/erraggy/oastools/joiner"
)
func main() {
outputPath := filepath.Join(os.TempDir(), "joined-collision.yaml")
defer func() { _ = os.Remove(outputPath) }()
// Use fail strategy to detect collisions
config := joiner.DefaultConfig()
config.PathStrategy = joiner.StrategyFailOnCollision
config.SchemaStrategy = joiner.StrategyFailOnCollision
j := joiner.New(config)
// This would fail if there were actual collisions
result, err := j.Join([]string{
"../testdata/join-base-3.0.yaml",
"../testdata/join-extension-3.0.yaml",
})
if err != nil {
// Handle collision error
fmt.Printf("Join error: collision detected\n")
return
}
err = j.WriteResult(result, outputPath)
if err != nil {
log.Fatalf("failed to write result: %v", err)
}
fmt.Println("No collisions detected")
}
Output: No collisions detected
Example (CustomStrategies) ¶
Example_customStrategies demonstrates using custom collision strategies for different component types.
package main
import (
"fmt"
"log"
"os"
"path/filepath"
"github.com/erraggy/oastools/joiner"
)
func main() {
outputPath := filepath.Join(os.TempDir(), "joined-custom.yaml")
defer func() { _ = os.Remove(outputPath) }()
// Configure custom strategies
config := joiner.JoinerConfig{
DefaultStrategy: joiner.StrategyFailOnCollision,
PathStrategy: joiner.StrategyFailOnPaths, // Fail on path collisions
SchemaStrategy: joiner.StrategyAcceptLeft, // Keep first schema definition
ComponentStrategy: joiner.StrategyAcceptRight, // Keep last component definition
DeduplicateTags: true,
MergeArrays: true,
}
j := joiner.New(config)
result, err := j.Join([]string{
"../testdata/join-base-3.0.yaml",
"../testdata/join-extension-3.0.yaml",
})
if err != nil {
log.Fatalf("failed to join: %v", err)
}
err = j.WriteResult(result, outputPath)
if err != nil {
log.Fatalf("failed to write result: %v", err)
}
fmt.Printf("Joined successfully\n")
fmt.Printf("Collisions resolved: %d\n", result.CollisionCount)
}
Output: Joined successfully Collisions resolved: 0
Example (DefaultConfig) ¶
Example_defaultConfig demonstrates using the default configuration.
package main
import (
"fmt"
"github.com/erraggy/oastools/joiner"
)
func main() {
config := joiner.DefaultConfig()
fmt.Printf("Default Strategy: %s\n", config.DefaultStrategy)
fmt.Printf("Path Strategy: %s\n", config.PathStrategy)
fmt.Printf("Schema Strategy: %s\n", config.SchemaStrategy)
fmt.Printf("Component Strategy: %s\n", config.ComponentStrategy)
fmt.Printf("Merge Arrays: %v\n", config.MergeArrays)
fmt.Printf("Deduplicate Tags: %v\n", config.DeduplicateTags)
}
Output: Default Strategy: fail Path Strategy: fail Schema Strategy: accept-left Component Strategy: accept-left Merge Arrays: true Deduplicate Tags: true
Example (JoinParsed) ¶
package main
import (
"fmt"
"log"
"github.com/erraggy/oastools/joiner"
"github.com/erraggy/oastools/parser"
)
func main() {
// Parse documents once
p := parser.New()
p.ValidateStructure = true
doc1, _ := p.Parse("../testdata/join-base-3.0.yaml")
doc2, _ := p.Parse("../testdata/join-extension-3.0.yaml")
// Join already-parsed documents
j := joiner.New(joiner.DefaultConfig())
result, err := j.JoinParsed([]parser.ParseResult{*doc1, *doc2})
if err != nil {
log.Fatalf("failed to join: %v", err)
}
if result == nil {
log.Fatalf("result is nil")
}
fmt.Printf("Version: %s\n", result.Version)
}
Output: Version: 3.0.3
Example (MultipleDocuments) ¶
Example_multipleDocuments demonstrates joining more than two documents.
package main
import (
"fmt"
"log"
"os"
"path/filepath"
"github.com/erraggy/oastools/joiner"
)
func main() {
outputPath := filepath.Join(os.TempDir(), "joined-multiple.yaml")
defer func() { _ = os.Remove(outputPath) }()
config := joiner.DefaultConfig()
config.SchemaStrategy = joiner.StrategyAcceptLeft
j := joiner.New(config)
result, err := j.Join([]string{
"../testdata/join-base-3.0.yaml",
"../testdata/join-extension-3.0.yaml",
"../testdata/join-additional-3.0.yaml",
})
if err != nil {
log.Fatalf("failed to join: %v", err)
}
err = j.WriteResult(result, outputPath)
if err != nil {
log.Fatalf("failed to write result: %v", err)
}
fmt.Printf("Joined 3 documents\n")
fmt.Printf("Version: %s\n", result.Version)
}
Output: Joined 3 documents Version: 3.0.3
Example (Oas2) ¶
Example_oas2 demonstrates joining OpenAPI 2.0 (Swagger) documents.
package main
import (
"fmt"
"log"
"os"
"path/filepath"
"github.com/erraggy/oastools/joiner"
)
func main() {
outputPath := filepath.Join(os.TempDir(), "joined-oas2.yaml")
defer func() { _ = os.Remove(outputPath) }()
config := joiner.DefaultConfig()
config.SchemaStrategy = joiner.StrategyAcceptLeft
j := joiner.New(config)
result, err := j.Join([]string{
"../testdata/join-base-2.0.yaml",
"../testdata/join-extension-2.0.yaml",
})
if err != nil {
log.Fatalf("failed to join: %v", err)
}
err = j.WriteResult(result, outputPath)
if err != nil {
log.Fatalf("failed to write result: %v", err)
}
fmt.Printf("OAS Version: %s\n", result.Version)
}
Output: OAS Version: 2.0
Example (TagDeduplication) ¶
Example_tagDeduplication demonstrates tag deduplication behavior.
package main
import (
"fmt"
"log"
"os"
"path/filepath"
"github.com/erraggy/oastools/joiner"
)
func main() {
outputPath := filepath.Join(os.TempDir(), "joined-tags.yaml")
defer func() { _ = os.Remove(outputPath) }()
// Enable tag deduplication (default)
config := joiner.DefaultConfig()
config.DeduplicateTags = true
config.SchemaStrategy = joiner.StrategyAcceptLeft
j := joiner.New(config)
result, err := j.Join([]string{
"../testdata/join-base-3.0.yaml",
"../testdata/join-extension-3.0.yaml",
})
if err != nil {
log.Fatalf("failed to join: %v", err)
}
err = j.WriteResult(result, outputPath)
if err != nil {
log.Fatalf("failed to write result: %v", err)
}
fmt.Println("Tags deduplicated successfully")
}
Output: Tags deduplicated successfully
Example (WithWarnings) ¶
Example_withWarnings demonstrates handling warnings during the join process.
package main
import (
"fmt"
"log"
"os"
"path/filepath"
"github.com/erraggy/oastools/joiner"
)
func main() {
outputPath := filepath.Join(os.TempDir(), "joined-warnings.yaml")
defer func() { _ = os.Remove(outputPath) }()
config := joiner.DefaultConfig()
j := joiner.New(config)
result, err := j.Join([]string{
"../testdata/join-base-3.0.yaml",
"../testdata/join-extension-3.0.yaml",
})
if err != nil {
log.Fatalf("failed to join: %v", err)
}
// Check for warnings
if len(result.Warnings) > 0 {
fmt.Printf("Warnings: %d\n", len(result.Warnings))
for _, warning := range result.Warnings {
fmt.Printf(" - %s\n", warning)
}
} else {
fmt.Println("No warnings")
}
err = j.WriteResult(result, outputPath)
if err != nil {
log.Fatalf("failed to write result: %v", err)
}
}
Output: No warnings
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func IsValidStrategy ¶
IsValidStrategy checks if a strategy string is valid
func ValidStrategies ¶
func ValidStrategies() []string
ValidStrategies returns all valid collision strategy strings
Types ¶
type CollisionError ¶
type CollisionError struct {
Section string
Key string
FirstFile string
FirstPath string
SecondFile string
SecondPath string
Strategy CollisionStrategy
}
CollisionError provides detailed information about a collision
func (*CollisionError) Error ¶
func (e *CollisionError) Error() string
type CollisionStrategy ¶
type CollisionStrategy string
CollisionStrategy defines how to handle collisions when merging documents
const ( // StrategyAcceptLeft keeps values from the first document when collisions occur StrategyAcceptLeft CollisionStrategy = "accept-left" // StrategyAcceptRight keeps values from the last document when collisions occur (overwrites) StrategyAcceptRight CollisionStrategy = "accept-right" // StrategyFailOnCollision returns an error if any collision is detected StrategyFailOnCollision CollisionStrategy = "fail" // StrategyFailOnPaths fails only on path collisions, allows schema/component collisions StrategyFailOnPaths CollisionStrategy = "fail-on-paths" )
type JoinResult ¶
type JoinResult struct {
// Document contains the joined document (*parser.OAS2Document or *parser.OAS3Document)
Document interface{}
// Version is the OpenAPI version of the joined document
Version string
// OASVersion is the enumerated version
OASVersion parser.OASVersion
// Warnings contains non-fatal issues encountered during joining
Warnings []string
// CollisionCount tracks the number of collisions resolved
CollisionCount int
// contains filtered or unexported fields
}
JoinResult contains the joined OpenAPI specification and metadata
type Joiner ¶
type Joiner struct {
// contains filtered or unexported fields
}
Joiner handles joining of multiple OpenAPI specifications.
Concurrency: Joiner instances are not safe for concurrent use. Create separate Joiner instances for concurrent operations.
func New ¶
func New(config JoinerConfig) *Joiner
New creates a new Joiner instance with the provided configuration
func (*Joiner) Join ¶
func (j *Joiner) Join(specPaths []string) (*JoinResult, error)
Join joins multiple OpenAPI specifications into a single document
func (*Joiner) JoinParsed ¶ added in v1.3.1
func (j *Joiner) JoinParsed(parsedDocs []parser.ParseResult) (*JoinResult, error)
func (*Joiner) WriteResult ¶
func (j *Joiner) WriteResult(result *JoinResult, outputPath string) error
WriteResult writes a join result to a file
The output file is written with restrictive permissions (0600 - owner read/write only) to protect potentially sensitive API specifications. If the file already exists, its permissions will be explicitly set to 0600 after writing.
type JoinerConfig ¶
type JoinerConfig struct {
// DefaultStrategy is the global strategy for all collisions
DefaultStrategy CollisionStrategy
// PathStrategy defines strategy specifically for path collisions
PathStrategy CollisionStrategy
// SchemaStrategy defines strategy specifically for schema/definition collisions
SchemaStrategy CollisionStrategy
// ComponentStrategy defines strategy for other component collisions (parameters, responses, etc.)
ComponentStrategy CollisionStrategy
// DeduplicateTags removes duplicate tags by name
DeduplicateTags bool
// MergeArrays determines whether to merge array fields (servers, security, etc.)
MergeArrays bool
}
JoinerConfig configures how documents are joined
func DefaultConfig ¶
func DefaultConfig() JoinerConfig
DefaultConfig returns a sensible default configuration