joiner

package
v1.3.1 Latest Latest
Warning

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

Go to latest
Published: Nov 12, 2025 License: MIT Imports: 4 Imported by: 0

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:

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

func IsValidStrategy(strategy string) bool

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

Jump to

Keyboard shortcuts

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