Skip to content
This repository was archived by the owner on Aug 31, 2021. It is now read-only.

Commit 126852e

Browse files
authored
Use OrderedDict to preserve elements order when parsing templates (#4)
1 parent 3e07d38 commit 126852e

File tree

2 files changed

+51
-9
lines changed

2 files changed

+51
-9
lines changed

serverlessrepo/parser.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import six
77
import yaml
88
from yaml.resolver import ScalarNode, SequenceNode
9+
from collections import OrderedDict
910

1011
from .application_metadata import ApplicationMetadata
1112
from .exceptions import ApplicationMetadataNotFoundError
@@ -51,6 +52,10 @@ def intrinsics_multi_constructor(loader, tag_prefix, node):
5152
return {cfntag: value}
5253

5354

55+
def _dict_representer(dumper, data):
56+
return dumper.represent_dict(data.items())
57+
58+
5459
def yaml_dump(dict_to_dump):
5560
"""
5661
This function dumps the dictionary as a YAML document
@@ -59,9 +64,14 @@ def yaml_dump(dict_to_dump):
5964
:return: YAML document
6065
:rtype: str
6166
"""
67+
yaml.SafeDumper.add_representer(OrderedDict, _dict_representer)
6268
return yaml.safe_dump(dict_to_dump, default_flow_style=False)
6369

6470

71+
def _dict_constructor(loader, node):
72+
return OrderedDict(loader.construct_pairs(node))
73+
74+
6575
def parse_template(template_str):
6676
"""
6777
This function parses the SAM template
@@ -75,10 +85,11 @@ def parse_template(template_str):
7585
# PyYAML doesn't support json as well as it should, so if the input
7686
# is actually just json it is better to parse it with the standard
7787
# json parser.
78-
return json.loads(template)
88+
return json.loads(template_str)
7989
except ValueError:
90+
yaml.SafeLoader.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, _dict_constructor)
8091
yaml.SafeLoader.add_multi_constructor('!', intrinsics_multi_constructor)
81-
return yaml.safe_load(template)
92+
return yaml.safe_load(template_str)
8293

8394

8495
def get_app_metadata(template_dict):
@@ -89,6 +100,7 @@ def get_app_metadata(template_dict):
89100
:type template_dict: dict
90101
:return: Application metadata as defined in the template
91102
:rtype: ApplicationMetadata
103+
:raises ApplicationMetadataNotFoundError
92104
"""
93105
if METADATA in template_dict and SERVERLESS_REPO_APPLICATION in template_dict[METADATA]:
94106
app_metadata_dict = template_dict[METADATA][SERVERLESS_REPO_APPLICATION]

tests/unit/test_parser.py

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from serverlessrepo.exceptions import ApplicationMetadataNotFoundError
44
from serverlessrepo.application_metadata import ApplicationMetadata
5-
from serverlessrepo.parser import parse_sam_template, yaml_dump, get_app_metadata
5+
from serverlessrepo.parser import parse_template, yaml_dump, get_app_metadata
66

77

88
class TestParser(TestCase):
@@ -46,12 +46,12 @@ class TestParser(TestCase):
4646
}
4747

4848
def test_parse_yaml_with_tags(self):
49-
output = parse_sam_template(self.yaml_with_tags)
49+
output = parse_template(self.yaml_with_tags)
5050
self.assertEquals(self.parsed_yaml_dict, output)
5151

5252
# Make sure formatter and parser work well with each other
5353
formatted_str = yaml_dump(output)
54-
output_again = parse_sam_template(formatted_str)
54+
output_again = parse_template(formatted_str)
5555
self.assertEquals(output, output_again)
5656

5757
def test_yaml_getatt(self):
@@ -70,14 +70,44 @@ def test_yaml_getatt(self):
7070
}
7171
}
7272

73-
actual_output = parse_sam_template(input)
73+
actual_output = parse_template(input)
7474
self.assertEquals(actual_output, output)
7575

7676
def test_parse_json_with_tabs(self):
7777
template = '{\n\t"foo": "bar"\n}'
78-
output = parse_sam_template(template)
78+
output = parse_template(template)
7979
self.assertEqual(output, {'foo': 'bar'})
8080

81+
def test_parse_yaml_preserve_elements_order(self):
82+
input_template = """
83+
B_Resource:
84+
Key2:
85+
Name: name2
86+
Key1:
87+
Name: name1
88+
A_Resource:
89+
Key2:
90+
Name: name2
91+
Key1:
92+
Name: name1
93+
"""
94+
output_dict = parse_template(input_template)
95+
expected_dict = {
96+
'B_Resource': {
97+
'Key2': {'Name': 'name2'},
98+
'Key1': {'Name': 'name1'}
99+
},
100+
'A_Resource': {
101+
'Key2': {'Name': 'name2'},
102+
'Key1': {'Name': 'name1'}
103+
}
104+
}
105+
self.assertEqual(expected_dict, output_dict)
106+
output_template = yaml_dump(output_dict)
107+
# yaml dump changes indentation, remove spaces and new line characters to just compare the text
108+
self.assertEqual(input_template.translate(None, '\n '),
109+
output_template.translate(None, '\n '))
110+
81111
def test_get_app_metadata_missing_metadata(self):
82112
template_dict_without_metadata = {
83113
'RandomKey': {
@@ -88,7 +118,7 @@ def test_get_app_metadata_missing_metadata(self):
88118
get_app_metadata(template_dict_without_metadata)
89119

90120
message = str(context.exception)
91-
expected = 'missing Metadata section'
121+
expected = 'missing AWS::ServerlessRepo::Application section in template Metadata'
92122
self.assertTrue(expected in message)
93123

94124
def test_get_app_metadata_missing_app_metadata(self):
@@ -101,7 +131,7 @@ def test_get_app_metadata_missing_app_metadata(self):
101131
get_app_metadata(template_dict_without_app_metadata)
102132

103133
message = str(context.exception)
104-
expected = 'missing AWS::ServerlessRepo::Application section'
134+
expected = 'missing AWS::ServerlessRepo::Application section in template Metadata'
105135
self.assertTrue(expected in message)
106136

107137
def test_get_app_metadata_return_metadata(self):

0 commit comments

Comments
 (0)