Skip to content
Closed
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
63 changes: 49 additions & 14 deletions src/factories/MetadataCommentTagFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -524,21 +524,56 @@ const PARSER: Record<
],
};
},
pattern: ({ value }) => ({
string: [
{
name: `Pattern<${JSON.stringify(value)}>`,
target: "string",
kind: "pattern",
value: value,
validate: `RegExp(${JSON.stringify(value)}).test($input)`,
exclusive: ["format"],
schema: {
pattern: value,
pattern: ({ value }) => {
// Support regex literal syntax: /pattern/flags
const match = value.match(/^\/(.*)\/([dgimsuy]*)$/);
if (match !== null) {
const [, pattern, flags] = match;
return {
string: [
{
name:
flags === ""
? `Pattern<${JSON.stringify(pattern)}>`
: `Pattern<${JSON.stringify(pattern)}, ${JSON.stringify(flags)}>`,
target: "string",
kind: "pattern",
value: pattern,
validate:
flags === ""
? `RegExp(${JSON.stringify(pattern)}).test($input)`
: `RegExp(${JSON.stringify(pattern)}, ${JSON.stringify(flags)}).test($input)`,
exclusive: ["format"],
schema:
flags === ""
? {
pattern: pattern,
}
: {
pattern: pattern,
"x-pattern-flags": flags,
},
},
],
};
}
// Legacy format: just the pattern (no flags)
return {
string: [
{
name: `Pattern<${JSON.stringify(value)}>`,
target: "string",
kind: "pattern",
value: value,
validate: `RegExp(${JSON.stringify(value)}).test($input)`,
exclusive: ["format"],
schema: {
pattern: value,
},
},
},
],
}),
],
};
},
length: (props) => ({
string: [
{
Expand Down
25 changes: 19 additions & 6 deletions src/programmers/RandomProgrammer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -504,22 +504,35 @@ export namespace RandomProgrammer {
.join("")}`,
arguments: [],
};
} else if (string.pattern !== undefined)
} else if (string.pattern !== undefined) {
const flags: string | undefined = (
schema as OpenApi.IJsonSchema.IString & {
"x-pattern-flags"?: string;
}
)["x-pattern-flags"];
return {
method: "pattern",
internal: "randomPattern",
arguments: [
ts.factory.createNewExpression(
ts.factory.createIdentifier("RegExp"),
undefined,
[
ts.factory.createStringLiteral(
(schema as OpenApi.IJsonSchema.IString).pattern!,
),
],
flags !== undefined
? [
ts.factory.createStringLiteral(
(schema as OpenApi.IJsonSchema.IString).pattern!,
),
ts.factory.createStringLiteral(flags),
]
: [
ts.factory.createStringLiteral(
(schema as OpenApi.IJsonSchema.IString).pattern!,
),
],
),
],
};
}
} else if (props.atomic.type === "number") {
const number:
| OpenApi.IJsonSchema.INumber
Expand Down
32 changes: 27 additions & 5 deletions src/tags/Pattern.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,42 @@ import { TagBase } from "./TagBase";
* type HexColor = string & Pattern<"^#[0-9A-Fa-f]{6}$">; // #FF5733
* ```
*
* You can also specify regex flags as a second parameter:
*
* ```ts
* // Case-insensitive matching
* type CaseInsensitive = string & Pattern<"^hello$", "i">;
*
* // Unicode property escapes (requires 'u' flag)
* type Identifier = string & Pattern<"^[\\p{ID_Start}_$][\\p{ID_Continue}_$]*$", "u">;
* ```
*
* Supported flags: `d`, `g`, `i`, `m`, `s`, `u`, `v`, `y` (can be combined)
*
* Note: This tag is mutually exclusive with the Format tag. You cannot use both
* Pattern and Format on the same type.
*
* @author Jeongho Nam - https://github.com/samchon
*/
export type Pattern<Value extends string> = TagBase<{
export type Pattern<
Value extends string,
Flags extends string = "",
> = TagBase<{
target: "string";
kind: "pattern";
value: Value;
validate: `RegExp("${Serialize<Value>}").test($input)`;
validate: Flags extends ""
? `RegExp("${Serialize<Value>}").test($input)`
: `RegExp("${Serialize<Value>}", "${Flags}").test($input)`;
exclusive: ["format", "pattern"];
schema: {
pattern: Value;
};
schema: Flags extends ""
? {
pattern: Value;
}
: {
pattern: Value;
"x-pattern-flags": Flags;
};
}>;

/// reference: https://github.com/type-challenges/type-challenges/issues/22394#issuecomment-1397158205
Expand Down
36 changes: 36 additions & 0 deletions test/src/structures/CommentTagPatternFlags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Spoiler } from "../helpers/Spoiler";

export interface CommentTagPatternFlags {
/** @pattern /^hello$/i */
caseInsensitive: string;

/** @pattern /^hello$/m */
multiline: string;

/** @pattern /^[a-z]+$/im */
multipleFlags: string;
}
export namespace CommentTagPatternFlags {
export function generate(): CommentTagPatternFlags {
return {
caseInsensitive: "HELLO",
multiline: "hello",
multipleFlags: "hello",
};
}

export const SPOILERS: Spoiler<CommentTagPatternFlags>[] = [
(input) => {
input.caseInsensitive = "world";
return ["$input.caseInsensitive"];
},
(input) => {
input.multiline = "world";
return ["$input.multiline"];
},
(input) => {
input.multipleFlags = "123";
return ["$input.multipleFlags"];
},
];
}
38 changes: 38 additions & 0 deletions test/src/structures/TypeTagPatternFlags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import typia from "typia";

import { Spoiler } from "../helpers/Spoiler";

export interface TypeTagPatternFlags {
// Case-insensitive matching
caseInsensitive: string & typia.tags.Pattern<"^hello$", "i">;

// Multiline flag
multiline: string & typia.tags.Pattern<"^hello$", "m">;

// Multiple flags combined
multipleFlags: string & typia.tags.Pattern<"^[a-z]+$", "im">;
}
export namespace TypeTagPatternFlags {
export function generate(): TypeTagPatternFlags {
return {
caseInsensitive: "HELLO",
multiline: "hello",
multipleFlags: "hello",
};
}

export const SPOILERS: Spoiler<TypeTagPatternFlags>[] = [
(input) => {
input.caseInsensitive = "world";
return ["$input.caseInsensitive"];
},
(input) => {
input.multiline = "world";
return ["$input.multiline"];
},
(input) => {
input.multipleFlags = "123";
return ["$input.multipleFlags"];
},
];
}
Loading