Skip to content

Instantly share code, notes, and snippets.

@lumie1337
Last active November 8, 2018 13:10
Show Gist options
  • Save lumie1337/8979a51b8d354d337444ca61f22c121e to your computer and use it in GitHub Desktop.
Save lumie1337/8979a51b8d354d337444ca61f22c121e to your computer and use it in GitHub Desktop.
// utilities from https://github.com/ksandin/toolbelt/blob/master/types/common.d.ts
type Identity<T> = { id: T };
type SubTypeKeys<Base, Condition> = {
[Key in keyof Base]: Base[Key] extends Condition ? Key : never
}[keyof Base];
type SubTypeKeysRecursive<Base, Condition> = SubTypeKeys<Base, Condition> | {
[Key in keyof Base]: Base[Key] extends object ?
[SubTypeKeysRecursive<Base[Key], Condition>] extends [never] ? never : Key :
never
}[keyof Base];
type ModelQueryValue = number | string | boolean | Date | Identity<any>;
type ModelQuery<T> = {
[P in SubTypeKeys<T, ModelQueryValue | ModelQueryValue[]>]?:
T[P] extends Identity<infer Id> | Identity<infer Id>[] ? Id | Id[] :
T[P] extends (infer Single)[] ? Single | Single[] :
T[P] | T[P][]
};
// some of my helpers
interface IOption<T> {
get(): T
defined(): boolean
getOrElse(otherwise: T): T
orElse(otherwise: T): IOption<T>
}
const { option, Some, Nothing } = (function() {
class Some<T> implements IOption<T> {
value: T;
constructor(value: T) {
this.value = value;
}
get() {
return this.value;
}
orElse(): IOption<T> { return this; } // noop
getOrElse() { return this.get(); }
defined() { return true; }
}
class Nothing<T> implements IOption<T> {
get(): T {
throw new Error("Option.get: Null")
}
orElse(otherwise: T): IOption<T> {
return new Some(otherwise);
}
getOrElse(otherwise: T) {
return otherwise;
}
defined() { return false; }
}
return {
option: <T>(value: T | null) => !value ? new Nothing<T>() : new Some<T>(value),
Some: <T>(value: T) => new Some<T>(value),
Nothing: <T>() => new Nothing<T>()
};
})();
// We implement ServiceOperations as object capabilites
// Note: this is just a helper (optional)
type CapabilityBuilder<Input, Output> = { implement(f: (input: Input) => Output, defined?: boolean): Capability<Input, Output> }
type CapabilitySpec<Input, Output> = { _capId: string } & CapabilityBuilder<Input, Output>
// arity-1 capabilities (always 1 argument)
type Capability<Input, Output> = ((input: Input) => Output) & { _spec: CapabilitySpec<Input, Output>, _defined: boolean }
// alternatives with arity-n capabilties
// version 1 generic tuple types (limited auto completion in vscode, no auto-complete in webstorm)
/*
type Capability<Input extends any[], Output> = ((...args: Input) => Output)
*/
// version 2 non-generic tuple types (arity-3 but can be extended, optimal auto completion in vscode, no auto-complete in webstorm)
/*
type Capability<Input extends any[], Output> =
Input extends [infer P1] ? ((p1: P1) => Output) :
Input extends [infer P1, infer P2] ? ((p1: P1, p2: P2) => Output) :
Input extends [infer P1, infer P2, infer P3] ? ((p1: P1, p2: P2, p3: P3) => Output) :
never
*/
function capability<Input, Output>(capId: string): CapabilitySpec<Input, Output> {
const spec = { implement, _capId: capId };
function implement(f: (input: Input) => Output, defined: boolean = true): Capability<Input, Output> {
const result = f as Capability<Input, Output>;
result._spec = spec;
result._defined = defined;
return result;
}
return spec;
}
// revokable capabilities
type RevokationHandler = () => void
type Revocable<Input, Output> = {
revoke: RevokationHandler,
capability: Capability<Input, Output>
}
function revocable<Input, Output>(capability: Capability<Input, Output>): Revocable<Input, Output> {
let revoked = false;
return {
revoke: () => { revoked = true; },
capability: capability._spec.implement((input: Input) => {
if (revoked) {
throw new Error(`Capability has been revoked: ${capability._spec._capId}`)
}
return capability(input);
})
}
}
// Generic Interceptor
type Interceptor<T> = {
addHandler: (handler: (first: T, ...args: any) => T) => RevokationHandler,
trigger: (first: T, ...args: any) => T
}
function interceptor<T>(): Interceptor<T> {
type Handler = (first: T, ...args: any) => T
let state: Handler[] = [];
function addHandler(f: Handler) {
state.push(f);
return function revoke() {
state = state.filter((e) => e === f);
}
}
function trigger(value: T) {
let result = value;
const args = Array.from(arguments);
args.unshift(null);
for (const handler of state) {
args[0] = result;
result = handler.apply(null, args)
}
return result;
}
return {
addHandler,
trigger
}
}
// Interceptors for individual Capabiltiies
type Interceptable<Input, Output> = {
handleBefore(f: (input: Input) => Input): RevokationHandler,
handleAfter(f: (output: Output, input: Input) => Output): RevokationHandler
capability: Capability<Input, Output>
}
function interceptable<Input, Output>(capability: Capability<Input, Output>): Interceptable<Input, Output> {
const before = interceptor<Input>();
const after = interceptor<Output>();
return {
handleBefore: before.addHandler,
handleAfter: after.addHandler,
capability: capability._spec.implement((input: Input) => {
const inputTemp = before.trigger(input);
const output = capability(inputTemp);
return after.trigger(output);
})
}
}
// Interceptors for groups of Capabiltiies
// XXX: no any please
type InterceptableMany<T extends { [key: string]: Capability<any, any> }> = {
handleBefore(f: (input: any, capability: string) => any): RevokationHandler,
handleAfter(f: (output: any, input: any, capability: string) => any): RevokationHandler,
capabilities: T
}
function interceptableMany<T extends { [key: string]: Capability<any, any> }>(capabilities: T): InterceptableMany<T> {
const before = interceptor();
const after = interceptor();
const result = {} as T;
for (const key in capabilities) {
if(!capabilities.hasOwnProperty(key))
continue;
const { capability, handleBefore, handleAfter } = interceptable(capabilities[key]);
handleBefore(function () {
const args = Array.from(arguments);
args.push(key);
return before.trigger.apply(null, args)
});
handleAfter(function () {
const args = Array.from(arguments);
args.push(key);
return after.trigger.apply(null, args)
});
result[key] = capability;
}
return {
handleBefore: before.addHandler,
handleAfter: after.addHandler,
capabilities: result
}
}
// resolves a map of capability specs to capability implementation types
type CapabilitiesFor<T> = {
[P in keyof T]:
T[P] extends CapabilitySpec<infer Input, infer Output> ? Capability<Input, Output> : never
};
// Recursive Capability Tree
type CapabilitiesRec = { [key: string]: CapabilitySpec<any, any> | CapabilitiesRec }
type CapabilitiesForRec<T> = {
[P in keyof T]:
T[P] extends CapabilitySpec<infer Input, infer Output> ? Capability<Input, Output> :
T[P] extends object ? CapabilitiesFor<T[P]> :
never
};
// type narrowing helpers
type CapabilitiesForRec_MatchRec<T> =
T extends CapabilitySpec<infer Input, infer Output> ? never :
T extends object ? T :
never
type CapabilitiesForRec_MatchCap<T> =
T extends CapabilitySpec<infer Input, infer Output> ? CapabilitySpec<Input, Output> :
never
// Global Capability Registry (one possible way of doing it)
class CapabilityContainer {
capabilities: { [key: string]: Capability<any, any> };
constructor() {
this.capabilities = {}
}
register(capability: Capability<any, any>): void {
this.capabilities[capability._spec._capId] = capability;
}
registerMany(capabilities: { [key: string]: Capability<any, any> }): void {
for (const key in capabilities) {
const capability = capabilities[key];
this.register(capability);
}
}
// XXX: currently we dynamically error out on capabilities
lookup<Input, Output>(capabilitySpec: CapabilitySpec<Input, Output>): Capability<Input, Output> {
let capability = this.capabilities[capabilitySpec._capId];
if (!capability)
capability = capabilitySpec.implement((input) => {
throw new Error(`Capability not implemented: ${capabilitySpec._capId}`);
}, false);
return capability;
}
lookupMany<T extends { [key: string]: CapabilitySpec<any, any> }>(capabilitySpecs: T): CapabilitiesFor<T> {
const result = {} as CapabilitiesFor<T>;
for (const key in capabilitySpecs) {
if(!capabilitySpecs.hasOwnProperty(key))
continue;
const capabilitySpec = capabilitySpecs[key];
result[key] = this.lookup(capabilitySpec) as any;
}
return result;
}
lookupManyRec<T extends CapabilitiesRec>(capabilitySpecs: T): CapabilitiesForRec<T> {
const result = {} as CapabilitiesForRec<T>;
for (const key in capabilitySpecs) {
if(!capabilitySpecs.hasOwnProperty(key))
continue;
const capabilitySpec = capabilitySpecs[key];
if (capabilitySpec._capId !== undefined)
result[key] = this.lookup(capabilitySpec as CapabilitiesForRec_MatchCap<typeof capabilitySpec>) as any;
else
result[key] = this.lookupManyRec(capabilitySpec as CapabilitiesForRec_MatchRec<typeof capabilitySpec>) as any;
}
return result;
}
}
const capabilities = new CapabilityContainer();
// CRUD Specific Implementations
// Specification for Crud Capabilities
type CrudCapabilitySpecs<Model, Query = ModelQuery<Model>> = {
create: CapabilitySpec<Model, Promise<Model>>,
read: CapabilitySpec<Query, Promise<[Model]>>,
update: CapabilitySpec<Model, Promise<Model>>,
delete: CapabilitySpec<Model, Promise<boolean>>
}
type CrudCapabilities<Model, Query = ModelQuery<Model>> = CapabilitiesFor<CrudCapabilitySpecs<Model, Query>>
function crudCapabilties<Model, Query = ModelQuery<Model>>(serviceIdPrefix: string): CrudCapabilitySpecs<Model, Query> {
return {
create: capability(`${serviceIdPrefix}.create`),
read: capability(`${serviceIdPrefix}.read`),
update: capability(`${serviceIdPrefix}.update`),
delete: capability(`${serviceIdPrefix}.delete`)
}
}
// HTTP Example with some mocks
type HttpRequest = {
method?: "GET" | "POST" | "PUT" | "DELETE";
url?: string;
requestParams?: { [key: string]: any };
body?: string;
// headers: {[key: string]: string};
}
class HttpResponse {
code: number;
body: string;
constructor() {
this.code = 0;
this.body = "";
}
successful(): boolean {
return this.code === 200;
}
}
interface HttpMock {
request(httpRequest: HttpRequest): Promise<HttpResponse>
}
const http: HttpMock = ({} as HttpMock);
// END OF MOCKS
// Isomorphism for Serialiser/Deserialisation and other things
type Iso<A, B> = {
from: (x: A) => B,
to: (x: B) => A
}
function makeIso<A, B>(from: (x: A) => B, to: (x: B) => A): Iso<A, B> {
return {
from,
to
};
}
const simpleSerialiser = makeIso(JSON.stringify, JSON.parse);
// Crud Adapter
function simpleRestCrudAdapter<Model extends { id: string }, Query = ModelQuery<Model>>(
capabilitySpecs: CrudCapabilitySpecs<Model, Query>,
url: string,
serialiser: Iso<Model, string>,
arraySerialiser: Iso<[Model], string>): CrudCapabilities<Model, Query> {
function ensureResponseCode(res: HttpResponse): HttpResponse {
if (!res.successful())
throw new Error("Unexpected Http Response Code.");
return res;
}
return {
create: capabilitySpecs.create.implement((model: Model) =>
http.request({ method: "POST", url: `${url}`, body: serialiser.from(model) })
.then(ensureResponseCode)
.then((res) => serialiser.to(res.body))
),
read: capabilitySpecs.read.implement((query: Query) =>
http.request({ method: "GET", url: `${url}`, requestParams: query })
.then(ensureResponseCode)
.then((res) => arraySerialiser.to(res.body))
),
update: capabilitySpecs.update.implement((model: Model) =>
http.request({ method: "PUT", url: `${url}/${model.id}`, body: serialiser.from(model) })
.then(ensureResponseCode)
.then((res) => serialiser.to(res.body))
),
delete: capabilitySpecs.delete.implement((model: Model) =>
http.request({ method: "DELETE", url: `${url}/${model.id}` })
.then(ensureResponseCode)
.then((_) => true) // TODO: Handle Failure Scenarios
)
}
}
// Interceptors for Model Changes
type CrudMutableAction = "create" | "update" | "delete"
const crudMutableAction: CrudMutableAction[] = ["create", "update", "delete"];
type OnModelChange<Model> = {
onModelChange(f: (after: Model, before: Model, action: CrudMutableAction) => void): RevokationHandler
}
function interceptModelChange<Model, Query = ModelQuery<Model>>(
/// XXX: Clean this up
capabilities: CrudCapabilities<Model, Query>): CrudCapabilities<Model, Query> & OnModelChange<Model> {
let result = capabilities as CrudCapabilities<Model, Query> & OnModelChange<Model>;
const { capabilities: resultTemp, handleAfter, handleBefore } = interceptableMany(capabilities);
result = resultTemp as any;
result.onModelChange = (f) => handleAfter((after: Model, before: Model, capability: string) => {
for (const action of crudMutableAction) {
if (capability === action)
f(after, before, capability);
}
return after;
});
return result;
}
// EXAMPLE USAGE
// Capability Definitions
type ProductModel = {
id: string,
price?: number
}
// could also specify manually:
//
// const productService = {
// create: capability<ProductModel, ProductModel>("product.create"),
// read: capability<ModelQuery<ProductModel>, [ProductModel]>("product.read"),
// update: capability<ProductModel, ProductModel>("product.update"),
// delete: capability<ProductModel, boolean>("product.delete")
// };
const productServiceSpec = crudCapabilties<ProductModel>("product");
const productServiceImpl = simpleRestCrudAdapter<ProductModel>(productServiceSpec, "http://base.url/products", simpleSerialiser, simpleSerialiser)
// if we want to we can add interceptors
const prodServiceImplWithInterceptors = interceptModelChange(productServiceImpl);
prodServiceImplWithInterceptors.onModelChange((after, before, action) => {
// XXX: Not Implemented
});
capabilities.registerMany(productServiceImpl);
// Capability Consumption
const productService = capabilities.lookupMany(productServiceSpec);
const newProduct = productService.create({ id: "example" });
// support for typed dependency injection
function implementCapability<T extends CapabilitiesRec, Input, Output>(
capabilitySpec: CapabilitySpec<Input, Output>,
component: (deps: CapabilitiesForRec<T>) => (input: Input) => Output, deps: T): Capability<Input, Output> {
return capabilitySpec.implement(component(capabilities.lookupManyRec(deps)))
}
const priceLookup = capability<string, Promise<number | undefined>>("product.price.lookup");
implementCapability(
priceLookup,
({ productService }) => (productId: string) => productService.read({ id: productId }).then((e) => e[0].price),
{
productService: productServiceSpec
}
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment