From 59ce50bfc3787ea7b7b81c735b945ad5c399107c Mon Sep 17 00:00:00 2001 From: takker99 <37929109+takker99@users.noreply.github.com> Date: Fri, 2 Aug 2024 10:17:50 +0900 Subject: [PATCH 1/5] feat(REST API): Replace my own `Result with `option-t`'s Also delete`is.ts` and use `unknownutil` `Result.value` has been changed into `Result.val` --- browser/dom/edit.ts | 2 +- browser/dom/node.ts | 44 ++++---- browser/websocket/deletePage.ts | 6 +- browser/websocket/id.ts | 32 ------ browser/websocket/listen.ts | 37 +++++- browser/websocket/patch.ts | 6 +- browser/websocket/pin.ts | 8 +- browser/websocket/pull.ts | 106 +++++++++++++++-- browser/websocket/push.ts | 97 +++++++++------- browser/websocket/updateCodeBlock.ts | 6 +- browser/websocket/updateCodeFile.ts | 10 +- deps/option-t.ts | 2 + deps/unknownutil.ts | 1 + is.ts | 36 ------ rest/auth.ts | 14 ++- rest/error.ts | 37 ------ rest/getCodeBlock.ts | 63 +++++++---- rest/getCodeBlocks.test.ts | 8 +- rest/getCodeBlocks.ts | 36 ++---- rest/getGyazoToken.ts | 31 +++-- rest/getTweetInfo.ts | 64 +++++++---- rest/getWebPageTitle.ts | 59 ++++++---- rest/link.ts | 163 ++++++++++++++++----------- rest/mod.ts | 2 +- rest/page-data.ts | 68 +++++++---- rest/pages.ts | 155 +++++++++++++++++-------- rest/parseHTTPError.ts | 94 +++++++++++++++ rest/profile.ts | 74 +++++++++--- rest/project.ts | 85 ++++++++++---- rest/replaceLinks.ts | 56 +++++++-- rest/responseIntoResult.ts | 18 +++ rest/robustFetch.ts | 48 ++++++++ rest/search.ts | 115 ++++++++++--------- rest/snapshot.ts | 78 +++++++++---- rest/table.ts | 64 +++++++---- rest/uploadToGCS.ts | 148 ++++++++++++++++-------- rest/util.ts | 17 +-- text.ts | 2 +- 38 files changed, 1237 insertions(+), 655 deletions(-) create mode 100644 deps/option-t.ts create mode 100644 deps/unknownutil.ts delete mode 100644 is.ts delete mode 100644 rest/error.ts create mode 100644 rest/parseHTTPError.ts create mode 100644 rest/responseIntoResult.ts create mode 100644 rest/robustFetch.ts diff --git a/browser/dom/edit.ts b/browser/dom/edit.ts index c22a84c..6bab2eb 100644 --- a/browser/dom/edit.ts +++ b/browser/dom/edit.ts @@ -3,7 +3,7 @@ import { press } from "./press.ts"; import { getLineCount } from "./node.ts"; import { range } from "../../range.ts"; import { textInput } from "./dom.ts"; -import { isArray, isNumber, isString } from "../../is.ts"; +import { isArray, isNumber, isString } from "../../deps/unknownutil.ts"; import { sleep } from "../../sleep.ts"; export const undo = (count = 1): void => { diff --git a/browser/dom/node.ts b/browser/dom/node.ts index 699a659..287e730 100644 --- a/browser/dom/node.ts +++ b/browser/dom/node.ts @@ -1,7 +1,7 @@ /// /// /// -import { isNone, isNumber, isString } from "../../is.ts"; +import { isNumber, isString, isUndefined } from "../../deps/unknownutil.ts"; import { ensureArray } from "../../ensure.ts"; import { getCachedLines } from "./getCachedLines.ts"; import { takeInternalLines } from "./takeInternalLines.ts"; @@ -18,7 +18,7 @@ import * as Text from "../../text.ts"; export const getLineId = ( value?: number | string | T, ): string | undefined => { - if (isNone(value)) return undefined; + if (isUndefined(value)) return undefined; // 行番号のとき if (isNumber(value)) return getBaseLine(value)?.id; @@ -43,7 +43,7 @@ export const getLineId = ( export const getLineNo = ( value?: number | string | T, ): number | undefined => { - if (isNone(value)) return undefined; + if (isUndefined(value)) return undefined; // 行番号のとき if (isNumber(value)) return value; @@ -55,7 +55,7 @@ export const getLineNo = ( export const getLine = ( value?: number | string | T, ): Line | undefined => { - if (isNone(value)) return undefined; + if (isUndefined(value)) return undefined; // 行番号のとき if (isNumber(value)) return getLines()[value]; @@ -67,7 +67,7 @@ export const getLine = ( export const getBaseLine = ( value?: number | string | T, ): BaseLine | undefined => { - if (isNone(value)) return undefined; + if (isUndefined(value)) return undefined; // 行番号のとき if (isNumber(value)) return takeInternalLines()[value]; @@ -82,9 +82,9 @@ export const getLineDOM = ( if (isLineDOM(value)) return value; const id = getLineId(value); - if (isNone(id)) return id; + if (isUndefined(id)) return id; const line = document.getElementById(`L${id}`); - if (isNone(line)) return undefined; + if (isUndefined(line)) return undefined; return line as HTMLDivElement; }; export const isLineDOM = (dom: unknown): dom is HTMLDivElement => @@ -101,7 +101,7 @@ export const getLines = (): readonly Line[] => { export const getText = ( value?: number | string | T, ): string | undefined => { - if (isNone(value)) return undefined; + if (isUndefined(value)) return undefined; // 数字と文字列は行として扱う if (isNumber(value) || isString(value)) return getBaseLine(value)?.text; @@ -121,7 +121,7 @@ export const getText = ( //中に含まれている文字の列番号を全て取得し、それに対応する文字列を返す const chars = [] as number[]; const line = getBaseLine(value); - if (isNone(line)) return; + if (isUndefined(line)) return; for (const dom of getChars(value)) { chars.push(getIndex(dom)); } @@ -130,30 +130,30 @@ export const getText = ( export const getExternalLink = (dom: HTMLElement): HTMLElement | undefined => { const link = dom.closest(".link"); - if (isNone(link)) return undefined; + if (isUndefined(link)) return undefined; return link as HTMLElement; }; export const getInternalLink = (dom: HTMLElement): HTMLElement | undefined => { const link = dom.closest(".page-link"); - if (isNone(link)) return undefined; + if (isUndefined(link)) return undefined; return link as HTMLElement; }; export const getLink = (dom: HTMLElement) => { const link = dom.closest(".link, .page-link"); - if (isNone(link)) return undefined; + if (isUndefined(link)) return undefined; return link as HTMLElement; }; export const getFormula = (dom: HTMLElement): HTMLElement | undefined => { const formula = dom.closest(".formula"); - if (isNone(formula)) return undefined; + if (isUndefined(formula)) return undefined; return formula as HTMLElement; }; export const getNextLine = ( value?: number | string | T, ): Line | undefined => { const index = getLineNo(value); - if (isNone(index)) return undefined; + if (isUndefined(index)) return undefined; return getLine(index + 1); }; @@ -162,26 +162,26 @@ export const getPrevLine = ( value?: number | string | T, ): Line | undefined => { const index = getLineNo(value); - if (isNone(index)) return undefined; + if (isUndefined(index)) return undefined; return getLine(index - 1); }; export const getHeadLineDOM = (): HTMLDivElement | undefined => { const line = lines()?.firstElementChild; - if (isNone(line)) return undefined; + if (isUndefined(line)) return undefined; return line as HTMLDivElement; }; export const getTailLineDOM = (): HTMLDivElement | undefined => { const line = lines()?.lastElementChild; - if (isNone(line)) return undefined; + if (isUndefined(line)) return undefined; return line as HTMLDivElement; }; export const getIndentCount = ( value?: number | string | T, ): number | undefined => { const text = getText(value); - if (isNone(text)) return undefined; + if (isUndefined(text)) return undefined; return Text.getIndentCount(text); }; /** 指定した行の配下にある行の数を返す @@ -192,7 +192,7 @@ export const getIndentLineCount = ( value?: number | string | T, ): number | undefined => { const index = getLineNo(value); - if (isNone(index)) return; + if (isUndefined(index)) return; return Text.getIndentLineCount(index, getLines()); }; @@ -213,7 +213,7 @@ export const getIndex = (dom: HTMLSpanElement): number => { if (!isCharDOM(dom)) throw Error("A char DOM is required."); const index = dom.className.match(/c-(\d+)/)?.[1]; - if (isNone(index)) throw Error('.char-index must have ".c-{\\d}"'); + if (isUndefined(index)) throw Error('.char-index must have ".c-{\\d}"'); return parseInt(index); }; export const getHeadCharDOM = ( @@ -245,7 +245,7 @@ export const getDOMFromPoint = ( const char = targets.find((target) => isCharDOM(target)); const line = targets.find((target) => isLineDOM(target)); return { - char: isNone(char) ? undefined : char as HTMLSpanElement, - line: isNone(line) ? undefined : line as HTMLDivElement, + char: isUndefined(char) ? undefined : char as HTMLSpanElement, + line: isUndefined(line) ? undefined : line as HTMLDivElement, }; }; diff --git a/browser/websocket/deletePage.ts b/browser/websocket/deletePage.ts index fe2f096..9659183 100644 --- a/browser/websocket/deletePage.ts +++ b/browser/websocket/deletePage.ts @@ -1,5 +1,5 @@ -import { push, PushOptions, RetryError } from "./push.ts"; -import { Result } from "../../rest/util.ts"; +import { Result } from "../../deps/option-t.ts"; +import { push, PushError, PushOptions } from "./push.ts"; export type DeletePageOptions = PushOptions; @@ -13,7 +13,7 @@ export const deletePage = ( project: string, title: string, options?: DeletePageOptions, -): Promise> => +): Promise> => push( project, title, diff --git a/browser/websocket/id.ts b/browser/websocket/id.ts index 0a69505..77c3551 100644 --- a/browser/websocket/id.ts +++ b/browser/websocket/id.ts @@ -1,35 +1,3 @@ -import { getProject } from "../../rest/project.ts"; -import { getProfile } from "../../rest/profile.ts"; - -/** cached user ID */ -let userId: string | undefined; -export const getUserId = async (): Promise => { - if (userId !== undefined) return userId; - - const user = await getProfile(); - if (user.isGuest) { - throw new Error("this script can only be executed by Logged in users"); - } - userId = user.id; - return userId; -}; - -/** cached pairs of project name and project id */ -const projectMap = new Map(); -export const getProjectId = async (project: string): Promise => { - const cachedId = projectMap.get(project); - if (cachedId !== undefined) return cachedId; - - const result = await getProject(project); - if (!result.ok) { - const { name, message } = result.value; - throw new Error(`${name} ${message}`); - } - const { id } = result.value; - projectMap.set(project, id); - return id; -}; - const zero = (n: string) => n.padStart(8, "0"); export const createNewLineId = (userId: string): string => { diff --git a/browser/websocket/listen.ts b/browser/websocket/listen.ts index 524c12f..f157400 100644 --- a/browser/websocket/listen.ts +++ b/browser/websocket/listen.ts @@ -1,3 +1,9 @@ +import { createOk, isErr, Result, unwrapOk } from "../../deps/option-t.ts"; +import { + NotFoundError, + NotLoggedInError, + NotMemberError, +} from "../../deps/scrapbox-rest.ts"; import { ProjectUpdatesStreamCommit, ProjectUpdatesStreamEvent, @@ -5,8 +11,10 @@ import { socketIO, wrap, } from "../../deps/socket.ts"; +import { HTTPError } from "../../rest/responseIntoResult.ts"; +import { AbortError, NetworkError } from "../../rest/robustFetch.ts"; +import { getProjectId } from "./pull.ts"; import { connect, disconnect } from "./socket.ts"; -import { getProjectId } from "./id.ts"; export type { ProjectUpdatesStreamCommit, ProjectUpdatesStreamEvent, @@ -27,11 +35,24 @@ export async function* listenStream( events: ["commit" | "event", ...("commit" | "event")[]], options?: ListenStreamOptions, ): AsyncGenerator< - ProjectUpdatesStreamEvent | ProjectUpdatesStreamCommit, + Result< + ProjectUpdatesStreamEvent | ProjectUpdatesStreamCommit, + | NotFoundError + | NotLoggedInError + | NotMemberError + | NetworkError + | AbortError + | HTTPError + >, void, unknown > { - const projectId = await getProjectId(project); + const result = await getProjectId(project); + if (isErr(result)) { + yield result; + return; + } + const projectId = unwrapOk(result); const injectedSocket = options?.socket; const socket = injectedSocket ?? await socketIO(); @@ -45,9 +66,13 @@ export async function* listenStream( data: { projectId, pageId: null, projectUpdatesStream: true }, }); - yield* response( - ...events.map((event) => `projectUpdatesStream:${event}` as const), - ); + for await ( + const streamEvent of response( + ...events.map((event) => `projectUpdatesStream:${event}` as const), + ) + ) { + yield createOk(streamEvent); + } } finally { if (!injectedSocket) await disconnect(socket); } diff --git a/browser/websocket/patch.ts b/browser/websocket/patch.ts index 0c6260f..8eeae22 100644 --- a/browser/websocket/patch.ts +++ b/browser/websocket/patch.ts @@ -1,9 +1,9 @@ import { Change, DeletePageChange, PinChange } from "../../deps/socket.ts"; import { makeChanges } from "./makeChanges.ts"; import { Line, Page } from "../../deps/scrapbox-rest.ts"; -import { push, PushOptions, RetryError } from "./push.ts"; +import { push, PushError, PushOptions } from "./push.ts"; import { suggestUnDupTitle } from "./suggestUnDupTitle.ts"; -import { Result } from "../../rest/util.ts"; +import { Result } from "../../deps/option-t.ts"; export type PatchOptions = PushOptions; @@ -32,7 +32,7 @@ export const patch = ( metadata: PatchMetadata, ) => string[] | undefined | Promise, options?: PatchOptions, -): Promise> => +): Promise> => push( project, title, diff --git a/browser/websocket/pin.ts b/browser/websocket/pin.ts index 57fb97e..933d679 100644 --- a/browser/websocket/pin.ts +++ b/browser/websocket/pin.ts @@ -1,6 +1,6 @@ +import { Result } from "../../deps/option-t.ts"; import { Change, Socket } from "../../deps/socket.ts"; -import { push, PushOptions, RetryError } from "./push.ts"; -import { Result } from "../../rest/util.ts"; +import { push, PushError, PushOptions } from "./push.ts"; export interface PinOptions extends PushOptions { /** ピン留め対象のページが存在しないときの振る舞いを変えるoption @@ -22,7 +22,7 @@ export const pin = ( project: string, title: string, options?: PinOptions, -): Promise> => +): Promise> => push( project, title, @@ -51,7 +51,7 @@ export const unpin = ( project: string, title: string, options: UnPinOptions, -): Promise> => +): Promise> => push( project, title, diff --git a/browser/websocket/pull.ts b/browser/websocket/pull.ts index 619ec7a..64e92ae 100644 --- a/browser/websocket/pull.ts +++ b/browser/websocket/pull.ts @@ -1,16 +1,104 @@ -import { Page } from "../../deps/scrapbox-rest.ts"; -import { getPage } from "../../rest/pages.ts"; +import { + createErr, + createOk, + isErr, + mapForResult, + Result, + unwrapOk, +} from "../../deps/option-t.ts"; +import { + NotFoundError, + NotLoggedInError, + NotMemberError, + Page, +} from "../../deps/scrapbox-rest.ts"; +import { getPage, GetPageOption, TooLongURIError } from "../../rest/pages.ts"; +import { getProfile } from "../../rest/profile.ts"; +import { getProject } from "../../rest/project.ts"; +import { HTTPError } from "../../rest/responseIntoResult.ts"; +import { AbortError, NetworkError } from "../../rest/robustFetch.ts"; +import { BaseOptions } from "../../rest/util.ts"; + +export interface PushMetadata extends Page { + projectId: string; + userId: string; +} + +export type PullError = + | NotFoundError + | NotLoggedInError + | Omit + | NotMemberError + | TooLongURIError + | HTTPError + | NetworkError + | AbortError; export const pull = async ( project: string, title: string, -): Promise => { - const result = await getPage(project, title); + options?: GetPageOption, +): Promise> => { + const [pageRes, userRes, projectRes] = await Promise.all([ + getPage(project, title, options), + getUserId(options), + getProjectId(project, options), + ]); + if (isErr(pageRes)) return pageRes; + if (isErr(userRes)) return userRes; + if (isErr(projectRes)) return projectRes; + return createOk({ + ...unwrapOk(pageRes), + projectId: unwrapOk(projectRes), + userId: unwrapOk(userRes), + }); +}; +// TODO: 編集不可なページはStream購読だけ提供する + +/** cached user ID */ +let userId: string | undefined; +const getUserId = async (init?: BaseOptions): Promise< + Result< + string, + Omit | NetworkError | AbortError | HTTPError + > +> => { + if (userId) return createOk(userId); - // TODO: 編集不可なページはStream購読だけ提供する - if (!result.ok) { - throw new Error(`You have no privilege of editing "/${project}/${title}".`); - } + const result = await getProfile(init); + if (isErr(result)) return result; + + const user = unwrapOk(result); + return "id" in user ? createOk(user.id) : createErr({ + name: "NotLoggedInError", + message: "This script cannot be used without login", + }); +}; + +/** cached pairs of project name and project id */ +const projectMap = new Map(); +export const getProjectId = async ( + project: string, + options?: BaseOptions, +): Promise< + Result< + string, + | NotFoundError + | NotLoggedInError + | NotMemberError + | NetworkError + | AbortError + | HTTPError + > +> => { + const cachedId = projectMap.get(project); + if (cachedId) return createOk(cachedId); - return result.value; + return mapForResult( + await getProject(project, options), + ({ id }) => { + projectMap.set(project, id); + return id; + }, + ); }; diff --git a/browser/websocket/push.ts b/browser/websocket/push.ts index 29c27b3..4ec0127 100644 --- a/browser/websocket/push.ts +++ b/browser/websocket/push.ts @@ -5,18 +5,32 @@ import { PageCommitError, PageCommitResponse, PinChange, + Result as SocketResult, Socket, socketIO, TimeoutError, - UnexpectedError, wrap, } from "../../deps/socket.ts"; import { connect, disconnect } from "./socket.ts"; -import { getProjectId, getUserId } from "./id.ts"; import { pull } from "./pull.ts"; -import { Page } from "../../deps/scrapbox-rest.ts"; +import { + ErrorLike, + NotFoundError, + NotLoggedInError, + NotMemberError, + Page, +} from "../../deps/scrapbox-rest.ts"; import { sleep } from "../../sleep.ts"; -import { Result } from "../../rest/util.ts"; +import { + createErr, + createOk, + isErr, + Result, + unwrapOk, +} from "../../deps/option-t.ts"; +import { TooLongURIError } from "../../mod.ts"; +import { HTTPError } from "../../rest/responseIntoResult.ts"; +import { AbortError, NetworkError } from "../../rest/robustFetch.ts"; export interface PushOptions { /** 外部からSocketを指定したいときに使う */ @@ -40,6 +54,22 @@ export interface PushMetadata extends Page { userId: string; } +export interface UnexpectedError extends ErrorLike { + name: "UnexpectedError"; +} + +export type PushError = + | RetryError + | UnexpectedError + | NotFoundError + | NotLoggedInError + | Omit + | NotMemberError + | TooLongURIError + | HTTPError + | NetworkError + | AbortError; + /** 特定のページのcommitをpushする * * serverからpush errorが返ってきた場合、エラーに応じてpushを再試行する @@ -64,18 +94,15 @@ export const push = async ( | [DeletePageChange] | [PinChange], options?: PushOptions, -): Promise> => { +): Promise> => { const injectedSocket = options?.socket; const socket = injectedSocket ?? await socketIO(); await connect(socket); + const pullResult = await pull(project, title); + if (isErr(pullResult)) return pullResult; + let metadata = unwrapOk(pullResult); try { - let page: PushMetadata = await Promise.all([ - pull(project, title), - getProjectId(project), - getUserId(), - ]).then(([page_, projectId, userId]) => ({ ...page_, projectId, userId })); - const { request } = wrap(socket); let attempts = 0; let changes: Change[] | [DeletePageChange] | [PinChange] = []; @@ -85,19 +112,17 @@ export const push = async ( while ( options?.maxAttempts === undefined || attempts < options.maxAttempts ) { - const pending = makeCommit(page, attempts, changes, reason); + const pending = makeCommit(metadata, attempts, changes, reason); changes = pending instanceof Promise ? await pending : pending; attempts++; - if (changes.length === 0) { - return { ok: true, value: page.commitId }; - } + if (changes.length === 0) return createOk(metadata.commitId); const data: PageCommit = { kind: "page", - projectId: page.projectId, - pageId: page.id, - parentId: page.commitId, - userId: page.userId, + projectId: metadata.projectId, + pageId: metadata.id, + parentId: metadata.commitId, + userId: metadata.userId, changes, cursor: null, freeze: true, @@ -108,22 +133,19 @@ export const push = async ( const result = (await request("socket.io-request", { method: "commit", data, - })) as Result< + })) as SocketResult< PageCommitResponse, UnexpectedError | TimeoutError | PageCommitError >; if (result.ok) { - page.commitId = result.value.commitId; + metadata.commitId = result.value.commitId; // success - return { ok: true, value: page.commitId }; + return createOk(metadata.commitId); } const name = result.value.name; if (name === "UnexpectedError") { - const error = new Error(); - error.name = result.value.name; - error.message = JSON.stringify(result.value); - throw error; + return createErr({ name, message: JSON.stringify(result.value) }); } if (name === "TimeoutError" || name === "SocketIOError") { await sleep(3000); @@ -132,26 +154,21 @@ export const push = async ( } if (name === "NotFastForwardError") { await sleep(1000); - page = { - ...await pull(project, title), - projectId: page.projectId, - userId: page.userId, - }; + const pullResult = await pull(project, title); + if (isErr(pullResult)) return pullResult; + metadata = unwrapOk(pullResult); } reason = name; // go back to the diff loop break; } } - return { - ok: false, - value: { - name: "RetryError", - attempts, - // from https://github.com/denoland/deno_std/blob/0.223.0/async/retry.ts#L23 - message: `Retrying exceeded the maxAttempts (${attempts}).`, - }, - }; + return createErr({ + name: "RetryError", + attempts, + // from https://github.com/denoland/deno_std/blob/0.223.0/async/retry.ts#L23 + message: `Retrying exceeded the maxAttempts (${attempts}).`, + }); } finally { if (!injectedSocket) await disconnect(socket); } diff --git a/browser/websocket/updateCodeBlock.ts b/browser/websocket/updateCodeBlock.ts index 9654aa1..2e9b350 100644 --- a/browser/websocket/updateCodeBlock.ts +++ b/browser/websocket/updateCodeBlock.ts @@ -5,8 +5,8 @@ import { diffToChanges } from "./diffToChanges.ts"; import { isSimpleCodeFile } from "./isSimpleCodeFile.ts"; import { SimpleCodeFile } from "./updateCodeFile.ts"; import { countBodyIndent, extractFromCodeTitle } from "./_codeBlock.ts"; -import { push, PushOptions, RetryError } from "./push.ts"; -import { Result } from "../../rest/util.ts"; +import { push, PushError, PushOptions } from "./push.ts"; +import { Result } from "../../deps/option-t.ts"; export interface UpdateCodeBlockOptions extends PushOptions { /** `true`でデバッグ出力ON */ @@ -26,7 +26,7 @@ export const updateCodeBlock = ( newCode: string | string[] | SimpleCodeFile, target: TinyCodeBlock, options?: UpdateCodeBlockOptions, -): Promise> => { +): Promise> => { const newCodeBody = getCodeBody(newCode); const bodyIndent = countBodyIndent(target); const oldCodeWithoutIndent: Line[] = target.bodyLines.map((e) => { diff --git a/browser/websocket/updateCodeFile.ts b/browser/websocket/updateCodeFile.ts index 0ccf88c..ea6ca31 100644 --- a/browser/websocket/updateCodeFile.ts +++ b/browser/websocket/updateCodeFile.ts @@ -4,8 +4,8 @@ import { getCodeBlocks, TinyCodeBlock } from "../../rest/getCodeBlocks.ts"; import { createNewLineId } from "./id.ts"; import { diff, toExtendedChanges } from "../../deps/onp.ts"; import { countBodyIndent } from "./_codeBlock.ts"; -import { push, PushOptions, RetryError } from "./push.ts"; -import { Result } from "../../rest/util.ts"; +import { push, PushError, PushOptions } from "./push.ts"; +import { Result } from "../../deps/option-t.ts"; /** コードブロックの上書きに使う情報のinterface */ export interface SimpleCodeFile { @@ -54,7 +54,7 @@ export const updateCodeFile = ( project: string, title: string, options?: UpdateCodeFileOptions, -): Promise> => { +): Promise> => { /** optionsの既定値はこの中に入れる */ const defaultOptions: Required< Omit @@ -68,9 +68,9 @@ export const updateCodeFile = ( return push( project, title, - async (page) => { + (page) => { const lines: Line[] = page.lines; - const codeBlocks = await getCodeBlocks({ project, title, lines }, { + const codeBlocks = getCodeBlocks({ project, title, lines }, { filename: codeFile.filename, }); const commits = [ diff --git a/deps/option-t.ts b/deps/option-t.ts new file mode 100644 index 0000000..88d42e0 --- /dev/null +++ b/deps/option-t.ts @@ -0,0 +1,2 @@ +export * from "https://esm.sh/option-t@49.1.0/plain_result"; +export * from "https://esm.sh/option-t@49.1.0/maybe"; diff --git a/deps/unknownutil.ts b/deps/unknownutil.ts new file mode 100644 index 0000000..7e93bbc --- /dev/null +++ b/deps/unknownutil.ts @@ -0,0 +1 @@ +export * from "https://deno.land/x/unknownutil@v3.18.1/mod.ts"; diff --git a/is.ts b/is.ts deleted file mode 100644 index eacc3b5..0000000 --- a/is.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { ErrorLike } from "./deps/scrapbox-rest.ts"; -// These code are based on https://deno.land/x/unknownutil@v1.1.0/is.ts - -export const isNone = (value: unknown): value is undefined | null => - value === null || value === undefined; -export const isString = (value: unknown): value is string => - typeof value === "string"; -export const isNumber = (value: unknown): value is number => - typeof value === "number"; -export const isArray = (value: unknown): value is T[] => - Array.isArray(value); -export const isObject = (value: unknown): value is Record => - typeof value === "object" && value !== null; - -export const isErrorLike = (e: unknown): e is ErrorLike => { - if (!isObject(e)) return false; - return (e.name === undefined || typeof e.name === "string") && - typeof e.message === "string"; -}; - -/** 与えられたobjectもしくはJSONテキストをErrorLikeに変換できるかどうか試す - * - * @param e 試したいobjectもしくはテキスト - * @return 変換できなかったら`false`を返す。変換できたらそのobjectを返す - */ -export const tryToErrorLike = (e: unknown): false | ErrorLike => { - try { - const json = typeof e === "string" ? JSON.parse(e) : e; - if (!isErrorLike(json)) return false; - return json; - } catch (e2: unknown) { - if (e2 instanceof SyntaxError) return false; - // JSONのparse error以外はそのまま投げる - throw e2; - } -}; diff --git a/rest/auth.ts b/rest/auth.ts index fe54500..0b084cd 100644 --- a/rest/auth.ts +++ b/rest/auth.ts @@ -1,4 +1,7 @@ +import { createOk, mapForResult, Result } from "../deps/option-t.ts"; import { getProfile } from "./profile.ts"; +import { HTTPError } from "./responseIntoResult.ts"; +import { AbortError, NetworkError } from "./robustFetch.ts"; import { BaseOptions } from "./util.ts"; // scrapbox.io内なら`window._csrf`にCSRF tokenが入っている @@ -20,9 +23,8 @@ export const cookie = (sid: string): string => `connect.sid=${sid}`; */ export const getCSRFToken = async ( init?: BaseOptions, -): Promise => { - if (globalThis._csrf) return globalThis._csrf; - - const user = await getProfile(init); - return user.csrfToken; -}; +): Promise> => + globalThis._csrf ? createOk(globalThis._csrf) : mapForResult( + await getProfile(init), + (user) => user.csrfToken, + ); diff --git a/rest/error.ts b/rest/error.ts deleted file mode 100644 index 3b19eaa..0000000 --- a/rest/error.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { ErrorLike } from "../deps/scrapbox-rest.ts"; -import { tryToErrorLike } from "../is.ts"; - -/** 想定されない応答が帰ってきたときに投げる例外 */ -export class UnexpectedResponseError extends Error { - name = "UnexpectedResponseError"; - - constructor( - public response: Response, - ) { - super( - `${response.status} ${response.statusText} when fetching ${response.url}`, - ); - - // @ts-ignore only available on V8 - if (Error.captureStackTrace) { - // @ts-ignore only available on V8 - Error.captureStackTrace(this, UnexpectedResponseError); - } - } -} - -/** 失敗した要求からエラー情報を取り出す */ -export const makeError = async ( - res: Response, -): Promise<{ ok: false; value: T }> => { - const response = res.clone(); - const text = await response.text(); - const value = tryToErrorLike(text); - if (!value) { - throw new UnexpectedResponseError(response); - } - return { - ok: false, - value: value as T, - }; -}; diff --git a/rest/getCodeBlock.ts b/rest/getCodeBlock.ts index f4cdc9a..7f45412 100644 --- a/rest/getCodeBlock.ts +++ b/rest/getCodeBlock.ts @@ -1,12 +1,21 @@ -import type { +import { NotFoundError, NotLoggedInError, NotMemberError, } from "../deps/scrapbox-rest.ts"; import { cookie } from "./auth.ts"; -import { makeError } from "./error.ts"; import { encodeTitleURI } from "../title.ts"; -import { BaseOptions, Result, setDefaults } from "./util.ts"; +import { BaseOptions, setDefaults } from "./util.ts"; +import { + isErr, + mapAsyncForResult, + mapErrAsyncForResult, + Result, + unwrapOk, +} from "../deps/option-t.ts"; +import { HTTPError, responseIntoResult } from "./responseIntoResult.ts"; +import { parseHTTPError } from "./parseHTTPError.ts"; +import { AbortError, NetworkError } from "./robustFetch.ts"; const getCodeBlock_toRequest: GetCodeBlock["toRequest"] = ( project, @@ -15,28 +24,30 @@ const getCodeBlock_toRequest: GetCodeBlock["toRequest"] = ( options, ) => { const { sid, hostName } = setDefaults(options ?? {}); - const path = `https://${hostName}/api/code/${project}/${ - encodeTitleURI(title) - }/${encodeTitleURI(filename)}`; return new Request( - path, + `https://${hostName}/api/code/${project}/${encodeTitleURI(title)}/${ + encodeTitleURI(filename) + }`, sid ? { headers: { Cookie: cookie(sid) } } : undefined, ); }; -const getCodeBlock_fromResponse: GetCodeBlock["fromResponse"] = async (res) => { - if (!res.ok) { - return res.status === 404 && - res.headers.get("Content-Type")?.includes?.("text/plain") - ? { - ok: false, - value: { name: "NotFoundError", message: "Code block is not found" }, - } - : makeError(res); - } - return { ok: true, value: await res.text() }; -}; +const getCodeBlock_fromResponse: GetCodeBlock["fromResponse"] = async (res) => + mapAsyncForResult( + await mapErrAsyncForResult( + responseIntoResult(res), + async (res) => + res.response.status === 404 && + res.response.headers.get("Content-Type")?.includes?.("text/plain") + ? { name: "NotFoundError", message: "Code block is not found" } + : (await parseHTTPError(res, [ + "NotLoggedInError", + "NotMemberError", + ])) ?? res, + ), + (res) => res.text(), + ); export interface GetCodeBlock { /** /api/code/:project/:title/:filename の要求を組み立てる @@ -62,7 +73,7 @@ export interface GetCodeBlock { fromResponse: (res: Response) => Promise< Result< string, - NotFoundError | NotLoggedInError | NotMemberError + NotFoundError | NotLoggedInError | NotMemberError | HTTPError > >; @@ -74,7 +85,12 @@ export interface GetCodeBlock { ): Promise< Result< string, - NotFoundError | NotLoggedInError | NotMemberError + | NotFoundError + | NotLoggedInError + | NotMemberError + | NetworkError + | AbortError + | HTTPError > >; } @@ -92,10 +108,9 @@ export const getCodeBlock: GetCodeBlock = async ( filename, options, ) => { - const { fetch } = setDefaults(options ?? {}); const req = getCodeBlock_toRequest(project, title, filename, options); - const res = await fetch(req); - return await getCodeBlock_fromResponse(res); + const res = await setDefaults(options ?? {}).fetch(req); + return isErr(res) ? res : getCodeBlock_fromResponse(unwrapOk(res)); }; getCodeBlock.toRequest = getCodeBlock_toRequest; diff --git a/rest/getCodeBlocks.test.ts b/rest/getCodeBlocks.test.ts index e409e59..5525f46 100644 --- a/rest/getCodeBlocks.test.ts +++ b/rest/getCodeBlocks.test.ts @@ -231,11 +231,11 @@ const sample: Line[] = [ Deno.test("getCodeBlocks()", async (t) => { await assertSnapshot( t, - await getCodeBlocks({ project, title, lines: sample }), + getCodeBlocks({ project, title, lines: sample }), ); await t.step("filename filter", async (st) => { const filename = "インデント.md"; - const codeBlocks = await getCodeBlocks({ project, title, lines: sample }, { + const codeBlocks = getCodeBlocks({ project, title, lines: sample }, { filename, }); const yet = []; @@ -247,7 +247,7 @@ Deno.test("getCodeBlocks()", async (t) => { }); await t.step("language name filter", async (st) => { const lang = "py"; - const codeBlocks = await getCodeBlocks({ project, title, lines: sample }, { + const codeBlocks = getCodeBlocks({ project, title, lines: sample }, { lang, }); const yet = []; @@ -259,7 +259,7 @@ Deno.test("getCodeBlocks()", async (t) => { }); await t.step("title line ID filter", async (st) => { const titleLineId = "63b7b1261280f00000c9bc34"; - const codeBlocks = await getCodeBlocks({ project, title, lines: sample }, { + const codeBlocks = getCodeBlocks({ project, title, lines: sample }, { titleLineId, }); const yet = []; diff --git a/rest/getCodeBlocks.ts b/rest/getCodeBlocks.ts index 549e026..01d788a 100644 --- a/rest/getCodeBlocks.ts +++ b/rest/getCodeBlocks.ts @@ -1,5 +1,4 @@ import type { Line } from "../deps/scrapbox-rest.ts"; -import { pull } from "../browser/websocket/pull.ts"; import { CodeTitle, extractFromCodeTitle, @@ -45,11 +44,10 @@ export interface GetCodeBlocksFilter { * @param filter 取得するコードブロックを絞り込むfilter * @return コードブロックの配列 */ -export const getCodeBlocks = async ( - target: { project: string; title: string; lines?: Line[] }, +export const getCodeBlocks = ( + target: { project: string; title: string; lines: Line[] }, filter?: GetCodeBlocksFilter, -): Promise => { - const lines = await getLines(target); +): TinyCodeBlock[] => { const codeBlocks: TinyCodeBlock[] = []; let currentCode: CodeTitle & { @@ -61,7 +59,7 @@ export const getCodeBlocks = async ( lang: "", indent: 0, }; - for (const line of lines) { + for (const line of target.lines) { if (currentCode.isCodeBlock) { const body = extractFromCodeBody(line.text, currentCode.indent); if (body === null) { @@ -93,30 +91,16 @@ export const getCodeBlocks = async ( return codeBlocks.filter((codeBlock) => isMatchFilter(codeBlock, filter)); }; -/** targetを`Line[]`に変換する */ -const getLines = async ( - target: { project: string; title: string; lines?: Line[] }, -): Promise => { - if (target.lines !== undefined) { - return target.lines; - } else { - const head = await pull(target.project, target.title); - return head.lines; - } -}; - /** コードブロックのフィルターに合致しているか検証する */ const isMatchFilter = ( codeBlock: TinyCodeBlock, filter?: GetCodeBlocksFilter, -): boolean => { - const equals = (a: unknown, b: unknown) => !a || a === b; - return ( - equals(filter?.filename, codeBlock.filename) && - equals(filter?.lang, codeBlock.lang) && - equals(filter?.titleLineId, codeBlock.titleLine.id) - ); -}; +): boolean => + equals(filter?.filename, codeBlock.filename) && + equals(filter?.lang, codeBlock.lang) && + equals(filter?.titleLineId, codeBlock.titleLine.id); + +const equals = (a: unknown, b: unknown): boolean => !a || a === b; /** 行テキストがコードブロックの一部であればそのテキストを、そうでなければnullを返す * diff --git a/rest/getGyazoToken.ts b/rest/getGyazoToken.ts index daefb6c..14040f7 100644 --- a/rest/getGyazoToken.ts +++ b/rest/getGyazoToken.ts @@ -1,7 +1,16 @@ +import { + isErr, + mapAsyncForResult, + mapErrAsyncForResult, + Result, + unwrapOk, +} from "../deps/option-t.ts"; import type { NotLoggedInError } from "../deps/scrapbox-rest.ts"; import { cookie } from "./auth.ts"; -import { makeError } from "./error.ts"; -import { BaseOptions, Result, setDefaults } from "./util.ts"; +import { parseHTTPError } from "./parseHTTPError.ts"; +import { HTTPError, responseIntoResult } from "./responseIntoResult.ts"; +import { AbortError, NetworkError } from "./robustFetch.ts"; +import { BaseOptions, setDefaults } from "./util.ts"; export interface GetGyazoTokenOptions extends BaseOptions { /** Gyazo Teamsのチーム名 @@ -21,10 +30,10 @@ export const getGyazoToken = async ( ): Promise< Result< string | undefined, - NotLoggedInError + NotLoggedInError | NetworkError | AbortError | HTTPError > > => { - const { sid, hostName, gyazoTeamsName } = setDefaults(init ?? {}); + const { fetch, sid, hostName, gyazoTeamsName } = setDefaults(init ?? {}); const req = new Request( `https://${hostName}/api/login/gyazo/oauth-upload/token${ gyazoTeamsName ? `?gyazoTeamsName=${gyazoTeamsName}` : "" @@ -33,10 +42,14 @@ export const getGyazoToken = async ( ); const res = await fetch(req); - if (!res.ok) { - return makeError(res); - } + if (isErr(res)) return res; - const { token } = (await res.json()) as { token?: string }; - return { ok: true, value: token }; + return mapAsyncForResult( + await mapErrAsyncForResult( + responseIntoResult(unwrapOk(res)), + async (error) => + (await parseHTTPError(error, ["NotLoggedInError"])) ?? error, + ), + (res) => res.json().then((json) => json.token as string | undefined), + ); }; diff --git a/rest/getTweetInfo.ts b/rest/getTweetInfo.ts index 4871b3e..039536d 100644 --- a/rest/getTweetInfo.ts +++ b/rest/getTweetInfo.ts @@ -1,12 +1,23 @@ -import type { +import { + isErr, + mapAsyncForResult, + mapErrAsyncForResult, + orElseAsyncForResult, + Result, + toResultOkFromMaybe, + unwrapOk, +} from "../deps/option-t.ts"; +import { BadRequestError, InvalidURLError, SessionError, TweetInfo, } from "../deps/scrapbox-rest.ts"; import { cookie, getCSRFToken } from "./auth.ts"; -import { makeError } from "./error.ts"; -import { ExtendedOptions, Result, setDefaults } from "./util.ts"; +import { parseHTTPError } from "./parseHTTPError.ts"; +import { HTTPError, responseIntoResult } from "./responseIntoResult.ts"; +import { AbortError, NetworkError } from "./robustFetch.ts"; +import { ExtendedOptions, setDefaults } from "./util.ts"; /** 指定したTweetの情報を取得する * @@ -23,18 +34,28 @@ export const getTweetInfo = async ( | SessionError | InvalidURLError | BadRequestError + | NetworkError + | AbortError + | HTTPError > > => { const { sid, hostName, fetch, csrf } = setDefaults(init ?? {}); + + const csrfResult = await orElseAsyncForResult( + toResultOkFromMaybe(csrf), + () => getCSRFToken(init), + ); + if (isErr(csrfResult)) return csrfResult; + const req = new Request( `https://${hostName}/api/embed-text/twitter?url=${ - encodeURIComponent(url.toString()) + encodeURIComponent(`${url}`) }`, { method: "POST", headers: { "Content-Type": "application/json;charset=utf-8", - "X-CSRF-TOKEN": csrf ?? await getCSRFToken(init), + "X-CSRF-TOKEN": unwrapOk(csrfResult), ...(sid ? { Cookie: cookie(sid) } : {}), }, body: JSON.stringify({ timeout: 3000 }), @@ -42,20 +63,25 @@ export const getTweetInfo = async ( ); const res = await fetch(req); + if (isErr(res)) return res; - if (!res.ok) { - if (res.status === 422) { - return { - ok: false, - value: { + return mapErrAsyncForResult( + await mapAsyncForResult( + responseIntoResult(unwrapOk(res)), + (res) => res.json() as Promise, + ), + async (res) => { + if (res.response.status === 422) { + return { name: "InvalidURLError", - message: (await res.json()).message as string, - }, - }; - } - return makeError(res); - } - - const tweet = (await res.json()) as TweetInfo; - return { ok: true, value: tweet }; + message: (await res.response.json()).message as string, + }; + } + const parsed = await parseHTTPError(res, [ + "SessionError", + "BadRequestError", + ]); + return parsed ?? res; + }, + ); }; diff --git a/rest/getWebPageTitle.ts b/rest/getWebPageTitle.ts index 0e23309..07a1314 100644 --- a/rest/getWebPageTitle.ts +++ b/rest/getWebPageTitle.ts @@ -1,11 +1,22 @@ +import { + isErr, + mapAsyncForResult, + mapErrAsyncForResult, + orElseAsyncForResult, + Result, + toResultOkFromMaybe, + unwrapOk, +} from "../deps/option-t.ts"; import type { BadRequestError, InvalidURLError, SessionError, } from "../deps/scrapbox-rest.ts"; import { cookie, getCSRFToken } from "./auth.ts"; -import { makeError } from "./error.ts"; -import { ExtendedOptions, Result, setDefaults } from "./util.ts"; +import { parseHTTPError } from "./parseHTTPError.ts"; +import { HTTPError, responseIntoResult } from "./responseIntoResult.ts"; +import { AbortError, NetworkError } from "./robustFetch.ts"; +import { ExtendedOptions, setDefaults } from "./util.ts"; /** 指定したURLのweb pageのtitleをscrapboxのserver経由で取得する * @@ -22,19 +33,28 @@ export const getWebPageTitle = async ( | SessionError | InvalidURLError | BadRequestError + | NetworkError + | AbortError + | HTTPError > > => { const { sid, hostName, fetch, csrf } = setDefaults(init ?? {}); + const csrfResult = await orElseAsyncForResult( + toResultOkFromMaybe(csrf), + () => getCSRFToken(init), + ); + if (isErr(csrfResult)) return csrfResult; + const req = new Request( `https://${hostName}/api/embed-text/url?url=${ - encodeURIComponent(url.toString()) + encodeURIComponent(`${url}`) }`, { method: "POST", headers: { "Content-Type": "application/json;charset=utf-8", - "X-CSRF-TOKEN": csrf ?? await getCSRFToken(init), + "X-CSRF-TOKEN": unwrapOk(csrfResult), ...(sid ? { Cookie: cookie(sid) } : {}), }, body: JSON.stringify({ timeout: 3000 }), @@ -42,20 +62,21 @@ export const getWebPageTitle = async ( ); const res = await fetch(req); + if (isErr(res)) return res; - if (!res.ok) { - if (res.status === 422) { - return { - ok: false, - value: { - name: "InvalidURLError", - message: (await res.json()).message as string, - }, - }; - } - return makeError(res); - } - - const { title } = (await res.json()) as { title: string }; - return { ok: true, value: title }; + return mapAsyncForResult( + await mapErrAsyncForResult( + responseIntoResult(unwrapOk(res)), + async (error) => + (await parseHTTPError(error, [ + "SessionError", + "BadRequestError", + "InvalidURLError", + ])) ?? error, + ), + async (res) => { + const { title } = (await res.json()) as { title: string }; + return title; + }, + ); }; diff --git a/rest/link.ts b/rest/link.ts index 576925f..52b6db5 100644 --- a/rest/link.ts +++ b/rest/link.ts @@ -1,3 +1,11 @@ +import { + createOk, + isErr, + mapAsyncForResult, + mapErrAsyncForResult, + Result, + unwrapOk, +} from "../deps/option-t.ts"; import type { ErrorLike, NotFoundError, @@ -5,8 +13,10 @@ import type { SearchedTitle, } from "../deps/scrapbox-rest.ts"; import { cookie } from "./auth.ts"; -import { makeError } from "./error.ts"; -import { BaseOptions, Result, setDefaults } from "./util.ts"; +import { parseHTTPError } from "./parseHTTPError.ts"; +import { HTTPError, responseIntoResult } from "./responseIntoResult.ts"; +import { AbortError, NetworkError } from "./robustFetch.ts"; +import { BaseOptions, setDefaults } from "./util.ts"; /** 不正なfollowingIdを渡されたときに発生するエラー */ export interface InvalidFollowingIdError extends ErrorLike { @@ -18,6 +28,11 @@ export interface GetLinksOptions extends BaseOptions { followingId?: string; } +export interface GetLinksResult { + pages: SearchedTitle[]; + followingId: string; +} + /** 指定したprojectのリンクデータを取得する * * @param project データを取得したいproject @@ -26,10 +41,15 @@ export const getLinks = async ( project: string, options?: GetLinksOptions, ): Promise< - Result<{ - pages: SearchedTitle[]; - followingId: string; - }, NotFoundError | NotLoggedInError | InvalidFollowingIdError> + Result< + GetLinksResult, + | NotFoundError + | NotLoggedInError + | InvalidFollowingIdError + | NetworkError + | AbortError + | HTTPError + > > => { const { sid, hostName, fetch, followingId } = setDefaults(options ?? {}); @@ -41,26 +61,28 @@ export const getLinks = async ( ); const res = await fetch(req); + if (isErr(res)) return res; - if (!res.ok) { - if (res.status === 422) { - return { - ok: false, - value: { - name: "InvalidFollowingIdError", - message: await res.text(), - }, - }; - } - - return makeError(res); - } - - const pages = (await res.json()) as SearchedTitle[]; - return { - ok: true, - value: { pages, followingId: res.headers.get("X-following-id") ?? "" }, - }; + return mapAsyncForResult( + await mapErrAsyncForResult( + responseIntoResult(unwrapOk(res)), + async (error) => + error.response.status === 422 + ? { + name: "InvalidFollowingIdError", + message: await error.response.text(), + } as InvalidFollowingIdError + : (await parseHTTPError(error, [ + "NotFoundError", + "NotLoggedInError", + ])) ?? error, + ), + (res) => + res.json().then((pages: SearchedTitle[]) => ({ + pages, + followingId: res.headers.get("X-following-id") ?? "", + })), + ); }; /** 指定したprojectの全てのリンクデータを取得する @@ -70,57 +92,64 @@ export const getLinks = async ( * @param project データを取得したいproject * @return 認証が通らなかったらエラーを、通ったらasync generatorを返す */ -export const readLinksBulk = async ( +export async function* readLinksBulk( project: string, options?: BaseOptions, -): Promise< - | NotFoundError - | NotLoggedInError - | InvalidFollowingIdError - | AsyncGenerator -> => { - const first = await getLinks(project, options); - if (!first.ok) return first.value; - - return async function* () { - yield first.value.pages; - let followingId = first.value.followingId; - - while (followingId) { - const result = await getLinks(project, { followingId, ...options }); - - // すでに認証は通っているので、ここでエラーになるはずがない - if (!result.ok) { - throw new Error("The authorization cannot be unavailable"); - } - yield result.value.pages; - followingId = result.value.followingId; +): AsyncGenerator< + Result< + SearchedTitle[], + | NotFoundError + | NotLoggedInError + | InvalidFollowingIdError + | NetworkError + | AbortError + | HTTPError + >, + void, + unknown +> { + let followingId: string | undefined; + do { + const result = await getLinks(project, { followingId, ...options }); + if (isErr(result)) { + yield result; + return; } - }(); -}; + const res = unwrapOk(result); + + yield createOk(res.pages); + followingId = res.followingId; + } while (followingId); +} /** 指定したprojectの全てのリンクデータを取得し、一つづつ返す * * @param project データを取得したいproject * @return 認証が通らなかったらエラーを、通ったらasync generatorを返す */ -export const readLinks = async ( +export async function* readLinks( project: string, options?: BaseOptions, -): Promise< - | NotFoundError - | NotLoggedInError - | InvalidFollowingIdError - | AsyncGenerator -> => { - const reader = await readLinksBulk(project, options); - if ("name" in reader) return reader; - - return async function* () { - for await (const titles of reader) { - for (const title of titles) { - yield title; - } +): AsyncGenerator< + Result< + SearchedTitle, + | NotFoundError + | NotLoggedInError + | InvalidFollowingIdError + | NetworkError + | AbortError + | HTTPError + >, + void, + unknown +> { + for await (const result of readLinksBulk(project, options)) { + if (isErr(result)) { + yield result; + return; } - }(); -}; + for (const page of unwrapOk(result)) { + yield createOk(page); + } + } +} diff --git a/rest/mod.ts b/rest/mod.ts index 849fefe..46e6960 100644 --- a/rest/mod.ts +++ b/rest/mod.ts @@ -12,7 +12,7 @@ export * from "./getTweetInfo.ts"; export * from "./getGyazoToken.ts"; export * from "./auth.ts"; export * from "./util.ts"; -export * from "./error.ts"; +export * from "./parseHTTPError.ts"; export * from "./getCodeBlocks.ts"; export * from "./getCodeBlock.ts"; export * from "./uploadToGCS.ts"; diff --git a/rest/page-data.ts b/rest/page-data.ts index 6407979..5c9c1ad 100644 --- a/rest/page-data.ts +++ b/rest/page-data.ts @@ -1,5 +1,14 @@ +import { + createOk, + isErr, + mapAsyncForResult, + mapErrAsyncForResult, + orElseAsyncForResult, + Result, + toResultOkFromMaybe, + unwrapOk, +} from "../deps/option-t.ts"; import type { - ErrorLike, ExportedData, ImportedData, NotFoundError, @@ -7,8 +16,10 @@ import type { NotPrivilegeError, } from "../deps/scrapbox-rest.ts"; import { cookie, getCSRFToken } from "./auth.ts"; -import { makeError } from "./error.ts"; -import { BaseOptions, ExtendedOptions, Result, setDefaults } from "./util.ts"; +import { parseHTTPError } from "./parseHTTPError.ts"; +import { HTTPError, responseIntoResult } from "./responseIntoResult.ts"; +import { AbortError, NetworkError } from "./robustFetch.ts"; +import { BaseOptions, ExtendedOptions, setDefaults } from "./util.ts"; /** projectにページをインポートする * * @param project - インポート先のprojectの名前 @@ -19,11 +30,9 @@ export const importPages = async ( data: ImportedData, init: ExtendedOptions, ): Promise< - Result + Result > => { - if (data.pages.length === 0) { - return { ok: true, value: "No pages to import." }; - } + if (data.pages.length === 0) return createOk("No pages to import."); const { sid, hostName, fetch, csrf } = setDefaults(init ?? {}); const formData = new FormData(); @@ -34,6 +43,13 @@ export const importPages = async ( }), ); formData.append("name", "undefined"); + + const csrfResult = await orElseAsyncForResult( + toResultOkFromMaybe(csrf), + () => getCSRFToken(init), + ); + if (isErr(csrfResult)) return csrfResult; + const req = new Request( `https://${hostName}/api/page-data/import/${project}.json`, { @@ -41,19 +57,19 @@ export const importPages = async ( headers: { ...(sid ? { Cookie: cookie(sid) } : {}), Accept: "application/json, text/plain, */*", - "X-CSRF-TOKEN": csrf ?? await getCSRFToken(init), + "X-CSRF-TOKEN": unwrapOk(csrfResult), }, body: formData, }, ); const res = await fetch(req); - if (!res.ok) { - return makeError(res); - } + if (isErr(res)) return res; - const { message } = (await res.json()) as { message: string }; - return { ok: true, value: message }; + return mapAsyncForResult( + responseIntoResult(unwrapOk(res)), + async (res) => (await res.json()).message as string, + ); }; /** `exportPages`の認証情報 */ @@ -71,7 +87,12 @@ export const exportPages = async ( ): Promise< Result< ExportedData, - NotFoundError | NotPrivilegeError | NotLoggedInError + | NotFoundError + | NotPrivilegeError + | NotLoggedInError + | NetworkError + | AbortError + | HTTPError > > => { const { sid, hostName, fetch, metadata } = setDefaults(init ?? {}); @@ -81,11 +102,18 @@ export const exportPages = async ( sid ? { headers: { Cookie: cookie(sid) } } : undefined, ); const res = await fetch(req); + if (isErr(res)) return res; - if (!res.ok) { - return makeError(res); - } - - const value = (await res.json()) as ExportedData; - return { ok: true, value }; + return mapAsyncForResult( + await mapErrAsyncForResult( + responseIntoResult(unwrapOk(res)), + async (error) => + (await parseHTTPError(error, [ + "NotFoundError", + "NotLoggedInError", + "NotPrivilegeError", + ])) ?? error, + ), + (res) => res.json() as Promise>, + ); }; diff --git a/rest/pages.ts b/rest/pages.ts index 2306453..ec6fd36 100644 --- a/rest/pages.ts +++ b/rest/pages.ts @@ -7,9 +7,18 @@ import type { PageList, } from "../deps/scrapbox-rest.ts"; import { cookie } from "./auth.ts"; -import { makeError } from "./error.ts"; +import { parseHTTPError } from "./parseHTTPError.ts"; import { encodeTitleURI } from "../title.ts"; -import { BaseOptions, Result, setDefaults } from "./util.ts"; +import { BaseOptions, setDefaults } from "./util.ts"; +import { + andThenAsyncForResult, + mapAsyncForResult, + mapErrAsyncForResult, + Result, + unwrapOrForMaybe, +} from "../deps/option-t.ts"; +import { HTTPError, responseIntoResult } from "./responseIntoResult.ts"; +import { AbortError, NetworkError } from "./robustFetch.ts"; /** Options for `getPage()` */ export interface GetPageOption extends BaseOptions { @@ -35,32 +44,43 @@ const getPage_toRequest: GetPage["toRequest"] = ( for (const id of projects ?? []) { params.append("projects", id); } - const path = `https://${hostName}/api/pages/${project}/${ - encodeTitleURI(title) - }?${params.toString()}`; return new Request( - path, + `https://${hostName}/api/pages/${project}/${ + encodeTitleURI(title) + }?${params}`, sid ? { headers: { Cookie: cookie(sid) } } : undefined, ); }; -const getPage_fromResponse: GetPage["fromResponse"] = async (res) => { - if (!res.ok) { - if (res.status === 414) { - return { - ok: false, - value: { +const getPage_fromResponse: GetPage["fromResponse"] = async (res) => + mapErrAsyncForResult( + await mapAsyncForResult( + responseIntoResult(res), + (res) => res.json() as Promise, + ), + async ( + error, + ) => { + if (error.response.status === 414) { + return { name: "TooLongURIError", message: "project ids may be too much.", - }, - }; - } - return makeError(res); - } - const value = (await res.json()) as Page; - return { ok: true, value }; -}; + }; + } + + return unwrapOrForMaybe< + NotFoundError | NotLoggedInError | NotMemberError | HTTPError + >( + await parseHTTPError(error, [ + "NotFoundError", + "NotLoggedInError", + "NotMemberError", + ]), + error, + ); + }, + ); export interface GetPage { /** /api/pages/:project/:title の要求を組み立てる @@ -84,14 +104,24 @@ export interface GetPage { fromResponse: (res: Response) => Promise< Result< Page, - NotFoundError | NotLoggedInError | NotMemberError | TooLongURIError + | NotFoundError + | NotLoggedInError + | NotMemberError + | TooLongURIError + | HTTPError > >; (project: string, title: string, options?: GetPageOption): Promise< Result< Page, - NotFoundError | NotLoggedInError | NotMemberError | TooLongURIError + | NotFoundError + | NotLoggedInError + | NotMemberError + | TooLongURIError + | HTTPError + | NetworkError + | AbortError > >; } @@ -106,12 +136,23 @@ export const getPage: GetPage = async ( project, title, options, -) => { - const { fetch } = setDefaults(options ?? {}); - const req = getPage_toRequest(project, title, options); - const res = await fetch(req); - return await getPage_fromResponse(res); -}; +) => + andThenAsyncForResult< + Response, + Page, + | NotFoundError + | NotLoggedInError + | NotMemberError + | TooLongURIError + | HTTPError + | NetworkError + | AbortError + >( + await setDefaults(options ?? {}).fetch( + getPage_toRequest(project, title, options), + ), + (input) => getPage_fromResponse(input), + ); getPage.toRequest = getPage_toRequest; getPage.fromResponse = getPage_fromResponse; @@ -163,14 +204,19 @@ export interface ListPages { fromResponse: (res: Response) => Promise< Result< PageList, - NotFoundError | NotLoggedInError | NotMemberError + NotFoundError | NotLoggedInError | NotMemberError | HTTPError > >; (project: string, options?: ListPagesOption): Promise< Result< PageList, - NotFoundError | NotLoggedInError | NotMemberError + | NotFoundError + | NotLoggedInError + | NotMemberError + | NetworkError + | AbortError + | HTTPError > >; } @@ -183,21 +229,31 @@ const listPages_toRequest: ListPages["toRequest"] = (project, options) => { if (sort !== undefined) params.append("sort", sort); if (limit !== undefined) params.append("limit", `${limit}`); if (skip !== undefined) params.append("skip", `${skip}`); - const path = `https://${hostName}/api/pages/${project}?${params.toString()}`; return new Request( - path, + `https://${hostName}/api/pages/${project}?${params}`, sid ? { headers: { Cookie: cookie(sid) } } : undefined, ); }; -const listPages_fromResponse: ListPages["fromResponse"] = async (res) => { - if (!res.ok) { - return makeError(res); - } - const value = (await res.json()) as PageList; - return { ok: true, value }; -}; +const listPages_fromResponse: ListPages["fromResponse"] = async (res) => + mapErrAsyncForResult( + await mapAsyncForResult( + responseIntoResult(res), + (res) => res.json() as Promise, + ), + async (error) => + unwrapOrForMaybe< + NotFoundError | NotLoggedInError | NotMemberError | HTTPError + >( + await parseHTTPError(error, [ + "NotFoundError", + "NotLoggedInError", + "NotMemberError", + ]), + error, + ), + ); /** 指定したprojectのページを一覧する * @@ -207,11 +263,22 @@ const listPages_fromResponse: ListPages["fromResponse"] = async (res) => { export const listPages: ListPages = async ( project, options?, -) => { - const { fetch } = setDefaults(options ?? {}); - const res = await fetch(listPages_toRequest(project, options)); - return await listPages_fromResponse(res); -}; +) => + andThenAsyncForResult< + Response, + PageList, + | NotFoundError + | NotLoggedInError + | NotMemberError + | NetworkError + | AbortError + | HTTPError + >( + await setDefaults(options ?? {})?.fetch( + listPages_toRequest(project, options), + ), + listPages_fromResponse, + ); listPages.toRequest = listPages_toRequest; listPages.fromResponse = listPages_fromResponse; diff --git a/rest/parseHTTPError.ts b/rest/parseHTTPError.ts new file mode 100644 index 0000000..28d724a --- /dev/null +++ b/rest/parseHTTPError.ts @@ -0,0 +1,94 @@ +import { + BadRequestError, + InvalidURLError, + NoQueryError, + NotFoundError, + NotLoggedInError, + NotMemberError, + NotPrivilegeError, + SessionError, +} from "../deps/scrapbox-rest.ts"; +import { Maybe } from "../deps/option-t.ts"; +import { + isArrayOf, + isLiteralOneOf, + isRecord, + isString, +} from "../deps/unknownutil.ts"; +import { HTTPError } from "./responseIntoResult.ts"; + +export interface RESTfullAPIErrorMap { + BadRequestError: BadRequestError; + NotFoundError: NotFoundError; + NotLoggedInError: NotLoggedInError; + NotMemberError: NotMemberError; + SessionError: SessionError; + InvalidURLError: InvalidURLError; + NoQueryError: NoQueryError; + NotPrivilegeError: NotPrivilegeError; +} + +/** 失敗した要求からエラー情報を取り出す */ +export const parseHTTPError = async < + ErrorNames extends keyof RESTfullAPIErrorMap, +>( + error: HTTPError, + errorNames: ErrorNames[], +): Promise> => { + const res = error.response.clone(); + const isErrorNames = isLiteralOneOf(errorNames); + try { + const json: unknown = await res.json(); + if (!isRecord(json)) return; + if (res.status === 422) { + if (!isString(json.message)) return; + for ( + const name of [ + "NoQueryError", + "InvalidURLError", + ] as (keyof RESTfullAPIErrorMap)[] + ) { + if (!(errorNames as string[]).includes(name)) continue; + return { + name, + message: json.message, + } as unknown as RESTfullAPIErrorMap[ErrorNames]; + } + } + if (!isErrorNames(json.name)) return; + if (!isString(json.message)) return; + if (json.name === "NotLoggedInError") { + if (!isRecord(json.detals)) return; + if (!isString(json.detals.project)) return; + if (!isArrayOf(isLoginStrategies)(json.detals.loginStrategies)) return; + return { + name: json.name, + message: json.message, + details: { + project: json.detals.project, + loginStrategies: json.detals.loginStrategies, + }, + } as unknown as RESTfullAPIErrorMap[ErrorNames]; + } + return { + name: json.name, + message: json.message, + } as unknown as RESTfullAPIErrorMap[ErrorNames]; + } catch (e: unknown) { + if (e instanceof SyntaxError) return; + // JSONのparse error以外はそのまま投げる + throw e; + } +}; + +const isLoginStrategies = isLiteralOneOf( + [ + "google", + "github", + "microsoft", + "gyazo", + "email", + "saml", + "easy-trial", + ] as const, +); diff --git a/rest/profile.ts b/rest/profile.ts index f2386d5..89086c5 100644 --- a/rest/profile.ts +++ b/rest/profile.ts @@ -1,23 +1,63 @@ -import type { GuestUser, MemberUser } from "../deps/scrapbox-rest.ts"; +import { + isErr, + mapAsyncForResult, + Result, + unwrapOk, +} from "../deps/option-t.ts"; +import { GuestUser, MemberUser } from "../deps/scrapbox-rest.ts"; import { cookie } from "./auth.ts"; +import { HTTPError, responseIntoResult } from "./responseIntoResult.ts"; +import { AbortError, NetworkError } from "./robustFetch.ts"; import { BaseOptions, setDefaults } from "./util.ts"; -import { UnexpectedResponseError } from "./error.ts"; - -/** get user profile - * - * @param init connect.sid etc. - */ -export const getProfile = async ( - init?: BaseOptions, -): Promise => { - const { sid, hostName, fetch } = setDefaults(init ?? {}); - const request = new Request( + +export interface GetProfile { + /** /api/users/me の要求を組み立てる + * + * @param init connect.sid etc. + * @return request + */ + toRequest: (init?: BaseOptions) => Request; + + /** get the user profile from the given response + * + * @param res response + * @return user profile + */ + fromResponse: ( + res: Response, + ) => Promise< + Result + >; + + (init?: BaseOptions): Promise< + Result + >; +} + +const getProfile_toRequest: GetProfile["toRequest"] = ( + init, +) => { + const { sid, hostName } = setDefaults(init ?? {}); + return new Request( `https://${hostName}/api/users/me`, sid ? { headers: { Cookie: cookie(sid) } } : undefined, ); - const response = await fetch(request); - if (!response.ok) { - throw new UnexpectedResponseError(response); - } - return (await response.json()) as MemberUser | GuestUser; }; + +const getProfile_fromResponse: GetProfile["fromResponse"] = (response) => + mapAsyncForResult( + responseIntoResult(response), + async (res) => (await res.json()) as MemberUser | GuestUser, + ); + +export const getProfile: GetProfile = async (init) => { + const { fetch, ...rest } = setDefaults(init ?? {}); + + const resResult = await fetch(getProfile_toRequest(rest)); + return isErr(resResult) + ? resResult + : getProfile_fromResponse(unwrapOk(resResult)); +}; + +getProfile.toRequest = getProfile_toRequest; +getProfile.fromResponse = getProfile_fromResponse; diff --git a/rest/project.ts b/rest/project.ts index fe8b7c5..8e6909a 100644 --- a/rest/project.ts +++ b/rest/project.ts @@ -1,3 +1,10 @@ +import { + isErr, + mapAsyncForResult, + mapErrAsyncForResult, + Result, + unwrapOk, +} from "../deps/option-t.ts"; import type { MemberProject, NotFoundError, @@ -8,8 +15,10 @@ import type { ProjectResponse, } from "../deps/scrapbox-rest.ts"; import { cookie } from "./auth.ts"; -import { makeError } from "./error.ts"; -import { BaseOptions, Result, setDefaults } from "./util.ts"; +import { parseHTTPError } from "./parseHTTPError.ts"; +import { HTTPError, responseIntoResult } from "./responseIntoResult.ts"; +import { AbortError, NetworkError } from "./robustFetch.ts"; +import { BaseOptions, setDefaults } from "./util.ts"; export interface GetProject { /** /api/project/:project の要求を組み立てる @@ -31,14 +40,24 @@ export interface GetProject { fromResponse: (res: Response) => Promise< Result< MemberProject | NotMemberProject, - NotFoundError | NotMemberError | NotLoggedInError + | NotFoundError + | NotMemberError + | NotLoggedInError + | NetworkError + | AbortError + | HTTPError > >; (project: string, options?: BaseOptions): Promise< Result< MemberProject | NotMemberProject, - NotFoundError | NotMemberError | NotLoggedInError + | NotFoundError + | NotMemberError + | NotLoggedInError + | NetworkError + | AbortError + | HTTPError > >; } @@ -52,14 +71,19 @@ const getProject_toRequest: GetProject["toRequest"] = (project, init) => { ); }; -const getProject_fromResponse: GetProject["fromResponse"] = async (res) => { - if (!res.ok) { - return makeError(res); - } - - const value = (await res.json()) as MemberProject | NotMemberProject; - return { ok: true, value }; -}; +const getProject_fromResponse: GetProject["fromResponse"] = async (res) => + mapAsyncForResult( + await mapErrAsyncForResult( + responseIntoResult(res), + async (error) => + (await parseHTTPError(error, [ + "NotFoundError", + "NotLoggedInError", + "NotMemberError", + ])) ?? error, + ), + (res) => res.json() as Promise, + ); /** get the project information * @@ -74,8 +98,9 @@ export const getProject: GetProject = async ( const req = getProject_toRequest(project, init); const res = await fetch(req); + if (isErr(res)) return res; - return getProject_fromResponse(res); + return getProject_fromResponse(unwrapOk(res)); }; getProject.toRequest = getProject_toRequest; @@ -100,12 +125,22 @@ export interface ListProjects { */ fromResponse: ( res: Response, - ) => Promise>; + ) => Promise< + Result< + ProjectResponse, + NotLoggedInError | NetworkError | AbortError | HTTPError + > + >; ( projectIds: ProjectId[], init?: BaseOptions, - ): Promise>; + ): Promise< + Result< + ProjectResponse, + NotLoggedInError | NetworkError | AbortError | HTTPError + > + >; } const ListProject_toRequest: ListProjects["toRequest"] = (projectIds, init) => { @@ -121,14 +156,15 @@ const ListProject_toRequest: ListProjects["toRequest"] = (projectIds, init) => { ); }; -const ListProject_fromResponse: ListProjects["fromResponse"] = async (res) => { - if (!res.ok) { - return makeError(res); - } - - const value = (await res.json()) as ProjectResponse; - return { ok: true, value }; -}; +const ListProject_fromResponse: ListProjects["fromResponse"] = async (res) => + mapAsyncForResult( + await mapErrAsyncForResult( + responseIntoResult(res), + async (error) => + (await parseHTTPError(error, ["NotLoggedInError"])) ?? error, + ), + (res) => res.json() as Promise, + ); /** list the projects' information * @@ -142,8 +178,9 @@ export const listProjects: ListProjects = async ( const { fetch } = setDefaults(init ?? {}); const res = await fetch(ListProject_toRequest(projectIds, init)); + if (isErr(res)) return res; - return ListProject_fromResponse(res); + return ListProject_fromResponse(unwrapOk(res)); }; listProjects.toRequest = ListProject_toRequest; diff --git a/rest/replaceLinks.ts b/rest/replaceLinks.ts index 630a9fc..41e2fa5 100644 --- a/rest/replaceLinks.ts +++ b/rest/replaceLinks.ts @@ -1,11 +1,22 @@ +import { + isErr, + mapAsyncForResult, + mapErrAsyncForResult, + orElseAsyncForResult, + Result, + toResultOkFromMaybe, + unwrapOk, +} from "../deps/option-t.ts"; import type { NotFoundError, NotLoggedInError, NotMemberError, } from "../deps/scrapbox-rest.ts"; import { cookie, getCSRFToken } from "./auth.ts"; -import { makeError } from "./error.ts"; -import { ExtendedOptions, Result, setDefaults } from "./util.ts"; +import { parseHTTPError } from "./parseHTTPError.ts"; +import { HTTPError, responseIntoResult } from "./responseIntoResult.ts"; +import { AbortError, NetworkError } from "./robustFetch.ts"; +import { ExtendedOptions, setDefaults } from "./util.ts"; /** 指定したproject内の全てのリンクを書き換える * @@ -26,31 +37,52 @@ export const replaceLinks = async ( ): Promise< Result< number, - NotFoundError | NotLoggedInError | NotMemberError + | NotFoundError + | NotLoggedInError + | NotMemberError + | NetworkError + | AbortError + | HTTPError > > => { const { sid, hostName, fetch, csrf } = setDefaults(init ?? {}); + const csrfResult = await orElseAsyncForResult( + toResultOkFromMaybe(csrf), + () => getCSRFToken(init), + ); + if (isErr(csrfResult)) return csrfResult; + const req = new Request( `https://${hostName}/api/pages/${project}/replace/links`, { method: "POST", headers: { "Content-Type": "application/json;charset=utf-8", - "X-CSRF-TOKEN": csrf ?? await getCSRFToken(init), + "X-CSRF-TOKEN": unwrapOk(csrfResult), ...(sid ? { Cookie: cookie(sid) } : {}), }, body: JSON.stringify({ from, to }), }, ); - const res = await fetch(req); - - if (!res.ok) { - return makeError(res); - } + const resResult = await fetch(req); + if (isErr(resResult)) return resResult; - // messageには"2 pages have been successfully updated!"というような文字列が入っているはず - const { message } = (await res.json()) as { message: string }; - return { ok: true, value: parseInt(message.match(/\d+/)?.[0] ?? "0") }; + return mapAsyncForResult( + await mapErrAsyncForResult( + responseIntoResult(unwrapOk(resResult)), + async (error) => + (await parseHTTPError(error, [ + "NotFoundError", + "NotLoggedInError", + "NotMemberError", + ])) ?? error, + ), + async (res) => { + // messageには"2 pages have been successfully updated!"というような文字列が入っているはず + const { message } = (await res.json()) as { message: string }; + return parseInt(message.match(/\d+/)?.[0] ?? "0"); + }, + ); }; diff --git a/rest/responseIntoResult.ts b/rest/responseIntoResult.ts new file mode 100644 index 0000000..f6f1ef0 --- /dev/null +++ b/rest/responseIntoResult.ts @@ -0,0 +1,18 @@ +import { createErr, createOk, Result } from "../deps/option-t.ts"; + +export interface HTTPError { + name: "HTTPError"; + message: string; + response: Response; +} + +export const responseIntoResult = ( + response: Response, +): Result => + !response.ok + ? createErr({ + name: "HTTPError", + message: `${response.status} ${response.statusText}`, + response, + }) + : createOk(response); diff --git a/rest/robustFetch.ts b/rest/robustFetch.ts new file mode 100644 index 0000000..6011e59 --- /dev/null +++ b/rest/robustFetch.ts @@ -0,0 +1,48 @@ +import { createErr, createOk, Result } from "../deps/option-t.ts"; + +export interface NetworkError { + name: "NetworkError"; + message: string; + request: Request; +} + +export interface AbortError { + name: "AbortError"; + message: string; + request: Request; +} + +export type RobustFetch = ( + input: RequestInfo | URL, + init?: RequestInit, +) => Promise>; + +/** + * Performs a network request using the Fetch API. + * + * @param input - The resource URL or a `Request` object. + * @param init - An optional object containing request options. + * @returns A promise that resolves to a `Result` object containing either a `Response` or an error. + */ +export const robustFetch: RobustFetch = async (input, init) => { + const request = new Request(input, init); + try { + return createOk(await globalThis.fetch(request)); + } catch (e: unknown) { + if (e instanceof DOMException && e.name === "AbortError") { + return createErr({ + name: "AbortError", + message: e.message, + request, + }); + } + if (e instanceof TypeError) { + return createErr({ + name: "NetworkError", + message: e.message, + request, + }); + } + throw e; + } +}; diff --git a/rest/search.ts b/rest/search.ts index 8884110..c34baf8 100644 --- a/rest/search.ts +++ b/rest/search.ts @@ -1,3 +1,10 @@ +import { + isErr, + mapAsyncForResult, + mapErrAsyncForResult, + Result, + unwrapOk, +} from "../deps/option-t.ts"; import type { NoQueryError, NotFoundError, @@ -7,8 +14,10 @@ import type { SearchResult, } from "../deps/scrapbox-rest.ts"; import { cookie } from "./auth.ts"; -import { makeError } from "./error.ts"; -import { BaseOptions, Result, setDefaults } from "./util.ts"; +import { parseHTTPError } from "./parseHTTPError.ts"; +import { HTTPError, responseIntoResult } from "./responseIntoResult.ts"; +import { AbortError, NetworkError } from "./robustFetch.ts"; +import { BaseOptions, setDefaults } from "./util.ts"; /** search a project for pages * @@ -23,7 +32,13 @@ export const searchForPages = async ( ): Promise< Result< SearchResult, - NotFoundError | NotMemberError | NotLoggedInError | NoQueryError + | NotFoundError + | NotMemberError + | NotLoggedInError + | NoQueryError + | NetworkError + | AbortError + | HTTPError > > => { const { sid, hostName, fetch } = setDefaults(init ?? {}); @@ -36,22 +51,21 @@ export const searchForPages = async ( ); const res = await fetch(req); - - if (!res.ok) { - if (res.status === 422) { - return { - ok: false, - value: { - name: "NoQueryError", - message: (await res.json()).message, - }, - }; - } - return makeError(res); - } - - const value = (await res.json()) as SearchResult; - return { ok: true, value }; + if (isErr(res)) return res; + + return mapAsyncForResult( + await mapErrAsyncForResult( + responseIntoResult(unwrapOk(res)), + async (error) => + (await parseHTTPError(error, [ + "NotFoundError", + "NotLoggedInError", + "NotMemberError", + "NoQueryError", + ])) ?? error, + ), + (res) => res.json() as Promise, + ); }; /** search for joined projects @@ -65,7 +79,7 @@ export const searchForJoinedProjects = async ( ): Promise< Result< ProjectSearchResult, - NotLoggedInError | NoQueryError + NotLoggedInError | NoQueryError | NetworkError | AbortError | HTTPError > > => { const { sid, hostName, fetch } = setDefaults(init ?? {}); @@ -78,22 +92,19 @@ export const searchForJoinedProjects = async ( ); const res = await fetch(req); - - if (!res.ok) { - if (res.status === 422) { - return { - ok: false, - value: { - name: "NoQueryError", - message: (await res.json()).message, - }, - }; - } - return makeError(res); - } - - const value = (await res.json()) as ProjectSearchResult; - return { ok: true, value }; + if (isErr(res)) return res; + + return mapAsyncForResult( + await mapErrAsyncForResult( + responseIntoResult(unwrapOk(res)), + async (error) => + (await parseHTTPError(error, [ + "NotLoggedInError", + "NoQueryError", + ])) ?? error, + ), + (res) => res.json() as Promise, + ); }; /** search for watch list @@ -113,7 +124,7 @@ export const searchForWatchList = async ( ): Promise< Result< ProjectSearchResult, - NotLoggedInError | NoQueryError + NotLoggedInError | NoQueryError | NetworkError | AbortError | HTTPError > > => { const { sid, hostName, fetch } = setDefaults(init ?? {}); @@ -130,21 +141,17 @@ export const searchForWatchList = async ( ); const res = await fetch(req); - - if (!res.ok) { - if (res.status === 422) { - return { - ok: false, - value: { - // 本当はproject idが不正なときも422 errorになる - name: "NoQueryError", - message: (await res.json()).message, - }, - }; - } - return makeError(res); - } - - const value = (await res.json()) as ProjectSearchResult; - return { ok: true, value }; + if (isErr(res)) return res; + + return mapAsyncForResult( + await mapErrAsyncForResult( + responseIntoResult(unwrapOk(res)), + async (error) => + (await parseHTTPError(error, [ + "NotLoggedInError", + "NoQueryError", + ])) ?? error, + ), + (res) => res.json() as Promise, + ); }; diff --git a/rest/snapshot.ts b/rest/snapshot.ts index 85d9973..b1c86ac 100644 --- a/rest/snapshot.ts +++ b/rest/snapshot.ts @@ -7,8 +7,17 @@ import type { PageSnapshotResult, } from "../deps/scrapbox-rest.ts"; import { cookie } from "./auth.ts"; -import { BaseOptions, Result, setDefaults } from "./util.ts"; -import { makeError } from "./error.ts"; +import { BaseOptions, setDefaults } from "./util.ts"; +import { parseHTTPError } from "./parseHTTPError.ts"; +import { + isErr, + mapAsyncForResult, + mapErrAsyncForResult, + Result, + unwrapOk, +} from "../deps/option-t.ts"; +import { AbortError, NetworkError } from "./robustFetch.ts"; +import { HTTPError, responseIntoResult } from "./responseIntoResult.ts"; /** 不正な`timestampId`を渡されたときに発生するエラー */ export interface InvalidPageSnapshotIdError extends ErrorLike { @@ -31,6 +40,9 @@ export const getSnapshot = async ( | NotLoggedInError | NotMemberError | InvalidPageSnapshotIdError + | NetworkError + | AbortError + | HTTPError > > => { const { sid, hostName, fetch } = setDefaults(options ?? {}); @@ -41,22 +53,25 @@ export const getSnapshot = async ( ); const res = await fetch(req); + if (isErr(res)) return res; - if (!res.ok) { - if (res.status === 422) { - return { - ok: false, - value: { - name: "InvalidPageSnapshotIdError", - message: await res.text(), - }, - }; - } - return makeError(res); - } - - const value = (await res.json()) as PageSnapshotResult; - return { ok: true, value }; + return mapAsyncForResult( + await mapErrAsyncForResult( + responseIntoResult(unwrapOk(res)), + async (error) => + error.response.status === 422 + ? { + name: "InvalidPageSnapshotIdError", + message: await error.response.text(), + } + : (await parseHTTPError(error, [ + "NotFoundError", + "NotLoggedInError", + "NotMemberError", + ])) ?? error, + ), + (res) => res.json() as Promise, + ); }; /** @@ -73,7 +88,15 @@ export const getTimestampIds = async ( pageId: string, options?: BaseOptions, ): Promise< - Result + Result< + PageSnapshotList, + | NotFoundError + | NotLoggedInError + | NotMemberError + | NetworkError + | AbortError + | HTTPError + > > => { const { sid, hostName, fetch } = setDefaults(options ?? {}); @@ -83,11 +106,18 @@ export const getTimestampIds = async ( ); const res = await fetch(req); + if (isErr(res)) return res; - if (!res.ok) { - return makeError(res); - } - - const value = (await res.json()) as PageSnapshotList; - return { ok: true, value }; + return mapAsyncForResult( + await mapErrAsyncForResult( + responseIntoResult(unwrapOk(res)), + async (error) => + (await parseHTTPError(error, [ + "NotFoundError", + "NotLoggedInError", + "NotMemberError", + ])) ?? error, + ), + (res) => res.json() as Promise, + ); }; diff --git a/rest/table.ts b/rest/table.ts index d60b10b..4ba4602 100644 --- a/rest/table.ts +++ b/rest/table.ts @@ -4,9 +4,18 @@ import type { NotMemberError, } from "../deps/scrapbox-rest.ts"; import { cookie } from "./auth.ts"; -import { makeError } from "./error.ts"; import { encodeTitleURI } from "../title.ts"; -import { BaseOptions, Result, setDefaults } from "./util.ts"; +import { BaseOptions, setDefaults } from "./util.ts"; +import { + isErr, + mapAsyncForResult, + mapErrAsyncForResult, + Result, + unwrapOk, +} from "../deps/option-t.ts"; +import { HTTPError, responseIntoResult } from "./responseIntoResult.ts"; +import { parseHTTPError } from "./parseHTTPError.ts"; +import { AbortError, NetworkError } from "./robustFetch.ts"; const getTable_toRequest: GetTable["toRequest"] = ( project, @@ -25,22 +34,24 @@ const getTable_toRequest: GetTable["toRequest"] = ( ); }; -const getTable_fromResponse: GetTable["fromResponse"] = async (res) => { - if (!res.ok) { - if (res.status === 404) { - // responseが空文字の時があるので、自前で組み立てる - return { - ok: false, - value: { - name: "NotFoundError", - message: "Table not found.", - }, - }; - } - return makeError(res); - } - return { ok: true, value: await res.text() }; -}; +const getTable_fromResponse: GetTable["fromResponse"] = async (res) => + mapAsyncForResult( + await mapErrAsyncForResult( + responseIntoResult(res), + async (error) => + error.response.status === 404 + ? { + // responseが空文字の時があるので、自前で組み立てる + name: "NotFoundError", + message: "Table not found.", + } + : (await parseHTTPError(error, [ + "NotLoggedInError", + "NotMemberError", + ])) ?? error, + ), + (res) => res.text(), + ); export interface GetTable { /** /api/table/:project/:title/:filename.csv の要求を組み立てる @@ -66,7 +77,12 @@ export interface GetTable { fromResponse: (res: Response) => Promise< Result< string, - NotFoundError | NotLoggedInError | NotMemberError + | NotFoundError + | NotLoggedInError + | NotMemberError + | NetworkError + | AbortError + | HTTPError > >; @@ -78,7 +94,12 @@ export interface GetTable { ): Promise< Result< string, - NotFoundError | NotLoggedInError | NotMemberError + | NotFoundError + | NotLoggedInError + | NotMemberError + | NetworkError + | AbortError + | HTTPError > >; } @@ -99,7 +120,8 @@ export const getTable: GetTable = async ( const { fetch } = setDefaults(options ?? {}); const req = getTable_toRequest(project, title, filename, options); const res = await fetch(req); - return await getTable_fromResponse(res); + if (isErr(res)) return res; + return await getTable_fromResponse(unwrapOk(res)); }; getTable.toRequest = getTable_toRequest; diff --git a/rest/uploadToGCS.ts b/rest/uploadToGCS.ts index 05642a0..c5a6608 100644 --- a/rest/uploadToGCS.ts +++ b/rest/uploadToGCS.ts @@ -1,8 +1,20 @@ import { cookie, getCSRFToken } from "./auth.ts"; -import { BaseOptions, ExtendedOptions, Result, setDefaults } from "./util.ts"; -import { makeError, UnexpectedResponseError } from "./error.ts"; +import { BaseOptions, ExtendedOptions, setDefaults } from "./util.ts"; import type { ErrorLike, NotFoundError } from "../deps/scrapbox-rest.ts"; import { Md5 } from "../deps/hash.ts"; +import { + createOk, + isErr, + mapAsyncForResult, + mapErrAsyncForResult, + mapForResult, + orElseAsyncForResult, + Result, + toResultOkFromMaybe, + unwrapOk, +} from "../deps/option-t.ts"; +import { AbortError, NetworkError } from "./robustFetch.ts"; +import { HTTPError, responseIntoResult } from "./responseIntoResult.ts"; /** uploadしたファイルのメタデータ */ export interface GCSFile { @@ -24,15 +36,24 @@ export const uploadToGCS = async ( projectId: string, options?: ExtendedOptions, ): Promise< - Result + Result< + GCSFile, + | GCSError + | NotFoundError + | FileCapacityError + | NetworkError + | AbortError + | HTTPError + > > => { - const md5 = new Md5().update(await file.arrayBuffer()).toString(); + const md5 = `${new Md5().update(await file.arrayBuffer())}`; const res = await uploadRequest(file, projectId, md5, options); - if (!res.ok) return res; - if ("embedUrl" in res.value) return { ok: true, value: res.value }; - const res2 = await upload(res.value.signedUrl, file, options); - if (!res2.ok) return res2; - return verify(projectId, res.value.fileId, md5, options); + if (isErr(res)) return res; + const fileOrRequest = unwrapOk(res); + if ("embedUrl" in fileOrRequest) return createOk(fileOrRequest); + const result = await upload(fileOrRequest.signedUrl, file, options); + if (isErr(result)) return result; + return verify(projectId, fileOrRequest.fileId, md5, options); }; /** 容量を使い切ったときに発生するerror */ @@ -60,7 +81,12 @@ const uploadRequest = async ( projectId: string, md5: string, init?: ExtendedOptions, -): Promise> => { +): Promise< + Result< + GCSFile | UploadRequest, + FileCapacityError | NetworkError | AbortError | HTTPError + > +> => { const { sid, hostName, fetch, csrf } = setDefaults(init ?? {}); const body = { md5, @@ -68,7 +94,11 @@ const uploadRequest = async ( contentType: file.type, name: file.name, }; - const token = csrf ?? await getCSRFToken(); + const csrfResult = await orElseAsyncForResult( + toResultOkFromMaybe(csrf), + () => getCSRFToken(init), + ); + if (isErr(csrfResult)) return csrfResult; const req = new Request( `https://${hostName}/api/gcs/${projectId}/upload-request`, { @@ -76,16 +106,27 @@ const uploadRequest = async ( body: JSON.stringify(body), headers: { "Content-Type": "application/json;charset=utf-8", - "X-CSRF-TOKEN": token, + "X-CSRF-TOKEN": unwrapOk(csrfResult), ...(sid ? { Cookie: cookie(sid) } : {}), }, }, ); const res = await fetch(req); - if (!res.ok) { - return makeError(res); - } - return { ok: true, value: await res.json() }; + if (isErr(res)) return res; + + return mapAsyncForResult( + await mapErrAsyncForResult( + responseIntoResult(unwrapOk(res)), + async (error) => + error.response.status === 402 + ? { + name: "FileCapacityError", + message: (await error.response.json()).message, + } as FileCapacityError + : error, + ), + (res) => res.json(), + ); }; /** Google Cloud Storage XML APIのerror @@ -101,7 +142,9 @@ const upload = async ( signedUrl: string, file: File, init?: BaseOptions, -): Promise> => { +): Promise< + Result +> => { const { sid, fetch } = setDefaults(init ?? {}); const res = await fetch( signedUrl, @@ -114,19 +157,21 @@ const upload = async ( }, }, ); - if (!res.ok) { - if (res.headers.get("Content-Type")?.includes?.("/xml")) { - return { - ok: false, - value: { - name: "GCSError", - message: await res.text(), - }, - }; - } - throw new UnexpectedResponseError(res); - } - return { ok: true, value: undefined }; + if (isErr(res)) return res; + + return mapForResult( + await mapErrAsyncForResult( + responseIntoResult(unwrapOk(res)), + async (error) => + error.response.headers.get("Content-Type")?.includes?.("/xml") + ? { + name: "GCSError", + message: await error.response.text(), + } as GCSError + : error, + ), + () => undefined, + ); }; /** uploadしたファイルの整合性を確認する */ @@ -135,9 +180,15 @@ const verify = async ( fileId: string, md5: string, init?: ExtendedOptions, -): Promise> => { +): Promise< + Result +> => { const { sid, hostName, fetch, csrf } = setDefaults(init ?? {}); - const token = csrf ?? await getCSRFToken(); + const csrfResult = await orElseAsyncForResult( + toResultOkFromMaybe(csrf), + () => getCSRFToken(init), + ); + if (isErr(csrfResult)) return csrfResult; const req = new Request( `https://${hostName}/api/gcs/${projectId}/verify`, { @@ -145,25 +196,26 @@ const verify = async ( body: JSON.stringify({ md5, fileId }), headers: { "Content-Type": "application/json;charset=utf-8", - "X-CSRF-TOKEN": token, + "X-CSRF-TOKEN": unwrapOk(csrfResult), ...(sid ? { Cookie: cookie(sid) } : {}), }, }, ); + const res = await fetch(req); - if (!res.ok) { - try { - if (res.status === 404) { - return { - ok: false, - value: { name: "NotFoundError", message: (await res.json()).message }, - }; - } - } catch (_) { - throw new UnexpectedResponseError(res); - } - throw new UnexpectedResponseError(res); - } - const gcs = await res.json(); - return { ok: true, value: gcs }; + if (isErr(res)) return res; + + return mapAsyncForResult( + await mapErrAsyncForResult( + responseIntoResult(unwrapOk(res)), + async (error) => + error.response.status === 404 + ? { + name: "NotFoundError", + message: (await error.response.json()).message, + } as NotFoundError + : error, + ), + (res) => res.json(), + ); }; diff --git a/rest/util.ts b/rest/util.ts index 2cd2a2a..c2262dc 100644 --- a/rest/util.ts +++ b/rest/util.ts @@ -1,14 +1,4 @@ -/** 正常値と異常値を格納する型 */ -export type Result = { ok: true; value: T } | { ok: false; value: E }; - -/** networkからdataをとってくる処理 - * - * interfaceは`fetch()`と同じ - */ -export type Fetch = ( - input: string | Request, - init?: RequestInit, -) => Promise; +import { RobustFetch, robustFetch } from "./robustFetch.ts"; /** 全てのREST APIに共通するopitons */ export interface BaseOptions { @@ -22,7 +12,7 @@ export interface BaseOptions { * * @default fetch */ - fetch?: Fetch; + fetch?: RobustFetch; /** REST APIのdomain * @@ -45,7 +35,6 @@ export interface ExtendedOptions extends BaseOptions { export const setDefaults = ( options: T, ): Omit & Required> => { - const { fetch = globalThis.fetch, hostName = "scrapbox.io", ...rest } = - options; + const { fetch = robustFetch, hostName = "scrapbox.io", ...rest } = options; return { fetch, hostName, ...rest }; }; diff --git a/text.ts b/text.ts index 450109c..2ffc2ec 100644 --- a/text.ts +++ b/text.ts @@ -1,4 +1,4 @@ -import { isString } from "./is.ts"; +import { isString } from "./deps/unknownutil.ts"; /** インデント数を数える */ export const getIndentCount = (text: string): number => From 2656284d2db710501517f009ffc3a9e2109a7b9b Mon Sep 17 00:00:00 2001 From: takker99 <37929109+takker99@users.noreply.github.com> Date: Fri, 2 Aug 2024 16:02:19 +0900 Subject: [PATCH 2/5] feat: export `./title` --- deno.jsonc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/deno.jsonc b/deno.jsonc index d8072c0..973c31c 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -28,6 +28,7 @@ "./browser": "./browser/mod.ts", "./browser/dom": "./browser/dom/mod.ts", "./browser/websocket": "./browser/websocket/mod.ts", + "./title": "./title.ts", "./text": "./text.ts" }, "compilerOptions": { @@ -43,4 +44,4 @@ "vendor/" ] } -} +} \ No newline at end of file From f5c67b091a66fadbbe731d5f627a1ddbd9e4bda9 Mon Sep 17 00:00:00 2001 From: takker99 <37929109+takker99@users.noreply.github.com> Date: Fri, 2 Aug 2024 16:23:18 +0900 Subject: [PATCH 3/5] refactor: Delete as many deps/*.ts as possible --- browser/dom/_internal.test.ts | 2 +- browser/dom/cursor.d.ts | 2 +- browser/dom/edit.ts | 2 +- browser/dom/extractCodeFiles.test.ts | 4 +-- browser/dom/extractCodeFiles.ts | 2 +- browser/dom/getCachedLines.ts | 2 +- browser/dom/node.ts | 4 +-- browser/dom/open.ts | 2 +- browser/dom/page.d.ts | 4 +-- browser/dom/selection.d.ts | 2 +- browser/dom/takeInternalLines.ts | 2 +- browser/dom/textInputEventListener.ts | 2 +- browser/websocket/_codeBlock.test.ts | 3 +- browser/websocket/applyCommit.ts | 3 +- browser/websocket/deletePage.ts | 2 +- browser/websocket/diffToChanges.test.ts | 2 +- browser/websocket/diffToChanges.ts | 2 +- browser/websocket/findMetadata.test.ts | 3 +- browser/websocket/findMetadata.ts | 3 +- browser/websocket/isSameArray.test.ts | 2 +- browser/websocket/isSimpleCodeFile.test.ts | 2 +- browser/websocket/listen.ts | 4 +-- browser/websocket/makeChanges.ts | 2 +- browser/websocket/patch.ts | 5 +-- browser/websocket/pin.ts | 2 +- browser/websocket/pull.ts | 4 +-- browser/websocket/push.ts | 4 +-- browser/websocket/suggestUnDupTitle.test.ts | 2 +- browser/websocket/updateCodeBlock.ts | 6 ++-- browser/websocket/updateCodeFile.ts | 5 +-- deno.jsonc | 2 +- deps/hash.ts | 1 - deps/option-t.ts | 2 -- deps/scrapbox-rest.ts | 29 ---------------- deps/scrapbox.ts | 7 ---- deps/testing.ts | 2 -- deps/unknownutil.ts | 1 - parseAbsoluteLink.ts | 2 +- parser/anchor-fm.test.ts | 2 +- parser/spotify.test.ts | 2 +- parser/vimeo.test.ts | 38 --------------------- parser/youtube.test.ts | 2 +- range.test.ts | 2 +- rest/auth.ts | 2 +- rest/getCodeBlock.ts | 4 +-- rest/getCodeBlocks.test.ts | 6 ++-- rest/getCodeBlocks.ts | 3 +- rest/getGyazoToken.ts | 4 +-- rest/getTweetInfo.ts | 6 ++-- rest/getWebPageTitle.ts | 6 ++-- rest/link.ts | 4 +-- rest/page-data.ts | 6 ++-- rest/pages.test.ts | 2 +- rest/pages.ts | 6 ++-- rest/parseHTTPError.ts | 6 ++-- rest/profile.ts | 4 +-- rest/project.test.ts | 2 +- rest/project.ts | 4 +-- rest/replaceLinks.ts | 6 ++-- rest/responseIntoResult.ts | 2 +- rest/robustFetch.ts | 2 +- rest/search.ts | 4 +-- rest/snapshot.ts | 4 +-- rest/table.ts | 4 +-- rest/uploadToGCS.ts | 8 ++--- text.test.ts | 2 +- text.ts | 2 +- title.test.ts | 2 +- 68 files changed, 105 insertions(+), 174 deletions(-) delete mode 100644 deps/hash.ts delete mode 100644 deps/option-t.ts delete mode 100644 deps/scrapbox-rest.ts delete mode 100644 deps/scrapbox.ts delete mode 100644 deps/testing.ts delete mode 100644 deps/unknownutil.ts delete mode 100644 parser/vimeo.test.ts diff --git a/browser/dom/_internal.test.ts b/browser/dom/_internal.test.ts index 39bbd01..20ef6b0 100644 --- a/browser/dom/_internal.test.ts +++ b/browser/dom/_internal.test.ts @@ -1,4 +1,4 @@ -import { assertEquals } from "../../deps/testing.ts"; +import { assertEquals } from "@std/assert"; import { decode, encode } from "./_internal.ts"; Deno.test("encode()", async (t) => { diff --git a/browser/dom/cursor.d.ts b/browser/dom/cursor.d.ts index 1485d88..fee9007 100644 --- a/browser/dom/cursor.d.ts +++ b/browser/dom/cursor.d.ts @@ -1,4 +1,4 @@ -import { type BaseLine, BaseStore } from "../../deps/scrapbox.ts"; +import { type BaseLine, BaseStore } from "@cosense/types/userscript"; import type { Position } from "./position.ts"; import type { Page } from "./page.d.ts"; diff --git a/browser/dom/edit.ts b/browser/dom/edit.ts index 127914b..069475b 100644 --- a/browser/dom/edit.ts +++ b/browser/dom/edit.ts @@ -3,7 +3,7 @@ import { press } from "./press.ts"; import { getLineCount } from "./node.ts"; import { range } from "../../range.ts"; import { textInput } from "./dom.ts"; -import { isArray, isNumber, isString } from "../../deps/unknownutil.ts"; +import { isArray, isNumber, isString } from "@core/unknownutil"; import { delay } from "@std/async/delay"; export const undo = (count = 1): void => { diff --git a/browser/dom/extractCodeFiles.test.ts b/browser/dom/extractCodeFiles.test.ts index c40df35..f2ead22 100644 --- a/browser/dom/extractCodeFiles.test.ts +++ b/browser/dom/extractCodeFiles.test.ts @@ -1,6 +1,6 @@ import { extractCodeFiles } from "./extractCodeFiles.ts"; -import type { Line } from "../../deps/scrapbox.ts"; -import { assertSnapshot } from "../../deps/testing.ts"; +import type { Line } from "@cosense/types/userscript"; +import { assertSnapshot } from "@std/testing/snapshot"; import sample from "./sample-lines1.json" with { type: "json" }; Deno.test("extractCodeFiles", async (t) => { diff --git a/browser/dom/extractCodeFiles.ts b/browser/dom/extractCodeFiles.ts index c425ad0..f5e1f11 100644 --- a/browser/dom/extractCodeFiles.ts +++ b/browser/dom/extractCodeFiles.ts @@ -1,4 +1,4 @@ -import type { Line } from "../../deps/scrapbox.ts"; +import type { Line } from "@cosense/types/userscript"; /** 一つのソースコードを表す */ export interface CodeFile { diff --git a/browser/dom/getCachedLines.ts b/browser/dom/getCachedLines.ts index edf7a80..2f5f2e0 100644 --- a/browser/dom/getCachedLines.ts +++ b/browser/dom/getCachedLines.ts @@ -1,4 +1,4 @@ -import type { Line, Scrapbox } from "../../deps/scrapbox.ts"; +import type { Line, Scrapbox } from "@cosense/types/userscript"; declare const scrapbox: Scrapbox; let isLatestData = false; diff --git a/browser/dom/node.ts b/browser/dom/node.ts index afd2bea..a46567a 100644 --- a/browser/dom/node.ts +++ b/browser/dom/node.ts @@ -1,8 +1,8 @@ -import { isNumber, isString, isUndefined } from "../../deps/unknownutil.ts"; +import { isNumber, isString, isUndefined } from "@core/unknownutil"; import { ensureArray } from "../../ensure.ts"; import { getCachedLines } from "./getCachedLines.ts"; import { takeInternalLines } from "./takeInternalLines.ts"; -import type { BaseLine, Line } from "../../deps/scrapbox.ts"; +import type { BaseLine, Line } from "@cosense/types/userscript"; import { lines } from "./dom.ts"; import * as Text from "../../text.ts"; diff --git a/browser/dom/open.ts b/browser/dom/open.ts index b99f1b4..0e7ed7d 100644 --- a/browser/dom/open.ts +++ b/browser/dom/open.ts @@ -3,7 +3,7 @@ import { type PageTransitionContext, pushPageTransition, } from "./pushPageTransition.ts"; -import type { Scrapbox } from "../../deps/scrapbox.ts"; +import type { Scrapbox } from "@cosense/types/userscript"; declare const scrapbox: Scrapbox; export interface OpenOptions { diff --git a/browser/dom/page.d.ts b/browser/dom/page.d.ts index 1f49b76..10fe41f 100644 --- a/browser/dom/page.d.ts +++ b/browser/dom/page.d.ts @@ -1,5 +1,5 @@ -import { BaseStore } from "../../deps/scrapbox.ts"; -import type { Page as PageData } from "../../deps/scrapbox-rest.ts"; +import { BaseStore } from "@cosense/types/userscript"; +import type { Page as PageData } from "@cosense/types/rest"; export interface SetPositionOptions { /** カーソルが画面外に移動したとき、カーソルが見える位置までページをスクロールするかどうか diff --git a/browser/dom/selection.d.ts b/browser/dom/selection.d.ts index b7d2616..a8b6896 100644 --- a/browser/dom/selection.d.ts +++ b/browser/dom/selection.d.ts @@ -1,4 +1,4 @@ -import { type BaseLine, BaseStore } from "../../deps/scrapbox.ts"; +import { type BaseLine, BaseStore } from "@cosense/types/userscript"; import type { Position } from "./position.ts"; export interface Range { diff --git a/browser/dom/takeInternalLines.ts b/browser/dom/takeInternalLines.ts index 666eb56..5877653 100644 --- a/browser/dom/takeInternalLines.ts +++ b/browser/dom/takeInternalLines.ts @@ -1,5 +1,5 @@ import { lines } from "./dom.ts"; -import type { BaseLine } from "../../deps/scrapbox.ts"; +import type { BaseLine } from "@cosense/types/userscript"; /** Scrapbox内部の本文データの参照を取得する * diff --git a/browser/dom/textInputEventListener.ts b/browser/dom/textInputEventListener.ts index dc2346a..4bb05ba 100644 --- a/browser/dom/textInputEventListener.ts +++ b/browser/dom/textInputEventListener.ts @@ -1,4 +1,4 @@ -import type { Scrapbox } from "../../deps/scrapbox.ts"; +import type { Scrapbox } from "@cosense/types/userscript"; import { textInput } from "./dom.ts"; import { decode, encode } from "./_internal.ts"; declare const scrapbox: Scrapbox; diff --git a/browser/websocket/_codeBlock.test.ts b/browser/websocket/_codeBlock.test.ts index 69ebd49..12fd9d6 100644 --- a/browser/websocket/_codeBlock.test.ts +++ b/browser/websocket/_codeBlock.test.ts @@ -1,4 +1,5 @@ -import { assertEquals, assertSnapshot } from "../../deps/testing.ts"; +import { assertEquals } from "@std/assert"; +import { assertSnapshot } from "@std/testing/snapshot"; import { extractFromCodeTitle } from "./_codeBlock.ts"; Deno.test("extractFromCodeTitle()", async (t) => { diff --git a/browser/websocket/applyCommit.ts b/browser/websocket/applyCommit.ts index 11fcb02..0a1db24 100644 --- a/browser/websocket/applyCommit.ts +++ b/browser/websocket/applyCommit.ts @@ -1,6 +1,7 @@ import type { CommitNotification } from "../../deps/socket.ts"; -import type { Line } from "../../deps/scrapbox-rest.ts"; +import type { Page } from "@cosense/types/rest"; import { getUnixTimeFromId } from "./id.ts"; +type Line = Page["lines"][number]; export interface ApplyCommitProp { /** changesの作成日時 diff --git a/browser/websocket/deletePage.ts b/browser/websocket/deletePage.ts index 65acc53..94de937 100644 --- a/browser/websocket/deletePage.ts +++ b/browser/websocket/deletePage.ts @@ -1,5 +1,5 @@ import { push, type PushError, type PushOptions } from "./push.ts"; -import type { Result } from "../../deps/option-t.ts"; +import type { Result } from "option-t/plain_result"; export type DeletePageOptions = PushOptions; diff --git a/browser/websocket/diffToChanges.test.ts b/browser/websocket/diffToChanges.test.ts index 506cb23..3c84153 100644 --- a/browser/websocket/diffToChanges.test.ts +++ b/browser/websocket/diffToChanges.test.ts @@ -1,5 +1,5 @@ import { diffToChanges } from "./diffToChanges.ts"; -import { assertEquals } from "../../deps/testing.ts"; +import { assertEquals } from "@std/assert"; Deno.test("diffToChanges()", async ({ step }) => { const userId = "xxxyyy"; diff --git a/browser/websocket/diffToChanges.ts b/browser/websocket/diffToChanges.ts index c5bd1b2..dc71094 100644 --- a/browser/websocket/diffToChanges.ts +++ b/browser/websocket/diffToChanges.ts @@ -1,5 +1,5 @@ import { diff, toExtendedChanges } from "../../deps/onp.ts"; -import type { Line } from "../../deps/scrapbox.ts"; +import type { Line } from "@cosense/types/userscript"; import type { DeleteChange, InsertChange, diff --git a/browser/websocket/findMetadata.test.ts b/browser/websocket/findMetadata.test.ts index 348c922..6cd12e7 100644 --- a/browser/websocket/findMetadata.test.ts +++ b/browser/websocket/findMetadata.test.ts @@ -1,5 +1,6 @@ import { findMetadata, getHelpfeels } from "./findMetadata.ts"; -import { assertEquals, assertSnapshot } from "../../deps/testing.ts"; +import { assertEquals } from "@std/assert"; +import { assertSnapshot } from "@std/testing/snapshot"; const text = `てすと [ふつうの]リンク diff --git a/browser/websocket/findMetadata.ts b/browser/websocket/findMetadata.ts index 7f13e60..59fe676 100644 --- a/browser/websocket/findMetadata.ts +++ b/browser/websocket/findMetadata.ts @@ -1,4 +1,5 @@ -import { type BaseLine, type Node, parse } from "../../deps/scrapbox.ts"; +import { type Node, parse } from "@progfay/scrapbox-parser"; +import type { BaseLine } from "@cosense/types/userscript"; import { toTitleLc } from "../../title.ts"; import { parseYoutube } from "../../parser/youtube.ts"; diff --git a/browser/websocket/isSameArray.test.ts b/browser/websocket/isSameArray.test.ts index 93ab60a..1a753a0 100644 --- a/browser/websocket/isSameArray.test.ts +++ b/browser/websocket/isSameArray.test.ts @@ -1,5 +1,5 @@ import { isSameArray } from "./isSameArray.ts"; -import { assert } from "../../deps/testing.ts"; +import { assert } from "@std/assert"; Deno.test("isSameArray()", () => { assert(isSameArray([1, 2, 3], [1, 2, 3])); diff --git a/browser/websocket/isSimpleCodeFile.test.ts b/browser/websocket/isSimpleCodeFile.test.ts index acf7973..babff62 100644 --- a/browser/websocket/isSimpleCodeFile.test.ts +++ b/browser/websocket/isSimpleCodeFile.test.ts @@ -1,4 +1,4 @@ -import { assert, assertFalse } from "../../deps/testing.ts"; +import { assert, assertFalse } from "@std/assert"; import { isSimpleCodeFile } from "./isSimpleCodeFile.ts"; import type { SimpleCodeFile } from "./updateCodeFile.ts"; diff --git a/browser/websocket/listen.ts b/browser/websocket/listen.ts index fb5f685..230f5ca 100644 --- a/browser/websocket/listen.ts +++ b/browser/websocket/listen.ts @@ -1,9 +1,9 @@ -import { createOk, isErr, type Result, unwrapOk } from "../../deps/option-t.ts"; +import { createOk, isErr, type Result, unwrapOk } from "option-t/plain_result"; import type { NotFoundError, NotLoggedInError, NotMemberError, -} from "../../deps/scrapbox-rest.ts"; +} from "@cosense/types/rest"; import { type ProjectUpdatesStreamCommit, type ProjectUpdatesStreamEvent, diff --git a/browser/websocket/makeChanges.ts b/browser/websocket/makeChanges.ts index 267ac8f..74581ee 100644 --- a/browser/websocket/makeChanges.ts +++ b/browser/websocket/makeChanges.ts @@ -1,5 +1,5 @@ import { diffToChanges } from "./diffToChanges.ts"; -import type { Page } from "../../deps/scrapbox-rest.ts"; +import type { Page } from "@cosense/types/rest"; import type { Change } from "../../deps/socket.ts"; import { findMetadata, getHelpfeels } from "./findMetadata.ts"; import { isSameArray } from "./isSameArray.ts"; diff --git a/browser/websocket/patch.ts b/browser/websocket/patch.ts index 7ed338d..a8a9532 100644 --- a/browser/websocket/patch.ts +++ b/browser/websocket/patch.ts @@ -1,11 +1,12 @@ import type { Change, DeletePageChange, PinChange } from "../../deps/socket.ts"; import { makeChanges } from "./makeChanges.ts"; -import type { Line, Page } from "../../deps/scrapbox-rest.ts"; +import type { Page } from "@cosense/types/rest"; import { push, type PushError, type PushOptions } from "./push.ts"; import { suggestUnDupTitle } from "./suggestUnDupTitle.ts"; -import type { Result } from "../../deps/option-t.ts"; +import type { Result } from "option-t/plain_result"; export type PatchOptions = PushOptions; +type Line = Page["lines"][number]; export interface PatchMetadata extends Page { /** 書き換えを再試行した回数 diff --git a/browser/websocket/pin.ts b/browser/websocket/pin.ts index 2f0f88c..7557975 100644 --- a/browser/websocket/pin.ts +++ b/browser/websocket/pin.ts @@ -1,4 +1,4 @@ -import type { Result } from "../../deps/option-t.ts"; +import type { Result } from "option-t/plain_result"; import type { Change, Socket } from "../../deps/socket.ts"; import { push, type PushError, type PushOptions } from "./push.ts"; diff --git a/browser/websocket/pull.ts b/browser/websocket/pull.ts index f5e9b5c..3982e2c 100644 --- a/browser/websocket/pull.ts +++ b/browser/websocket/pull.ts @@ -5,13 +5,13 @@ import { mapForResult, type Result, unwrapOk, -} from "../../deps/option-t.ts"; +} from "option-t/plain_result"; import type { NotFoundError, NotLoggedInError, NotMemberError, Page, -} from "../../deps/scrapbox-rest.ts"; +} from "@cosense/types/rest"; import { getPage, type GetPageOption, diff --git a/browser/websocket/push.ts b/browser/websocket/push.ts index 104a4fb..c2a0747 100644 --- a/browser/websocket/push.ts +++ b/browser/websocket/push.ts @@ -19,7 +19,7 @@ import type { NotLoggedInError, NotMemberError, Page, -} from "../../deps/scrapbox-rest.ts"; +} from "@cosense/types/rest"; import { delay } from "@std/async/delay"; import { createErr, @@ -27,7 +27,7 @@ import { isErr, type Result, unwrapOk, -} from "../../deps/option-t.ts"; +} from "option-t/plain_result"; import type { HTTPError } from "../../rest/responseIntoResult.ts"; import type { AbortError, NetworkError } from "../../rest/robustFetch.ts"; import type { TooLongURIError } from "../../rest/pages.ts"; diff --git a/browser/websocket/suggestUnDupTitle.test.ts b/browser/websocket/suggestUnDupTitle.test.ts index b0be4df..5835123 100644 --- a/browser/websocket/suggestUnDupTitle.test.ts +++ b/browser/websocket/suggestUnDupTitle.test.ts @@ -1,5 +1,5 @@ import { suggestUnDupTitle } from "./suggestUnDupTitle.ts"; -import { assertEquals } from "../../deps/testing.ts"; +import { assertEquals } from "@std/assert"; Deno.test("suggestUnDupTitle()", () => { assertEquals(suggestUnDupTitle("title"), "title_2"); diff --git a/browser/websocket/updateCodeBlock.ts b/browser/websocket/updateCodeBlock.ts index 9860065..f0db325 100644 --- a/browser/websocket/updateCodeBlock.ts +++ b/browser/websocket/updateCodeBlock.ts @@ -1,4 +1,4 @@ -import type { Line } from "../../deps/scrapbox-rest.ts"; +import type { Page } from "@cosense/types/rest"; import type { DeleteChange, InsertChange, @@ -10,7 +10,9 @@ import { isSimpleCodeFile } from "./isSimpleCodeFile.ts"; import type { SimpleCodeFile } from "./updateCodeFile.ts"; import { countBodyIndent, extractFromCodeTitle } from "./_codeBlock.ts"; import { push, type PushError, type PushOptions } from "./push.ts"; -import type { Result } from "../../deps/option-t.ts"; +import type { Result } from "option-t/plain_result"; + +type Line = Page["lines"][number]; export interface UpdateCodeBlockOptions extends PushOptions { /** `true`でデバッグ出力ON */ diff --git a/browser/websocket/updateCodeFile.ts b/browser/websocket/updateCodeFile.ts index d5d66b6..4a8f0f8 100644 --- a/browser/websocket/updateCodeFile.ts +++ b/browser/websocket/updateCodeFile.ts @@ -1,4 +1,4 @@ -import type { Line } from "../../deps/scrapbox-rest.ts"; +import type { Page } from "@cosense/types/rest"; import type { DeleteChange, InsertChange, @@ -9,7 +9,8 @@ import { createNewLineId } from "./id.ts"; import { diff, toExtendedChanges } from "../../deps/onp.ts"; import { countBodyIndent } from "./_codeBlock.ts"; import { push, type PushError, type PushOptions } from "./push.ts"; -import type { Result } from "../../deps/option-t.ts"; +import type { Result } from "option-t/plain_result"; +type Line = Page["lines"][number]; /** コードブロックの上書きに使う情報のinterface */ export interface SimpleCodeFile { diff --git a/deno.jsonc b/deno.jsonc index 973c31c..0065b44 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -44,4 +44,4 @@ "vendor/" ] } -} \ No newline at end of file +} diff --git a/deps/hash.ts b/deps/hash.ts deleted file mode 100644 index 9958823..0000000 --- a/deps/hash.ts +++ /dev/null @@ -1 +0,0 @@ -export { Md5 } from "@std/hash"; diff --git a/deps/option-t.ts b/deps/option-t.ts deleted file mode 100644 index 4b9ce88..0000000 --- a/deps/option-t.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "option-t/plain_result"; -export * from "option-t/maybe"; diff --git a/deps/scrapbox-rest.ts b/deps/scrapbox-rest.ts deleted file mode 100644 index 660036a..0000000 --- a/deps/scrapbox-rest.ts +++ /dev/null @@ -1,29 +0,0 @@ -export type { - BadRequestError, - BaseLine as Line, - ErrorLike, - ExportedData, - GuestUser, - ImportedData, - InvalidURLError, - MemberProject, - MemberUser, - NoQueryError, - NotFoundError, - NotLoggedInError, - NotMemberError, - NotMemberProject, - NotPrivilegeError, - Page, - PageList, - PageSnapshotList, - PageSnapshotResult, - ProjectId, - ProjectResponse, - ProjectSearchResult, - SearchedTitle, - SearchResult, - SessionError, - Snapshot, - TweetInfo, -} from "@cosense/types/rest"; diff --git a/deps/scrapbox.ts b/deps/scrapbox.ts deleted file mode 100644 index d3e17f1..0000000 --- a/deps/scrapbox.ts +++ /dev/null @@ -1,7 +0,0 @@ -export type { - BaseLine, - BaseStore, - Line, - Scrapbox, -} from "@cosense/types/userscript"; -export * from "@progfay/scrapbox-parser"; diff --git a/deps/testing.ts b/deps/testing.ts deleted file mode 100644 index a165a85..0000000 --- a/deps/testing.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "@std/assert"; -export * from "@std/testing/snapshot"; diff --git a/deps/unknownutil.ts b/deps/unknownutil.ts deleted file mode 100644 index 5330a34..0000000 --- a/deps/unknownutil.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@core/unknownutil"; diff --git a/parseAbsoluteLink.ts b/parseAbsoluteLink.ts index 576bf5b..48627b7 100644 --- a/parseAbsoluteLink.ts +++ b/parseAbsoluteLink.ts @@ -1,4 +1,4 @@ -import type { LinkNode } from "./deps/scrapbox.ts"; +import type { LinkNode } from "@progfay/scrapbox-parser"; import { parseYoutube } from "./parser/youtube.ts"; import { parseVimeo } from "./parser/vimeo.ts"; import { parseSpotify } from "./parser/spotify.ts"; diff --git a/parser/anchor-fm.test.ts b/parser/anchor-fm.test.ts index fdbe9af..d31fe4b 100644 --- a/parser/anchor-fm.test.ts +++ b/parser/anchor-fm.test.ts @@ -1,5 +1,5 @@ import { parseAnchorFM } from "./anchor-fm.ts"; -import { assertSnapshot } from "../deps/testing.ts"; +import { assertSnapshot } from "@std/testing/snapshot"; Deno.test("spotify links", async (t) => { await t.step("is", async (t) => { diff --git a/parser/spotify.test.ts b/parser/spotify.test.ts index cc4c780..9a9b34e 100644 --- a/parser/spotify.test.ts +++ b/parser/spotify.test.ts @@ -1,5 +1,5 @@ import { parseSpotify } from "./spotify.ts"; -import { assertSnapshot } from "../deps/testing.ts"; +import { assertSnapshot } from "@std/testing/snapshot"; Deno.test("spotify links", async (t) => { await t.step("is", async (t) => { diff --git a/parser/vimeo.test.ts b/parser/vimeo.test.ts deleted file mode 100644 index 80bf1bc..0000000 --- a/parser/vimeo.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { parseVimeo } from "./vimeo.ts"; -import { assertSnapshot } from "../deps/testing.ts"; - -Deno.test("vimeo links", async (t) => { - await t.step("is", async (t) => { - await assertSnapshot( - t, - parseVimeo("https://vimeo.com/121284607"), - ); - }); - - await t.step("is not", async (t) => { - await assertSnapshot( - t, - parseVimeo( - "https://gyazo.com/da78df293f9e83a74b5402411e2f2e01", - ), - ); - await assertSnapshot( - t, - parseVimeo( - "ほげほげ", - ), - ); - await assertSnapshot( - t, - parseVimeo( - "https://yourtube.com/watch?v=rafere", - ), - ); - await assertSnapshot( - t, - parseVimeo( - "https://example.com", - ), - ); - }); -}); diff --git a/parser/youtube.test.ts b/parser/youtube.test.ts index 10c96a8..184310a 100644 --- a/parser/youtube.test.ts +++ b/parser/youtube.test.ts @@ -1,5 +1,5 @@ import { parseYoutube } from "./youtube.ts"; -import { assertSnapshot } from "../deps/testing.ts"; +import { assertSnapshot } from "@std/testing/snapshot"; Deno.test("youtube links", async (t) => { await t.step("is", async (t) => { diff --git a/range.test.ts b/range.test.ts index 18288a3..047841f 100644 --- a/range.test.ts +++ b/range.test.ts @@ -1,5 +1,5 @@ import { range } from "./range.ts"; -import { assertStrictEquals } from "./deps/testing.ts"; +import { assertStrictEquals } from "@std/assert"; Deno.test("range()", () => { let count = 0; diff --git a/rest/auth.ts b/rest/auth.ts index 1dcee39..483f9ad 100644 --- a/rest/auth.ts +++ b/rest/auth.ts @@ -1,4 +1,4 @@ -import { createOk, mapForResult, type Result } from "../deps/option-t.ts"; +import { createOk, mapForResult, type Result } from "option-t/plain_result"; import { getProfile } from "./profile.ts"; import type { HTTPError } from "./responseIntoResult.ts"; import type { AbortError, NetworkError } from "./robustFetch.ts"; diff --git a/rest/getCodeBlock.ts b/rest/getCodeBlock.ts index f4b90e7..03e2146 100644 --- a/rest/getCodeBlock.ts +++ b/rest/getCodeBlock.ts @@ -2,7 +2,7 @@ import type { NotFoundError, NotLoggedInError, NotMemberError, -} from "../deps/scrapbox-rest.ts"; +} from "@cosense/types/rest"; import { cookie } from "./auth.ts"; import { encodeTitleURI } from "../title.ts"; import { type BaseOptions, setDefaults } from "./util.ts"; @@ -12,7 +12,7 @@ import { mapErrAsyncForResult, type Result, unwrapOk, -} from "../deps/option-t.ts"; +} from "option-t/plain_result"; import { type HTTPError, responseIntoResult } from "./responseIntoResult.ts"; import { parseHTTPError } from "./parseHTTPError.ts"; import type { AbortError, NetworkError } from "./robustFetch.ts"; diff --git a/rest/getCodeBlocks.test.ts b/rest/getCodeBlocks.test.ts index 344e1cc..dee7774 100644 --- a/rest/getCodeBlocks.test.ts +++ b/rest/getCodeBlocks.test.ts @@ -1,6 +1,8 @@ -import type { Line } from "../deps/scrapbox-rest.ts"; -import { assertEquals, assertSnapshot } from "../deps/testing.ts"; +import type { Page } from "@cosense/types/rest"; +import { assertEquals } from "@std/assert"; +import { assertSnapshot } from "@std/testing/snapshot"; import { getCodeBlocks } from "./getCodeBlocks.ts"; +type Line = Page["lines"][number]; // https://scrapbox.io/takker/コードブロック記法 const project = "takker"; diff --git a/rest/getCodeBlocks.ts b/rest/getCodeBlocks.ts index 4c1519b..db127ba 100644 --- a/rest/getCodeBlocks.ts +++ b/rest/getCodeBlocks.ts @@ -1,8 +1,9 @@ -import type { Line } from "../deps/scrapbox-rest.ts"; +import type { Page } from "@cosense/types/rest"; import { type CodeTitle, extractFromCodeTitle, } from "../browser/websocket/_codeBlock.ts"; +type Line = Page["lines"][number]; /** pull()から取れる情報で構成したコードブロックの最低限の情報 */ export interface TinyCodeBlock { diff --git a/rest/getGyazoToken.ts b/rest/getGyazoToken.ts index a6a50f5..df05a46 100644 --- a/rest/getGyazoToken.ts +++ b/rest/getGyazoToken.ts @@ -4,8 +4,8 @@ import { mapErrAsyncForResult, type Result, unwrapOk, -} from "../deps/option-t.ts"; -import type { NotLoggedInError } from "../deps/scrapbox-rest.ts"; +} from "option-t/plain_result"; +import type { NotLoggedInError } from "@cosense/types/rest"; import { cookie } from "./auth.ts"; import { parseHTTPError } from "./parseHTTPError.ts"; import { type HTTPError, responseIntoResult } from "./responseIntoResult.ts"; diff --git a/rest/getTweetInfo.ts b/rest/getTweetInfo.ts index 95cb24c..4962cac 100644 --- a/rest/getTweetInfo.ts +++ b/rest/getTweetInfo.ts @@ -4,15 +4,15 @@ import { mapErrAsyncForResult, orElseAsyncForResult, type Result, - toResultOkFromMaybe, unwrapOk, -} from "../deps/option-t.ts"; +} from "option-t/plain_result"; +import { toResultOkFromMaybe } from "option-t/maybe"; import type { BadRequestError, InvalidURLError, SessionError, TweetInfo, -} from "../deps/scrapbox-rest.ts"; +} from "@cosense/types/rest"; import { cookie, getCSRFToken } from "./auth.ts"; import { parseHTTPError } from "./parseHTTPError.ts"; import { type HTTPError, responseIntoResult } from "./responseIntoResult.ts"; diff --git a/rest/getWebPageTitle.ts b/rest/getWebPageTitle.ts index 41116a5..503a943 100644 --- a/rest/getWebPageTitle.ts +++ b/rest/getWebPageTitle.ts @@ -4,14 +4,14 @@ import { mapErrAsyncForResult, orElseAsyncForResult, type Result, - toResultOkFromMaybe, unwrapOk, -} from "../deps/option-t.ts"; +} from "option-t/plain_result"; +import { toResultOkFromMaybe } from "option-t/maybe"; import type { BadRequestError, InvalidURLError, SessionError, -} from "../deps/scrapbox-rest.ts"; +} from "@cosense/types/rest"; import { cookie, getCSRFToken } from "./auth.ts"; import { parseHTTPError } from "./parseHTTPError.ts"; import { type HTTPError, responseIntoResult } from "./responseIntoResult.ts"; diff --git a/rest/link.ts b/rest/link.ts index 3fa61f6..b8fbc24 100644 --- a/rest/link.ts +++ b/rest/link.ts @@ -5,13 +5,13 @@ import { mapErrAsyncForResult, type Result, unwrapOk, -} from "../deps/option-t.ts"; +} from "option-t/plain_result"; import type { ErrorLike, NotFoundError, NotLoggedInError, SearchedTitle, -} from "../deps/scrapbox-rest.ts"; +} from "@cosense/types/rest"; import { cookie } from "./auth.ts"; import { parseHTTPError } from "./parseHTTPError.ts"; import { type HTTPError, responseIntoResult } from "./responseIntoResult.ts"; diff --git a/rest/page-data.ts b/rest/page-data.ts index a68548d..cddea9b 100644 --- a/rest/page-data.ts +++ b/rest/page-data.ts @@ -5,16 +5,16 @@ import { mapErrAsyncForResult, orElseAsyncForResult, type Result, - toResultOkFromMaybe, unwrapOk, -} from "../deps/option-t.ts"; +} from "option-t/plain_result"; +import { toResultOkFromMaybe } from "option-t/maybe"; import type { ExportedData, ImportedData, NotFoundError, NotLoggedInError, NotPrivilegeError, -} from "../deps/scrapbox-rest.ts"; +} from "@cosense/types/rest"; import { cookie, getCSRFToken } from "./auth.ts"; import { parseHTTPError } from "./parseHTTPError.ts"; import { type HTTPError, responseIntoResult } from "./responseIntoResult.ts"; diff --git a/rest/pages.test.ts b/rest/pages.test.ts index 3c7f1b9..bbec994 100644 --- a/rest/pages.test.ts +++ b/rest/pages.test.ts @@ -1,5 +1,5 @@ import { getPage, listPages } from "./pages.ts"; -import { assertSnapshot } from "../deps/testing.ts"; +import { assertSnapshot } from "@std/testing/snapshot"; Deno.test("getPage", async (t) => { await assertSnapshot( diff --git a/rest/pages.ts b/rest/pages.ts index 8c1993f..1094851 100644 --- a/rest/pages.ts +++ b/rest/pages.ts @@ -5,7 +5,7 @@ import type { NotMemberError, Page, PageList, -} from "../deps/scrapbox-rest.ts"; +} from "@cosense/types/rest"; import { cookie } from "./auth.ts"; import { parseHTTPError } from "./parseHTTPError.ts"; import { encodeTitleURI } from "../title.ts"; @@ -15,8 +15,8 @@ import { mapAsyncForResult, mapErrAsyncForResult, type Result, - unwrapOrForMaybe, -} from "../deps/option-t.ts"; +} from "option-t/plain_result"; +import { unwrapOrForMaybe } from "option-t/maybe"; import { type HTTPError, responseIntoResult } from "./responseIntoResult.ts"; import type { AbortError, NetworkError } from "./robustFetch.ts"; diff --git a/rest/parseHTTPError.ts b/rest/parseHTTPError.ts index 7ce1edb..cec4fbf 100644 --- a/rest/parseHTTPError.ts +++ b/rest/parseHTTPError.ts @@ -7,14 +7,14 @@ import type { NotMemberError, NotPrivilegeError, SessionError, -} from "../deps/scrapbox-rest.ts"; -import type { Maybe } from "../deps/option-t.ts"; +} from "@cosense/types/rest"; +import type { Maybe } from "option-t/maybe"; import { isArrayOf, isLiteralOneOf, isRecord, isString, -} from "../deps/unknownutil.ts"; +} from "@core/unknownutil"; import type { HTTPError } from "./responseIntoResult.ts"; export interface RESTfullAPIErrorMap { diff --git a/rest/profile.ts b/rest/profile.ts index 2f9aed6..67e2996 100644 --- a/rest/profile.ts +++ b/rest/profile.ts @@ -3,8 +3,8 @@ import { mapAsyncForResult, type Result, unwrapOk, -} from "../deps/option-t.ts"; -import type { GuestUser, MemberUser } from "../deps/scrapbox-rest.ts"; +} from "option-t/plain_result"; +import type { GuestUser, MemberUser } from "@cosense/types/rest"; import { cookie } from "./auth.ts"; import { type HTTPError, responseIntoResult } from "./responseIntoResult.ts"; import type { AbortError, NetworkError } from "./robustFetch.ts"; diff --git a/rest/project.test.ts b/rest/project.test.ts index 044fd81..3e51935 100644 --- a/rest/project.test.ts +++ b/rest/project.test.ts @@ -1,5 +1,5 @@ import { getProject, listProjects } from "./project.ts"; -import { assertSnapshot } from "../deps/testing.ts"; +import { assertSnapshot } from "@std/testing/snapshot"; Deno.test("getProject", async (t) => { await assertSnapshot( diff --git a/rest/project.ts b/rest/project.ts index 28851f2..cf0feab 100644 --- a/rest/project.ts +++ b/rest/project.ts @@ -4,7 +4,7 @@ import { mapErrAsyncForResult, type Result, unwrapOk, -} from "../deps/option-t.ts"; +} from "option-t/plain_result"; import type { MemberProject, NotFoundError, @@ -13,7 +13,7 @@ import type { NotMemberProject, ProjectId, ProjectResponse, -} from "../deps/scrapbox-rest.ts"; +} from "@cosense/types/rest"; import { cookie } from "./auth.ts"; import { parseHTTPError } from "./parseHTTPError.ts"; import { type HTTPError, responseIntoResult } from "./responseIntoResult.ts"; diff --git a/rest/replaceLinks.ts b/rest/replaceLinks.ts index 946555c..8ff2c36 100644 --- a/rest/replaceLinks.ts +++ b/rest/replaceLinks.ts @@ -4,14 +4,14 @@ import { mapErrAsyncForResult, orElseAsyncForResult, type Result, - toResultOkFromMaybe, unwrapOk, -} from "../deps/option-t.ts"; +} from "option-t/plain_result"; +import { toResultOkFromMaybe } from "option-t/maybe"; import type { NotFoundError, NotLoggedInError, NotMemberError, -} from "../deps/scrapbox-rest.ts"; +} from "@cosense/types/rest"; import { cookie, getCSRFToken } from "./auth.ts"; import { parseHTTPError } from "./parseHTTPError.ts"; import { type HTTPError, responseIntoResult } from "./responseIntoResult.ts"; diff --git a/rest/responseIntoResult.ts b/rest/responseIntoResult.ts index 7670c65..280230d 100644 --- a/rest/responseIntoResult.ts +++ b/rest/responseIntoResult.ts @@ -1,4 +1,4 @@ -import { createErr, createOk, type Result } from "../deps/option-t.ts"; +import { createErr, createOk, type Result } from "option-t/plain_result"; export interface HTTPError { name: "HTTPError"; diff --git a/rest/robustFetch.ts b/rest/robustFetch.ts index ce67fcf..b786343 100644 --- a/rest/robustFetch.ts +++ b/rest/robustFetch.ts @@ -1,4 +1,4 @@ -import { createErr, createOk, type Result } from "../deps/option-t.ts"; +import { createErr, createOk, type Result } from "option-t/plain_result"; export interface NetworkError { name: "NetworkError"; diff --git a/rest/search.ts b/rest/search.ts index a67d3a5..205ea0e 100644 --- a/rest/search.ts +++ b/rest/search.ts @@ -4,7 +4,7 @@ import { mapErrAsyncForResult, type Result, unwrapOk, -} from "../deps/option-t.ts"; +} from "option-t/plain_result"; import type { NoQueryError, NotFoundError, @@ -12,7 +12,7 @@ import type { NotMemberError, ProjectSearchResult, SearchResult, -} from "../deps/scrapbox-rest.ts"; +} from "@cosense/types/rest"; import { cookie } from "./auth.ts"; import { parseHTTPError } from "./parseHTTPError.ts"; import { type HTTPError, responseIntoResult } from "./responseIntoResult.ts"; diff --git a/rest/snapshot.ts b/rest/snapshot.ts index 566f607..47ced0e 100644 --- a/rest/snapshot.ts +++ b/rest/snapshot.ts @@ -5,7 +5,7 @@ import type { NotMemberError, PageSnapshotList, PageSnapshotResult, -} from "../deps/scrapbox-rest.ts"; +} from "@cosense/types/rest"; import { cookie } from "./auth.ts"; import { type BaseOptions, setDefaults } from "./util.ts"; import { parseHTTPError } from "./parseHTTPError.ts"; @@ -15,7 +15,7 @@ import { mapErrAsyncForResult, type Result, unwrapOk, -} from "../deps/option-t.ts"; +} from "option-t/plain_result"; import type { AbortError, NetworkError } from "./robustFetch.ts"; import { type HTTPError, responseIntoResult } from "./responseIntoResult.ts"; diff --git a/rest/table.ts b/rest/table.ts index 8f3e14b..91e195f 100644 --- a/rest/table.ts +++ b/rest/table.ts @@ -2,7 +2,7 @@ import type { NotFoundError, NotLoggedInError, NotMemberError, -} from "../deps/scrapbox-rest.ts"; +} from "@cosense/types/rest"; import { cookie } from "./auth.ts"; import { encodeTitleURI } from "../title.ts"; import { type BaseOptions, setDefaults } from "./util.ts"; @@ -12,7 +12,7 @@ import { mapErrAsyncForResult, type Result, unwrapOk, -} from "../deps/option-t.ts"; +} from "option-t/plain_result"; import { type HTTPError, responseIntoResult } from "./responseIntoResult.ts"; import { parseHTTPError } from "./parseHTTPError.ts"; import type { AbortError, NetworkError } from "./robustFetch.ts"; diff --git a/rest/uploadToGCS.ts b/rest/uploadToGCS.ts index fd35dd7..98c287f 100644 --- a/rest/uploadToGCS.ts +++ b/rest/uploadToGCS.ts @@ -1,7 +1,7 @@ import { cookie, getCSRFToken } from "./auth.ts"; import { type BaseOptions, type ExtendedOptions, setDefaults } from "./util.ts"; -import type { ErrorLike, NotFoundError } from "../deps/scrapbox-rest.ts"; -import { Md5 } from "../deps/hash.ts"; +import type { ErrorLike, NotFoundError } from "@cosense/types/rest"; +import { Md5 } from "@std/hash"; import { createOk, isErr, @@ -10,9 +10,9 @@ import { mapForResult, orElseAsyncForResult, type Result, - toResultOkFromMaybe, unwrapOk, -} from "../deps/option-t.ts"; +} from "option-t/plain_result"; +import { toResultOkFromMaybe } from "option-t/maybe"; import type { AbortError, NetworkError } from "./robustFetch.ts"; import { type HTTPError, responseIntoResult } from "./responseIntoResult.ts"; diff --git a/text.test.ts b/text.test.ts index bb97108..c3c544f 100644 --- a/text.test.ts +++ b/text.test.ts @@ -1,5 +1,5 @@ import { getIndentCount } from "./text.ts"; -import { assertStrictEquals } from "./deps/testing.ts"; +import { assertStrictEquals } from "@std/assert"; Deno.test("getIndentCount()", () => { assertStrictEquals(getIndentCount("sample text "), 0); diff --git a/text.ts b/text.ts index 2ffc2ec..69668e0 100644 --- a/text.ts +++ b/text.ts @@ -1,4 +1,4 @@ -import { isString } from "./deps/unknownutil.ts"; +import { isString } from "@core/unknownutil"; /** インデント数を数える */ export const getIndentCount = (text: string): number => diff --git a/title.test.ts b/title.test.ts index be71ce0..135c791 100644 --- a/title.test.ts +++ b/title.test.ts @@ -4,7 +4,7 @@ import { toReadableTitleURI, toTitleLc, } from "./title.ts"; -import { assertStrictEquals } from "./deps/testing.ts"; +import { assertStrictEquals } from "@std/assert"; Deno.test("toTitleLc()", async (t) => { await t.step("` ` -> `_`", () => { From 7ad0cd0185df45f181a7a357326ce04c01ed0731 Mon Sep 17 00:00:00 2001 From: takker99 <37929109+takker99@users.noreply.github.com> Date: Fri, 2 Aug 2024 16:35:14 +0900 Subject: [PATCH 4/5] feat: Export `parseAbsoluteLink` Individually --- deno.jsonc | 1 + parseAbsoluteLink.ts | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/deno.jsonc b/deno.jsonc index 0065b44..7143942 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -28,6 +28,7 @@ "./browser": "./browser/mod.ts", "./browser/dom": "./browser/dom/mod.ts", "./browser/websocket": "./browser/websocket/mod.ts", + "./parseAbsoluteLink": "./parseAbsoluteLink.ts", "./title": "./title.ts", "./text": "./text.ts" }, diff --git a/parseAbsoluteLink.ts b/parseAbsoluteLink.ts index 48627b7..de83d44 100644 --- a/parseAbsoluteLink.ts +++ b/parseAbsoluteLink.ts @@ -1,3 +1,8 @@ +/** Parse `LinkNode` of [@progfay/scrapbox-parser](https://jsr.io/@progfay/scrapbox-parser) in detail + * + * @module + */ + import type { LinkNode } from "@progfay/scrapbox-parser"; import { parseYoutube } from "./parser/youtube.ts"; import { parseVimeo } from "./parser/vimeo.ts"; @@ -112,10 +117,10 @@ export const parseAbsoluteLink = ( return { type: "absoluteLink", content, href, ...baseLink }; }; -type AudioURL = `${string}.${"mp3" | "ogg" | "wav" | "acc"}`; +export type AudioURL = `${string}.${"mp3" | "ogg" | "wav" | "acc"}`; const isAudioURL = (url: string): url is AudioURL => /\.(?:mp3|ogg|wav|aac)$/.test(url); -type VideoURL = `${string}.${"mp4" | "webm"}`; +export type VideoURL = `${string}.${"mp4" | "webm"}`; const isVideoURL = (url: string): url is VideoURL => /\.(?:mp4|webm)$/.test(url); From a13ce32535a61e73bd6d7d601e8445bef2892ce0 Mon Sep 17 00:00:00 2001 From: takker99 <37929109+takker99@users.noreply.github.com> Date: Fri, 2 Aug 2024 16:35:32 +0900 Subject: [PATCH 5/5] refactor: Remove `ensure.ts` --- browser/dom/node.ts | 7 +++---- ensure.ts | 7 ------- 2 files changed, 3 insertions(+), 11 deletions(-) delete mode 100644 ensure.ts diff --git a/browser/dom/node.ts b/browser/dom/node.ts index a46567a..f4c60ed 100644 --- a/browser/dom/node.ts +++ b/browser/dom/node.ts @@ -1,5 +1,5 @@ import { isNumber, isString, isUndefined } from "@core/unknownutil"; -import { ensureArray } from "../../ensure.ts"; +import { ensure, isArray } from "@core/unknownutil"; import { getCachedLines } from "./getCachedLines.ts"; import { takeInternalLines } from "./takeInternalLines.ts"; import type { BaseLine, Line } from "@cosense/types/userscript"; @@ -90,9 +90,8 @@ export const isLineDOM = (dom: unknown): dom is HTMLDivElement => export const getLineCount = (): number => takeInternalLines().length; export const getLines = (): readonly Line[] => { - const lines = getCachedLines(); - ensureArray(lines, "scrapbox.Page.lines"); - return lines; + const lines = ensure(getCachedLines(), isArray); + return lines as Line[]; }; export const getText = ( diff --git a/ensure.ts b/ensure.ts deleted file mode 100644 index d002b0a..0000000 --- a/ensure.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const ensureArray: ( - value: unknown, - name: string, -) => asserts value is T[] = (value, name) => { - if (Array.isArray(value)) return; - throw new TypeError(`"${name}" must be an array but actual is "${value}"`); -};