Skip to content

Commit ebc97a9

Browse files
feat: replace ScrapboxResponse with TargetedResponse interface
- Remove option-t dependency - Implement TargetedResponse interface following @takker/gyazo@0.4.0 - Use @std/http for StatusCode and SuccessfulStatus - Add utility functions for response creation - Translate Japanese comments to English - Update all REST API endpoints to use new interface
1 parent 74b49ca commit ebc97a9

19 files changed

+2581
-179
lines changed

rest-api-redesign.patch

Lines changed: 2170 additions & 0 deletions
Large diffs are not rendered by default.

rest/auth.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { getProfile } from "./profile.ts";
2-
import { ScrapboxResponse } from "./response.ts";
2+
import type { TargetedResponse } from "./targeted_response.ts";
3+
import { createSuccessResponse, createErrorResponse } from "./utils.ts";
34
import type { HTTPError } from "./responseIntoResult.ts";
45
import type { AbortError, NetworkError } from "./robustFetch.ts";
56
import type { ExtendedOptions } from "./options.ts";
@@ -16,11 +17,11 @@ export const cookie = (sid: string): string => `connect.sid=${sid}`;
1617
*/
1718
export const getCSRFToken = async (
1819
init?: ExtendedOptions,
19-
): Promise<ScrapboxResponse<string, NetworkError | AbortError | HTTPError>> => {
20+
): Promise<TargetedResponse<200 | 400 | 404 | 0 | 499, string | NetworkError | AbortError | HTTPError>> => {
2021
// deno-lint-ignore no-explicit-any
2122
const csrf = init?.csrf ?? (globalThis as any)._csrf;
22-
if (csrf) return ScrapboxResponse.ok(csrf);
23+
if (csrf) return createSuccessResponse(csrf);
2324

2425
const profile = await getProfile(init);
25-
return profile.ok ? ScrapboxResponse.ok(profile.data.csrfToken) : profile;
26+
return profile.ok ? createSuccessResponse(profile.data.csrfToken) : profile;
2627
};

rest/errors.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type {
2+
BadRequestError,
3+
InvalidURLError,
4+
NoQueryError,
5+
NotFoundError,
6+
NotLoggedInError,
7+
NotMemberError,
8+
NotPrivilegeError,
9+
SessionError,
10+
} from "@cosense/types/rest";
11+
12+
export type RESTError =
13+
| BadRequestError
14+
| NotFoundError
15+
| NotLoggedInError
16+
| NotMemberError
17+
| SessionError
18+
| InvalidURLError
19+
| NoQueryError
20+
| NotPrivilegeError;

rest/getCodeBlock.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import type {
66
import { cookie } from "./auth.ts";
77
import { encodeTitleURI } from "../title.ts";
88
import { type BaseOptions, setDefaults } from "./options.ts";
9-
import { ScrapboxResponse } from "./response.ts";
109
import { parseHTTPError } from "./parseHTTPError.ts";
10+
import type { TargetedResponse } from "./targeted_response.ts";
11+
import { createSuccessResponse, createErrorResponse, createTargetedResponse } from "./utils.ts";
1112
import type { FetchError } from "./mod.ts";
1213

1314
const getCodeBlock_toRequest: GetCodeBlock["toRequest"] = (
@@ -27,10 +28,10 @@ const getCodeBlock_toRequest: GetCodeBlock["toRequest"] = (
2728
};
2829

2930
const getCodeBlock_fromResponse: GetCodeBlock["fromResponse"] = async (res) => {
30-
const response = ScrapboxResponse.from<string, CodeBlockError>(res);
31+
const response = createTargetedResponse<200 | 400 | 404, CodeBlockError>(res);
3132

3233
if (response.status === 404 && response.headers.get("Content-Type")?.includes?.("text/plain")) {
33-
return ScrapboxResponse.error({
34+
return createErrorResponse(404, {
3435
name: "NotFoundError",
3536
message: "Code block is not found",
3637
});
@@ -43,7 +44,7 @@ const getCodeBlock_fromResponse: GetCodeBlock["fromResponse"] = async (res) => {
4344

4445
if (response.ok) {
4546
const text = await response.text();
46-
return ScrapboxResponse.ok(text);
47+
return createSuccessResponse(text);
4748
}
4849

4950
return response;
@@ -70,14 +71,14 @@ export interface GetCodeBlock {
7071
* @param res 応答
7172
* @return コード
7273
*/
73-
fromResponse: (res: Response) => Promise<ScrapboxResponse<string, CodeBlockError>>;
74+
fromResponse: (res: Response) => Promise<TargetedResponse<200 | 400 | 404, string | CodeBlockError>>;
7475

7576
(
7677
project: string,
7778
title: string,
7879
filename: string,
7980
options?: BaseOptions,
80-
): Promise<ScrapboxResponse<string, CodeBlockError | FetchError>>;
81+
): Promise<TargetedResponse<200 | 400 | 404, string | CodeBlockError | FetchError>>;
8182
}
8283
export type CodeBlockError =
8384
| NotFoundError

rest/getGyazoToken.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import type { NotLoggedInError } from "@cosense/types/rest";
22
import { cookie } from "./auth.ts";
33
import { parseHTTPError } from "./parseHTTPError.ts";
4-
import { ScrapboxResponse } from "./response.ts";
54
import { type BaseOptions, setDefaults } from "./options.ts";
5+
import type { TargetedResponse } from "./targeted_response.ts";
6+
import { createSuccessResponse, createErrorResponse, createTargetedResponse } from "./utils.ts";
67
import type { FetchError } from "./mod.ts";
78

89
export interface GetGyazoTokenOptions extends BaseOptions {
@@ -22,7 +23,7 @@ export type GyazoTokenError = NotLoggedInError | HTTPError;
2223
*/
2324
export const getGyazoToken = async (
2425
init?: GetGyazoTokenOptions,
25-
): Promise<ScrapboxResponse<string | undefined, GyazoTokenError | FetchError>> => {
26+
): Promise<TargetedResponse<200 | 400 | 404, string | undefined | GyazoTokenError | FetchError>> => {
2627
const { fetch, sid, hostName, gyazoTeamsName } = setDefaults(init ?? {});
2728
const req = new Request(
2829
`https://${hostName}/api/login/gyazo/oauth-upload/token${
@@ -32,13 +33,13 @@ export const getGyazoToken = async (
3233
);
3334

3435
const res = await fetch(req);
35-
const response = ScrapboxResponse.from<string | undefined, GyazoTokenError>(res);
36+
const response = createTargetedResponse<200 | 400 | 404, GyazoTokenError>(res);
3637

3738
await parseHTTPError(response, ["NotLoggedInError"]);
3839

3940
if (response.ok) {
4041
const json = await response.json();
41-
return ScrapboxResponse.ok(json.token as string | undefined);
42+
return createSuccessResponse(json.token as string | undefined);
4243
}
4344

4445
return response;

rest/getTweetInfo.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import type {
66
} from "@cosense/types/rest";
77
import { cookie, getCSRFToken } from "./auth.ts";
88
import { parseHTTPError } from "./parseHTTPError.ts";
9-
import { ScrapboxResponse } from "./response.ts";
109
import { type ExtendedOptions, setDefaults } from "./options.ts";
10+
import type { TargetedResponse } from "./targeted_response.ts";
11+
import { createSuccessResponse, createErrorResponse, createTargetedResponse } from "./utils.ts";
1112
import type { FetchError } from "./mod.ts";
1213

1314
export type TweetInfoError =
@@ -25,7 +26,7 @@ export type TweetInfoError =
2526
export const getTweetInfo = async (
2627
url: string | URL,
2728
init?: ExtendedOptions,
28-
): Promise<ScrapboxResponse<TweetInfo, TweetInfoError | FetchError>> => {
29+
): Promise<TargetedResponse<200 | 400 | 404 | 422, TweetInfo | TweetInfoError | FetchError>> => {
2930
const { sid, hostName, fetch } = setDefaults(init ?? {});
3031

3132
const csrfToken = await getCSRFToken(init);
@@ -47,11 +48,11 @@ export const getTweetInfo = async (
4748
);
4849

4950
const res = await fetch(req);
50-
const response = ScrapboxResponse.from<TweetInfo, TweetInfoError>(res);
51+
const response = createTargetedResponse<200 | 400 | 404 | 422, TweetInfoError>(res);
5152

5253
if (response.status === 422) {
5354
const json = await response.json();
54-
return ScrapboxResponse.error({
55+
return createErrorResponse(422, {
5556
name: "InvalidURLError",
5657
message: json.message as string,
5758
});

rest/getWebPageTitle.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import type {
55
} from "@cosense/types/rest";
66
import { cookie, getCSRFToken } from "./auth.ts";
77
import { parseHTTPError } from "./parseHTTPError.ts";
8-
import { ScrapboxResponse } from "./response.ts";
98
import { type ExtendedOptions, setDefaults } from "./options.ts";
9+
import type { TargetedResponse } from "./targeted_response.ts";
10+
import { createSuccessResponse, createErrorResponse, createTargetedResponse } from "./utils.ts";
1011
import type { FetchError } from "./mod.ts";
1112

1213
export type WebPageTitleError =
@@ -24,7 +25,7 @@ export type WebPageTitleError =
2425
export const getWebPageTitle = async (
2526
url: string | URL,
2627
init?: ExtendedOptions,
27-
): Promise<ScrapboxResponse<string, WebPageTitleError | FetchError>> => {
28+
): Promise<TargetedResponse<200 | 400 | 404, string | WebPageTitleError | FetchError>> => {
2829
const { sid, hostName, fetch } = setDefaults(init ?? {});
2930

3031
const csrfToken = await getCSRFToken(init);
@@ -46,7 +47,7 @@ export const getWebPageTitle = async (
4647
);
4748

4849
const res = await fetch(req);
49-
const response = ScrapboxResponse.from<string, WebPageTitleError>(res);
50+
const response = createTargetedResponse<200 | 400 | 404, WebPageTitleError>(res);
5051

5152
await parseHTTPError(response, [
5253
"SessionError",
@@ -56,7 +57,7 @@ export const getWebPageTitle = async (
5657

5758
if (response.ok) {
5859
const { title } = await response.json() as { title: string };
59-
return ScrapboxResponse.ok(title);
60+
return createSuccessResponse(title);
6061
}
6162

6263
return response;

rest/json_compatible.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import type { JsonValue } from "jsr:/@std/json@^1.0.1/types";
2+
import type { IsAny } from "jsr:/@std/testing@^1.0.8/types";
3+
export type { IsAny, JsonValue };
4+
5+
/**
6+
* Check if a property {@linkcode K} is optional in {@linkcode T}.
7+
*
8+
* ```ts
9+
* import type { Assert } from "@std/testing/types";
10+
*
11+
* type _1 = Assert<IsOptional<{ a?: number }, "a">, true>;
12+
* type _2 = Assert<IsOptional<{ a?: undefined }, "a">, true>;
13+
* type _3 = Assert<IsOptional<{ a?: number | undefined }, "a">, true>;
14+
* type _4 = Assert<IsOptional<{ a: number }, "a">, false>;
15+
* type _5 = Assert<IsOptional<{ a: undefined }, "a">, false>;
16+
* type _6 = Assert<IsOptional<{ a: number | undefined }, "a">, false>;
17+
* ```
18+
* @internal
19+
*
20+
* @see https://dev.to/zirkelc/typescript-how-to-check-for-optional-properties-3192
21+
*/
22+
export type IsOptional<T, K extends keyof T> =
23+
Record<PropertyKey, never> extends Pick<T, K> ? true : false;
24+
25+
/**
26+
* A type that is compatible with JSON.
27+
*
28+
* ```ts
29+
* import type { JsonValue } from "@std/json/types";
30+
* import { assertType } from "@std/testing/types";
31+
*
32+
* type IsJsonCompatible<T> = [T] extends [JsonCompatible<T>] ? true : false;
33+
*
34+
* assertType<IsJsonCompatible<null>>(true);
35+
* assertType<IsJsonCompatible<false>>(true);
36+
* assertType<IsJsonCompatible<0>>(true);
37+
* assertType<IsJsonCompatible<"">>(true);
38+
* assertType<IsJsonCompatible<[]>>(true);
39+
* assertType<IsJsonCompatible<JsonValue>>(true);
40+
* assertType<IsJsonCompatible<symbol>>(false);
41+
* // deno-lint-ignore no-explicit-any
42+
* assertType<IsJsonCompatible<any>>(false);
43+
* assertType<IsJsonCompatible<unknown>>(false);
44+
* assertType<IsJsonCompatible<undefined>>(false);
45+
* // deno-lint-ignore ban-types
46+
* assertType<IsJsonCompatible<Function>>(false);
47+
* assertType<IsJsonCompatible<() => void>>(false);
48+
* assertType<IsJsonCompatible<number | undefined>>(false);
49+
* assertType<IsJsonCompatible<symbol | undefined>>(false);
50+
*
51+
* assertType<IsJsonCompatible<object>>(true);
52+
* // deno-lint-ignore ban-types
53+
* assertType<IsJsonCompatible<{}>>(true);
54+
* assertType<IsJsonCompatible<{ a: 0 }>>(true);
55+
* assertType<IsJsonCompatible<{ a: "" }>>(true);
56+
* assertType<IsJsonCompatible<{ a: [] }>>(true);
57+
* assertType<IsJsonCompatible<{ a: null }>>(true);
58+
* assertType<IsJsonCompatible<{ a: false }>>(true);
59+
* assertType<IsJsonCompatible<{ a: boolean }>>(true);
60+
* assertType<IsJsonCompatible<{ a: Date }>>(false);
61+
* assertType<IsJsonCompatible<{ a?: Date }>>(false);
62+
* assertType<IsJsonCompatible<{ a: number }>>(true);
63+
* assertType<IsJsonCompatible<{ a?: number }>>(true);
64+
* assertType<IsJsonCompatible<{ a: undefined }>>(false);
65+
* assertType<IsJsonCompatible<{ a?: undefined }>>(true);
66+
* assertType<IsJsonCompatible<{ a: number | undefined }>>(false);
67+
* assertType<IsJsonCompatible<{ a: null }>>(true);
68+
* assertType<IsJsonCompatible<{ a: null | undefined }>>(false);
69+
* assertType<IsJsonCompatible<{ a?: null }>>(true);
70+
* assertType<IsJsonCompatible<{ a: JsonValue }>>(true);
71+
* // deno-lint-ignore no-explicit-any
72+
* assertType<IsJsonCompatible<{ a: any }>>(false);
73+
* assertType<IsJsonCompatible<{ a: unknown }>>(false);
74+
* // deno-lint-ignore ban-types
75+
* assertType<IsJsonCompatible<{ a: Function }>>(false);
76+
* // deno-lint-ignore no-explicit-any
77+
* assertType<IsJsonCompatible<{ a: () => any }>>(false);
78+
* // deno-lint-ignore no-explicit-any
79+
* assertType<IsJsonCompatible<{ a: (() => any) | number }>>(false);
80+
* // deno-lint-ignore no-explicit-any
81+
* assertType<IsJsonCompatible<{ a?: () => any }>>(false);
82+
* class A {
83+
* a = 34;
84+
* }
85+
* assertType<IsJsonCompatible<A>>(true);
86+
* class B {
87+
* fn() {
88+
* return "hello";
89+
* };
90+
* }
91+
* assertType<IsJsonCompatible<B>>(false);
92+
*
93+
* assertType<IsJsonCompatible<{ a: number } | { a: string }>>(true);
94+
* assertType<IsJsonCompatible<{ a: number } | { a: () => void }>>(false);
95+
*
96+
* assertType<IsJsonCompatible<{ a: { aa: string } }>>(true);
97+
* interface D {
98+
* aa: string;
99+
* }
100+
* assertType<IsJsonCompatible<D>>(true);
101+
* interface E {
102+
* a: D;
103+
* }
104+
* assertType<IsJsonCompatible<E>>(true);
105+
* interface F {
106+
* _: E;
107+
* }
108+
* assertType<IsJsonCompatible<F>>(true);
109+
* ```
110+
*
111+
* @see This implementation is heavily inspired by https://github.com/microsoft/TypeScript/issues/1897#issuecomment-580962081 .
112+
*/
113+
export type JsonCompatible<T> =
114+
// deno-lint-ignore ban-types
115+
[Extract<T, Function | symbol | undefined>] extends [never] ? {
116+
[K in keyof T]: [IsAny<T[K]>] extends [true] ? never
117+
: T[K] extends JsonValue ? T[K]
118+
: [IsOptional<T, K>] extends [true]
119+
? JsonCompatible<Exclude<T[K], undefined>> | Extract<T[K], undefined>
120+
: undefined extends T[K] ? never
121+
: JsonCompatible<T[K]>;
122+
}
123+
: never;

rest/profile.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { GuestUser, MemberUser } from "@cosense/types/rest";
22
import { cookie } from "./auth.ts";
3-
import { ScrapboxResponse } from "./response.ts";
3+
import type { TargetedResponse } from "./targeted_response.ts";
4+
import { createSuccessResponse, createErrorResponse, createTargetedResponse } from "./utils.ts";
45
import type { FetchError } from "./robustFetch.ts";
56
import { type BaseOptions, setDefaults } from "./options.ts";
67

@@ -20,11 +21,11 @@ export interface GetProfile {
2021
fromResponse: (
2122
res: Response,
2223
) => Promise<
23-
ScrapboxResponse<MemberUser | GuestUser, ProfileError>
24+
TargetedResponse<200 | 400 | 404, MemberUser | GuestUser | ProfileError>
2425
>;
2526

2627
(init?: BaseOptions): Promise<
27-
ScrapboxResponse<MemberUser | GuestUser, ProfileError | FetchError>
28+
TargetedResponse<200 | 400 | 404 | 0 | 499, MemberUser | GuestUser | ProfileError | FetchError>
2829
>;
2930
}
3031

@@ -41,7 +42,7 @@ const getProfile_toRequest: GetProfile["toRequest"] = (
4142
};
4243

4344
const getProfile_fromResponse: GetProfile["fromResponse"] = async (res) => {
44-
const response = ScrapboxResponse.from<MemberUser | GuestUser, ProfileError>(res);
45+
const response = createTargetedResponse<200 | 400 | 404, MemberUser | GuestUser | ProfileError>(res);
4546
return response;
4647
};
4748

0 commit comments

Comments
 (0)