Skip to content

Commit

Permalink
Merge pull request #1 from cabiri-io/feat/app-idea
Browse files Browse the repository at this point in the history
feat: all in shutters but starts introducing generics
  • Loading branch information
kamaz authored Jun 14, 2020
2 parents 0b1a237 + 0982bfd commit 70a9a36
Show file tree
Hide file tree
Showing 16 changed files with 5,062 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
dist
7 changes: 7 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"singleQuote": true,
"printWidth": 120,
"semi": false,
"trailingComma": "none",
"arrowParens": "avoid"
}
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
}
24 changes: 24 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"engines": {
"node": "0.12"
},
"private": true,
"scripts": {
"commit": "git-cz",
"pretty-quick": "pretty-quick",
"prettify:fix": "prettier --write 'packages/**/*.{js,ts}'",
"prettify": "prettier --check 'packages/**/*.{js,ts}'"
},
"workspaces": [
"packages/*"
],
"devDependencies": {
"@babel/core": "^7.9.6",
"@babel/preset-env": "^7.9.6",
"@babel/preset-typescript": "^7.9.0",
"babel-jest": "^26.0.1",
"jest": "^26.0.1",
"prettier": "^2.0.5",
"pretty-quick": "^2.0.1"
}
}
5 changes: 5 additions & 0 deletions packages/sls-app/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
setupFilesAfterEnv: ['jest-extended']
}
16 changes: 16 additions & 0 deletions packages/sls-app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "@cabiri-io/sls-app",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"test": "jest"
},
"devDependencies": {
"@types/jest": "^26.0.0",
"jest": "^26.0.1",
"jest-extended": "^0.11.5",
"ts-jest": "^26.1.0",
"typescript": "^3.9.5"
}
}
62 changes: 62 additions & 0 deletions packages/sls-app/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
type ActionFunction<P, D, R> = (payload: P, dependencies: D) => R

// configuration I want to be able to debug invocation
type Application<P, D, R> = {
pre(actionContextFunction: ActionContextFunction<P, D>): Application<P, D, R>
action(actionFunction: ActionFunction<P, D, R>): Application<P, D, R> | never
post(): Application<P, D, R>
run(payload: P, dependencies: D): Promise<R>
}

type PreActionContext<P, D> = {
payload: P
dependencies: D
}

type ActionContextFunction<P, D> = (actionContext: PreActionContext<P, D>) => void

type PreAction<P, D> = {
func: ActionContextFunction<P, D>
// maybe we have type
}

export class ApplicationError extends Error {
public type: string = 'ApplicationError'
}

// allow to configure the application e.g. log each entry
export function application<P, D, R>(): Application<P, D, R> {
// maybe instead of using array we an explore using Task concept and
// and instead we can just compose function

const preActions: Array<PreAction<P, D>> = []
let mainAction: ActionFunction<P, D, R>
return {
pre(actionFunction) {
// allow to push logger
preActions.push({ func: actionFunction })
return this
},

action(actionFunction) {
//@ts-ignore
if (mainAction) throw new ApplicationError('you can only have a single action')
mainAction = actionFunction
return this
},

post() {
return this
},

run(payload, dependencies) {
return (
preActions
// we work with promises because this allows us easy capture all the async stuff
.reduce((acc, v) => acc.then(r => (v.func(r), r)), Promise.resolve({ payload, dependencies }))
// do action now will work with pre
.then(({ payload, dependencies }) => mainAction(payload, dependencies))
)
}
}
}
60 changes: 60 additions & 0 deletions packages/sls-app/tests/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { application, ApplicationError } from '../src'

describe('application flow', () => {
describe('action', () => {
it('creates an application with an action only', () =>
application<string, { appendString: string }, string>()
.action((payload, { appendString }) => `${payload} ${appendString}`)
.run('hello', { appendString: 'world' })
.then(result => {
expect(result).toBe('hello world')
}))

it('throws an error when multiple actions are created', () =>
expect(() =>
application<string, { appendString: string }, string>()
.action((payload, { appendString }) => `${payload} ${appendString}`)
.action((payload, { appendString }) => `${payload} ${appendString}`)
.run('hello', { appendString: 'world' })
).toThrowError(new ApplicationError('you can only have a single action')))
})

describe('pre action', () => {
it('execute pre action before main action', async () => {
const preAction = jest.fn()
const action = jest.fn()

return application<string, { appendString: string }, void>()
.pre(preAction)
.action(action)
.run('hello', { appendString: 'world' })
.then(() => {
expect(preAction).toHaveBeenCalledWith({ payload: 'hello', dependencies: { appendString: 'world' } })
expect(action).toHaveBeenCalledWith('hello', { appendString: 'world' })
//@ts-ignore
expect(preAction).toHaveBeenCalledBefore(action)
})
})

it('executes pre actions in order of definition', async () => {
const preAction1 = jest.fn()
const preAction2 = jest.fn()
const action = jest.fn()

return application<string, { appendString: string }, void>()
.pre(preAction1)
.pre(preAction2)
.action(action)
.run('hello', { appendString: 'world' })
.then(() => {
expect(preAction1).toHaveBeenCalledWith({ payload: 'hello', dependencies: { appendString: 'world' } })
expect(preAction2).toHaveBeenCalledWith({ payload: 'hello', dependencies: { appendString: 'world' } })
expect(action).toHaveBeenCalledWith('hello', { appendString: 'world' })
//@ts-ignore
expect(preAction1).toHaveBeenCalledBefore(preAction2)
//@ts-ignore
expect(preAction2).toHaveBeenCalledBefore(action)
})
})
})
})
9 changes: 9 additions & 0 deletions packages/sls-app/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"declaration": true
},
"include": ["src/**/*", "tests/**/*"],
"exclude": ["dist/**/*"]
}
5 changes: 5 additions & 0 deletions packages/sls-env/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
setupFilesAfterEnv: ['jest-extended']
}
16 changes: 16 additions & 0 deletions packages/sls-env/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "@cabiri-io/sls-env",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"test": "jest"
},
"devDependencies": {
"@types/jest": "^26.0.0",
"jest": "^26.0.1",
"jest-extended": "^0.11.5",
"ts-jest": "^26.1.0",
"typescript": "^3.9.5"
}
}
79 changes: 79 additions & 0 deletions packages/sls-env/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//

type PayloadConstructor<E, C, R> = (event: E, context: C) => R
// but that makes it very specific to even and context
// so maybe we extract Request to be Request {event, context}
// or maybe request becomes something abstract
// what does Request mean in context of async event
type Event<I, C, O> = {
input: I
context?: C
output?: O
}

// maybe application gets event, context under something different like `dependencies`
// event = <Event, Context, Response> where Event can be Request,
// event = {input, context?, output?}
// or maybe at this point we don't allow working with raw events as a good practice and abstraction
type AppConstructor<P, D, R> = ({ payload, dependencies }: { payload: P; dependencies: D }) => R | Promise<R>

// E - event
// C - context
// D - dependencies
// R - result
type SlsEnvironment<E, C, D, P, R> = {
// but that may not be true as result should be handled by response
global: (func: Function) => SlsEnvironment<E, C, D, P, R>
payload: (payloadConstructor: PayloadConstructor<E, C, P>) => SlsEnvironment<E, C, D, P, R>
app: (app: AppConstructor<P, D, R>) => SlsEnvironment<E, C, D, P, R>
start: (event: E, context: C) => Promise<R>
}

type SlsEnvironmentConfig = {}

// maybe we have really generic environment and then we have wrappers for all different scenarios?
// with this level abstraction we would need to have awsEnv
// with this level abstraction we would need to have expressEnv
// with this level abstraction we would need to have googleFunctionEnv
export const environment = <E, C, D, P, R>(config?: SlsEnvironmentConfig): SlsEnvironment<E, C, D, P, R> => {
let appConstructor: AppConstructor<P, D, R>
// at the moment we are cheating
let dependencies: D = ({} as unknown) as D
// todo: we need to have something like no payload
let payloadFactory: PayloadConstructor<E, C, P> = (event, context) => (({ event, context } as unknown) as P)
return {
// dependency
// or maybe we have something similar to app and we have a function that
// creates dependencies
// dependencies(deps().global().global().prototype().create)
// dependencies(deps().global().global().prototype())
global(func: Function) {
dependencies = { ...dependencies, [func.name]: func }
return this
},
payload(payloadConstructor) {
payloadFactory = payloadConstructor
return this
},
app(appFactory) {
// maybe we wrap that with dependencies
// ({}) => appFactory
appConstructor = appFactory
// how can we remove usage of this
return this
},
start: async (event, context) => {
// now you can really chain that nicely
// maybe we start currying
//
// Promise.resolve().then(appConstructor(event, context))
const invocation = { event, context, dependencies }
return Promise.resolve(invocation)
.then(({ event, context }) => ({
payload: payloadFactory(event, context),
dependencies
}))
.then(appConstructor)
}
}
}
68 changes: 68 additions & 0 deletions packages/sls-env/tests/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { environment } from '../src'
describe('serverless environment', () => {
// here we will have adapter
// but for testing it should be enough for time being

type EmptyEvent = {}
type EmptyContext = {}
it('supports application', () =>
environment<EmptyEvent, EmptyContext, void, void, string>()
.app(() => 'hello world!')
.start({}, {})
.then(result => expect(result).toBe('hello world!')))

type MessageEvent = { message: string }
type NameContext = { name: string }
type EventPayload = { event: MessageEvent; context: NameContext }
it('supports passing an event and context to application', () =>
environment<MessageEvent, NameContext, void, EventPayload, string>()
.app(({ payload: { event, context } }) => `${event.message} ${context.name}!`)
.start({ message: 'hello' }, { name: 'world' })
.then(result => expect(result).toBe('hello world!')))

type BuildMessage = (message: string, name: string) => string
type BuildMessageDependencies = {
buildMessage: BuildMessage
}
it('supports adding dependencies to environment', () => {
const buildMessage: BuildMessage = (message, name) => `${message} ${name}!`

return environment<MessageEvent, NameContext, BuildMessageDependencies, EventPayload, string>()
.global(buildMessage)
.app(({ payload: { event, context }, dependencies: { buildMessage } }) =>
buildMessage(event.message, context.name)
)
.start({ message: 'hello' }, { name: 'world' })
.then(result => expect(result).toBe('hello world!'))
})

it.todo('supports adding not named dependencies to environment')

it('supports creating a payload for the application', () => {
type HelloWorldMessage = {
hello: string
world: string
}

type BuildHelloWorldMessage = (message: HelloWorldMessage) => string

const buildMessage: BuildHelloWorldMessage = message => `${message.hello} ${message.world}!`

type BuildHelloWorldMessageDependencies = {
buildMessage: BuildHelloWorldMessage
}

return environment<MessageEvent, NameContext, BuildHelloWorldMessageDependencies, HelloWorldMessage, string>()
.global(buildMessage)
.payload((event, context) => ({
hello: event.message,
world: context.name
}))
.app(({ payload, dependencies: { buildMessage } }) => buildMessage(payload))
.start({ message: 'hello' }, { name: 'world' })
.then(result => expect(result).toBe('hello world!'))
})
// google run against express
// it('runs request / response environment', () => environment().run(request, response))
// it('runs google function environment', () => environment().run(event, context))
})
9 changes: 9 additions & 0 deletions packages/sls-env/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"declaration": true
},
"include": ["src/**/*", "tests/**/*"],
"exclude": ["dist/**/*"]
}
Loading

0 comments on commit 70a9a36

Please sign in to comment.