Skip to content

Commit 0012f92

Browse files
am29ddreamorosi
andauthored
feat(parser): implement safeParse option (#2244)
* first draft on safeParse with major refactoring * add safeParse * fixed sns tests * bump coverage * remove throw error and return ParsedResult * remove one level to reduce complexity score * make static methods readonly * simplified cryptic ternary operation into something readble * Update packages/parser/src/parserDecorator.ts Co-authored-by: Andrea Amorosi <[email protected]> * merged * simplify export * add invisible character for decorator rendering * fix docs and tests * Update packages/parser/src/parserDecorator.ts Co-authored-by: Andrea Amorosi <[email protected]> * add comment with description * remove context * remove unintentional safeParse export * add examples to parse standalone function --------- Co-authored-by: Andrea Amorosi <[email protected]>
1 parent b348b95 commit 0012f92

40 files changed

+2381
-631
lines changed

Diff for: packages/parser/src/envelopes/apigw.ts

+33-11
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,40 @@
1-
import { parse } from './envelope.js';
2-
import { z, ZodSchema } from 'zod';
1+
import { Envelope } from './envelope.js';
2+
import { z, type ZodSchema } from 'zod';
33
import { APIGatewayProxyEventSchema } from '../schemas/apigw.js';
4+
import type { ParsedResult } from '../types/parser.js';
45

56
/**
67
* API Gateway envelope to extract data within body key
78
*/
8-
export const apiGatewayEnvelope = <T extends ZodSchema>(
9-
data: unknown,
10-
schema: T
11-
): z.infer<T> => {
12-
const parsedEnvelope = APIGatewayProxyEventSchema.parse(data);
13-
if (!parsedEnvelope.body) {
14-
throw new Error('Body field of API Gateway event is undefined');
9+
export class ApiGatewayEnvelope extends Envelope {
10+
public static parse<T extends ZodSchema>(
11+
data: unknown,
12+
schema: T
13+
): z.infer<T> {
14+
return super.parse(APIGatewayProxyEventSchema.parse(data).body, schema);
1515
}
1616

17-
return parse(parsedEnvelope.body, schema);
18-
};
17+
public static safeParse<T extends ZodSchema>(
18+
data: unknown,
19+
schema: T
20+
): ParsedResult<unknown, z.infer<T>> {
21+
const parsedEnvelope = APIGatewayProxyEventSchema.safeParse(data);
22+
if (!parsedEnvelope.success) {
23+
return {
24+
...parsedEnvelope,
25+
originalEvent: data,
26+
};
27+
}
28+
29+
const parsedBody = super.safeParse(parsedEnvelope.data.body, schema);
30+
31+
if (!parsedBody.success) {
32+
return {
33+
...parsedBody,
34+
originalEvent: data,
35+
};
36+
}
37+
38+
return parsedBody;
39+
}
40+
}

Diff for: packages/parser/src/envelopes/apigwv2.ts

+33-11
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,40 @@
1-
import { parse } from './envelope.js';
2-
import { z, ZodSchema } from 'zod';
1+
import { z, type ZodSchema } from 'zod';
32
import { APIGatewayProxyEventV2Schema } from '../schemas/apigwv2.js';
3+
import { Envelope } from './envelope.js';
4+
import type { ParsedResult } from '../types/index.js';
45

56
/**
67
* API Gateway V2 envelope to extract data within body key
78
*/
8-
export const apiGatewayV2Envelope = <T extends ZodSchema>(
9-
data: unknown,
10-
schema: T
11-
): z.infer<T> => {
12-
const parsedEnvelope = APIGatewayProxyEventV2Schema.parse(data);
13-
if (!parsedEnvelope.body) {
14-
throw new Error('Body field of API Gateway event is undefined');
9+
export class ApiGatewayV2Envelope extends Envelope {
10+
public static parse<T extends ZodSchema>(
11+
data: unknown,
12+
schema: T
13+
): z.infer<T> {
14+
return super.parse(APIGatewayProxyEventV2Schema.parse(data).body, schema);
1515
}
1616

17-
return parse(parsedEnvelope.body, schema);
18-
};
17+
public static safeParse<T extends ZodSchema>(
18+
data: unknown,
19+
schema: T
20+
): ParsedResult {
21+
const parsedEnvelope = APIGatewayProxyEventV2Schema.safeParse(data);
22+
if (!parsedEnvelope.success) {
23+
return {
24+
...parsedEnvelope,
25+
originalEvent: data,
26+
};
27+
}
28+
29+
const parsedBody = super.safeParse(parsedEnvelope.data.body, schema);
30+
31+
if (!parsedBody.success) {
32+
return {
33+
...parsedBody,
34+
originalEvent: data,
35+
};
36+
}
37+
38+
return parsedBody;
39+
}
40+
}

Diff for: packages/parser/src/envelopes/cloudwatch.ts

+49-12
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { parse } from './envelope.js';
2-
import { z, ZodSchema } from 'zod';
3-
import { CloudWatchLogsSchema } from '../schemas/cloudwatch.js';
1+
import { z, type ZodSchema } from 'zod';
2+
import { Envelope } from './envelope.js';
3+
import { CloudWatchLogsSchema } from '../schemas/index.js';
4+
import type { ParsedResult } from '../types/index.js';
45

56
/**
67
* CloudWatch Envelope to extract a List of log records.
@@ -11,13 +12,49 @@ import { CloudWatchLogsSchema } from '../schemas/cloudwatch.js';
1112
*
1213
* Note: The record will be parsed the same way so if model is str
1314
*/
14-
export const cloudWatchEnvelope = <T extends ZodSchema>(
15-
data: unknown,
16-
schema: T
17-
): z.infer<T> => {
18-
const parsedEnvelope = CloudWatchLogsSchema.parse(data);
15+
export class CloudWatchEnvelope extends Envelope {
16+
public static parse<T extends ZodSchema>(
17+
data: unknown,
18+
schema: T
19+
): z.infer<T> {
20+
const parsedEnvelope = CloudWatchLogsSchema.parse(data);
1921

20-
return parsedEnvelope.awslogs.data.logEvents.map((record) => {
21-
return parse(record.message, schema);
22-
});
23-
};
22+
return parsedEnvelope.awslogs.data.logEvents.map((record) => {
23+
return super.parse(record.message, schema);
24+
});
25+
}
26+
27+
public static safeParse<T extends ZodSchema>(
28+
data: unknown,
29+
schema: T
30+
): ParsedResult {
31+
const parsedEnvelope = CloudWatchLogsSchema.safeParse(data);
32+
33+
if (!parsedEnvelope.success) {
34+
return {
35+
success: false,
36+
error: parsedEnvelope.error,
37+
originalEvent: data,
38+
};
39+
}
40+
const parsedLogEvents: z.infer<T>[] = [];
41+
42+
for (const record of parsedEnvelope.data.awslogs.data.logEvents) {
43+
const parsedMessage = super.safeParse(record.message, schema);
44+
if (!parsedMessage.success) {
45+
return {
46+
success: false,
47+
error: parsedMessage.error,
48+
originalEvent: data,
49+
};
50+
} else {
51+
parsedLogEvents.push(parsedMessage.data);
52+
}
53+
}
54+
55+
return {
56+
success: true,
57+
data: parsedLogEvents,
58+
};
59+
}
60+
}

Diff for: packages/parser/src/envelopes/dynamodb.ts

+56-13
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { parse } from './envelope.js';
2-
import { z, ZodSchema } from 'zod';
3-
import { DynamoDBStreamSchema } from '../schemas/dynamodb.js';
1+
import { z, type ZodSchema } from 'zod';
2+
import { DynamoDBStreamSchema } from '../schemas/index.js';
3+
import type { ParsedResult, ParsedResultError } from '../types/index.js';
4+
import { Envelope } from './envelope.js';
45

56
type DynamoDBStreamEnvelopeResponse<T extends ZodSchema> = {
67
NewImage: z.infer<T>;
@@ -13,16 +14,58 @@ type DynamoDBStreamEnvelopeResponse<T extends ZodSchema> = {
1314
* Note: Values are the parsed models. Images' values can also be None, and
1415
* length of the list is the record's amount in the original event.
1516
*/
16-
export const dynamoDDStreamEnvelope = <T extends ZodSchema>(
17-
data: unknown,
18-
schema: T
19-
): DynamoDBStreamEnvelopeResponse<T>[] => {
20-
const parsedEnvelope = DynamoDBStreamSchema.parse(data);
17+
export class DynamoDBStreamEnvelope extends Envelope {
18+
public static parse<T extends ZodSchema>(
19+
data: unknown,
20+
schema: T
21+
): DynamoDBStreamEnvelopeResponse<z.infer<T>>[] {
22+
const parsedEnvelope = DynamoDBStreamSchema.parse(data);
23+
24+
return parsedEnvelope.Records.map((record) => {
25+
return {
26+
NewImage: super.parse(record.dynamodb.NewImage, schema),
27+
OldImage: super.parse(record.dynamodb.OldImage, schema),
28+
};
29+
});
30+
}
31+
32+
public static safeParse<T extends ZodSchema>(
33+
data: unknown,
34+
schema: T
35+
): ParsedResult {
36+
const parsedEnvelope = DynamoDBStreamSchema.safeParse(data);
37+
38+
if (!parsedEnvelope.success) {
39+
return {
40+
success: false,
41+
error: parsedEnvelope.error,
42+
originalEvent: data,
43+
};
44+
}
45+
const parsedLogEvents: DynamoDBStreamEnvelopeResponse<z.infer<T>>[] = [];
46+
47+
for (const record of parsedEnvelope.data.Records) {
48+
const parsedNewImage = super.safeParse(record.dynamodb.NewImage, schema);
49+
const parsedOldImage = super.safeParse(record.dynamodb.OldImage, schema);
50+
if (!parsedNewImage.success || !parsedOldImage.success) {
51+
return {
52+
success: false,
53+
error: !parsedNewImage.success
54+
? parsedNewImage.error
55+
: (parsedOldImage as ParsedResultError<unknown>).error,
56+
originalEvent: data,
57+
};
58+
} else {
59+
parsedLogEvents.push({
60+
NewImage: parsedNewImage.data,
61+
OldImage: parsedOldImage.data,
62+
});
63+
}
64+
}
2165

22-
return parsedEnvelope.Records.map((record) => {
2366
return {
24-
NewImage: parse(record.dynamodb.NewImage, schema),
25-
OldImage: parse(record.dynamodb.OldImage, schema),
67+
success: true,
68+
data: parsedLogEvents,
2669
};
27-
});
28-
};
70+
}
71+
}

Diff for: packages/parser/src/envelopes/envelope.ts

+69-22
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,70 @@
1-
import { z, ZodSchema } from 'zod';
1+
import { z, type ZodSchema } from 'zod';
2+
import type { ParsedResult } from '../types/parser.js';
23

3-
/**
4-
* Abstract function to parse the content of the envelope using provided schema.
5-
* Both inputs are provided as unknown by the user.
6-
* We expect the data to be either string that can be parsed to json or object.
7-
* @internal
8-
* @param data data to parse
9-
* @param schema schema
10-
*/
11-
export const parse = <T extends ZodSchema>(
12-
data: unknown,
13-
schema: T
14-
): z.infer<T>[] => {
15-
if (typeof data === 'string') {
16-
return schema.parse(JSON.parse(data));
17-
} else if (typeof data === 'object') {
18-
return schema.parse(data);
19-
} else
20-
throw new Error(
21-
`Invalid data type for envelope. Expected string or object, got ${typeof data}`
22-
);
23-
};
4+
export class Envelope {
5+
/**
6+
* Abstract function to parse the content of the envelope using provided schema.
7+
* Both inputs are provided as unknown by the user.
8+
* We expect the data to be either string that can be parsed to json or object.
9+
* @internal
10+
* @param data data to parse
11+
* @param schema schema
12+
*/
13+
public static readonly parse = <T extends ZodSchema>(
14+
data: unknown,
15+
schema: T
16+
): z.infer<T> => {
17+
if (typeof data === 'string') {
18+
return schema.parse(JSON.parse(data));
19+
} else if (typeof data === 'object') {
20+
return schema.parse(data);
21+
} else
22+
throw new Error(
23+
`Invalid data type for envelope. Expected string or object, got ${typeof data}`
24+
);
25+
};
26+
27+
/**
28+
* Abstract function to safely parse the content of the envelope using provided schema.
29+
* safeParse is used to avoid throwing errors, thus we catuch all errors and wrap them in the result.
30+
* @param input
31+
* @param schema
32+
*/
33+
public static readonly safeParse = <T extends ZodSchema>(
34+
input: unknown,
35+
schema: T
36+
): ParsedResult<unknown, z.infer<T>> => {
37+
try {
38+
if (typeof input !== 'object' && typeof input !== 'string') {
39+
return {
40+
success: false,
41+
error: new Error(
42+
`Invalid data type for envelope. Expected string or object, got ${typeof input}`
43+
),
44+
originalEvent: input,
45+
};
46+
}
47+
48+
const parsed = schema.safeParse(
49+
typeof input === 'string' ? JSON.parse(input) : input
50+
);
51+
52+
return parsed.success
53+
? {
54+
success: true,
55+
data: parsed.data,
56+
}
57+
: {
58+
success: false,
59+
error: parsed.error,
60+
originalEvent: input,
61+
};
62+
} catch (e) {
63+
return {
64+
success: false,
65+
error: e as Error,
66+
originalEvent: input,
67+
};
68+
}
69+
};
70+
}

Diff for: packages/parser/src/envelopes/event-bridge.ts

+37-9
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,41 @@
1-
import { parse } from './envelope.js';
2-
import { z, ZodSchema } from 'zod';
3-
import { EventBridgeSchema } from '../schemas/eventbridge.js';
1+
import { Envelope } from './envelope.js';
2+
import { z, type ZodSchema } from 'zod';
3+
import { EventBridgeSchema } from '../schemas/index.js';
4+
import type { ParsedResult } from '../types/index.js';
45

56
/**
67
* Envelope for EventBridge schema that extracts and parses data from the `detail` key.
78
*/
8-
export const eventBridgeEnvelope = <T extends ZodSchema>(
9-
data: unknown,
10-
schema: T
11-
): z.infer<T> => {
12-
return parse(EventBridgeSchema.parse(data).detail, schema);
13-
};
9+
export class EventBridgeEnvelope extends Envelope {
10+
public static parse<T extends ZodSchema>(
11+
data: unknown,
12+
schema: T
13+
): z.infer<T> {
14+
return super.parse(EventBridgeSchema.parse(data).detail, schema);
15+
}
16+
17+
public static safeParse<T extends ZodSchema>(
18+
data: unknown,
19+
schema: T
20+
): ParsedResult {
21+
const parsedEnvelope = EventBridgeSchema.safeParse(data);
22+
23+
if (!parsedEnvelope.success) {
24+
return {
25+
...parsedEnvelope,
26+
originalEvent: data,
27+
};
28+
}
29+
30+
const parsedDetail = super.safeParse(parsedEnvelope.data.detail, schema);
31+
32+
if (!parsedDetail.success) {
33+
return {
34+
...parsedDetail,
35+
originalEvent: data,
36+
};
37+
}
38+
39+
return parsedDetail;
40+
}
41+
}

0 commit comments

Comments
 (0)