Skip to content

Commit f240a5f

Browse files
authored
Overlay - Support "extends" for referencing OpenAPI documents (#178)
* Overlay - Support "extends" for referencing OpenAPI documents
1 parent d06f2c9 commit f240a5f

11 files changed

Lines changed: 117 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
## unreleased
22

3+
- Overlay: Support "extends" for referencing OpenAPI documents (#176)
34
- Improvement: Added safe checks for invalid YAML
4-
- CLI - Fix relative path generation in $ref when splitting document (#175)
5+
- CLI: Fix relative path generation in $ref when splitting document (#175)
56

67
## [1.27.3] - 2025-07-30
78

bin/cli.js

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ function increaseVerbosity(dummyValue, previous) {
1313
}
1414

1515
program
16-
.arguments('<oaFile>')
16+
.arguments('[oaFile]')
1717
.usage('<file> [options]')
1818
.description('Format an OpenAPI document by ordering, formatting and filtering fields.')
1919
.option('-o, --output <output>', 'save the formatted OpenAPI file as JSON/YAML')
@@ -59,11 +59,6 @@ async function run(oaFile, options) {
5959
let cliLog = {};
6060
const consoleLine = process.stdout.columns ? '='.repeat(process.stdout.columns) : '='.repeat(80);
6161

62-
if (!oaFile) {
63-
console.error('Please provide a file path for the OpenAPI document');
64-
return;
65-
}
66-
6762
infoOut(`${consoleLine}`); // LOG - horizontal rule
6863
infoOut(`OpenAPI-Format CLI settings:`); // LOG - config file
6964

@@ -216,24 +211,72 @@ async function run(oaFile, options) {
216211
}
217212
}
218213

214+
// Allow missing input file if overlay extends is provided
215+
if (!oaFile) {
216+
const hasOverlay = !!options?.overlaySet;
217+
const extendsRef = options?.overlaySet?.extends;
218+
if (extendsRef) {
219+
// Resolve local relative paths against the overlay file location
220+
const isRemote =
221+
typeof extendsRef === 'string' && (extendsRef.startsWith('http://') || extendsRef.startsWith('https://'));
222+
if (isRemote) {
223+
oaFile = extendsRef;
224+
} else {
225+
const baseDir = options?.overlayFile ? path.dirname(path.resolve(options.overlayFile)) : process.cwd();
226+
oaFile = path.isAbsolute(extendsRef) ? extendsRef : path.resolve(baseDir, extendsRef);
227+
}
228+
infoOut(`- Input file (extends):\t${oaFile}`);
229+
} else {
230+
if (!hasOverlay) {
231+
console.error('Please provide a file path for the OpenAPI document');
232+
} else {
233+
console.error('Please provide an input file or an overlay with an "extends" property');
234+
}
235+
return;
236+
}
237+
}
238+
219239
let resObj = {};
220240
let output = {};
221241
let input = {};
222242
let fileOptions = {keepComments: options.keepComments ?? false, bundle: options.bundle ?? true};
223243

224244
try {
225-
infoOut(`- Input file:\t\t${oaFile}`); // LOG - Input file
245+
if (!options?.overlaySet?.extends) {
246+
infoOut(`- Input file:\t\t${oaFile}`); // LOG - Input file (standard)
247+
}
226248

227249
// Parse input content
228250
resObj = await openapiFormat.parseFile(oaFile, fileOptions);
229251
input = resObj;
230252
} catch (err) {
231-
if (err.code !== 'ENOENT') {
232-
console.error('\x1b[31m', `Input file error - Failed to read file: ${err.message}`);
253+
// If input file missing but overlay extends is present, fallback to extends
254+
const extendsRef = options?.overlaySet?.extends;
255+
if (err.code === 'ENOENT' && extendsRef) {
256+
const isRemote =
257+
typeof extendsRef === 'string' && (extendsRef.startsWith('http://') || extendsRef.startsWith('https://'));
258+
if (isRemote) {
259+
oaFile = extendsRef;
260+
} else {
261+
const baseDir = options?.overlayFile ? path.dirname(path.resolve(options.overlayFile)) : process.cwd();
262+
oaFile = path.isAbsolute(extendsRef) ? extendsRef : path.resolve(baseDir, extendsRef);
263+
}
264+
infoOut(`- Input file (extends):\t${oaFile}`);
265+
try {
266+
resObj = await openapiFormat.parseFile(oaFile, fileOptions);
267+
input = resObj;
268+
} catch (err2) {
269+
console.error('\x1b[31m', `Input file error - Failed to read file: ${err2.message}`);
270+
process.exit(1);
271+
}
272+
} else {
273+
if (err.code !== 'ENOENT') {
274+
console.error('\x1b[31m', `Input file error - Failed to read file: ${err.message}`);
275+
process.exit(1);
276+
}
277+
console.error('\x1b[31m', `Input file error - Failed to read file: ${oaFile}`);
233278
process.exit(1);
234279
}
235-
console.error('\x1b[31m', `Input file error - Failed to read file: ${oaFile}`);
236-
process.exit(1);
237280
}
238281

239282
// Generate elements for OpenAPI document

readme.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1217,6 +1217,27 @@ example:
12171217
$ openapi-format openapi.yaml --overlayFile overlay.yaml -o openapi-updated.yaml
12181218
```
12191219

1220+
You can also let the overlay declare the base OpenAPI document using the top-level `extends` property. When `extends` is present, the input file argument becomes optional:
1221+
1222+
```yaml
1223+
overlay: 1.0.0
1224+
info:
1225+
title: Overlay for Tic Tac Toe
1226+
version: 1.0.0
1227+
extends: 'https://raw.githubusercontent.com/OAI/learn.openapis.org/refs/heads/main/examples/v3.1/tictactoe.yaml'
1228+
# actions: [...] # optional
1229+
```
1230+
1231+
CLI usage with `extends` (no input file argument):
1232+
1233+
```shell
1234+
$ openapi-format --overlayFile overlay.yaml -o openapi-updated.yaml
1235+
```
1236+
1237+
Notes:
1238+
- `extends` supports both local paths and remote `http(s)` URLs.
1239+
- Local relative paths are resolved relative to the overlay file’s location.
1240+
12201241
## CLI generate usage
12211242

12221243
- Generate OpenAPI elements and saves it as a new file
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
openapi: 3.1.0
2+
info:
3+
title: Base Local
4+
version: 1.0.0
5+
paths: {}
6+
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
overlayFile: overlay.yaml
2+
output: output.yaml
3+
no-sort: true
4+
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
openapi: 3.1.0
2+
info:
3+
title: Base Local
4+
version: 1.0.0
5+
paths: {}
6+
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
overlay: 1.0.0
2+
info:
3+
title: Local Extends Overlay
4+
version: 1.0.0
5+
extends: './base.yaml'
6+
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
overlayFile: overlay.yaml
2+
output: output.yaml
3+
no-sort: true
4+
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
openapi: 3.0.2
2+
info:
3+
title: Original API
4+
version: 1.0.0
5+
servers:
6+
- url: 'https://api.example.com'
7+
description: Default server
8+
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
overlay: 1.0.0
2+
info:
3+
title: Remote Extends Overlay
4+
version: 1.0.0
5+
extends: 'https://raw.githubusercontent.com/thim81/openapi-format/main/test/overlay-combi/input.yaml'

0 commit comments

Comments
 (0)