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
47 changes: 25 additions & 22 deletions packages/openapi-v3/src/json-to-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {JsonDefinition} from '@loopback/repository-json-schema';
import {JSONSchema} from '@loopback/repository-json-schema';
import {SchemaObject} from '@loopback/openapi-v3-types';
import * as _ from 'lodash';

export function jsonToSchemaObject(json: JsonDefinition): SchemaObject {
/**
* Converts JSON Schemas into a SchemaObject
* @param json JSON Schema to convert from
*/
export function jsonToSchemaObject(json: JSONSchema): SchemaObject {
const result: SchemaObject = {};
const propsToIgnore = [
'anyOf',
Expand Down Expand Up @@ -36,42 +40,27 @@ export function jsonToSchemaObject(json: JsonDefinition): SchemaObject {
}
case 'definitions': {
result.definitions = _.mapValues(json.definitions, def =>
jsonToSchemaObject(def),
jsonToSchemaObject(jsonOrBooleanToJSON(def)),
);
break;
}
case 'properties': {
result.properties = _.mapValues(json.properties, item =>
jsonToSchemaObject(item),
jsonToSchemaObject(jsonOrBooleanToJSON(item)),
);
break;
}
case 'additionalProperties': {
if (typeof json.additionalProperties !== 'boolean') {
result.additionalProperties = jsonToSchemaObject(
json.additionalProperties as JsonDefinition,
json.additionalProperties!,
);
}
break;
}
case 'items': {
const items = Array.isArray(json.items) ? json.items[0] : json.items;
result.items = jsonToSchemaObject(items as JsonDefinition);
break;
}
case 'enum': {
const newEnum = [];
const primitives = ['string', 'number', 'boolean'];
for (const element of json.enum!) {
if (primitives.includes(typeof element) || element === null) {
newEnum.push(element);
} else {
// if element is JsonDefinition, convert to SchemaObject
newEnum.push(jsonToSchemaObject(element as JsonDefinition));
}
}
result.enum = newEnum;

result.items = jsonToSchemaObject(jsonOrBooleanToJSON(items!));
break;
}
case '$ref': {
Expand All @@ -82,11 +71,25 @@ export function jsonToSchemaObject(json: JsonDefinition): SchemaObject {
break;
}
default: {
result[property] = json[property as keyof JsonDefinition];
result[property] = json[property as keyof JSONSchema];
break;
}
}
}

return result;
}

Copy link
Contributor

Choose a reason for hiding this comment

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

TSDocs here perhaps?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added

/**
* Helper function used to interpret boolean values as JSON Schemas.
* See http://json-schema.org/draft-06/json-schema-release-notes.html
* @param jsonOrBool converts boolean values into their representative JSON Schemas
* @returns JSONSchema
*/
export function jsonOrBooleanToJSON(jsonOrBool: boolean | JSONSchema) {
if (typeof jsonOrBool === 'object') {
return jsonOrBool;
} else {
return jsonOrBool ? {} : {not: {}};
}
}
75 changes: 31 additions & 44 deletions packages/openapi-v3/test/unit/json-to-schema.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,23 @@
// License text available at https://opensource.org/licenses/MIT

import {expect} from '@loopback/testlab';
import {JsonDefinition} from '@loopback/repository-json-schema';

import {SchemaObject} from '@loopback/openapi-v3-types';
import {jsonToSchemaObject} from '../..';
import {jsonToSchemaObject, jsonOrBooleanToJSON} from '../..';
import {JSONSchema} from '@loopback/repository-json-schema';

describe('jsonToSchemaObject', () => {
it('does nothing when given an empty object', () => {
expect({}).to.eql({});
});
const typeDef: JsonDefinition = {type: ['string', 'number']};
const typeDef: JSONSchema = {type: ['string', 'number']};
const expectedType: SchemaObject = {type: 'string'};
it('converts type', () => {
propertyConversionTest(typeDef, expectedType);
});

it('ignores non-compatible JSON schema properties', () => {
const nonCompatibleDef: JsonDefinition = {
const nonCompatibleDef = {
anyOf: [],
oneOf: [],
additionalItems: {
Expand All @@ -34,7 +34,7 @@ describe('jsonToSchemaObject', () => {
});

it('converts allOf', () => {
const allOfDef: JsonDefinition = {
const allOfDef: JSONSchema = {
allOf: [typeDef, typeDef],
};
const expectedAllOf: SchemaObject = {
Expand All @@ -44,7 +44,7 @@ describe('jsonToSchemaObject', () => {
});

it('converts definitions', () => {
const definitionsDef: JsonDefinition = {
const definitionsDef: JSONSchema = {
definitions: {foo: typeDef, bar: typeDef},
};
const expectedDef: SchemaObject = {
Expand All @@ -54,7 +54,7 @@ describe('jsonToSchemaObject', () => {
});

it('converts properties', () => {
const propertyDef: JsonDefinition = {
const propertyDef: JSONSchema = {
properties: {
foo: typeDef,
},
Expand All @@ -68,8 +68,8 @@ describe('jsonToSchemaObject', () => {
});

context('additionalProperties', () => {
it('is converted properly when the type is JsonDefinition', () => {
const additionalDef: JsonDefinition = {
it('is converted properly when the type is JSONSchema', () => {
const additionalDef: JSONSchema = {
additionalProperties: typeDef,
};
const expectedAdditional: SchemaObject = {
Expand All @@ -79,7 +79,7 @@ describe('jsonToSchemaObject', () => {
});

it('is converted properly when it is "false"', () => {
const noAdditionalDef: JsonDefinition = {
const noAdditionalDef: JSONSchema = {
additionalProperties: false,
};
const expectedDef: SchemaObject = {};
Expand All @@ -88,7 +88,7 @@ describe('jsonToSchemaObject', () => {
});

it('converts items', () => {
const itemsDef: JsonDefinition = {
const itemsDef: JSONSchema = {
type: 'array',
items: typeDef,
};
Expand All @@ -99,40 +99,8 @@ describe('jsonToSchemaObject', () => {
propertyConversionTest(itemsDef, expectedItems);
});

context('enum', () => {
it('is converted properly when the type is primitive', () => {
const enumStringDef: JsonDefinition = {
enum: ['foo', 'bar'],
};
const expectedStringDef: SchemaObject = {
enum: ['foo', 'bar'],
};
propertyConversionTest(enumStringDef, expectedStringDef);
});

it('is converted properly when it is null', () => {
const enumNullDef: JsonDefinition = {
enum: [null, null],
};
const expectedNullDef: JsonDefinition = {
enum: [null, null],
};
propertyConversionTest(enumNullDef, expectedNullDef);
});

it('is converted properly when the type is complex', () => {
const enumCustomDef: JsonDefinition = {
enum: [typeDef, typeDef],
};
const expectedCustomDef: SchemaObject = {
enum: [expectedType, expectedType],
};
propertyConversionTest(enumCustomDef, expectedCustomDef);
});
});

it('retains given properties in the conversion', () => {
const inputDef: JsonDefinition = {
const inputDef: JSONSchema = {
title: 'foo',
type: 'object',
properties: {
Expand Down Expand Up @@ -171,3 +139,22 @@ describe('jsonToSchemaObject', () => {
expect(jsonToSchemaObject(property)).to.deepEqual(expected);
}
});

describe('jsonOrBooleanToJson', () => {
it('converts true to {}', () => {
expect(jsonOrBooleanToJSON(true)).to.eql({});
});

it('converts false to {}', () => {
expect(jsonOrBooleanToJSON(false)).to.eql({not: {}});
});

it('makes no changes to JSON Schema', () => {
const jsonSchema: JSONSchema = {
properties: {
number: {type: 'number'},
},
};
expect(jsonOrBooleanToJSON(jsonSchema)).to.eql(jsonSchema);
});
});
4 changes: 1 addition & 3 deletions packages/repository-json-schema/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,11 @@
"dependencies": {
"@loopback/context": "^0.8.1",
"@loopback/repository": "^0.8.1",
"lodash": "^4.17.5",
"typescript-json-schema": "^0.22.0"
"@types/json-schema": "^6.0.1"
},
"devDependencies": {
"@loopback/build": "^0.6.0",
"@loopback/testlab": "^0.8.0",
"@types/lodash": "^4.14.106",
"@types/node": "^8.10.4"
},
"files": [
Expand Down
41 changes: 14 additions & 27 deletions packages/repository-json-schema/src/build-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,37 +8,22 @@ import {
PropertyDefinition,
ModelDefinition,
} from '@loopback/repository';
import {includes} from 'lodash';
import {Definition, PrimitiveType} from 'typescript-json-schema';
import {MetadataInspector, MetadataAccessor} from '@loopback/context';
import {
JSONSchema6 as JSONSchema,
JSONSchema6TypeName as JSONSchemaTypeName,
} from 'json-schema';

export const JSON_SCHEMA_KEY = MetadataAccessor.create<JsonDefinition>(
export const JSON_SCHEMA_KEY = MetadataAccessor.create<JSONSchema>(
'loopback:json-schema',
);

/**
* Type definition for JSON Schema
*/
export interface JsonDefinition extends Definition {
allOf?: JsonDefinition[];
oneOf?: JsonDefinition[];
anyOf?: JsonDefinition[];
items?: JsonDefinition | JsonDefinition[];
additionalItems?: {
anyOf: JsonDefinition[];
};
enum?: PrimitiveType[] | JsonDefinition[];
additionalProperties?: JsonDefinition | boolean;
definitions?: {[definition: string]: JsonDefinition};
properties?: {[property: string]: JsonDefinition};
}

/**
* Gets the JSON Schema of a TypeScript model/class by seeing if one exists
* in a cache. If not, one is generated and then cached.
* @param ctor Contructor of class to get JSON Schema from
*/
export function getJsonSchema(ctor: Function): JsonDefinition {
export function getJsonSchema(ctor: Function): JSONSchema {
// NOTE(shimks) currently impossible to dynamically update
const jsonSchema = MetadataInspector.getClassMetadata(JSON_SCHEMA_KEY, ctor);
if (jsonSchema) {
Expand Down Expand Up @@ -82,16 +67,18 @@ export function stringTypeToWrapper(type: string): Function {
* @param ctor Constructor
*/
export function isComplexType(ctor: Function) {
return !includes([String, Number, Boolean, Object, Function], ctor);
return !([String, Number, Boolean, Object, Function] as Function[]).includes(
ctor,
);
}

/**
* Converts property metadata into a JSON property definition
* @param meta
*/
export function metaToJsonProperty(meta: PropertyDefinition): JsonDefinition {
export function metaToJsonProperty(meta: PropertyDefinition): JSONSchema {
let ctor = meta.type as string | Function;
let def: JsonDefinition = {};
let def: JSONSchema = {};

// errors out if @property.array() is not used on a property of array
if (ctor === Array) {
Expand All @@ -104,7 +91,7 @@ export function metaToJsonProperty(meta: PropertyDefinition): JsonDefinition {

const propDef = isComplexType(ctor)
? {$ref: `#/definitions/${ctor.name}`}
: {type: ctor.name.toLowerCase()};
: {type: <JSONSchemaTypeName>ctor.name.toLowerCase()};

if (meta.array) {
def.type = 'array';
Expand All @@ -124,9 +111,9 @@ export function metaToJsonProperty(meta: PropertyDefinition): JsonDefinition {
* reflection API
* @param ctor Constructor of class to convert from
*/
export function modelToJsonSchema(ctor: Function): JsonDefinition {
export function modelToJsonSchema(ctor: Function): JSONSchema {
const meta: ModelDefinition | {} = ModelMetadataHelper.getModelMetadata(ctor);
const result: JsonDefinition = {};
const result: JSONSchema = {};

// returns an empty object if metadata is an empty object
if (!(meta instanceof ModelDefinition)) {
Expand Down
3 changes: 3 additions & 0 deletions packages/repository-json-schema/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@
// License text available at https://opensource.org/licenses/MIT

export * from './build-schema';

import {JSONSchema6 as JSONSchema} from 'json-schema';
export {JSONSchema};
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@
// License text available at https://opensource.org/licenses/MIT

import {model, property} from '@loopback/repository';
import {modelToJsonSchema} from '../../src/build-schema';
import {
modelToJsonSchema,
JSON_SCHEMA_KEY,
getJsonSchema,
JSONSchema,
} from '../..';
import {expect} from '@loopback/testlab';
import {MetadataInspector} from '@loopback/context';
import {JSON_SCHEMA_KEY, getJsonSchema} from '../../index';

describe('build-schema', () => {
describe('modelToJsonSchema', () => {
Expand Down Expand Up @@ -339,7 +343,7 @@ describe('build-schema', () => {
class TestModel {
@property() foo: number;
}
const cachedSchema = {
const cachedSchema: JSONSchema = {
properties: {
cachedProperty: {
type: 'string',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import {expect} from '@loopback/testlab';
import {
isComplexType,
stringTypeToWrapper,
metaToJsonProperty,
} from '../../index';
import {isComplexType, stringTypeToWrapper, metaToJsonProperty} from '../..';

describe('build-schema', () => {
describe('stringTypeToWrapper', () => {
Expand Down