diff --git a/README-extensions.rst b/README-extensions.rst index da9ce85b..eb40f054 100644 --- a/README-extensions.rst +++ b/README-extensions.rst @@ -203,6 +203,46 @@ The ``${beta:${alpha:two}}`` construct first resolves the ``${alpha:two}`` refer the reference ``${beta:a}`` to the value 99. +Default values +----------------- + +References can now have a default value, if the reference couldn't get resolved, for example: + +.. code-block:: yaml + + # nodes/node1.yml + parameters: + myvalue: ${not:existing:ref||default} + +``reclass.py --nodeinfo node1`` then gives: + +.. code-block:: yaml + + parameters: + myvalue: default + +The ``${not:existing:ref||default}`` construct searches for a value at ``not:existing:ref``, and then because it can't find any, it will take the default value. + +This works for nested references as well: + +.. code-block:: yaml + + # nodes/node1.yml + parameters: + default: 123 + a: ${not:existing:ref||${default}} + b: ${not:existing:ref||${not:existing:default||fallback}} + +``reclass.py --nodeinfo node1`` then gives: + +.. code-block:: yaml + + parameters: + default: 123 + a: 123 + b: fallback + + Ignore overwritten missing references ------------------------------------- diff --git a/reclass/datatypes/tests/test_parameters.py b/reclass/datatypes/tests/test_parameters.py index 80fd8de1..d32dba3b 100644 --- a/reclass/datatypes/tests/test_parameters.py +++ b/reclass/datatypes/tests/test_parameters.py @@ -414,6 +414,20 @@ def test_nested_deep_references(self): p.interpolate() self.assertEqual(p.as_dict(), r) + def test_nested_references_default_exists(self): + values = {"a": "value", "b": "${not:existing||${a}}"} + reference = {"a": "value", "b": "value"} + parameters = Parameters(values, SETTINGS, '') + parameters.interpolate() + self.assertEqual(parameters.as_dict(), reference) + + def test_nested_references_default_exists(self): + values = {"a": "value", "b": "${not:existing||${again||fallback}}"} + reference = {"a": "value", "b": "fallback"} + parameters = Parameters(values, SETTINGS, '') + parameters.interpolate() + self.assertEqual(parameters.as_dict(), reference) + def test_stray_occurrence_overwrites_during_interpolation(self): p1 = Parameters({'r' : 1, 'b': '${r}'}, SETTINGS, '') p2 = Parameters({'b' : 2}, SETTINGS, '') diff --git a/reclass/utils/dictpath.py b/reclass/utils/dictpath.py index 70c7bb51..2afd60fb 100644 --- a/reclass/utils/dictpath.py +++ b/reclass/utils/dictpath.py @@ -67,6 +67,8 @@ def __init__(self, delim, contents=None): elif isinstance(contents, list): self._parts = contents elif isinstance(contents, six.string_types): + # parse the default value from contents, strip default section (||...) from parts + contents, self.default = self._split_default(contents) self._parts = self._split_string(contents) elif isinstance(contents, tuple): self._parts = list(contents) @@ -115,6 +117,18 @@ def _get_innermost_container(self, base): def _split_string(self, string): return re.split(r'(?<!\\)' + re.escape(self._delim), string) + def _split_default(self, string): + """ + splits reference tag 'my:tag||default-value' into two parts + """ + parts = string.split("||") + if not parts: + return None # no contents at all + elif len(parts) == 1: + return parts[0], None # no default + else: + return parts[:-1][0], parts[-1] # contain '||' in contents and use just the last '||' + def key_parts(self): return self._parts[:-1] diff --git a/reclass/values/refitem.py b/reclass/values/refitem.py index 64bf4503..73880815 100644 --- a/reclass/values/refitem.py +++ b/reclass/values/refitem.py @@ -29,6 +29,10 @@ def _resolve(self, ref, context): try: return path.get_value(context) except (KeyError, TypeError) as e: + if refpath.default is not None: + if refpath.default == '': + refpath.default = None + return refpath.default raise ResolveError(ref) def render(self, context, inventory): diff --git a/reclass/values/tests/test_refitem.py b/reclass/values/tests/test_refitem.py index 65814782..692ec3d3 100644 --- a/reclass/values/tests/test_refitem.py +++ b/reclass/values/tests/test_refitem.py @@ -44,6 +44,16 @@ def test__resolve_ok(self): self.assertEquals(result, 1) + def test_default_resolve_ok(self): + reference = RefItem('', Settings({'delimiter': ':'})) + result = reference._resolve('non:existing||default', {'foo':'bar'}) + self.assertEquals(result, 'default') + + def test_default_exists_resolve_ok(self): + reference = RefItem('', Settings({'delimiter': ':'})) + result = reference._resolve('foo||default', {'foo':'bar'}) + self.assertEquals(result, 'bar') + def test__resolve_fails(self): refitem = RefItem('', Settings({'delimiter': ':'})) context = {'foo':{'bar': 1}}