Documentation
¶
Overview ¶
Package joiner provides joining for multiple OpenAPI Specification documents.
The joiner merges multiple OAS documents of the same major version into a single document. It supports OAS 2.0 documents with other 2.0 documents, and all OAS 3.x versions together (3.0.x, 3.1.x, 3.2.x). It uses the version and format (JSON or YAML) from the first document as the result version and format, ensuring format consistency when writing output with WriteResult.
Quick Start ¶
Join files using functional options:
result, err := joiner.JoinWithOptions(
joiner.WithFilePaths([]string{"base.yaml", "ext.yaml"}),
joiner.WithPathStrategy(joiner.StrategyAcceptLeft),
)
if err != nil {
log.Fatal(err)
}
_ = joiner.WriteResult(result, "merged.yaml")
Or use a full config with options:
config := joiner.DefaultConfig()
config.PathStrategy = joiner.StrategyAcceptLeft
result, err := joiner.JoinWithOptions(
joiner.WithFilePaths([]string{"base.yaml", "ext.yaml"}),
joiner.WithConfig(config),
)
Or create a reusable Joiner instance:
j := joiner.New(joiner.DefaultConfig())
result1, _ := j.Join([]string{"api1-base.yaml", "api1-ext.yaml"})
result2, _ := j.Join([]string{"api2-base.yaml", "api2-ext.yaml"})
j.WriteResult(result1, "merged1.yaml")
j.WriteResult(result2, "merged2.yaml")
Collision Strategies ¶
Control how collisions between documents are handled:
- StrategyFailOnCollision: Fail on any collision (default)
- StrategyAcceptLeft: Keep value from first document
- StrategyAcceptRight: Keep value from last document
- StrategyFailOnPaths: Fail only on path collisions, allow schema merging
- StrategyRenameLeft: Rename left schema, keep right under original name
- StrategyRenameRight: Rename right schema, keep left under original name
- StrategyDeduplicateEquivalent: Merge structurally identical schemas
Set strategies globally (DefaultStrategy) or per component type (PathStrategy, SchemaStrategy, ComponentStrategy). The rename and deduplicate strategies provide advanced collision handling with automatic reference rewriting.
Advanced Collision Handling ¶
The rename strategies preserve both colliding schemas by renaming one and automatically updating all references throughout the merged document:
config := joiner.DefaultConfig()
config.SchemaStrategy = joiner.StrategyRenameRight
config.RenameTemplate = "{{.Name}}_{{.Source}}"
result, err := joiner.JoinWithOptions(
joiner.WithFilePaths([]string{"users-api.yaml", "billing-api.yaml"}),
joiner.WithConfig(config),
)
The deduplicate strategy uses semantic equivalence detection to merge structurally identical schemas while failing on true structural conflicts:
config := joiner.DefaultConfig()
config.SchemaStrategy = joiner.StrategyDeduplicateEquivalent
config.EquivalenceMode = "deep"
result, err := joiner.JoinWithOptions(
joiner.WithFilePaths([]string{"base.yaml", "ext.yaml"}),
joiner.WithConfig(config),
)
See the examples in example_test.go for more configuration patterns.
Features and Limitations ¶
The joiner validates all input documents, prevents output file overwrites with restrictive 0600 permissions, deduplicates tags, and optionally merges arrays (servers, security, tags). It uses the info object from the first document; subsequent info sections are ignored.
External References ¶
The joiner preserves external $ref values but does NOT resolve or merge them. This is intentional to avoid ambiguity and maintain document structure.
If your documents contain external references, you have two options:
Resolve references before joining: Use parser.ParseWithOptions(parser.WithResolveRefs(true)) before joining
Keep external references and resolve after joining: Join the documents, then parse the result with WithResolveRefs(true)
Example with external references:
// Document 1: base.yaml // paths: // /users: // get: // responses: // 200: // schema: // $ref: "./schemas/user.yaml#/User" // // Document 2: extension.yaml // paths: // /posts: // get: // responses: // 200: // schema: // $ref: "./schemas/post.yaml#/Post" // // After joining, both $ref values are preserved in the merged document. // Use parser.WithResolveRefs(true) to resolve them if needed.
Related Packages ¶
The joiner integrates with other oastools packages:
- github.com/erraggy/oastools/parser - Parse specifications before joining
- github.com/erraggy/oastools/validator - Validate documents before joining (required)
- github.com/erraggy/oastools/fixer - Fix common validation errors before joining
- github.com/erraggy/oastools/converter - Convert between OAS versions before joining
- github.com/erraggy/oastools/differ - Compare joined results with original documents
- github.com/erraggy/oastools/generator - Generate code from joined specifications
- github.com/erraggy/oastools/builder - Programmatically build specifications to join
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() {
outputPath := filepath.Join(os.TempDir(), "joined-example.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)
}
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 (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) }()
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{
"../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
Index ¶
- func IsValidEquivalenceMode(mode string) bool
- func IsValidStrategy(strategy string) bool
- func ValidEquivalenceModes() []string
- func ValidStrategies() []string
- type CollisionError
- type CollisionEvent
- type CollisionReport
- type CollisionStrategy
- type EquivalenceMode
- type EquivalenceResult
- type JoinResult
- type Joiner
- type JoinerConfig
- type Option
- func WithAlwaysApplyPrefix(enabled bool) Option
- func WithCollisionReport(enabled bool) Option
- func WithComponentStrategy(strategy CollisionStrategy) Option
- func WithConfig(config JoinerConfig) Option
- func WithDeduplicateTags(enabled bool) Option
- func WithDefaultStrategy(strategy CollisionStrategy) Option
- func WithEquivalenceMode(mode string) Option
- func WithFilePaths(paths ...string) Option
- func WithMergeArrays(enabled bool) Option
- func WithNamespacePrefix(sourcePath, prefix string) Option
- func WithParsed(docs ...parser.ParseResult) Option
- func WithPathStrategy(strategy CollisionStrategy) Option
- func WithRenameTemplate(template string) Option
- func WithSchemaStrategy(strategy CollisionStrategy) Option
- type SchemaDifference
- type SchemaRewriter
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func IsValidEquivalenceMode ¶ added in v1.23.0
IsValidEquivalenceMode checks if an equivalence mode string is valid
func IsValidStrategy ¶
IsValidStrategy checks if a strategy string is valid
func ValidEquivalenceModes ¶ added in v1.23.0
func ValidEquivalenceModes() []string
ValidEquivalenceModes returns all valid equivalence mode strings
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 CollisionEvent ¶ added in v1.23.0
type CollisionEvent struct {
SchemaName string
LeftSource string
RightSource string
Strategy CollisionStrategy
Resolution string // "renamed", "deduplicated", "kept-left", "kept-right", "failed"
NewName string // For rename resolutions
Differences []SchemaDifference
Severity severity.Severity
}
CollisionEvent represents a single collision occurrence with resolution details
type CollisionReport ¶ added in v1.23.0
type CollisionReport struct {
TotalCollisions int
ResolvedByRename int
ResolvedByDedup int
ResolvedByAccept int
FailedCollisions int
Events []CollisionEvent
}
CollisionReport provides detailed analysis of collisions encountered during join operations
func NewCollisionReport ¶ added in v1.23.0
func NewCollisionReport() *CollisionReport
NewCollisionReport creates an empty collision report
func (*CollisionReport) AddEvent ¶ added in v1.23.0
func (r *CollisionReport) AddEvent(event CollisionEvent)
AddEvent adds a collision event to the report and updates counters
func (*CollisionReport) GetByResolution ¶ added in v1.23.0
func (r *CollisionReport) GetByResolution(resolution string) []CollisionEvent
GetByResolution returns events with a specific resolution type
func (*CollisionReport) GetCriticalEvents ¶ added in v1.23.0
func (r *CollisionReport) GetCriticalEvents() []CollisionEvent
GetCriticalEvents returns events with Critical severity
func (*CollisionReport) HasFailures ¶ added in v1.23.0
func (r *CollisionReport) HasFailures() bool
HasFailures returns true if any collisions failed to resolve
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" // StrategyRenameLeft keeps the right-side schema and renames the left-side schema StrategyRenameLeft CollisionStrategy = "rename-left" // StrategyRenameRight keeps the left-side schema and renames the right-side schema StrategyRenameRight CollisionStrategy = "rename-right" // StrategyDeduplicateEquivalent uses semantic comparison to deduplicate structurally identical schemas StrategyDeduplicateEquivalent CollisionStrategy = "deduplicate" )
type EquivalenceMode ¶ added in v1.23.0
type EquivalenceMode string
EquivalenceMode defines how deeply to compare schemas
const ( // EquivalenceModeNone disables equivalence detection EquivalenceModeNone EquivalenceMode = "none" // EquivalenceModeShallow compares only top-level schema properties EquivalenceModeShallow EquivalenceMode = "shallow" // EquivalenceModeDeep recursively compares all nested schemas EquivalenceModeDeep EquivalenceMode = "deep" )
type EquivalenceResult ¶ added in v1.23.0
type EquivalenceResult struct {
Equivalent bool
Differences []SchemaDifference
}
EquivalenceResult contains the outcome of schema comparison
func CompareSchemas ¶ added in v1.23.0
func CompareSchemas(left, right *parser.Schema, mode EquivalenceMode) EquivalenceResult
CompareSchemas compares two schemas for structural equivalence Ignores: description, title, example, deprecated, and extension fields (x-*)
type JoinResult ¶
type JoinResult struct {
// Document contains the joined document (*parser.OAS2Document or *parser.OAS3Document)
Document any
// Version is the OpenAPI version of the joined document
Version string
// OASVersion is the enumerated version
OASVersion parser.OASVersion
// SourceFormat is the format of the first source file (JSON or YAML)
SourceFormat parser.SourceFormat
// Warnings contains non-fatal issues encountered during joining
Warnings []string
// CollisionCount tracks the number of collisions resolved
CollisionCount int
// Stats contains statistical information about the joined document
Stats parser.DocumentStats
// CollisionDetails contains detailed collision analysis (when CollisionReport is enabled)
CollisionDetails *CollisionReport
// contains filtered or unexported fields
}
JoinResult contains the joined OpenAPI specification and metadata
func JoinWithOptions ¶ added in v1.11.0
func JoinWithOptions(opts ...Option) (*JoinResult, error)
JoinWithOptions joins multiple OpenAPI specifications using functional options. This provides a flexible, extensible API that combines input source selection and configuration in a single function call.
Example:
result, err := joiner.JoinWithOptions(
joiner.WithFilePaths("api1.yaml", "api2.yaml"),
joiner.WithPathStrategy(joiner.StrategyAcceptLeft),
)
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 in YAML or JSON format (matching the source format)
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
// Advanced collision strategies configuration
// RenameTemplate is a Go template for renamed schema names (default: "{{.Name}}_{{.Source}}")
// Available variables: {{.Name}} (original name), {{.Source}} (source file), {{.Index}} (doc index)
RenameTemplate string
// NamespacePrefix maps source file paths to namespace prefixes for schema names
// Example: {"users-api.yaml": "Users", "billing-api.yaml": "Billing"}
// When a prefix is configured, schemas from that source get prefixed: User -> Users_User
NamespacePrefix map[string]string
// AlwaysApplyPrefix when true applies namespace prefix to all schemas from a source,
// not just those that collide. When false (default), prefix is only applied on collision.
AlwaysApplyPrefix bool
// EquivalenceMode controls depth of schema comparison: "none", "shallow", or "deep"
EquivalenceMode string
// CollisionReport enables detailed collision analysis reporting
CollisionReport bool
}
JoinerConfig configures how documents are joined
func DefaultConfig ¶
func DefaultConfig() JoinerConfig
DefaultConfig returns a sensible default configuration
type Option ¶ added in v1.11.0
type Option func(*joinConfig) error
Option is a function that configures a join operation
func WithAlwaysApplyPrefix ¶ added in v1.23.0
WithAlwaysApplyPrefix enables or disables applying namespace prefix to all schemas, not just those that collide. When false (default), prefix is only applied on collision.
func WithCollisionReport ¶ added in v1.23.0
WithCollisionReport enables or disables detailed collision reporting Default: false
func WithComponentStrategy ¶ added in v1.11.0
func WithComponentStrategy(strategy CollisionStrategy) Option
WithComponentStrategy sets the collision strategy for components
func WithConfig ¶ added in v1.11.0
func WithConfig(config JoinerConfig) Option
WithConfig applies an entire JoinerConfig struct This is useful for reusing existing configurations or loading from files
func WithDeduplicateTags ¶ added in v1.11.0
WithDeduplicateTags enables or disables tag deduplication Default: true
func WithDefaultStrategy ¶ added in v1.11.0
func WithDefaultStrategy(strategy CollisionStrategy) Option
WithDefaultStrategy sets the global collision strategy
func WithEquivalenceMode ¶ added in v1.23.0
WithEquivalenceMode sets the schema comparison mode for deduplication Valid values: "none", "shallow", "deep" Default: "none"
func WithFilePaths ¶ added in v1.11.0
WithFilePaths specifies file paths as input sources
func WithMergeArrays ¶ added in v1.11.0
WithMergeArrays enables or disables array merging (servers, security, etc.) Default: true
func WithNamespacePrefix ¶ added in v1.23.0
WithNamespacePrefix adds a namespace prefix mapping for a source file. When schemas from a source file collide (or when AlwaysApplyPrefix is true), the prefix is applied to schema names: e.g., "User" -> "Users_User" Can be called multiple times to add multiple mappings.
func WithParsed ¶ added in v1.11.0
func WithParsed(docs ...parser.ParseResult) Option
WithParsed specifies parsed ParseResults as input sources
func WithPathStrategy ¶ added in v1.11.0
func WithPathStrategy(strategy CollisionStrategy) Option
WithPathStrategy sets the collision strategy for paths
func WithRenameTemplate ¶ added in v1.23.0
WithRenameTemplate sets the Go template for renamed schema names Default: "{{.Name}}_{{.Source}}" Available variables: {{.Name}}, {{.Source}}, {{.Index}}, {{.Suffix}}
func WithSchemaStrategy ¶ added in v1.11.0
func WithSchemaStrategy(strategy CollisionStrategy) Option
WithSchemaStrategy sets the collision strategy for schemas/definitions
type SchemaDifference ¶ added in v1.23.0
type SchemaDifference struct {
Path string // JSON path to differing element (e.g., "properties.name.type")
LeftValue any
RightValue any
Description string
}
SchemaDifference describes a structural difference between two schemas
type SchemaRewriter ¶ added in v1.23.0
type SchemaRewriter struct {
// contains filtered or unexported fields
}
SchemaRewriter handles rewriting of schema references throughout an OpenAPI document
func NewSchemaRewriter ¶ added in v1.23.0
func NewSchemaRewriter() *SchemaRewriter
NewSchemaRewriter creates a new rewriter instance
func (*SchemaRewriter) RegisterRename ¶ added in v1.23.0
func (r *SchemaRewriter) RegisterRename(oldName, newName string, version parser.OASVersion)
RegisterRename registers a schema rename operation
func (*SchemaRewriter) RewriteDocument ¶ added in v1.23.0
func (r *SchemaRewriter) RewriteDocument(doc any) error
RewriteDocument traverses and rewrites all references in the document