Last active
November 8, 2018 13:10
-
-
Save lumie1337/8979a51b8d354d337444ca61f22c121e 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
// 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