deb-murano/murano/dsl/contracts/contracts.py
Stan Lagun 2b30594c5e Migrate JSON schema generator to new framework
JSON schema generator code was using its own
implementation of contract methods (string(), class() etc.)

This commit moves schema generation into dedicated
contract classes so that all contract method implementations
are accumulated in the single class (per each method)

Change-Id: I156d0b8e685a99aafac9d16325264ba06b8a3174
2016-08-29 13:38:29 -07:00

304 lines
11 KiB
Python

# Copyright (c) 2016 Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import copy
import six
from yaql.language import specs
from yaql.language import utils
from yaql.language import yaqltypes
from murano.dsl import constants
from murano.dsl.contracts import basic
from murano.dsl.contracts import check
from murano.dsl.contracts import instances
from murano.dsl import dsl
from murano.dsl import dsl_types
from murano.dsl import exceptions
from murano.dsl import helpers
from murano.dsl import yaql_integration
CONTRACT_METHODS = [
basic.String,
basic.Bool,
basic.Int,
basic.NotNull,
check.Check,
instances.Class,
instances.Template,
instances.Owned,
instances.NotOwned
]
class Contract(object):
def __init__(self, spec, declaring_type):
self._spec = spec
self._runtime_version = declaring_type.package.runtime_version
@property
def spec(self):
return self._spec
@staticmethod
def _get_contract_factory(cls, action_func):
def payload(context, value, *args, **kwargs):
instance = object.__new__(cls)
instance.value = value
instance.context = context
instance.__init__(*args, **kwargs)
return action_func(instance)
name = yaql_integration.CONVENTION.convert_function_name(cls.name)
try:
init_spec = specs.get_function_definition(
helpers.function(cls.__init__),
name=name, method=True, convention=yaql_integration.CONVENTION)
except AttributeError:
init_spec = specs.get_function_definition(
lambda self: None,
name=name, method=True)
init_spec.parameters['self'] = specs.ParameterDefinition(
'self', yaqltypes.PythonType(object, nullable=True), 0)
init_spec.insert_parameter(specs.ParameterDefinition(
'?1', yaqltypes.Context(), 0))
init_spec.payload = payload
return init_spec
@staticmethod
def _prepare_context(runtime_version, action):
context = yaql_integration.create_context(
runtime_version).create_child_context()
for cls in CONTRACT_METHODS:
context.register_function(Contract._get_contract_factory(
cls, action))
return context
@staticmethod
@helpers.memoize
def _prepare_transform_context(runtime_version, finalize):
if finalize:
def action(c):
c.value = c.transform()
return c.finalize()
else:
def action(c):
return c.transform()
return Contract._prepare_context(
runtime_version, action)
@staticmethod
@helpers.memoize
def _prepare_validate_context(runtime_version):
return Contract._prepare_context(
runtime_version, lambda c: c.validate())
@staticmethod
@helpers.memoize
def _prepare_check_type_context(runtime_version):
return Contract._prepare_context(
runtime_version, lambda c: c.check_type())
@staticmethod
@helpers.memoize
def _prepare_schema_generation_context(runtime_version):
context = Contract._prepare_context(
runtime_version, lambda c: c.generate_schema())
@specs.name('#finalize')
def finalize(obj):
if isinstance(obj, dict):
obj.pop('_notNull', None)
return obj
context.register_function(finalize)
return context
@staticmethod
@helpers.memoize
def _prepare_finalize_context(runtime_version):
return Contract._prepare_context(
runtime_version, lambda c: c.finalize())
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(
'Value {0} is not of a dictionary type'.format(
helpers.format_scalar(data)))
if not spec:
return data
result = {}
yaql_key = None
for key, value in six.iteritems(spec):
if isinstance(key, dsl_types.YaqlExpression):
if yaql_key is not None:
raise exceptions.DslContractSyntaxError(
'Dictionary contract '
'cannot have more than one expression key')
else:
yaql_key = key
else:
result[key] = self._map(
data.get(key), value, context, '{0}[{1}]'.format(
path, helpers.format_scalar(key)))
if yaql_key is not None:
yaql_value = spec[yaql_key]
for key, value in six.iteritems(data):
if key in result:
continue
key = self._map(key, yaql_key, context, path)
result[key] = self._map(
value, yaql_value, context, '{0}[{1}]'.format(
path, helpers.format_scalar(key)))
return utils.FrozenDict(result)
def _map_list(self, data, spec, context, path):
if utils.is_iterator(data):
data = list(data)
elif not utils.is_sequence(data):
if data is None or data is dsl.NO_VALUE:
data = []
else:
data = [data]
if len(spec) < 1:
return data
shift = 0
max_length = -1
min_length = 0
if isinstance(spec[-1], int):
min_length = spec[-1]
shift += 1
if len(spec) >= 2 and isinstance(spec[-2], int):
max_length = min_length
min_length = spec[-2]
shift += 1
if max_length >= 0 and not min_length <= len(data) <= max_length:
raise exceptions.ContractViolationException(
'Array length {0} is not within [{1}..{2}] range'.format(
len(data), min_length, max_length))
elif not min_length <= len(data):
raise exceptions.ContractViolationException(
'Array length {0} must not be less than {1}'.format(
len(data), min_length))
def map_func():
for index, item in enumerate(data):
spec_item = (
spec[-1 - shift]
if index >= len(spec) - shift
else spec[index]
)
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(
helpers.format_scalar(data), spec))
else:
return data
def _map(self, data, spec, context, path):
if helpers.is_passkey(data):
return data
child_context = context.create_child_context()
if isinstance(spec, dsl_types.YaqlExpression):
child_context[''] = data
try:
result = spec(context=child_context)
return result
except exceptions.ContractViolationException as e:
e.path = path
raise
elif isinstance(spec, utils.MappingType):
return self._map_dict(data, spec, child_context, path)
elif utils.is_sequence(spec):
return self._map_list(data, spec, child_context, path)
else:
return self._map_scalar(data, spec)
def _execute(self, base_context_func, data, context, default, **kwargs):
# TODO(ativelkov, slagun): temporary fix, need a better way of handling
# composite defaults
# A bug (#1313694) has been filed
if data is dsl.NO_VALUE:
data = helpers.evaluate(default, context)
if helpers.is_passkey(data):
return data
contract_context = base_context_func(
self._runtime_version).create_child_context()
contract_context['root_context'] = context
for key, value in six.iteritems(kwargs):
contract_context[key] = value
contract_context[constants.CTX_NAMES_SCOPE] = \
context[constants.CTX_NAMES_SCOPE]
return self._map(data, self._spec, contract_context, '')
def transform(self, data, context, this, owner, default, calling_type,
finalize=True):
return self._execute(
lambda runtime_version: self._prepare_transform_context(
runtime_version, finalize), data, context, default,
this=this, owner=owner, calling_type=calling_type)
def validate(self, data, context, default, calling_type):
self._execute(self._prepare_validate_context,
data, context, default, calling_type=calling_type)
return True
def check_type(self, data, context, default, calling_type):
if helpers.is_passkey(data):
return False
try:
self._execute(self._prepare_check_type_context,
data, context, default, calling_type=calling_type)
return True
except exceptions.ContractViolationException:
return False
def finalize(self, data, context, calling_type):
return self._execute(
self._prepare_finalize_context,
data, context, None, calling_type=calling_type)
@staticmethod
def generate_expression_schema(expression, context, runtime_version,
initial_schema=None):
if initial_schema is None:
initial_schema = {}
else:
initial_schema = copy.deepcopy(initial_schema)
contract_context = Contract._prepare_schema_generation_context(
runtime_version).create_child_context()
contract_context['root_context'] = context
contract_context[constants.CTX_NAMES_SCOPE] = \
context[constants.CTX_NAMES_SCOPE]
contract_context['$'] = initial_schema
return expression(context=contract_context)