Skip to content

Commit

Permalink
feat: Fully featured types for logic processors
Browse files Browse the repository at this point in the history
  • Loading branch information
eric-crowell committed Aug 3, 2024
1 parent 8481547 commit 57e1e6d
Show file tree
Hide file tree
Showing 9 changed files with 116 additions and 86 deletions.
2 changes: 1 addition & 1 deletion packages/action/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export * as actions from '@do-ob/action/actions';
export * from '@do-ob/action/actions';

export async function schema($id: string = 'action.json') {
const definitions = await import('@do-ob/action/schema');
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/io/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export function inputify<
A extends Action<string, unknown>,
>(
input: Partial<Input<A>>,
): Input<A> {
) {
return {
client: input.client || 0,
$dispatch: input.$dispatch || nanoid(),
Expand All @@ -93,5 +93,5 @@ export function inputify<
headers: {},
pathname: '/',
...input,
};
} satisfies Input<A>;
}
25 changes: 15 additions & 10 deletions packages/core/src/io/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,20 @@ export type Output<
> = {
status: OutputStatus;

payload: never;
} | {

status: OutputStatus.Success;

payload: Payload;

} | {
status: OutputStatus.Failure;

payload: OutputFailure;
};

/**
* Constructs a new output object with placeholder values.
*/
export function outputify<
P = unknown,
>(
output: Partial<Output<P>>,
) {
return {
status: OutputStatus.Success,
payload: undefined as P,
...output,
} satisfies Output<P>;
}
7 changes: 7 additions & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,10 @@
* Extracts the arguments from a function type.
*/
export type Arguments<F extends (...args: any) => any> = F extends (...args: infer A) => any ? A : never;

/**
* Converts an array of objects with a key property to a record mapped by that key value.
*/
export type ArrayToRecord<T extends { key: string }[]> = {
[K in T[number]['key']]: Extract<T[number], { key: K }>
};
6 changes: 2 additions & 4 deletions packages/logic/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { logic } from '@do-ob/logic/logic';
export * from '@do-ob/logic/process';
export * from '@do-ob/logic/logic';

export {
logic
};
29 changes: 21 additions & 8 deletions packages/logic/src/logic.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { test, expect } from 'vitest';
import { logic } from '@do-ob/logic/logic';
import { register } from '@do-ob/action';
import { register, locale_define } from '@do-ob/action';
import { contextify } from '@do-ob/core';
import { processify } from './process';

Expand All @@ -14,21 +14,34 @@ test('should be able to use a custom process', async () => {

const context = contextify({});

const processTest = processify(
'account',
const processAccount = processify(
context,
[ register, async () => {
return {
status: 1,
payload: {
username: 'test'
}
username: 'test'
};
} ],
[ locale_define, async () => {
return {
code: 'en-US'
};
} ]
);

const processSubscription = processify(
context,
[ register, async () => {
return {
subscription: 'hello_world'
};
} ],
);

const { dispatch } = logic({
processes: [ processTest ]
pool: {
account: processAccount,
subscription: processSubscription,
}
});

expect(dispatch).toBeDefined();
Expand Down
46 changes: 25 additions & 21 deletions packages/logic/src/logic.ts
Original file line number Diff line number Diff line change
@@ -1,61 +1,65 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

import { inputify, type Action } from '@do-ob/core/io';
import { inputify, type Action } from '@do-ob/core';
import type { Process } from './process';

// type ArrayToRecord<T extends Process[]> = {
// [K in T[number]['key']]: Extract<T[number], { key: K }>
// };

export interface LogicDispatchOptions {
client?: () => number;
headers?: () => Record<string, string>;
}

export interface Logic<P extends Process> {
dispatch: <A extends Action<string, unknown>, I extends keyof P['_infer_output']>(
action: A,
options?: LogicDispatchOptions,
) => Promise<
{ [K in P['key']]: A['type'] extends I ? P['_infer_output'][I] : never }
>;
}
// export interface Logic<P extends Process> {
// dispatch: <A extends Action<string, unknown>, I extends keyof P['_']['output']>(
// action: A,
// options?: LogicDispatchOptions,
// ) => Promise<
// { [K in P['key']]: A['type'] extends I ? P['_']['output'][I] : never }
// >;
// }

export type LogicHandlerKeys<
A extends Action<string, unknown>,
P extends Process
> = { [K in keyof P['_infer_output']]: K extends A['type'] ? true : never };
> = { [K in keyof P['_']['output']]: K extends A['type'] ? true : never };

/**
* Configuration for the logic function wrapper generation.
*/
export interface LogicOptions<
P extends Process,
T extends Record<string, Process>
> {
/**
* List processes used to hande action dispatches.
*/
processes?: P[];
pool?: T;
}

/**
* Generates a logic function wrapper for the given schema.
*/
export function logic<
P extends Process,
>(options: LogicOptions<P> = {}): Logic<P> {
T extends Record<string, P>
>(options: LogicOptions<T> = {}) {

const {
processes = [],
pool = {},
} = options;

const processKeys = processes.map((process) => process.key);
const poolKeys = Object.keys(pool) as Array<keyof T>;
const processes = Object.values(pool) as Array<P>;


return {
dispatch: async <
A extends Action<string, unknown>,
I extends keyof P['_infer_output']
>(
action: A,
options: LogicDispatchOptions = {},
): Promise<
{ [K in P['key']]: A['type'] extends I ? P['_infer_output'][I] : never }
{ [K in keyof T]: A['type'] extends keyof T[K]['_']['output'] ? T[K]['_']['output'][A['type']] : never }
> => {

const input = inputify({
Expand All @@ -73,9 +77,9 @@ export function logic<
if (!result) {
return acc;
}
acc[processKeys[index] as P['key']] = result;
acc[poolKeys[index] as keyof T] = result;
return acc;
}, {} as Record<P['key'], any>);
}, {} as Record<keyof T, any>);
}
};

Expand Down
13 changes: 7 additions & 6 deletions packages/logic/src/process.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { test, expect, assert } from 'vitest';
import { processify } from '@do-ob/logic/process';
import { database, adapter as dataAdapter } from '@do-ob/data';
import { register } from '@do-ob/action';
import { register, locale_define } from '@do-ob/action';
import { inputify, OutputStatus } from '@do-ob/core/io';
import { contextify } from '@do-ob/core';

Expand All @@ -13,14 +13,15 @@ const context = contextify({

test('should create a process, add a handler, and execute', async () => {
const process = processify(
'test',
context,
[ register, async (_) => {
return {
status: OutputStatus.Success,
payload: {
username: 'test'
}
username: 'test'
};
} ],
[ locale_define, async (_) => {
return {
locale: 'us'
};
} ],
);
Expand Down
70 changes: 36 additions & 34 deletions packages/logic/src/process.ts
Original file line number Diff line number Diff line change
@@ -1,61 +1,63 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { Action, ActionResult, Output, Context, ActionModule, Input } from '@do-ob/core/io';

import type { Action, Context, ActionModule, Input, Output } from '@do-ob/core';
import { outputify } from '@do-ob/core/io';
// import { OutputStatus, OutputFailureType } from '@do-ob/core/io';
// export type Process<C extends Context, A extends Action<string, unknown> = Action<string, unknown>> = (context: C, input: Input<A>) => Promise<Output>;

// export type ProcessHandlers = Record<string, Process<Context>>;

export type ProcessHandler<
M extends ActionModule,
C extends Context,
R
> = (input: Input<ReturnType<M['act']>>, adapter: C['adapter']) => Promise<Output<R>>;

export interface Process<
K extends string = string,
// C extends Context = Context,
C extends Context = Context,
M extends ActionModule = ActionModule,
R = unknown
> = (input: Input<ReturnType<M['act']>>, adapter: C['adapter']) => Promise<R>;

export interface Process<
L extends [ActionModule, any][] = [ActionModule, any][],
> {
key: K;
_infer_output: Record<M['type'], Output<R>>;
execute: <A extends Action<string, unknown>>(input: Input<A>) => Promise<A['type'] extends M['type'] ? Output<ActionResult<A>> : undefined>;
_: {
output: { [ K in L[number][0]['type']]: Output<Awaited<
ReturnType<Extract<L[number], [{ type: K }, ProcessHandler ]>[1]>
>> };
}
execute: <A extends Action<string, unknown>>(input: Input<A>) => Promise<A['type'] extends L[number][0]['type'] ?
Output<Awaited<ReturnType<Extract<L[number], [{ type: A['type'] }, ProcessHandler ]>[1]>>> :
undefined
>;
}

/**
* Creates a new logic processing function to handle actions and return results.
*/
export function processify<
K extends string,
C extends Context,
M extends ActionModule,
R
H extends ProcessHandler<C>,
L extends [ActionModule, H][],
>(
/**
* The key name of the process.
* This is used to identify the process in the logic system.
*/
key: K,
context: C,
...handler: [M, ProcessHandler<M, C, R>][]
): Process<K, M, R> {
const handlers: Record<M['type'], ProcessHandler<M, C, R>> = handler.reduce((acc, [ module, handler ]) => {
acc[module.type as M['type']] = handler;
...handler: L
): Process<L> {
const handlers: Record<string, ProcessHandler<C>> = handler.reduce((acc, [ module, handle ]) => {
acc[module.type] = handle;
return acc;
}, {} as Record<M['type'], ProcessHandler<M, C, R>>);
}, {} as Record<string, ProcessHandler<C>>);

return {
key,
execute: async <
A extends Action<string, unknown>,
>(input: Input<A>): Promise<A['type'] extends M['type'] ? Output<ActionResult<A>> : undefined> => {
const handler = handlers[input.action.type as M['type']];
const process: Process<L> = {
execute: async (input) => {
const handler = handlers[input.action.type];
if (!handler) {
return undefined as any;
}

return handler(input as Input<ReturnType<M['act']>>, context.adapter) as Promise<Output<ActionResult<A>>> as any;
const payload = await handler(input, context.adapter);

return outputify({
status: 1,
payload,
}) as any;
},
} as Process<K, M, R>;
} as Process<L>;

return process;

}

0 comments on commit 57e1e6d

Please sign in to comment.