feat(spec-merge): add alternative DEEP merge mode with by-method path merging and configurable conflict resolution strategy

Merged
PI
3mo ago
+2942-92

Note: This change should be fully backwards compatible as it is entirely opt-in for now.

Problem

When merging multiple OpenAPI specs, the previous MergedSpecBuilder overwrote a path
whenever two specs defined the same URL path. If one spec defined GET /pets and another
defined POST /pets, one of the operations was silently lost.

What changed

  • Merge paths by HTTP method: colliding paths now combine their operations
    (e.g. GET from one file + POST from another) instead of overwriting.
  • Path-level data preserved: path-level parameters, servers, and x- vendor
    extensions are merged (first definition wins on conflict); server URLs are de-duplicated.
  • Only real spec files scanned: only .yaml, .yml, .json are processed, in a
    deterministic (sorted) order; other files are ignored.
  • Explicit ordered file list: you can now pass an ordered list of spec files to merge
    (with an output dir) across CLI, Gradle, and Maven, instead of only scanning a directory.

Why the new mergeMode and mergeConflictStrategy

Two merge modes exist so behavior stays backward-compatible while still allowing a real merge:

  • mergeMode

    • REF (default): the original behavior - produces a lightweight index spec where each
      path is a $ref back to its source file. Kept as default so existing setups don't change.
    • DEEP: fully inlines all paths and components into one self-contained spec, with component
      deduplication and the by-method path merging described above.
  • mergeConflictStrategy (only applies in DEEP mode)
    When two specs define the same component name (schema, response, etc.) with different
    definitions, we need a policy:

    • WARN (default): keep the first definition and log a warning.
    • FAIL: abort the merge. Useful in CI to catch accidental conflicts early.

    Note: identical duplicate components are silently de-duplicated. Duplicate path+method
    definitions always fail, since there is no valid reason for the same method on the same path
    to be defined twice.

Where it's exposed

  • CLI: --merge-mode, --merge-conflict-strategy, --input-spec-files, --merged-file-output-dir
  • Gradle: mergeMode, mergeConflictStrategy, inputSpecFiles, mergedFileOutputDir
  • Maven: mergeMode, mergeConflictStrategy, inputSpecFiles, mergedFileOutputDir

Invalid values fail fast with a clear error. Docs and tests updated accordingly.

PR checklist

  • Read the contribution guidelines.
  • Run the following to build the project and update samples:
    ./mvnw clean package || exit
    ./bin/generate-samples.sh ./bin/configs/*.yaml || exit
    ./bin/utils/export_docs_generators.sh || exit
    
    (For Windows users, please run the script in WSL)
    Commit all changed files.
    This is important, as CI jobs will verify all generator outputs of your HEAD commit as it would merge with master.
    These must match the expectations made by your contribution.
    You may regenerate an individual generator by passing the relevant config(s) as an argument to the script, for example ./bin/generate-samples.sh bin/configs/java*.
    IMPORTANT: Do NOT purge/delete any folders/files (e.g. tests) when regenerating the samples as manually written tests may be removed.
  • If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.

Summary by cubic

Adds a DEEP merge mode that merges paths by HTTP method, preserves path-level data, resolves external $refs for a self-contained spec, and uses a configurable conflict strategy. Also supports ordered input spec lists and root metadata merging across CLI, Gradle, and Maven, while keeping REF as the original $ref-based mode.

  • Bug Fixes

    • Merge path items by method on collisions; detect duplicate path+method and apply mergeConflictStrategy in DEEP; REF keeps last.
    • Preserve path-level parameters and vendor extensions; dedupe params by $ref; dedupe servers and add http://localhost:8080 if none.
    • Resolve external $refs into components in DEEP; CLI forwards --auth for authenticated refs.
    • Propagate root-level security to operations unless overridden; warn-and-rename duplicate operationIds.
    • Merge root-level tags, externalDocs, webhooks (first-wins) and components.pathItems (OAS 3.1).
    • Scan only .yaml, .yml, .json deterministically; respect inputSpecFiles order; keep JSON if the first valid spec is JSON.
    • Validate --merge-conflict-strategy regardless of mode; Gradle cleans output before writing; when inputSpecFiles is set, skip directory scan; fail fast on empty.
    • Validate OpenAPI version in DEEP: enforce strict MAJOR.MINOR.PATCH semver, reject missing/malformed values (e.g., 3.0, 3.0.3.1, leading zeroes), and prevent mixing different major versions with clear errors.
  • New Features

    • Add mergeMode (REF, DEEP) and mergeConflictStrategy (WARN, FAIL) in CLI (--merge-mode, --merge-conflict-strategy), Gradle, and Maven; invalid values fail fast.
    • Support an explicit ordered list of spec files to merge across CLI (--input-spec-files with --merged-file-output-dir), Gradle (inputSpecFiles, mergedFileOutputDir), and Maven (inputSpecFiles, mergedFileOutputDir); order overrides directory scanning.
    • Add merged spec Info fields on all three (mergedFileInfoName, mergedFileInfoDescription, mergedFileInfoVersion); Gradle also adds mergedFileName (defaults to merged).