feat: types organization and cleaning up code base

This commit is contained in:
Jacob Nguyen
2023-05-09 22:49:29 -05:00
parent cd1568ff69
commit 8a537d670b
39 changed files with 591 additions and 536 deletions

View File

@@ -19,17 +19,4 @@ export interface ErrorHandling {
*/
updateAlive(error: Error): void;
}
/**
* @since 2.0.0
*/
export class DefaultErrorHandling implements ErrorHandling {
keepAlive = 5;
crash(error: Error): never {
throw error;
}
updateAlive(_: Error) {
this.keepAlive--;
}
}

View File

@@ -1,3 +1,3 @@
export { type ErrorHandling, DefaultErrorHandling } from './error-handling';
export { type Logging, DefaultLogging } from './logging';
export { type ModuleManager, DefaultModuleManager } from './module-manager';
export { type ErrorHandling } from './error-handling';
export type { Logging, LogPayload } from './logging';
export { type ModuleManager } from './module-manager';

View File

@@ -1,4 +1,3 @@
import type { LogPayload } from '../../types/core';
/**
* @since 2.0.0
*/
@@ -8,24 +7,5 @@ export interface Logging<T = unknown> {
info(payload: LogPayload<T>): void;
debug(payload: LogPayload<T>): void;
}
/**
* @since 2.0.0
*/
export class DefaultLogging implements Logging {
private date = () => new Date();
debug(payload: LogPayload): void {
console.debug(`DEBUG: ${this.date().toISOString()} -> ${payload.message}`);
}
error(payload: LogPayload): void {
console.error(`ERROR: ${this.date().toISOString()} -> ${payload.message}`);
}
info(payload: LogPayload): void {
console.info(`INFO: ${this.date().toISOString()} -> ${payload.message}`);
}
warning(payload: LogPayload): void {
console.warn(`WARN: ${this.date().toISOString()} -> ${payload.message}`);
}
}
export type LogPayload<T = unknown> = { message: T };

View File

@@ -1,6 +1,5 @@
import { ModuleStore } from '../../types/core';
import { CommandModule } from '../../types/module';
import { importModule } from '../module-loading';
import { CommandModule } from "../types/modules";
/**
* @since 2.0.0
*/
@@ -10,30 +9,4 @@ export interface ModuleManager {
getPublishableCommands(): Promise<CommandModule[]>;
remove(id: string) : boolean
}
/**
* @since 2.0.0
*/
export class DefaultModuleManager implements ModuleManager {
constructor(private moduleStore: ModuleStore) {}
remove(id: string): boolean {
throw new Error('Method not implemented.');
}
get(id: string) {
return this.moduleStore.get(id);
}
set(id: string, path: string): void {
this.moduleStore.set(id, path);
}
//not tested
getPublishableCommands(): Promise<CommandModule[]> {
const entries = this.moduleStore.entries();
const publishable = 0b000000110;
return Promise.all(
Array.from(entries)
.filter(([id]) => (Number.parseInt(id.at(-1)!) & publishable) !== 0)
.map(([, path]) => importModule<CommandModule>(path)),
);
}
}

View File

@@ -1,5 +1,5 @@
import { CommandType, EventType, PluginType } from './structures';
import type { Plugin, PluginResult, EventArgs, CommandArgs } from '../types/plugin';
import type { Plugin, PluginResult, EventArgs, CommandArgs } from './types/plugins';
import type { ClientEvents } from 'discord.js';
export function makePlugin<V extends unknown[]>(

View File

@@ -1,152 +0,0 @@
import { Container } from 'iti';
import type { Dependencies, DependencyConfiguration, MapDeps, Wrapper } from '../types/core';
import { DefaultErrorHandling, DefaultLogging, DefaultModuleManager } from './contracts';
import { SernEmitter } from './structures';
import { SernError } from './structures/errors';
import * as assert from 'node:assert'
import * as types from 'node:util/types'
import { Awaitable } from '../types/handler';
export let containerSubject: Container<{}, {}>;
const requiredDependencyKeys = ['@sern/emitter', '@sern/errors', '@sern/logger'] as const;
/**
* @__PURE__
* @since 2.0.0.
* use single if you want a singleton, or an object that is called once.
* @param cb
*/
export function single<T>(cb: () => T) {
return cb;
}
/**
* @__PURE__
* @since 2.0.0
* Following iti's singleton and transient implementation,
* use transient if you want a new dependency every time your container getter is called
* @param cb
*/
export function transient<T>(cb: () => () => T) {
return cb;
}
/**
* Given the user's conf, check for any excluded dependency keys.
* Then, call conf.build to get the rest of the users' dependencies.
* Finally, update the containerSubject with the new container state
* @param conf
*/
export async function composeRoot<T extends Dependencies>(conf: DependencyConfiguration<T>) {
//container should have no client or logger yet.
const excludeLogger = conf.exclude?.has('@sern/logger');
if (!excludeLogger) {
containerSubject.add({
'@sern/logger': () => new DefaultLogging(),
});
}
//Build the container based on the callback provided by the user
const updatedContainer = await conf.build(containerSubject as Container<Omit<Dependencies, '@sern/client'>, {}>);
try {
updatedContainer.get('@sern/client');
} catch {
throw new Error(SernError.MissingRequired + " No client was provided")
}
if (!excludeLogger) {
updatedContainer.get('@sern/logger')?.info({ message: 'All dependencies loaded successfully.' });
}
}
export function useContainer<const T extends Dependencies>() {
console.warn(`Warning: using a container hook is not recommended. Could lead to many unwanted side effects`);
return <V extends (keyof T)[]>(...keys: [...V]) =>
keys.map(key => (containerSubject as Container<T, {}>).get(key)) as MapDeps<T, V>;
}
/**
* Returns the underlying data structure holding all dependencies.
* Exposes methods from iti
*/
export function useContainerRaw() {
assert.ok(
containerSubject && (containerSubject as CoreContainer).isReady(),
"Could not find container or container wasn't ready. Did you call makeDependencies?"
);
return containerSubject;
}
/**
* @since 2.0.0
* @param conf a configuration for creating your project dependencies
*/
export async function makeDependencies<const T extends Dependencies>(
conf: DependencyConfiguration<T>,
) {
containerSubject = new CoreContainer();
//Until there are more optional dependencies, just check if the logger exists
await composeRoot(conf);
(containerSubject as CoreContainer).ready();
return useContainer<T>();
}
export interface Init {
init() : Awaitable<unknown>
}
/**
* Provides all the defaults for sern to function properly.
* The only user provided dependency needs to be @sern/client
*/
class CoreContainer extends Container<Dependencies, {}> {
private _ready = false;
constructor() {
super();
(this as Container<{}, {}>)
.add({
'@sern/errors': () => new DefaultErrorHandling(),
'@sern/store': () => new Map<string, string>(),
'@sern/emitter': () => new SernEmitter()
})
.add(ctx => {
return { '@sern/modules': () => new DefaultModuleManager(ctx['@sern/store']) };
})
}
async withInit<const Keys extends keyof Dependencies>(...keys: Keys[]) {
if(this.isReady()) {
throw Error("You cannot call this method after sern has started");
}
for await (const k of keys) {
const dep = this.get(k);
assert.ok(dep !== undefined);
if('init' in dep && typeof dep.init === 'function') {
types.isAsyncFunction(dep.init)
? await dep.init()
: dep.init()
} else {
throw Error(`called withInit with key ${k} but found nothing to init`)
}
}
return this;
}
isReady() {
return this._ready;
}
ready() {
this._ready = true;
}
}
/**
* A way for sern to grab only the necessary dependencies.
* Returns a function which allows for the user to call for more dependencies.
*/
export function makeFetcher<Dep extends Dependencies>(
containerConfig: Wrapper['containerConfig'],
) {
return <const Keys extends (keyof Dep)[]>(otherKeys: [...Keys]) =>
containerConfig.get(
...requiredDependencyKeys,
...(otherKeys as (keyof Dependencies)[]),
) as MapDeps<Dep, [...typeof requiredDependencyKeys, ...Keys]>;
}

View File

@@ -1,6 +1,6 @@
import { Err, Ok } from 'ts-results-es';
import { ApplicationCommandOptionType, AutocompleteInteraction } from 'discord.js';
import type { SernAutocompleteData, SernOptionsData } from '../types/module';
import type { SernAutocompleteData, SernOptionsData } from './types/modules';
//function wrappers for empty ok / err
export const ok = /* @__PURE__*/ () => Ok.EMPTY;

View File

@@ -1,4 +1,34 @@
export * from './contracts';
export * from './create-plugins';
export * from './structures';
export { single, transient, useContainerRaw, makeDependencies } from './dependencies';
export * from './ioc';
export type {
CommandModule,
EventModule,
BothCommand,
ContextMenuMsg,
ContextMenuUser,
SlashCommand,
TextCommand,
ButtonCommand,
StringSelectCommand,
MentionableSelectCommand,
UserSelectCommand,
ChannelSelectCommand,
RoleSelectCommand,
ModalSubmitCommand,
DiscordEventCommand,
SernEventCommand,
ExternalEventCommand,
CommandModuleDefs,
EventModuleDefs,
BaseOptions,
SernAutocompleteData
} from './types/modules';
export type {
Controller,
PluginResult,
InitPlugin,
ControlPlugin,
Plugin
} from './types/plugins';

39
src/core/ioc/base.ts Normal file
View File

@@ -0,0 +1,39 @@
import * as assert from "assert";
import { composeRoot, useContainer } from "./dependency-injection";
import { DependencyConfiguration, Dependencies } from "./types";
import { CoreContainer } from "../structures/container";
//SIDE EFFECT: GLOBAL DI
let containerSubject: CoreContainer<Partial<Dependencies>>;
/**
* Returns the underlying data structure holding all dependencies.
* Exposes methods from iti
*/
export function useContainerRaw() {
assert.ok(
containerSubject && containerSubject.isReady(),
"Could not find container or container wasn't ready. Did you call makeDependencies?"
);
return containerSubject;
}
/**
* @since 2.0.0
* @param conf a configuration for creating your project dependencies
*/
export async function makeDependencies<const T extends Dependencies>(
conf: DependencyConfiguration<T>,
) {
//Until there are more optional dependencies, just check if the logger exists
//SIDE EFFECT
containerSubject = new CoreContainer()
await composeRoot(conf);
//SIDE EFFECT
containerSubject.ready();
return useContainer<T>();
}

View File

@@ -0,0 +1,78 @@
import type { CoreDependencies, Dependencies, DependencyConfiguration, MapDeps, IntoDependencies } from './types';
import { DefaultLogging } from '../structures';
import { SernError } from '../structures/errors';
import { useContainerRaw } from './base';
import { CoreContainer } from '../structures/container';
/**
* @__PURE__
* @since 2.0.0.
* Creates a singleton object.
* @param cb
*/
export function single<T>(cb: () => T) {
return cb;
}
/**
* @__PURE__
* @since 2.0.0
* Creates a transient object
* @param cb
*/
export function transient<T>(cb: () => () => T) {
return cb;
}
export function Service(key: string): unknown
export function Service<T extends keyof Dependencies>(key: T) {
return useContainerRaw().get(key)!
}
export function Services<const T extends (keyof Dependencies)[]>(...keys: [...T]) {
const container = useContainerRaw();
return keys.map(k => container.get(k)!) as IntoDependencies<T>
}
/**
* Given the user's conf, check for any excluded dependency keys.
* Then, call conf.build to get the rest of the users' dependencies.
* Finally, update the containerSubject with the new container state
* @param conf
*/
export async function composeRoot<T extends Dependencies>(conf: DependencyConfiguration<T>) {
//container should have no client or logger yet.
const excludeLogger = conf.exclude?.has('@sern/logger');
const container = useContainerRaw();
if (!excludeLogger) {
container.upsert({
'@sern/logger': () => new DefaultLogging(),
});
}
//Build the container based on the callback provided by the user
const updatedContainer = await conf.build(container as CoreContainer<CoreDependencies>);
try {
updatedContainer.get('@sern/client');
} catch {
throw new Error(SernError.MissingRequired + " No client was provided")
}
if (!excludeLogger) {
updatedContainer.get('@sern/logger')?.info({ message: 'All dependencies loaded successfully.' });
}
}
export function useContainer<const T extends Dependencies>() {
console.warn(`
Warning: using a container hook is not recommended.
Could lead to many unwanted side effects.
Use the new Service(s) api function instead.
`
);
return <V extends (keyof T)[]>(...keys: [...V]) =>
keys.map(key => useContainerRaw().get(key as keyof Dependencies)) as MapDeps<T, V>;
}

3
src/core/ioc/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export { useContainerRaw, makeDependencies } from './base';
export { Service, Services, single, transient } from './dependency-injection';
export type { Singleton, Transient } from './types'

47
src/core/ioc/types.ts Normal file
View File

@@ -0,0 +1,47 @@
import { Container, UnpackFunction } from "iti";
import { Awaitable, ModuleStore } from "../../shared";
import { ErrorHandling, Logging, ModuleManager } from "../contracts";
import { SernEmitter } from "../";
import EventEmitter from "node:events";
export type Singleton<T> = () => T;
export type Transient<T> = () => () => T;
export interface CoreDependencies {
'@sern/logger'?: Singleton<Logging>;
'@sern/emitter': Singleton<SernEmitter>;
'@sern/store': Singleton<ModuleStore>;
'@sern/modules': Singleton<ModuleManager>;
'@sern/errors': Singleton<ErrorHandling>;
}
export interface Dependencies extends CoreDependencies {
'@sern/client': Singleton<EventEmitter>;
}
export type DependencyFromKey<T extends keyof Dependencies> = Dependencies[T];
export type IntoDependencies<Tuple extends [...any[]]> = {
[Index in keyof Tuple]: UnpackFunction<DependencyFromKey<Tuple[Index]>&{}>; //Unpack and make NonNullable
} & { length: Tuple['length'] };
export interface DependencyConfiguration<T extends Dependencies> {
//@deprecated. Loggers will always be included in the future
exclude?: Set<'@sern/logger'>;
build: (root: Container<CoreDependencies, {}>) => Awaitable<Container<T, {}>>;
}
//To be removed in future
//prettier-ignore
export type MapDeps<Deps extends Dependencies, T extends readonly unknown[]> = T extends [
infer First extends keyof Deps,
...infer Rest extends readonly unknown[],
]
? [
UnpackFunction<Deps[First]>,
...(MapDeps<Deps, Rest> extends [never] ? [] : MapDeps<Deps, Rest>),
]
: [never];

View File

@@ -1,14 +1,15 @@
import { SernError } from './structures/errors';
import { type Result, Err, Ok } from 'ts-results-es';
import { Processed } from '../types/core';
import { Module } from '../types/module';
import { Module } from './types/modules';
import * as assert from 'node:assert';
import util from 'node:util';
import { type Observable, from, mergeMap, ObservableInput } from 'rxjs';
import { readdir, stat } from 'fs/promises';
import { basename, join, resolve } from 'path';
import { Processed } from '../handler/types';
export type ModuleResult<T> = Promise<Result<Processed<T>, SernError>>;
export async function importModule<T>(absPath: string) {
/// #if MODE === 'esm'
return import(absPath).then(i => i.default as T);

View File

@@ -17,12 +17,12 @@ import {
share,
switchMap,
} from 'rxjs';
import type { PluginResult, VoidResult } from '../types/plugin';
import { Result } from 'ts-results-es';
import { Awaitable } from '../types/handler';
import { EventEmitter } from 'node:events';
import { ErrorHandling, Logging } from './contracts';
import util from 'node:util'
import { Awaitable } from '../shared';
import { PluginResult, VoidResult } from './types/plugins';
/**
* if {src} is true, mapTo V, else ignore
* @param item

View File

@@ -0,0 +1,45 @@
import { Container } from "iti";
import { DefaultErrorHandling, DefaultModuleManager, SernEmitter } from "../";
import { isAsyncFunction} from "node:util/types";
import * as assert from 'node:assert'
import { Dependencies } from "../ioc/types";
/**
* Provides all the defaults for sern to function properly.
* The only user provided dependency needs to be @sern/client
*/
export class CoreContainer<T extends Partial<Dependencies>> extends Container<T, {}> {
private _ready = false;
constructor() {
super();
(this as Container<{}, {}>)
.add({
'@sern/errors': () => new DefaultErrorHandling(),
'@sern/emitter': () => new SernEmitter(),
'@sern/modules': () => new DefaultModuleManager(new Map())
})
}
async withInit<const Keys extends keyof Dependencies>(...keys: Keys[]) {
if(this.isReady()) {
throw Error("You cannot call this method after sern has started");
}
for await (const k of keys) {
const dep = this.get(k);
assert.ok(dep !== undefined);
if('init' in dep && typeof dep.init === 'function') {
isAsyncFunction(dep.init)
? await dep.init()
: dep.init()
} else {
throw Error(`called withInit with key ${k} but found nothing to init`)
}
}
return this;
}
isReady() {
return this._ready;
}
ready() {
this._ready = true;
}
}

View File

@@ -10,8 +10,8 @@ import {
} from 'discord.js';
import { CoreContext } from './core-context';
import { Result, Ok, Err } from 'ts-results-es';
import { ReplyOptions } from '../../types/handler';
import * as assert from 'assert';
import { ReplyOptions } from '../../shared';
/**
* @since 1.0.0

View File

@@ -1,3 +1,4 @@
export * from './enums';
export * from './context';
export * from './sern-emitter';
export * from './services'

View File

@@ -1,7 +1,7 @@
import { EventEmitter } from 'node:events';
import type { Payload, SernEventsMapping } from '../../types/handler';
import { PayloadType } from '../../core/structures';
import type { Module } from '../../types/module';
import { Payload, SernEventsMapping } from '../../shared';
import { Module } from '../types/modules';
/**
* @since 1.0.0

View File

@@ -0,0 +1,14 @@
import { ErrorHandling } from "../../contracts";
/**
* @since 2.0.0
*/
export class DefaultErrorHandling implements ErrorHandling {
keepAlive = 5;
crash(error: Error): never {
throw error;
}
updateAlive(_: Error) {
this.keepAlive--;
}
}

View File

@@ -0,0 +1,3 @@
export * from './error-handling';
export * from './logger';
export * from './module-manager';

View File

@@ -0,0 +1,23 @@
import { LogPayload, Logging } from "../../contracts";
/**
* @since 2.0.0
*/
export class DefaultLogging implements Logging {
private date = () => new Date();
debug(payload: LogPayload): void {
console.debug(`DEBUG: ${this.date().toISOString()} -> ${payload.message}`);
}
error(payload: LogPayload): void {
console.error(`ERROR: ${this.date().toISOString()} -> ${payload.message}`);
}
info(payload: LogPayload): void {
console.info(`INFO: ${this.date().toISOString()} -> ${payload.message}`);
}
warning(payload: LogPayload): void {
console.warn(`WARN: ${this.date().toISOString()} -> ${payload.message}`);
}
}

View File

@@ -0,0 +1,32 @@
import { ModuleStore } from "../../../shared";
import { ModuleManager } from "../../contracts";
import { importModule } from "../../module-loading";
import { CommandModule } from "../../types/modules";
/**
* @since 2.0.0
*/
export class DefaultModuleManager implements ModuleManager {
constructor(private moduleStore: ModuleStore) {}
remove(id: string): boolean {
throw new Error('Method not implemented.');
}
get(id: string) {
return this.moduleStore.get(id);
}
set(id: string, path: string): void {
this.moduleStore.set(id, path);
}
//not tested
getPublishableCommands(): Promise<CommandModule[]> {
const entries = this.moduleStore.entries();
const publishable = 0b000000110;
return Promise.all(
Array.from(entries)
.filter(([id]) => (Number.parseInt(id.at(-1)!) & publishable) !== 0)
.map(([, path]) => importModule<CommandModule>(path)),
);
}
}

236
src/core/types/modules.ts Normal file
View File

@@ -0,0 +1,236 @@
import type {
ApplicationCommandAttachmentOption,
ApplicationCommandChannelOptionData,
ApplicationCommandChoicesData,
ApplicationCommandNonOptionsData,
ApplicationCommandNumericOptionData,
ApplicationCommandOptionData,
ApplicationCommandOptionType,
ApplicationCommandSubCommandData,
ApplicationCommandSubGroupData,
BaseApplicationCommandOptionsData,
} from 'discord.js';
import {
AutocompleteInteraction,
ButtonInteraction,
ChannelSelectMenuInteraction,
ClientEvents,
MentionableSelectMenuInteraction,
MessageContextMenuCommandInteraction,
ModalSubmitInteraction,
RoleSelectMenuInteraction,
StringSelectMenuInteraction,
UserContextMenuCommandInteraction,
UserSelectMenuInteraction,
} from 'discord.js';
import { CommandType, Context, EventType } from '../structures';
import { AnyCommandPlugin, AnyEventPlugin, ControlPlugin, InitPlugin } from './plugins';
import { Awaitable, SernEventsMapping } from '../../shared';
import { sernMeta } from '../../handler/commands';
import { Processed } from '../../handler/types';
import { Args, SlashOptions } from '../../shared';
interface CommandMeta {
fullPath: string;
id: string;
}
export type AnyDefinedModule = Processed<CommandModule | EventModule>;
export interface Module {
type: CommandType | EventType;
name?: string;
onEvent: ControlPlugin[];
plugins: InitPlugin[];
description?: string;
[sernMeta]: CommandMeta;
execute: (...args: any[]) => Awaitable<any>;
}
export interface SernEventCommand<T extends keyof SernEventsMapping = keyof SernEventsMapping>
extends Module {
name?: T;
type: EventType.Sern;
execute(...args: SernEventsMapping[T]): Awaitable<unknown>;
}
export interface ExternalEventCommand extends Module {
name?: string;
emitter: string;
type: EventType.External;
execute(...args: unknown[]): Awaitable<unknown>;
}
export interface ContextMenuUser extends Module {
type: CommandType.CtxUser;
execute: (ctx: UserContextMenuCommandInteraction) => Awaitable<unknown>;
}
export interface ContextMenuMsg extends Module {
type: CommandType.CtxMsg;
execute: (ctx: MessageContextMenuCommandInteraction) => Awaitable<unknown>;
}
export interface ButtonCommand extends Module {
type: CommandType.Button;
execute: (ctx: ButtonInteraction) => Awaitable<unknown>;
}
export interface StringSelectCommand extends Module {
type: CommandType.StringSelect;
execute: (ctx: StringSelectMenuInteraction) => Awaitable<unknown>;
}
export interface ChannelSelectCommand extends Module {
type: CommandType.ChannelSelect;
execute: (ctx: ChannelSelectMenuInteraction) => Awaitable<unknown>;
}
export interface RoleSelectCommand extends Module {
type: CommandType.RoleSelect;
execute: (ctx: RoleSelectMenuInteraction) => Awaitable<unknown>;
}
export interface MentionableSelectCommand extends Module {
type: CommandType.MentionableSelect;
execute: (ctx: MentionableSelectMenuInteraction) => Awaitable<unknown>;
}
export interface UserSelectCommand extends Module {
type: CommandType.UserSelect;
execute: (ctx: UserSelectMenuInteraction) => Awaitable<unknown>;
}
export interface ModalSubmitCommand extends Module {
type: CommandType.Modal;
execute: (ctx: ModalSubmitInteraction) => Awaitable<unknown>;
}
export interface AutocompleteCommand
extends Omit<Module, 'name' | 'type' | 'plugins' | 'description' | typeof sernMeta> {
onEvent: ControlPlugin[];
execute: (ctx: AutocompleteInteraction) => Awaitable<unknown>;
}
export interface DiscordEventCommand<T extends keyof ClientEvents = keyof ClientEvents>
extends Module {
name?: T;
type: EventType.Discord;
execute(...args: ClientEvents[T]): Awaitable<unknown>;
}
export interface TextCommand extends Module {
type: CommandType.Text;
alias?: string[];
execute: (ctx: Context, args: ['text', string[]]) => Awaitable<unknown>;
}
export interface SlashCommand extends Module {
type: CommandType.Slash;
description: string;
options?: SernOptionsData[];
execute: (ctx: Context, args: ['slash', SlashOptions]) => Awaitable<unknown>;
}
export interface BothCommand extends Module {
type: CommandType.Both;
alias?: string[];
description: string;
options?: SernOptionsData[];
execute: (ctx: Context, args: Args) => Awaitable<unknown>;
}
export type EventModule = DiscordEventCommand | SernEventCommand | ExternalEventCommand;
export type CommandModule =
| TextCommand
| SlashCommand
| BothCommand
| ContextMenuUser
| ContextMenuMsg
| ButtonCommand
| StringSelectCommand
| MentionableSelectCommand
| UserSelectCommand
| ChannelSelectCommand
| RoleSelectCommand
| ModalSubmitCommand;
export type AnyModule = CommandModule | EventModule;
//https://stackoverflow.com/questions/64092736/alternative-to-switch-statement-for-typescript-discriminated-union
// Explicit Module Definitions for mapping
export interface CommandModuleDefs {
[CommandType.Text]: TextCommand;
[CommandType.Slash]: SlashCommand;
[CommandType.Both]: BothCommand;
[CommandType.CtxMsg]: ContextMenuMsg;
[CommandType.CtxUser]: ContextMenuUser;
[CommandType.Button]: ButtonCommand;
[CommandType.StringSelect]: StringSelectCommand;
[CommandType.RoleSelect]: RoleSelectCommand;
[CommandType.ChannelSelect]: ChannelSelectCommand;
[CommandType.MentionableSelect]: MentionableSelectCommand;
[CommandType.UserSelect]: UserSelectCommand;
[CommandType.Modal]: ModalSubmitCommand;
}
export interface EventModuleDefs {
[EventType.Sern]: SernEventCommand;
[EventType.Discord]: DiscordEventCommand;
[EventType.External]: ExternalEventCommand;
}
export interface SernAutocompleteData
extends Omit<BaseApplicationCommandOptionsData, 'autocomplete'> {
autocomplete: true;
type:
| ApplicationCommandOptionType.String
| ApplicationCommandOptionType.Number
| ApplicationCommandOptionType.Integer;
command: AutocompleteCommand;
}
export type CommandModuleNoPlugins = {
[T in CommandType]: Omit<CommandModuleDefs[T], 'plugins' | 'onEvent' | typeof sernMeta>;
};
export type EventModulesNoPlugins = {
[T in EventType]: Omit<EventModuleDefs[T], 'plugins' | 'onEvent' | typeof sernMeta>;
};
export type InputEvent = {
[T in EventType]: EventModulesNoPlugins[T] & { plugins?: AnyEventPlugin[] };
}[EventType];
export type InputCommand = {
[T in CommandType]: CommandModuleNoPlugins[T] & { plugins?: AnyCommandPlugin[] };
}[CommandType];
/**
* Type that replaces autocomplete with {@link SernAutocompleteData}
*/
export type BaseOptions =
| ApplicationCommandChoicesData
| ApplicationCommandNonOptionsData
| ApplicationCommandChannelOptionData
| ApplicationCommandNumericOptionData
| ApplicationCommandAttachmentOption
| SernAutocompleteData;
export interface SernSubCommandData extends BaseApplicationCommandOptionsData {
type: ApplicationCommandOptionType.Subcommand;
required?: never;
options?: BaseOptions[];
}
export interface SernSubCommandGroupData extends BaseApplicationCommandOptionsData {
type: ApplicationCommandOptionType.SubcommandGroup;
required?: never;
options?: SernSubCommandData[];
}
export type SernOptionsData<U extends ApplicationCommandOptionData = ApplicationCommandOptionData> =
U extends ApplicationCommandSubCommandData
? SernSubCommandData
: U extends ApplicationCommandSubGroupData
? SernSubCommandGroupData
: BaseOptions;

119
src/core/types/plugins.ts Normal file
View File

@@ -0,0 +1,119 @@
/*
* Plugins can be inserted on all commands and are emitted
*
* 1. On ready event, where all commands are loaded.
* 2. On corresponding observable (when command triggers)
*
* The goal of plugins is to organize commands and
* provide extensions to repetitive patterns
* examples include refreshing modules,
* categorizing commands, cool-downs, permissions, etc.
* Plugins are reminiscent of middleware in express.
*/
import type { Err, Ok, Result } from 'ts-results-es';
import type { BothCommand, ButtonCommand, ChannelSelectCommand, CommandModule, ContextMenuMsg, ContextMenuUser, DiscordEventCommand, EventModule, ExternalEventCommand, MentionableSelectCommand, ModalSubmitCommand, RoleSelectCommand, SernEventCommand, SlashCommand, StringSelectCommand, TextCommand, UserSelectCommand } from './modules';
import { Args, Awaitable, Payload, SlashOptions } from '../../shared';
import { CommandType, Context, EventType, PluginType } from '../structures';
import { InitArgs, Processed } from '../../handler/types';
import { ButtonInteraction, ChannelSelectMenuInteraction, ClientEvents, MentionableSelectMenuInteraction, MessageContextMenuCommandInteraction, ModalSubmitInteraction, RoleSelectMenuInteraction, StringSelectMenuInteraction, UserContextMenuCommandInteraction, UserSelectMenuInteraction } from 'discord.js';
export type PluginResult = Awaitable<VoidResult>;
export type VoidResult = Result<void, void>;
export interface Controller {
next: () => Ok<void>;
stop: () => Err<void>;
}
export interface Plugin<Args extends any[] = any[]> {
type: PluginType;
execute: (...args: Args) => PluginResult;
}
export interface InitPlugin<Args extends any[] = any[]> {
type: PluginType.Init;
execute: (...args: Args) => PluginResult;
}
export interface ControlPlugin<Args extends any[] = any[]> {
type: PluginType.Control;
execute: (...args: Args) => PluginResult;
}
export type AnyCommandPlugin = ControlPlugin | InitPlugin<[InitArgs<Processed<CommandModule>>]>;
export type AnyEventPlugin = ControlPlugin | InitPlugin<[InitArgs<Processed<EventModule>>]>;
export type CommandArgs<
I extends CommandType = CommandType,
J extends PluginType = PluginType,
> = CommandArgsMatrix[I][J];
export type EventArgs<
I extends EventType = EventType,
J extends PluginType = PluginType,
> = EventArgsMatrix[I][J];
export interface CommandArgsMatrix {
[CommandType.Text]: {
[PluginType.Control]: [Context, ['text', string[]]];
[PluginType.Init]: [InitArgs<Processed<TextCommand>>];
};
[CommandType.Slash]: {
[PluginType.Control]: [Context, ['slash', /* library coupled */ SlashOptions]];
[PluginType.Init]: [InitArgs<Processed<SlashCommand>>];
};
[CommandType.Both]: {
[PluginType.Control]: [Context, Args];
[PluginType.Init]: [InitArgs<Processed<BothCommand>>];
};
[CommandType.CtxMsg]: {
[PluginType.Control]: [/* library coupled */ MessageContextMenuCommandInteraction];
[PluginType.Init]: [InitArgs<Processed<ContextMenuMsg>>];
};
[CommandType.CtxUser]: {
[PluginType.Control]: [/* library coupled */ UserContextMenuCommandInteraction];
[PluginType.Init]: [InitArgs<Processed<ContextMenuUser>>];
};
[CommandType.Button]: {
[PluginType.Control]: [/* library coupled */ ButtonInteraction];
[PluginType.Init]: [InitArgs<Processed<ButtonCommand>>];
};
[CommandType.StringSelect]: {
[PluginType.Control]: [/* library coupled */ StringSelectMenuInteraction];
[PluginType.Init]: [InitArgs<Processed<StringSelectCommand>>];
};
[CommandType.RoleSelect]: {
[PluginType.Control]: [/* library coupled */ RoleSelectMenuInteraction];
[PluginType.Init]: [InitArgs<Processed<RoleSelectCommand>>];
};
[CommandType.ChannelSelect]: {
[PluginType.Control]: [/* library coupled */ ChannelSelectMenuInteraction];
[PluginType.Init]: [InitArgs<Processed<ChannelSelectCommand>>];
};
[CommandType.MentionableSelect]: {
[PluginType.Control]: [/* library coupled */ MentionableSelectMenuInteraction];
[PluginType.Init]: [InitArgs<Processed<MentionableSelectCommand>>];
};
[CommandType.UserSelect]: {
[PluginType.Control]: [/* library coupled */ UserSelectMenuInteraction];
[PluginType.Init]: [InitArgs<Processed<UserSelectCommand>>];
};
[CommandType.Modal]: {
[PluginType.Control]: [/* library coupled */ ModalSubmitInteraction];
[PluginType.Init]: [InitArgs<Processed<ModalSubmitCommand>>];
};
}
export interface EventArgsMatrix {
[EventType.Discord]: {
[PluginType.Control]: /* library coupled */ ClientEvents[keyof ClientEvents];
[PluginType.Init]: [InitArgs<Processed<DiscordEventCommand>>];
};
[EventType.Sern]: {
[PluginType.Control]: [Payload];
[PluginType.Init]: [InitArgs<Processed<SernEventCommand>>];
};
[EventType.External]: {
[PluginType.Control]: unknown[];
[PluginType.Init]: [InitArgs<Processed<ExternalEventCommand>>];
};
}