Skip to content
Open
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
34 changes: 17 additions & 17 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,24 +44,24 @@
"license": "ISC",
"homepage": "https://github.com/apiture/openapi-down-convert#readme",
"dependencies": {
"commander": "^9.4.1",
"js-yaml": "^4.1.0",
"typescript": "^4.8.4"
"commander": "9.4.1",
"js-yaml": "4.1.0",
"typescript": "5.5.3"
},
"devDependencies": {
"@jest/globals": "^29.2.2",
"@types/commander": "^2.12.2",
"@typescript-eslint/eslint-plugin": "^5.41.0",
"@typescript-eslint/parser": "^5.41.0",
"eslint": "^8.26.0",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-jsx-a11y": "^6.6.1",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.31.10",
"jest": "^27.5.1",
"pre-push": "^0.1.4",
"prettier": "^2.7.1",
"rimraf": "^4.1.2",
"ts-jest": "^27.1.3"
"@jest/globals": "29.2.2",
"@types/commander": "2.12.2",
"@typescript-eslint/eslint-plugin": "5.41.0",
"@typescript-eslint/parser": "5.41.0",
"eslint": "8.26.0",
"eslint-plugin-import": "2.25.4",
"eslint-plugin-jsx-a11y": "6.6.1",
"eslint-plugin-prettier": "4.2.1",
"eslint-plugin-react": "7.31.10",
"jest": "27.5.1",
"pre-push": "0.1.4",
"prettier": "2.7.1",
"rimraf": "4.1.2",
"ts-jest": "27.1.3"
}
}
17 changes: 17 additions & 0 deletions src/RefVisitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,20 @@ export function walkObject(node: object, objectCallback: ObjectVisitor): JsonNod
return array;
}
}

/**
* Loads the schema/component located at $ref
*/
export function getRefSchema(node: object, ref: RefObject) {
const propertyName = ref.$ref.split('/').reverse()[0];
if (node.hasOwnProperty('components')) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not a safe assumption; i.e. the $ref could be a reference to a schema in another file, such as
$ref: '../components.yaml/#/components/schemas/foo' in which case fetching schemas[propertyName] may fail or may fetch the wrong schema.
This tool does not resolve external references; see the README which says
"The tool only supports self-contained documents. It does not follow or resolve external $ref documents embedded in the source document."
See https://github.com/apiture/api-ref-resolver for tooling to supports resolving external $ref references before down converting. (However this tool should not assume that all external $ref have been resolved)

So instead, I suggest either checking that the $ref starts with '#/components/schemasor after splitting on '/' thatcomponents[0] === '#' && components[1] === 'components' && components[2] === 'schemas'`

const components = node['components'];
if (components != null && typeof components === 'object' && components.hasOwnProperty('schemas')) {
const schemas = components['schemas'];
if (schemas.hasOwnProperty(propertyName)) {
return schemas[propertyName];
}
}
}
return null;
}
75 changes: 75 additions & 0 deletions src/converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
JsonNode,
RefObject,
SchemaObject,
isRef,
getRefSchema,
} from './RefVisitor';

/** Lightweight OAS document top-level fields */
Expand Down Expand Up @@ -144,6 +146,7 @@ export class Converter {
this.convertJsonSchemaContentMediaType();
this.convertConstToEnum();
this.convertNullableTypeArray();
this.convertMergedNullableType();
this.removeWebhooksObject();
this.removeUnsupportedSchemaKeywords();
if (this.convertSchemaComments) {
Expand Down Expand Up @@ -244,6 +247,78 @@ export class Converter {
visitSchemaObjects(this.openapi30, schemaVisitor);
}

/**
* Converts `type: null` merged with other types via anyOf/oneOf to `nullable: true`
*/
convertMergedNullableType() {
const schemaVisitor: SchemaVisitor = (schema: SchemaObject): SchemaObject => {
const nullableOf = ['anyOf', 'oneOf'] as const;

nullableOf.forEach((of) => {
if (!schema[of]) {
return;
}

const entries = schema[of];

if (!Array.isArray(entries)) {
return;
}

const typeNullIndex = entries.findIndex((v) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this would be cleaner with

const isNullable = entries.find( (v) => .... );

if (!v) {
return false;
}
return v.hasOwnProperty('type') && v['type'] === 'null';
});

let isNullable = typeNullIndex > -1;

// Get the main type of the root object. nullable can't exist with the type property in 3.0.3.
const mainType = entries.reduce((acc, cur) => {
const sub = isRef(cur) ? getRefSchema(this.openapi30, cur) : cur;

if (sub['type']) {
// if our sub-type...type has null in it, it's still nullable
if (Array.isArray(sub['type'])) {
if (sub['type'].includes('null')) {
isNullable = true;
}
// return the first non-null type. multiple unrelated types aren't
// currently supported.
return sub['type'].filter((_) => _ !== 'null')[0];
}
// only set non null types
if (sub['type'] !== 'null') {
return sub['type'];
}
}
return acc;
}, null);

if (isNullable) {
// has a type: null entry in the array
schema['nullable'] = true;
schema['type'] = mainType;
if (typeNullIndex > -1) {
schema[of].splice(typeNullIndex, 1);
}

if (entries.length === 1) {
// if only one entry, anyOf/oneOf probably shouldn't be used.
// Instead, convert to allOf with nullable & ref
schema['allOf'] = [schema[of][0]];

delete schema[of];
}
}
});

return this.walkNestedSchemaObjects(schema, schemaVisitor);
};
visitSchemaObjects(this.openapi30, schemaVisitor);
}

removeWebhooksObject() {
if (Object.hasOwnProperty.call(this.openapi30, 'webhooks')) {
this.log(`Deleted webhooks object`);
Expand Down
Loading