From 5e5affc10b785ae37d8109f810ed57423c60aec8 Mon Sep 17 00:00:00 2001 From: Randall Burt Date: Tue, 18 Jun 2013 20:38:27 -0500 Subject: [PATCH] Allow JSON values for parameters Adds a paramter type that accepts JSON objects or strings representing JSON objects as values. Implements: blueprint json-parameters Change-Id: I1ad62df28832a2e07249452133fd9f14995f8e23 --- heat/engine/parameters.py | 59 ++++++++++++++++++++-- heat/tests/test_parameters.py | 92 +++++++++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+), 3 deletions(-) diff --git a/heat/engine/parameters.py b/heat/engine/parameters.py index a379bc95..64cc2ced 100644 --- a/heat/engine/parameters.py +++ b/heat/engine/parameters.py @@ -14,6 +14,7 @@ # under the License. import collections +import json import re from heat.common import exception @@ -29,9 +30,9 @@ PARAMETER_KEYS = ( 'Description', 'ConstraintDescription' ) PARAMETER_TYPES = ( - STRING, NUMBER, COMMA_DELIMITED_LIST + STRING, NUMBER, COMMA_DELIMITED_LIST, JSON ) = ( - 'String', 'Number', 'CommaDelimitedList' + 'String', 'Number', 'CommaDelimitedList', 'Json' ) PSEUDO_PARAMETERS = ( PARAM_STACK_ID, PARAM_STACK_NAME, PARAM_REGION @@ -55,6 +56,8 @@ class Parameter(object): ParamClass = NumberParam elif param_type == COMMA_DELIMITED_LIST: ParamClass = CommaDelimitedListParam + elif param_type == JSON: + ParamClass = JsonParam else: raise ValueError('Invalid Parameter type "%s"' % param_type) @@ -198,7 +201,7 @@ class CommaDelimitedListParam(Parameter, collections.Sequence): def _validate(self, value): '''Check that the supplied value is compatible with the constraints.''' try: - sp = value.split(',') + value.split(',') except AttributeError: raise ValueError('Value must be a comma-delimited list string') @@ -214,6 +217,56 @@ class CommaDelimitedListParam(Parameter, collections.Sequence): return self.value().split(',')[index] +class JsonParam(Parameter, collections.Mapping): + """A template parameter who's value is valid map.""" + + def _validate(self, value): + message = 'Value must be valid JSON' + if isinstance(value, collections.Mapping): + try: + self.user_value = json.dumps(value) + except (ValueError, TypeError) as err: + raise ValueError("%s: %s" % (message, str(err))) + self.parsed = value + else: + try: + self.parsed = json.loads(value) + except ValueError: + raise ValueError(message) + + # check length + my_len = len(self.parsed) + if MAX_LENGTH in self.schema: + max_length = int(self.schema[MAX_LENGTH]) + if my_len > max_length: + message = ('value length (%d) overflows %s %s' + % (my_len, MAX_LENGTH, max_length)) + raise ValueError(self._error_msg(message)) + if MIN_LENGTH in self.schema: + min_length = int(self.schema[MIN_LENGTH]) + if my_len < min_length: + message = ('value length (%d) underflows %s %s' + % (my_len, MIN_LENGTH, min_length)) + raise ValueError(self._error_msg(message)) + # check valid keys + if VALUES in self.schema: + allowed = self.schema[VALUES] + bad_keys = [k for k in self.parsed if k not in allowed] + if bad_keys: + message = ('keys %s are not in %s %s' + % (bad_keys, VALUES, allowed)) + raise ValueError(self._error_msg(message)) + + def __getitem__(self, key): + return self.parsed[key] + + def __iter__(self): + return iter(self.parsed) + + def __len__(self): + return len(self.parsed) + + class Parameters(collections.Mapping): ''' The parameters of a stack, with type checking, defaults &c. specified by diff --git a/heat/tests/test_parameters.py b/heat/tests/test_parameters.py index 8e5bda93..d8e4c5a1 100644 --- a/heat/tests/test_parameters.py +++ b/heat/tests/test_parameters.py @@ -33,6 +33,10 @@ class ParameterTest(testtools.TestCase): p = parameters.Parameter('p', {'Type': 'CommaDelimitedList'}) self.assertTrue(isinstance(p, parameters.CommaDelimitedListParam)) + def test_new_json(self): + p = parameters.Parameter('p', {'Type': 'Json'}) + self.assertTrue(isinstance(p, parameters.JsonParam)) + def test_new_bad_type(self): self.assertRaises(ValueError, parameters.Parameter, 'p', {'Type': 'List'}) @@ -257,6 +261,94 @@ class ParameterTest(testtools.TestCase): else: self.fail('ValueError not raised') + def test_map_value(self): + '''Happy path for value thats already a map.''' + schema = {'Type': 'Json'} + val = {"foo": "bar", "items": [1, 2, 3]} + val_s = json.dumps(val) + p = parameters.Parameter('p', schema, val) + self.assertEqual(val_s, p.value()) + self.assertEqual(val, p.parsed) + + def test_map_value_bad(self): + '''Map value is not JSON parsable.''' + schema = {'Type': 'Json', + 'ConstraintDescription': 'wibble'} + val = {"foo": "bar", "not_json": len} + try: + parameters.Parameter('p', schema, val) + except ValueError as verr: + self.assertIn('Value must be valid JSON', str(verr)) + else: + self.fail("Value error not raised") + + def test_map_value_parse(self): + '''Happy path for value that's a string.''' + schema = {'Type': 'Json'} + val = {"foo": "bar", "items": [1, 2, 3]} + val_s = json.dumps(val) + p = parameters.Parameter('p', schema, val_s) + self.assertEqual(val_s, p.value()) + self.assertEqual(val, p.parsed) + + def test_map_value_bad_parse(self): + '''Test value error for unparsable string value.''' + schema = {'Type': 'Json', + 'ConstraintDescription': 'wibble'} + val = "I am not a map" + try: + parameters.Parameter('p', schema, val) + except ValueError as verr: + self.assertIn('Value must be valid JSON', str(verr)) + else: + self.fail("Value error not raised") + + def test_map_values_good(self): + '''Happy path for map keys.''' + schema = {'Type': 'Json', + 'AllowedValues': ["foo", "bar", "baz"]} + val = {"foo": "bar", "baz": [1, 2, 3]} + val_s = json.dumps(val) + p = parameters.Parameter('p', schema, val_s) + self.assertEqual(val_s, p.value()) + self.assertEqual(val, p.parsed) + + def test_map_values_bad(self): + '''Test failure of invalid map keys.''' + schema = {'Type': 'Json', + 'AllowedValues': ["foo", "bar", "baz"]} + val = {"foo": "bar", "items": [1, 2, 3]} + try: + parameters.Parameter('p', schema, val) + except ValueError as verr: + self.assertIn("items", str(verr)) + else: + self.fail("Value error not raised") + + def test_map_underrun(self): + '''Test map length under MIN_LEN.''' + schema = {'Type': 'Json', + 'MinLength': 3} + val = {"foo": "bar", "items": [1, 2, 3]} + try: + parameters.Parameter('p', schema, val) + except ValueError as verr: + self.assertIn('underflows', str(verr)) + else: + self.fail("Value error not raised") + + def test_map_overrun(self): + '''Test map length over MAX_LEN.''' + schema = {'Type': 'Json', + 'MaxLength': 1} + val = {"foo": "bar", "items": [1, 2, 3]} + try: + parameters.Parameter('p', schema, val) + except ValueError as verr: + self.assertIn('overflows', str(verr)) + else: + self.fail("Value error not raised") + params_schema = json.loads('''{ "Parameters" : { -- 2.45.2