Skip to content
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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
397 changes: 397 additions & 0 deletions packages/tspec/src/generator/jsonSchemaConverter.ts
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);
};

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:

  1. The code follows an object-oriented approach with functions and constants defined using const and export. It provides utility functions such as isNullableObject, isObjectSchemaObject, isReferenceObject, etc., for schema validation and conversion.

  2. There are several helper functions like createItem, convertItems, convertProperties, convertSchemaArray, etc., that handle specific conversion scenarios based on the given schema properties.

  3. The code uses type guards (defOrBool is boolean) and type checking to ensure proper handling of different cases.

  4. 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.

  5. 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:

  1. Some of the functions lack proper error handling. For example, in covertToArraySchemaObject, if convertedItems is not defined, it throws an error but doesn't provide any context for debugging. Consider adding more meaningful error messages or exception handling.

  2. The code relies on some external dependencies such as debug, openapi-types, and typescript-json-schema. Ensure that these dependencies are correctly installed and compatible with the code.

  3. Although the code looks well-structured, it would benefit from additional comments explaining the purpose and functionality of each major section or function.

  4. 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.

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 for converting TypeScript JSON schema objects into OpenAPIv3 schema objects. Here are some suggestions and potential areas of improvement:

  1. Code Formatting: Ensure consistent code formatting throughout the file to improve readability. Consider using an automatic code formatter or adhering to a style guide.

  2. Naming Conventions: Follow common naming conventions for variables, functions, and constants. For example, use camelCase for variable and function names, and use uppercase for constant names.

  3. Dependency Management: Make sure to manage dependencies properly, preferably using a dependency management tool like npm or yarn. Check if the required dependencies are already installed.

  4. Error Handling: Add appropriate error handling and validation checks in functions like covertToStringSchemaObject to handle invalid regular expressions.

  5. Documentation: Include inline comments explaining the purpose and functionality of complex functions, especially where code logic isn't immediately obvious.

  6. Consistency: Ensure consistent usage of brackets and indentation throughout the codebase to enhance readability and maintainability.

  7. Testing & Integration: Consider adding unit tests to verify the correctness of the conversion functions. Additionally, integrate the module into a larger project (if applicable) and perform end-to-end testing to validate its functionality.

  8. Review Origin: Consider reviewing the original source or context of this code patch to better understand its intended use case and any specific requirements it may have.

Remember to thoroughly test the code and adapt it to your specific needs before deploying it to a production environment.

Loading