Created
March 25, 2021 18:18
-
-
Save williammartin/f999569e02e692c3697c9cd26c9bfb07 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Helpers | |
// optional | |
type Some<T> = { | |
_tag: "some"; | |
value: T; | |
}; | |
type None = { | |
_tag: "none"; | |
}; | |
type Optional<T> = Some<T> | None; | |
const some = <T>(value: T): Some<T> => ({ _tag: "some", value }); | |
const none: None = { _tag: "none" }; | |
const isSome = <T>(optional: Optional<T>): optional is Some<T> => optional._tag === "some"; | |
const isNone = <T>(optional: Optional<T>): optional is None => optional._tag === "none"; | |
function applyOptional<T, R>(fns: { Some: (value: T) => R; None: () => R }): (optional: Optional<T>) => R { | |
return function (optional: Optional<T>): R { | |
if (optional._tag === "some") { | |
return fns.Some(optional.value); | |
} else if (optional._tag === "none") { | |
return fns.None(); | |
} else { | |
return unreachable(optional); | |
} | |
}; | |
} | |
function matchOptional<T, R>(optional: Optional<T>, fns: { Some: (value: T) => R; None: () => R }): R { | |
return applyOptional(fns)(optional); | |
} | |
// result | |
type Ok<O> = { | |
_tag: "ok"; | |
value: O; | |
}; | |
type Err<E> = { | |
_tag: "err"; | |
value: E; | |
}; | |
type Result<O, E> = Ok<O> | Err<E>; | |
const ok = <O>(value: O): Ok<O> => ({ _tag: "ok", value }); | |
const err = <E>(value: E): Err<E> => ({ _tag: "err", value }); | |
function applyResult<O, E, R>(fns: { Ok: (ok: O) => R; Err: (err: E) => R }): (result: Result<O, E>) => R { | |
return function (result: Result<O, E>): R { | |
if (result._tag === "ok") { | |
return fns.Ok(result.value); | |
} else if (result._tag === "err") { | |
return fns.Err(result.value); | |
} else { | |
return unreachable(result); | |
} | |
}; | |
} | |
function matchResult<O, E, R>( | |
result: Result<O, E>, | |
fns: { | |
Ok: (ok: O) => R; | |
Err: (err: E) => R; | |
} | |
): R { | |
return applyResult(fns)(result); | |
} | |
// patterns | |
const unreachable = (x: never): never => { | |
throw new Error("unreachable encountered value which was supposed to be never"); | |
}; | |
// log | |
const log = (message: string) => console.log(message); | |
const warn = (message: string) => console.warn(message); | |
const error = (message: string) => console.error(message); | |
// pages | |
type PUID = string; | |
type PConstant = { | |
Constant: { | |
display_text: string; | |
value_type: string; | |
value: unknown; | |
}; | |
}; | |
type PExpressionValue = null | [PConstant, unknown]; | |
function applyPExpressionValue<R>(fns: { | |
None: () => R; | |
Some: (value: [PConstant, unknown]) => R; | |
}): (expressionValue: PExpressionValue) => R { | |
return (expressionValue: PExpressionValue) => { | |
if (expressionValue === null) { | |
return fns.None(); | |
} else if (expressionValue !== null) { | |
return fns.Some(expressionValue); | |
} else { | |
// Clearly unreachable right now when there are only two states... | |
return unreachable(expressionValue); | |
} | |
}; | |
} | |
function matchPExpressionValue<R>( | |
expressionValue: PExpressionValue, | |
fns: { | |
None: () => R; | |
Some: (value: [PConstant, unknown]) => R; | |
} | |
): R { | |
return applyPExpressionValue(fns)(expressionValue); | |
} | |
type PServiceParam = { ServiceParam: { depends_on_atom: PUID[]; field_registry_id: PUID } }; | |
type PExpressionKind = PServiceParam; | |
function applyPExpressionKind<R>(fns: { | |
PServiceParam: (serviceParam: PServiceParam) => R; | |
}): (kind: PExpressionKind) => R { | |
return (kind: PExpressionKind) => { | |
if ("ServiceParam" in kind) { | |
return fns.PServiceParam(kind); | |
} else { | |
return unreachable(kind); | |
} | |
}; | |
} | |
function matchPExpressionKind<R>( | |
kind: PExpressionKind, | |
fns: { | |
PServiceParam: (serviceParam: PServiceParam) => R; | |
} | |
): R { | |
return applyPExpressionKind(fns)(kind); | |
} | |
type PExpression = { | |
Expression: { | |
uid: PUID; | |
expression: { | |
value: PExpressionValue; | |
text: string; | |
}; | |
kind: PExpressionKind; | |
}; | |
}; | |
type PMarkKind = "Bold" | "Italics" | "Mono" | "Underline" | "Strikethrough"; | |
type PText = { | |
Text: [string, PMarkKind[]]; | |
}; | |
type PServiceAction = { | |
ServiceAction: { | |
action_registry_id: PUID; | |
depends_on_atom: PUID; | |
}; | |
}; | |
type PService = { | |
Service: { | |
service_local_id: PUID; | |
service_registry_id: PUID; | |
}; | |
}; | |
type PAtomKind = PService | PServiceAction; | |
function applyPAtomKind<R>(fns: { | |
PService: (service: PService) => R; | |
PServiceAction: (serviceAction: PServiceAction) => R; | |
}): (kind: PAtomKind) => R { | |
return (kind: PAtomKind) => { | |
if ("Service" in kind) { | |
return fns.PService(kind); | |
} else if ("ServiceAction" in kind) { | |
return fns.PServiceAction(kind); | |
} else { | |
return unreachable(kind); | |
} | |
}; | |
} | |
function matchPAtomKind<R>( | |
kind: PAtomKind, | |
fns: { | |
PService: (service: PService) => R; | |
PServiceAction: (serviceAction: PServiceAction) => R; | |
} | |
): R { | |
return applyPAtomKind(fns)(kind); | |
} | |
type PAtom = { | |
Atom: { | |
uid: PUID; | |
kind: PAtomKind; | |
}; | |
}; | |
type PToken = PAtom | PText | PExpression; | |
function applyPToken<R>(fns: { | |
PAtom: (atom: PAtom) => R; | |
PText: (text: PText) => R; | |
PExpression: (expression: PExpression) => R; | |
}): (token: PToken) => R { | |
return (token: PToken) => { | |
if ("Atom" in token) { | |
return fns.PAtom(token); | |
} else if ("Text" in token) { | |
return fns.PText(token); | |
} else if ("Expression" in token) { | |
return fns.PExpression(token); | |
} else { | |
return unreachable(token); | |
} | |
}; | |
} | |
function matchPToken<R>( | |
token: PToken, | |
fns: { | |
PAtom: (atom: PAtom) => R; | |
PText: (text: PText) => R; | |
PExpression: (expression: PExpression) => R; | |
} | |
): R { | |
return applyPToken(fns)(token); | |
} | |
type PTokenLine = { | |
style: "Paragraph"; | |
tokens: PToken[]; | |
}; | |
type PBlockKind = { TokenLine: PTokenLine }; | |
type PBlock = { | |
uid: PUID; | |
kind: PBlockKind; | |
}; | |
function applyPBlockKind<R>(fns: { TokenLine(tokenLine: PTokenLine): R }): (blockKind: PBlockKind) => R { | |
return (blockKind: PBlockKind) => { | |
if ("TokenLine" in blockKind) { | |
return fns.TokenLine(blockKind.TokenLine); | |
} else { | |
return unreachable(blockKind); | |
} | |
}; | |
} | |
function matchPBlockKind<R>( | |
blockKind: PBlockKind, | |
fns: { | |
TokenLine(tokenLine: PTokenLine): R; | |
} | |
): R { | |
return applyPBlockKind(fns)(blockKind); | |
} | |
type PSkill = { | |
uid: PUID; | |
blocks: PBlock[]; | |
}; | |
// Drivers | |
type ServiceCredentials = unknown; | |
type ServiceArgs = unknown; | |
type ServiceShares = unknown; | |
interface ServiceAction { | |
perform: (credentials: ServiceCredentials, args: ServiceArgs) => Promise<Result<ServiceShares, Error>>; | |
} | |
interface ServiceEvent { | |
subscribe: (credentials: ServiceCredentials, args: ServiceArgs) => void; | |
} | |
namespace slack { | |
type ParsedSlackCredentials = { onegraphAppId: string; accessToken: string }; | |
const parseCredentials = (credentials: ServiceCredentials): Result<ParsedSlackCredentials, Error> => { | |
// Proper validation would happen here | |
return ok({ | |
onegraphAppId: (credentials as any).onegraphAppId, | |
accessToken: (credentials as any).accessToken, | |
}); | |
}; | |
// Janky types that match prisma GraphQLClient just to prove out e2e | |
class GraphQLClient { | |
constructor(private uri: string, private options: any) {} | |
public async request(query: string, variables: any) { | |
log( | |
`Faking GraphQL Request: ${JSON.stringify({ | |
uri: this.uri, | |
options: this.options, | |
query, | |
variables, | |
})}` | |
); | |
} | |
} | |
const createClient = (credentials: ParsedSlackCredentials) => { | |
const graphQLClient = new GraphQLClient( | |
`https://serve.onegraph.com/graphql?app_id=${credentials.onegraphAppId}`, | |
{ | |
headers: { | |
authorization: `Bearer ${credentials.accessToken}`, | |
}, | |
} | |
); | |
return { | |
// Silly types | |
// Which layer holds responsibility for transforming vessels and representations? | |
// For example, `slack send to:Message author` | |
// Something needs to turn Message author into a representation that satisfies | |
// the slack send action interface. | |
// What about `slack send to:#bot` | |
// How will we bubble errors? | |
send: async (to: string, contents: string) => { | |
const query = "query contents or whatever"; | |
// How does error handling work? | |
await graphQLClient.request(query, { | |
channel: to, | |
text: contents, | |
}); | |
}, | |
}; | |
}; | |
export namespace send { | |
// Very silly types for this | |
type ParsedSendArgs = { to: string; contents: string }; | |
const parseArgs = (credentials: ServiceArgs): Result<ParsedSendArgs, Error> => { | |
// Proper validation would happen here | |
return ok({ | |
to: (credentials as any).to, | |
contents: (credentials as any).contents, | |
}); | |
}; | |
export const slackSendAction = { | |
// Not sure whether we would hand the unknown types directly to perform or chain perform | |
// after a `parse` step that is offered by the service / action e.g. | |
// pipe( | |
//. zip( | |
// parseCredentials(credentials), | |
// parseArgs(args) | |
// ), | |
// perform, | |
// ) | |
// Or something like that... | |
// Hmmm Promises and Results don't play that well together | |
perform: async ( | |
credentials: ServiceCredentials, | |
args: ServiceArgs | |
): Promise<Result<ServiceShares, Error>> => | |
// Railway Oriented Programming would probably help here but oh well | |
matchResult(parseCredentials(credentials), { | |
// Need a return type on the branch here to make TS understand that Ok and Err are both part of Result? | |
// Nothing shared quite yet... | |
Ok: async (parsedCredentials): Promise<Result<ServiceShares, Error>> => | |
matchResult(parseArgs(args), { | |
Ok: async (parsedArgs): Promise<Result<ServiceShares, Error>> => { | |
// This will be repetitive, maybe actions should just be given the prepared client? | |
const slackClient = createClient(parsedCredentials); | |
// We always resolve this promise and let the layer above handle the Result type | |
// which might be confusing I guess? | |
return new Promise((resolve, reject) => { | |
slackClient | |
.send(parsedArgs.to, parsedArgs.contents) | |
.then(() => resolve(ok({}))) | |
.catch((e) => resolve(err(e))); | |
}); | |
}, | |
Err: async (e) => err(e), | |
}), | |
// we never actually allow it in parseCredentials yet but might as well pass it on | |
Err: async (e) => err(e), | |
}), | |
}; | |
} | |
} | |
// Runtime | |
// We should be able to be smarter here than a general interpreter | |
// because we know the UID that shared things into scope, and we know | |
// the UID that is being referenced as a lookup. | |
// Is a list of Expression the right type here? | |
// Can we just accrete scope over time since we don't need to worry about | |
// shadowing? | |
type Scope = Record<UID, Expression[]>; | |
type Diagnostics = {}; | |
type Context = { scope: Scope; diagnostics: Diagnostics }; | |
// Early modelling of diagnostics | |
// Runtime relationships? | |
type UID = string; | |
type LocalServiceID = string; | |
type DriverID = string; | |
// Hmm, what information do we actually need in here to make a decision? DriverID? | |
type Constant = { | |
_tag: "constant"; | |
id: DriverID; | |
value: unknown; | |
}; | |
const constant = (input: { id: DriverID; value: unknown }): Constant => ({ | |
_tag: "constant", | |
id: input.id, | |
value: input.value, | |
}); | |
type Reference = { | |
_tag: "reference"; | |
}; | |
type Value = Constant | Reference; | |
type Refinement = { | |
id: DriverID; | |
}; | |
type Expression = { | |
value: Optional<[Value, Refinement[]]>; | |
text: string; | |
}; | |
type ArgID = string; | |
type Args = Record<ArgID, Expression>; | |
type Iter = { next: () => Optional<Expression> }; | |
type Block = { | |
_tag: "block"; | |
// uid: UID; | |
instructions: Instruction[]; | |
}; | |
type Action = { | |
_tag: "action"; | |
// uid: UID; | |
local_service_id: LocalServiceID; | |
service_id: DriverID; // Is this needed? | |
service_action_id: DriverID; | |
args: Args; | |
}; | |
const action = (inputs: { | |
local_service_id: LocalServiceID; | |
service_id: DriverID; | |
service_action_id: DriverID; | |
args: Args; | |
}): Action => ({ | |
_tag: "action", | |
local_service_id: inputs.local_service_id, | |
service_id: inputs.service_id, | |
service_action_id: inputs.service_action_id, | |
args: inputs.args, | |
}); | |
type Subscription = { | |
_tag: "subscription"; | |
// uid: UID; | |
local_service_id: LocalServiceID; | |
service_registry_id: DriverID; // Is this needed? | |
service_event_id: DriverID; | |
args: Args; | |
// Hmmm, I'm not sure this is going to work very well... | |
// We sort of need an exact 1:1 mapping of UIDs if we're going to do things like | |
// "hey, this particular line failed" | |
block: Block; | |
}; | |
// How to model else if? | |
type Conditional = { | |
_tag: "conditional"; | |
// uid: UID; | |
expression: Expression; | |
left: Block; | |
right: Optional<Block>; | |
}; | |
// How to model the iteration subject? | |
type Iteration = { | |
_tag: "iteration"; | |
// uid: UID; | |
iterable: Iter; | |
block: Block; | |
}; | |
type Effect = Action | Subscription; | |
function applyEffect<R>(fns: { | |
Action: (action: Action) => R; | |
Subscription: (subscription: Subscription) => R; | |
}): (effect: Effect) => R { | |
return function (effect: Effect): R { | |
if (effect._tag === "action") { | |
return fns.Action(effect); | |
} else if (effect._tag === "subscription") { | |
return fns.Subscription(effect); | |
} else { | |
return unreachable(effect); | |
} | |
}; | |
} | |
function matchEffect<R>( | |
effect: Effect, | |
fns: { | |
Action: (action: Action) => R; | |
Subscription: (subscription: Subscription) => R; | |
} | |
): R { | |
return applyEffect(fns)(effect); | |
} | |
type Instruction = Effect | Conditional | Iteration; | |
function applyInstruction<R>(fns: { | |
Effect: (effect: Effect) => R; | |
Conditional: (conditional: Conditional) => R; | |
Iteration: (iteration: Iteration) => R; | |
}): (instruction: Instruction) => R { | |
return function (instruction: Instruction): R { | |
if (instruction._tag === "action") { | |
return fns.Effect(instruction); | |
} else if (instruction._tag === "subscription") { | |
return fns.Effect(instruction); | |
} else if (instruction._tag === "conditional") { | |
return fns.Conditional(instruction); | |
} else if (instruction._tag === "iteration") { | |
return fns.Iteration(instruction); | |
} else { | |
return unreachable(instruction); | |
} | |
}; | |
} | |
function matchInstruction<R>( | |
instruction: Instruction, | |
fns: { | |
Effect: (effect: Effect) => R; | |
Conditional: (conditional: Conditional) => R; | |
Iteration: (iteration: Iteration) => R; | |
} | |
): R { | |
return applyInstruction(fns)(instruction); | |
} | |
type CredentialLoader = (id: LocalServiceID) => ServiceCredentials; | |
type ActionLoader = (id: DriverID) => ServiceAction; | |
type EventLoader = (id: DriverID) => ServiceEvent; | |
// Too much closing over mutable state here e.g. scope when that's just initial scope | |
// Can we use an immutable library like ImmutableJS? | |
// Can we make a reducer for Promises here? | |
// preduce should act like reduce but with Promises | |
function preduce<A, C>( | |
collection: C[], | |
initialValue: A, | |
callbackFn: (accumulator: A, current: C) => Promise<A> | |
): Promise<A> { | |
return collection.reduce( | |
(accP: Promise<A>, curr: C): Promise<A> => | |
// each subsequent accumulation should depend upon the resolution of the previous step | |
accP.then((acc) => callbackFn(acc, curr)), | |
Promise.resolve(initialValue) | |
); | |
} | |
type Tracer = (block: Block) => void; | |
type TraceID = string; | |
const tracer = (credentialsFor: CredentialLoader, loadAction: ActionLoader, loadEvent: EventLoader) => ( | |
id: TraceID, | |
initialScope: Scope, | |
block: Block | |
) => | |
preduce( | |
block.instructions, | |
initialScope, | |
(accumulatingScope, instruction): Promise<Scope> => | |
applyInstruction({ | |
Effect: applyEffect({ | |
Action: async (actionInstruction: Action): Promise<Scope> => { | |
log(`Tracing ${id}: In Action`); | |
// Probably this could be pretty functional with fp-ts or something. | |
// Maybe we shouldn't do the action in here but pass it back up so this | |
// could remain effect-free and easily testable, but I dunno, seems ok. | |
// Don't look too closely at the wiring, maybe the credentials are used | |
// at loading time or something. Also these have error conditions e.g. | |
// no credentials with service id (which may be an error or not) or | |
// no action with id | |
const credentials = credentialsFor(actionInstruction.local_service_id); | |
const action = loadAction(actionInstruction.service_action_id); | |
const performResult = await action.perform(credentials, actionInstruction.args); | |
// This should probably have a way to signal failure? | |
return applyResult({ | |
Ok: (shares) => { | |
// convert shares to expressions? | |
// should this return scope + new scope or just new scope? | |
// Let's pretend that we have a way to create new scope from | |
// shares... | |
return { | |
...accumulatingScope, | |
// Need to accumulate scope perhaps by UID here? | |
// [actionInstruction.uid]: [], | |
}; | |
}, | |
Err: (err) => { | |
return accumulatingScope; | |
}, | |
})(performResult); | |
// And don't look too closely at these args. There's probably a better | |
// way to map them into the driver interface. For example, given some | |
// constant value or reference, we probably need to "resolve"/"evaluate" | |
// them before passing them through? | |
}, | |
Subscription: async (subscriptionInstruction: Subscription): Promise<Scope> => { | |
return accumulatingScope; | |
}, | |
}), | |
// Shouldn't really be able to return any new scope from these? | |
Conditional: async (conditionalInstruction: Conditional): Promise<Scope> => { | |
return accumulatingScope; | |
}, | |
Iteration: async (iterationInstruction: Iteration): Promise<Scope> => { | |
return accumulatingScope; | |
}, | |
})(instruction) | |
); | |
namespace transformation { | |
type QualifiedService = { | |
_tag: "qualifiedService"; | |
service_local_id: LocalServiceID; | |
service_id: DriverID; | |
}; | |
const qualifiedService = (inputs: { | |
service_local_id: LocalServiceID; | |
service_id: DriverID; | |
}): QualifiedService => ({ | |
_tag: "qualifiedService", | |
service_local_id: inputs.service_local_id, | |
service_id: inputs.service_id, | |
}); | |
type QualifiedServiceAction = { | |
_tag: "qualifiedServiceAction"; | |
service_local_id: LocalServiceID; | |
service_id: DriverID; | |
service_action_id: DriverID; | |
args: Args; | |
}; | |
const qualifiedServiceAction = (inputs: { | |
service_local_id: LocalServiceID; | |
service_id: DriverID; | |
service_action_id: DriverID; | |
args: Args; | |
}): QualifiedServiceAction => ({ | |
_tag: "qualifiedServiceAction", | |
service_local_id: inputs.service_local_id, | |
service_id: inputs.service_id, | |
service_action_id: inputs.service_action_id, | |
args: inputs.args, | |
}); | |
type Unqualified = { | |
_tag: "unqualified"; | |
}; | |
const unqualified: Unqualified = { _tag: "unqualified" }; | |
type Disqualified = { | |
_tag: "disqualified"; | |
reason: string; | |
}; | |
const disqualified = (reason: string): Disqualified => ({ _tag: "disqualified", reason }); | |
type Qualification = Unqualified | QualifiedService | QualifiedServiceAction | Disqualified; | |
function applyQualification<R>(fns: { | |
Unqualified: (unqualified: Unqualified) => R; | |
QualifiedService: (qualifiedService: QualifiedService) => R; | |
QualifiedServiceAction: (qualifiedServiceAction: QualifiedServiceAction) => R; | |
Disqualified: (disqualified: Disqualified) => R; | |
}): (qualification: Qualification) => R { | |
return (qualification: Qualification) => { | |
if (qualification._tag === "unqualified") { | |
return fns.Unqualified(qualification); | |
} else if (qualification._tag === "qualifiedService") { | |
return fns.QualifiedService(qualification); | |
} else if (qualification._tag === "qualifiedServiceAction") { | |
return fns.QualifiedServiceAction(qualification); | |
} else if (qualification._tag === "disqualified") { | |
return fns.Disqualified(qualification); | |
} else { | |
return unreachable(qualification); | |
} | |
}; | |
} | |
function matchQualification<R>( | |
qualification: Qualification, | |
fns: { | |
Unqualified: (unqualified: Unqualified) => R; | |
QualifiedService: (qualifiedService: QualifiedService) => R; | |
QualifiedServiceAction: (qualifiedServiceAction: QualifiedServiceAction) => R; | |
Disqualified: (disqualified: Disqualified) => R; | |
} | |
): R { | |
return applyQualification(fns)(qualification); | |
} | |
export const transformSkill = (skill: PSkill): Block => { | |
const extendBlock = (block: Block, instruction: Instruction): Block => ({ | |
_tag: "block", | |
instructions: [...block.instructions, instruction], | |
}); | |
const startingBlock: Block = { _tag: "block", instructions: [] }; | |
const qualifyTokenline = (tokenLine: PTokenLine) => | |
tokenLine.tokens | |
.filter( | |
applyPToken({ | |
PAtom: () => true, | |
PText: () => false, | |
PExpression: () => true, | |
}) | |
) | |
.reduce( | |
(qualification: Qualification, token): Qualification => | |
matchQualification(qualification, { | |
Unqualified: (): Qualification => | |
matchPToken(token, { | |
PAtom: (pAtom): Qualification => | |
matchPAtomKind(pAtom.Atom.kind, { | |
PService: (service): Qualification => | |
qualifiedService({ | |
service_local_id: service.Service.service_local_id, | |
service_id: service.Service.service_registry_id, | |
}), | |
PServiceAction: () => | |
disqualified("the token line began with a service action"), | |
}), | |
PText: () => disqualified("the token line began with text"), | |
PExpression: () => disqualified("the token line began with an expression"), | |
}), | |
QualifiedService: (service) => | |
matchPToken(token, { | |
PAtom: (pAtom): Qualification => | |
matchPAtomKind(pAtom.Atom.kind, { | |
PService: (): Qualification => | |
disqualified("the token line had two sequential services"), | |
PServiceAction: (serviceAction) => | |
qualifiedServiceAction({ | |
service_local_id: service.service_local_id, | |
service_id: service.service_id, | |
service_action_id: serviceAction.ServiceAction.action_registry_id, | |
args: {}, | |
}), | |
}), | |
PText: () => disqualified("the token line had text following the service"), | |
PExpression: () => | |
disqualified("the token line had an expression following the service"), | |
}), | |
QualifiedServiceAction: (serviceAction) => | |
matchPToken(token, { | |
PAtom: (): Qualification => | |
disqualified("the token line had an atom following the service action"), | |
PText: () => disqualified("the token line had text following the service atom"), | |
PExpression: (pExpression): Qualification => | |
matchPExpressionKind(pExpression.Expression.kind, { | |
PServiceParam: (serviceParam): Qualification => | |
qualifiedServiceAction({ | |
service_local_id: serviceAction.service_local_id, | |
service_id: serviceAction.service_id, | |
service_action_id: serviceAction.service_action_id, | |
args: { | |
...serviceAction.args, | |
// Only handling constants right now | |
[serviceParam.ServiceParam.field_registry_id]: { | |
text: pExpression.Expression.expression.text, | |
value: matchPExpressionValue( | |
pExpression.Expression.expression.value, | |
{ | |
None: (): Optional<[Value, Refinement[]]> => none, | |
// Can't really do anything with refinements right here because they are unknown type hmmm | |
Some: (value) => | |
some([ | |
constant({ | |
id: value[0].Constant.value_type, | |
value: value[0].Constant.value, | |
}), | |
[], | |
]), | |
} | |
), | |
}, | |
}, | |
}), | |
}), | |
}), | |
Disqualified: (disqualification) => disqualification, | |
}), | |
unqualified | |
); | |
const transformTokenLine = (tokenLine: PTokenLine): Optional<Instruction> => { | |
const qualification = qualifyTokenline(tokenLine); | |
return matchQualification(qualification, { | |
Unqualified: (): Optional<Instruction> => none, | |
Disqualified: () => none, | |
QualifiedService: () => none, | |
QualifiedServiceAction: (qualification) => | |
some( | |
action({ | |
local_service_id: qualification.service_local_id, | |
service_id: qualification.service_id, | |
service_action_id: qualification.service_action_id, | |
args: qualification.args, | |
}) | |
), | |
}); | |
}; | |
return skill.blocks.reduce( | |
(acc, curr): Block => | |
matchPBlockKind(curr.kind, { | |
TokenLine: (tokenLine): Block => | |
matchOptional(transformTokenLine(tokenLine), { | |
Some: (instruction): Block => extendBlock(acc, instruction), | |
None: () => acc, | |
}), | |
}), | |
startingBlock | |
); | |
}; | |
} | |
namespace examples { | |
type Example = { | |
id: string; | |
page: PSkill; | |
runnable: Block; | |
}; | |
// Slack send to:#general content:"Hello" -> Instruction.Effect.Action | |
export const e1: Example = { | |
id: "simple slack send", | |
page: { | |
uid: "JytJZuF4TTW_9D9Fbxgqjw", | |
blocks: [ | |
{ | |
uid: "!blank--token-line-1", | |
kind: { | |
TokenLine: { | |
style: "Paragraph", | |
tokens: [ | |
{ | |
Atom: { | |
uid: "2UpjJw2tTLiJGcbK2ZG0rA", | |
kind: { | |
Service: { | |
service_local_id: "my-slack", | |
service_registry_id: "service:slack", | |
}, | |
}, | |
}, | |
}, | |
{ | |
Text: [" ", []], | |
}, | |
{ | |
Atom: { | |
uid: "gHKaeAzMRTmiJOSEnZLRLA", | |
kind: { | |
ServiceAction: { | |
action_registry_id: "service/action:slack/send", | |
depends_on_atom: "2UpjJw2tTLiJGcbK2ZG0rA", | |
}, | |
}, | |
}, | |
}, | |
{ | |
Text: [" ", []], | |
}, | |
{ | |
Expression: { | |
uid: "K0wZfh31RdO1Zdhp9TYiQQ", | |
expression: { | |
value: [ | |
{ | |
Constant: { | |
display_text: "#general", | |
value_type: "service/type:slack/Channel", | |
value: "#general", | |
}, | |
}, | |
[], | |
], | |
text: "", | |
}, | |
kind: { | |
ServiceParam: { | |
depends_on_atom: ["2UpjJw2tTLiJGcbK2ZG0rA", "gHKaeAzMRTmiJOSEnZLRLA"], | |
field_registry_id: "service/action/field:slack/send/recipient", | |
}, | |
}, | |
}, | |
}, | |
{ | |
// Why is this an empty space? | |
Text: [" ", []], | |
}, | |
{ | |
Expression: { | |
uid: "qR_UDyFVQTu-UopVrY6Z9g", | |
expression: { | |
value: null, | |
text: "Hello", | |
}, | |
kind: { | |
ServiceParam: { | |
depends_on_atom: ["2UpjJw2tTLiJGcbK2ZG0rA", "gHKaeAzMRTmiJOSEnZLRLA"], | |
field_registry_id: "service/action/field:slack/send/message", | |
}, | |
}, | |
}, | |
}, | |
], | |
}, | |
}, | |
}, | |
], | |
}, | |
runnable: { | |
_tag: "block", | |
instructions: [ | |
{ | |
_tag: "action", | |
local_service_id: "my-slack", | |
service_id: "service:slack", | |
service_action_id: "service/action:slack/send", | |
args: { | |
"service/action/field:slack/send/recipient": { | |
text: "", | |
value: { | |
_tag: "some", | |
value: [ | |
{ | |
_tag: "constant", | |
id: "service/type:slack/Channel", | |
// Probably not quite the right value here... | |
// it should really be a channel id, only the display | |
// is #bot | |
value: "#general", | |
}, | |
[], | |
], | |
}, | |
}, | |
"service/action/field:slack/send/message": { | |
text: "Hello", | |
value: { _tag: "none" }, | |
}, | |
}, | |
}, | |
], | |
}, | |
}; | |
// Other Examples to come: | |
// Slack send to:#bot content:"Hello" -> Instruction.Effect.Action | |
// Slack send to:@Will content:"Hello" -> Instruction.Effect.Action | |
// when Slack hears in:#echo -> Instruction.Effect.Subscription | |
//. slack reply to:Message content:Message Contents -> Instruction.Effect.Action | |
// when Slack hears in:#engineering -> Instruction.Effect.Subscription | |
//. if Message contents contains "will" -> Instruction.Conditional | |
//. slack reply to:Message content:"mmm TDD" | |
// when Slack hears in:#engineering -> Instruction.Effect.Subscription | |
//. if Message contents contains "will" -> Instruction.Conditional | |
//. slack reply to:Message content:"mmm TDD" -> Instruction.Effect.Action | |
//. else -> No Construct | |
//. slack reply to:Message content:"Testing sucks" -> Instruction.Effect.Action | |
// when Event is cancelled -> Instruction.Effect.Subscription | |
//. for each Event attendee -> -> Instruction.Iteration | |
//. email to:Attendee message:"Event cancelled due to Covid" -> Instruction.Effect.Action | |
// when Stripe invoice create -> Instruction.Effect.Subscription | |
//. after 30 days -> Instruction.Effect.Subscription ?? How to model dropping back in here ?? | |
//. if Invoice is unpaid -> Instruction.Conditional | |
// email to:Invoice recipient message:"Pay up" -> Instruction.Effect.Action | |
export const all = [e1]; | |
} | |
const main = async () => { | |
const credentialsFor = (id: LocalServiceID): ServiceCredentials => { | |
if (id === "my-slack") { | |
return { accessToken: "fake-access-token", onegraphAppId: "fake-onegraph-app-id" }; | |
} else { | |
throw new Error(`unknown driver id: ${id}`); | |
} | |
}; | |
const loadAction = (id: DriverID): ServiceAction => { | |
if (id === "service/action:slack/send") { | |
return slack.send.slackSendAction; | |
} else { | |
throw new Error(`unknown driver id: ${id}`); | |
} | |
}; | |
const loadEvent = (id: DriverID): ServiceEvent => ({ | |
subscribe: (credentials: ServiceCredentials, args: ServiceArgs) => {}, | |
}); | |
const trace = tracer(credentialsFor, loadAction, loadEvent); | |
examples.all.forEach(async (e) => { | |
log(`starting Trace: ${e.id}`); | |
const scope = trace(e.id, {}, transformation.transformSkill(e.page)); | |
log(`Finishing Trace: ${e.id} with scope: ${JSON.stringify(scope)}`); | |
}); | |
}; | |
const testTransformation = () => { | |
log("beginning transformation tests"); | |
examples.all.forEach((e) => { | |
log(`testing transformation for "${e.id}"`); | |
log(`transformation equality: ${JSON.stringify(transformation.transformSkill(e.page)) === JSON.stringify(e.runnable)}`); | |
log(`finished testing transformation for "${e.id}"`); | |
}); | |
log("finished transformation tests"); | |
}; | |
testTransformation(); | |
main() | |
.then(() => log("complete")) | |
.catch((e) => console.error(e)); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment