MuranoPL forms implementation
Generation of json-schema from MuranoPL class or method and engine RPC call for it were added Implements: blueprint muranopl-forms Change-Id: I43ccd2d9d94f9f89db1855932280539f69f2f8d8
This commit is contained in:
parent
db0e6abb83
commit
e0e3b10b8d
|
@ -17,3 +17,8 @@ Name: ModelBuilder
|
||||||
Usage: Meta
|
Usage: Meta
|
||||||
Applies: Method
|
Applies: Method
|
||||||
Inherited: true
|
Inherited: true
|
||||||
|
|
||||||
|
Properties:
|
||||||
|
enabled:
|
||||||
|
Contract: $.bool().notNull()
|
||||||
|
Default: true
|
|
@ -17,3 +17,8 @@ Name: Hidden
|
||||||
Usage: Meta
|
Usage: Meta
|
||||||
Applies: [Property, Argument]
|
Applies: [Property, Argument]
|
||||||
Inherited: true
|
Inherited: true
|
||||||
|
|
||||||
|
Properties:
|
||||||
|
visible:
|
||||||
|
Contract: $.bool().notNull()
|
||||||
|
Default: false
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
Namespaces:
|
Namespaces:
|
||||||
=: io.murano.metadata
|
=: io.murano.metadata.forms
|
||||||
|
|
||||||
Name: Position
|
Name: Position
|
||||||
Usage: Meta
|
Usage: Meta
|
|
@ -73,9 +73,9 @@ Classes:
|
||||||
io.murano.metadata.Description: metadata/Description.yaml
|
io.murano.metadata.Description: metadata/Description.yaml
|
||||||
io.murano.metadata.HelpText: metadata/HelpText.yaml
|
io.murano.metadata.HelpText: metadata/HelpText.yaml
|
||||||
io.murano.metadata.ModelBuilder: metadata/ModelBuilder.yaml
|
io.murano.metadata.ModelBuilder: metadata/ModelBuilder.yaml
|
||||||
io.murano.metadata.Position: metadata/Position.yaml
|
|
||||||
io.murano.metadata.Title: metadata/Title.yaml
|
io.murano.metadata.Title: metadata/Title.yaml
|
||||||
io.murano.metadata.forms.Hidden: metadata/forms/Hidden.yaml
|
io.murano.metadata.forms.Hidden: metadata/forms/Hidden.yaml
|
||||||
|
io.murano.metadata.forms.Position: metadata/forms/Position.yaml
|
||||||
io.murano.metadata.forms.Section: metadata/forms/Section.yaml
|
io.murano.metadata.forms.Section: metadata/forms/Section.yaml
|
||||||
io.murano.metadata.engine.Serialize: metadata/engine/Serialize.yaml
|
io.murano.metadata.engine.Serialize: metadata/engine/Serialize.yaml
|
||||||
io.murano.metadata.engine.Synchronize: metadata/engine/Synchronize.yaml
|
io.murano.metadata.engine.Synchronize: metadata/engine/Synchronize.yaml
|
||||||
|
|
|
@ -28,7 +28,7 @@ import six
|
||||||
from webob import exc
|
from webob import exc
|
||||||
|
|
||||||
import murano.api.v1
|
import murano.api.v1
|
||||||
from murano.api.v1 import schemas
|
from murano.api.v1 import validation_schemas
|
||||||
from murano.common import exceptions
|
from murano.common import exceptions
|
||||||
from murano.common import policy
|
from murano.common import policy
|
||||||
import murano.common.utils as murano_utils
|
import murano.common.utils as murano_utils
|
||||||
|
@ -235,7 +235,8 @@ class Controller(object):
|
||||||
file_obj, package_meta = _validate_body(body)
|
file_obj, package_meta = _validate_body(body)
|
||||||
if package_meta:
|
if package_meta:
|
||||||
try:
|
try:
|
||||||
jsonschema.validate(package_meta, schemas.PKG_UPLOAD_SCHEMA)
|
jsonschema.validate(package_meta,
|
||||||
|
validation_schemas.PKG_UPLOAD_SCHEMA)
|
||||||
except jsonschema.ValidationError as e:
|
except jsonschema.ValidationError as e:
|
||||||
msg = _("Package schema is not valid: {reason}").format(
|
msg = _("Package schema is not valid: {reason}").format(
|
||||||
reason=e)
|
reason=e)
|
||||||
|
|
|
@ -20,6 +20,7 @@ from murano.api.v1 import deployments
|
||||||
from murano.api.v1 import environments
|
from murano.api.v1 import environments
|
||||||
from murano.api.v1 import instance_statistics
|
from murano.api.v1 import instance_statistics
|
||||||
from murano.api.v1 import request_statistics
|
from murano.api.v1 import request_statistics
|
||||||
|
from murano.api.v1 import schemas
|
||||||
from murano.api.v1 import services
|
from murano.api.v1 import services
|
||||||
from murano.api.v1 import sessions
|
from murano.api.v1 import sessions
|
||||||
from murano.api.v1 import static_actions
|
from murano.api.v1 import static_actions
|
||||||
|
@ -275,5 +276,14 @@ class API(wsgi.Router):
|
||||||
controller=req_stats_resource,
|
controller=req_stats_resource,
|
||||||
action='get',
|
action='get',
|
||||||
conditions={'method': ['GET']})
|
conditions={'method': ['GET']})
|
||||||
|
schemas_resource = schemas.create_resource()
|
||||||
|
mapper.connect('/schemas/{class_name}/{method_names}',
|
||||||
|
controller=schemas_resource,
|
||||||
|
action='get_schema',
|
||||||
|
conditions={'method': ['GET']})
|
||||||
|
mapper.connect('/schemas/{class_name}',
|
||||||
|
controller=schemas_resource,
|
||||||
|
action='get_schema',
|
||||||
|
conditions={'method': ['GET']})
|
||||||
|
|
||||||
super(API, self).__init__(mapper)
|
super(API, self).__init__(mapper)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (c) 2013 Mirantis, Inc.
|
# Copyright (c) 2016 Mirantis, Inc.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
# 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
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
@ -12,63 +12,48 @@
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
# TODO(all): write detailed schema.
|
from oslo_log import log as logging
|
||||||
ENV_SCHEMA = {
|
from oslo_messaging.rpc import client
|
||||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
import six
|
||||||
|
from webob import exc
|
||||||
|
|
||||||
"type": "object",
|
from murano.api.v1 import request_statistics
|
||||||
"properties": {
|
from murano.common import policy
|
||||||
"id": {"type": "string"},
|
from murano.common import rpc
|
||||||
"name": {"type": "string"}
|
from murano.common import wsgi
|
||||||
},
|
|
||||||
"required": ["id", "name"]
|
|
||||||
}
|
|
||||||
|
|
||||||
PKG_UPLOAD_SCHEMA = {
|
|
||||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
|
||||||
|
|
||||||
"type": "object",
|
LOG = logging.getLogger(__name__)
|
||||||
"properties": {
|
API_NAME = 'Schemas'
|
||||||
"tags": {
|
|
||||||
"type": "array",
|
|
||||||
"minItems": 0,
|
|
||||||
"items": {"type": "string"},
|
|
||||||
"uniqueItems": True
|
|
||||||
},
|
|
||||||
"categories": {
|
|
||||||
"type": "array",
|
|
||||||
"minItems": 0,
|
|
||||||
"items": {"type": "string"},
|
|
||||||
"uniqueItems": True
|
|
||||||
},
|
|
||||||
"description": {"type": "string"},
|
|
||||||
"name": {"type": "string"},
|
|
||||||
"is_public": {"type": "boolean"},
|
|
||||||
"enabled": {"type": "boolean"}
|
|
||||||
},
|
|
||||||
"additionalProperties": False
|
|
||||||
}
|
|
||||||
|
|
||||||
PKG_UPDATE_SCHEMA = {
|
|
||||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
|
||||||
|
|
||||||
"type": "object",
|
class Controller(object):
|
||||||
"properties": {
|
@request_statistics.stats_count(API_NAME, 'GetSchema')
|
||||||
"tags": {
|
def get_schema(self, request, class_name, method_names=None):
|
||||||
"type": "array",
|
LOG.debug('GetSchema:GetSchema')
|
||||||
"items": {"type": "string"},
|
target = {"class_name": class_name}
|
||||||
"uniqueItems": True
|
policy.check("get_schema", request.context, target)
|
||||||
},
|
class_version = request.GET.get('classVersion')
|
||||||
"categories": {
|
package_name = request.GET.get('packageName')
|
||||||
"type": "array",
|
credentials = {
|
||||||
"items": {"type": "string"},
|
'token': request.context.auth_token,
|
||||||
"uniqueItems": True
|
'project_id': request.context.tenant
|
||||||
},
|
}
|
||||||
"description": {"type": "string"},
|
|
||||||
"name": {"type": "string"},
|
try:
|
||||||
"is_public": {"type": "boolean"},
|
methods = (list(
|
||||||
"enabled": {"type": "boolean"}
|
six.moves.map(six.text_type.strip, method_names.split(',')))
|
||||||
},
|
if method_names else [])
|
||||||
"additionalProperties": False,
|
return rpc.engine().generate_schema(
|
||||||
"minProperties": 1,
|
credentials, class_name, methods,
|
||||||
}
|
class_version, package_name)
|
||||||
|
except client.RemoteError as e:
|
||||||
|
if e.exc_type in ('NoClassFound',
|
||||||
|
'NoPackageForClassFound',
|
||||||
|
'NoPackageFound'):
|
||||||
|
raise exc.HTTPNotFound(e.value)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def create_resource():
|
||||||
|
return wsgi.Resource(Controller())
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
# Copyright (c) 2013 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.
|
||||||
|
|
||||||
|
# TODO(all): write detailed schema.
|
||||||
|
ENV_SCHEMA = {
|
||||||
|
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||||
|
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {"type": "string"},
|
||||||
|
"name": {"type": "string"}
|
||||||
|
},
|
||||||
|
"required": ["id", "name"]
|
||||||
|
}
|
||||||
|
|
||||||
|
PKG_UPLOAD_SCHEMA = {
|
||||||
|
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||||
|
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"tags": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 0,
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"uniqueItems": True
|
||||||
|
},
|
||||||
|
"categories": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 0,
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"uniqueItems": True
|
||||||
|
},
|
||||||
|
"description": {"type": "string"},
|
||||||
|
"name": {"type": "string"},
|
||||||
|
"is_public": {"type": "boolean"},
|
||||||
|
"enabled": {"type": "boolean"}
|
||||||
|
},
|
||||||
|
"additionalProperties": False
|
||||||
|
}
|
||||||
|
|
||||||
|
PKG_UPDATE_SCHEMA = {
|
||||||
|
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||||
|
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"tags": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"uniqueItems": True
|
||||||
|
},
|
||||||
|
"categories": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"uniqueItems": True
|
||||||
|
},
|
||||||
|
"description": {"type": "string"},
|
||||||
|
"name": {"type": "string"},
|
||||||
|
"is_public": {"type": "boolean"},
|
||||||
|
"enabled": {"type": "boolean"}
|
||||||
|
},
|
||||||
|
"additionalProperties": False,
|
||||||
|
"minProperties": 1,
|
||||||
|
}
|
|
@ -33,6 +33,7 @@ from murano.dsl import context_manager
|
||||||
from murano.dsl import dsl_exception
|
from murano.dsl import dsl_exception
|
||||||
from murano.dsl import executor as dsl_executor
|
from murano.dsl import executor as dsl_executor
|
||||||
from murano.dsl import helpers
|
from murano.dsl import helpers
|
||||||
|
from murano.dsl import schema_generator
|
||||||
from murano.dsl import serializer
|
from murano.dsl import serializer
|
||||||
from murano.engine import execution_session
|
from murano.engine import execution_session
|
||||||
from murano.engine import package_loader
|
from murano.engine import package_loader
|
||||||
|
@ -57,7 +58,11 @@ class EngineService(service.Service):
|
||||||
self.server = None
|
self.server = None
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
endpoints = [TaskProcessingEndpoint(), StaticActionEndpoint()]
|
endpoints = [
|
||||||
|
TaskProcessingEndpoint(),
|
||||||
|
StaticActionEndpoint(),
|
||||||
|
SchemaEndpoint()
|
||||||
|
]
|
||||||
|
|
||||||
transport = messaging.get_transport(CONF)
|
transport = messaging.get_transport(CONF)
|
||||||
s_target = target.Target('murano', 'tasks', server=str(uuid.uuid4()))
|
s_target = target.Target('murano', 'tasks', server=str(uuid.uuid4()))
|
||||||
|
@ -103,6 +108,17 @@ class ContextManager(context_manager.ContextManager):
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class SchemaEndpoint(object):
|
||||||
|
@classmethod
|
||||||
|
def generate_schema(cls, context, *args, **kwargs):
|
||||||
|
session = execution_session.ExecutionSession()
|
||||||
|
session.token = context['token']
|
||||||
|
session.project_id = context['project_id']
|
||||||
|
with package_loader.CombinedPackageLoader(session) as pkg_loader:
|
||||||
|
return schema_generator.generate_schema(
|
||||||
|
pkg_loader, ContextManager(), *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class TaskProcessingEndpoint(object):
|
class TaskProcessingEndpoint(object):
|
||||||
@classmethod
|
@classmethod
|
||||||
def handle_task(cls, context, task):
|
def handle_task(cls, context, task):
|
||||||
|
|
|
@ -43,6 +43,16 @@ class EngineClient(object):
|
||||||
def call_static_action(self, task):
|
def call_static_action(self, task):
|
||||||
return self._client.call({}, 'call_static_action', task=task)
|
return self._client.call({}, 'call_static_action', task=task)
|
||||||
|
|
||||||
|
def generate_schema(self, credentials, class_name, method_names=None,
|
||||||
|
class_version=None, package_name=None):
|
||||||
|
return self._client.call(
|
||||||
|
credentials, 'generate_schema',
|
||||||
|
class_name=class_name,
|
||||||
|
method_names=method_names,
|
||||||
|
class_version=class_version,
|
||||||
|
package_name=package_name
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def api():
|
def api():
|
||||||
global TRANSPORT
|
global TRANSPORT
|
||||||
|
|
|
@ -38,7 +38,7 @@ import six
|
||||||
import webob.dec
|
import webob.dec
|
||||||
import webob.exc
|
import webob.exc
|
||||||
|
|
||||||
from murano.api.v1 import schemas
|
from murano.api.v1 import validation_schemas
|
||||||
from murano.common import config
|
from murano.common import config
|
||||||
from murano.common import exceptions
|
from murano.common import exceptions
|
||||||
from murano.common.i18n import _, _LE, _LW
|
from murano.common.i18n import _, _LE, _LW
|
||||||
|
@ -918,7 +918,8 @@ class JSONPatchDeserializer(TextDeserializer):
|
||||||
property_to_update = {change_path: change['value']}
|
property_to_update = {change_path: change['value']}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
jsonschema.validate(property_to_update, schemas.PKG_UPDATE_SCHEMA)
|
jsonschema.validate(property_to_update,
|
||||||
|
validation_schemas.PKG_UPDATE_SCHEMA)
|
||||||
except jsonschema.ValidationError as e:
|
except jsonschema.ValidationError as e:
|
||||||
LOG.error(_LE("Schema validation error occured: {error}")
|
LOG.error(_LE("Schema validation error occured: {error}")
|
||||||
.format(error=e))
|
.format(error=e))
|
||||||
|
|
|
@ -29,8 +29,8 @@ class MetaProvider(object):
|
||||||
|
|
||||||
|
|
||||||
class MetaData(MetaProvider):
|
class MetaData(MetaProvider):
|
||||||
def __init__(self, definition, target, scope_type):
|
def __init__(self, definition, target, declaring_type):
|
||||||
scope_type = weakref.ref(scope_type)
|
declaring_type = weakref.proxy(declaring_type)
|
||||||
definition = helpers.list_value(definition)
|
definition = helpers.list_value(definition)
|
||||||
factories = []
|
factories = []
|
||||||
used_types = set()
|
used_types = set()
|
||||||
|
@ -43,7 +43,7 @@ class MetaData(MetaProvider):
|
||||||
else:
|
else:
|
||||||
name = d
|
name = d
|
||||||
props = {}
|
props = {}
|
||||||
type_obj = helpers.resolve_type(name, scope_type())
|
type_obj = helpers.resolve_type(name, declaring_type)
|
||||||
if type_obj.usage != dsl_types.ClassUsages.Meta:
|
if type_obj.usage != dsl_types.ClassUsages.Meta:
|
||||||
raise ValueError('Only Meta classes can be attached')
|
raise ValueError('Only Meta classes can be attached')
|
||||||
if target not in type_obj.targets:
|
if target not in type_obj.targets:
|
||||||
|
@ -56,10 +56,15 @@ class MetaData(MetaProvider):
|
||||||
'with cardinality One')
|
'with cardinality One')
|
||||||
|
|
||||||
used_types.add(type_obj)
|
used_types.add(type_obj)
|
||||||
factory_maker = lambda template: \
|
|
||||||
lambda context: helpers.get_object_store().load(
|
def factory_maker(template):
|
||||||
template, owner=None,
|
def instantiate(context):
|
||||||
context=context, scope_type=scope_type())
|
obj = helpers.get_object_store().load(
|
||||||
|
template, owner=None,
|
||||||
|
context=context, scope_type=declaring_type)
|
||||||
|
obj.declaring_type = declaring_type
|
||||||
|
return obj
|
||||||
|
return instantiate
|
||||||
|
|
||||||
factories.append(factory_maker({type_obj: props}))
|
factories.append(factory_maker({type_obj: props}))
|
||||||
self._meta_factories = factories
|
self._meta_factories = factories
|
||||||
|
@ -107,3 +112,15 @@ def merge_providers(initial_class, producer, context):
|
||||||
|
|
||||||
meta = merger([initial_class], set())
|
meta = merger([initial_class], set())
|
||||||
return list(six.moves.map(operator.itemgetter(1), meta))
|
return list(six.moves.map(operator.itemgetter(1), meta))
|
||||||
|
|
||||||
|
|
||||||
|
def aggregate_meta(provider, context, group_by_name=True):
|
||||||
|
key_func = lambda m: m.type.name if group_by_name else m.type
|
||||||
|
meta = provider.get_meta(context)
|
||||||
|
result = {}
|
||||||
|
for item in meta:
|
||||||
|
if item.type.cardinality == dsl_types.MetaCardinality.One:
|
||||||
|
result[key_func(item)] = item
|
||||||
|
else:
|
||||||
|
result.setdefault(key_func(item), []).append(item)
|
||||||
|
return result
|
||||||
|
|
|
@ -538,3 +538,21 @@ def _create_meta_class(cls, name, ns_resolver, data, package, *args, **kwargs):
|
||||||
meta_cls.cardinality = cardinality
|
meta_cls.cardinality = cardinality
|
||||||
meta_cls.inherited = inherited
|
meta_cls.inherited = inherited
|
||||||
return meta_cls
|
return meta_cls
|
||||||
|
|
||||||
|
|
||||||
|
def weigh_type_hierarchy(cls):
|
||||||
|
"""Weighs classes in type hierarchy by their distance from the root
|
||||||
|
|
||||||
|
:param cls: root of hierarchy
|
||||||
|
:return: dictionary that has class name as keys and distance from the root
|
||||||
|
a values. Root class has always a distance of 0. If the class
|
||||||
|
(or different versions of that class) is achievable through
|
||||||
|
several paths the shortest distance is used.
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
for c, w in helpers.traverse(
|
||||||
|
[(cls, 0)], lambda t: six.moves.map(
|
||||||
|
lambda p: (p, t[1] + 1), t[0].parents(cls))):
|
||||||
|
result.setdefault(c.name, w)
|
||||||
|
return result
|
||||||
|
|
|
@ -0,0 +1,508 @@
|
||||||
|
# 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 six
|
||||||
|
from yaql.language import exceptions as yaql_exceptions
|
||||||
|
from yaql.language import expressions
|
||||||
|
from yaql.language import specs
|
||||||
|
from yaql.language import utils
|
||||||
|
from yaql.language import yaqltypes
|
||||||
|
|
||||||
|
from murano.dsl import constants
|
||||||
|
from murano.dsl import dsl
|
||||||
|
from murano.dsl import dsl_types
|
||||||
|
from murano.dsl import executor
|
||||||
|
from murano.dsl import helpers
|
||||||
|
from murano.dsl import meta as meta_module
|
||||||
|
from murano.dsl import murano_type
|
||||||
|
|
||||||
|
|
||||||
|
def generate_schema(pkg_loader, context_manager,
|
||||||
|
class_name, method_names=None,
|
||||||
|
class_version=None, package_name=None):
|
||||||
|
"""Generate JSON schema
|
||||||
|
|
||||||
|
JSON Schema is generated either for the class with all model builders
|
||||||
|
or for specified model builders only. The return value is a dictionary
|
||||||
|
with keys being model builder names and the values are JSON schemas for
|
||||||
|
them. The class itself is represented by an empty string key.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if method_names and not isinstance(method_names, (list, tuple)):
|
||||||
|
method_names = (method_names,)
|
||||||
|
version = helpers.parse_version_spec(class_version)
|
||||||
|
if package_name:
|
||||||
|
package = pkg_loader.load_package(package_name, version)
|
||||||
|
else:
|
||||||
|
package = pkg_loader.load_class_package(class_name, version)
|
||||||
|
|
||||||
|
cls = package.find_class(class_name, search_requirements=False)
|
||||||
|
exc = executor.MuranoDslExecutor(pkg_loader, context_manager)
|
||||||
|
with helpers.with_object_store(exc.object_store):
|
||||||
|
context = prepare_context(exc, cls)
|
||||||
|
model_builders = set(list_model_builders(cls, context))
|
||||||
|
method_names = model_builders.intersection(
|
||||||
|
method_names or model_builders)
|
||||||
|
|
||||||
|
result = {
|
||||||
|
name: generate_entity_schema(
|
||||||
|
get_entity(cls, name), context, cls,
|
||||||
|
get_meta(cls, name, context))
|
||||||
|
for name in method_names
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def list_model_builders(cls, context):
|
||||||
|
"""List model builder names of the class
|
||||||
|
|
||||||
|
Yield names of all model builders (static actions marked with appropriate
|
||||||
|
metadata) plus empty string for the class itself.
|
||||||
|
"""
|
||||||
|
|
||||||
|
yield ''
|
||||||
|
for method_name in cls.all_method_names:
|
||||||
|
try:
|
||||||
|
method = cls.find_single_method(method_name)
|
||||||
|
if not method.is_action or not method.is_static:
|
||||||
|
continue
|
||||||
|
meta = meta_module.aggregate_meta(method, context)
|
||||||
|
is_builder = meta.get('io.murano.metadata.ModelBuilder')
|
||||||
|
if is_builder and is_builder.get_property('enabled'):
|
||||||
|
yield method.name
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def get_meta(cls, method_name, context):
|
||||||
|
"""Get metadata dictionary for the method or class"""
|
||||||
|
if not method_name:
|
||||||
|
return meta_module.aggregate_meta(cls, context)
|
||||||
|
|
||||||
|
method = cls.find_single_method(method_name)
|
||||||
|
return meta_module.aggregate_meta(method, context)
|
||||||
|
|
||||||
|
|
||||||
|
def get_entity(cls, method_name):
|
||||||
|
"""Get MuranoMethod of the class by its name"""
|
||||||
|
if not method_name:
|
||||||
|
return cls
|
||||||
|
method = cls.find_single_method(method_name)
|
||||||
|
return method
|
||||||
|
|
||||||
|
|
||||||
|
def get_properties(entity):
|
||||||
|
"""Get properties/arg scheme of the class/method"""
|
||||||
|
if isinstance(entity, dsl_types.MuranoType):
|
||||||
|
properties = entity.all_property_names
|
||||||
|
result = {}
|
||||||
|
for prop_name in properties:
|
||||||
|
prop = entity.find_single_property(prop_name)
|
||||||
|
|
||||||
|
if prop.usage not in (dsl_types.PropertyUsages.In,
|
||||||
|
dsl_types.PropertyUsages.InOut):
|
||||||
|
continue
|
||||||
|
result[prop_name] = prop
|
||||||
|
return result
|
||||||
|
return entity.arguments_scheme
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_context(exc, cls):
|
||||||
|
"""Registers alternative implementations of contract YAQL functions"""
|
||||||
|
context = exc.create_object_context(cls).create_child_context()
|
||||||
|
context[constants.CTX_NAMES_SCOPE] = cls
|
||||||
|
context.register_function(string_)
|
||||||
|
context.register_function(int_)
|
||||||
|
context.register_function(bool_)
|
||||||
|
context.register_function(not_null)
|
||||||
|
context.register_function(check)
|
||||||
|
context.register_function(class_factory(context))
|
||||||
|
context.register_function(owned)
|
||||||
|
context.register_function(not_owned)
|
||||||
|
context.register_function(finalize)
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
def generate_entity_schema(entity, context, declaring_type, meta):
|
||||||
|
"""Generate schema for single class or method by it DSL entity"""
|
||||||
|
properties = get_properties(entity)
|
||||||
|
type_weights = murano_type.weigh_type_hierarchy(declaring_type)
|
||||||
|
schema = {
|
||||||
|
'$schema': 'http://json-schema.org/draft-04/schema#',
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
name: generate_property_schema(prop, context, type_weights)
|
||||||
|
for name, prop in six.iteritems(properties)
|
||||||
|
},
|
||||||
|
'additionalProperties': False,
|
||||||
|
'formSections': generate_sections(meta, type_weights)
|
||||||
|
}
|
||||||
|
schema.update(generate_ui_hints(entity, context, type_weights))
|
||||||
|
return schema
|
||||||
|
|
||||||
|
|
||||||
|
def generate_sections(meta, type_weights):
|
||||||
|
"""Builds sections definitions for the schema
|
||||||
|
|
||||||
|
Sections are UI hint for UI for grouping inputs into tabs/group-boxes.
|
||||||
|
The code collects section definitions from type hierarchy considering that
|
||||||
|
the section might be redefined in ancestor with the different section
|
||||||
|
index and then re-enumerates them in a way that sections from the most
|
||||||
|
base classes in hierarchy will get lower index values and there be no
|
||||||
|
two sections with the same index.
|
||||||
|
"""
|
||||||
|
section_list = meta.get('io.murano.metadata.forms.Section', [])
|
||||||
|
sections_map = {}
|
||||||
|
for section in section_list:
|
||||||
|
name = section.get_property('name')
|
||||||
|
ex_section = sections_map.get(name)
|
||||||
|
if not ex_section:
|
||||||
|
pass
|
||||||
|
elif (type_weights[ex_section.declaring_type.name] <
|
||||||
|
type_weights[section.declaring_type.name]):
|
||||||
|
continue
|
||||||
|
elif (type_weights[ex_section.declaring_type.name] ==
|
||||||
|
type_weights[section.declaring_type.name]):
|
||||||
|
index = section.get_property('index')
|
||||||
|
if index is None:
|
||||||
|
continue
|
||||||
|
ex_index = ex_section.get_property('index')
|
||||||
|
if ex_index is not None and ex_index <= index:
|
||||||
|
continue
|
||||||
|
sections_map[name] = section
|
||||||
|
|
||||||
|
ordered_sections, unordered_sections = sort_by_index(
|
||||||
|
sections_map.values(), type_weights)
|
||||||
|
sections = {}
|
||||||
|
index = 0
|
||||||
|
for section in ordered_sections:
|
||||||
|
name = section.get_property('name')
|
||||||
|
if name not in sections:
|
||||||
|
sections[name] = {
|
||||||
|
'title': section.get_property('title'),
|
||||||
|
'index': index
|
||||||
|
}
|
||||||
|
index += 1
|
||||||
|
for section in unordered_sections:
|
||||||
|
name = section.get_property('name')
|
||||||
|
if name not in sections:
|
||||||
|
sections[name] = {
|
||||||
|
'title': section.get_property('title')
|
||||||
|
}
|
||||||
|
return sections
|
||||||
|
|
||||||
|
|
||||||
|
def generate_property_schema(prop, context, type_weights):
|
||||||
|
"""Generate schema for single property/argument"""
|
||||||
|
schema = translate(prop.contract.spec, context)
|
||||||
|
if prop.has_default:
|
||||||
|
schema['default'] = prop.default
|
||||||
|
schema.update(generate_ui_hints(prop, context, type_weights))
|
||||||
|
return schema
|
||||||
|
|
||||||
|
|
||||||
|
def generate_ui_hints(entity, context, type_weights):
|
||||||
|
"""Translate know property/arg meta into json-schema UI hints"""
|
||||||
|
schema = {}
|
||||||
|
meta = meta_module.aggregate_meta(entity, context)
|
||||||
|
for cls_name, schema_prop, meta_prop in (
|
||||||
|
('io.murano.metadata.Title', 'title', 'text'),
|
||||||
|
('io.murano.metadata.Description', 'description', 'text'),
|
||||||
|
('io.murano.metadata.HelpText', 'helpText', 'text'),
|
||||||
|
('io.murano.metadata.forms.Hidden', 'visible', 'visible')):
|
||||||
|
value = meta.get(cls_name)
|
||||||
|
if value is not None:
|
||||||
|
schema[schema_prop] = value.get_property(meta_prop)
|
||||||
|
|
||||||
|
position = meta.get('io.murano.metadata.forms.Position')
|
||||||
|
if position:
|
||||||
|
schema['formSection'] = position.get_property('section')
|
||||||
|
index = position.get_property('index')
|
||||||
|
if index is not None:
|
||||||
|
schema['formIndex'] = (
|
||||||
|
(position.get_property('index') + 1) * len(type_weights) -
|
||||||
|
type_weights[position.declaring_type.name])
|
||||||
|
|
||||||
|
return schema
|
||||||
|
|
||||||
|
|
||||||
|
def sort_by_index(meta, type_weights, property_name='index'):
|
||||||
|
"""Sorts meta definitions by its distance in the class hierarchy"""
|
||||||
|
has_index = six.moves.filter(
|
||||||
|
lambda m: m.get_property(property_name) is not None, meta)
|
||||||
|
has_no_index = six.moves.filter(
|
||||||
|
lambda m: m.get_property(property_name) is None, meta)
|
||||||
|
|
||||||
|
return (
|
||||||
|
sorted(has_index,
|
||||||
|
key=lambda m: (
|
||||||
|
(m.get_property(property_name) + 1) *
|
||||||
|
len(type_weights) -
|
||||||
|
type_weights[m.declaring_type.name])),
|
||||||
|
has_no_index)
|
||||||
|
|
||||||
|
|
||||||
|
class Schema(object):
|
||||||
|
"""Container object to define YAQL contracts on"""
|
||||||
|
def __init__(self, data):
|
||||||
|
self.data = data
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return repr(self.data)
|
||||||
|
|
||||||
|
|
||||||
|
@specs.parameter('schema', Schema)
|
||||||
|
@specs.method
|
||||||
|
def string_(schema):
|
||||||
|
"""Implementation of string() contract that generates schema instead"""
|
||||||
|
types = 'string'
|
||||||
|
if '_notNull' not in schema.data:
|
||||||
|
types = [types] + ['null']
|
||||||
|
|
||||||
|
return Schema({
|
||||||
|
'type': types
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def class_factory(context):
|
||||||
|
"""Factory for class() contract function that generates schema instead"""
|
||||||
|
@specs.parameter('schema', Schema)
|
||||||
|
@specs.parameter('name', dsl.MuranoTypeParameter(
|
||||||
|
nullable=False, context=context))
|
||||||
|
@specs.parameter('default_name', dsl.MuranoTypeParameter(
|
||||||
|
nullable=True, context=context))
|
||||||
|
@specs.parameter('version_spec', yaqltypes.String(True))
|
||||||
|
@specs.method
|
||||||
|
def class_(schema, name, default_name=None, version_spec=None):
|
||||||
|
types = 'muranoObject'
|
||||||
|
if '_notNull' not in schema.data:
|
||||||
|
types = [types] + ['null']
|
||||||
|
|
||||||
|
return Schema({
|
||||||
|
'type': types,
|
||||||
|
'muranoType': name.type.name
|
||||||
|
})
|
||||||
|
|
||||||
|
return class_
|
||||||
|
|
||||||
|
|
||||||
|
@specs.parameter('schema', Schema)
|
||||||
|
@specs.method
|
||||||
|
def not_owned(schema):
|
||||||
|
"""Implementation of notOwned() contract that generates schema instead"""
|
||||||
|
schema.data['owned'] = False
|
||||||
|
return schema
|
||||||
|
|
||||||
|
|
||||||
|
@specs.parameter('schema', Schema)
|
||||||
|
@specs.method
|
||||||
|
def owned(schema):
|
||||||
|
"""Implementation of owned() contract that generates schema instead"""
|
||||||
|
schema.data['owned'] = True
|
||||||
|
return schema
|
||||||
|
|
||||||
|
|
||||||
|
@specs.parameter('schema', Schema)
|
||||||
|
@specs.method
|
||||||
|
def int_(schema):
|
||||||
|
"""Implementation of int() contract that generates schema instead"""
|
||||||
|
types = 'integer'
|
||||||
|
if '_notNull' not in schema.data:
|
||||||
|
types = [types] + ['null']
|
||||||
|
|
||||||
|
return Schema({
|
||||||
|
'type': types
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@specs.parameter('schema', Schema)
|
||||||
|
@specs.method
|
||||||
|
def bool_(schema):
|
||||||
|
"""Implementation of bool() contract that generates schema instead"""
|
||||||
|
types = 'boolean'
|
||||||
|
if '_notNull' not in schema.data:
|
||||||
|
types = [types] + ['null']
|
||||||
|
|
||||||
|
return Schema({
|
||||||
|
'type': types
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@specs.parameter('schema', Schema)
|
||||||
|
@specs.method
|
||||||
|
def not_null(schema):
|
||||||
|
"""Implementation of notNull() contract that generates schema instead"""
|
||||||
|
types = schema.data.get('type')
|
||||||
|
if isinstance(types, list) and 'null' in types:
|
||||||
|
types.remove('null')
|
||||||
|
if len(types) == 1:
|
||||||
|
types = types[0]
|
||||||
|
schema.data['type'] = types
|
||||||
|
schema.data['_notNull'] = True
|
||||||
|
return schema
|
||||||
|
|
||||||
|
|
||||||
|
@specs.inject('up', yaqltypes.Super())
|
||||||
|
@specs.name('#finalize')
|
||||||
|
def finalize(obj, up):
|
||||||
|
"""Wrapper around YAQL contracts that removes temporary schema data"""
|
||||||
|
res = up(obj)
|
||||||
|
if isinstance(res, Schema):
|
||||||
|
res = res.data
|
||||||
|
if isinstance(res, dict):
|
||||||
|
res.pop('_notNull', None)
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
@specs.parameter('expr', yaqltypes.YaqlExpression())
|
||||||
|
@specs.parameter('schema', Schema)
|
||||||
|
@specs.method
|
||||||
|
def check(schema, expr, engine, context):
|
||||||
|
"""Implementation of check() contract that generates schema instead"""
|
||||||
|
rest = [True]
|
||||||
|
while rest:
|
||||||
|
if (isinstance(expr, expressions.BinaryOperator) and
|
||||||
|
expr.operator == 'and'):
|
||||||
|
rest = expr.args[1]
|
||||||
|
expr = expr.args[0]
|
||||||
|
else:
|
||||||
|
rest = []
|
||||||
|
res = extract_pattern(expr, engine, context)
|
||||||
|
if res is not None:
|
||||||
|
schema.data.update(res)
|
||||||
|
expr = rest
|
||||||
|
return schema
|
||||||
|
|
||||||
|
|
||||||
|
def extract_pattern(expr, engine, context):
|
||||||
|
"""Translation of certain known patterns of check() contract expressions"""
|
||||||
|
if isinstance(expr, expressions.BinaryOperator):
|
||||||
|
ops = ('>', '<', '>=', '<=')
|
||||||
|
if expr.operator in ops:
|
||||||
|
op_index = ops.index(expr.operator)
|
||||||
|
if is_dollar(expr.args[0]):
|
||||||
|
constant = evaluate_constant(expr.args[1], engine, context)
|
||||||
|
if constant is None:
|
||||||
|
return None
|
||||||
|
elif is_dollar(expr.args[1]):
|
||||||
|
constant = evaluate_constant(expr.args[0], engine, context)
|
||||||
|
if constant is None:
|
||||||
|
return None
|
||||||
|
op_index = -1 - op_index
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
op = ops[op_index]
|
||||||
|
if op == '>':
|
||||||
|
return {'minimum': constant, 'exclusiveMinimum': True}
|
||||||
|
elif op == '>=':
|
||||||
|
return {'minimum': constant, 'exclusiveMinimum': False}
|
||||||
|
if op == '<':
|
||||||
|
return {'maximum': constant, 'exclusiveMaximum': True}
|
||||||
|
elif op == '<=':
|
||||||
|
return {'maximum': constant, 'exclusiveMaximum': False}
|
||||||
|
elif expr.operator == 'in' and is_dollar(expr.args[0]):
|
||||||
|
lst = evaluate_constant(expr.args[1], engine, context)
|
||||||
|
if isinstance(lst, tuple):
|
||||||
|
return {'enum': list(lst)}
|
||||||
|
|
||||||
|
elif (expr.operator == '.' and is_dollar(expr.args[0]) and
|
||||||
|
isinstance(expr.args[1], expressions.Function)):
|
||||||
|
func = expr.args[1]
|
||||||
|
if func.name == 'matches':
|
||||||
|
constant = evaluate_constant(func.args[0], engine, context)
|
||||||
|
if constant is not None:
|
||||||
|
return {'pattern': constant}
|
||||||
|
|
||||||
|
|
||||||
|
def is_dollar(expr):
|
||||||
|
"""Check $-expressions in YAQL AST"""
|
||||||
|
return (isinstance(expr, expressions.GetContextValue) and
|
||||||
|
expr.path.value in ('$', '$1'))
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate_constant(expr, engine, context):
|
||||||
|
"""Evaluate yaql expression into constant value if possible"""
|
||||||
|
if isinstance(expr, expressions.Constant):
|
||||||
|
return expr.value
|
||||||
|
context = context.create_child_context()
|
||||||
|
trap = utils.create_marker('trap')
|
||||||
|
context['$'] = trap
|
||||||
|
|
||||||
|
@specs.parameter('name', yaqltypes.StringConstant())
|
||||||
|
@specs.name('#get_context_data')
|
||||||
|
def get_context_data(name, context):
|
||||||
|
res = context[name]
|
||||||
|
if res is trap:
|
||||||
|
raise yaql_exceptions.ResolutionError()
|
||||||
|
return res
|
||||||
|
|
||||||
|
context.register_function(get_context_data)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return expressions.Statement(expr, engine).evaluate(context=context)
|
||||||
|
except yaql_exceptions.YaqlException:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def translate(contract, context):
|
||||||
|
"""Translates contracts into json-schema equivalents"""
|
||||||
|
if isinstance(contract, dict):
|
||||||
|
return translate_dict(contract, context)
|
||||||
|
elif isinstance(contract, list):
|
||||||
|
return translate_list(contract, context)
|
||||||
|
elif isinstance(contract, (dsl_types.YaqlExpression,
|
||||||
|
expressions.Statement)):
|
||||||
|
context = context.create_child_context()
|
||||||
|
context['$'] = Schema({})
|
||||||
|
return contract(context=context)
|
||||||
|
|
||||||
|
|
||||||
|
def translate_dict(contract, context):
|
||||||
|
"""Translates dictionary contracts into json-schema objects"""
|
||||||
|
properties = {}
|
||||||
|
additional_properties = False
|
||||||
|
for key, value in six.iteritems(contract):
|
||||||
|
if isinstance(key, dsl_types.YaqlExpression):
|
||||||
|
additional_properties = translate(value, context)
|
||||||
|
else:
|
||||||
|
properties[key] = translate(value, context)
|
||||||
|
return {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': properties,
|
||||||
|
'additionalProperties': additional_properties
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def translate_list(contract, context):
|
||||||
|
"""Translates list contracts into json-schema arrays"""
|
||||||
|
items = []
|
||||||
|
for value in contract:
|
||||||
|
if isinstance(value, int):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
items.append(translate(value, context))
|
||||||
|
if len(items) == 0:
|
||||||
|
return {'type': 'array'}
|
||||||
|
elif len(items) == 1:
|
||||||
|
return {
|
||||||
|
'type': 'array',
|
||||||
|
'items': items[0],
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
'type': 'array',
|
||||||
|
'items': items,
|
||||||
|
'additionalItems': items[-1]
|
||||||
|
}
|
|
@ -31,6 +31,10 @@ class TypeScheme(object):
|
||||||
def __init__(self, spec):
|
def __init__(self, spec):
|
||||||
self._spec = spec
|
self._spec = spec
|
||||||
|
|
||||||
|
@property
|
||||||
|
def spec(self):
|
||||||
|
return self._spec
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def prepare_transform_context(root_context, this, owner, default,
|
def prepare_transform_context(root_context, this, owner, default,
|
||||||
calling_type):
|
calling_type):
|
||||||
|
|
|
@ -0,0 +1,125 @@
|
||||||
|
Namespaces:
|
||||||
|
m: io.murano.metadata
|
||||||
|
mf: io.murano.metadata.forms
|
||||||
|
|
||||||
|
Name: TestSchema
|
||||||
|
|
||||||
|
Meta:
|
||||||
|
- mf:Section:
|
||||||
|
name: mySection
|
||||||
|
title: Section Title
|
||||||
|
index: 1
|
||||||
|
|
||||||
|
Properties:
|
||||||
|
stringProperty:
|
||||||
|
Contract: $.string()
|
||||||
|
|
||||||
|
stringNotNullProperty:
|
||||||
|
Contract: $.string().notNull()
|
||||||
|
|
||||||
|
intProperty:
|
||||||
|
Contract: $.int()
|
||||||
|
|
||||||
|
intNotNullProperty:
|
||||||
|
Contract: $.int().notNull()
|
||||||
|
|
||||||
|
boolProperty:
|
||||||
|
Contract: $.bool()
|
||||||
|
|
||||||
|
boolNotNullProperty:
|
||||||
|
Contract: $.bool().notNull()
|
||||||
|
|
||||||
|
listProperty:
|
||||||
|
Contract:
|
||||||
|
- $.string().notNull()
|
||||||
|
|
||||||
|
dictProperty:
|
||||||
|
Contract:
|
||||||
|
key1: $.string().notNull()
|
||||||
|
key2: $.string().notNull()
|
||||||
|
$.string().notNull(): $.int()
|
||||||
|
|
||||||
|
classProperty:
|
||||||
|
Contract: $.class(SampleClass1)
|
||||||
|
|
||||||
|
defaultProperty:
|
||||||
|
Contract: $.int()
|
||||||
|
Default: 999
|
||||||
|
|
||||||
|
complexProperty:
|
||||||
|
Contract:
|
||||||
|
$.string(): [$.int().notNull()]
|
||||||
|
|
||||||
|
minimumContract:
|
||||||
|
Contract: $.int().notNull().check($ >= 5)
|
||||||
|
|
||||||
|
maximumContract:
|
||||||
|
Contract: $.int().notNull().check($ < 15)
|
||||||
|
|
||||||
|
rangeContract:
|
||||||
|
Contract: $.int().notNull().check($ > 0 and $ <= 10)
|
||||||
|
|
||||||
|
chainContract:
|
||||||
|
Contract: $.int().notNull().check($ > 0).check($ <= 10)
|
||||||
|
|
||||||
|
regexContract:
|
||||||
|
Contract: $.string().notNull().check($.matches(`\d+`))
|
||||||
|
|
||||||
|
enumContract:
|
||||||
|
Contract: $.string().notNull().check($ in [a, b])
|
||||||
|
|
||||||
|
enumFuncContract:
|
||||||
|
Contract: $.string().notNull().check($ in $this.staticEnumMethod())
|
||||||
|
|
||||||
|
decoratedProperty:
|
||||||
|
Contract: $.string().notNull()
|
||||||
|
Meta:
|
||||||
|
- m:Title:
|
||||||
|
text: Title!
|
||||||
|
- m:Description:
|
||||||
|
text: Description!
|
||||||
|
- m:HelpText:
|
||||||
|
text: Help!
|
||||||
|
- mf:Hidden
|
||||||
|
- mf:Position:
|
||||||
|
index: 1
|
||||||
|
section: mySection
|
||||||
|
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
staticEnumMethod:
|
||||||
|
Body:
|
||||||
|
Return:
|
||||||
|
- x
|
||||||
|
- y
|
||||||
|
Usage: Static
|
||||||
|
|
||||||
|
modelBuilder:
|
||||||
|
Meta:
|
||||||
|
- m:ModelBuilder
|
||||||
|
- m:Title:
|
||||||
|
text: Model Builder!
|
||||||
|
Usage: Static
|
||||||
|
Scope: Public
|
||||||
|
Arguments:
|
||||||
|
- arg1:
|
||||||
|
Meta:
|
||||||
|
m:Title:
|
||||||
|
text: Arg1!
|
||||||
|
Contract: $.string().notNull()
|
||||||
|
- arg2:
|
||||||
|
Contract: $.int().notNull()
|
||||||
|
|
||||||
|
invalidModelBuilder1:
|
||||||
|
Meta:
|
||||||
|
- m:ModelBuilder
|
||||||
|
|
||||||
|
invalidModelBuilder2:
|
||||||
|
Meta:
|
||||||
|
- m:ModelBuilder
|
||||||
|
Usage: Static
|
||||||
|
|
||||||
|
invalidModelBuilder3:
|
||||||
|
Meta:
|
||||||
|
- m:ModelBuilder
|
||||||
|
Scope: Public
|
|
@ -0,0 +1,175 @@
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
from testtools import matchers
|
||||||
|
|
||||||
|
from murano.dsl import schema_generator
|
||||||
|
from murano.tests.unit.dsl.foundation import runner
|
||||||
|
from murano.tests.unit.dsl.foundation import test_case
|
||||||
|
|
||||||
|
|
||||||
|
class TestSchemaGeneration(test_case.DslTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(TestSchemaGeneration, self).setUp()
|
||||||
|
schema = schema_generator.generate_schema(
|
||||||
|
self.package_loader, runner.TestContextManager({}),
|
||||||
|
'TestSchema')
|
||||||
|
|
||||||
|
self._class_schema = schema.pop('')
|
||||||
|
self._model_builders_schema = schema
|
||||||
|
|
||||||
|
def test_general_structure(self):
|
||||||
|
self.assertIn('$schema', self._class_schema)
|
||||||
|
self.assertIn('type', self._class_schema)
|
||||||
|
self.assertIn('properties', self._class_schema)
|
||||||
|
self.assertEqual(
|
||||||
|
'http://json-schema.org/draft-04/schema#',
|
||||||
|
self._class_schema['$schema'])
|
||||||
|
self.assertEqual('object', self._class_schema['type'])
|
||||||
|
|
||||||
|
def _test_simple_property(self, name_or_schema, types):
|
||||||
|
if not isinstance(name_or_schema, dict):
|
||||||
|
props = self._class_schema['properties']
|
||||||
|
self.assertIn(name_or_schema, props)
|
||||||
|
schema = props[name_or_schema]
|
||||||
|
else:
|
||||||
|
schema = name_or_schema
|
||||||
|
self.assertIn('type', schema)
|
||||||
|
if isinstance(types, list):
|
||||||
|
self.assertItemsEqual(schema['type'], types)
|
||||||
|
else:
|
||||||
|
self.assertEqual(schema['type'], types)
|
||||||
|
return schema
|
||||||
|
|
||||||
|
def test_string_property(self):
|
||||||
|
self._test_simple_property('stringProperty', ['null', 'string'])
|
||||||
|
|
||||||
|
def test_not_null_string_property(self):
|
||||||
|
self._test_simple_property('stringNotNullProperty', 'string')
|
||||||
|
|
||||||
|
def test_int_property(self):
|
||||||
|
self._test_simple_property('intProperty', ['null', 'integer'])
|
||||||
|
|
||||||
|
def test_not_null_int_property(self):
|
||||||
|
self._test_simple_property('intNotNullProperty', 'integer')
|
||||||
|
|
||||||
|
def test_bool_property(self):
|
||||||
|
self._test_simple_property('boolProperty', ['null', 'boolean'])
|
||||||
|
|
||||||
|
def test_not_null_bool_property(self):
|
||||||
|
self._test_simple_property('boolNotNullProperty', 'boolean')
|
||||||
|
|
||||||
|
def test_class_property(self):
|
||||||
|
schema = self._test_simple_property(
|
||||||
|
'classProperty', ['null', 'muranoObject'])
|
||||||
|
self.assertEqual('SampleClass1', schema.get('muranoType'))
|
||||||
|
|
||||||
|
def test_default_property(self):
|
||||||
|
schema = self._test_simple_property(
|
||||||
|
'defaultProperty', ['null', 'integer'])
|
||||||
|
self.assertEqual(999, schema.get('default'))
|
||||||
|
|
||||||
|
def test_list_property(self):
|
||||||
|
schema = self._test_simple_property('listProperty', 'array')
|
||||||
|
self.assertIn('items', schema)
|
||||||
|
items = schema['items']
|
||||||
|
self._test_simple_property(items, 'string')
|
||||||
|
|
||||||
|
def test_dict_property(self):
|
||||||
|
schema = self._test_simple_property('dictProperty', 'object')
|
||||||
|
self.assertIn('properties', schema)
|
||||||
|
props = schema['properties']
|
||||||
|
self.assertIn('key1', props)
|
||||||
|
self._test_simple_property(props['key1'], 'string')
|
||||||
|
self.assertIn('key2', props)
|
||||||
|
self._test_simple_property(props['key2'], 'string')
|
||||||
|
self.assertIn('additionalProperties', schema)
|
||||||
|
extra_props = schema['additionalProperties']
|
||||||
|
self._test_simple_property(extra_props, ['null', 'integer'])
|
||||||
|
|
||||||
|
def test_complex_property(self):
|
||||||
|
schema = self._test_simple_property('complexProperty', 'object')
|
||||||
|
self.assertIn('properties', schema)
|
||||||
|
self.assertEqual({}, schema['properties'])
|
||||||
|
self.assertIn('additionalProperties', schema)
|
||||||
|
extra_props = schema['additionalProperties']
|
||||||
|
self._test_simple_property(extra_props, 'array')
|
||||||
|
self.assertIn('items', extra_props)
|
||||||
|
items = extra_props['items']
|
||||||
|
self._test_simple_property(items, 'integer')
|
||||||
|
|
||||||
|
def test_minimum_contract(self):
|
||||||
|
schema = self._test_simple_property('minimumContract', 'integer')
|
||||||
|
self.assertFalse(schema.get('exclusiveMinimum', True))
|
||||||
|
self.assertEqual(5, schema.get('minimum'))
|
||||||
|
|
||||||
|
def test_maximum_contract(self):
|
||||||
|
schema = self._test_simple_property('maximumContract', 'integer')
|
||||||
|
self.assertTrue(schema.get('exclusiveMaximum', False))
|
||||||
|
self.assertEqual(15, schema.get('maximum'))
|
||||||
|
|
||||||
|
def test_range_contract(self):
|
||||||
|
schema = self._test_simple_property('rangeContract', 'integer')
|
||||||
|
self.assertFalse(schema.get('exclusiveMaximum', True))
|
||||||
|
self.assertTrue(schema.get('exclusiveMinimum', False))
|
||||||
|
self.assertEqual(0, schema.get('minimum'))
|
||||||
|
self.assertEqual(10, schema.get('maximum'))
|
||||||
|
|
||||||
|
def test_chain_contract(self):
|
||||||
|
schema = self._test_simple_property('chainContract', 'integer')
|
||||||
|
self.assertFalse(schema.get('exclusiveMaximum', True))
|
||||||
|
self.assertTrue(schema.get('exclusiveMinimum', False))
|
||||||
|
self.assertEqual(0, schema.get('minimum'))
|
||||||
|
self.assertEqual(10, schema.get('maximum'))
|
||||||
|
|
||||||
|
def test_regex_contract(self):
|
||||||
|
schema = self._test_simple_property('regexContract', 'string')
|
||||||
|
self.assertEqual(r'\d+', schema.get('pattern'))
|
||||||
|
|
||||||
|
def test_enum_contract(self):
|
||||||
|
schema = self._test_simple_property('enumContract', 'string')
|
||||||
|
self.assertEqual(['a', 'b'], schema.get('enum'))
|
||||||
|
|
||||||
|
def test_enum_func_contract(self):
|
||||||
|
schema = self._test_simple_property('enumFuncContract', 'string')
|
||||||
|
self.assertEqual(['x', 'y'], schema.get('enum'))
|
||||||
|
|
||||||
|
def test_ui_hints(self):
|
||||||
|
schema = self._test_simple_property('decoratedProperty', 'string')
|
||||||
|
self.assertEqual('Title!', schema.get('title'))
|
||||||
|
self.assertEqual('Description!', schema.get('description'))
|
||||||
|
self.assertEqual('Help!', schema.get('helpText'))
|
||||||
|
self.assertFalse(schema.get('visible'))
|
||||||
|
self.assertThat(schema.get('formIndex'), matchers.GreaterThan(-1))
|
||||||
|
self.assertEqual('mySection', schema.get('formSection'))
|
||||||
|
|
||||||
|
sections = self._class_schema.get('formSections')
|
||||||
|
self.assertIsInstance(sections, dict)
|
||||||
|
section = sections.get('mySection')
|
||||||
|
self.assertIsInstance(section, dict)
|
||||||
|
self.assertThat(section.get('index'), matchers.GreaterThan(-1))
|
||||||
|
self.assertEqual('Section Title', section.get('title'))
|
||||||
|
|
||||||
|
def test_model_builders(self):
|
||||||
|
self.assertEqual(1, len(self._model_builders_schema))
|
||||||
|
schema = self._model_builders_schema.get('modelBuilder')
|
||||||
|
self.assertIsInstance(schema, dict)
|
||||||
|
self._class_schema = schema
|
||||||
|
self.test_general_structure()
|
||||||
|
self.assertEqual('Model Builder!', schema.get('title'))
|
||||||
|
args = schema['properties']
|
||||||
|
self._test_simple_property(args.get('arg1'), 'string')
|
||||||
|
self._test_simple_property(args.get('arg2'), 'integer')
|
||||||
|
arg1 = args['arg1']
|
||||||
|
self.assertEqual('Arg1!', arg1.get('title'))
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
features:
|
||||||
|
- New engine RPC call to generate json-schema from MuranoPL class.
|
||||||
|
Schema may be generated either from entire class or for specific
|
||||||
|
model builders - static actions that can be used to generate
|
||||||
|
object model from their input. Class schema is built by inspecting
|
||||||
|
class properties and method schema using the same algorithm but applied
|
||||||
|
to its arguments.
|
Loading…
Reference in New Issue