template() contract function was introduced

template() works similar to the class() contract
in regards to the data validation but do not
instantiate objects. Instead the data is left in the
object model dictionary format so that it could be
instantiated later with the new() function. In addition
it allows to excluse specified properties from validation
and result template so that they could be provided later.
Objects that are assigned to the property or argument
with template() contract will be automatically converted
to their object model representation.

Change-Id: Id1016ae0cab1a18663900b27943f150d008e61f0
This commit is contained in:
Stan Lagun 2016-08-02 22:41:09 -07:00
parent 30846bb339
commit 23a67181eb
14 changed files with 304 additions and 60 deletions

View File

@ -14,6 +14,7 @@
import semantic_version import semantic_version
EXPRESSION_MEMORY_QUOTA = 512 * 1024 EXPRESSION_MEMORY_QUOTA = 512 * 1024
ITERATORS_LIMIT = 2000 ITERATORS_LIMIT = 2000
@ -49,6 +50,9 @@ TL_CONTEXT = '__murano_context'
TL_ID = '__thread_id' TL_ID = '__thread_id'
TL_OBJECT_STORE = '__murano_object_store' TL_OBJECT_STORE = '__murano_object_store'
TL_SESSION = '__murano_execution_session' TL_SESSION = '__murano_execution_session'
TL_CONTRACT_PASSKEY = '__murano_contract_passkey'
TL_OBJECTS_DRY_RUN = '__murano_objects_dry_run'
RUNTIME_VERSION_1_0 = semantic_version.Version('1.0.0') RUNTIME_VERSION_1_0 = semantic_version.Version('1.0.0')
RUNTIME_VERSION_1_1 = semantic_version.Version('1.1.0') RUNTIME_VERSION_1_1 = semantic_version.Version('1.1.0')

View File

@ -71,6 +71,13 @@ class MethodArgumentUsages(object):
All = {Standard, VarArgs, KwArgs} All = {Standard, VarArgs, KwArgs}
class DumpTypes(object):
Serializable = 'Serializable'
Inline = 'Inline'
Mixed = 'Mixed'
All = {Serializable, Inline, Mixed}
class MuranoType(object): class MuranoType(object):
pass pass

View File

@ -223,6 +223,16 @@ def get_class(name, context=None):
return murano_type.package.find_class(name) return murano_type.package.find_class(name)
def get_contract_passkey():
current_thread = eventlet.greenthread.getcurrent()
return getattr(current_thread, constants.TL_CONTRACT_PASSKEY, None)
def is_objects_dry_run_mode():
current_thread = eventlet.greenthread.getcurrent()
return bool(getattr(current_thread, constants.TL_OBJECTS_DRY_RUN, False))
def get_current_thread_id(): def get_current_thread_id():
global _threads_sequencer global _threads_sequencer
@ -562,6 +572,34 @@ def parse_object_definition(spec, scope_type, context):
} }
def assemble_object_definition(parsed, model_format=dsl_types.DumpTypes.Mixed):
if model_format == dsl_types.DumpTypes.Inline:
result = {
parsed['type']: parsed['properties'],
'id': parsed['id'],
'name': parsed['name']
}
result.update(parsed['extra'])
return result
result = parsed['properties']
header = {
'id': parsed['id'],
'name': parsed['name']
}
header.update(parsed['extra'])
result['?'] = header
if model_format == dsl_types.DumpTypes.Mixed:
header['type'] = parsed['type']
return result
elif model_format == dsl_types.DumpTypes.Serializable:
cls = parsed['type']
if cls:
header['type'] = format_type_string(cls)
return result
else:
raise ValueError('Invalid Serialization Type')
def function(c): def function(c):
if hasattr(c, 'im_func'): if hasattr(c, 'im_func'):
return c.im_func return c.im_func
@ -606,3 +644,17 @@ def format_type_string(type_obj):
type_obj.name, type_obj.version, type_obj.package.name) type_obj.name, type_obj.version, type_obj.package.name)
else: else:
raise ValueError('Invalid argument') raise ValueError('Invalid argument')
def patch_dict(dct, path, value):
parts = path.split('.')
for i in range(len(parts) - 1):
if not isinstance(dct, dict):
dct = None
break
dct = dct.get(parts[i])
if isinstance(dct, dict):
if value is yaqlutils.NO_VALUE:
dct.pop(parts[-1])
else:
dct[parts[-1]] = value

View File

@ -19,7 +19,6 @@ from murano.dsl import dsl
from murano.dsl import dsl_types from murano.dsl import dsl_types
from murano.dsl import exceptions from murano.dsl import exceptions
from murano.dsl import helpers from murano.dsl import helpers
from murano.dsl import serializer
from murano.dsl import yaql_integration from murano.dsl import yaql_integration
@ -143,7 +142,8 @@ class MuranoObject(dsl_types.MuranoObject):
init.invoke(self.real_this, (), init_args, init.invoke(self.real_this, (), init_args,
context.create_child_context()) context.create_child_context())
if not object_store.initializing and init: if (not object_store.initializing and init
and not helpers.is_objects_dry_run_mode()):
yield run_init yield run_init
@property @property
@ -254,13 +254,13 @@ class MuranoObject(dsl_types.MuranoObject):
self.type.name, self.type.version, self.object_id, id(self)) self.type.name, self.type.version, self.object_id, id(self))
def to_dictionary(self, include_hidden=False, def to_dictionary(self, include_hidden=False,
serialization_type=serializer.DumpTypes.Serializable, serialization_type=dsl_types.DumpTypes.Serializable,
allow_refs=False): allow_refs=False):
context = helpers.get_context() context = helpers.get_context()
result = {} result = {}
for parent in self.__parents.values(): for parent in self.__parents.values():
result.update(parent.to_dictionary( result.update(parent.to_dictionary(
include_hidden, serializer.DumpTypes.Serializable, include_hidden, dsl_types.DumpTypes.Serializable,
allow_refs)) allow_refs))
for property_name in self.type.properties: for property_name in self.type.properties:
if property_name in self.__properties: if property_name in self.__properties:
@ -276,14 +276,14 @@ class MuranoObject(dsl_types.MuranoObject):
'as', context) == 'reference': 'as', context) == 'reference':
prop_value = prop_value.object_id prop_value = prop_value.object_id
result[property_name] = prop_value result[property_name] = prop_value
if serialization_type == serializer.DumpTypes.Inline: if serialization_type == dsl_types.DumpTypes.Inline:
result.pop('?') result.pop('?')
result = { result = {
self.type: result, self.type: result,
'id': self.object_id, 'id': self.object_id,
'name': self.name 'name': self.name
} }
elif serialization_type == serializer.DumpTypes.Mixed: elif serialization_type == dsl_types.DumpTypes.Mixed:
result.update({'?': { result.update({'?': {
'type': self.type, 'type': self.type,
'id': self.object_id, 'id': self.object_id,

View File

@ -58,7 +58,8 @@ class ObjectStore(object):
scope_type=None, context=None, keep_ids=False): scope_type=None, context=None, keep_ids=False):
# do the object model load in a temporary object store and copy # do the object model load in a temporary object store and copy
# loaded objects here after that # loaded objects here after that
model_store = InitializationObjectStore(owner, self, keep_ids) model_store = InitializationObjectStore(
owner, self, keep_ids)
with helpers.with_object_store(model_store): with helpers.with_object_store(model_store):
result = model_store.load( result = model_store.load(
value, owner, scope_type=scope_type, value, owner, scope_type=scope_type,
@ -111,45 +112,44 @@ class InitializationObjectStore(ObjectStore):
if not parsed: if not parsed:
raise ValueError('Invalid object representation format') raise ValueError('Invalid object representation format')
try: if owner is self._root_owner:
if owner is self._root_owner: self._initializing = True
self._initializing = True
class_obj = parsed['type'] or default_type class_obj = parsed['type'] or default_type
if not class_obj: if not class_obj:
raise ValueError( raise ValueError(
'Invalid object representation: ' 'Invalid object representation: '
'no type information was provided') 'no type information was provided')
if isinstance(class_obj, dsl_types.MuranoTypeReference): if isinstance(class_obj, dsl_types.MuranoTypeReference):
class_obj = class_obj.type class_obj = class_obj.type
object_id = parsed['id'] object_id = parsed['id']
obj = None if object_id is None else self._store.get(object_id) obj = None if object_id is None else self._store.get(object_id)
if not obj: if not obj:
obj = murano_object.MuranoObject( obj = murano_object.MuranoObject(
class_obj, helpers.weak_proxy(owner), class_obj, helpers.weak_proxy(owner),
name=parsed['name'], name=parsed['name'],
object_id=object_id if self._keep_ids else None) object_id=object_id if self._keep_ids else None)
self.put(obj, object_id or obj.object_id) self.put(obj, object_id or obj.object_id)
system_value = ObjectStore._get_designer_attributes( system_value = ObjectStore._get_designer_attributes(
parsed['extra']) parsed['extra'])
self._designer_attributes_store[object_id] = system_value self._designer_attributes_store[object_id] = system_value
if context is None: if context is None:
context = self.executor.create_object_context(obj) context = self.executor.create_object_context(obj)
def run_initialize(): def run_initialize():
self._initializers.extend( self._initializers.extend(
obj.initialize(context, parsed['properties'])) obj.initialize(context, parsed['properties']))
run_initialize()
if owner is self._root_owner:
self._initializing = False
run_initialize() run_initialize()
if owner is self._root_owner:
self._initializing = False if owner is self._root_owner:
run_initialize() with helpers.with_object_store(self.parent_store):
finally: for fn in self._initializers:
if owner is self._root_owner: fn()
with helpers.with_object_store(self.parent_store):
for fn in self._initializers:
fn()
return obj return obj

View File

@ -128,10 +128,11 @@ def prepare_context(exc, cls):
context.register_function(bool_) context.register_function(bool_)
context.register_function(not_null) context.register_function(not_null)
context.register_function(check) context.register_function(check)
context.register_function(class_factory(context))
context.register_function(owned) context.register_function(owned)
context.register_function(not_owned) context.register_function(not_owned)
context.register_function(finalize) context.register_function(finalize)
for fn in class_factory(context):
context.register_function(fn)
return context return context
@ -295,7 +296,24 @@ def class_factory(context):
'muranoType': name.type.name 'muranoType': name.type.name
}) })
return class_ @specs.parameter('schema', Schema)
@specs.parameter('type_', dsl.MuranoTypeParameter(
nullable=False, context=context))
@specs.parameter('default_type', dsl.MuranoTypeParameter(
nullable=True, context=context))
@specs.parameter('version_spec', yaqltypes.String(True))
@specs.parameter(
'exclude_properties', yaqltypes.Sequence(nullable=True))
@specs.method
def template(schema, type_, exclude_properties=None,
default_type=None, version_spec=None):
result = class_(schema, type_, default_type, version_spec)
result.data['owned'] = True
if exclude_properties:
result.data['excludedProperties'] = exclude_properties
return result
return class_, template
@specs.parameter('schema', Schema) @specs.parameter('schema', Schema)

View File

@ -21,19 +21,13 @@ from murano.dsl import dsl_types
from murano.dsl import helpers from murano.dsl import helpers
class DumpTypes(object):
Serializable = 'Serializable'
Inline = 'Inline'
Mixed = 'Mixed'
All = {Serializable, Inline, Mixed}
class ObjRef(object): class ObjRef(object):
def __init__(self, obj): def __init__(self, obj):
self.ref_obj = obj self.ref_obj = obj
def serialize(obj, executor, serialization_type=DumpTypes.Serializable): def serialize(obj, executor,
serialization_type=dsl_types.DumpTypes.Serializable):
with helpers.with_object_store(executor.object_store): with helpers.with_object_store(executor.object_store):
return serialize_model( return serialize_model(
obj, executor, True, obj, executor, True,
@ -45,7 +39,7 @@ def serialize(obj, executor, serialization_type=DumpTypes.Serializable):
def _serialize_object(root_object, designer_attributes, allow_refs, def _serialize_object(root_object, designer_attributes, allow_refs,
executor, serialize_actions=True, executor, serialize_actions=True,
serialization_type=DumpTypes.Serializable): serialization_type=dsl_types.DumpTypes.Serializable):
serialized_objects = set() serialized_objects = set()
obj = root_object obj = root_object
@ -65,7 +59,7 @@ def serialize_model(root_object, executor,
make_copy=True, make_copy=True,
serialize_attributes=True, serialize_attributes=True,
serialize_actions=True, serialize_actions=True,
serialization_type=DumpTypes.Serializable): serialization_type=dsl_types.DumpTypes.Serializable):
designer_attributes = executor.object_store.designer_attributes designer_attributes = executor.object_store.designer_attributes
if root_object is None: if root_object is None:
@ -138,11 +132,13 @@ def _pass12_serialize(value, parent, serialized_objects,
if isinstance(value, (dsl_types.MuranoType, if isinstance(value, (dsl_types.MuranoType,
dsl_types.MuranoTypeReference)): dsl_types.MuranoTypeReference)):
return helpers.format_type_string(value), False return helpers.format_type_string(value), False
if value is helpers.get_contract_passkey():
return value, False
if isinstance(value, dsl_types.MuranoObject): if isinstance(value, dsl_types.MuranoObject):
result = value.to_dictionary( result = value.to_dictionary(
serialization_type=serialization_type, allow_refs=allow_refs) serialization_type=serialization_type, allow_refs=allow_refs)
if designer_attributes_getter is not None: if designer_attributes_getter is not None:
if serialization_type == DumpTypes.Inline: if serialization_type == dsl_types.DumpTypes.Inline:
system_data = result system_data = result
else: else:
system_data = result['?'] system_data = result['?']
@ -161,13 +157,13 @@ def _pass12_serialize(value, parent, serialized_objects,
for d_key, d_value in six.iteritems(value): for d_key, d_value in six.iteritems(value):
if (isinstance(d_key, dsl_types.MuranoType) and if (isinstance(d_key, dsl_types.MuranoType) and
serialization_type == DumpTypes.Serializable): serialization_type == dsl_types.DumpTypes.Serializable):
result_key = str(d_key) result_key = str(d_key)
else: else:
result_key = d_key result_key = d_key
if (result_key == 'type' and if (result_key == 'type' and
isinstance(d_value, dsl_types.MuranoType) and isinstance(d_value, dsl_types.MuranoType) and
serialization_type == DumpTypes.Mixed): serialization_type == dsl_types.DumpTypes.Mixed):
result_value = d_value, False result_value = d_value, False
else: else:
result_value = _pass12_serialize( result_value = _pass12_serialize(

View File

@ -17,10 +17,12 @@ from yaql.language import specs
from yaql.language import utils from yaql.language import utils
from yaql.language import yaqltypes from yaql.language import yaqltypes
from murano.dsl import constants
from murano.dsl import dsl from murano.dsl import dsl
from murano.dsl import dsl_types from murano.dsl import dsl_types
from murano.dsl import exceptions from murano.dsl import exceptions
from murano.dsl import helpers from murano.dsl import helpers
from murano.dsl import serializer
class TypeScheme(object): class TypeScheme(object):
@ -187,9 +189,69 @@ class TypeScheme(object):
version_spec or helpers.get_type(root_context)): version_spec or helpers.get_type(root_context)):
raise exceptions.ContractViolationException( raise exceptions.ContractViolationException(
'Object of type {0} is not compatible with ' 'Object of type {0} is not compatible with '
'requested type {1}'.format(obj.type.name, name)) 'requested type {1}'.format(obj.type.name, murano_class))
return obj return obj
@specs.parameter('type_', dsl.MuranoTypeParameter(
nullable=False, context=root_context))
@specs.parameter('default_type', dsl.MuranoTypeParameter(
nullable=True, context=root_context))
@specs.parameter('value', nullable=True)
@specs.parameter('version_spec', yaqltypes.String(True))
@specs.parameter(
'exclude_properties', yaqltypes.Sequence(nullable=True))
@specs.method
def template(engine, value, type_, exclude_properties=None,
default_type=None, version_spec=None):
object_store = helpers.get_object_store()
passkey = None
if not default_type:
default_type = type_
murano_class = type_.type
if value is None:
return None
if isinstance(value, dsl_types.MuranoObject):
obj = value
elif isinstance(value, dsl_types.MuranoObjectInterface):
obj = value.object
elif isinstance(value, utils.MappingType):
passkey = utils.create_marker('<Contract Passkey>')
if exclude_properties:
parsed = helpers.parse_object_definition(
value, calling_type, context)
props = dsl.to_mutable(parsed['properties'], engine)
for p in exclude_properties:
helpers.patch_dict(props, p, passkey)
parsed['properties'] = props
value = helpers.assemble_object_definition(parsed)
with helpers.thread_local_attribute(
constants.TL_CONTRACT_PASSKEY, passkey):
with helpers.thread_local_attribute(
constants.TL_OBJECTS_DRY_RUN, True):
obj = object_store.load(
value, owner, context=context,
default_type=default_type, scope_type=calling_type)
else:
raise exceptions.ContractViolationException(
'Value {0} cannot be represented as class {1}'.format(
format_scalar(value), type_))
if not helpers.is_instance_of(
obj, murano_class.name,
version_spec or helpers.get_type(root_context)):
raise exceptions.ContractViolationException(
'Object of type {0} is not compatible with '
'requested type {1}'.format(obj.type.name, type_))
with helpers.thread_local_attribute(
constants.TL_CONTRACT_PASSKEY, passkey):
result = serializer.serialize(
obj.real_this, object_store.executor,
dsl_types.DumpTypes.Mixed)
if exclude_properties:
for p in exclude_properties:
helpers.patch_dict(result, p, utils.NO_VALUE)
return result
context = root_context.create_child_context() context = root_context.create_child_context()
context.register_function(int_) context.register_function(int_)
context.register_function(string) context.register_function(string)
@ -198,6 +260,7 @@ class TypeScheme(object):
context.register_function(not_null) context.register_function(not_null)
context.register_function(error) context.register_function(error)
context.register_function(class_) context.register_function(class_)
context.register_function(template)
context.register_function(owned_ref) context.register_function(owned_ref)
context.register_function(owned) context.register_function(owned)
context.register_function(not_owned_ref) context.register_function(not_owned_ref)
@ -249,12 +312,33 @@ class TypeScheme(object):
@specs.parameter('version_spec', yaqltypes.String(True)) @specs.parameter('version_spec', yaqltypes.String(True))
@specs.method @specs.method
def class_(value, type, version_spec=None): def class_(value, type, version_spec=None):
if helpers.is_instance_of( if value is None or helpers.is_instance_of(
value, type.type.name, value, type.type.name,
version_spec or helpers.get_names_scope(root_context)): version_spec or helpers.get_names_scope(root_context)):
return value return value
raise exceptions.ContractViolationException() raise exceptions.ContractViolationException()
@specs.parameter('type_', dsl.MuranoTypeParameter(
nullable=False, context=root_context))
@specs.parameter('default_type', dsl.MuranoTypeParameter(
nullable=True, context=root_context))
@specs.parameter('value', nullable=True)
@specs.parameter('version_spec', yaqltypes.String(True))
@specs.parameter(
'exclude_properties', yaqltypes.Sequence(nullable=True))
@specs.method
def template(value, type_, exclude_properties=None,
default_type=None, version_spec=None):
if value is None or isinstance(value, utils.MappingType):
return value
if helpers.is_instance_of(
value, type_.type.name,
version_spec or helpers.get_names_scope(root_context)):
return value
raise exceptions.ContractViolationException()
context = root_context.create_child_context() context = root_context.create_child_context()
context.register_function(int_) context.register_function(int_)
context.register_function(string) context.register_function(string)
@ -262,6 +346,7 @@ class TypeScheme(object):
context.register_function(check) context.register_function(check)
context.register_function(not_null) context.register_function(not_null)
context.register_function(class_) context.register_function(class_)
context.register_function(template)
return context return context
def _map_dict(self, data, spec, context, path): def _map_dict(self, data, spec, context, path):
@ -351,6 +436,8 @@ class TypeScheme(object):
return data return data
def _map(self, data, spec, context, path): def _map(self, data, spec, context, path):
if is_passkey(data):
return data
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
@ -375,6 +462,9 @@ class TypeScheme(object):
if data is dsl.NO_VALUE: if data is dsl.NO_VALUE:
data = helpers.evaluate(default, context) data = helpers.evaluate(default, context)
if is_passkey(data):
return data
context = self.prepare_transform_context( context = self.prepare_transform_context(
context, this, owner, default, calling_type) context, this, owner, default, calling_type)
return self._map(data, self._spec, context, '') return self._map(data, self._spec, context, '')
@ -383,6 +473,9 @@ class TypeScheme(object):
if data is dsl.NO_VALUE: if data is dsl.NO_VALUE:
data = helpers.evaluate(default, context) data = helpers.evaluate(default, context)
if is_passkey(data):
return True
context = self.prepare_validate_context(context) context = self.prepare_validate_context(context)
try: try:
self._map(data, self._spec, context, '') self._map(data, self._spec, context, '')
@ -395,3 +488,8 @@ def format_scalar(value):
if isinstance(value, six.string_types): if isinstance(value, six.string_types):
return "'{0}'".format(value) return "'{0}'".format(value)
return six.text_type(value) return six.text_type(value)
def is_passkey(value):
passkey = helpers.get_contract_passkey()
return passkey is not None and value is passkey

View File

@ -245,9 +245,9 @@ def call_func(context, op_dot, base, name, args, kwargs,
@specs.parameter('obj', dsl.MuranoObjectParameter(decorate=False)) @specs.parameter('obj', dsl.MuranoObjectParameter(decorate=False))
@specs.parameter('serialization_type', yaqltypes.String()) @specs.parameter('serialization_type', yaqltypes.String())
@specs.parameter('ignore_upcasts', bool) @specs.parameter('ignore_upcasts', bool)
def dump(obj, serialization_type=serializer.DumpTypes.Serializable, def dump(obj, serialization_type=dsl_types.DumpTypes.Serializable,
ignore_upcasts=True): ignore_upcasts=True):
if serialization_type not in serializer.DumpTypes.All: if serialization_type not in dsl_types.DumpTypes.All:
raise ValueError('Invalid Serialization Type') raise ValueError('Invalid Serialization Type')
executor = helpers.get_executor() executor = helpers.get_executor()
if ignore_upcasts: if ignore_upcasts:

View File

@ -38,6 +38,28 @@ Methods:
Body: Body:
Return: $arg Return: $arg
testTemplateContract:
Arguments:
arg:
Contract: $.template(CreatedClass2)
Body:
Return: $arg
testTemplateContractExcludePropertyFromMpl:
Body:
- $model:
:CreatedClass2:
property1: qwerty
property2: 'not integer'
- Return: $.testTemplateContractExcludeProperty($model)
testTemplateContractExcludeProperty:
Arguments:
arg:
Contract: $.template(CreatedClass2, excludeProperties => [property2])
Body:
Return: $arg
testClassFromIdContract: testClassFromIdContract:
Arguments: Arguments:
arg: arg:

View File

@ -42,6 +42,9 @@ Properties:
classProperty: classProperty:
Contract: $.class(SampleClass1) Contract: $.class(SampleClass1)
templateProperty:
Contract: $.template(SampleClass1, excludeProperties => [stringProperty])
defaultProperty: defaultProperty:
Contract: $.int() Contract: $.int()
Default: 999 Default: 999

View File

@ -126,6 +126,30 @@ class TestContracts(test_case.DslTestCase):
self.assertIsInstance(result, dsl.MuranoObjectInterface) self.assertIsInstance(result, dsl.MuranoObjectInterface)
self.assertEqual(object_id, result.id) self.assertEqual(object_id, result.id)
def test_template_contract(self):
arg = om.Object('CreatedClass2', property1='qwerty', property2=123)
result = self._runner.testTemplateContract(arg)
self.assertIsInstance(result, dict)
self.assertItemsEqual(['?', 'property1', 'property2'], result.keys())
def test_template_contract_fail_on_type(self):
arg = om.Object('SampleClass2', class2Property='qwerty')
self.assertRaises(
exceptions.ContractViolationException,
self._runner.testTemplateContract, arg)
def test_template_contract_with_property_exclusion(self):
arg = om.Object('CreatedClass2', property1='qwerty',
property2='INVALID')
result = self._runner.testTemplateContractExcludeProperty(arg)
self.assertIsInstance(result, dict)
self.assertItemsEqual(['?', 'property1'], result.keys())
def test_template_contract_with_property_exclusion_from_mpl(self):
result = self._runner.testTemplateContractExcludePropertyFromMpl()
self.assertIsInstance(result, dict)
self.assertItemsEqual(['?', 'property1'], result.keys())
def test_check_contract(self): def test_check_contract(self):
arg = om.Object('SampleClass2', class2Property='qwerty') arg = om.Object('SampleClass2', class2Property='qwerty')
self.assertIsNone(self._runner.testCheckContract(arg, 100)) self.assertIsNone(self._runner.testCheckContract(arg, 100))

View File

@ -75,6 +75,15 @@ class TestSchemaGeneration(test_case.DslTestCase):
'classProperty', ['null', 'muranoObject']) 'classProperty', ['null', 'muranoObject'])
self.assertEqual('SampleClass1', schema.get('muranoType')) self.assertEqual('SampleClass1', schema.get('muranoType'))
def test_template_property(self):
schema = self._test_simple_property(
'templateProperty', ['null', 'muranoObject'])
self.assertEqual('SampleClass1', schema.get('muranoType'))
self.assertTrue(schema.get('owned'))
self.assertItemsEqual(
['stringProperty'],
schema.get('excludedProperties'))
def test_default_property(self): def test_default_property(self):
schema = self._test_simple_property( schema = self._test_simple_property(
'defaultProperty', ['null', 'integer']) 'defaultProperty', ['null', 'integer'])

View File

@ -0,0 +1,11 @@
---
features:
- New contract function ``template`` was introduced. ``template`` works
similar to the ``class`` in regards to the data validation but does not
instantiate objects. Instead the data is left in the object model
in dictionary format so that it could be instantiated later with the new()
function. In addition it allows to exclude specified properties from
validation and resulting template so that they could be provided later.
Objects that are assigned to the property or argument with ``template``
contract will be automatically converted to their object model
representation.