-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
6487e1c
commit 6df0f7a
Showing
4 changed files
with
535 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,397 @@ | ||
/* eslint-disable no-use-before-define */ | ||
import debug from 'debug'; | ||
import { OpenAPIV3 as oapi3 } from 'openapi-types'; | ||
import * as tjs from 'typescript-json-schema'; | ||
|
||
import { isDefined } from '../utils/types'; | ||
|
||
import { Schema } from './types'; | ||
import { | ||
isNullableObject, | ||
isObjectSchemaObject, | ||
isReferenceObject, | ||
isEmptyObject | ||
} from '../utils/schema'; | ||
|
||
|
||
export const isDefinitionBoolean = (defOrBool: tjs.DefinitionOrBoolean): defOrBool is boolean => { | ||
if (defOrBool === true || defOrBool === false) { | ||
return true; | ||
} | ||
return false; | ||
}; | ||
|
||
|
||
export const DEBUG = debug('tspec'); | ||
|
||
const createItem = (items: tjs.DefinitionOrBoolean[]) => { | ||
let nullable = false; | ||
const schema = items.map((item) => { | ||
const convertedItem = convertDefinition(item); | ||
if (convertedItem && isNullableObject(convertedItem)) { | ||
nullable = true; | ||
return undefined; | ||
} | ||
return convertedItem; | ||
}); | ||
|
||
const nullableProperty = nullable ? { nullable } : {}; | ||
|
||
const filteredSchema = schema.filter(isDefined); | ||
if (filteredSchema.length === 0) { | ||
return nullableProperty; | ||
} if (filteredSchema.length === 1) { | ||
const onlySchema = filteredSchema[0]; | ||
if (isReferenceObject(onlySchema) && nullable) { | ||
return { | ||
anyOf: [onlySchema, nullableProperty], | ||
}; | ||
} | ||
return { | ||
...onlySchema, | ||
...nullableProperty, | ||
}; | ||
} if (filteredSchema.length > 1) { | ||
return { | ||
anyOf: filteredSchema, | ||
...nullableProperty, | ||
}; | ||
} | ||
}; | ||
|
||
const convertItems = ( | ||
items: tjs.DefinitionOrBoolean | tjs.DefinitionOrBoolean[], | ||
) => { | ||
if (!Array.isArray(items)) { | ||
return convertDefinition(items); | ||
} | ||
|
||
if (items.length === 1) { | ||
return convertDefinition(items[0]); | ||
} | ||
|
||
return createItem(items); | ||
}; | ||
|
||
export const convertProperties = (obj: { | ||
[key: string]: tjs.DefinitionOrBoolean, | ||
}) => { | ||
const convertedObj: { [key: string]: Schema } = {}; | ||
for (const [key, val] of Object.entries(obj)) { | ||
const convertedProperty = convertDefinition(val); | ||
if (convertedProperty) { | ||
convertedObj[key] = convertedProperty; | ||
} | ||
} | ||
return convertedObj; | ||
}; | ||
|
||
const convertSchemaArray = ( | ||
defs: tjs.DefinitionOrBoolean[], | ||
property: 'anyOf' | 'oneOf' | 'allOf', | ||
) => { | ||
let schema: Schema = {}; | ||
let nullable = false; | ||
|
||
const object: Schema = { type: 'object', properties: {} }; | ||
|
||
const filteredDefs = defs.map((def) => { | ||
const convertedDef = convertDefinition(def); | ||
// undefined 제외 | ||
if (!convertedDef) { | ||
return undefined; | ||
} | ||
// {nullable: true}인 경우 nullable = true하고 제외 | ||
if (isNullableObject(convertedDef)) { | ||
nullable = true; | ||
return undefined; | ||
} | ||
|
||
if (property === 'allOf') { | ||
// object의 proeprty 모아서 하나의 object로 만들기 | ||
if (isObjectSchemaObject(convertedDef) && Object.keys(convertedDef.properties).length > 0) { | ||
DEBUG(convertedDef); | ||
object.properties = { | ||
...object.properties, | ||
...convertedDef.properties, | ||
}; | ||
return undefined; | ||
} | ||
} | ||
|
||
return convertedDef; | ||
}); | ||
|
||
const convertedSchema = filteredDefs.filter(isDefined); | ||
if (object.properties && Object.keys(object.properties).length > 0) { | ||
convertedSchema.push(object); | ||
} | ||
|
||
if (convertedSchema.length === 1) { | ||
const onlySchema = convertedSchema[0]; | ||
if (isReferenceObject(onlySchema) && nullable) { | ||
// ReferenceObject는 다른 속성들과 함께 사용할 수 없음 | ||
schema[property] = [onlySchema, { nullable }]; | ||
} else { | ||
schema = onlySchema; | ||
} | ||
} else if (convertedSchema.length > 1) { | ||
schema[property] = convertedSchema; | ||
} | ||
|
||
if (!isReferenceObject(schema)) { | ||
if (nullable) { | ||
schema.nullable = true; | ||
} | ||
} | ||
|
||
return schema; | ||
}; | ||
export const convertCombinedProperty = ( | ||
def: tjs.Definition, | ||
): Pick<oapi3.BaseSchemaObject, 'allOf' | 'anyOf' | 'oneOf' | 'not'> => { | ||
const { | ||
allOf, oneOf, anyOf, not, | ||
} = def; | ||
let schema: oapi3.BaseSchemaObject = {}; | ||
|
||
if (allOf) { | ||
const convertedSchemaArray = convertSchemaArray(allOf, 'allOf'); | ||
schema = { ...schema, ...convertedSchemaArray }; | ||
} | ||
|
||
if (oneOf) { | ||
const convertedSchemaArray = convertSchemaArray(oneOf, 'oneOf'); | ||
schema = { ...schema, ...convertedSchemaArray }; | ||
} | ||
|
||
if (anyOf) { | ||
const convertedSchemaArray = convertSchemaArray(anyOf, 'anyOf'); | ||
schema = { ...schema, ...convertedSchemaArray }; | ||
} | ||
|
||
if (not) { | ||
schema.not = convertDefinition(not); | ||
} | ||
|
||
return schema; | ||
}; | ||
|
||
export const extractCommonProperty = ( | ||
def: tjs.Definition, | ||
): Pick< | ||
oapi3.BaseSchemaObject, | ||
'title' | 'enum' | 'example' | 'description' | 'format' | 'default' | ||
> => { | ||
const { | ||
title, | ||
enum: _enum, | ||
examples, | ||
description, | ||
format, | ||
default: _default, | ||
} = def; | ||
return { | ||
title, | ||
enum: _enum, | ||
example: Array.isArray(examples) ? examples[0] : examples, | ||
description, | ||
format, | ||
default: _default, | ||
}; | ||
}; | ||
|
||
const covertToBooleanSchemaObject = (): oapi3.SchemaObject => ({ | ||
type: 'boolean', | ||
}); | ||
|
||
const covertToNumberSchemaObject = ( | ||
def: tjs.Definition, | ||
type: 'number' | 'integer' = 'number', | ||
): oapi3.SchemaObject => { | ||
const { | ||
multipleOf, maximum, exclusiveMaximum, exclusiveMinimum, minimum, | ||
} = def; | ||
return { | ||
type, | ||
multipleOf, | ||
maximum: maximum !== undefined ? maximum : exclusiveMaximum, | ||
exclusiveMaximum: exclusiveMaximum !== undefined ? true : undefined, | ||
minimum: minimum !== undefined ? minimum : exclusiveMinimum, | ||
exclusiveMinimum: exclusiveMinimum !== undefined ? true : undefined, | ||
}; | ||
}; | ||
|
||
const covertToStringSchemaObject = ( | ||
def: tjs.Definition, | ||
): oapi3.SchemaObject => { | ||
const { maxLength, minLength, pattern } = def; | ||
|
||
if (pattern) { | ||
const isValidRegExp = RegExp.prototype.test(pattern); | ||
if (!isValidRegExp) { | ||
throw Error(`${pattern} is not valid RegExp`); | ||
} | ||
} | ||
|
||
return { | ||
type: 'string', | ||
maxLength, | ||
minLength, | ||
}; | ||
}; | ||
|
||
const covertToArraySchemaObject = ( | ||
def: tjs.Definition, | ||
): oapi3.ArraySchemaObject => { | ||
const { | ||
items, maxItems, minItems, uniqueItems, | ||
} = def; // additionalItems, contains 제외 | ||
|
||
const convertedItems = items ? convertItems(items) : undefined; | ||
|
||
if (!convertedItems) { | ||
throw Error('array type need items'); | ||
} | ||
|
||
return { | ||
type: 'array', | ||
items: convertedItems, | ||
maxItems, | ||
minItems, | ||
uniqueItems, | ||
}; | ||
}; | ||
|
||
const covertToObjectSchemaObject = ( | ||
def: tjs.Definition, | ||
): oapi3.SchemaObject => { | ||
const commonSchema = extractCommonProperty(def); | ||
|
||
const { | ||
maxProperties, | ||
minProperties, | ||
required, | ||
properties, | ||
additionalProperties, | ||
} = def; | ||
|
||
const convertedAdditionalProperties = additionalProperties | ||
? convertDefinition(additionalProperties) | ||
: undefined; | ||
const convertedProperties = properties | ||
? convertProperties(properties) | ||
: undefined; | ||
|
||
return { | ||
type: 'object', | ||
maxProperties, | ||
minProperties, | ||
required, | ||
properties: convertedProperties, | ||
additionalProperties: convertedAdditionalProperties, | ||
...commonSchema, | ||
}; | ||
}; | ||
|
||
const convertSchemaObjectByType = (type: string, def: tjs.Definition) => { | ||
if (type === 'number' || type === 'integer') { | ||
return covertToNumberSchemaObject(def, type); | ||
} if (type === 'string') { | ||
return covertToStringSchemaObject(def); | ||
} if (type === 'object') { | ||
return covertToObjectSchemaObject(def); | ||
} if (type === 'array') { | ||
return covertToArraySchemaObject(def); | ||
} if (type === 'boolean') { | ||
return covertToBooleanSchemaObject(); | ||
} | ||
return { nullable: true }; | ||
}; | ||
|
||
const convertType = ( | ||
def: tjs.Definition, | ||
commonSchema: Schema, | ||
): Schema => { | ||
const types = def.type | ||
? Array.isArray(def.type) | ||
? def.type | ||
: [def.type] | ||
: []; | ||
|
||
let nullable = false; | ||
|
||
const splitedSchemas = types.map((type) => { | ||
if (type === 'null') { | ||
nullable = true; | ||
return undefined; | ||
} | ||
const ret = convertSchemaObjectByType(type, def); | ||
return ret; | ||
}); | ||
|
||
const nullableProperty = nullable ? { nullable } : {}; | ||
|
||
const refinedSchemas = splitedSchemas | ||
.filter(isDefined) | ||
.map((schema) => ({ ...schema, ...commonSchema })); // 모든 property는 동시에 만족해야함 | ||
|
||
const referenceObject = def.$ref | ||
? { | ||
$ref: def.$ref.replace(/[^A-Za-z0-9_.-]/g, '_').replace( | ||
/(__definitions_)(\w)/, | ||
'#/components/schemas/$2', | ||
), | ||
} | ||
: undefined; | ||
if (referenceObject) { | ||
refinedSchemas.push(referenceObject); | ||
} | ||
|
||
if (refinedSchemas.length === 0) { | ||
return { | ||
...commonSchema, | ||
...nullableProperty, | ||
}; | ||
} if (refinedSchemas.length === 1) { | ||
const onlySchema = refinedSchemas[0]; | ||
|
||
if (onlySchema && isReferenceObject(onlySchema)) { | ||
const baseSchema = { ...commonSchema, ...nullableProperty }; | ||
if (!isEmptyObject(baseSchema)) { | ||
// reference object는 baseSchema랑 같이 사용할 수 없음 | ||
return { | ||
allOf: [baseSchema, onlySchema], | ||
}; | ||
} | ||
return onlySchema; | ||
} | ||
|
||
return { | ||
...onlySchema, | ||
...nullableProperty, | ||
}; | ||
} | ||
return { | ||
anyOf: refinedSchemas, | ||
...nullableProperty, | ||
}; | ||
}; | ||
|
||
export const convertDefinition = ( | ||
def: tjs.DefinitionOrBoolean, | ||
): Schema | undefined => { | ||
if (isDefinitionBoolean(def)) { | ||
return undefined; | ||
} | ||
|
||
const commonProperty = extractCommonProperty(def); | ||
const combinedProperty = convertCombinedProperty(def); | ||
|
||
const commonSchema = { | ||
...commonProperty, | ||
...combinedProperty, | ||
}; | ||
|
||
return convertType(def, commonSchema); | ||
}; |
Oops, something went wrong.