diff --git a/src/utils/schema-utils.js b/src/utils/schema-utils.js index a4cd21da..b98e264a 100644 --- a/src/utils/schema-utils.js +++ b/src/utils/schema-utils.js @@ -61,7 +61,7 @@ export function getTypeInfo(parameter, options = { includeNulls: false, enableEx info.allowedValues = typeof schema.const !== 'undefined' && [schema.const] || schema.items.enum || null; } - if (schema.uniqueItems) { + if (schema.uniqueItems && dataType.match(/array|\[\]/)) { info.constraints.push('Requires unique items'); } @@ -387,6 +387,7 @@ export function schemaInObjectNotation(rawSchema, options, level = 0, suffix = ' resultObj['::flags'] = { '🆁': readOnly && '🆁', '🆆': writeOnly && '🆆' }; resultObj['::title'] = schema.title || ''; resultObj['::description'] = schema.description || ''; + resultObj['::deprecated'] = schema.deprecated || false; resultObj['::metadata'] = metadata; return resultObj; } @@ -412,18 +413,23 @@ export function schemaInObjectNotation(rawSchema, options, level = 0, suffix = ' complexTypes.push(v); } }); + // Rendered as one option of several: title, description and deprecation belong to the property itself + const asPrimitiveOption = (type) => getTypeInfo({ ...subSchema, type, title: undefined, description: undefined, deprecated: undefined }, options); let multiPrimitiveTypes; if (primitiveType.length > 0) { subSchema.type = primitiveType.join('┃'); - multiPrimitiveTypes = getTypeInfo(subSchema, options); if (complexTypes.length === 0) { - return `${multiPrimitiveTypes?.html || ''}`; + return `${getTypeInfo(subSchema, options)?.html || ''}`; } + multiPrimitiveTypes = asPrimitiveOption(subSchema.type); } if (complexTypes.length > 0) { obj['::link'] = schema.title || ''; obj['::circular'] = !!schema.circularReference; obj['::type'] = 'object'; + obj['::title'] = schema.title || ''; + obj['::description'] = schema.description || ''; + obj['::deprecated'] = schema.deprecated || false; const multiTypeOptions = { '::type': 'xxx-of-option', }; @@ -433,20 +439,15 @@ export function schemaInObjectNotation(rawSchema, options, level = 0, suffix = ' if (v === 'null') { multiTypeOptions[`::OPTION~${i + 1}`] = 'NULL~|~~|~~|~~|~~|~~|~~|~~|~'; } else if ('integer, number, string, boolean,'.includes(`${v},`)) { - subSchema.type = Array.isArray(v) ? v.join('┃') : v; - const primitiveTypeInfo = getTypeInfo(subSchema, options); + const primitiveTypeInfo = asPrimitiveOption(Array.isArray(v) ? v.join('┃') : v); multiTypeOptions[`::OPTION~${i + 1}`] = primitiveTypeInfo.html; } else if (v === 'object') { // If object type iterate all the properties and create an object-type-option const objTypeOption = { - '::title': schema.title || '', - '::description': schema.description || '', '::flags': { '🆁': schema.readOnly && '🆁', '🆆': schema.writeOnly && '🆆' }, '::link': schema.title || '', '::circular': !!schema.circularReference, - '::type': 'object', - '::deprecated': schema.deprecated || false, - '::metadata': metadata + '::type': 'object' }; for (const key in schemaProperties) { if (!schema.deprecated && !schemaProperties[key].deprecated && schema.required?.includes(key)) { @@ -465,15 +466,13 @@ export function schemaInObjectNotation(rawSchema, options, level = 0, suffix = ' multiTypeOptions[`::OPTION~${i + 1}`] = objTypeOption; } else if (v === 'array') { multiTypeOptions[`::OPTION~${i + 1}`] = { - '::title': schema.title || '', - '::description': schema.description || arrayItemsSchema?.description || '', + '::description': arrayItemsSchema?.description || '', '::flags': { '🆁': schema.readOnly && '🆁', '🆆': schema.writeOnly && '🆆' }, '::link': arrayItemsSchema.title || schema.title || '', '::circular': !!schema.circularReference, '::type': 'array', // Array properties are read from the ::props object instead of reading from the keys of this object - '::props': schemaInObjectNotation(Object.assign({}, schema, arrayItemsSchema, { description: schema.description || arrayItemsSchema?.description }), options, (level + 1)), - '::deprecated': schema.deprecated || false, + '::props': schemaInObjectNotation(Object.assign({}, schema, { deprecated: undefined }, arrayItemsSchema, { description: arrayItemsSchema?.description }), options, (level + 1)), '::metadata': metadata }; } diff --git a/tests/schema-utils.test.js b/tests/schema-utils.test.js index 00a23849..d1fa5a71 100644 --- a/tests/schema-utils.test.js +++ b/tests/schema-utils.test.js @@ -2,7 +2,7 @@ import test from 'ava'; import { expect } from 'chai'; import xmlFormatter from '../src/utils/xml/xml.js'; -import { getExampleValuesFromSchema, schemaInObjectNotation } from '../src/utils/schema-utils.js'; +import { getExampleValuesFromSchema, getTypeInfo, schemaInObjectNotation } from '../src/utils/schema-utils.js'; test('schema-utils.js getExampleValuesFromSchema', t => { const schema = { @@ -97,3 +97,63 @@ const findPatternOwner = node => { t.pass(); }); }); + +const multiTypeProperty = { + type: ['array', 'object', 'integer', 'null'], + title: 'Tags', + description: 'Property description', + deprecated: true, + uniqueItems: true, + minItems: 1, + minimum: 0, + items: { type: 'object', description: 'Item description', properties: { id: { type: 'integer' } } }, + properties: { name: { type: 'string' } } +}; +const optionsOf = result => Object.entries(Object.entries(result).find(([key]) => key.startsWith('::ONE~OF'))[1]).filter(([key]) => key.startsWith('::OPTION')).map(([, option]) => option); + +test('schema-utils.js schemaInObjectNotation puts title, description and deprecation of a multi-type property on the property', t => { + const result = schemaInObjectNotation(multiTypeProperty, {}); + expect(result['::title']).to.equal('Tags'); + expect(result['::description']).to.equal('Property description'); + expect(result['::deprecated']).to.equal(true); + t.pass(); +}); + +test('schema-utils.js schemaInObjectNotation does not copy the property description or deprecation down to the type options', t => { + const [arrayOption, objectOption, ...primitiveOptions] = optionsOf(schemaInObjectNotation(multiTypeProperty, {})); + expect(arrayOption['::description']).to.equal('Item description'); + expect(arrayOption['::deprecated'] || false).to.equal(false); + expect(objectOption['::description'] || '').to.equal(''); + expect(objectOption['::deprecated'] || false).to.equal(false); + primitiveOptions.filter(o => o.startsWith('{')).map(o => JSON.parse(o)).forEach(option => { + expect(option.schemaDescription).to.equal(''); + expect(option.schemaTitle).to.equal(''); + expect(option.deprecated).to.equal(false); + }); + t.pass(); +}); + +test('schema-utils.js schemaInObjectNotation shows constraints only on the type option they apply to', t => { + const [arrayOption, objectOption, ...primitiveOptions] = optionsOf(schemaInObjectNotation(multiTypeProperty, {})); + expect(arrayOption['::metadata'].constraints).to.include('Requires unique items'); + expect(objectOption['::metadata']?.constraints || []).to.eql([]); + const integerOption = primitiveOptions.filter(o => o.startsWith('{')).map(o => JSON.parse(o)).find(o => o.type.includes('integer')); + expect(integerOption.constraints).to.eql(['Range: [0,)']); + t.pass(); +}); + +test('schema-utils.js schemaInObjectNotation marks a deprecated one-of property as deprecated', t => { + const result = schemaInObjectNotation({ deprecated: true, oneOf: [{ type: 'string' }, { type: 'integer', deprecated: true }] }, {}); + expect(result['::deprecated']).to.equal(true); + const [stringOption, integerOption] = optionsOf(result).map(o => JSON.parse(o)); + expect(stringOption.deprecated).to.equal(false); + expect(integerOption.deprecated).to.equal(true); + t.pass(); +}); + +test('schema-utils.js getTypeInfo shows the unique items constraint only for array types', t => { + expect(getTypeInfo({ type: ['integer', 'string', 'null'], uniqueItems: true }).constraints).to.eql([]); + expect(getTypeInfo({ type: 'array', items: { type: 'string' }, uniqueItems: true }).constraints).to.eql(['Requires unique items']); + expect(getTypeInfo({ type: 'string[]┃null', uniqueItems: true }).constraints).to.eql(['Requires unique items']); + t.pass(); +});