diff --git a/denops/@ddc-sources/stamp.ts b/denops/@ddc-sources/stamp.ts index 388e8f1..ae74017 100644 --- a/denops/@ddc-sources/stamp.ts +++ b/denops/@ddc-sources/stamp.ts @@ -1,21 +1,23 @@ import { + assert, ddcVim, ddcVimSource, Denops, - ensureString, + ensure, + is, traq, vars, } from "../traqvim/deps.ts"; - import { getStamps } from "../traqvim/model.ts"; import { api } from "../traqvim/api.ts"; +import { isDdcItem } from "../traqvim/type_check.ts"; type Params = Record; export class Source extends ddcVim.BaseSource { async onInit(args: ddcVimSource.OnInitArguments): Promise { const path = await vars.globals.get(args.denops, "traqvim#token_file_path"); - ensureString(path); + assert(path, is.String); api.tokenFilePath = path; return Promise.resolve(); } @@ -25,9 +27,9 @@ export class Source extends ddcVim.BaseSource { return stamps .filter((stamp) => stamp.name) .map((stamp) => { - return { + return ensure({ word: ":" + stamp.name + ":", - } as ddcVim.Item; + }, isDdcItem); }); } diff --git a/denops/@ddu-columns/channel.ts b/denops/@ddu-columns/channel.ts index 90b2859..0cdc621 100644 --- a/denops/@ddu-columns/channel.ts +++ b/denops/@ddu-columns/channel.ts @@ -1,4 +1,4 @@ -import { dduVim, dduVimColumn, fn } from "../traqvim/deps.ts"; +import { dduVim, dduVimColumn, ensure, fn, is } from "../traqvim/deps.ts"; export type Params = { collapsedParentIcon: string; @@ -36,7 +36,7 @@ export class Column extends dduVim.BaseColumn { const text = " ".repeat(args.columnParams.indentationWidth * args.item.__level) + icon + " " + args.item.word; - const width = await fn.strwidth(args.denops, text) as number; + const width = ensure(await fn.strwidth(args.denops, text), is.Number); const padding = " ".repeat(args.endCol - args.startCol - width); return Promise.resolve({ text: text + padding, diff --git a/denops/@ddu-kinds/channel.ts b/denops/@ddu-kinds/channel.ts index 631dd04..eff511f 100644 --- a/denops/@ddu-kinds/channel.ts +++ b/denops/@ddu-kinds/channel.ts @@ -1,8 +1,10 @@ import { + assert, dduVim, Denops, - ensureArray, - ensureNumber, + ensure, + is, + Predicate, vars, } from "../traqvim/deps.ts"; import { channelMessageOptions, channelTimeline } from "../traqvim/model.ts"; @@ -10,16 +12,25 @@ import { actionOpenChannel } from "../traqvim/action.ts"; import { Message } from "../traqvim/type.d.ts"; // TODO: unkownutilのアプデしたらtype.d.tsのChannelとかに変更する +// ChannelとUnreadChannelをUnion型にできなかったので、kindごと分ける export interface ActionData { id: string; } +export const isActionData: Predicate = is.ObjectOf({ + id: is.String, +}); + type Params = Record; type OpenParams = { command: string; }; +const isOpenParams: Predicate = is.ObjectOf({ + command: is.String, +}); + export class Kind extends dduVim.BaseKind { actions: dduVim.Actions = { open: async (args: { @@ -31,15 +42,14 @@ export class Kind extends dduVim.BaseKind { if (!item.action) { continue; } - // TODO: unkownutilのアプデしたらasをensureに変更 - const action = item.action as ActionData; + const action = ensure(item.action, isActionData); const channelPath: string = item.word; const channelID: string = action.id; const limit = await vars.globals.get( args.denops, "traqvim#fetch_limit", ); - ensureNumber(limit); + assert(limit, is.Number); const timelineOption: channelMessageOptions = { id: channelID, channelPath: channelPath, @@ -47,7 +57,7 @@ export class Kind extends dduVim.BaseKind { until: new Date().toISOString(), order: "desc", }; - const params = args.actionParams as OpenParams; + const params = ensure(args.actionParams, isOpenParams); if (params.command) { await args.denops.cmd(params.command); } @@ -65,7 +75,7 @@ export class Kind extends dduVim.BaseKind { item: dduVim.DduItem; }, ): Promise { - const action = args.item.action as ActionData; + const action = ensure(args.item.action, isActionData); if (!action) { return undefined; } @@ -80,7 +90,7 @@ export class Kind extends dduVim.BaseKind { message, args.previewContext.width, ); - ensureArray(ret); + assert(ret, is.ArrayOf(is.String)); return ret; }), ); diff --git a/denops/@ddu-sources/channel.ts b/denops/@ddu-sources/channel.ts index 805f43a..6e39ef5 100644 --- a/denops/@ddu-sources/channel.ts +++ b/denops/@ddu-sources/channel.ts @@ -1,9 +1,10 @@ import { + assert, dduVim, dduVimSource, Denops, - ensureString, helper, + is, vars, } from "../traqvim/deps.ts"; import { ActionData } from "../@ddu-kinds/channel.ts"; @@ -17,7 +18,7 @@ export class Source extends dduVim.BaseSource { kind = "channel"; async onInit(args: dduVimSource.OnInitArguments): Promise { const path = await vars.globals.get(args.denops, "traqvim#token_file_path"); - ensureString(path); + assert(path, is.String); api.tokenFilePath = path; return Promise.resolve(); } diff --git a/denops/@ddu-sources/channel_rec.ts b/denops/@ddu-sources/channel_rec.ts index ba71a01..b46a560 100644 --- a/denops/@ddu-sources/channel_rec.ts +++ b/denops/@ddu-sources/channel_rec.ts @@ -1,8 +1,9 @@ import { + assert, dduVim, dduVimSource, Denops, - ensureString, + is, vars, } from "../traqvim/deps.ts"; import { ActionData } from "../@ddu-kinds/channel.ts"; @@ -18,7 +19,7 @@ export class Source extends dduVim.BaseSource { kind = "channel"; async onInit(args: dduVimSource.OnInitArguments): Promise { const path = await vars.globals.get(args.denops, "traqvim#token_file_path"); - ensureString(path); + assert(path, is.String); api.tokenFilePath = path; return Promise.resolve(); } diff --git a/denops/traqvim/action.ts b/denops/traqvim/action.ts index b37da9b..1aee84f 100644 --- a/denops/traqvim/action.ts +++ b/denops/traqvim/action.ts @@ -1,4 +1,4 @@ -import { bufname, Denops, ensureArray, fn, helper, vars } from "./deps.ts"; +import { assert, bufname, Denops, fn, helper, is, vars } from "./deps.ts"; import { ChannelBuffer, Message } from "./type.d.ts"; import { activity, @@ -9,6 +9,7 @@ import { editMessage, removePin, } from "./model.ts"; +import { isMessage } from "./type_check.ts"; export const actionOpenChannel = async ( denops: Denops, @@ -56,7 +57,7 @@ export const actionForwardChannelMessage = async ( ): Promise => { // 既存メッセージの取得 const timeline = await vars.buffers.get(denops, "channelTimeline"); - ensureArray(timeline); + assert(timeline, is.ArrayOf(isMessage)); // 追記したものをセット await vars.buffers.set( denops, @@ -79,7 +80,7 @@ export const actionBackChannelMessage = async ( ): Promise => { // 既存メッセージの取得 const timeline = await vars.buffers.get(denops, "channelTimeline"); - ensureArray(timeline); + assert(timeline, is.ArrayOf(isMessage)); // 追記したものをセット await vars.buffers.set( denops, @@ -103,7 +104,7 @@ export const actionDeleteMessage = async ( } // 既存メッセージの取得 const timeline = await vars.buffers.get(denops, "channelTimeline"); - ensureArray(timeline); + assert(timeline, is.ArrayOf(isMessage)); // 削除したものをセット await vars.buffers.set( denops, @@ -128,7 +129,7 @@ export const actionEditMessage = async ( // 既存メッセージの取得 // const timeline = await vars.buffers.get(denops, "channelTimeline"); const timeline = await fn.getbufvar(denops, bufNum, "channelTimeline"); - ensureArray(timeline); + assert(timeline, is.ArrayOf(isMessage)); const editedTimeline = timeline.map((m) => { if (m.id === message.id) { return { @@ -209,7 +210,7 @@ export const actionCreatePin = async ( } // 既存メッセージの取得 const timeline = await vars.buffers.get(denops, "channelTimeline"); - ensureArray(timeline); + assert(timeline, is.ArrayOf(isMessage)); message.pinned = true; // ピン留めしたものをセット await vars.buffers.set( @@ -239,7 +240,7 @@ export const actionRemovePin = async ( } // 既存メッセージの取得 const timeline = await vars.buffers.get(denops, "channelTimeline"); - ensureArray(timeline); + assert(timeline, is.ArrayOf(isMessage)); message.pinned = false; // ピン留め解除したものをセット await vars.buffers.set( diff --git a/denops/traqvim/deps.ts b/denops/traqvim/deps.ts index f9b3f41..2c32e7d 100644 --- a/denops/traqvim/deps.ts +++ b/denops/traqvim/deps.ts @@ -2,11 +2,11 @@ export * as oauth2Client from "https://deno.land/x/oauth2_client@v1.0.0/mod.ts"; export * as oak from "https://deno.land/x/oak@v6.3.0/mod.ts"; export * as path from "https://deno.land/std@0.177.0/path/mod.ts"; export { - ensureArray, - ensureNumber, - ensureObject, - ensureString, -} from "https://deno.land/x/unknownutil@v1.0.0/mod.ts"; + assert, + ensure, + is, +} from "https://deno.land/x/unknownutil@v3.18.1/mod.ts"; +export type { Predicate } from "https://deno.land/x/unknownutil@v3.18.1/mod.ts"; export type { Denops } from "https://deno.land/x/denops_std@v4.0.0/mod.ts"; export * as fn from "https://deno.land/x/denops_std@v4.0.0/function/mod.ts"; export * as vars from "https://deno.land/x/denops_std@v4.0.0/variable/mod.ts"; diff --git a/denops/traqvim/main.ts b/denops/traqvim/main.ts index 7ef41c5..1f0e97f 100644 --- a/denops/traqvim/main.ts +++ b/denops/traqvim/main.ts @@ -8,13 +8,13 @@ import { sendMessage, } from "./model.ts"; import { + assert, bufname, Denops, - ensureArray, - ensureNumber, - ensureString, + ensure, fn, helper, + is, vars, } from "./deps.ts"; import { @@ -31,10 +31,11 @@ import { } from "./action.ts"; import { ChannelMessageBuffer, Message } from "./type.d.ts"; import { api } from "./api.ts"; +import { isMessage } from "./type_check.ts"; export async function main(denops: Denops) { const path = await vars.globals.get(denops, "traqvim#token_file_path"); - ensureString(path); + assert(path, is.String); api.tokenFilePath = path; // oauthの仮オブジェクト let oauth: OAuth; @@ -62,7 +63,7 @@ export async function main(denops: Denops) { "No", "Warning", ); - ensureNumber(choice); + assert(choice, is.Number); switch (choice) { // dialogの中断 case 0: @@ -87,7 +88,7 @@ export async function main(denops: Denops) { const homePath = await homeChannelPath(); const homeId = await homeChannelId(); const limit = await vars.globals.get(denops, "traqvim#fetch_limit"); - ensureNumber(limit); + assert(limit, is.Number); const timelineOption: channelMessageOptions = { id: homeId, channelPath: homePath, @@ -99,7 +100,7 @@ export async function main(denops: Denops) { return; }, async timeline(channelPath: unknown): Promise { - ensureString(channelPath); + assert(channelPath, is.String); // '#gps/times/kamecha' → ['gps', 'times', 'kamecha'] const channelPathArray = channelPath.substring(1).split("/"); const channelID = await channelUUID(channelPathArray); @@ -107,9 +108,9 @@ export async function main(denops: Denops) { helper.echo(denops, "Channel not found"); return; } - ensureString(channelID); + assert(channelID, is.String); const limit = await vars.globals.get(denops, "traqvim#fetch_limit"); - ensureNumber(limit); + assert(limit, is.Number); const timelineOption: channelMessageOptions = { id: channelID, channelPath: channelPath, @@ -126,8 +127,8 @@ export async function main(denops: Denops) { }, async reload(bufNum: unknown, bufName: unknown): Promise { // バッファ番号は被らないが、バッファ名は被る可能性がある - ensureNumber(bufNum); - ensureString(bufName); + assert(bufNum, is.Number); + assert(bufName, is.String); const bufnameParsed = bufname.parse(bufName); if (bufnameParsed.expr === "/Activity") { actionOpenActivity(denops, bufNum); @@ -137,9 +138,9 @@ export async function main(denops: Denops) { const bufNameWithoutNumber = bufnameParsed.fragment?.replace(/\(\d+\)$/, "") || ""; const channelID = await vars.buffers.get(denops, "channelID"); - ensureString(channelID); + assert(channelID, is.String); const limit = await vars.globals.get(denops, "traqvim#fetch_limit"); - ensureNumber(limit); + assert(limit, is.Number); const timelineOption: channelMessageOptions = { id: channelID, channelPath: bufNameWithoutNumber, @@ -152,17 +153,17 @@ export async function main(denops: Denops) { return; }, async messageForward(bufNum: unknown, bufName: unknown): Promise { - ensureNumber(bufNum); - ensureString(bufName); + assert(bufNum, is.Number); + assert(bufName, is.String); // 対応するバッファのメッセージの新しいメッセージの日付を取得 try { const timeline = await vars.buffers.get(denops, "channelTimeline"); - ensureArray(timeline); + assert(timeline, is.ArrayOf(isMessage)); const bufNameWithoutNumber = bufName.replace(/\(\d+\)$/, ""); const channelID = await vars.buffers.get(denops, "channelID"); - ensureString(channelID); + assert(channelID, is.String); const limit = await vars.globals.get(denops, "traqvim#fetch_limit"); - ensureNumber(limit); + assert(limit, is.Number); // 最後のメッセージの内容 const timelineOption: channelMessageOptions = { id: channelID, @@ -189,17 +190,17 @@ export async function main(denops: Denops) { } }, async messageBack(bufNum: unknown, bufName: unknown): Promise { - ensureNumber(bufNum); - ensureString(bufName); + assert(bufNum, is.Number); + assert(bufName, is.String); // 対応するバッファのメッセージの古いメッセージの日付を取得 try { const timeline = await vars.buffers.get(denops, "channelTimeline"); - ensureArray(timeline); + assert(timeline, is.ArrayOf(isMessage)); const bufNameWithoutNumber = bufName.replace(/\(\d+\)$/, ""); const channelID = await vars.buffers.get(denops, "channelID"); - ensureString(channelID); + assert(channelID, is.String); const limit = await vars.globals.get(denops, "traqvim#fetch_limit"); - ensureNumber(limit); + assert(limit, is.Number); // 最後のメッセージの内容 const timelineOption: channelMessageOptions = { id: channelID, @@ -224,10 +225,10 @@ export async function main(denops: Denops) { } }, async messageOpen(bufNum: unknown, bufName: unknown): Promise { - ensureNumber(bufNum); - ensureString(bufName); + assert(bufNum, is.Number); + assert(bufName, is.String); const channelID = await fn.getbufvar(denops, bufNum, "channelID"); - ensureString(channelID); + assert(channelID, is.String); const channelMessageVars: ChannelMessageBuffer = { channelID: channelID, }; @@ -257,25 +258,27 @@ export async function main(denops: Denops) { }, async messageSend(bufNum: unknown, contents: unknown): Promise { helper.echo(denops, "Sending..."); - ensureNumber(bufNum); + assert(bufNum, is.Number); const channelID = await fn.getbufvar(denops, bufNum, "channelID"); - ensureString(channelID); - const content = (contents as string[]).join("\n"); + assert(channelID, is.String); + const content = (ensure(contents, is.ArrayOf(is.String))).join("\n"); await sendMessage(channelID, content); await denops.cmd(":bdelete"); return; }, async yankMessageLink(message: unknown): Promise { - // ensureでの型チェックの仕方分からんから、とりあえずasで:awoo: - await actionYankMessageLink(denops, message as Message); + assert(message, isMessage); + await actionYankMessageLink(denops, message); return Promise.resolve(); }, async yankMessageMarkdown(message: unknown): Promise { - await actionYankMessageMarkdown(denops, message as Message); + assert(message, isMessage); + await actionYankMessageMarkdown(denops, message); return Promise.resolve(); }, async messageDelete(bufNum: unknown, message: unknown): Promise { - ensureNumber(bufNum); + assert(bufNum, is.Number); + assert(message, isMessage); const choice = await fn.confirm( denops, "Delete message?", @@ -283,7 +286,7 @@ export async function main(denops: Denops) { "No", "Warning", ); - ensureNumber(choice); + assert(choice, is.Number); switch (choice) { // dialogの中断 case 0: @@ -292,7 +295,7 @@ export async function main(denops: Denops) { // Yes case 1: helper.echo(denops, "delete message..."); - await actionDeleteMessage(denops, message as Message, bufNum); + await actionDeleteMessage(denops, message, bufNum); break; // No case 2: @@ -305,7 +308,7 @@ export async function main(denops: Denops) { return Promise.resolve(); }, async messageEditOpen(bufNum: unknown, message: unknown): Promise { - ensureNumber(bufNum); + assert(bufNum, is.Number); const bufName = await fn.bufname(denops, bufNum); const messageBufName = bufname.format({ scheme: bufname.parse(bufName).scheme, @@ -316,7 +319,7 @@ export async function main(denops: Denops) { fragment: bufname.parse(bufName).fragment, }); const messageBufNum = await fn.bufnr(denops, messageBufName, true); - ensureNumber(messageBufNum); + assert(messageBufNum, is.Number); await fn.setbufvar(denops, bufNum, "&splitbelow", 1); await denops.cmd(`split +buffer\\ ${messageBufNum}`); await fn.setbufvar(denops, bufNum, "&splitright", 0); @@ -325,7 +328,7 @@ export async function main(denops: Denops) { denops, messageBufNum, 1, - (message as Message).content.split("\n"), + (ensure(message, isMessage)).content.split("\n"), ); await fn.setbufvar( denops, @@ -349,9 +352,10 @@ export async function main(denops: Denops) { message: unknown, contents: unknown, ): Promise { - ensureNumber(bufNum); - const content = (contents as string[]).join("\n"); - await actionEditMessage(denops, message as Message, content, bufNum); + assert(bufNum, is.Number); + assert(message, isMessage); + const content = (ensure(contents, is.ArrayOf(is.String))).join("\n"); + await actionEditMessage(denops, message, content, bufNum); await denops.cmd(":bdelete"); return; }, @@ -359,16 +363,18 @@ export async function main(denops: Denops) { bufNum: unknown, message: unknown, ): Promise { - ensureNumber(bufNum); - await actionCreatePin(denops, message as Message, bufNum); + assert(bufNum, is.Number); + assert(message, isMessage); + await actionCreatePin(denops, message, bufNum); return; }, async removePin( bufNum: unknown, message: unknown, ): Promise { - ensureNumber(bufNum); - await actionRemovePin(denops, message as Message, bufNum); + assert(bufNum, is.Number); + assert(message, isMessage); + await actionRemovePin(denops, message, bufNum); return; }, }; diff --git a/denops/traqvim/model.ts b/denops/traqvim/model.ts index 0265273..b46fc53 100644 --- a/denops/traqvim/model.ts +++ b/denops/traqvim/model.ts @@ -1,6 +1,6 @@ import { api } from "./api.ts"; import { Channel, Message, UnreadChannel } from "./type.d.ts"; -import { traq } from "./deps.ts"; +import { ensure, is, traq } from "./deps.ts"; export type channelMessageOptions = { // channelUUID @@ -133,7 +133,7 @@ export const homeChannelId = async (): Promise => { if (me.homeChannel === null) { return ""; } - return me.homeChannel as string; + return ensure(me.homeChannel, is.String); }; // userIdからユーザー情報を取得する diff --git a/denops/traqvim/type_check.ts b/denops/traqvim/type_check.ts new file mode 100644 index 0000000..74585af --- /dev/null +++ b/denops/traqvim/type_check.ts @@ -0,0 +1,70 @@ +import { ddcVim, is, Predicate, traq } from "./deps.ts"; +import { Message } from "./type.d.ts"; + +const isPumHighlight: Predicate = is.ObjectOf({ + name: is.String, + type: is.LiteralOneOf(["abbr", "kind", "menu"] as const), + hl_group: is.String, + col: is.Number, + width: is.Number, +}); + +export const isDdcItem: Predicate = is.ObjectOf({ + word: is.String, + abbr: is.OptionalOf(is.String), + menu: is.OptionalOf(is.String), + info: is.OptionalOf(is.String), + kind: is.OptionalOf(is.String), + user_data: is.OptionalOf(is.Unknown), + highlights: is.OptionalOf(is.ArrayOf(isPumHighlight)), + columns: is.OptionalOf(is.RecordOf(is.String)), +}); + +export const isMessageStamp: Predicate = is.ObjectOf({ + userId: is.String, + stampId: is.String, + count: is.Number, + createdAt: is.String, + updatedAt: is.String, +}); + +const isUserAccountState: Predicate = is.UnionOf([ + // 停止 + is.LiteralOf(0), + // 有効 + is.LiteralOf(1), + // 一時停止 + is.LiteralOf(2), +]); + +const isUser: Predicate = is.ObjectOf({ + id: is.String, + name: is.String, + displayName: is.String, + iconFileId: is.String, + bot: is.Boolean, + state: isUserAccountState, + updatedAt: is.String, +}); + +export const isMessage: Predicate = is.ObjectOf({ + // traq.Message + id: is.String, + userId: is.String, + channelId: is.String, + content: is.String, + createdAt: is.String, + updatedAt: is.String, + pinned: is.Boolean, + stamps: is.ArrayOf(isMessageStamp), + threadId: is.UnionOf([is.String, is.Null]), + + user: isUser, + position: is.OptionalOf(is.ObjectOf({ + index: is.Number, + start: is.Number, + end: is.Number, + })), + // quote?: Message[]; + quote: is.OptionalOf(is.ArrayOf((x: unknown): x is Message => isMessage(x))), +});