Skip to content

Commit

Permalink
Functional middleware (#85)
Browse files Browse the repository at this point in the history
* Make handler, middleware, coreHandler, etc, functional again

* Make CoreHandler match functional interface

* Update MiddlewareStack to use functional interfaces

* Update existing middleware to use functional interface

* Update middleware, stack, client, and command interfaces to allow combining command and client stacks

* Update SDK packages to use functional middleware interfaces
  • Loading branch information
jeskew authored and srchase committed Jun 16, 2023
1 parent a78d2e9 commit 38975a6
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 135 deletions.
10 changes: 8 additions & 2 deletions packages/config-resolver/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,16 @@ import {

export type IndexedObject = {[key: string]: any};

export function resolveConfiguration<T extends IndexedObject, R extends T>(
export function resolveConfiguration<
T extends IndexedObject,
R extends T,
Input extends object,
Output extends object,
Stream = Uint8Array
>(
providedConfiguration: T,
configurationDefinition: ConfigurationDefinition<T, R>,
middlewareStack: MiddlewareStack<any, any, any>
middlewareStack: MiddlewareStack<Input, Output, Stream>
): R {
const out: Partial<R> = {};

Expand Down
45 changes: 28 additions & 17 deletions packages/middleware-content-length/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,40 @@
import {
BuildHandler,
BuildHandlerArguments,
BuildMiddleware,
BodyLengthCalculator,
Handler,
HandlerArguments
} from '@aws/types';

export class ContentLengthMiddleware implements Handler<any, any, any> {
constructor(
private readonly bodyLengthCalculator: BodyLengthCalculator,
private readonly next: Handler<any, any, any>
) {}

async handle(args: HandlerArguments<any, any>): Promise<any> {
const request = args.request;

export function contentLengthMiddleware<
Input extends object,
Output extends object,
Stream
>(
bodyLengthCalculator: BodyLengthCalculator
): BuildMiddleware<Input, Output, Stream> {
return (
next: BuildHandler<Input, Output, Stream>
): BuildHandler<Input, Output, Stream> => async (
args: BuildHandlerArguments<Input, Stream>
): Promise<Output> => {
const {request} = args;
if (!request) {
throw new Error('Unable to determine request content-length due to missing request.');
}

if (request.body) {
request.headers['Content-Length'] = String(this.bodyLengthCalculator(request.body));
const {body, headers} = request;
if (
body &&
Object.keys(headers)
.map(str => str.toLowerCase())
.indexOf('content-length') === -1
) {
headers['Content-Length'] = String(bodyLengthCalculator(body));
}

return this.next.handle({
request,
...args
return next({
...args,
request
});
}
}
}
165 changes: 80 additions & 85 deletions packages/middleware-stack/src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,20 @@ import {MiddlewareStack} from "./";
import {
Handler,
HandlerArguments,
FinalizeHandlerArguments,
} from "@aws/types";

type input = Array<string>;
type output = object;

class ConcatMiddleware implements Handler<input, output> {
constructor(
private readonly message: string,
private readonly next: Handler<input, output>
) {}

handle(args: HandlerArguments<input>): Promise<output> {
return this.next.handle({
...args,
input: args.input.concat(this.message),
})
}
function concatMiddleware(
message: string,
next: Handler<input, output>
): Handler<input, output> {
return (args: HandlerArguments<input>): Promise<output> => next({
...args,
input: args.input.concat(message),
});
}

function shuffle<T>(arr: Array<T>): Array<T> {
Expand All @@ -36,16 +33,16 @@ describe('MiddlewareStack', () => {
const stack = new MiddlewareStack<input, output>();

const middleware = shuffle([
[ConcatMiddleware.bind(null, 'second')],
[ConcatMiddleware.bind(null, 'first'), {priority: 10}],
[ConcatMiddleware.bind(null, 'fourth'), {step: 'build'}],
[concatMiddleware.bind(null, 'second')],
[concatMiddleware.bind(null, 'first'), {priority: 10}],
[concatMiddleware.bind(null, 'fourth'), {step: 'build'}],
[
ConcatMiddleware.bind(null, 'third'),
concatMiddleware.bind(null, 'third'),
{step: 'build', priority: 1}
],
[ConcatMiddleware.bind(null, 'fifth'), {step: 'finalize'}],
[concatMiddleware.bind(null, 'fifth'), {step: 'finalize'}],
[
ConcatMiddleware.bind(null, 'sixth'),
concatMiddleware.bind(null, 'sixth'),
{step: 'finalize', priority: -1}
],
]);
Expand All @@ -54,128 +51,126 @@ describe('MiddlewareStack', () => {
stack.add(mw, options);
}

const inner = {
handle: jest.fn(({input}: HandlerArguments<input>) => {
expect(input).toEqual([
'first',
'second',
'third',
'fourth',
'fifth',
'sixth',
]);
return {};
})
};
const inner = jest.fn(({input}: FinalizeHandlerArguments<input>) => {
expect(input).toEqual([
'first',
'second',
'third',
'fourth',
'fifth',
'sixth',
]);
return {};
});

const composed = stack.resolve(inner, {} as any);
await composed.handle({input: []});
await composed({input: []});

expect(inner.handle.mock.calls.length).toBe(1);
expect(inner.mock.calls.length).toBe(1);
});

it('should allow cloning', async () => {
const stack = new MiddlewareStack<input, output>();
stack.add(ConcatMiddleware.bind(null, 'second'));
stack.add(ConcatMiddleware.bind(null, 'first'), {priority: 100});
stack.add(concatMiddleware.bind(null, 'second'));
stack.add(concatMiddleware.bind(null, 'first'), {priority: 100});

const secondStack = stack.clone();

let inner = {
handle: jest.fn(({input}: HandlerArguments<input>) => {
expect(input).toEqual([
'first',
'second',
]);
return Promise.resolve({});
})
};
await secondStack.resolve(inner, {} as any).handle({input: []});
expect(inner.handle.mock.calls.length).toBe(1);
let inner = jest.fn(({input}: FinalizeHandlerArguments<input>) => {
expect(input).toEqual([
'first',
'second',
]);
return Promise.resolve({});
});
await secondStack.resolve(inner, {} as any)({input: []});
expect(inner.mock.calls.length).toBe(1);
});

it('should allow combining stacks', async () => {
const stack = new MiddlewareStack<input, output>();
stack.add(ConcatMiddleware.bind(null, 'second'));
stack.add(ConcatMiddleware.bind(null, 'first'), {priority: 100});
stack.add(concatMiddleware.bind(null, 'second'));
stack.add(concatMiddleware.bind(null, 'first'), {priority: 100});

const secondStack = new MiddlewareStack<input, output>();
secondStack.add(ConcatMiddleware.bind(null, 'fourth'), {step: 'build'});
secondStack.add(concatMiddleware.bind(null, 'fourth'), {step: 'build'});
secondStack.add(
ConcatMiddleware.bind(null, 'third'),
concatMiddleware.bind(null, 'third'),
{step: 'build', priority: 100}
);

let inner = {
handle: jest.fn(({input}: HandlerArguments<input>) => {
expect(input).toEqual([
'first',
'second',
'third',
'fourth',
]);
return Promise.resolve({});
})
};
await stack.concat(secondStack).resolve(inner, {} as any)
.handle({input: []});

expect(inner.handle.mock.calls.length).toBe(1);
let inner = jest.fn(({input}: FinalizeHandlerArguments<input>) => {
expect(input).toEqual([
'first',
'second',
'third',
'fourth',
]);
return Promise.resolve({});
});
await stack.concat(secondStack).resolve(inner, {} as any)({input: []});

expect(inner.mock.calls.length).toBe(1);
});

it('should allow the removal of middleware by constructor identity', async () => {
const MyMiddleware = ConcatMiddleware.bind(null, 'remove me!');
const MyMiddleware = concatMiddleware.bind(null, 'remove me!');
const stack = new MiddlewareStack<input, output>();
stack.add(MyMiddleware);
stack.add(ConcatMiddleware.bind(null, "don't remove me"));
stack.add(concatMiddleware.bind(null, "don't remove me"));

await stack.resolve({
handle: ({input}: HandlerArguments<Array<string>>) => {
await stack.resolve(
({input}: FinalizeHandlerArguments<Array<string>>) => {
expect(input.sort()).toEqual([
"don't remove me",
'remove me!',
]);
return Promise.resolve({});
}
}, {} as any).handle({input: []});
},
{} as any
)({input: []});

stack.remove(MyMiddleware);

await stack.resolve({
handle: ({input}: HandlerArguments<Array<string>>) => {
await stack.resolve(
({input}: FinalizeHandlerArguments<Array<string>>) => {
expect(input).toEqual(["don't remove me"]);
return Promise.resolve({});
}
}, {} as any).handle({input: []});
},
{} as any
)({input: []});
});

it('should allow the removal of middleware by tag', async () => {
const stack = new MiddlewareStack<input, output>();
stack.add(
ConcatMiddleware.bind(null, 'not removed'),
concatMiddleware.bind(null, 'not removed'),
{tags: new Set(['foo', 'bar'])}
);
stack.add(
ConcatMiddleware.bind(null, 'remove me!'),
concatMiddleware.bind(null, 'remove me!'),
{tags: new Set(['foo', 'bar', 'baz'])}
);

await stack.resolve({
handle: ({input}: HandlerArguments<Array<string>>) => {
await stack.resolve(
({input}: FinalizeHandlerArguments<Array<string>>) => {
expect(input.sort()).toEqual([
'not removed',
'remove me!',
]);
return Promise.resolve({});
}
}, {} as any).handle({input: []});
},
{} as any
)({input: []});

stack.remove('baz');

await stack.resolve({
handle: ({input}: HandlerArguments<Array<string>>) => {
await stack.resolve(
({input}: FinalizeHandlerArguments<Array<string>>) => {
expect(input).toEqual(['not removed']);
return Promise.resolve({});
}
}, {} as any).handle({input: []});
},
{} as any
)({input: []});
});
});
Loading

0 comments on commit 38975a6

Please sign in to comment.