From 10cb9d7eb941e72a074c7851b1a48f9869b371aa Mon Sep 17 00:00:00 2001 From: ayntee Date: Tue, 23 Mar 2021 19:04:46 +0400 Subject: [PATCH] types(util): add case utility types (#683) * types(util): add case utility types * Rename *ID to *Id --- .github/CONTRIBUTING.md | 4 +- .../commands/APPLICATION_COMMAND_CREATE.ts | 8 +- .../commands/APPLICATION_COMMAND_DELETE.ts | 8 +- .../commands/APPLICATION_COMMAND_UPDATE.ts | 8 +- .../integrations/INTEGRATION_CREATE.ts | 8 +- .../integrations/INTEGRATION_DELETE.ts | 8 +- .../integrations/INTEGRATION_UPDATE.ts | 8 +- src/handlers/invites/INVITE_CREATE.ts | 8 +- src/types/options.ts | 24 ++-- src/types/util.ts | 125 ++++++++++++++++-- 10 files changed, 161 insertions(+), 48 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index b01dc0335..9715e5d60 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -66,9 +66,9 @@ Examples of bad PR title: Example: ```ts -// Discordeno has utility type Camelize, where T is an interface with keys in snake case. +// Discordeno has utility type CamelCaseProps, where T is an interface with keys in snake case. // It can be used to "generate" corresponding "Discordeno type" from "Discord type". -// Example: export type BanOptions = Camelize +// Example: export type BanOptions = CamelCaseProps export interface EditMemberOptions { /** Value to set users nickname to. Requires MANAGE_NICKNAMES permission. */ diff --git a/src/handlers/commands/APPLICATION_COMMAND_CREATE.ts b/src/handlers/commands/APPLICATION_COMMAND_CREATE.ts index d60db03a9..e4d31e341 100644 --- a/src/handlers/commands/APPLICATION_COMMAND_CREATE.ts +++ b/src/handlers/commands/APPLICATION_COMMAND_CREATE.ts @@ -5,14 +5,14 @@ export function handleApplicationCommandCreate( data: DiscordPayload, ) { const { - guild_id: guildID, - application_id: applicationID, + guild_id: guildId, + application_id: applicationId, ...rest } = data.d as ApplicationCommandEvent; eventHandlers.applicationCommandCreate?.({ ...rest, - guildID, - applicationID, + guildId, + applicationId, }); } diff --git a/src/handlers/commands/APPLICATION_COMMAND_DELETE.ts b/src/handlers/commands/APPLICATION_COMMAND_DELETE.ts index 6636003e1..b5486d093 100644 --- a/src/handlers/commands/APPLICATION_COMMAND_DELETE.ts +++ b/src/handlers/commands/APPLICATION_COMMAND_DELETE.ts @@ -3,14 +3,14 @@ import { ApplicationCommandEvent, DiscordPayload } from "../../types/mod.ts"; export function handleApplicationCommandDelete(data: DiscordPayload) { const { - application_id: applicationID, - guild_id: guildID, + application_id: applicationId, + guild_id: guildId, ...rest } = data.d as ApplicationCommandEvent; eventHandlers.applicationCommandDelete?.({ ...rest, - guildID, - applicationID, + guildId, + applicationId, }); } diff --git a/src/handlers/commands/APPLICATION_COMMAND_UPDATE.ts b/src/handlers/commands/APPLICATION_COMMAND_UPDATE.ts index 5ac3fbde3..35f0302a7 100644 --- a/src/handlers/commands/APPLICATION_COMMAND_UPDATE.ts +++ b/src/handlers/commands/APPLICATION_COMMAND_UPDATE.ts @@ -3,14 +3,14 @@ import { ApplicationCommandEvent, DiscordPayload } from "../../types/mod.ts"; export function handleApplicationCommandUpdate(data: DiscordPayload) { const { - application_id: applicationID, - guild_id: guildID, + application_id: applicationId, + guild_id: guildId, ...rest } = data.d as ApplicationCommandEvent; eventHandlers.applicationCommandUpdate?.({ ...rest, - guildID, - applicationID, + guildId, + applicationId, }); } diff --git a/src/handlers/integrations/INTEGRATION_CREATE.ts b/src/handlers/integrations/INTEGRATION_CREATE.ts index 7987653fd..2709ee275 100644 --- a/src/handlers/integrations/INTEGRATION_CREATE.ts +++ b/src/handlers/integrations/INTEGRATION_CREATE.ts @@ -8,24 +8,24 @@ export function handleIntegrationCreate( data: DiscordPayload, ) { const { - guild_id: guildID, + guild_id: guildId, enable_emoticons: enableEmoticons, expire_behavior: expireBehavior, expire_grace_period: expireGracePeriod, subscriber_count: subscriberCount, - role_id: roleID, + role_id: roleId, synced_at: syncedAt, ...rest } = data.d as IntegrationCreateUpdateEvent; eventHandlers.integrationCreate?.({ ...rest, - guildID, + guildId, enableEmoticons, expireBehavior, expireGracePeriod, syncedAt, subscriberCount, - roleID, + roleId, }); } diff --git a/src/handlers/integrations/INTEGRATION_DELETE.ts b/src/handlers/integrations/INTEGRATION_DELETE.ts index 6b5d0fc74..4586d03de 100644 --- a/src/handlers/integrations/INTEGRATION_DELETE.ts +++ b/src/handlers/integrations/INTEGRATION_DELETE.ts @@ -3,14 +3,14 @@ import { DiscordPayload, IntegrationDeleteEvent } from "../../types/mod.ts"; export function handleIntegrationDelete(data: DiscordPayload) { const { - guild_id: guildID, - application_id: applicationID, + guild_id: guildId, + application_id: applicationId, ...rest } = data.d as IntegrationDeleteEvent; eventHandlers.integrationDelete?.({ ...rest, - applicationID, - guildID, + applicationId, + guildId, }); } diff --git a/src/handlers/integrations/INTEGRATION_UPDATE.ts b/src/handlers/integrations/INTEGRATION_UPDATE.ts index 896088c17..f5daf44c9 100644 --- a/src/handlers/integrations/INTEGRATION_UPDATE.ts +++ b/src/handlers/integrations/INTEGRATION_UPDATE.ts @@ -9,20 +9,20 @@ export function handleIntegrationUpdate(data: DiscordPayload) { enable_emoticons: enableEmoticons, expire_behavior: expireBehavior, expire_grace_period: expireGracePeriod, - role_id: roleID, + role_id: roleId, subscriber_count: subscriberCount, synced_at: syncedAt, - guild_id: guildID, + guild_id: guildId, ...rest } = data.d as IntegrationCreateUpdateEvent; eventHandlers.integrationUpdate?.({ ...rest, - guildID, + guildId, subscriberCount, enableEmoticons, expireGracePeriod, - roleID, + roleId, expireBehavior, syncedAt, }); diff --git a/src/handlers/invites/INVITE_CREATE.ts b/src/handlers/invites/INVITE_CREATE.ts index 1b343bffa..d0964068e 100644 --- a/src/handlers/invites/INVITE_CREATE.ts +++ b/src/handlers/invites/INVITE_CREATE.ts @@ -5,10 +5,10 @@ export function handleInviteCreate(payload: DiscordPayload) { if (payload.t !== "INVITE_CREATE") return; //TODO: replace with tocamelcase const { - channel_id: channelID, + channel_id: channelId, created_at: createdAt, max_age: maxAge, - guild_id: guildID, + guild_id: guildId, target_user: targetUser, target_user_type: targetUserType, max_uses: maxUses, @@ -17,8 +17,8 @@ export function handleInviteCreate(payload: DiscordPayload) { eventHandlers.inviteCreate?.({ ...rest, - channelID, - guildID, + channelId, + guildId, maxAge, targetUser, targetUserType, diff --git a/src/types/options.ts b/src/types/options.ts index fd964abed..7ceb6ad5a 100644 --- a/src/types/options.ts +++ b/src/types/options.ts @@ -25,7 +25,7 @@ import { PartialMessage, ReactionPayload, } from "./message.ts"; -import { Camelize } from "./util.ts"; +import { CamelCaseProps } from "./util.ts"; export interface BotConfig { token: string; @@ -93,15 +93,15 @@ export interface EventHandlers { rateLimit?: (data: RateLimitData) => unknown; /** Sent when a new Slash Command is created, relevant to the current user. */ applicationCommandCreate?: ( - data: Camelize, + data: CamelCaseProps, ) => unknown; /** Sent when a Slash Command relevant to the current user is updated. */ applicationCommandUpdate?: ( - data: Camelize, + data: CamelCaseProps, ) => unknown; /** Sent when a Slash Command relevant to the current user is deleted. */ applicationCommandDelete?: ( - data: Camelize, + data: CamelCaseProps, ) => unknown; /** Sent when properties about the user change. */ botUpdate?: (user: UserPayload) => unknown; @@ -242,15 +242,21 @@ export interface EventHandlers { /** Sent when a member has passed the guild's Membership Screening requirements */ membershipScreeningPassed?: (guild: Guild, member: Member) => unknown; /** Sent when an integration is created on a server such as twitch, youtube etc.. */ - integrationCreate?: (data: Camelize) => unknown; + integrationCreate?: ( + data: CamelCaseProps, + ) => unknown; /** Sent when an integration is updated. */ - integrationUpdate?: (data: Camelize) => unknown; + integrationUpdate?: ( + data: CamelCaseProps, + ) => unknown; /** Sent when an integration is deleted. */ - integrationDelete?: (data: Camelize) => undefined; + integrationDelete?: ( + data: CamelCaseProps, + ) => undefined; /** Sent when a new invite to a channel is created. */ - inviteCreate?: (data: Camelize) => unknown; + inviteCreate?: (data: CamelCaseProps) => unknown; /** Sent when an invite is deleted. */ - inviteDelete?: (data: Camelize) => unknown; + inviteDelete?: (data: CamelCaseProps) => unknown; } /** https://discord.com/developers/docs/topics/gateway#list-of-intents */ diff --git a/src/types/util.ts b/src/types/util.ts index 2a02f1f50..26afb53d9 100644 --- a/src/types/util.ts +++ b/src/types/util.ts @@ -1,10 +1,117 @@ -export type CamelizeString = T extends string - ? string extends T ? string - : T extends `${infer F}_${infer R}` - ? `${F}${T extends `${infer F}_id` ? Uppercase - : Capitalize>}` - : T - : T; +type UpperCaseCharacters = + | "A" + | "B" + | "C" + | "D" + | "E" + | "F" + | "G" + | "H" + | "I" + | "J" + | "K" + | "L" + | "M" + | "N" + | "O" + | "P" + | "Q" + | "R" + | "S" + | "T" + | "U" + | "V" + | "W" + | "X" + | "Y" + | "Z"; -// deno-fmt-ignore -export type Camelize = { [K in keyof T as CamelizeString]: T[K] }; +type WordSeparators = "-" | "_" | " "; + +type SplitIncludingDelimiters< + Source extends string, + Delimiter extends string, +> = Source extends "" ? [] + : Source extends `${infer FirstPart}${Delimiter}${infer SecondPart}` ? ( + Source extends `${FirstPart}${infer UsedDelimiter}${SecondPart}` + ? UsedDelimiter extends Delimiter + ? Source extends `${infer FirstPart}${UsedDelimiter}${infer SecondPart}` + ? [ + ...SplitIncludingDelimiters, + UsedDelimiter, + ...SplitIncludingDelimiters, + ] + : never + : never + : never + ) + : [Source]; +type StringPartToDelimiterCase< + StringPart extends string, + UsedWordSeparators extends string, + UsedUpperCaseCharacters extends string, + Delimiter extends string, +> = StringPart extends UsedWordSeparators ? Delimiter + : StringPart extends UsedUpperCaseCharacters + ? `${Delimiter}${Lowercase}` + : StringPart; +type StringArrayToDelimiterCase< + Parts extends any[], + UsedWordSeparators extends string, + UsedUpperCaseCharacters extends string, + Delimiter extends string, +> = Parts extends [`${infer FirstPart}`, ...infer RemainingParts] + ? `${StringPartToDelimiterCase< + FirstPart, + UsedWordSeparators, + UsedUpperCaseCharacters, + Delimiter + >}${StringArrayToDelimiterCase< + RemainingParts, + UsedWordSeparators, + UsedUpperCaseCharacters, + Delimiter + >}` + : ""; +type DelimiterCase = Value extends string + ? StringArrayToDelimiterCase< + SplitIncludingDelimiters, + WordSeparators, + UpperCaseCharacters, + Delimiter + > + : Value; +type InnerCamelCaseStringArray = + Parts extends [`${infer FirstPart}`, ...infer RemainingParts] + ? FirstPart extends undefined ? "" + : FirstPart extends "" + ? InnerCamelCaseStringArray + : `${PreviousPart extends "" ? FirstPart + : Capitalize}${InnerCamelCaseStringArray< + RemainingParts, + FirstPart + >}` + : ""; +type CamelCaseStringArray = Parts extends + [`${infer FirstPart}`, ...infer RemainingParts] ? Uncapitalize< + `${FirstPart}${InnerCamelCaseStringArray}` +> + : never; +type Split = string extends S ? string[] + : S extends "" ? [] + : S extends `${infer T}${D}${infer U}` ? [T, ...Split] + : [S]; + +export type SnakeCase = DelimiterCase; + +export type CamelCase = K extends string + ? CamelCaseStringArray> + : K; + +export type CamelCaseProps = { + [K in keyof T as CamelCase]: T[K]; +}; + +export type SnakeCaseProps = { + [K in keyof T as SnakeCase]: T[K]; +};