Skip to content

Commit

Permalink
Merge pull request #124 from dtymon/feature/handle-unknown-types
Browse files Browse the repository at this point in the history
Attempt to handle unsupported Joi types rather than omitting them
  • Loading branch information
mrjono1 authored Jul 7, 2021
2 parents 760858f + 0c23b01 commit 9607f78
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 19 deletions.
53 changes: 53 additions & 0 deletions src/__tests__/joiExtensions/joiExtensions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import Joi from 'joi';
import { convertSchema } from '../..';

// Add a couple of extensions to Joi, one without specifying the base type
const ExtendedJoi = Joi.extend(joi => {
const ext: Joi.Extension = {
type: 'objectId',
base: joi.string().meta({ baseType: 'string' })
};
return ext;
}).extend((joi: any) => {
const ext: Joi.Extension = {
type: 'dollars',
base: joi.number()
};
return ext;
});

describe('Joi Extensions', () => {
test('An extended type with baseType set in metadata', () => {
const schema = Joi.object({
doStuff: ExtendedJoi.objectId()
}).meta({ className: 'Test' });

const result = convertSchema({ debug: true }, schema);
expect(result).not.toBeUndefined;
// prettier-ignore
expect(result?.content).toBe(
[
'export interface Test {',
' doStuff?: string;',
'}'
].join('\n')
);
});

test('An extended type with baseType not set in metadata', () => {
const schema = Joi.object({
doStuff: ExtendedJoi.dollars()
}).meta({ className: 'Test' });

const result = convertSchema({ debug: true }, schema);
expect(result).not.toBeUndefined;
// prettier-ignore
expect(result?.content).toBe(
[
'export interface Test {',
' doStuff?: unknown;',
'}'
].join('\n')
);
});
});
44 changes: 32 additions & 12 deletions src/__tests__/joiTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,54 +32,74 @@ describe('`Joi.types()`', () => {
});

test('Joi.function()', () => {
const consoleSpy = jest.spyOn(console, 'debug');
const schema = Joi.object({
doStuff: Joi.function(),
moreThings: Joi.func()
}).meta({ className: 'Test' });

const result = convertSchema({ debug: true }, schema);
expect(result).not.toBeUndefined;
expect(result?.content).toBe(`export interface Test {}`);
expect(consoleSpy).toHaveBeenCalledWith('unsupported type: function');
expect(result?.content).toBe(
[
'export interface Test {',
' doStuff?: (...args: any[]) => any;',
' moreThings?: (...args: any[]) => any;',
'}'
].join('\n')
);
});

// TODO: It might be possible to support link
// I guess this would find the referenced schema and get its type
test('Joi.link()', () => {
const consoleSpy = jest.spyOn(console, 'debug');
const schema = Joi.object({
doStuff: Joi.link()
}).meta({ className: 'Test' });

const result = convertSchema({ debug: true }, schema);
expect(result).not.toBeUndefined;
expect(result?.content).toBe(`export interface Test {}`);
expect(consoleSpy).toHaveBeenCalledWith('unsupported type: link');
// prettier-ignore
expect(result?.content).toBe(
[
'export interface Test {',
' doStuff?: unknown;',
'}'
].join('\n')
);
});

test('Joi.symbol()', () => {
const consoleSpy = jest.spyOn(console, 'debug');
const schema = Joi.object({
doStuff: Joi.symbol()
}).meta({ className: 'Test' });

const result = convertSchema({ debug: true }, schema);
expect(result).not.toBeUndefined;
expect(result?.content).toBe(`export interface Test {}`);
expect(consoleSpy).toHaveBeenCalledWith('unsupported type: symbol');
// prettier-ignore
expect(result?.content).toBe(
[
'export interface Test {',
' doStuff?: symbol;',
'}'
].join('\n')
);
});

// TODO: Support Binary
test('Joi.binary()', () => {
const consoleSpy = jest.spyOn(console, 'debug');
const schema = Joi.object({
doStuff: Joi.binary()
}).meta({ className: 'Test' });

const result = convertSchema({ debug: true }, schema);
expect(result).not.toBeUndefined;
expect(result?.content).toBe(`export interface Test {}`);
expect(consoleSpy).toHaveBeenCalledWith('unsupported type: binary');
// prettier-ignore
expect(result?.content).toBe(
[
'export interface Test {',
' doStuff?: Buffer;',
'}'
].join('\n')
);
});
});
20 changes: 17 additions & 3 deletions src/joiUtils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
import { Settings } from '.';
import { Describe } from './joiDescribeTypes';

/**
* Fetch the metadata values for a given field. Note that it is possible to have
* more than one metadata record for a given field hence it is possible to get
* back a list of values.
*
* @param field - the name of the metadata field to fetch
* @param details - the schema details
* @returns the values for the given field
*/
export function getMetadataFromDetails(field: string, details: Describe): any[] {
const metas: any[] = details?.metas ?? [];
return metas.filter(entry => entry[field]).map(entry => entry[field]);
}

/**
* Get the interface name from the Joi
* @returns a string if it can find one
Expand All @@ -10,11 +24,11 @@ export function getInterfaceOrTypeName(settings: Settings, details: Describe): s
return details?.flags?.label?.replace(/\s/g, '');
} else {
if (details?.metas && details.metas.length > 0) {
const classNames = details.metas.filter(meta => meta.className);
if (classNames.length !== 0){
const classNames: string[] = getMetadataFromDetails('className', details);
if (classNames.length !== 0) {
// If Joi.concat() has been used then there may be multiple
// get the last one as that should be the correct one
const className = classNames[classNames.length - 1].className;
const className = classNames.pop();
return className?.replace(/\s/g, '');
}
}
Expand Down
41 changes: 37 additions & 4 deletions src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
ObjectDescribe,
StringDescribe
} from './joiDescribeTypes';
import { getInterfaceOrTypeName } from './joiUtils';
import { getInterfaceOrTypeName, getMetadataFromDetails } from './joiUtils';

// see __tests__/joiTypes.ts for more information
export const supportedJoiTypes = ['array', 'object', 'alternatives', 'any', 'boolean', 'date', 'number', 'string'];
Expand Down Expand Up @@ -223,9 +223,42 @@ export function parseSchema(
}
return child;
}
if (settings.debug && !supportedJoiTypes.includes(details.type)) {
console.debug(`unsupported type: ${details.type}`);
return undefined;
if (!supportedJoiTypes.includes(details.type)) {
// See if we can find a base type for this type in the details.
let typeToUse;
const baseTypes: string[] = getMetadataFromDetails('baseType', details);
if (baseTypes.length > 0) {
// If there are multiple base types then the deepest one will be at the
// end of the list which is most likely the one to use.
typeToUse = baseTypes.pop() as string;
}

// If we could not get the base type from the metadata then see if we can
// map it to something sensible. If not, then set it to 'unknown'.
if (typeToUse === undefined) {
switch (details.type as string) {
case 'function':
typeToUse = '(...args: any[]) => any';
break;

case 'symbol':
typeToUse = 'symbol';
break;

case 'binary':
typeToUse = 'Buffer';
break;

default:
typeToUse = 'unknown';
break;
}
}

if (settings.debug) {
console.debug(`Using '${typeToUse}' for unsupported type '${details.type}'`);
}
return makeTypeContentChild({ content: typeToUse, interfaceOrTypeName, jsDoc });
}
const parsedSchema = parseHelper();
if (!parsedSchema) {
Expand Down

0 comments on commit 9607f78

Please sign in to comment.