Skip to content

Commit

Permalink
Merge pull request #107 from mrjono1/id-as-interface-name
Browse files Browse the repository at this point in the history
Id as interface name
  • Loading branch information
mrjono1 authored Jun 20, 2021
2 parents f285c17 + da1c490 commit 13a96a5
Show file tree
Hide file tree
Showing 46 changed files with 530 additions and 216 deletions.
40 changes: 28 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ Convert [Joi](https://github.com/sideway/joi) Schemas to TypeScript interfaces

This will allow you to reuse a Joi Schema that validates your [Hapi](https://github.com/hapijs/hapi) API to generate TypeScript interfaces so you don't have to manually create the same structure again saving you time.

For generating Open Api/Swagger this project works with

- [hapi-swagger](https://github.com/glennjones/hapi-swagger) using `.label('')` this has been well tested and used in production
- [joi-to-swagger](https://github.com/Twipped/joi-to-swagger) using `.meta({className:''})` limited testing but this is looking like a better approach

Version 2, why the move to `.meta({className:'')` from `.label('')`? `Joi.label()` is intended to be used for meaningful error message, using it for another purpose makes the Joi loose a standard feature, this is especially noticeable for frontend usages of Joi. The choice of the property `className` is because this property is used by joi-to-swagger making this project work with other projects is important.

## Installation Notes

This package is intended as a development time tool so is installed in the `devDependencies`
Expand All @@ -21,7 +28,7 @@ yarn add joi-to-typescript --dev
npm install joi-to-typescript --save-dev
```

- This has been built for `"joi": "^17.2.1"` and will probaly not work for older versions, mainly due to package name changes
- This has been built for `"joi": "^17"` and will not work for older versions
- Minimum node version 12 as Joi requries node 12

## Suggested Usage
Expand All @@ -48,20 +55,18 @@ import Joi from 'joi';
export const JobSchema = Joi.object({
businessName: Joi.string().required(),
jobTitle: Joi.string().required()
}).label('Job');
}).meta({ className: 'Job' });

export const PersonSchema = Joi.object({
firstName: Joi.string().required(),
lastName: Joi.string()
.required()
.description('Last Name'),
lastName: Joi.string().required().description('Last Name'),
job: JobSchema
}).label('Person');
}).meta({ className: 'Person' });

export const PeopleSchema = Joi.array()
.items(PersonSchema)
.required()
.label('People')
.meta({ className: 'People' })
.description('A list of People');

// Output
Expand Down Expand Up @@ -97,7 +102,11 @@ export interface Person {

- `export const PersonSchema` schema must be exported
- `export const PersonSchema` includes a suffix of Schema so the schema and interface are not confused when using `import` statements (recommended not requried)
- `.label('Person');` Sets `interface` name using TypeScript conventions (TitleCase Interface name, camlCase property name)
- `.meta({className:'Person'});` Sets `interface` name using TypeScript conventions (TitleCase Interface name, camlCase property name)

#### Upgrade Notice

- Version 1 used `.label('Person')` as the way to define the `interface` name, to use this option set `{ useLabelAsInterfaceName: true }`

#### Example Call

Expand Down Expand Up @@ -130,18 +139,24 @@ export interface Settings {
* Will also attempt to create this directory
*/
typeOutputDirectory: string;
/**
* Use .label('InterfaceName') instead of .meta({className:'InterfaceName'}) for interface names
*/
useLabelAsInterfaceName: boolean;
/**
* Should interface properties be defaulted to optional or required
* @default false
*/
defaultToRequired: boolean;
/**
* What schema file name suffix will be removed when creating the interface file name
* Defaults to `Schema`
* @default "Schema"
* This ensures that an interface and Schema with the file name are not confused
*/
schemaFileSuffix: string;
/**
* If `true` the console will include more information
* @default false
*/
debug: boolean;
/**
Expand All @@ -150,7 +165,7 @@ export interface Settings {
fileHeader: string;
/**
* If true will sort properties on interface by name
* Defaults to `true`
* @default true
*/
sortPropertiesByName: boolean;
/**
Expand All @@ -167,12 +182,13 @@ export interface Settings {
indexAllToRoot: boolean;
/**
* Comment every interface and property even with just a duplicate of the interface and property name
* Defaults to `false`
* @default false
*/
commentEverything: boolean;
/**
* List of files or folders that should be ignored from conversion. These can either be
* filenames (AddressSchema.ts) or filepaths postfixed with a / (addressSchemas/)
* @default []
*/
ignoreFiles: string[];
/**
Expand All @@ -185,7 +201,7 @@ export interface Settings {

## Joi Features Supported

- .label('InterfaceName') - interface Name and in jsDoc
- .meta({className:'InterfaceName'}) - interface Name and in jsDoc
- .description('What this interface is for') - jsdoc
- .valid(['red', 'green', 'blue']) - enumerations
- .optional() - optional properties `?`
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "joi-to-typescript",
"description": "Convert Joi Schemas to TypeScript interfaces",
"version": "1.14.1",
"version": "2.0.0",
"author": "Jono Clarnette",
"keywords": [
"joi",
Expand Down
6 changes: 3 additions & 3 deletions src/__tests__/allow/allow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ describe('union types using allow()', () => {
nullNumber: Joi.number().optional().allow(null),
date: Joi.date().allow(null).description('This is date')
})
.label('TestSchema')
.meta({ className: 'TestSchema' })
.description('a test schema definition');

const result = convertSchema({ sortPropertiesByName: false }, schema);
Expand Down Expand Up @@ -61,7 +61,7 @@ export interface TestSchema {
blankNullUndefined: Joi.string().optional().allow(null, '', undefined),
blankNullUndefinedRequired: Joi.string().required().allow(null, '', undefined)
})
.label('TestSchema')
.meta({ className: 'TestSchema' })
.description('a test schema definition');

const invalidResult = convertSchema({}, invalidSchema);
Expand All @@ -76,7 +76,7 @@ export interface TestSchema {
// then some tests for things you can do but probably shouldnt
sillyProperty: Joi.object().allow(null, 'joe'),
sillyArray: Joi.array().items(Joi.string()).allow(null, 'fred')
}).label('TestSchema');
}).meta({ className: 'TestSchema' });

const result = convertSchema({ sortPropertiesByName: false }, schema);
expect(result).not.toBeUndefined;
Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/allow/schemas/ParentSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import Joi from 'joi';

export const childSchema = Joi.object({
item: Joi.number().required().example(0)
}).label('Child');
}).meta({ className: 'Child' });

export const parentSchema = Joi.object({
child: childSchema.allow(null).required()
}).label('Parent');
}).meta({ className: 'Parent' });
11 changes: 7 additions & 4 deletions src/__tests__/alternatives/alternatives.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { existsSync, readFileSync, rmdirSync } from 'fs';
import Joi from 'joi';

import { convertFromDirectory, convertSchema, Settings } from '../../index';
import { convertFromDirectory, convertSchema } from '../../index';

describe('alternative types', () => {
const typeOutputDirectory = './src/__tests__/alternatives/interfaces';
Expand Down Expand Up @@ -63,16 +63,19 @@ export interface Thing {

test('blank alternative throws in joi', () => {
expect(() => {
Joi.alternatives().try().label('Basic').description('a description for basic');
Joi.alternatives().try().meta({ className: 'Basic' }).description('a description for basic');
}).toThrow();
});

test.skip('blank alternative thrown by joi but extra test if joi changes it', () => {
expect(() => {
const invalidSchema = Joi.alternatives().try().label('Basic').description('a description for basic');
const invalidSchema = Joi.alternatives()
.try()
.meta({ className: 'Basic' })
.description('a description for basic');

// the next code will not run as already thrown
convertSchema({} as Settings, invalidSchema);
convertSchema({}, invalidSchema);
}).toThrow();
});
});
10 changes: 5 additions & 5 deletions src/__tests__/alternatives/schemas/OneSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,27 @@ import Joi from 'joi';

export const thingSchema = Joi.object({
thing: Joi.string().required()
}).label('Thing');
}).meta({ className: 'Thing' });

export const otherSchema = Joi.object({
other: Joi.string().optional()
}).label('Other');
}).meta({ className: 'Other' });

export const basicSchema = Joi.alternatives()
.try(Joi.number(), Joi.string())
.label('Basic')
.meta({ className: 'Basic' })
.description('a description for basic');

export const TestSchema = Joi.object({
name: Joi.string().optional(),
value: Joi.alternatives().try(thingSchema, otherSchema),
basic: basicSchema
})
.label('Test')
.meta({ className: 'Test' })
.description('a test schema definition');

export const TestListOfAltsSchema = Joi.array()
.items(Joi.alt().try(Joi.bool(), Joi.string()))
.required()
.label('TestList')
.meta({ className: 'TestList' })
.description('A list of Test object');
6 changes: 3 additions & 3 deletions src/__tests__/array/schemas/OneSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@ import Joi from 'joi';

export const ItemSchema = Joi.object({
name: Joi.string().required()
}).label('Item');
}).meta({ className: 'Item' });

export const TestSchema = Joi.object({
name: Joi.string().optional(),
propertyName1: Joi.bool().required(),
items: Joi.array().items(ItemSchema).optional()
})
.label('Test')
.meta({ className: 'Test' })
.description('a test schema definition');

export const TestListSchema = Joi.array()
.items(TestSchema)
.required()
.label('TestList')
.meta({ className: 'TestList' })
.description('A list of Test object');
12 changes: 6 additions & 6 deletions src/__tests__/basic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe('some basic tests', () => {
count: Joi.number(),
obj: Joi.object()
})
.label('TestSchema')
.meta({ className: 'TestSchema' })
.description('a test schema definition');

const result = convertSchema({ sortPropertiesByName: true }, schema);
Expand Down Expand Up @@ -41,7 +41,7 @@ export interface TestSchema {
count: Joi.array().items(Joi.number()),
arr: Joi.array()
})
.label('ArrayObject')
.meta({ className: 'ArrayObject' })
.description('an Array test schema definition');

const arrayResult = convertSchema({ sortPropertiesByName: true }, schemaArray);
Expand All @@ -64,10 +64,10 @@ export interface ArrayObject {
nested: Joi.object({ a: Joi.object({ b: Joi.string() }) }),
nestedComments: Joi.object({ a: Joi.object({ b: Joi.string().description('nested comment') }) }),
nestedObject: Joi.object({
aType: Joi.object().label('Blue').description('A blue object property')
aType: Joi.object().meta({ className: 'Blue' }).description('A blue object property')
}),
'x.y': Joi.string()
}).label('TestSchema');
}).meta({ className: 'TestSchema' });

const result = convertSchema({ sortPropertiesByName: false }, schema);
expect(result).not.toBeUndefined;
Expand Down Expand Up @@ -99,7 +99,7 @@ export interface ArrayObject {
const schema = Joi.object({
a: Joi.string(),
A: Joi.string()
}).label('TestSchema');
}).meta({ className: 'TestSchema' });

const result = convertSchema({ sortPropertiesByName: false }, schema);
expect(result).not.toBeUndefined;
Expand All @@ -110,7 +110,7 @@ export interface ArrayObject {
});

test('no properties on a schema', () => {
const schema = Joi.object({}).label('TestSchema');
const schema = Joi.object({}).meta({ className: 'TestSchema' });

const result = convertSchema({ sortPropertiesByName: true }, schema);
expect(result).not.toBeUndefined;
Expand Down
Loading

0 comments on commit 13a96a5

Please sign in to comment.