Skip to content

Commit fe59e6b

Browse files
authored
feat(repository-json-schema): add in top-level metadata for json schema (#907)
* feat(repository-json-schema): add in top-level metadata for json schema
1 parent 43256e8 commit fe59e6b

File tree

11 files changed

+531
-295
lines changed

11 files changed

+531
-295
lines changed

packages/openapi-v2/test/unit/controller-spec/controller-decorators/param-decorators/param-body.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ describe('Routing metadata for parameters', () => {
6161
const actualSpec = getControllerSpec(MyController);
6262
expect(actualSpec.definitions).to.deepEqual({
6363
MyData: {
64+
title: 'MyData',
6465
properties: {
6566
name: {
6667
type: 'string',

packages/openapi-v2/test/unit/controller-spec/controller-decorators/param-decorators/param.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -311,13 +311,15 @@ describe('Routing metadata for parameters', () => {
311311
// tslint:disable-next-line:no-any
312312
expect(defs).to.have.keys('Foo', 'Bar');
313313
expect(defs.Foo).to.deepEqual({
314+
title: 'Foo',
314315
properties: {
315316
price: {
316317
type: 'number',
317318
},
318319
},
319320
});
320321
expect(defs.Bar).to.deepEqual({
322+
title: 'Bar',
321323
properties: {
322324
name: {
323325
type: 'string',
@@ -354,7 +356,7 @@ describe('Routing metadata for parameters', () => {
354356
expect(defs.MyBody).to.not.have.key('definitions');
355357
});
356358

357-
it('infers empty body parameter schema if no property metadata is present', () => {
359+
it('infers no properties if no property metadata is present', () => {
358360
const paramSpec: ParameterObject = {
359361
name: 'foo',
360362
in: 'body',
@@ -372,7 +374,7 @@ describe('Routing metadata for parameters', () => {
372374
.definitions as DefinitionsObject;
373375

374376
expect(defs).to.have.key('MyBody');
375-
expect(defs.MyBody).to.deepEqual({});
377+
expect(defs.MyBody).to.not.have.key('properties');
376378
});
377379

378380
it('does not infer definition if no class metadata is present', () => {

packages/repository-json-schema/src/build-schema.ts

Lines changed: 105 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,72 @@ export function getJsonSchema(ctor: Function): JsonDefinition {
4848
}
4949
}
5050

51+
/**
52+
* Gets the wrapper function of primitives string, number, and boolean
53+
* @param type Name of type
54+
*/
55+
export function stringTypeToWrapper(type: string): Function {
56+
type = type.toLowerCase();
57+
let wrapper;
58+
switch (type) {
59+
case 'number': {
60+
wrapper = Number;
61+
break;
62+
}
63+
case 'string': {
64+
wrapper = String;
65+
break;
66+
}
67+
case 'boolean': {
68+
wrapper = Boolean;
69+
break;
70+
}
71+
default: {
72+
throw new Error('Unsupported type');
73+
}
74+
}
75+
return wrapper;
76+
}
77+
78+
/**
79+
* Determines whether the given constructor is a custom type or not
80+
* @param ctor Constructor
81+
*/
82+
export function isComplexType(ctor: Function) {
83+
return !includes([String, Number, Boolean, Object, Function], ctor);
84+
}
85+
86+
/**
87+
* Converts property metadata into a JSON property definition
88+
* @param meta
89+
*/
90+
export function metaToJsonProperty(meta: PropertyDefinition): JsonDefinition {
91+
let ctor = meta.type as string | Function;
92+
let def: JsonDefinition = {};
93+
94+
// errors out if @property.array() is not used on a property of array
95+
if (ctor === Array) {
96+
throw new Error('type is defined as an array');
97+
}
98+
99+
if (typeof ctor === 'string') {
100+
ctor = stringTypeToWrapper(ctor);
101+
}
102+
103+
const propDef = isComplexType(ctor)
104+
? {$ref: `#definitions/${ctor.name}`}
105+
: {type: ctor.name.toLowerCase()};
106+
107+
if (meta.array) {
108+
def.type = 'array';
109+
def.items = propDef;
110+
} else {
111+
Object.assign(def, propDef);
112+
}
113+
114+
return def;
115+
}
116+
51117
// NOTE(shimks) no metadata for: union, optional, nested array, any, enum,
52118
// string literal, anonymous types, and inherited properties
53119

@@ -57,80 +123,59 @@ export function getJsonSchema(ctor: Function): JsonDefinition {
57123
* @param ctor Constructor of class to convert from
58124
*/
59125
export function modelToJsonSchema(ctor: Function): JsonDefinition {
60-
const meta: ModelDefinition = ModelMetadataHelper.getModelMetadata(ctor);
61-
const schema: JsonDefinition = {};
126+
const meta: ModelDefinition | {} = ModelMetadataHelper.getModelMetadata(ctor);
127+
const result: JsonDefinition = {};
62128

63-
const isComplexType = (constructor: Function) =>
64-
!includes([String, Number, Boolean, Object], constructor);
129+
// returns an empty object if metadata is an empty object
130+
if (!(meta instanceof ModelDefinition)) {
131+
return {};
132+
}
65133

66-
const determinePropertyDef = (constructor: Function) =>
67-
isComplexType(constructor)
68-
? {$ref: `#definitions/${constructor.name}`}
69-
: {type: constructor.name.toLowerCase()};
134+
result.title = meta.title || ctor.name;
135+
136+
if (meta.description) {
137+
result.description = meta.description;
138+
}
70139

71140
for (const p in meta.properties) {
72-
const propMeta = meta.properties[p];
73-
let propCtor = propMeta.type;
74-
if (typeof propCtor === 'string') {
75-
const type = propCtor.toLowerCase();
76-
switch (type) {
77-
case 'number': {
78-
propCtor = Number;
79-
break;
80-
}
81-
case 'string': {
82-
propCtor = String;
83-
break;
84-
}
85-
case 'boolean': {
86-
propCtor = Boolean;
87-
break;
88-
}
89-
default: {
90-
throw new Error('Unsupported type');
91-
}
92-
}
141+
if (!meta.properties[p].type) {
142+
continue;
93143
}
94-
if (propCtor && typeof propCtor === 'function') {
95-
// errors out if @property.array() is not used on a property of array
96-
if (propCtor === Array) {
97-
throw new Error('type is defined as an array');
98-
}
99144

100-
const propDef: JsonDefinition = determinePropertyDef(propCtor);
145+
result.properties = result.properties || {};
146+
result.properties[p] = result.properties[p] || {};
101147

102-
if (!schema.properties) {
103-
schema.properties = {};
104-
}
148+
const property = result.properties[p];
149+
const metaProperty = meta.properties[p];
150+
const metaType = metaProperty.type;
105151

106-
if (propMeta.array === true) {
107-
schema.properties[p] = {
108-
type: 'array',
109-
items: propDef,
110-
};
111-
} else {
112-
schema.properties[p] = propDef;
113-
}
152+
// populating "properties" key
153+
result.properties[p] = metaToJsonProperty(metaProperty);
114154

115-
if (isComplexType(propCtor)) {
116-
const propSchema = getJsonSchema(propCtor);
155+
// populating JSON Schema 'definitions'
156+
if (typeof metaType === 'function' && isComplexType(metaType)) {
157+
const propSchema = getJsonSchema(metaType);
117158

118-
if (propSchema && Object.keys(propSchema).length > 0) {
119-
if (!schema.definitions) {
120-
schema.definitions = {};
121-
}
159+
if (propSchema && Object.keys(propSchema).length > 0) {
160+
result.definitions = result.definitions || {};
122161

123-
if (propSchema.definitions) {
124-
for (const key in propSchema.definitions) {
125-
schema.definitions[key] = propSchema.definitions[key];
126-
}
127-
delete propSchema.definitions;
162+
// delete nested definition
163+
if (propSchema.definitions) {
164+
for (const key in propSchema.definitions) {
165+
result.definitions[key] = propSchema.definitions[key];
128166
}
129-
130-
schema.definitions[propCtor.name] = propSchema;
167+
delete propSchema.definitions;
131168
}
169+
170+
result.definitions[metaType.name] = propSchema;
132171
}
133172
}
173+
174+
// handling 'required' metadata
175+
if (metaProperty.required) {
176+
result.required = result.required || [];
177+
result.required.push(p);
178+
}
134179
}
135-
return schema;
180+
return result;
136181
}

0 commit comments

Comments
 (0)