Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(parser): implement parser decorator #1831

Merged
merged 6 commits into from
Dec 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions packages/parser/src/middleware/parser.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import { type MiddyLikeRequest } from '@aws-lambda-powertools/commons/types';
import { type MiddlewareObj } from '@middy/core';
import { type ZodSchema } from 'zod';
import { type Envelope } from '../types/envelope.js';

interface ParserOptions<S extends ZodSchema> {
schema: S;
envelope?: Envelope;
}
import { type ParserOptions } from '../types/ParserOptions.js';

/**
* A middiy middleware to parse your event.
Expand Down
59 changes: 59 additions & 0 deletions packages/parser/src/parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { HandlerMethodDecorator } from '@aws-lambda-powertools/commons/types';
import { Context, Handler } from 'aws-lambda';
import { ZodSchema } from 'zod';
import { type ParserOptions } from './types/ParserOptions.js';

/**
* A decorator to parse your event.
*
* @example
* ```typescript
*
* import { parser } from '@aws-lambda-powertools/parser';
* import { sqsEnvelope } from '@aws-lambda-powertools/parser/envelopes/sqs';
*
*
* const Order = z.object({
* orderId: z.string(),
* description: z.string(),
* }
*
* class Lambda extends LambdaInterface {
*
* @parser({ envelope: sqsEnvelope, schema: OrderSchema })
* public async handler(event: Order, _context: Context): Promise<unknown> {
* // sqs event is parsed and the payload is extracted and parsed
* // apply business logic to your Order event
* const res = processOrder(event);
* return res;
* }
* }
*
* @param options
*/
const parser = <S extends ZodSchema>(
options: ParserOptions<S>
): HandlerMethodDecorator => {
return (_target, _propertyKey, descriptor) => {
const original = descriptor.value!;

const { schema, envelope } = options;

descriptor.value = async function (
this: Handler,
event: unknown,
context: Context,
callback
) {
const parsedEvent = envelope
? envelope(event, schema)
: schema.parse(event);

return original.call(this, parsedEvent, context, callback);
};

return descriptor;
};
};

export { parser };
9 changes: 9 additions & 0 deletions packages/parser/src/types/ParserOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { ZodSchema } from 'zod';
import { Envelope } from './envelope.js';

type ParserOptions<S extends ZodSchema> = {
schema: S;
envelope?: Envelope;
};

export { type ParserOptions };
118 changes: 118 additions & 0 deletions packages/parser/tests/unit/parser.decorator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/**
* Test decorator parser
*
* @group unit/parser
*/

import { LambdaInterface } from '@aws-lambda-powertools/commons/lib/esm/types';
import { Context, EventBridgeEvent } from 'aws-lambda';
import { parser } from '../../src/parser';
import { TestSchema, TestEvents } from './schema/utils';
import { generateMock } from '@anatine/zod-mock';
import { eventBridgeEnvelope } from '../../src/envelopes/event-bridge';
import { EventBridgeSchema } from '../../src/schemas/eventbridge';
import { z } from 'zod';

describe('Parser Decorator', () => {
const customEventBridgeSchema = EventBridgeSchema.extend({
detail: TestSchema,
});

type TestSchema = z.infer<typeof TestSchema>;

class TestClass implements LambdaInterface {
@parser({ schema: TestSchema })
public async handler(
event: TestSchema,
_context: Context
): Promise<unknown> {
return event;
}

@parser({ schema: customEventBridgeSchema })
public async handlerWithCustomSchema(
event: unknown,
_context: Context
): Promise<unknown> {
return event;
}

@parser({ schema: TestSchema, envelope: eventBridgeEnvelope })
public async handlerWithParserCallsAnotherMethod(
event: unknown,
_context: Context
): Promise<unknown> {
return this.anotherMethod(event as TestSchema);
}

@parser({ envelope: eventBridgeEnvelope, schema: TestSchema })
public async handlerWithSchemaAndEnvelope(
event: unknown,
_context: Context
): Promise<unknown> {
return event;
}

private async anotherMethod(event: TestSchema): Promise<TestSchema> {
return event;
}
}

const lambda = new TestClass();

it('should parse custom schema event', async () => {
const testEvent = generateMock(TestSchema);

const resp = await lambda.handler(testEvent, {} as Context);

expect(resp).toEqual(testEvent);
});

it('should parse custom schema with envelope event', async () => {
const customPayload = generateMock(TestSchema);
const testEvent = TestEvents.eventBridgeEvent as EventBridgeEvent<
string,
unknown
>;
testEvent.detail = customPayload;

const resp = await lambda.handlerWithSchemaAndEnvelope(
testEvent,
{} as Context
);

expect(resp).toEqual(customPayload);
});

it('should parse extended envelope event', async () => {
const customPayload = generateMock(TestSchema);

const testEvent = generateMock(customEventBridgeSchema);
testEvent.detail = customPayload;

const resp: z.infer<typeof customEventBridgeSchema> =
(await lambda.handlerWithCustomSchema(
testEvent,
{} as Context
)) as z.infer<typeof customEventBridgeSchema>;

expect(customEventBridgeSchema.parse(resp)).toEqual(testEvent);
expect(resp.detail).toEqual(customPayload);
});

it('should parse and call private async method', async () => {
const customPayload = generateMock(TestSchema);
const testEvent = TestEvents.eventBridgeEvent as EventBridgeEvent<
string,
unknown
>;
testEvent.detail = customPayload;

const resp = await lambda.handlerWithParserCallsAnotherMethod(
testEvent,
{} as Context
);

expect(resp).toEqual(customPayload);
});
});