diff --git a/CHANGELOG.md b/CHANGELOG.md index a7ad910..32b49b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## unreleased +- CLI: add yamlCompat option for YAML 1.1 + ## [1.33.3] - 2026-06-18 - Sort: Keep stable sorting for trailing-slash paths (#228) @@ -24,7 +26,7 @@ ## [1.32.0] - 2026-05-23 -- Casing - Configure characters to keep +- Casing: Configure characters to keep - CLI: Fix YAML output to preserve x-version number formatting ## [1.31.0] - 2026-04-12 diff --git a/bin/__snapshots__/cli.test.js.snap b/bin/__snapshots__/cli.test.js.snap index 0d3a635..dd41ab6 100644 --- a/bin/__snapshots__/cli.test.js.snap +++ b/bin/__snapshots__/cli.test.js.snap @@ -117,6 +117,7 @@ Options: --no-sort don't sort the OpenAPI file --keepComments don't remove the comments from the OpenAPI YAML file (default: false) --yamlQuoteStyle YAML quote style: single, double, or detect + --yamlCompat YAML compatibility schema for older OpenAPI tools (yaml-1.1) --sortComponentsFile the file with components to sort alphabetically --sortComponentsProps sort properties within schema components alphabetically (default: false) --lineWidth max line width of YAML output (default: -1) diff --git a/bin/cli.js b/bin/cli.js index 73e8652..a33cbb8 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -27,6 +27,7 @@ program .option('--no-sort', `don't sort the OpenAPI file`) .option('--keepComments', `don't remove the comments from the OpenAPI YAML file`, false) .option('--yamlQuoteStyle ', 'YAML quote style: single, double, or detect') + .option('--yamlCompat ', 'YAML compatibility schema for older OpenAPI tools (yaml-1.1)') .option('--sortComponentsFile ', 'the file with components to sort alphabetically') .option('--sortComponentsProps', 'sort properties within schema components alphabetically', false) .option('--lineWidth ', 'max line width of YAML output', -1) diff --git a/bin/cli.test.js b/bin/cli.test.js index 0e31443..4a33846 100644 --- a/bin/cli.test.js +++ b/bin/cli.test.js @@ -246,6 +246,34 @@ describe('openapi-format CLI command', () => { expect(sanitize(result.stderr)).toStrictEqual(sanitize(output)); }); + it('should apply yamlCompat from config files', async () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'openapi-format-yaml-compat-')); + const inputFile = path.join(tempDir, 'input.yaml'); + const configFile = path.join(tempDir, 'options.json'); + const outputFile = path.join(tempDir, 'output.yaml'); + + fs.writeFileSync( + inputFile, + `openapi: 3.0.3\ninfo:\n title: t\n version: 1.0.0\npaths: {}\ncomponents:\n schemas:\n CountryCode:\n type: string\n enum:\n - ca\n - "no"\n - us\n Feature:\n type: object\n properties:\n status:\n type: string\n enum:\n - "yes"\n - "no"\n - "on"\n - "off"\n example:\n status: "on"\n` + ); + fs.writeFileSync(configFile, JSON.stringify({yamlCompat: 'yaml-1.1', sort: false}, null, 2)); + + const result = await testUtils.cli(['input.yaml', '--configFile options.json', `--output ${outputFile}`], tempDir); + + expect(result.code).toBe(0); + expect(result.stdout).toContain('formatted successfully'); + const output = fs.readFileSync(outputFile, 'utf8'); + expect(output).toContain('CountryCode:'); + expect(output).toContain('Feature:'); + expect(output).toContain('- ca'); + expect(output).toContain('- us'); + expect(output).toContain('- "no"'); + expect(output).toContain('- "yes"'); + expect(output).toContain('- "on"'); + expect(output).toContain('- "off"'); + expect(output).toContain('status: "on"'); + }); + it('should load the default .openapiformatrc if configFile is not provided', async () => { // Mock the existence of the .openapiformatrc file const defaultConfigPath = '.openapiformatrc'; diff --git a/readme.md b/readme.md index 867ae61..f01be3c 100644 --- a/readme.md +++ b/readme.md @@ -158,38 +158,39 @@ Arguments: Options: - --output, -o Save the formatted OpenAPI file as JSON/YAML [path] + --output, -o Save the formatted OpenAPI file as JSON/YAML [path] - --sortFile The file to specify custom OpenAPI fields ordering [path] - --casingFile The file to specify casing rules [path] - --generateFile The file to specify generate rules [path] - --filterFile The file to specify filter rules [path] - --overlayFile The file to specify OpenAPI overlay actions [path] + --sortFile The file to specify custom OpenAPI fields ordering [path] + --casingFile The file to specify casing rules [path] + --generateFile The file to specify generate rules [path] + --filterFile The file to specify filter rules [path] + --overlayFile The file to specify OpenAPI overlay actions [path] - --no-sort Don't sort the OpenAPI file [boolean] - --keepComments Don't remove the comments from the OpenAPI YAML file [boolean] - --yamlQuoteStyle Preferred YAML quote style: single, double, detect [string] - --sortComponentsFile The file with components to sort alphabetically [path] - --sortComponentsProps Sort properties within schema components alphabetically [boolean] + --no-sort Don't sort the OpenAPI file [boolean] + --keepComments Don't remove the comments from the OpenAPI YAML file [boolean] + --yamlQuoteStyle Preferred YAML quote style: single, double, detect [string] + --yamlCompat YAML compatibility schema for older OpenAPI tools [string] + --sortComponentsFile The file with components to sort alphabetically [path] + --sortComponentsProps Sort properties within schema components alphabetically [boolean] - --no-bundle Don't bundle the local and remote $ref [boolean] - --split Split OpenAPI document into a multi-file structure [boolean] + --no-bundle Don't bundle the local and remote $ref [boolean] + --split Split OpenAPI document into a multi-file structure [boolean] - --rename Rename the OpenAPI title [string] + --rename Rename the OpenAPI title [string] - --convertTo convert the OpenAPI document to OpenAPI version 3.1 or 3.2 [string] + --convertTo convert the OpenAPI document to OpenAPI version 3.1 or 3.2 [string] - --configFile The file with the OpenAPI-format CLI options [path] + --configFile The file with the OpenAPI-format CLI options [path] - --lineWidth Max line width of YAML output [number] + --lineWidth Max line width of YAML output [number] - --json Prints the file to stdout as JSON [boolean] - --yaml Prints the file to stdout as YAML [boolean] + --json Prints the file to stdout as JSON [boolean] + --yaml Prints the file to stdout as YAML [boolean] --playground, -p Open config in online playground - --help Show help [boolean] + --help Show help [boolean] --version Output the version number - --verbose Output more details of the filter process [count] + --verbose Output more details of the filter process [count] ``` ## OpenAPI format CLI options @@ -206,6 +207,7 @@ Options: | --no-sort | | don't sort the OpenAPI file | boolean | FALSE | optional | | --keepComments | | don't remove the comments from the OpenAPI YAML file | boolean | FALSE | optional | | --yamlQuoteStyle | | preferred YAML quote style for YAML output (`single`, `double`, `detect`) | string | detect | optional | +| --yamlCompat | | YAML compatibility schema for older OpenAPI tools (`yaml-1.1`) | string | | optional | | --sortComponentsFile | | sort the items of the components (schemas, parameters, ...) by alphabet | path to file | defaultSortComponents.json | optional | | --sortComponentsProps | | sort properties within schema components alphabetically | boolean | FALSE | optional | | --no-bundle | | don't bundle the local and remote $ref in the OpenAPI document | boolean | FALSE | optional | diff --git a/schemas/openapi-format.schema.json b/schemas/openapi-format.schema.json index bd9ea63..9c6a962 100644 --- a/schemas/openapi-format.schema.json +++ b/schemas/openapi-format.schema.json @@ -418,6 +418,11 @@ "enum": ["single", "double", "detect"], "default": "detect" }, + "yamlCompat": { + "type": "string", + "description": "Compatibility schema to use when stringifying YAML. Enables YAML 1.1-safe quoting for compatibility with older parsers.", + "enum": ["yaml-1.1"] + }, "lineWidth": { "type": "integer", "description": "Maximum line width of YAML output. Use -1 for unlimited (default).", diff --git a/test/util-file.test.js b/test/util-file.test.js index 85a0070..7322551 100644 --- a/test/util-file.test.js +++ b/test/util-file.test.js @@ -248,6 +248,90 @@ describe('openapi-format CLI file tests', () => { expect(result).not.toContain('name: "John"'); }); + test('should leave YAML 1.2 boolean-like strings unquoted by default', async () => { + const obj = { + components: { + schemas: { + CountryCode: { + type: 'string', + enum: ['ca', 'no', 'us'] + }, + Feature: { + type: 'object', + properties: { + status: { + type: 'string', + enum: ['yes', 'no', 'on', 'off'] + } + }, + example: { + status: 'on' + } + } + } + } + }; + + const result = await stringify(obj, { + format: 'yaml' + }); + + expect(result).toContain('CountryCode:'); + expect(result).toContain('enum:'); + expect(result).toContain('- ca'); + expect(result).toContain('- no'); + expect(result).toContain('- us'); + expect(result).toContain('Feature:'); + expect(result).toContain('- yes'); + expect(result).toContain('- on'); + expect(result).toContain('- off'); + expect(result).toContain('status: on'); + }); + + test('should quote YAML 1.1 boolean-like strings when yamlCompat is yaml-1.1', async () => { + const obj = { + components: { + schemas: { + CountryCode: { + type: 'string', + enum: ['ca', 'no', 'us'] + }, + Feature: { + type: 'object', + properties: { + status: { + type: 'string', + enum: ['yes', 'no', 'on', 'off'] + } + }, + example: { + status: 'on' + } + } + } + } + }; + + const result = await stringify(obj, { + format: 'yaml', + yamlCompat: 'yaml-1.1' + }); + + expect(result).toContain('CountryCode:'); + expect(result).toContain('- ca'); + expect(result).toContain("- 'no'"); + expect(result).toContain('- us'); + expect(result).toContain('Feature:'); + expect(result).toContain("- 'yes'"); + expect(result).toContain("- 'on'"); + expect(result).toContain("- 'off'"); + expect(result).toContain("status: 'on'"); + expect(result).not.toContain('- yes'); + expect(result).not.toContain('- no'); + expect(result).not.toContain('- on'); + expect(result).not.toContain('- off'); + }); + test('should preserve comments while honoring the resolved quote style', async () => { const parsed = yaml.parseDocument('name: "Hello: world" # person\ncity: London\n'); const options = { @@ -289,6 +373,14 @@ describe('openapi-format CLI file tests', () => { expect(result).toEqual(JSON.stringify(obj, null, 2)); }); + + test('should leave JSON output unchanged when yamlCompat is set', async () => { + const obj = {name: 'John'}; + + const result = await stringify(obj, {format: 'json', yamlCompat: 'yaml-1.1'}); + + expect(result).toEqual(JSON.stringify(obj, null, 2)); + }); }); describe('parseString', () => { diff --git a/test/yaml-compat/input.yaml b/test/yaml-compat/input.yaml new file mode 100644 index 0000000..f402626 --- /dev/null +++ b/test/yaml-compat/input.yaml @@ -0,0 +1,25 @@ +openapi: 3.0.3 +info: + title: t + version: 1.0.0 +paths: {} +components: + schemas: + CountryCode: + type: string + enum: + - ca + - "no" + - us + Feature: + type: object + properties: + status: + type: string + enum: + - "yes" + - "no" + - "on" + - "off" + example: + status: "on" diff --git a/test/yaml-compat/options.yaml b/test/yaml-compat/options.yaml new file mode 100644 index 0000000..3681e57 --- /dev/null +++ b/test/yaml-compat/options.yaml @@ -0,0 +1,4 @@ +verbose: true +no-sort: true +output: output.yaml +yamlCompat: yaml-1.1 diff --git a/test/yaml-compat/output.yaml b/test/yaml-compat/output.yaml new file mode 100644 index 0000000..f402626 --- /dev/null +++ b/test/yaml-compat/output.yaml @@ -0,0 +1,25 @@ +openapi: 3.0.3 +info: + title: t + version: 1.0.0 +paths: {} +components: + schemas: + CountryCode: + type: string + enum: + - ca + - "no" + - us + Feature: + type: object + properties: + status: + type: string + enum: + - "yes" + - "no" + - "on" + - "off" + example: + status: "on" diff --git a/types/openapi-format.d.ts b/types/openapi-format.d.ts index 1fa8d00..4780483 100644 --- a/types/openapi-format.d.ts +++ b/types/openapi-format.d.ts @@ -184,6 +184,7 @@ declare module 'openapi-format' { json?: boolean; keepComments?: boolean; yamlQuoteStyle?: 'single' | 'double' | 'detect'; + yamlCompat?: 'yaml-1.1'; yamlComments?: Record; lineWidth?: string | number; mode?: string; diff --git a/utils/file.js b/utils/file.js index 2433fc7..6fb5f27 100644 --- a/utils/file.js +++ b/utils/file.js @@ -169,14 +169,20 @@ function resolveYamlQuoteStyle(options = {}) { * Build YAML stringify/toString options, including quote-style control. * @param {number} lineWidth * @param {object} [options] - * @returns {{lineWidth: number, singleQuote: boolean}} + * @returns {{lineWidth: number, singleQuote: boolean, compat?: 'yaml-1.1'}} */ function buildYamlStringifyOptions(lineWidth, options = {}) { const style = resolveYamlQuoteStyle(options); - return { + const yamlStringifyOptions = { lineWidth, singleQuote: style !== YAML_QUOTE_STYLE.DOUBLE }; + + if (options.yamlCompat === 'yaml-1.1') { + yamlStringifyOptions.compat = 'yaml-1.1'; + } + + return yamlStringifyOptions; } /**