Skip to content

Commit 7c72c97

Browse files
jeskewsrchase
authored andcommitted
Add a middleware stack implementation (#42)
* Add a middleware stack implementation * Ensure @aws/types is always compiled during the pretest phase * Restructure middleware stack to reflect new repository format
1 parent 65ffa92 commit 7c72c97

File tree

6 files changed

+324
-0
lines changed

6 files changed

+324
-0
lines changed

packages/middleware-stack/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
*.js
2+
*.js.map
3+
*.d.ts
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "@aws/middleware-stack",
3+
"private": true,
4+
"version": "0.0.1",
5+
"description": "Provides a means for composing multiple middleware functions into a single handler",
6+
"scripts": {
7+
"prepublishOnly": "tsc",
8+
"pretest": "tsc -p tsconfig.test.json",
9+
"test": "jest"
10+
},
11+
"author": "[email protected]",
12+
"license": "Apache-2.0",
13+
"main": "./build/index.js",
14+
"types": "./build/index.d.ts",
15+
"dependencies": {
16+
"@aws/types": "^0.0.1"
17+
},
18+
"devDependencies": {
19+
"@types/jest": "^20.0.2",
20+
"jest": "^20.0.4",
21+
"typescript": "^2.3"
22+
}
23+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import {MiddlewareStack} from "./";
2+
import {HandlerArguments, Middleware} from "@aws/types";
3+
4+
describe('MiddlewareStack', () => {
5+
it('should resolve the stack into a composed handler', async () => {
6+
const stack = new MiddlewareStack<Array<string>, object>();
7+
stack.prependInit(next => args => next({
8+
...args,
9+
input: args.input.concat('first')
10+
}));
11+
stack.appendBuild(next => args => next({
12+
...args,
13+
input: args.input.concat('fourth')
14+
}));
15+
stack.appendInit(next => args => next({
16+
...args,
17+
input: args.input.concat('second')
18+
}));
19+
stack.prependBuild(next => args => next({
20+
...args,
21+
input: args.input.concat('third')
22+
}));
23+
stack.appendSign(next => args => next({
24+
...args,
25+
input: args.input.concat('sixth')
26+
}));
27+
stack.prependSign(next => args => next({
28+
...args,
29+
input: args.input.concat('fifth')
30+
}));
31+
32+
const inner = jest.fn(({input}: HandlerArguments<Array<string>>) => {
33+
expect(input).toEqual([
34+
'first',
35+
'second',
36+
'third',
37+
'fourth',
38+
'fifth',
39+
'sixth',
40+
]);
41+
return {};
42+
});
43+
const composed = stack.resolve(inner);
44+
await composed({input: []});
45+
46+
expect(inner.mock.calls.length).toBe(1);
47+
});
48+
49+
it('should allow cloning', async () => {
50+
const stack = new MiddlewareStack<Array<string>, object>();
51+
stack.appendInit(next => args => next({
52+
...args,
53+
input: args.input.concat('second')
54+
}));
55+
stack.prependInit(next => args => next({
56+
...args,
57+
input: args.input.concat('first')
58+
}));
59+
60+
const secondStack = stack.clone();
61+
62+
let inner = jest.fn(({input}: HandlerArguments<Array<string>>) => {
63+
expect(input).toEqual([
64+
'first',
65+
'second',
66+
]);
67+
return Promise.resolve({});
68+
});
69+
await secondStack.resolve(inner)({input: []});
70+
expect(inner.mock.calls.length).toBe(1);
71+
});
72+
73+
it('should allow the removal of middleware by object identity', async () => {
74+
const stack = new MiddlewareStack<Array<string>, object>();
75+
let middleware: Middleware<Array<string>, object> = next => args => next({
76+
...args,
77+
input: args.input.concat('first')
78+
});
79+
stack.prependInit(middleware);
80+
81+
await stack.resolve(({input}: HandlerArguments<Array<string>>) => {
82+
expect(input).toEqual([
83+
'first',
84+
]);
85+
return Promise.resolve({});
86+
})({input: []});
87+
88+
stack.remove(middleware);
89+
90+
await stack.resolve(({input}: HandlerArguments<Array<string>>) => {
91+
expect(input).toEqual([]);
92+
return Promise.resolve({});
93+
})({input: []});
94+
});
95+
96+
it('should allow the removal of middleware by name', async () => {
97+
const stack = new MiddlewareStack<Array<string>, object>();
98+
stack.prependInit(next => args => next({
99+
...args,
100+
input: args.input.concat('first')
101+
}), 'first');
102+
103+
await stack.resolve(({input}: HandlerArguments<Array<string>>) => {
104+
expect(input).toEqual([
105+
'first',
106+
]);
107+
return Promise.resolve({});
108+
})({input: []});
109+
110+
stack.remove('first');
111+
112+
await stack.resolve(({input}: HandlerArguments<Array<string>>) => {
113+
expect(input).toEqual([]);
114+
return Promise.resolve({});
115+
})({input: []});
116+
});
117+
});
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import {Handler, Middleware} from '@aws/types';
2+
3+
interface Step<
4+
InputType extends object,
5+
OutputType extends object,
6+
StreamType
7+
> {
8+
middleware: Middleware<InputType, OutputType, StreamType>;
9+
name?: string;
10+
}
11+
12+
/**
13+
* Builds a single handler function from zero or more middleware functions and a
14+
* handler. The single handler is meant to send command objects to AWS services
15+
* and return promises that will resolve with the operation result or be
16+
* rejected with an error.
17+
*
18+
* When a composed handler is invoked, the arguments will pass through all
19+
* middleware in a defined order, and the return from the innermost handler will
20+
* pass through all middleware in the reverse of that order. Handlers are
21+
* ordered using a "step" that describes the step at which the SDK is when
22+
* sending a command. The available steps are:
23+
*
24+
* - init: The command is being initialized, allowing you to do things like add
25+
* default options.
26+
* - build: The command is being serialized into an HTTP request.
27+
* - sign: The request is being signed and prepared to be sent over the wire.
28+
*
29+
* You can add middleware to the top of the stack (i.e., make middleware the
30+
* first to receive incoming arguments and the last to receive a response) by
31+
* using the "prependInit" method or to the bottom of the stack (i.e., the first
32+
* to receive the response and the last to receive incoming arguments) by using
33+
* the "appendSign" method. Each step has comparable "prepend" and "append"
34+
* methods.
35+
*
36+
* Middleware can be registered with a name to allow you to easily add a
37+
* middleware before or after another middleware by name. This also allows you
38+
* to remove a middleware by name (in addition to removing by instance).
39+
*/
40+
export class MiddlewareStack<
41+
InputType extends object,
42+
OutputType extends object,
43+
StreamType = Uint8Array
44+
> {
45+
private initSteps: Array<Step<InputType, OutputType, StreamType>> = [];
46+
private buildSteps: Array<Step<InputType, OutputType, StreamType>> = [];
47+
private signSteps: Array<Step<InputType, OutputType, StreamType>> = [];
48+
49+
/**
50+
* Add middleware to be executed after other members of the build phase.
51+
*/
52+
appendBuild(
53+
middleware: Middleware<InputType, OutputType, StreamType>,
54+
name?: string
55+
): void {
56+
this.buildSteps.unshift({middleware, name});
57+
}
58+
59+
/**
60+
* Add middleware to be executed after other members of the init phase.
61+
*/
62+
appendInit(
63+
middleware: Middleware<InputType, OutputType, StreamType>,
64+
name?: string
65+
): void {
66+
this.initSteps.unshift({middleware, name});
67+
}
68+
69+
/**
70+
* Add middleware to be executed after other members of the sign phase.
71+
*/
72+
appendSign(
73+
middleware: Middleware<InputType, OutputType, StreamType>,
74+
name?: string
75+
): void {
76+
this.signSteps.unshift({middleware, name});
77+
}
78+
79+
/**
80+
* Copy the stack into a new instance.
81+
*/
82+
clone(): MiddlewareStack<InputType, OutputType, StreamType> {
83+
const stack = new MiddlewareStack<InputType, OutputType, StreamType>();
84+
stack.initSteps = this.initSteps.slice(0);
85+
stack.buildSteps = this.buildSteps.slice(0);
86+
stack.signSteps = this.signSteps.slice(0);
87+
return stack;
88+
}
89+
90+
/**
91+
* Add middleware to be executed before other members of the build phase.
92+
*/
93+
prependBuild(
94+
middleware: Middleware<InputType, OutputType, StreamType>,
95+
name?: string
96+
): void {
97+
this.buildSteps.push({middleware, name});
98+
}
99+
100+
/**
101+
* Add middleware to be executed before other members of the init phase.
102+
*/
103+
prependInit(
104+
middleware: Middleware<InputType, OutputType, StreamType>,
105+
name?: string
106+
): void {
107+
this.initSteps.push({middleware, name});
108+
}
109+
110+
/**
111+
* Add middleware to be executed before other members of the sign phase.
112+
*/
113+
prependSign(
114+
middleware: Middleware<InputType, OutputType, StreamType>,
115+
name?: string
116+
): void {
117+
this.signSteps.push({middleware, name});
118+
}
119+
120+
/**
121+
* Remove middleware with the provided name or identity.
122+
*/
123+
remove(nameOrType: string|Middleware<InputType, OutputType, StreamType>): void {
124+
if (typeof nameOrType === 'string') {
125+
this.filterSteps(named => nameOrType !== named.name);
126+
} else {
127+
this.filterSteps(named => nameOrType !== named.middleware);
128+
}
129+
}
130+
131+
/**
132+
* Reduce the stack into a composite handler function.
133+
*/
134+
resolve(
135+
handler: Handler<InputType, OutputType, StreamType>
136+
): Handler<InputType, OutputType, StreamType> {
137+
for (let steps of [this.signSteps, this.buildSteps, this.initSteps]) {
138+
for (let step of steps) {
139+
handler = step.middleware(handler);
140+
}
141+
}
142+
143+
return handler;
144+
}
145+
146+
private filterSteps(
147+
predicate: (
148+
named: Step<InputType, OutputType, StreamType>
149+
) => boolean
150+
): void {
151+
this.initSteps = this.initSteps.filter(predicate);
152+
this.buildSteps = this.buildSteps.filter(predicate);
153+
this.signSteps = this.signSteps.filter(predicate);
154+
}
155+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"compilerOptions": {
3+
"target": "es5",
4+
"module": "commonjs",
5+
"declaration": true,
6+
"strict": true,
7+
"sourceMap": true,
8+
"lib": [
9+
"es5",
10+
"es2015.promise",
11+
"es2015.collection"
12+
],
13+
"rootDir": "./src",
14+
"outDir": "./build"
15+
}
16+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"compilerOptions": {
4+
"sourceMap": false,
5+
"inlineSourceMap": true,
6+
"inlineSources": true,
7+
"rootDir": "./src",
8+
"outDir": "./build"
9+
}
10+
}

0 commit comments

Comments
 (0)