refactor: move things to core, imports not fixed yet

This commit is contained in:
Jacob Nguyen
2023-04-12 13:08:45 -05:00
parent 21febd2c90
commit 58b3d85da8
20 changed files with 10 additions and 1 deletions

View File

@@ -0,0 +1,50 @@
import type { Observable } from 'rxjs';
import type { Logging } from './logging';
import util from 'util';
/**
* @since 2.0.0
*/
export interface ErrorHandling {
/**
* Number of times the process should throw an error until crashing and exiting
*/
keepAlive: number;
/**
* Utility function to crash
* @param error
*/
crash(error: Error): never;
/**
* A function that is called on every crash. Updates keepAlive
* @param error
*/
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--;
}
}
export function handleError<C>(crashHandler: ErrorHandling, logging?: Logging) {
return (pload: unknown, caught: Observable<C>) => {
// This is done to fit the ErrorHandling contract
const err = pload instanceof Error ? pload : Error(util.format(pload));
if (crashHandler.keepAlive == 0) {
crashHandler.crash(err);
}
//formatted payload
logging?.error({ message: util.format(pload) });
crashHandler.updateAlive(err);
return caught;
};
}

View File

@@ -0,0 +1,3 @@
export { ErrorHandling, DefaultErrorHandling } from './errorHandling';
export { Logging, DefaultLogging } from './logging';
export { ModuleManager, DefaultModuleManager } from './moduleManager';

View File

@@ -0,0 +1,31 @@
import type { LogPayload } from '../../types/handler';
/**
* @since 2.0.0
*/
export interface Logging<T = unknown> {
error(payload: LogPayload<T>): void;
warning(payload: LogPayload<T>): void;
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}`);
}
}

View File

@@ -0,0 +1,27 @@
import type { CommandModuleDefs } from '../../types/module';
import type { CommandType, ModuleStore } from '../structures';
import type { Processed } from '../../types/handler';
/**
* @since 2.0.0
*/
export interface ModuleManager {
get<T extends CommandType>(
strat: (ms: ModuleStore) => Processed<CommandModuleDefs[T]> | undefined,
): Processed<CommandModuleDefs[T]> | undefined;
set(strat: (ms: ModuleStore) => void): void;
}
/**
* @since 2.0.0
*/
export class DefaultModuleManager implements ModuleManager {
constructor(private moduleStore: ModuleStore) {}
get<T extends CommandType>(
strat: (ms: ModuleStore) => Processed<CommandModuleDefs[T]> | undefined,
) {
return strat(this.moduleStore);
}
set(strat: (ms: ModuleStore) => void): void {
strat(this.moduleStore);
}
}

View File

@@ -0,0 +1,2 @@
export { single, transient, many } from './lifetimeFunctions';
export { useContainerRaw } from './provider';

View File

@@ -0,0 +1,63 @@
import { _const } from '../utilities/functions';
type NotFunction =
| string
| number
| boolean
| null
| undefined
| bigint
| readonly any[]
| { apply?: never; [k: string]: any }
| { call?: never; [k: string]: any };
/**
* @deprecated
* @param cb
*/
export function single<T extends NotFunction>(cb: T): () => T;
/**
* New signature
* @since 2.0.0
* @param cb
*/
export function single<T extends () => unknown>(cb: T): T;
/**
* @__PURE__
* @since 2.0.0.
* Please note that on intellij, the deprecation is for all signatures, which is unintended behavior (and
* very annoying).
* For future versions, ensure that single is being passed as a **callback!!**
* @param cb
*/
export function single<T>(cb: T) {
if (typeof cb === 'function') return cb;
return () => cb;
}
/**
* @deprecated
* @param cb
* Deprecated signature
*/
export function transient<T extends NotFunction>(cb: T): () => () => T;
export function transient<T extends () => () => unknown>(cb: T): T;
/**
* @__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) | T) {
if (typeof cb !== 'function') return () => () => cb;
return cb;
}
/**
* @__PURE__
* @deprecated
* @param value
* Please use the transient function instead
*/
// prettier-ignore
export const many = <T>(value: T) => () => _const(value);

View File

@@ -0,0 +1,86 @@
import type { Container } from 'iti';
import type { Dependencies, DependencyConfiguration, MapDeps } from '../../types/handler';
import SernEmitter from '../sernEmitter';
import { DefaultErrorHandling, DefaultLogging, DefaultModuleManager } from '../contracts';
import { Result } from 'ts-results-es';
import { BehaviorSubject } from 'rxjs';
import { createContainer } from 'iti';
import { type Wrapper, ModuleStore, SernError } from '../structures';
export const containerSubject = new BehaviorSubject(defaultContainer());
/**
* 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 function composeRoot<T extends Dependencies>(conf: DependencyConfiguration<T>) {
//Get the current container. This should have no client or possible logger yet.
const currentContainer = containerSubject.getValue();
const excludeLogger = conf.exclude?.has('@sern/logger');
if (!excludeLogger) {
currentContainer.add({
'@sern/logger': () => new DefaultLogging(),
});
}
//Build the container based on the callback provided by the user
const container = conf.build(currentContainer);
//Check if the built container contains @sern/client or throw
// a runtime exception
Result.wrap(() => container.get('@sern/client')).expect(SernError.MissingRequired);
if (!excludeLogger) {
container.get('@sern/logger')?.info({ message: 'All dependencies loaded successfully.' });
}
//I'm sorry little one
containerSubject.next(container as any);
}
export function useContainer<T extends Dependencies>() {
const container = containerSubject.getValue() as Container<T, {}>;
return <V extends (keyof T)[]>(...keys: [...V]) =>
keys.map(key => Result.wrap(() => container.get(key)).unwrapOr(undefined)) as MapDeps<T, V>;
}
/**
* Returns the underlying data structure holding all dependencies.
* Please be careful as this only gets the client's current state.
* Exposes some methods from iti
*/
export function useContainerRaw<T extends Dependencies>() {
return containerSubject.getValue() as Container<T, {}>;
}
/**
* Provides all the defaults for sern to function properly.
* The only user provided dependency needs to be @sern/client
*/
function defaultContainer() {
return createContainer()
.add({ '@sern/errors': () => new DefaultErrorHandling() })
.add({ '@sern/store': () => new ModuleStore() })
.add(ctx => {
return {
'@sern/modules': () => new DefaultModuleManager(ctx['@sern/store']),
};
})
.add({ '@sern/emitter': () => new SernEmitter() }) as Container<
Omit<Dependencies, '@sern/client' | '@sern/logger'>,
{}
>;
}
export function makeFetcher(wrapper: Wrapper) {
const requiredDependencyKeys = [
'@sern/emitter',
'@sern/client',
'@sern/errors',
'@sern/logger',
] as ['@sern/emitter', '@sern/client', '@sern/errors', '@sern/logger'];
return <Keys extends (keyof Dependencies)[]>(otherKeys: [...Keys]) =>
wrapper.containerConfig.get(...requiredDependencyKeys, ...otherKeys) as MapDeps<
Dependencies,
[...typeof requiredDependencyKeys, ...Keys]
>;
}

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

@@ -0,0 +1,3 @@
import SernEmitter from './sernEmitter'
export { SernEmitter };

View File

@@ -0,0 +1,64 @@
import { readdirSync, statSync } from 'fs';
import { join } from 'path';
import { type Observable, from, mergeMap } from 'rxjs';
import { SernError } from '../structures/errors';
import { type Result, Err, Ok } from 'ts-results-es';
import { ImportPayload } from '../../types/handler';
import { pathToFileURL } from 'node:url';
// Courtesy @Townsy45
function readPath(dir: string, arrayOfFiles: string[] = []): string[] {
try {
const files = readdirSync(dir);
for (const file of files) {
if (statSync(dir + '/' + file).isDirectory()) readPath(dir + '/' + file, arrayOfFiles);
else arrayOfFiles.push(join(dir, '/', file));
}
} catch (err) {
throw err;
}
return arrayOfFiles;
}
export const fmtFileName = (n: string) => n.substring(0, n.length - 3);
// export const isLazy = (n: string) => n.indexOf(".lazy.", n.length-9) !== -1;
export async function defaultModuleLoader<T>(
absPath: string,
): Promise<Result<ImportPayload<T>, SernError>> {
// prettier-ignore
let module: T | undefined
/// #if MODE === 'esm'
= (await import(pathToFileURL(absPath).toString())).default
/// #elif MODE === 'cjs'
= require(absPath).default; // eslint-disable-line
/// #endif
if (module === undefined) {
return Err(SernError.UndefinedModule);
}
try {
module = new (module as unknown as new () => T)();
} catch {}
return Ok({ module, absPath });
}
/**
* a directory string is converted into a stream of modules.
* starts the stream of modules that sern needs to process on init
* @returns {Observable<{ mod: Module; absPath: string; }[]>} data from command files
* @param commandDir
*/
export function buildModuleStream<T>(
commandDir: string,
): Observable<Result<ImportPayload<T>, SernError>> {
const commands = getCommands(commandDir);
return from(commands).pipe(mergeMap(defaultModuleLoader<T>));
}
export function fullPathFrom(dir: string) {
return join(process.cwd(), dir);
}
export function getCommands(dir: string): string[] {
return readPath(fullPathFrom(dir));
}

117
src/core/plugins/args.ts Normal file
View File

@@ -0,0 +1,117 @@
import type { CommandType } from '../structures/enums';
import type { PluginType } from '../structures/enums';
import type { ClientEvents } from 'discord.js';
import type {
BothCommand,
ButtonCommand,
ChannelSelectCommand,
ContextMenuUser,
DiscordEventCommand,
ExternalEventCommand,
MentionableSelectCommand,
ModalSubmitCommand,
RoleSelectCommand,
SernEventCommand,
SlashCommand,
StringSelectCommand,
TextCommand,
UserSelectCommand,
ContextMenuMsg,
Module,
} from '../../types/module';
import type { Args, Payload, Processed, SlashOptions } from '../../types/handler';
import type Context from '../structures/context';
import type { MessageContextMenuCommandInteraction } from 'discord.js';
import type {
ButtonInteraction,
RoleSelectMenuInteraction,
StringSelectMenuInteraction,
UserContextMenuCommandInteraction,
} from 'discord.js';
import type {
ChannelSelectMenuInteraction,
MentionableSelectMenuInteraction,
ModalSubmitInteraction,
UserSelectMenuInteraction,
} from 'discord.js';
import { EventType } from '../structures/enums';
type 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>>];
};
};
type 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>>];
};
};
export interface InitArgs<T extends Processed<Module>> {
module: T;
absPath: string;
}
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];

View File

@@ -0,0 +1,64 @@
import { CommandType, EventType, PluginType } from '../structures';
import type { Plugin, PluginResult } from '../../types/plugin';
import type { CommandArgs, EventArgs } from './args';
import type { ClientEvents } from 'discord.js';
export const guayin = Symbol('twice<3');
export function makePlugin<V extends unknown[]>(
type: PluginType,
execute: (...args: any[]) => any,
): Plugin<V> {
return {
type,
execute,
[guayin]: undefined,
} as Plugin<V>;
}
/**
* @since 2.5.0
*
*/
export function EventInitPlugin<I extends EventType>(
execute: (...args: EventArgs<I, PluginType.Init>) => PluginResult,
) {
return makePlugin(PluginType.Init, execute);
}
/**
* @since 2.5.0
*
*/
export function CommandInitPlugin<I extends CommandType>(
execute: (...args: CommandArgs<I, PluginType.Init>) => PluginResult,
) {
return makePlugin(PluginType.Init, execute);
}
/**
* @since 2.5.0
*
*/
export function CommandControlPlugin<I extends CommandType>(
execute: (...args: CommandArgs<I, PluginType.Control>) => PluginResult,
) {
return makePlugin(PluginType.Control, execute);
}
/**
* @since 2.5.0
*
*/
export function EventControlPlugin<I extends EventType>(
execute: (...args: EventArgs<I, PluginType.Control>) => PluginResult,
) {
return makePlugin(PluginType.Control, execute);
}
/**
* @since 2.5.0
* @Experimental
* A specialized function for creating control plugins with discord.js ClientEvents.
* Will probably be moved one day!
*/
export function DiscordEventControlPlugin<T extends keyof ClientEvents>(
name: T,
execute: (...args: ClientEvents[T]) => PluginResult,
) {
return makePlugin(PluginType.Control, execute);
}

View File

@@ -0,0 +1,2 @@
export { EventArgs, InitArgs, CommandArgs } from './args';
export * from './createPlugin';

88
src/core/sernEmitter.ts Normal file
View File

@@ -0,0 +1,88 @@
import { EventEmitter } from 'events';
import type { Payload, SernEventsMapping } from '../types/handler';
import { PayloadType } from '../handler/structures';
import type { Module } from '../types/module';
/**
* @since 1.0.0
*/
class SernEmitter extends EventEmitter {
/**
* Listening to sern events with on. This event stays on until a crash or a normal exit
* @param eventName
* @param listener what to do with the data
*/
public override on<T extends keyof SernEventsMapping>(
eventName: T,
listener: (...args: SernEventsMapping[T][]) => void,
): this {
return super.on(eventName, listener);
}
/**
* Listening to sern events with on. This event stays on until a crash or a normal exit
* @param eventName
* @param listener what to do with the data
*/
public override once<T extends keyof SernEventsMapping>(
eventName: T,
listener: (...args: SernEventsMapping[T][]) => void,
): this {
return super.once(eventName, listener);
}
/**
* Listening to sern events with on. This event stays on until a crash or a normal exit
* @param eventName
* @param args the arguments for emitting the eventName
*/
public override emit<T extends keyof SernEventsMapping>(
eventName: T,
...args: SernEventsMapping[T]
): boolean {
return super.emit(eventName, ...args);
}
private static payload<T extends Payload>(
type: PayloadType,
module?: Module,
reason?: unknown,
) {
return { type, module, reason } as T;
}
/**
* Creates a compliant SernEmitter failure payload
* @param module
* @param reason
*/
static failure(module?: Module, reason?: unknown) {
//The generic cast Payload & { type : PayloadType.* } coerces the type to be a failure payload
// same goes to the other methods below
return SernEmitter.payload<Payload & { type: PayloadType.Failure }>(
PayloadType.Failure,
module,
reason,
);
}
/**
* Creates a compliant SernEmitter module success payload
* @param module
*/
static success(module: Module) {
return SernEmitter.payload<Payload & { type: PayloadType.Success }>(
PayloadType.Success,
module,
);
}
/**
* Creates a compliant SernEmitter module warning payload
* @param reason
*/
static warning(reason: unknown) {
return SernEmitter.payload<Payload & { type: PayloadType.Warning }>(
PayloadType.Warning,
undefined,
reason,
);
}
}
export default SernEmitter;

View File

@@ -0,0 +1,140 @@
/**
* @since 1.0.0
* A bitfield that discriminates command modules
* @enum { number }
* @example
* ```ts
* export default commandModule({
* // highlight-next-line
* type : CommandType.Text,
* name : 'a text command'
* execute(message) {
* console.log(message.content)
* }
* })
* ```
*/
export enum CommandType {
/**
* The CommandType for text commands
*/
Text = 1,
/**
* The CommandType for slash commands
*/
Slash = 2,
/**
* The CommandType for hybrid commands, text and slash
*/
Both = 3,
/**
* The CommandType for UserContextMenuInteraction commands
*/
CtxUser = 4,
/**
* The CommandType for MessageContextMenuInteraction commands
*/
CtxMsg = 8,
/**
* The CommandType for ButtonInteraction commands
*/
Button = 16,
/**
* The CommandType for StringSelectMenuInteraction commands
*/
StringSelect = 32,
/**
* The CommandType for ModalSubmitInteraction commands
*/
Modal = 64,
/**
* The CommandType for the other SelectMenuInteractions
*/
ChannelSelect = 256,
MentionableSelect = 512,
RoleSelect = 1024,
UserSelect = 2048,
}
/**
* A bitfield that discriminates event modules
* @enum { number }
* @example
* ```ts
* export default eventModule({
* //highlight-next-line
* type : EventType.Discord,
* name : 'guildMemberAdd'
* execute(member : GuildMember) {
* console.log(member)
* }
* })
* ```
*/
export enum EventType {
/**
* The EventType for handling discord events
*/
Discord = 1,
/**
* The EventType for handling sern events
*/
Sern = 2,
/**
* The EventType for handling external events.
* Could be for example, `process` events, database events
*/
External = 3,
}
/**
* A bitfield that discriminates plugins
* @enum { number }
* @example
* ```ts
* export default function myPlugin() : EventPlugin<CommandType.Text> {
* //highlight-next-line
* type : PluginType.Event,
* execute([ctx, args], controller) {
* return controller.next();
* }
* }
* ```
*/
export enum PluginType {
/**
* The PluginType for InitPlugins
*/
Init = 1,
/**
* @deprecated
* Use PluginType.Init instead
*/
Command = 1,
/**
* @deprecated
* Use PluginType.Control instead
*/
Event = 2,
/**
* The PluginType for EventPlugins
*/
Control = 2,
}
/**
* @enum { string }
*/
export enum PayloadType {
/**
* The PayloadType for a SernEmitter success event
*/
Success = 'success',
/**
* The PayloadType for a SernEmitter failure event
*/
Failure = 'failure',
/**
* The PayloadType for a SernEmitter warning event
*/
Warning = 'warning',
}

View File

@@ -0,0 +1,38 @@
/**
* @enum { string }
*/
export enum SernError {
/**
* Throws when registering an invalid module.
* This means it is undefined or an invalid command type was provided
*/
InvalidModuleType = 'Detected an unknown module type',
/**
* Attempted to lookup module in command module store. Nothing was found!
*/
UndefinedModule = `A module could not be detected`,
/**
* Attempted to lookup module in command module store. Nothing was found!
*/
MismatchModule = `A module type mismatched with event emitted!`,
/**
* Unsupported interaction at this moment.
*/
NotSupportedInteraction = `This interaction is not supported.`,
/**
* One plugin called `controller.stop()` (end command execution / loading)
*/
PluginFailure = `A plugin failed to call controller.next()`,
/**
* A crash that occurs when accessing an invalid property of Context
*/
MismatchEvent = `You cannot use message when an interaction fired or vice versa`,
/**
* Unsupported feature attempted to access at this time
*/
NotSupportedYet = `This feature is not supported yet`,
/**
* Required Dependency not found
*/
MissingRequired = `@sern/client is required but was not found`,
}

View File

@@ -0,0 +1,6 @@
import Context from './context';
import type Wrapper from './wrapper';
import { ModuleStore } from './moduleStore';
export * from './errors';
export * from './enums';
export { Context, Wrapper, ModuleStore };

View File

@@ -0,0 +1,27 @@
import type { CommandModule } from '../../types/module';
import { ApplicationCommandType, ComponentType } from 'discord.js';
import type { Processed } from '../../types/handler';
/**
* @since 2.0.0
* Storing all command modules
* This dependency is usually injected into ModuleManager
*/
export class ModuleStore {
readonly BothCommands = new Map<string, Processed<CommandModule>>();
readonly ApplicationCommands = {
[ApplicationCommandType.User]: new Map<string, Processed<CommandModule>>(),
[ApplicationCommandType.Message]: new Map<string, Processed<CommandModule>>(),
[ApplicationCommandType.ChatInput]: new Map<string, Processed<CommandModule>>(),
};
readonly ModalSubmit = new Map<string, Processed<CommandModule>>();
readonly TextCommands = new Map<string, Processed<CommandModule>>();
readonly InteractionHandlers = {
[ComponentType.Button]: new Map<string, Processed<CommandModule>>(),
[ComponentType.StringSelect]: new Map<string, Processed<CommandModule>>(),
[ComponentType.ChannelSelect]: new Map<string, Processed<CommandModule>>(),
[ComponentType.MentionableSelect]: new Map<string, Processed<CommandModule>>(),
[ComponentType.RoleSelect]: new Map<string, Processed<CommandModule>>(),
[ComponentType.UserSelect]: new Map<string, Processed<CommandModule>>(),
};
}

View File

@@ -0,0 +1,16 @@
import type { Dependencies } from '../../types/handler';
/**
* @since 1.0.0
* An object to be passed into Sern#init() function.
* @typedef {object} Wrapper
*/
interface Wrapper {
readonly defaultPrefix?: string;
readonly commands: string;
readonly events?: string;
readonly containerConfig: {
get: (...keys: (keyof Dependencies)[]) => unknown[];
};
}
export default Wrapper;

View File

@@ -0,0 +1,36 @@
import * as Files from '../module-loading/readFile';
import { basename } from 'path';
import { Err, Ok } from 'ts-results-es';
/**
* A function that returns whatever value is provided.
* Warning: this evaluates { @param value }. It does not defer a value.
* @param value
* @__PURE__
*/
// prettier-ignore
export const _const = <T>(value: T) => () => value;
/**
*
* @param modName
* @param absPath
*/
export function nameOrFilename(modName: string | undefined, absPath: string) {
return modName ?? Files.fmtFileName(basename(absPath));
}
//function wrappers for empty ok / err
export const ok = _const(Ok.EMPTY);
export const err = _const(Err.EMPTY);
export function partition<T, V>(arr: (T & V)[], condition: (e: T & V) => boolean): [T[], V[]] {
const t: T[] = [];
const v: V[] = [];
for (const el of arr) {
if (condition(el)) {
t.push(el as T);
} else {
v.push(el as V);
}
}
return [t, v];
}

View File

@@ -0,0 +1,47 @@
import { ApplicationCommandOptionType, AutocompleteInteraction } from 'discord.js';
import type { SernAutocompleteData, SernOptionsData } from '../../types/module';
/**
* Uses an iterative DFS to check if an autocomplete node exists
* @param iAutocomplete
* @param options
*/
export default function treeSearch(
iAutocomplete: AutocompleteInteraction,
options: SernOptionsData[] | undefined,
): SernAutocompleteData | undefined {
if (options === undefined) return undefined;
const _options = options.slice(); // required to prevent direct mutation of options
let autocompleteData: SernAutocompleteData | undefined;
while (_options.length > 0) {
const cur = _options.pop()!;
switch (cur.type) {
case ApplicationCommandOptionType.Subcommand:
{
for (const option of cur.options ?? []) {
_options.push(option);
}
}
break;
case ApplicationCommandOptionType.SubcommandGroup:
{
for (const command of cur.options ?? []) {
_options.push(command);
}
}
break;
default:
{
if (cur.autocomplete) {
const choice = iAutocomplete.options.getFocused(true);
if (cur.name === choice.name && cur.autocomplete) {
autocompleteData = cur;
}
}
}
break;
}
}
return autocompleteData;
}