File size: 2,296 Bytes
4e23b01
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import { z } from 'zod';

import { parseActionSuffix } from './action-suffix';

export interface ActionHandler<TExtra> {
  readonly body?: z.ZodTypeAny;
  handle(ctx: TExtra & { readonly id: string; readonly body: unknown }): Promise<void> | void;
}

export type ActionTable<TAction extends string, TExtra> = Readonly<
  Record<TAction, ActionHandler<TExtra>>
>;

export function actionNames<TAction extends string, TExtra>(
  actions: ActionTable<TAction, TExtra>,
): readonly TAction[] {
  return Object.keys(actions) as unknown as readonly TAction[];
}

export function resolveActionTarget<TAction extends string, TExtra>(opts: {
  readonly tail: string;
  readonly actions: ActionTable<TAction, TExtra>;
  readonly resourceLabel: string;
}): { readonly id: string; readonly action: TAction } | { readonly message: string } {
  const parsed = parseActionSuffix({
    tail: opts.tail,
    allowedActions: actionNames(opts.actions),
    resourceLabel: opts.resourceLabel,
  });
  if (parsed.kind !== 'action') {
    return {
      message: parsed.kind === 'invalid' ? parsed.reason : `unsupported action: ${opts.tail}`,
    };
  }
  return { id: parsed.id, action: parsed.action };
}

export async function runAction<TAction extends string, TExtra>(opts: {
  readonly action: string;
  readonly id: string;
  readonly actions: ActionTable<TAction, TExtra>;
  readonly extra: TExtra;
  readonly body?: unknown;
}): Promise<boolean> {
  const entry = opts.actions[opts.action as TAction];
  if (entry === undefined) {
    return false;
  }
  const body = entry.body === undefined ? opts.body : entry.body.parse(opts.body);
  await entry.handle({ ...opts.extra, id: opts.id, body });
  return true;
}

export async function dispatchAction<TAction extends string, TExtra>(opts: {
  readonly tail: string;
  readonly actions: ActionTable<TAction, TExtra>;
  readonly resourceLabel: string;
  readonly extra: TExtra;
  readonly body?: unknown;
  readonly onUnsupported: (message: string) => void;
}): Promise<boolean> {
  const target = resolveActionTarget(opts);
  if ('message' in target) {
    opts.onUnsupported(target.message);
    return false;
  }
  return runAction({
    action: target.action,
    id: target.id,
    actions: opts.actions,
    extra: opts.extra,
    body: opts.body,
  });
}