-
Notifications
You must be signed in to change notification settings - Fork 6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
json schema convert #30
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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); | ||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Overall, the code appears to be a TypeScript module for converting TypeScript JSON schema objects into OpenAPIv3 schema objects. Here are some suggestions and potential areas of improvement:
Remember to thoroughly test the code and adapt it to your specific needs before deploying it to a production environment. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall, the code appears to be a TypeScript module that converts TypeScript JSON schema definitions into OpenAPIv3 schema objects. Here's a brief review of the code:
The code follows an object-oriented approach with functions and constants defined using
const
andexport
. It provides utility functions such asisNullableObject
,isObjectSchemaObject
,isReferenceObject
, etc., for schema validation and conversion.There are several helper functions like
createItem
,convertItems
,convertProperties
,convertSchemaArray
, etc., that handle specific conversion scenarios based on the given schema properties.The code uses type guards (
defOrBool is boolean
) and type checking to ensure proper handling of different cases.The
convertType
function determines the appropriate schema type based on the provided TypeScript JSON schema definition. It handles multiple types, nullable types, and reference objects.The
convertDefinition
function is the entry point for converting a schema definition. It invokes other helper functions to convert different parts of the schema and combines them into a final OpenAPIv3 schema object.Possible bug risks or improvement suggestions:
Some of the functions lack proper error handling. For example, in
covertToArraySchemaObject
, ifconvertedItems
is not defined, it throws an error but doesn't provide any context for debugging. Consider adding more meaningful error messages or exception handling.The code relies on some external dependencies such as
debug
,openapi-types
, andtypescript-json-schema
. Ensure that these dependencies are correctly installed and compatible with the code.Although the code looks well-structured, it would benefit from additional comments explaining the purpose and functionality of each major section or function.
Since this is only a code snippet and doesn't include the usage context, it's hard to assess the overall correctness and completeness of the implementation. Make sure to test the code with various TypeScript JSON schema definitions to ensure it produces the expected OpenAPIv3 schema objects.
Note: It's recommended to follow best practices for error handling, testing, and maintaining dependencies in your specific use case.