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
27 changes: 13 additions & 14 deletions src/utils/schema-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}

Expand Down Expand Up @@ -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;
}
Expand All @@ -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',
};
Expand All @@ -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)) {
Expand All @@ -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
};
}
Expand Down
62 changes: 61 additions & 1 deletion tests/schema-utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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();
});
Loading