Skip to content

Commit 5ee14b1

Browse files
authored
Merge pull request #153 from heitorlessa/feat/validator-utility
feat: simple JSON Schema validator utility
2 parents 063d8cd + f7f3c99 commit 5ee14b1

File tree

15 files changed

+1152
-90
lines changed

15 files changed

+1152
-90
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## [Unreleased]
88

9+
### Added
10+
- **Utilities**: Add new `Validator` utility to validate inbound events and responses using JSON Schema
11+
912
## [1.5.0] - 2020-09-04
1013

1114
### Added

Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ target:
44

55
dev:
66
pip install --upgrade pip poetry pre-commit
7-
poetry install
7+
poetry install --extras "jmespath"
88
pre-commit install
99

1010
dev-docs:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""
2+
Simple validator to enforce incoming/outgoing event conforms with JSON Schema
3+
"""
4+
5+
from .exceptions import InvalidEnvelopeExpressionError, InvalidSchemaFormatError, SchemaValidationError
6+
from .validator import validate, validator
7+
8+
__all__ = [
9+
"validate",
10+
"validator",
11+
"InvalidSchemaFormatError",
12+
"SchemaValidationError",
13+
"InvalidEnvelopeExpressionError",
14+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import logging
2+
from typing import Any, Dict
3+
4+
import fastjsonschema
5+
import jmespath
6+
from jmespath.exceptions import LexerError
7+
8+
from .exceptions import InvalidEnvelopeExpressionError, InvalidSchemaFormatError, SchemaValidationError
9+
from .jmespath_functions import PowertoolsFunctions
10+
11+
logger = logging.getLogger(__name__)
12+
13+
14+
def validate_data_against_schema(data: Dict, schema: Dict):
15+
"""Validate dict data against given JSON Schema
16+
17+
Parameters
18+
----------
19+
data : Dict
20+
Data set to be validated
21+
schema : Dict
22+
JSON Schema to validate against
23+
24+
Raises
25+
------
26+
SchemaValidationError
27+
When schema validation fails against data set
28+
InvalidSchemaFormatError
29+
When JSON schema provided is invalid
30+
"""
31+
try:
32+
fastjsonschema.validate(definition=schema, data=data)
33+
except fastjsonschema.JsonSchemaException as e:
34+
message = f"Failed schema validation. Error: {e.message}, Path: {e.path}, Data: {e.value}" # noqa: B306, E501
35+
raise SchemaValidationError(message)
36+
except (TypeError, AttributeError) as e:
37+
raise InvalidSchemaFormatError(f"Schema received: {schema}. Error: {e}")
38+
39+
40+
def unwrap_event_from_envelope(data: Dict, envelope: str, jmespath_options: Dict) -> Any:
41+
"""Searches data using JMESPath expression
42+
43+
Parameters
44+
----------
45+
data : Dict
46+
Data set to be filtered
47+
envelope : str
48+
JMESPath expression to filter data against
49+
jmespath_options : Dict
50+
Alternative JMESPath options to be included when filtering expr
51+
52+
Returns
53+
-------
54+
Any
55+
Data found using JMESPath expression given in envelope
56+
"""
57+
if not jmespath_options:
58+
jmespath_options = {"custom_functions": PowertoolsFunctions()}
59+
60+
try:
61+
logger.debug(f"Envelope detected: {envelope}. JMESPath options: {jmespath_options}")
62+
return jmespath.search(envelope, data, options=jmespath.Options(**jmespath_options))
63+
except (LexerError, TypeError, UnicodeError) as e:
64+
message = f"Failed to unwrap event from envelope using expression. Error: {e} Exp: {envelope}, Data: {data}" # noqa: B306, E501
65+
raise InvalidEnvelopeExpressionError(message)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""Built-in envelopes"""
2+
3+
API_GATEWAY_REST = "powertools_json(body)"
4+
API_GATEWAY_HTTP = API_GATEWAY_REST
5+
SQS = "Records[*].powertools_json(body)"
6+
SNS = "Records[0].Sns.Message | powertools_json(@)"
7+
EVENTBRIDGE = "detail"
8+
CLOUDWATCH_EVENTS_SCHEDULED = EVENTBRIDGE
9+
KINESIS_DATA_STREAM = "Records[*].kinesis.powertools_json(powertools_base64(data))"
10+
CLOUDWATCH_LOGS = "awslogs.powertools_base64_gzip(data) | powertools_json(@).logEvents[*]"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
class SchemaValidationError(Exception):
2+
"""When serialization fail schema validation"""
3+
4+
pass
5+
6+
7+
class InvalidSchemaFormatError(Exception):
8+
"""When JSON Schema is in invalid format"""
9+
10+
pass
11+
12+
13+
class InvalidEnvelopeExpressionError(Exception):
14+
"""When JMESPath fails to parse expression"""
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import base64
2+
import gzip
3+
import json
4+
5+
import jmespath
6+
7+
8+
class PowertoolsFunctions(jmespath.functions.Functions):
9+
@jmespath.functions.signature({"types": ["string"]})
10+
def _func_powertools_json(self, value):
11+
return json.loads(value)
12+
13+
@jmespath.functions.signature({"types": ["string"]})
14+
def _func_powertools_base64(self, value):
15+
return base64.b64decode(value).decode()
16+
17+
@jmespath.functions.signature({"types": ["string"]})
18+
def _func_powertools_base64_gzip(self, value):
19+
encoded = base64.b64decode(value)
20+
uncompressed = gzip.decompress(encoded)
21+
22+
return uncompressed.decode()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import logging
2+
from typing import Any, Callable, Dict, Union
3+
4+
from ...middleware_factory import lambda_handler_decorator
5+
from .base import unwrap_event_from_envelope, validate_data_against_schema
6+
7+
logger = logging.getLogger(__name__)
8+
9+
10+
@lambda_handler_decorator
11+
def validator(
12+
handler: Callable,
13+
event: Union[Dict, str],
14+
context: Any,
15+
inbound_schema: Dict = None,
16+
outbound_schema: Dict = None,
17+
envelope: str = None,
18+
jmespath_options: Dict = None,
19+
) -> Any:
20+
"""Lambda handler decorator to validate incoming/outbound data using a JSON Schema
21+
22+
Example
23+
-------
24+
25+
**Validate incoming event**
26+
27+
from aws_lambda_powertools.utilities.validation import validator
28+
29+
@validator(inbound_schema=json_schema_dict)
30+
def handler(event, context):
31+
return event
32+
33+
**Validate incoming and outgoing event**
34+
35+
from aws_lambda_powertools.utilities.validation import validator
36+
37+
@validator(inbound_schema=json_schema_dict, outbound_schema=response_json_schema_dict)
38+
def handler(event, context):
39+
return event
40+
41+
**Unwrap event before validating against actual payload - using built-in envelopes**
42+
43+
from aws_lambda_powertools.utilities.validation import validator, envelopes
44+
45+
@validator(inbound_schema=json_schema_dict, envelope=envelopes.API_GATEWAY_REST)
46+
def handler(event, context):
47+
return event
48+
49+
**Unwrap event before validating against actual payload - using custom JMESPath expression**
50+
51+
from aws_lambda_powertools.utilities.validation import validator
52+
53+
@validator(inbound_schema=json_schema_dict, envelope="payload[*].my_data")
54+
def handler(event, context):
55+
return event
56+
57+
**Unwrap and deserialize JSON string event before validating against actual payload - using built-in functions**
58+
59+
from aws_lambda_powertools.utilities.validation import validator
60+
61+
@validator(inbound_schema=json_schema_dict, envelope="Records[*].powertools_json(body)")
62+
def handler(event, context):
63+
return event
64+
65+
**Unwrap, decode base64 and deserialize JSON string event before validating against actual payload - using built-in functions** # noqa: E501
66+
67+
from aws_lambda_powertools.utilities.validation import validator
68+
69+
@validator(inbound_schema=json_schema_dict, envelope="Records[*].kinesis.powertools_json(powertools_base64(data))")
70+
def handler(event, context):
71+
return event
72+
73+
**Unwrap, decompress ZIP archive and deserialize JSON string event before validating against actual payload - using built-in functions** # noqa: E501
74+
75+
from aws_lambda_powertools.utilities.validation import validator
76+
77+
@validator(inbound_schema=json_schema_dict, envelope="awslogs.powertools_base64_gzip(data) | powertools_json(@).logEvents[*]")
78+
def handler(event, context):
79+
return event
80+
81+
Parameters
82+
----------
83+
handler : Callable
84+
Method to annotate on
85+
event : Dict
86+
Lambda event to be validated
87+
context : Any
88+
Lambda context object
89+
inbound_schema : Dict
90+
JSON Schema to validate incoming event
91+
outbound_schema : Dict
92+
JSON Schema to validate outbound event
93+
envelope : Dict
94+
JMESPath expression to filter data against
95+
jmespath_options : Dict
96+
Alternative JMESPath options to be included when filtering expr
97+
98+
Returns
99+
-------
100+
Any
101+
Lambda handler response
102+
103+
Raises
104+
------
105+
SchemaValidationError
106+
When schema validation fails against data set
107+
InvalidSchemaFormatError
108+
When JSON schema provided is invalid
109+
InvalidEnvelopeExpressionError
110+
When JMESPath expression to unwrap event is invalid
111+
"""
112+
if envelope:
113+
event = unwrap_event_from_envelope(data=event, envelope=envelope, jmespath_options=jmespath_options)
114+
115+
if inbound_schema:
116+
logger.debug("Validating inbound event")
117+
validate_data_against_schema(data=event, schema=inbound_schema)
118+
119+
response = handler(event, context)
120+
121+
if outbound_schema:
122+
logger.debug("Validating outbound event")
123+
validate_data_against_schema(data=response, schema=outbound_schema)
124+
125+
return response
126+
127+
128+
def validate(event: Dict, schema: Dict = None, envelope: str = None, jmespath_options: Dict = None):
129+
"""Standalone function to validate event data using a JSON Schema
130+
131+
Typically used when you need more control over the validation process.
132+
133+
**Validate event**
134+
135+
from aws_lambda_powertools.utilities.validation import validate
136+
137+
def handler(event, context):
138+
validate(event=event, schema=json_schema_dict)
139+
return event
140+
141+
**Unwrap event before validating against actual payload - using built-in envelopes**
142+
143+
from aws_lambda_powertools.utilities.validation import validate, envelopes
144+
145+
def handler(event, context):
146+
validate(event=event, schema=json_schema_dict, envelope=envelopes.API_GATEWAY_REST)
147+
return event
148+
149+
**Unwrap event before validating against actual payload - using custom JMESPath expression**
150+
151+
from aws_lambda_powertools.utilities.validation import validate
152+
153+
def handler(event, context):
154+
validate(event=event, schema=json_schema_dict, envelope="payload[*].my_data")
155+
return event
156+
157+
**Unwrap and deserialize JSON string event before validating against actual payload - using built-in functions**
158+
159+
from aws_lambda_powertools.utilities.validation import validate
160+
161+
def handler(event, context):
162+
validate(event=event, schema=json_schema_dict, envelope="Records[*].powertools_json(body)")
163+
return event
164+
165+
**Unwrap, decode base64 and deserialize JSON string event before validating against actual payload - using built-in functions**
166+
167+
from aws_lambda_powertools.utilities.validation import validate
168+
169+
def handler(event, context):
170+
validate(event=event, schema=json_schema_dict, envelope="Records[*].kinesis.powertools_json(powertools_base64(data))")
171+
return event
172+
173+
**Unwrap, decompress ZIP archive and deserialize JSON string event before validating against actual payload - using built-in functions** # noqa: E501
174+
175+
from aws_lambda_powertools.utilities.validation import validate
176+
177+
def handler(event, context):
178+
validate(event=event, schema=json_schema_dict, envelope="awslogs.powertools_base64_gzip(data) | powertools_json(@).logEvents[*]")
179+
return event
180+
181+
Parameters
182+
----------
183+
event : Dict
184+
Lambda event to be validated
185+
schema : Dict
186+
JSON Schema to validate incoming event
187+
envelope : Dict
188+
JMESPath expression to filter data against
189+
jmespath_options : Dict
190+
Alternative JMESPath options to be included when filtering expr
191+
192+
Raises
193+
------
194+
SchemaValidationError
195+
When schema validation fails against data set
196+
InvalidSchemaFormatError
197+
When JSON schema provided is invalid
198+
InvalidEnvelopeExpressionError
199+
When JMESPath expression to unwrap event is invalid
200+
"""
201+
if envelope:
202+
event = unwrap_event_from_envelope(data=event, envelope=envelope, jmespath_options=jmespath_options)
203+
204+
validate_data_against_schema(data=event, schema=schema)

0 commit comments

Comments
 (0)