From 9ec5e65f38b356f934abffd2eeb4ec25d5a9cf33 Mon Sep 17 00:00:00 2001 From: ayntee Date: Sun, 14 Mar 2021 19:50:22 +0400 Subject: [PATCH] feat(types/util): add case utility types --- src/types/README.md | 166 ------------------------------------------- src/types/options.ts | 22 +++--- src/types/util.ts | 131 +++++++++++++++++++++++++++++++--- 3 files changed, 135 insertions(+), 184 deletions(-) delete mode 100644 src/types/README.md diff --git a/src/types/README.md b/src/types/README.md deleted file mode 100644 index 37a5133e4..000000000 --- a/src/types/README.md +++ /dev/null @@ -1,166 +0,0 @@ -# Discordeno Typings Guidelines / Explanations - -Discordeno has a certain standard guidelines for our typings. Please follow this -as best as possible when contributing to the library. - -1. Discordeno Types - -These types will be specifically used by the end user for functions inside -Discordeno. - -Example: - -```ts -interface EditMemberOptions { - /** Value to set users nickname to. Requires MANAGE_NICKNAMES permission. */ - nick?: string; - /** Array of role ids the member will have after this edit. Useful for adding/removing multiple roles in 1 API call. Requires MANAGE_ROLES permission. */ - roles?: string[]; - /** Whether the user is muted in voice channels. Requires MUTE_MEMBERS permission. */ - mute?: boolean; - /** Whether the user is deafened in voice channels. Requires DEAFEN_MEMBERS permission. */ - deaf?: boolean; - /** The id of the channel to move user to if they are connected to voice. To kick the user from their current channel, set to null. Requires MOVE_MEMBERS permission. When moving members to channels, must have permissions to both CONNECT to the channel and have the MOVE_MEMBER permission. */ - channelID?: string; -} -``` - -Rules: - -- Camel Case -- Do not allow `null` -- Everything should have a comment explaining it -- These typings should be kept in the file with the function. - - Example: EditMemberOptions is at the bottom of the file where editMember() - is declared. - -2. Discord Types Incoming - -These types are meant for the payloads that we receive from Discord, whether -through gateway or REST. - -Example: - -```ts -export interface MemberCreatePayload { - /** The user this guild member represents */ - user: UserPayload; - /** The user's guild nickname if one is set. */ - nick?: string; - /** Array of role ids that the member has */ - roles: string[]; - /** When the user joined the guild. */ - joined_at: string; - /** When the user used their nitro boost on the server. */ - premium_since?: string; - /** Whether the user is deafened in voice channels */ - deaf: boolean; - /** Whether the user is muted in voice channels */ - mute: boolean; - /** Whether the user has passed the guild's Membership Screening requirements */ - pending?: boolean; -} -``` - -Rules: - -- Snake case (Or whatever discord uses. Everything here should be letter to - letter in accordance with discord's docs.) -- Everything should have a comment explaining it -- Kept in the src/types/api/incoming folder - -3. Discord Types Outgoing - -These types are meant **US** as we develop Discordeno. These will help us -prevent bugs when we are sending payloads to Discord whether through gateway or -REST. - -Example: - -```ts -export interface EditMemberPayload { - /** Value to set users nickname to. Requires MANAGE_NICKNAMES permission. */ - nick?: string; - /** Array of role ids the member is assigned. Requires MANAGE_ROLES permission. */ - roles?: string[]; - /** Whether the user is muted in voice channels. Requires MUTE_MEMBERS permission. */ - mute?: boolean; - /** Whether the user is deafened in voice channels. Requires DEAFEN_MEMBERS permission. */ - deaf?: boolean; - /** The id of the channel to move user to if they are connected to voice. To kick the user from their current channel, set to null. Requires MOVE_MEMBERS permission. When moving members to channels, must have permissions to both CONNECT to the channel and have the MOVE_MEMBER permission. */ - channel_id?: string | null; -} -``` - -Rules: - -- Snake case (Or whatever discord uses. Everything here should be letter to - letter in accordance with discord's docs.) -- Everything should have a comment explaining it -- Kept in the src/types/api/outgoing folder - -## Minimalistic - -Since Discord uses `snake_case` but we use `camelCase` we will end up with -redundant typings. In order to solve this, we have the `Camelize` type which -helps create a type using the snake case. - -If we have the base payload for Discord's `User` as follows: - -```ts -export interface DiscordUserPayload { - /** The user's id */ - id: string; - /** the user's username, not unique across the platform */ - username: string; - /** The user's 4 digit discord tag */ - discriminator: string; - /** The user's avatar hash */ - avatar: string | null; - /** Whether the user is a bot */ - bot?: boolean; - /** Whether the user is an official discord system user (part of the urgent message system.) */ - system?: boolean; - /** Whether the user has two factor enabled on their account */ - "mfa_enabled"?: boolean; - /** the user's chosen language option */ - locale?: string; - /** Whether the email on this account has been verified */ - verified?: boolean; - /** The user's email */ - email?: string; - /** The flags on a user's account. */ - flags?: number; - /** The type of Nitro subscription on a user's account. */ - premium_type?: number; -} -``` - -To create the Discordeno version for this it would be done as: - -```ts -export interface UserPayload extends Camelize {} -``` - -Now we have 2 unique interfaces, without having all the extra work or headaches -of maintaing 2 different interfaces. - -Similarily, for any outgoing Discord types, we can also camelize them to have -better user experience for users. - -Example: - -```ts -export interface DiscordBanOptions { - /** number of days to delete messages for (0-7) */ - delete_message_days?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; - /** The reason for the ban. */ - reason?: string; -} -``` - -To create the Discordeno version for this it would be done as: - -```ts -export interface BanOptions extends Camelize {} -``` diff --git a/src/types/options.ts b/src/types/options.ts index fd964abed..bef4acfb6 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 { CamelCase } 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: CamelCase, ) => unknown; /** Sent when a Slash Command relevant to the current user is updated. */ applicationCommandUpdate?: ( - data: Camelize, + data: CamelCase, ) => unknown; /** Sent when a Slash Command relevant to the current user is deleted. */ applicationCommandDelete?: ( - data: Camelize, + data: CamelCase, ) => unknown; /** Sent when properties about the user change. */ botUpdate?: (user: UserPayload) => unknown; @@ -242,15 +242,19 @@ 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: CamelCase, + ) => unknown; /** Sent when an integration is updated. */ - integrationUpdate?: (data: Camelize) => unknown; + integrationUpdate?: ( + data: CamelCase, + ) => unknown; /** Sent when an integration is deleted. */ - integrationDelete?: (data: Camelize) => undefined; + integrationDelete?: (data: CamelCase) => undefined; /** Sent when a new invite to a channel is created. */ - inviteCreate?: (data: Camelize) => unknown; + inviteCreate?: (data: CamelCase) => unknown; /** Sent when an invite is deleted. */ - inviteDelete?: (data: Camelize) => unknown; + inviteDelete?: (data: CamelCase) => 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..21fa269a9 100644 --- a/src/types/util.ts +++ b/src/types/util.ts @@ -1,10 +1,123 @@ -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 CamelCasedProps = { + [K in keyof T as CamelCase]: T[K]; +}; + +export type SnakeCasedProps = { + [K in keyof T as SnakeCase]: T[K]; +};