-
Notifications
You must be signed in to change notification settings - Fork 146
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
Changes from 5 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
a365f8a
add middy middleware
am29d 2260f5b
add type to imports
am29d cd2fd60
merged
am29d 98144c2
add parser decorator
am29d 3bafad7
we can keep ParserOptions as type instead of interface
am29d 2ed22fe
changed decorator to async function
am29d File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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 = 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 }; |
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
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 }; |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
/** | ||
* 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({ envelope: eventBridgeEnvelope, schema: TestSchema }) | ||
public async handlerWithSchemaAndEnvelope( | ||
event: unknown, | ||
_context: Context | ||
): Promise<unknown> { | ||
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 return original event if no schema is provided', async () => {}); | ||
}); |
File renamed without changes.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this still keep the decorated method async? I haven't stepped through with the debugger, but I think with this implementation we are implicitly replacing an async method with a sync one and when calling it with the
apply()
we are returning an unresolved promise.Not sure if this is an issue but I think this is why we made all other decorators async.
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great observation! I checked in the debugger and the call
original.apply()
remains async, whiledescriptor.value
is of course sync function and thus all the method of the class will be replaced with a sync function, changing return type value instead of Promise.We could check
original.constructor.name === AsyncFunction
to decide what we should return. The more interesting case will bubble up once users want to bring their customtransforms
orrefines
which include async code. This would force us to exposeparseAsync
and branch the parsing for envelopes and decorator/middy.I'd suggest to change the modified
descriptor.value
to async, to keep it consistent with other decorator and we will circle back in a separate issue on this matter. We planned to address the sync/async decorators anyway.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea I agree that we should stick to async and keep things simple and consistent.
We discussed doing runtime checks for the constructor type to determine if it's sync or async when we implemented the Batch Processor and we eventually decided to drop the idea.
The main argument against this is that even though within the function body you might be able to do this check, the function signature will always have to be
async
, which then leads to the return type always being aPromise<T>
.