Specification of which property/argument violated contract was added

When some property or argument value violated its contract it was impossible
to tell which property/argument caused the exception.

This change adds prefix to ContractViolationException message that tells
not only which property or argument failed to validate but for composite
properties also the path within the data.
For example if we change ApacheHttpServer name property contract from
$.string() to $.int() we will get

[io.murano.Environment.applications[0]]
[io.murano.apps.apache.ApacheHttpServer.name]
Value 'ApacheHttpServer' violates int() contract

Also for check() contracts ability to provide custom
error message was added

Change-Id: I6953ec84140f4bed3d50aa181244f89682b2b2fb
Closes-Bug: #1496044
This commit is contained in:
Stan Lagun 2015-09-27 19:23:18 +03:00
parent c9cc6a95a2
commit 1d72b9dd61
5 changed files with 85 additions and 29 deletions

View File

@ -83,7 +83,17 @@ class DslContractSyntaxError(Exception):
class ContractViolationException(Exception):
pass
def __init__(self, *args, **kwargs):
super(ContractViolationException, self).__init__(*args, **kwargs)
self._path = ''
@property
def path(self):
return self._path
@path.setter
def path(self, value):
self._path = value
class ValueIsMissingError(Exception):

View File

@ -67,7 +67,8 @@ class MuranoClass(dsl_types.MuranoClass):
properties = data.get('Properties') or {}
for property_name, property_spec in properties.iteritems():
spec = typespec.PropertySpec(property_spec, type_obj)
spec = typespec.PropertySpec(
property_name, property_spec, type_obj)
type_obj.add_property(property_name, spec)
methods = data.get('Methods') or data.get('Workflow') or {}

View File

@ -69,7 +69,7 @@ class MuranoMethod(dsl_types.MuranoMethod):
raise ValueError()
name = record.keys()[0]
self._arguments_scheme[name] = typespec.ArgumentSpec(
record[name], self.murano_class)
self.name, name, record[name], self.murano_class)
self._yaql_function_definition = \
yaql_integration.build_wrapper_function_definition(self)

View File

@ -47,7 +47,8 @@ class TypeScheme(object):
return int(value)
except Exception:
raise exceptions.ContractViolationException(
'Value {0} violates int() contract'.format(value))
'Value {0} violates int() contract'.format(
format_scalar(value)))
@specs.parameter('value', nullable=True)
@specs.method
@ -60,7 +61,8 @@ class TypeScheme(object):
return unicode(value)
except Exception:
raise exceptions.ContractViolationException(
'Value {0} violates string() contract'.format(value))
'Value {0} violates string() contract'.format(
format_scalar(value)))
@specs.parameter('value', nullable=True)
@specs.method
@ -89,14 +91,17 @@ class TypeScheme(object):
@specs.parameter('value', nullable=True)
@specs.parameter('predicate', yaqltypes.Lambda(with_context=True))
@specs.parameter('msg', yaqltypes.String(nullable=True))
@specs.method
def check(value, predicate):
def check(value, predicate, msg=None):
if isinstance(value, TypeScheme.ObjRef) or predicate(
root_context.create_child_context(), value):
return value
else:
raise exceptions.ContractViolationException(
"Value {0} doesn't match predicate".format(value))
if not msg:
msg = "Value {0} doesn't match predicate".format(
format_scalar(value))
raise exceptions.ContractViolationException(msg)
@specs.parameter('obj', TypeScheme.ObjRef, nullable=True)
@specs.name('owned')
@ -117,8 +122,7 @@ class TypeScheme(object):
p = p.owner
raise exceptions.ContractViolationException(
'Object {0} violates owned() contract'.format(
obj.object_id))
'Object {0} violates owned() contract'.format(obj))
@specs.parameter('obj', TypeScheme.ObjRef, nullable=True)
@specs.name('not_owned')
@ -139,8 +143,7 @@ class TypeScheme(object):
return obj
else:
raise exceptions.ContractViolationException(
'Object {0} violates notOwned() contract'.format(
obj.object_id))
'Object {0} violates notOwned() contract'.format(obj))
@specs.parameter('name', dsl.MuranoTypeName(
False, root_context))
@ -182,7 +185,7 @@ class TypeScheme(object):
else:
raise exceptions.ContractViolationException(
'Value {0} cannot be represented as class {1}'.format(
value, name))
format_scalar(value), name))
if not helpers.is_instance_of(
obj, murano_class.name,
version_spec or helpers.get_type(root_context)):
@ -205,12 +208,13 @@ class TypeScheme(object):
context.register_function(not_owned)
return context
def _map_dict(self, data, spec, context):
def _map_dict(self, data, spec, context, path):
if data is None or data is dsl.NO_VALUE:
data = {}
if not isinstance(data, utils.MappingType):
raise exceptions.ContractViolationException(
'Supplied is not of a dictionary type')
'Value {0} is not of a dictionary type'.format(
format_scalar(data)))
if not spec:
return data
result = {}
@ -220,23 +224,27 @@ class TypeScheme(object):
if yaql_key is not None:
raise exceptions.DslContractSyntaxError(
'Dictionary contract '
'cannot have more than one expression keys')
'cannot have more than one expression key')
else:
yaql_key = key
else:
result[key] = self._map(data.get(key), value, context)
result[key] = self._map(
data.get(key), value, context, '{0}[{1}]'.format(
path, format_scalar(key)))
if yaql_key is not None:
yaql_value = spec[yaql_key]
for key, value in data.iteritems():
if key in result:
continue
result[self._map(key, yaql_key, context)] = self._map(
value, yaql_value, context)
key = self._map(key, yaql_key, context, path)
result[key] = self._map(
value, yaql_value, context, '{0}[{1}]'.format(
path, format_scalar(key)))
return utils.FrozenDict(result)
def _map_list(self, data, spec, context):
def _map_list(self, data, spec, context, path):
if not utils.is_sequence(data):
if data is None or data is dsl.NO_VALUE:
data = []
@ -267,26 +275,32 @@ class TypeScheme(object):
if index >= len(spec) - shift
else spec[index]
)
yield self._map(item, spec_item, context)
yield self._map(
item, spec_item, context, '{0}[{1}]'.format(path, index))
return tuple(map_func())
def _map_scalar(self, data, spec):
if data != spec:
raise exceptions.ContractViolationException(
'Value {0} is not equal to {1}'.format(data, spec))
'Value {0} is not equal to {1}'.format(
format_scalar(data), spec))
else:
return data
def _map(self, data, spec, context):
def _map(self, data, spec, context, path):
child_context = context.create_child_context()
if isinstance(spec, dsl_types.YaqlExpression):
child_context[''] = data
return spec(context=child_context)
try:
return spec(context=child_context)
except exceptions.ContractViolationException as e:
e.path = path
raise
elif isinstance(spec, utils.MappingType):
return self._map_dict(data, spec, child_context)
return self._map_dict(data, spec, child_context, path)
elif utils.is_sequence(spec):
return self._map_list(data, spec, child_context)
return self._map_list(data, spec, child_context, path)
else:
return self._map_scalar(data, spec)
@ -299,4 +313,10 @@ class TypeScheme(object):
data = helpers.evaluate(default, context)
context = self.prepare_context(context, this, owner, default)
return self._map(data, self._spec, context)
return self._map(data, self._spec, context, '')
def format_scalar(value):
if isinstance(value, types.StringTypes):
return "'{0}'".format(value)
return unicode(value)

View File

@ -12,6 +12,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import sys
import weakref
from murano.dsl import exceptions
@ -63,8 +64,32 @@ class Spec(object):
class PropertySpec(Spec):
pass
def __init__(self, property_name, declaration, container_class):
super(PropertySpec, self).__init__(declaration, container_class)
self.property_name = property_name
self.class_name = container_class.name
def validate(self, *args, **kwargs):
try:
return super(PropertySpec, self).validate(*args, **kwargs)
except exceptions.ContractViolationException as e:
msg = u'[{0}.{1}{2}] {3}'.format(
self.class_name, self.property_name, e.path, unicode(e))
raise exceptions.ContractViolationException, msg, sys.exc_info()[2]
class ArgumentSpec(Spec):
pass
def __init__(self, method_name, arg_name, declaration, container_class):
super(ArgumentSpec, self).__init__(declaration, container_class)
self.method_name = method_name
self.arg_name = arg_name
self.class_name = container_class.name
def validate(self, *args, **kwargs):
try:
return super(ArgumentSpec, self).validate(*args, **kwargs)
except exceptions.ContractViolationException as e:
msg = u'[{0}::{1}({2}{3})] {4}'.format(
self.class_name, self.method_name, self.arg_name,
e.path, unicode(e))
raise exceptions.ContractViolationException, msg, sys.exc_info()[2]