diff --git a/package.json b/package.json index 5d8bad80cd..bdef083c60 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "typescript-json", - "version": "2.0.1", + "version": "2.0.2", "description": "Faster JSON stringify with only one line", "main": "lib/index.js", "typings": "lib/index.d.ts", diff --git a/src/factories/FunctionFactory.ts b/src/factories/FunctionFactory.ts new file mode 100644 index 0000000000..5d8c96eeb1 --- /dev/null +++ b/src/factories/FunctionFactory.ts @@ -0,0 +1,139 @@ +import crypto from "crypto"; +import path from "path"; +import ts from "typescript"; + +import { JsonFactory } from "../factories/JsonFactory"; +import { SchemaFactory } from "../factories/SchemaFactory"; +import { IProject } from "../structures/IProject"; +import { ISchema } from "../structures/ISchema"; + +export namespace FunctionFactory +{ + export type Closure = + ( + project: IProject, + expression: ts.CallExpression, + type: ts.Type | null + ) => ts.Expression; + + export function generate + ( + project: IProject, + expression: ts.CallExpression + ): Closure | null + { + //---- + // VALIDATIONS + //---- + // SIGNATURE + const signature: ts.Signature | undefined = project.checker.getResolvedSignature(expression); + if (!signature || !signature.declaration) + return null; + + // EXACT FILE + const file: string = path.resolve(signature.declaration.getSourceFile().fileName); + if (file !== LIB_PATH && file !== SRC_PATH) + return null; + + // FIND FUNCTION + const name: string = project.checker.getTypeAtLocation(signature.declaration).symbol.name; + const generator: IGenerator | undefined = FUNCTORS[name]; + + if (generator === undefined) + return null; + else if (expression.arguments.length !== generator.count) + return null; + + // RETURNS + return generator.argument(); + } + + function argue_stringify + ( + project: IProject, + expression: ts.CallExpression, + type: ts.Type | null + ): ts.ArrayLiteralExpression + { + if (type === null) + { + const top: ts.Expression = expression.arguments[0]! + type = project.checker.getTypeAtLocation(top); + } + + const app: ISchema.IApplication | null = SchemaFactory.generate(project.checker, type); + const tuple: ts.ArrayLiteralExpression = JsonFactory.application(app); + const script: string = project.printer.printNode + ( + ts.EmitHint.Unspecified, + tuple, + expression.getSourceFile() + ); + const key: string = crypto + .createHash("sha256") + .update(script) + .digest("base64"); + + return ts.factory.createArrayLiteralExpression + ([ + ts.factory.createStringLiteral(key), + ts.factory.createArrowFunction + ( + undefined, + undefined, + [], + undefined, + undefined, + tuple + ) + ]); + } + + function argue_stringifier + ( + project: IProject, + expression: ts.CallExpression, + type: ts.Type | null + ): ts.ArrowFunction + { + if (type === null) + { + const file: ts.SourceFile = expression.getSourceFile(); + const { line, character } = file.getLineAndCharacterOfPosition(expression.pos); + + throw new Error(`Error on TSON.createStringifier(): the generic argument must be specified - ${file.fileName}:${line + 1}:${character + 1}.`); + } + + const app: ISchema.IApplication | null = SchemaFactory.generate(project.checker, type); + const tuple: ts.ArrayLiteralExpression = JsonFactory.application(app); + + return ts.factory.createArrowFunction + ( + undefined, + undefined, + [], + undefined, + undefined, + tuple + ); + } + + interface IGenerator + { + count: number; + argument: () => Closure; + } + + const LIB_PATH = path.resolve(path.join(__dirname, "..", "module.d.ts")); + const SRC_PATH = path.resolve(path.join(__dirname, "..", "module.ts")); + const FUNCTORS: Record = { + stringify: { + count: 1, + argument: () => argue_stringify, + }, + createStringifier: { + count: 0, + argument: () => argue_stringifier, + } + } +} \ No newline at end of file diff --git a/src/functional/argue_stringifier.ts b/src/functional/argue_stringifier.ts deleted file mode 100644 index 2d5dbfcc09..0000000000 --- a/src/functional/argue_stringifier.ts +++ /dev/null @@ -1,22 +0,0 @@ -import ts from "typescript"; -import { JsonFactory } from "../factories/JsonFactory"; - -import { SchemaFactory } from "../factories/SchemaFactory"; -import { IProject } from "../structures/IProject"; -import { ISchema } from "../structures/ISchema"; - -export function argue_stringifier(project: IProject, type: ts.Type,): ts.ArrowFunction -{ - const app: ISchema.IApplication | null = SchemaFactory.generate(project.checker, type); - const tuple: ts.ArrayLiteralExpression = JsonFactory.application(app); - - return ts.factory.createArrowFunction - ( - undefined, - undefined, - [], - undefined, - undefined, - tuple - ); -} \ No newline at end of file diff --git a/src/functional/argue_stringify.ts b/src/functional/argue_stringify.ts deleted file mode 100644 index 94236e2573..0000000000 --- a/src/functional/argue_stringify.ts +++ /dev/null @@ -1,42 +0,0 @@ -import crypto from "crypto"; -import ts from "typescript"; - -import { JsonFactory } from "../factories/JsonFactory"; -import { SchemaFactory } from "../factories/SchemaFactory"; -import { IProject } from "../structures/IProject"; -import { ISchema } from "../structures/ISchema"; - -export function argue_stringify - ( - project: IProject, - type: ts.Type, - node: ts.TypeNode, - ): ts.ArrayLiteralExpression -{ - const app: ISchema.IApplication | null = SchemaFactory.generate(project.checker, type); - const tuple: ts.ArrayLiteralExpression = JsonFactory.application(app); - const script: string = project.printer.printNode - ( - ts.EmitHint.Unspecified, - tuple, - node.getSourceFile() - ); - const key: string = crypto - .createHash("sha256") - .update(script) - .digest("base64"); - - return ts.factory.createArrayLiteralExpression - ([ - ts.factory.createStringLiteral(key), - ts.factory.createArrowFunction - ( - undefined, - undefined, - [], - undefined, - undefined, - tuple - ) - ]); -} \ No newline at end of file diff --git a/src/functional/get_json_function.ts b/src/functional/get_json_function.ts deleted file mode 100644 index 627f0ba616..0000000000 --- a/src/functional/get_json_function.ts +++ /dev/null @@ -1,69 +0,0 @@ -import path from "path"; -import ts from "typescript"; - -import { IProject } from "../structures/IProject"; -import { argue_stringifier } from "./argue_stringifier"; -import { argue_stringify } from "./argue_stringify"; - -export function get_json_function - ( - project: IProject, - expression: ts.CallExpression - ): ReturnType | null -{ - //---- - // VALIDATIONS - //---- - // SIGNATURE - const signature: ts.Signature | undefined = project.checker.getResolvedSignature(expression); - if (!signature || !signature.declaration) - return null; - - // EXACT FILE - const file: string = path.resolve(signature.declaration.getSourceFile().fileName); - if (file !== LIB_PATH && file !== SRC_PATH) - return null; - - // FIND FUNCTION - const name: string = project.checker.getTypeAtLocation(signature.declaration).symbol.name; - const func: IFunction | undefined = FUNCTORS[name]; - - if (func === undefined) - return null; - else if (expression.arguments.length !== func.count) - return null; - else if (expression.typeArguments?.length !== 1) - { - const file: ts.SourceFile = expression.getSourceFile(); - const { line, character } = file.getLineAndCharacterOfPosition(expression.pos); - - throw new Error(`Error on TSON.${name}(): the generic argument must be specified - ${file.fileName}:${line + 1}:${character + 1}.`); - } - - // RETURNS - return func.argument(); -} - -const LIB_PATH = path.resolve(path.join(__dirname, "..", "module.d.ts")); -const SRC_PATH = path.resolve(path.join(__dirname, "..", "module.ts")); -const FUNCTORS: Record = { - stringify: { - count: 1, - argument: () => argue_stringify, - }, - createStringifier: { - count: 0, - argument: () => argue_stringifier, - } -} - -interface IFunction -{ - count: number; - argument(): - ( - project: IProject, - type: ts.Type, - node: ts.TypeNode, - ) => ts.Expression; -} \ No newline at end of file diff --git a/src/module.ts b/src/module.ts index ef5d2cb95e..dcb36e0d1b 100644 --- a/src/module.ts +++ b/src/module.ts @@ -60,6 +60,34 @@ export function stringify return JsonMemory.stringify(key, closure)(input); } +/** + * > You must configure the generic argument `T`. + * + * 2x faster `JSON.stringify()` function generator. + * + * Creates a function who can convert TypeScript value to the JSON (JavaSript Object Noation) + * string. directly. The returned function is two times faster than the native + * `JSON.stringify()`, because the function constructs a dedicated JSON string builder + * only for the type `T`. + * + * Also, the returned function is always reusable until you forget the returned function + * variable. Of course, it means that you've to manage the returned function by yourself. + * If you feel annoying for management, you can choose {@link stringify} instead. The + * {@link stringify} function stores the type `T` and its JSON string builder in the + * global memory and reused whenever the {@link stringify} function be called with the + * same type `T`. + * + * On the other hand, if you've encountered an error which starts from the + * "no transform has been configured" message when calling this {@link createStringifier} + * function, it means that you haven't configured the `tsconfig.json` file. Visit the + * https://github.com/samchon/typescript-json and configure the `tsconfig.json` file follow + * the [README](https://github.com/samchon/typescript-json#tsconfigjson) content. + * + * @return 2x faster `JSON.stringify()` function. + * @author Jeongho Nam - https://github.com/samchon + */ +export function createStringifier(): never; + /** * 2x faster `JSON.stringify()` function generator. * diff --git a/src/transformers/NodeTransformer.ts b/src/transformers/NodeTransformer.ts index e95aa5503c..a401deee97 100644 --- a/src/transformers/NodeTransformer.ts +++ b/src/transformers/NodeTransformer.ts @@ -1,7 +1,7 @@ import ts from "typescript"; +import { FunctionFactory } from "../factories/FunctionFactory"; import { IProject } from "../structures/IProject"; -import { get_json_function } from "../functional/get_json_function"; export namespace NodeTransformer { @@ -14,19 +14,25 @@ export namespace NodeTransformer if (!ts.isCallExpression(expression)) return expression; - const func = get_json_function(project, expression); + const func: FunctionFactory.Closure | null = FunctionFactory.generate + ( + project, + expression + ); if (func === null) return expression; - const generic: ts.TypeNode = expression.typeArguments![0]!; - const type: ts.Type = project.checker.getTypeFromTypeNode(generic); + const noe: ts.TypeNode | null = (expression.typeArguments || [])[0] || null; + const type: ts.Type | null = noe + ? project.checker.getTypeFromTypeNode(noe) + : null; return ts.factory.updateCallExpression ( expression, expression.expression, expression.typeArguments, - [ ...expression.arguments, func(project, type, generic) ] + [ ...expression.arguments, func(project, expression, type) ] ); } } \ No newline at end of file diff --git a/test/features/test_stringify_atomic_implicit.ts b/test/features/test_stringify_atomic_implicit.ts new file mode 100644 index 0000000000..de38052352 --- /dev/null +++ b/test/features/test_stringify_atomic_implicit.ts @@ -0,0 +1,18 @@ +import TSON from "../../src"; + +export function test_stringify_atomic_implicit(): void +{ + test(input => TSON.stringify(input), true); + test(input => TSON.stringify(input), 3); + test(input => TSON.stringify(input), "hello"); + test(input => TSON.stringify(input), null); +} + +function test(stringify: (input: T) => string, input: T) +{ + const json: string = stringify(input); + const expected: string = JSON.stringify(input); + + if (json !== expected) + throw new Error("Bug on TSON.stringify(): failed to understand the implicit atomic type."); +} \ No newline at end of file diff --git a/test/features/test_stringify_class_closure.ts b/test/features/test_stringify_class_closure.ts index 24bd4d6497..325738e95d 100644 --- a/test/features/test_stringify_class_closure.ts +++ b/test/features/test_stringify_class_closure.ts @@ -1,6 +1,6 @@ import TSON from "../../src"; -export function test_stringify_class_closure() +export function test_stringify_class_closure(): void { const something: Something = new Something("1234"); const json: string = TSON.stringify(something); diff --git a/test/features/test_stringify_object_implicit.ts b/test/features/test_stringify_object_implicit.ts new file mode 100644 index 0000000000..391fc865d7 --- /dev/null +++ b/test/features/test_stringify_object_implicit.ts @@ -0,0 +1,23 @@ +import TSON from "../../src"; +import { RandomGenerator } from "../internal/RandomGenerator"; + +export function test_stringify_object_implicit(): void +{ + const lecture: ILecture = { + name: RandomGenerator.string(), + professor: RandomGenerator.string(), + grade: RandomGenerator.number() + }; + const json: string = TSON.stringify(lecture); + const expected: string = JSON.stringify(lecture); + + if (expected !== json) + throw new Error(`Bug on TSON.stringify(): failed to understand the implicit object.`); +} + +interface ILecture +{ + name: string; + professor: string; + grade: number; +} \ No newline at end of file diff --git a/test/manual.js b/test/manual.js index f0e9dde597..642bf91a4a 100644 --- a/test/manual.js +++ b/test/manual.js @@ -7,7 +7,7 @@ runner.register({ compiler: "ttypescript" }); -const program = require("./features/test_stringify_object_intersection.ts"); +const program = require("./features/test_stringify_object_implicit.ts"); for (const value of Object.values(program)) if (typeof value === "function") value(); \ No newline at end of file