Skip to content

Commit 4a9faba

Browse files
Copilotsamchon
andcommitted
Implement basic decorator transformer infrastructure
Co-authored-by: samchon <[email protected]>
1 parent cda0796 commit 4a9faba

File tree

5 files changed

+293
-21
lines changed

5 files changed

+293
-21
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import ts from "typescript";
2+
3+
import { FunctionalAssertParametersProgrammer } from "../functional/FunctionalAssertParametersProgrammer";
4+
5+
import { ITypiaContext } from "../../transformers/ITypiaContext";
6+
7+
export namespace DecoratorAssertParametersProgrammer {
8+
export interface IConfig {
9+
equals: boolean;
10+
}
11+
12+
export interface IProps {
13+
context: ITypiaContext;
14+
modulo: ts.LeftHandSideExpression;
15+
config: IConfig;
16+
method: ts.MethodDeclaration;
17+
expression: ts.Expression;
18+
init?: ts.Expression | undefined;
19+
}
20+
21+
export const write = (props: IProps): ts.Expression => {
22+
// Convert the method to a function declaration for the functional programmer
23+
const functionDeclaration = createFunctionDeclarationFromMethod(props.method);
24+
25+
// Reuse the functional programmer logic
26+
return FunctionalAssertParametersProgrammer.write({
27+
context: props.context,
28+
modulo: props.modulo,
29+
config: props.config,
30+
declaration: functionDeclaration,
31+
expression: props.expression,
32+
init: props.init,
33+
});
34+
};
35+
36+
const createFunctionDeclarationFromMethod = (
37+
method: ts.MethodDeclaration,
38+
): ts.FunctionDeclaration => {
39+
// Create a synthetic function declaration that matches the method signature
40+
return ts.factory.createFunctionDeclaration(
41+
method.modifiers?.filter(ts.isModifier),
42+
method.asteriskToken,
43+
ts.factory.createIdentifier("__method"),
44+
method.typeParameters,
45+
method.parameters,
46+
method.type,
47+
method.body || ts.factory.createBlock([]),
48+
);
49+
};
50+
}
Lines changed: 115 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,125 @@
11
import ts from "typescript";
22

33
import { ITypiaContext } from "./ITypiaContext";
4+
import { DecoratorGenericTransformer } from "./features/decorators/DecoratorGenericTransformer";
45

56
export namespace DecoratorTransformer {
7+
export interface IProps {
8+
context: ITypiaContext;
9+
method: ts.MethodDeclaration;
10+
}
11+
12+
export const transformMethod = (props: IProps): ts.MethodDeclaration => {
13+
const decorators = (props.method as any).decorators as ts.Decorator[] | undefined;
14+
if (!decorators) {
15+
return props.method;
16+
}
17+
18+
let transformedMethod = props.method;
19+
20+
// Process each decorator
21+
for (const decorator of decorators) {
22+
const result = transform({
23+
context: props.context,
24+
decorator,
25+
method: transformedMethod,
26+
});
27+
28+
if (result) {
29+
transformedMethod = result;
30+
}
31+
}
32+
33+
return transformedMethod;
34+
};
35+
636
export const transform = (props: {
737
context: ITypiaContext;
838
decorator: ts.Decorator;
9-
}): ts.Decorator | null => {
10-
// For now, just return the original decorator
11-
// TODO: Implement decorator transformation logic
12-
return props.decorator;
39+
method: ts.MethodDeclaration;
40+
}): ts.MethodDeclaration | null => {
41+
// Check if this is a typia decorator
42+
if (!ts.isCallExpression(props.decorator.expression)) {
43+
return null;
44+
}
45+
46+
const callExpression = props.decorator.expression;
47+
if (!ts.isPropertyAccessExpression(callExpression.expression)) {
48+
return null;
49+
}
50+
51+
const propertyAccess = callExpression.expression;
52+
if (!ts.isPropertyAccessExpression(propertyAccess.expression)) {
53+
return null;
54+
}
55+
56+
const typia = propertyAccess.expression;
57+
if (!ts.isIdentifier(typia.expression) || typia.expression.text !== "typia") {
58+
return null;
59+
}
60+
61+
if (!ts.isIdentifier(typia.name) || typia.name.text !== "decorators") {
62+
return null;
63+
}
64+
65+
const decoratorName = propertyAccess.name.text;
66+
67+
// Map decorator names to configurations
68+
const config = getDecoratorConfig(decoratorName);
69+
if (!config) {
70+
return null;
71+
}
72+
73+
// Use the generic transformer
74+
return DecoratorGenericTransformer.transform({
75+
method: decoratorName,
76+
config: config.config,
77+
programmer: config.programmer,
78+
})({
79+
context: props.context,
80+
decorator: props.decorator,
81+
method: props.method,
82+
expression: callExpression,
83+
});
84+
};
85+
86+
const getDecoratorConfig = (decoratorName: string) => {
87+
// Import the programmers dynamically to avoid circular dependencies
88+
const { DecoratorAssertParametersProgrammer } = require("../programmers/decorators/DecoratorAssertParametersProgrammer");
89+
90+
const configs: Record<string, DecoratorGenericTransformer.ISpecification> = {
91+
assert: {
92+
method: "assert",
93+
config: { equals: false },
94+
programmer: DecoratorAssertParametersProgrammer.write,
95+
},
96+
assertEquals: {
97+
method: "assertEquals",
98+
config: { equals: true },
99+
programmer: DecoratorAssertParametersProgrammer.write,
100+
},
101+
is: {
102+
method: "is",
103+
config: { equals: false },
104+
programmer: DecoratorAssertParametersProgrammer.write, // TODO: Create DecoratorIsParametersProgrammer
105+
},
106+
equals: {
107+
method: "equals",
108+
config: { equals: true },
109+
programmer: DecoratorAssertParametersProgrammer.write, // TODO: Create DecoratorIsParametersProgrammer
110+
},
111+
validate: {
112+
method: "validate",
113+
config: { equals: false },
114+
programmer: DecoratorAssertParametersProgrammer.write, // TODO: Create DecoratorValidateParametersProgrammer
115+
},
116+
validateEquals: {
117+
method: "validateEquals",
118+
config: { equals: true },
119+
programmer: DecoratorAssertParametersProgrammer.write, // TODO: Create DecoratorValidateParametersProgrammer
120+
},
121+
};
122+
123+
return configs[decoratorName];
13124
};
14125
}
Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,30 @@
11
import ts from "typescript";
22

33
import { CallExpressionTransformer } from "./CallExpressionTransformer";
4+
import { DecoratorTransformer } from "./DecoratorTransformer";
45
import { ITypiaContext } from "./ITypiaContext";
56

67
export namespace NodeTransformer {
78
export const transform = (props: {
89
context: ITypiaContext;
910
node: ts.Node;
10-
}): ts.Node | null =>
11-
ts.isCallExpression(props.node) && props.node.parent
12-
? CallExpressionTransformer.transform({
13-
context: props.context,
14-
expression: props.node,
15-
})
16-
: props.node;
11+
}): ts.Node | null => {
12+
// Handle call expressions
13+
if (ts.isCallExpression(props.node) && props.node.parent) {
14+
return CallExpressionTransformer.transform({
15+
context: props.context,
16+
expression: props.node,
17+
});
18+
}
19+
20+
// Handle method declarations with decorators
21+
if (ts.isMethodDeclaration(props.node) && (props.node as any).decorators) {
22+
return DecoratorTransformer.transformMethod({
23+
context: props.context,
24+
method: props.node,
25+
});
26+
}
27+
28+
return props.node;
29+
};
1730
}

src/transformers/features/decorators/DecoratorGenericTransformer.ts

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,98 @@ export namespace DecoratorGenericTransformer {
66
export interface IConfig {
77
equals: boolean;
88
}
9+
910
export interface ISpecification {
1011
method: string;
1112
config: IConfig;
1213
programmer: (p: {
1314
context: ITypiaContext;
1415
modulo: ts.LeftHandSideExpression;
1516
expression: ts.Expression;
16-
declaration: ts.MethodDeclaration;
17+
method: ts.MethodDeclaration;
1718
config: IConfig;
1819
init?: ts.Expression;
1920
}) => ts.Expression;
2021
}
22+
2123
export const transform =
22-
(_spec: ISpecification) =>
24+
(spec: ISpecification) =>
2325
(props: {
2426
context: ITypiaContext;
2527
decorator: ts.Decorator;
28+
method: ts.MethodDeclaration;
2629
expression: ts.CallExpression;
27-
}): ts.Decorator | null => {
28-
// This is a simplified placeholder - we need to transform the method
29-
// that this decorator is applied to. For now, return the original decorator
30-
return props.decorator;
30+
}): ts.MethodDeclaration => {
31+
// Get the error factory from decorator arguments (if provided)
32+
const init = props.expression.arguments[0];
33+
34+
// Create a module reference for the generated code
35+
const modulo = ts.factory.createPropertyAccessExpression(
36+
ts.factory.createIdentifier("typia"),
37+
"functional",
38+
);
39+
40+
// Generate the wrapper function using the programmer
41+
const wrapperExpression = spec.programmer({
42+
context: props.context,
43+
modulo,
44+
expression: createMethodReference(props.method),
45+
method: props.method,
46+
config: spec.config,
47+
init,
48+
});
49+
50+
// Create the transformed method body that calls the wrapper
51+
const transformedBody = createTransformedMethodBody({
52+
method: props.method,
53+
wrapperExpression,
54+
});
55+
56+
// Return the method with transformed body and removed decorators
57+
const decorators = (props.method as any).decorators as ts.Decorator[] | undefined;
58+
const otherDecorators = decorators?.filter(
59+
(d: ts.Decorator) => d !== props.decorator,
60+
);
61+
62+
// Use object spread to create a new method with updated properties
63+
const newMethod = Object.assign(Object.create(Object.getPrototypeOf(props.method)), props.method, {
64+
decorators: otherDecorators,
65+
body: transformedBody,
66+
});
67+
68+
return newMethod as ts.MethodDeclaration;
3169
};
70+
71+
const createMethodReference = (method: ts.MethodDeclaration): ts.Expression => {
72+
// Create a function expression that represents the original method
73+
return ts.factory.createArrowFunction(
74+
method.modifiers?.filter(ts.isModifier),
75+
method.typeParameters,
76+
method.parameters,
77+
method.type,
78+
ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
79+
method.body || ts.factory.createBlock([]),
80+
);
81+
};
82+
83+
const createTransformedMethodBody = (props: {
84+
method: ts.MethodDeclaration;
85+
wrapperExpression: ts.Expression;
86+
}): ts.Block => {
87+
// Create parameter references for the wrapper call
88+
const parameterNames = props.method.parameters.map((param) =>
89+
ts.isIdentifier(param.name) ? param.name : ts.factory.createIdentifier("param"),
90+
);
91+
92+
// Call the generated wrapper function with the parameters
93+
const wrapperCall = ts.factory.createCallExpression(
94+
props.wrapperExpression,
95+
undefined,
96+
parameterNames,
97+
);
98+
99+
return ts.factory.createBlock([
100+
ts.factory.createReturnStatement(wrapperCall),
101+
]);
102+
};
32103
}

test/src/features/decorators/test_decorator_basic.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,45 @@ interface UserQuery {
66
}
77

88
class UserService {
9-
// Test the decorator - for now it's just a stub
9+
// Test the decorator - should validate parameters
1010
@typia.decorators.assertEquals()
1111
async findMany(query: UserQuery): Promise<UserQuery[]> {
12-
// Implementation here
12+
// This should be validated by the decorator
13+
return [query];
14+
}
15+
16+
// Test regular method for comparison
17+
async findManyUnsafe(query: UserQuery): Promise<UserQuery[]> {
1318
return [query];
1419
}
1520
}
1621

17-
export const test_decorator_basic = (): void => {
18-
console.log("Decorators module is available");
22+
export const test_decorator_basic = async (): Promise<void> => {
23+
console.log("Testing decorators module...");
1924

2025
// Test that we can instantiate the class with decorator
2126
const service = new UserService();
2227
console.log("Service created successfully with decorator");
28+
29+
// Test valid input
30+
const validQuery: UserQuery = { name: "John", age: 30 };
31+
try {
32+
const result = await service.findMany(validQuery);
33+
console.log("Valid query succeeded:", result);
34+
} catch (error) {
35+
console.error("Valid query failed:", error);
36+
throw error;
37+
}
38+
39+
// Test invalid input should throw an error
40+
const invalidQuery = { name: "John", age: "thirty" } as any;
41+
try {
42+
const result = await service.findMany(invalidQuery);
43+
console.error("Invalid query should have failed but didn't:", result);
44+
throw new Error("Invalid query should have thrown an error");
45+
} catch (error) {
46+
console.log("Invalid query correctly failed:", error.message);
47+
}
48+
49+
console.log("All decorator tests passed!");
2350
};

0 commit comments

Comments
 (0)