-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Fix apigw input path formatting #12379
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
Changes from all commits
93343f5
9cb1524
2cf4d0f
93014c9
b8b9e50
2d6b6fd
00e191b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,6 +21,9 @@ | |
from typing import Any, TypedDict | ||
from urllib.parse import quote_plus, unquote_plus | ||
|
||
import airspeed | ||
from airspeed.operators import dict_to_string | ||
|
||
from localstack import config | ||
from localstack.services.apigateway.next_gen.execute_api.variables import ( | ||
ContextVariables, | ||
|
@@ -50,6 +53,74 @@ class MappingTemplateVariables(TypedDict, total=False): | |
stageVariables: dict[str, str] | ||
|
||
|
||
def cast_to_vtl_object(value): | ||
if isinstance(value, dict): | ||
return VTLMap(value) | ||
if isinstance(value, list): | ||
return [cast_to_vtl_object(item) for item in value] | ||
return value | ||
|
||
|
||
def cast_to_vtl_json_object(value: Any) -> Any: | ||
if isinstance(value, dict): | ||
return VTLJsonDict(value) | ||
if isinstance(value, list): | ||
return VTLJsonList(value) | ||
return value | ||
|
||
|
||
class VTLMap(dict): | ||
"""Overrides __str__ of python dict (and all child dict) to return a Java like string representation""" | ||
|
||
# TODO apply this class more generally through the template mappings | ||
|
||
def __init__(self, *args, **kwargs): | ||
super().__init__(*args, **kwargs) | ||
self.update(*args, **kwargs) | ||
|
||
@staticmethod | ||
def cast_factory(value: Any) -> Any: | ||
return cast_to_vtl_object(value) | ||
|
||
def update(self, *args, **kwargs): | ||
for k, v in self.items(): | ||
self[k] = self.cast_factory(v) | ||
|
||
def __str__(self) -> str: | ||
return dict_to_string(self) | ||
|
||
|
||
class VTLJsonList(list): | ||
"""Some VTL List behave differently when being represented as string and everything | ||
inside will be represented as a json string | ||
|
||
Example: $input.path('$').b // Where path is {"a": 1, "b": [{"c": 5}]} | ||
Results: '[{"c":5}]' // Where everything inside the list is a valid json object | ||
""" | ||
|
||
def __init__(self, *args): | ||
super(VTLJsonList, self).__init__(*args) | ||
for idx, item in enumerate(self): | ||
self[idx] = cast_to_vtl_json_object(item) | ||
|
||
def __str__(self): | ||
if isinstance(self, list): | ||
return json.dumps(self, separators=(",", ":")) | ||
|
||
|
||
class VTLJsonDict(VTLMap): | ||
"""Some VTL Map behave differently when being represented as string and a list | ||
encountered in the dictionary will be represented as a json string | ||
|
||
Example: $input.path('$') // Where path is {"a": 1, "b": [{"c": 5}]} | ||
Results: '{a=1, b=[{"c":5}]}' // Where everything inside the list is a valid json object | ||
""" | ||
|
||
@staticmethod | ||
def cast_factory(value: Any) -> Any: | ||
return cast_to_vtl_json_object(value) | ||
|
||
|
||
class AttributeDict(dict): | ||
""" | ||
Wrapper returned by VelocityUtilApiGateway.parseJson to allow access to dict values as attributes (dot notation), | ||
|
@@ -138,15 +209,18 @@ def __init__(self, body, params): | |
self.parameters = params or {} | ||
self.value = body | ||
|
||
def path(self, path): | ||
def _extract_json_path(self, path): | ||
if not self.value: | ||
return {} | ||
value = self.value if isinstance(self.value, dict) else json.loads(self.value) | ||
return extract_jsonpath(value, path) | ||
|
||
def path(self, path): | ||
return cast_to_vtl_json_object(self._extract_json_path(path)) | ||
|
||
def json(self, path): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. as we are calling There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
str(input.path("$"))='{a=1, b=[{"c":5}]}'
str(input.json("$"))='{"a": 1, "b": [{"c": 5}]}' |
||
path = path or "$" | ||
matching = self.path(path) | ||
matching = self._extract_json_path(path) | ||
if isinstance(matching, (list, dict)): | ||
matching = json_safe(matching) | ||
return json.dumps(matching) | ||
|
@@ -202,3 +276,13 @@ def render_response( | |
) | ||
result = self.render_vtl(template=template.strip(), variables=variables_copy) | ||
return result, variables_copy["context"]["responseOverride"] | ||
|
||
|
||
# patches required to allow our custom class operations in VTL templates processed by airspeed | ||
airspeed.operators.__additional_methods__[VTLMap] = airspeed.operators.__additional_methods__[dict] | ||
airspeed.operators.__additional_methods__[VTLJsonDict] = airspeed.operators.__additional_methods__[ | ||
dict | ||
] | ||
airspeed.operators.__additional_methods__[VTLJsonList] = airspeed.operators.__additional_methods__[ | ||
list | ||
] |
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.
Nice! Can't wait to have more parity here, and also introduce the "hidden" dict sometimes used! We're getting real close now 👀