Skip to content

Commit

Permalink
Close #575 - new function literals()
Browse files Browse the repository at this point in the history
  • Loading branch information
samchon committed Apr 7, 2023
1 parent 1fbcc20 commit 0d60af4
Show file tree
Hide file tree
Showing 12 changed files with 217 additions and 42 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"prettier": "prettier --write ./**/*.ts",
"-----------------------------------------------": "",
"issue": "node test/issue",
"issue:generate": "ts-node src/executable/typia generate --input test/issues/generate --output test/issues/generate/output --project test/tsconfig.json",
"issue:generate": "ts-node src/executable/typia generate --input test/issues/generate/input --output test/issues/generate --project test/tsconfig.json",
"test": "node bin/test",
"test:manual": "node test/manual",
"------------------------------------------------": "",
Expand Down
43 changes: 43 additions & 0 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { Namespace } from "./functional/Namespace";
import { IMetadataApplication } from "./metadata/IMetadataApplication";
import { IJsonApplication } from "./schemas/IJsonApplication";

import { Atomic } from "./typings/Atomic";

import { MapUtil } from "./utils/MapUtil";

import { CustomValidatorMap } from "./CustomValidatorMap";
Expand Down Expand Up @@ -942,6 +944,47 @@ export function random(): never {
}
Object.assign(random, Namespace.random());

/**
* > You must configure the generic argument `T`.
*
* Union literal type to array.
*
* Converts a union literal type to an array of its members.
*
* ```typescript
* literals<"A" | "B" | 1>; // [1, 2, 3] as const
* ```
*
* @template T Union literal type
* @return Array of union literal type's members
*
* @author Jeongho Nam - https://github.com/samchon
*/
export function literals(): never;

/**
* Union literal type to array.
*
* Converts a union literal type to an array of its members.
*
* ```typescript
* literals<"A" | "B" | 1>; // [1, 2, 3] as const
* ```
*
* @template T Union literal type
* @return Array of union literal type's members
*
* @author Jeongho Nam - https://github.com/samchon
*/
export function literals<T extends Atomic.Type>(): T[];

/**
* @internal
*/
export function literals(): never {
halt("literals");
}

/**
* Clone a data.
*
Expand Down
65 changes: 65 additions & 0 deletions src/programmers/LiteralsProgrammer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import ts from "typescript";

import { MetadataCollection } from "../factories/MetadataCollection";
import { MetadataFactory } from "../factories/MetadataFactory";

import { Metadata } from "../metadata/Metadata";

import { IProject } from "../transformers/IProject";

import { Atomic } from "../typings/Atomic";

import { ArrayUtil } from "../utils/ArrayUtil";

export namespace LiteralsProgrammer {
export const generate = (project: IProject) => (type: ts.Type) => {
const meta: Metadata = MetadataFactory.generate(
project.checker,
new MetadataCollection(),
type,
{
resolve: true,
constant: true,
validate: (meta) => {
const length: number =
meta.constants
.map((c) => c.values.length)
.reduce((a, b) => a + b, 0) +
meta.atomics.filter((type) => type === "boolean")
.length;
if (0 === length) throw new Error(ErrorMessages.NO);
else if (meta.size() !== length)
throw new Error(ErrorMessages.ONLY);
},
},
);
const values: Set<Atomic.Type> = new Set([
...ArrayUtil.flat<Atomic.Type>(meta.constants.map((c) => c.values)),
...(meta.atomics.filter((type) => type === "boolean").length
? [true, false]
: []),
]);
return ts.factory.createAsExpression(
ts.factory.createArrayLiteralExpression(
[...values].map((v) =>
typeof v === "boolean"
? v
? ts.factory.createTrue()
: ts.factory.createFalse()
: typeof v === "number"
? ts.factory.createNumericLiteral(v)
: typeof v === "bigint"
? ts.factory.createBigIntLiteral(v.toString())
: ts.factory.createStringLiteral(v),
),
true,
),
ts.factory.createTypeReferenceNode("const"),
);
};
}

enum ErrorMessages {
NO = "Error on typia.literals(): no literal type found.",
ONLY = "Error on typia.literals(): only literal type allowed.",
}
2 changes: 2 additions & 0 deletions src/transformers/CallExpressionTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { CreateValidateCloneTransformer } from "./features/miscellaneous/CreateV
import { CreateValidatePruneTransformer } from "./features/miscellaneous/CreateValidatePruneTransformer";
import { IsCloneTransformer } from "./features/miscellaneous/IsCloneTransformer";
import { IsPruneTransformer } from "./features/miscellaneous/IsPruneTransformer";
import { LiteralsTransformer } from "./features/miscellaneous/LiteralsTransformer";
import { MetadataTransformer } from "./features/miscellaneous/MetadataTransformer";
import { PruneTransformer } from "./features/miscellaneous/PruneTransformer";
import { RandomTransformer } from "./features/miscellaneous/RandomTransformer";
Expand Down Expand Up @@ -122,6 +123,7 @@ const FUNCTORS: Record<string, () => Task> = {
// MISC
metadata: () => MetadataTransformer.transform,
random: () => RandomTransformer.transform,
literals: () => LiteralsTransformer.transform,

clone: () => CloneTransformer.transform,
assertClone: () => AssertCloneTransformer.transform,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export namespace ApplicationTransformer {
expression: ts.CallExpression,
): ts.Expression {
if (!expression.typeArguments?.length)
throw new Error(ErrorMessages.NO_GENERIC_ARGUMENT);
throw new Error(NO_GENERIC_ARGUMENT);

//----
// GET ARGUMENTS
Expand All @@ -34,7 +34,7 @@ export namespace ApplicationTransformer {
checker.getTypeFromTypeNode(child as ts.TypeNode),
);
if (types.some((t) => t.isTypeParameter()))
throw new Error(ErrorMessages.GENERIC_ARGUMENT);
throw new Error(GENERIC_ARGUMENT);

// ADDITIONAL PARAMETERS
const purpose: "swagger" | "ajv" = get_parameter(
Expand Down Expand Up @@ -68,7 +68,7 @@ export namespace ApplicationTransformer {
constant: true,
validate: (meta) => {
if (meta.atomics.find((str) => str === "bigint"))
throw new Error(ErrorMessages.NO_BIGIT);
throw new Error(NO_BIGIT);
},
}),
);
Expand Down Expand Up @@ -112,9 +112,8 @@ export namespace ApplicationTransformer {
}
}

const enum ErrorMessages {
NO_GENERIC_ARGUMENT = "Error on typia.application(): no generic argument.",
GENERIC_ARGUMENT = "Error on typia.application(): non-specified generic argument(s).",
NO_BIGIT = "Error on typia.application(): does not allow bigint type.",
NO_ZERO_LENGTH_TUPLE = "Error on typia.application(): swagger does not support zero length tuple type.",
}
const NO_GENERIC_ARGUMENT =
"Error on typia.application(): no generic argument.";
const GENERIC_ARGUMENT =
"Error on typia.application(): non-specified generic argument(s).";
const NO_BIGIT = "Error on typia.application(): does not allow bigint type.";
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,13 @@ export namespace CreateRandomTransformer {
expression: ts.CallExpression,
): ts.Expression {
// CHECK GENERIC ARGUMENT EXISTENCE
if (!expression.typeArguments?.[0])
throw new Error(ErrorMessages.NOT_SPECIFIED);
if (!expression.typeArguments?.[0]) throw new Error(NOT_SPECIFIED);

// GET TYPE INFO
const node: ts.TypeNode = expression.typeArguments[0];
const type: ts.Type = project.checker.getTypeFromTypeNode(node);

if (type.isTypeParameter())
throw new Error(ErrorMessages.NO_GENERIC_ARGUMENT);
if (type.isTypeParameter()) throw new Error(NO_GENERIC_ARGUMENT);

// DO TRANSFORM
return RandomProgrammer.generate(
Expand All @@ -37,7 +35,7 @@ export namespace CreateRandomTransformer {
}
}

const enum ErrorMessages {
NOT_SPECIFIED = "Error on typia.createRandom(): generic argument is not specified.",
NO_GENERIC_ARGUMENT = "Error on typia.createRandom(): non-specified generic argument.",
}
const NOT_SPECIFIED =
"Error on typia.createRandom(): generic argument is not specified.";
const NO_GENERIC_ARGUMENT =
"Error on typia.createRandom(): non-specified generic argument.";
30 changes: 30 additions & 0 deletions src/transformers/features/miscellaneous/LiteralsTransformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import ts from "typescript";

import { LiteralsProgrammer } from "../../../programmers/LiteralsProgrammer";

import { IProject } from "../../IProject";

export namespace LiteralsTransformer {
export function transform(
project: IProject,
_modulo: ts.LeftHandSideExpression,
expression: ts.CallExpression,
): ts.Expression {
// CHECK GENERIC ARGUMENT EXISTENCE
if (!expression.typeArguments?.[0]) throw new Error(NOT_SPECIFIED);

// GET TYPE INFO
const node: ts.TypeNode = expression.typeArguments[0];
const type: ts.Type = project.checker.getTypeFromTypeNode(node);

if (type.isTypeParameter()) throw new Error(NO_GENERIC_ARGUMENT);

// DO TRANSFORM
return LiteralsProgrammer.generate(project)(type);
}
}

const NOT_SPECIFIED =
"Error on typia.literals(): generic argument is not specified.";
const NO_GENERIC_ARGUMENT =
"Error on typia.literals(): non-specified generic argument.";
11 changes: 5 additions & 6 deletions src/transformers/features/miscellaneous/MetadataTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export namespace MetadataTransformer {
expression: ts.CallExpression,
): ts.Expression {
if (!expression.typeArguments?.length)
throw new Error(ErrorMessages.NO_GENERIC_ARGUMENT);
throw new Error(NO_GENERIC_ARGUMENT);

// VALIDATE TUPLE ARGUMENTS
const top: ts.Node = expression.typeArguments[0]!;
Expand All @@ -29,7 +29,7 @@ export namespace MetadataTransformer {
checker.getTypeFromTypeNode(child as ts.TypeNode),
);
if (types.some((t) => t.isTypeParameter()))
throw new Error(ErrorMessages.GENERIC_ARGUMENT);
throw new Error(GENERIC_ARGUMENT);

// METADATA
const collection: MetadataCollection = new MetadataCollection();
Expand All @@ -49,7 +49,6 @@ export namespace MetadataTransformer {
}
}

const enum ErrorMessages {
NO_GENERIC_ARGUMENT = "Error on typia.metadata(): no generic argument.",
GENERIC_ARGUMENT = "Error on typia.metadata(): non-specified generic argument(s).",
}
const NO_GENERIC_ARGUMENT = "Error on typia.metadata(): no generic argument.";
const GENERIC_ARGUMENT =
"Error on typia.metadata(): non-specified generic argument(s).";
14 changes: 6 additions & 8 deletions src/transformers/features/miscellaneous/RandomTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,13 @@ export namespace RandomTransformer {
expression: ts.CallExpression,
): ts.Expression {
// CHECK GENERIC ARGUMENT EXISTENCE
if (!expression.typeArguments?.[0])
throw new Error(ErrorMessages.NOT_SPECIFIED);
if (!expression.typeArguments?.[0]) throw new Error(NOT_SPECIFIED);

// GET TYPE INFO
const node: ts.TypeNode = expression.typeArguments[0];
const type: ts.Type = project.checker.getTypeFromTypeNode(node);

if (type.isTypeParameter())
throw new Error(ErrorMessages.NO_GENERIC_ARGUMENT);
if (type.isTypeParameter()) throw new Error(NO_GENERIC_ARGUMENT);

// DO TRANSFORM
return ts.factory.createCallExpression(
Expand All @@ -42,7 +40,7 @@ export namespace RandomTransformer {
}
}

const enum ErrorMessages {
NOT_SPECIFIED = "Error on typia.random(): generic argument is not specified.",
NO_GENERIC_ARGUMENT = "Error on typia.random(): non-specified generic argument.",
}
const NOT_SPECIFIED =
"Error on typia.random(): generic argument is not specified.";
const NO_GENERIC_ARGUMENT =
"Error on typia.random(): non-specified generic argument.";
14 changes: 14 additions & 0 deletions test/features/literals/test_literals_constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import typia from "../../../src";
import { primitive_equal_to } from "../../helpers/primitive_equal_to";

export function test_literals_constants(): void {
const values = typia.literals<Values>();
values.sort();

if (!primitive_equal_to(values, ["A", "B", 1, 2, 3].sort()))
throw Error(
"Bug on typia.literals(): failed to understand constant type.",
);
}

type Values = "A" | "B" | 1 | 2 | 3;
20 changes: 20 additions & 0 deletions test/features/literals/test_literals_enumeration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import typia from "../../../src";
import { primitive_equal_to } from "../../helpers/primitive_equal_to";

export function test_literals_enumeration(): void {
const values = typia.literals<TestEnum>();
values.sort();

if (!primitive_equal_to(values, ["A", "B", 1, 2, 3].sort()))
throw Error(
"Bug on typia.literals(): failed to understand enumeration type.",
);
}

enum TestEnum {
A = "A",
B = "B",
one = 1,
two = 2,
three = 3,
}
27 changes: 17 additions & 10 deletions test/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import fs from "fs";

import { DynamicImportIterator } from "./helpers/DynamicImportIterator";
import { IPointer } from "./helpers/IPointer";

Expand All @@ -16,17 +18,22 @@ async function main(): Promise<void> {
},
);

console.log("-------------------------------------------------------");
console.log(" GENERATION TESTING");
console.log("-------------------------------------------------------");
if (fs.existsSync(__dirname + "/generated/output")) {
console.log("-------------------------------------------------------");
console.log(" GENERATION TESTING");
console.log("-------------------------------------------------------");

exceptions.push(
...(await DynamicImportIterator.force(__dirname + "/generated/output", {
prefix: "test",
parameters: () => [],
counter,
})),
);
exceptions.push(
...(await DynamicImportIterator.force(
__dirname + "/generated/output",
{
prefix: "test",
parameters: () => [],
counter,
},
)),
);
}

// TERMINATE
if (exceptions.length === 0) console.log("Success", counter.value);
Expand Down

0 comments on commit 0d60af4

Please sign in to comment.