Skip to content

Commit

Permalink
/a/bに対して/:aのようなパターンがマッチしてしまう問題を修正 (#83)
Browse files Browse the repository at this point in the history
  • Loading branch information
mpppk authored Aug 27, 2024
1 parent d6a81f5 commit 59044f3
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 12 deletions.
39 changes: 39 additions & 0 deletions src/common/type.t-test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { Equal, Expect } from "./type-test";
import {
CountChar,
ExtractByPrefix,
FilterNever,
IsEqualNumber,
Replace,
ReplaceAll,
SameSlashNum,
Split,
} from "./type";
import { NormalizePath } from "./url";
Expand Down Expand Up @@ -69,3 +72,39 @@ type NormalizePathTestCases = [
Expect<Equal<NormalizePath<"//users">, "/users">>,
Expect<Equal<NormalizePath<"users//:userId">, "users/:userId">>,
];

// eslint-disable-next-line @typescript-eslint/no-unused-vars
type IsEqualNumberTestCases = [
Expect<Equal<IsEqualNumber<1, 1>, true>>,
Expect<Equal<IsEqualNumber<1, 2>, false>>,
Expect<Equal<IsEqualNumber<1, 1 | 2>, false>>,
Expect<Equal<IsEqualNumber<1 | 2, 1 | 2>, true>>,
Expect<Equal<IsEqualNumber<1 | 2, 1>, false>>,
];

// eslint-disable-next-line @typescript-eslint/no-unused-vars
type CountCharTestCases = [
Expect<Equal<CountChar<"a", "a">, 1>>,
Expect<Equal<CountChar<"a", "b">, 0>>,
Expect<Equal<CountChar<"a", "">, 0>>,
Expect<Equal<CountChar<"a", "a" | "b">, 1>>,
Expect<Equal<CountChar<"ab", "a" | "b">, 2>>,
Expect<Equal<CountChar<"ab", "c">, 0>>,
Expect<Equal<CountChar<"abc", "a" | "b">, 2>>,
Expect<Equal<CountChar<"a" | "b", "a">, 0 | 1>>,

Expect<Equal<CountChar<"banana", "a">, 3>>,
Expect<Equal<CountChar<"banana", "b">, 1>>,
Expect<Equal<CountChar<"banana", "n">, 2>>,
Expect<Equal<CountChar<"banana", "x">, 0>>,
];

// eslint-disable-next-line @typescript-eslint/no-unused-vars
type SameSlashNumTestCases = [
Expect<Equal<SameSlashNum<"", "">, true>>,
Expect<Equal<SameSlashNum<"/a", "/b">, true>>,
Expect<Equal<SameSlashNum<"/a", "/a/">, false>>,
Expect<Equal<SameSlashNum<string, "/">, false>>,
Expect<Equal<SameSlashNum<`/${string}`, "/a">, true>>,
Expect<Equal<SameSlashNum<`/${string}`, "/a/b">, false>>,
];
51 changes: 51 additions & 0 deletions src/common/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,54 @@ export type StrictProperty<T, TExpected> =
Exclude<keyof T, keyof TExpected> extends never ? T : never;

export type UndefinedTo<T, U> = T extends undefined ? U : T;

/**
* Compare two numbers
*
* @example
* ```
* type T0 = IsEqualNumber<1, 1>; // => true
* type T1 = IsEqualNumber<1, 2>; // => false
* ```
*/
export type IsEqualNumber<
A extends number,
B extends number,
> = `${A}` extends `${B}` ? (`${B}` extends `${A}` ? true : false) : false;

/**
* Count character in string
*
* @example
* ```
* type CountA = CountChar<"banana", "a">; // 3
* type CountB = CountChar<"banana", "b">; // 1
* type CountN = CountChar<"banana", "n">; // 2
* type CountX = CountChar<"banana", "x">; // 0
* ```
*/
export type CountChar<
S extends string,
C extends string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Count extends any[] = [],
> = S extends `${infer First}${infer Rest}`
? First extends C
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
CountChar<Rest, C, [any, ...Count]>
: CountChar<Rest, C, Count>
: Count["length"];

/**
* Check if two strings have the same number of slashes
*
* @example
* ```
* type T0 = SameSlashNum<`/${string}/b`, `/aaa/b`>; // true
* type T1 = SameSlashNum<`/${string}/b`, `/aaa`>; // false
* ```
*/
export type SameSlashNum<P1 extends string, P2 extends string> = IsEqualNumber<
CountChar<P1, "/">,
CountChar<P2, "/">
>;
11 changes: 10 additions & 1 deletion src/common/url.t-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ type MatchedPatternsTestCases = [
Expect<
Equal<
MatchedPatterns<"/users/1", "/users/:userId" | "/:userId">,
"/users/:userId" | "/:userId"
"/users/:userId"
>
>,
Expect<
Expand All @@ -72,6 +72,15 @@ type MatchedPatternsTestCases = [
"/users/:userId"
>
>,
Expect<
Equal<
MatchedPatterns<
"/users/1/profile",
"/users/:userId" | "/users/:userId/profile"
>,
"/users/:userId/profile"
>
>,
];

// eslint-disable-next-line @typescript-eslint/no-unused-vars
Expand Down
8 changes: 6 additions & 2 deletions src/common/url.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ParseQueryString } from "./query-string";
import { ExtractByPrefix, Split, UndefinedTo } from "./type";
import { ExtractByPrefix, SameSlashNum, Split, UndefinedTo } from "./type";

type ExtractParams<T extends string> = ExtractByPrefix<T, ":">;

Expand Down Expand Up @@ -75,7 +75,11 @@ export type ToUrlPattern<T extends string> = T extends `${infer O}?${infer R}`
* ```
*/
export type MatchedPatterns<T extends string, Patterns extends string> = keyof {
[P in Patterns as T extends ToUrlPattern<P> ? P : never]: true;
[P in Patterns as T extends ToUrlPattern<P>
? SameSlashNum<P, T> extends true
? P
: never
: never]: true;
};

/**
Expand Down
29 changes: 20 additions & 9 deletions src/fetch/index.t-test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AsJsonApi, DefineApiEndpoints } from "../common";
import FetchT from "./index";
import JSONT from "../json";
const JSONT = JSON as JSONT;

{
type Spec = DefineApiEndpoints<{
Expand Down Expand Up @@ -175,21 +176,28 @@ import JSONT from "../json";
if (res.ok) {
(await res.json()).prop;
}

{
// @ts-expect-error queryが定義されているSpecに対してクエリパラメータを指定しなかった場合は型エラー
f(`/api/projects/projectA/workflow/packages/list`, {
headers: { Cookie: "a=b" },
});
}
}
{
// @ts-expect-error queryが定義されているSpecに対してクエリパラメータを指定しなかった場合は型エラー
f(`/api/projects/projectA/workflow/packages/list`, {
headers: { Cookie: "a=b" },
});
}
})();
}

{
type Spec = DefineApiEndpoints<{
"/vectorize/indexes/:indexName": {
get: {
post: {
resBody: {
200: { prop2: string };
};
};
};
"/vectorize/indexes/:indexName/get-by-ids": {
post: {
body: { ids: string[] };
resBody: {
200: { prop: string };
};
Expand All @@ -203,7 +211,10 @@ import JSONT from "../json";
const basePath = getCloudflareAccountEndpoint("accountId");
const f = fetch as FetchT<typeof basePath, Spec>;
{
const res = await f(`${basePath}/vectorize/indexes/indexA`, {});
const res = await f(`${basePath}/vectorize/indexes/indexA/get-by-ids`, {
method: "POST",
body: JSONT.stringify({ ids: ["1", "2", "3"] }),
});
if (res.ok) {
(await res.json()).prop;
}
Expand Down

0 comments on commit 59044f3

Please sign in to comment.