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): 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): class ValueIsMissingError(Exception):

View File

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

View File

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

View File

@ -47,7 +47,8 @@ class TypeScheme(object):
return int(value) return int(value)
except Exception: except Exception:
raise exceptions.ContractViolationException( 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.parameter('value', nullable=True)
@specs.method @specs.method
@ -60,7 +61,8 @@ class TypeScheme(object):
return unicode(value) return unicode(value)
except Exception: except Exception:
raise exceptions.ContractViolationException( 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.parameter('value', nullable=True)
@specs.method @specs.method
@ -89,14 +91,17 @@ class TypeScheme(object):
@specs.parameter('value', nullable=True) @specs.parameter('value', nullable=True)
@specs.parameter('predicate', yaqltypes.Lambda(with_context=True)) @specs.parameter('predicate', yaqltypes.Lambda(with_context=True))
@specs.parameter('msg', yaqltypes.String(nullable=True))
@specs.method @specs.method
def check(value, predicate): def check(value, predicate, msg=None):
if isinstance(value, TypeScheme.ObjRef) or predicate( if isinstance(value, TypeScheme.ObjRef) or predicate(
root_context.create_child_context(), value): root_context.create_child_context(), value):
return value return value
else: else:
raise exceptions.ContractViolationException( if not msg:
"Value {0} doesn't match predicate".format(value)) msg = "Value {0} doesn't match predicate".format(
format_scalar(value))
raise exceptions.ContractViolationException(msg)
@specs.parameter('obj', TypeScheme.ObjRef, nullable=True) @specs.parameter('obj', TypeScheme.ObjRef, nullable=True)
@specs.name('owned') @specs.name('owned')
@ -117,8 +122,7 @@ class TypeScheme(object):
p = p.owner p = p.owner
raise exceptions.ContractViolationException( raise exceptions.ContractViolationException(
'Object {0} violates owned() contract'.format( 'Object {0} violates owned() contract'.format(obj))
obj.object_id))
@specs.parameter('obj', TypeScheme.ObjRef, nullable=True) @specs.parameter('obj', TypeScheme.ObjRef, nullable=True)
@specs.name('not_owned') @specs.name('not_owned')
@ -139,8 +143,7 @@ class TypeScheme(object):
return obj return obj
else: else:
raise exceptions.ContractViolationException( raise exceptions.ContractViolationException(
'Object {0} violates notOwned() contract'.format( 'Object {0} violates notOwned() contract'.format(obj))
obj.object_id))
@specs.parameter('name', dsl.MuranoTypeName( @specs.parameter('name', dsl.MuranoTypeName(
False, root_context)) False, root_context))
@ -182,7 +185,7 @@ class TypeScheme(object):
else: else:
raise exceptions.ContractViolationException( raise exceptions.ContractViolationException(
'Value {0} cannot be represented as class {1}'.format( 'Value {0} cannot be represented as class {1}'.format(
value, name)) format_scalar(value), name))
if not helpers.is_instance_of( if not helpers.is_instance_of(
obj, murano_class.name, obj, murano_class.name,
version_spec or helpers.get_type(root_context)): version_spec or helpers.get_type(root_context)):
@ -205,12 +208,13 @@ class TypeScheme(object):
context.register_function(not_owned) context.register_function(not_owned)
return context 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: if data is None or data is dsl.NO_VALUE:
data = {} data = {}
if not isinstance(data, utils.MappingType): if not isinstance(data, utils.MappingType):
raise exceptions.ContractViolationException( 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: if not spec:
return data return data
result = {} result = {}
@ -220,23 +224,27 @@ class TypeScheme(object):
if yaql_key is not None: if yaql_key is not None:
raise exceptions.DslContractSyntaxError( raise exceptions.DslContractSyntaxError(
'Dictionary contract ' 'Dictionary contract '
'cannot have more than one expression keys') 'cannot have more than one expression key')
else: else:
yaql_key = key yaql_key = key
else: 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: if yaql_key is not None:
yaql_value = spec[yaql_key] yaql_value = spec[yaql_key]
for key, value in data.iteritems(): for key, value in data.iteritems():
if key in result: if key in result:
continue continue
result[self._map(key, yaql_key, context)] = self._map( key = self._map(key, yaql_key, context, path)
value, yaql_value, context) result[key] = self._map(
value, yaql_value, context, '{0}[{1}]'.format(
path, format_scalar(key)))
return utils.FrozenDict(result) 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 not utils.is_sequence(data):
if data is None or data is dsl.NO_VALUE: if data is None or data is dsl.NO_VALUE:
data = [] data = []
@ -267,26 +275,32 @@ class TypeScheme(object):
if index >= len(spec) - shift if index >= len(spec) - shift
else spec[index] 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()) return tuple(map_func())
def _map_scalar(self, data, spec): def _map_scalar(self, data, spec):
if data != spec: if data != spec:
raise exceptions.ContractViolationException( 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: else:
return data return data
def _map(self, data, spec, context): def _map(self, data, spec, context, path):
child_context = context.create_child_context() child_context = context.create_child_context()
if isinstance(spec, dsl_types.YaqlExpression): if isinstance(spec, dsl_types.YaqlExpression):
child_context[''] = data child_context[''] = data
try:
return spec(context=child_context) return spec(context=child_context)
except exceptions.ContractViolationException as e:
e.path = path
raise
elif isinstance(spec, utils.MappingType): 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): elif utils.is_sequence(spec):
return self._map_list(data, spec, child_context) return self._map_list(data, spec, child_context, path)
else: else:
return self._map_scalar(data, spec) return self._map_scalar(data, spec)
@ -299,4 +313,10 @@ class TypeScheme(object):
data = helpers.evaluate(default, context) data = helpers.evaluate(default, context)
context = self.prepare_context(context, this, owner, default) 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 # License for the specific language governing permissions and limitations
# under the License. # under the License.
import sys
import weakref import weakref
from murano.dsl import exceptions from murano.dsl import exceptions
@ -63,8 +64,32 @@ class Spec(object):
class PropertySpec(Spec): 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): 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]