Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bin/__snapshots__/cli.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ Options:
-c, --configFile <configFile> the file with the OpenAPI-format CLI options
--no-sort don't sort the OpenAPI file
--keepComments don't remove the comments from the OpenAPI YAML file (default: false)
--yamlQuoteStyle <yamlQuoteStyle> YAML quote style: single, double, or detect
--sortComponentsFile <sortComponentsFile> the file with components to sort alphabetically
--sortComponentsProps sort properties within schema components alphabetically (default: false)
--lineWidth <lineWidth> max line width of YAML output (default: -1)
Expand Down
9 changes: 8 additions & 1 deletion bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ program
.option('-c, --configFile <configFile>', 'the file with the OpenAPI-format CLI options')
.option('--no-sort', `don't sort the OpenAPI file`)
.option('--keepComments', `don't remove the comments from the OpenAPI YAML file`, false)
.option('--yamlQuoteStyle <yamlQuoteStyle>', 'YAML quote style: single, double, or detect')
.option('--sortComponentsFile <sortComponentsFile>', 'the file with components to sort alphabetically')
.option('--sortComponentsProps', 'sort properties within schema components alphabetically', false)
.option('--lineWidth <lineWidth>', 'max line width of YAML output', -1)
Expand Down Expand Up @@ -245,7 +246,11 @@ async function run(oaFile, options) {
let resObj = {};
let output = {};
let input = {};
let fileOptions = {keepComments: options.keepComments ?? false, bundle: options.bundle ?? true};
let fileOptions = {
keepComments: options.keepComments ?? false,
bundle: options.bundle ?? true,
yamlQuoteStyle: options.yamlQuoteStyle
};

try {
if (!options?.overlaySet?.extends) {
Expand Down Expand Up @@ -347,6 +352,8 @@ async function run(oaFile, options) {

options.yamlComments = fileOptions.yamlComments || {};
options.yamlValueFormats = fileOptions.yamlValueFormats || {};
options.detectedYamlQuoteStyle = fileOptions.detectedYamlQuoteStyle;
options.detectedYamlQuoteStyleHasQuotedScalars = fileOptions.detectedYamlQuoteStyleHasQuotedScalars;
if (options.output) {
if (options.split !== true) {
try {
Expand Down
130 changes: 130 additions & 0 deletions bin/cli.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,136 @@ describe('openapi-format CLI command', () => {
expect(output).toStrictEqual(snap);
});

it('should detect YAML quote style by default from config-driven output', async () => {
const testName = 'yaml-quote-style-detect';
const testPath = `test/${testName}`;
const expectedOutputFile = `${testPath}/output.yaml`;
const tempOutputFile = path.join(os.tmpdir(), `openapi-format-${testName}-output.yaml`);
const expectedOutput = fs.readFileSync(expectedOutputFile, 'utf8');

const result = await testUtils.cli(
['input.yaml', '--configFile options.yaml', `--output ${tempOutputFile}`],
testPath
);

expect(result.code).toBe(0);
expect(result.stdout).toContain('formatted successfully');
expect(fs.readFileSync(tempOutputFile, 'utf8')).toBe(expectedOutput);
});

it('should apply yamlQuoteStyle from config files', async () => {
const testName = 'yaml-quote-style-config-double';
const testPath = `test/${testName}`;
const expectedOutputFile = `${testPath}/output.yaml`;
const tempOutputFile = path.join(os.tmpdir(), `openapi-format-${testName}-output.yaml`);
const expectedOutput = fs.readFileSync(expectedOutputFile, 'utf8');

const result = await testUtils.cli(
['input.yaml', '--configFile options.yaml', `--output ${tempOutputFile}`],
testPath
);

expect(result.code).toBe(0);
expect(result.stdout).toContain('formatted successfully');
expect(fs.readFileSync(tempOutputFile, 'utf8')).toBe(expectedOutput);
});

it('should let the CLI yamlQuoteStyle flag override config file values', async () => {
const testName = 'yaml-quote-style-config-double';
const testPath = `test/${testName}`;
const tempOutputFile = path.join(os.tmpdir(), `openapi-format-${testName}-override-output.yaml`);

const result = await testUtils.cli(
['input.yaml', '--configFile options.yaml', '--yamlQuoteStyle single', `--output ${tempOutputFile}`],
testPath
);

expect(result.code).toBe(0);
expect(result.stdout).toContain('formatted successfully');
expect(fs.readFileSync(tempOutputFile, 'utf8')).toContain("description: 'Hello: world'");
expect(fs.readFileSync(tempOutputFile, 'utf8')).not.toContain('description: "Hello: world"');
});

it('should not force quotes onto plain YAML strings for explicit yamlQuoteStyle values', async () => {
const testName = 'yaml-quote-style-config-double';
const testPath = `test/${testName}`;
const tempOutputFile = path.join(os.tmpdir(), `openapi-format-${testName}-plain-output.yaml`);

const result = await testUtils.cli(
['input.yaml', '--configFile options.yaml', `--output ${tempOutputFile}`],
testPath
);

const output = fs.readFileSync(tempOutputFile, 'utf8');
expect(result.code).toBe(0);
expect(result.stdout).toContain('formatted successfully');
expect(output).toContain('name: John');
expect(output).not.toContain('name: "John"');
expect(output).not.toContain("name: 'John'");
});

it('should keep comments for YAML with explicit quote style', async () => {
const tempOutputFile = path.join(os.tmpdir(), 'openapi-format-yaml-quote-style-comments-output.yaml');
const result = await testUtils.cli(
[
'input.yaml',
'--filterFile customFilter.yaml',
'--configFile options.yaml',
'--keepComments',
'--yamlQuoteStyle double',
`--output ${tempOutputFile}`
],
'test/yaml-no-sort-keep-comments'
);

const output = fs.readFileSync(tempOutputFile, 'utf8');
expect(result.code).toBe(0);
expect(result.stdout).toContain('formatted successfully');
expect(output).toContain('#');
expect(output).toContain('"');
});

it('should apply detected quote style uniformly to required quoted keys and values', async () => {
const path = 'test/yaml-preserve-example-props';
const inputFile = `${path}/input.yaml`;
const outputFile = `${path}/output.yaml`;
const output = await getLocalFile(outputFile);

const result = await testUtils.cli([inputFile, '--no-sort'], '.');
expect(result.code).toBe(0);
expect(result.stdout).toContain('formatted successfully');
expect(sanitize(result.stderr)).toStrictEqual(sanitize(output));
});

it('should preserve detected root quote style when bundling refs', async () => {
const testName = 'yaml-quote-style-detect-bundle';
const testPath = `test/${testName}`;
const expectedOutputFile = `${testPath}/output.yaml`;
const tempOutputFile = path.join(os.tmpdir(), `openapi-format-${testName}-output.yaml`);
const expectedOutput = fs.readFileSync(expectedOutputFile, 'utf8');

const result = await testUtils.cli(
['input.yaml', '--configFile options.yaml', `--output ${tempOutputFile}`],
testPath
);

expect(result.code).toBe(0);
expect(result.stdout).toContain('formatted successfully');
expect(fs.readFileSync(tempOutputFile, 'utf8')).toBe(expectedOutput);
});

it('should keep JSON output unaffected by yamlQuoteStyle CLI option', async () => {
const path = `test/json-default`;
const inputFile = `${path}/input.json`;
const outputFile = `${path}/output.json`;
const output = await getLocalFile(outputFile);

let result = await testUtils.cli([inputFile, '--json', '--yamlQuoteStyle double'], '.');
expect(result.code).toBe(0);
expect(result.stdout).toContain('formatted successfully');
expect(sanitize(result.stderr)).toStrictEqual(sanitize(output));
});

it('should load the default .openapiformatrc if configFile is not provided', async () => {
// Mock the existence of the .openapiformatrc file
const defaultConfigPath = '.openapiformatrc';
Expand Down
2 changes: 2 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ Options:

--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]

Expand Down Expand Up @@ -204,6 +205,7 @@ Options:
| --overlayFile | -l | the file to specify OpenAPI overlay actions | path to file | | optional |
| --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 |
| --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 |
Expand Down
158 changes: 158 additions & 0 deletions test/util-file.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,123 @@ describe('openapi-format CLI file tests', () => {
const expectedJSON = JSON.stringify(obj, null, 2);
expect(result).toEqual(expectedJSON);
});

test('should not add quotes to plain YAML strings when yamlQuoteStyle is single', async () => {
const obj = {name: 'John'};

const result = await stringify(obj, {format: 'yaml', yamlQuoteStyle: 'single'});

expect(result).toContain('name: John');
expect(result).not.toContain("name: 'John'");
});

test('should not add quotes to plain YAML strings when yamlQuoteStyle is double', async () => {
const obj = {name: 'John'};

const result = await stringify(obj, {format: 'yaml', yamlQuoteStyle: 'double'});

expect(result).toContain('name: John');
expect(result).not.toContain('name: "John"');
});

test('should use single quotes when YAML quoting is required and yamlQuoteStyle is single', async () => {
const obj = {name: 'Hello: world'};

const result = await stringify(obj, {format: 'yaml', yamlQuoteStyle: 'single'});

expect(result).toContain("name: 'Hello: world'");
});

test('should use double quotes when YAML quoting is required and yamlQuoteStyle is double', async () => {
const obj = {name: 'Hello: world'};

const result = await stringify(obj, {format: 'yaml', yamlQuoteStyle: 'double'});

expect(result).toContain('name: "Hello: world"');
});

test('should use detected quote style when YAML quoting is required', async () => {
const obj = {name: 'Hello: world'};

const result = await stringify(obj, {
format: 'yaml',
yamlQuoteStyle: 'detect',
detectedYamlQuoteStyle: 'double',
detectedYamlQuoteStyleHasQuotedScalars: true
});

expect(result).toContain('name: "Hello: world"');
});

test('should use detected quote style for keys when YAML key quoting is required', async () => {
const obj = {responses: {'200': {description: 'ok'}}};

const result = await stringify(obj, {
format: 'yaml',
yamlQuoteStyle: 'detect',
detectedYamlQuoteStyle: 'single',
detectedYamlQuoteStyleHasQuotedScalars: true
});

expect(result).toContain(" '200':");
expect(result).not.toContain(' "200":');
});

test('should not add quotes in detect mode when YAML quoting is not required', async () => {
const obj = {name: 'John'};

const result = await stringify(obj, {
format: 'yaml',
yamlQuoteStyle: 'detect',
detectedYamlQuoteStyle: 'double',
detectedYamlQuoteStyleHasQuotedScalars: true
});

expect(result).toContain('name: John');
expect(result).not.toContain('name: "John"');
});

test('should preserve comments while honoring the resolved quote style', async () => {
const parsed = yaml.parseDocument('name: "Hello: world" # person\ncity: London\n');
const options = {
format: 'yaml',
yamlQuoteStyle: 'double',
keepComments: true,
yamlComments: [
{
path: ['name'],
type: 'inlineValue',
text: ' person'
}
]
};

const result = await stringify(parsed.toJS(), options);

expect(result).toContain('name: "Hello: world" # person');
});

test('should preserve YAML value formats while honoring the resolved quote style', async () => {
const input = yaml.parseDocument('name: "Hello: world"\nx-version: 1.00\n');
const options = {
format: 'yaml',
yamlQuoteStyle: 'double',
yamlValueFormats: extractYamlValueFormats(input)
};

const result = await stringify(input.toJS({keepScalar: false}), options);

expect(result).toContain('name: "Hello: world"');
expect(result).toContain('x-version: 1.00');
});

test('should leave JSON output unchanged when yamlQuoteStyle is set', async () => {
const obj = {name: 'John'};

const result = await stringify(obj, {format: 'json', yamlQuoteStyle: 'double'});

expect(result).toEqual(JSON.stringify(obj, null, 2));
});
});

describe('parseString', () => {
Expand All @@ -193,6 +310,47 @@ describe('openapi-format CLI file tests', () => {
expect(result).toEqual({name: 'John', age: 30});
});

it('should detect dominant double quotes from YAML input', async () => {
const yamlString = 'name: "John"\ncity: "London"\ncountry: \'UK\'\n';
const options = {yamlQuoteStyle: 'detect'};

const result = await parseString(yamlString, options);

expect(result).toEqual({name: 'John', city: 'London', country: 'UK'});
expect(options.detectedYamlQuoteStyle).toBe('double');
});

it('should detect quote style from quoted keys when they dominate', async () => {
const yamlString = 'responses:\n "200": ok\n "404": nope\nmeta: \'value\'\n';
const options = {yamlQuoteStyle: 'detect'};

const result = await parseString(yamlString, options);

expect(result).toEqual({
responses: {'200': 'ok', '404': 'nope'},
meta: 'value'
});
expect(options.detectedYamlQuoteStyle).toBe('double');
});

it('should resolve tied YAML quote detection to single', async () => {
const yamlString = 'name: "John"\ncity: \'London\'\n';
const options = {yamlQuoteStyle: 'detect'};

await parseString(yamlString, options);

expect(options.detectedYamlQuoteStyle).toBe('single');
});

it('should resolve YAML quote detection without quoted scalars to single', async () => {
const yamlString = 'name: John\ncity: London\n';
const options = {yamlQuoteStyle: 'detect'};

await parseString(yamlString, options);

expect(options.detectedYamlQuoteStyle).toBe('single');
});

it('should return YAML parsing error if YAML parsing fail', async () => {
const invalidString = '#name 1John\nage 30#'; // Invalid YAML
const result = await parseString(invalidString, {format: 'yaml'});
Expand Down
Loading
Loading