Skip to content

Commit

Permalink
Validate dynamic json payload upon creation. Closes #13
Browse files Browse the repository at this point in the history
  • Loading branch information
JakubTesarek committed Apr 26, 2021
1 parent a22ceea commit 1e001fb
Show file tree
Hide file tree
Showing 3 changed files with 36 additions and 7 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Added support for custom unauthorized response
- Added dynamic json responses
- Dynamic Route payload is validated upon creation

## [2.0.2] - 2021-04-23
### Fixed
Expand Down
11 changes: 10 additions & 1 deletion tests/unit/routing/test_routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,4 +269,13 @@ def test_as_flask_response_dynamic_list_item(self):

def test_default_headers(self):
response_body = JsonResponseBody({'key': 'value'})
assert response_body.default_headers == {'content-type': 'application/json'}
assert response_body.default_headers == {'content-type': 'application/json'}

@pytest.mark.parametrize('payload', [
{'$ref': '$.key1 + $.key2'},
[{'$ref': 'invalid payload'}],
{'key': {'$ref': 'invalid payload'}}
])
def test_raises_error_when_created_with_invalid_payload(self, payload):
with pytest.raises(RouteConfigurationError):
JsonResponseBody(payload)
31 changes: 25 additions & 6 deletions trickster/routing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,30 +139,49 @@ def deserialize(cls, data: Any) -> ResponseBody:
class JsonResponseBody(ResponseBody):
"""Json body of response."""

def __init__(self, content: Any):
self._validate_attribute(content)
super().__init__(content)

@property
def default_headers(self) -> Dict[str, str]:
"""Get default headers of response."""
return {'content-type': 'application/json'}

def as_flask_response(self, context: ResponseContext) -> str:
"""Convert response body to string within given context."""
return json.dumps(self._render(self.content, context))
return json.dumps(self._render_attribute(self.content, context))

def _is_dynamic_attr(self, attr: Dict[str, Any]) -> bool:
def _is_dynamic_attribute(self, attr: Dict[str, Any]) -> bool:
"""Return True if given attribute should be evaluated within context."""
return match_shema(attr, 'dynamic_attribute.schema.json')

def _render(self, attr: Any, context: ResponseContext) -> Any:
def _render_attribute(self, attr: Any, context: ResponseContext) -> Any:
"""Evaluate given attribute within context."""
if self._is_dynamic_attr(attr):
if self._is_dynamic_attribute(attr):
return context.get(attr['$ref'])
elif isinstance(attr, dict):
return {k: self._render(v, context) for k, v in attr.items()}
return {k: self._render_attribute(v, context) for k, v in attr.items()}
elif isinstance(attr, list):
return [self._render(v, context) for v in attr]
return [self._render_attribute(v, context) for v in attr]
else:
return attr

def _validate_attribute(self, attr: Any) -> None: # noqa: C901
"""Recursively validate given attribute."""
# TODO: This method is too similiar to `_render_attribute`. Refactor!
if self._is_dynamic_attribute(attr):
try:
jsonpath.parse(attr['$ref'])
except Exception as e:
raise RouteConfigurationError(f'Invalid jsonpath query "{attr["$ref"]}": {e}') from e
elif isinstance(attr, dict):
for v in attr.values():
self._validate_attribute(v)
elif isinstance(attr, list):
for v in attr:
self._validate_attribute(v)


class Response:
"""Container for predefined response."""
Expand Down

0 comments on commit 1e001fb

Please sign in to comment.