Merge "Initial commit for repository API support"
This commit is contained in:
commit
3621c57404
@ -28,6 +28,9 @@ use_syslog = False
|
||||
#Syslog facility to receive log lines
|
||||
syslog_log_facility = LOG_LOCAL0
|
||||
|
||||
# Role used to identify an authenticated user as administrator
|
||||
#admin_role = admin
|
||||
|
||||
# Use durable queues in amqp. (boolean value)
|
||||
# Deprecated group/name - [DEFAULT]/rabbit_durable_queues
|
||||
#amqp_durable_queues=false
|
||||
|
@ -15,9 +15,17 @@
|
||||
from oslo.config import cfg
|
||||
|
||||
import muranoapi.context
|
||||
from muranoapi.openstack.common.gettextutils import _ # noqa
|
||||
import muranoapi.openstack.common.log as logging
|
||||
from muranoapi.openstack.common import wsgi
|
||||
|
||||
context_opts = [
|
||||
cfg.StrOpt('admin_role', default='admin',
|
||||
help=_('Role used to identify an authenticated user as '
|
||||
'administrator.'))]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(context_opts)
|
||||
LOG = logging.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
|
||||
@ -32,11 +40,14 @@ class ContextMiddleware(wsgi.Middleware):
|
||||
|
||||
:param req: wsgi request object that will be given the context object
|
||||
"""
|
||||
|
||||
kwargs = {
|
||||
'user': req.headers.get('X-User-Id'),
|
||||
'tenant': req.headers.get('X-Tenant-Id'),
|
||||
'auth_token': req.headers.get('X-Auth-Token'),
|
||||
'session': req.headers.get('X-Configuration-Session')
|
||||
'session': req.headers.get('X-Configuration-Session'),
|
||||
'is_admin': CONF.admin_role in [
|
||||
role.strip() for role in req.headers.get('X-Roles').split(',')]
|
||||
}
|
||||
req.context = muranoapi.context.RequestContext(**kwargs)
|
||||
|
||||
|
71
muranoapi/api/v1/catalog.py
Normal file
71
muranoapi/api/v1/catalog.py
Normal file
@ -0,0 +1,71 @@
|
||||
# Copyright (c) 2014 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 oslo.config import cfg
|
||||
from webob import exc
|
||||
|
||||
from muranoapi.db.catalog import api as db_api
|
||||
from muranoapi.openstack.common import exception
|
||||
from muranoapi.openstack.common.gettextutils import _ # noqa
|
||||
from muranoapi.openstack.common import log as logging
|
||||
from muranoapi.openstack.common import wsgi
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
def _check_content_type(req, content_type):
|
||||
try:
|
||||
req.get_content_type((content_type,))
|
||||
except exception.InvalidContentType:
|
||||
msg = _("Content-Type must be '{0}'").format(content_type)
|
||||
LOG.debug(msg)
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
|
||||
class Controller(object):
|
||||
"""
|
||||
WSGI controller for application catalog resource in Murano v1 API
|
||||
"""
|
||||
|
||||
def update(self, req, body, package_id):
|
||||
"""
|
||||
List of allowed changes:
|
||||
{ "op": "add", "path": "/tags", "value": [ "foo", "bar" ] }
|
||||
{ "op": "add", "path": "/categories", "value": [ "foo", "bar" ] }
|
||||
{ "op": "remove", "path": "/tags" }
|
||||
{ "op": "replace", "path": "/tags", "value": ["foo", "bar"] }
|
||||
{ "op": "replace", "path": "/is_public", "value": true }
|
||||
{ "op": "replace", "path": "/description",
|
||||
"value":"New description" }
|
||||
{ "op": "replace", "path": "/name", "value": "New name" }
|
||||
"""
|
||||
_check_content_type(req, 'application/murano-packages-json-patch')
|
||||
if not isinstance(body, list):
|
||||
msg = _('Request body must be a JSON array of operation objects.')
|
||||
LOG.error(msg)
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
package = db_api.package_update(package_id, body, req.context)
|
||||
|
||||
return package.to_dict()
|
||||
|
||||
def get(self, req, package_id):
|
||||
package = db_api.package_get(package_id, req.context)
|
||||
return package.to_dict()
|
||||
|
||||
|
||||
def create_resource():
|
||||
return wsgi.Resource(Controller())
|
@ -13,6 +13,7 @@
|
||||
# under the License.
|
||||
import routes
|
||||
|
||||
from muranoapi.api.v1 import catalog
|
||||
from muranoapi.api.v1 import deployments
|
||||
from muranoapi.api.v1 import environments
|
||||
from muranoapi.api.v1 import services
|
||||
@ -119,4 +120,13 @@ class API(wsgi.Router):
|
||||
action='deploy',
|
||||
conditions={'method': ['POST']})
|
||||
|
||||
catalog_resource = catalog.create_resource()
|
||||
mapper.connect('/catalog/packages/{package_id}',
|
||||
controller=catalog_resource,
|
||||
action='get',
|
||||
conditions={'method': ['GET']})
|
||||
mapper.connect('/catalog/packages/{package_id}',
|
||||
controller=catalog_resource,
|
||||
action='update',
|
||||
conditions={'method': ['PATCH']})
|
||||
super(API, self).__init__(mapper)
|
||||
|
@ -23,3 +23,28 @@ ENV_SCHEMA = {
|
||||
},
|
||||
"required": ["id", "name"]
|
||||
}
|
||||
|
||||
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",
|
||||
"minItems": 1,
|
||||
"items": {"type": "string"},
|
||||
"uniqueItems": True
|
||||
},
|
||||
"description": {"type": "string"},
|
||||
"name": {"type": "string"},
|
||||
"is_public": {"type": "boolean"},
|
||||
"enabled": {"type": "boolean"}
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"minProperties": 1,
|
||||
}
|
||||
|
@ -19,11 +19,13 @@ class RequestContext(object):
|
||||
accesses the system, as well as additional request information.
|
||||
"""
|
||||
|
||||
def __init__(self, auth_token=None, user=None, tenant=None, session=None):
|
||||
def __init__(self, auth_token=None, user=None,
|
||||
tenant=None, session=None, is_admin=None):
|
||||
self.auth_token = auth_token
|
||||
self.user = user
|
||||
self.tenant = tenant
|
||||
self.session = session
|
||||
self.is_admin = is_admin
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
|
0
muranoapi/db/catalog/__init__.py
Normal file
0
muranoapi/db/catalog/__init__.py
Normal file
206
muranoapi/db/catalog/api.py
Normal file
206
muranoapi/db/catalog/api.py
Normal file
@ -0,0 +1,206 @@
|
||||
# Copyright (c) 2014 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 webob import exc
|
||||
|
||||
from muranoapi.db import models
|
||||
from muranoapi.db import session as db_session
|
||||
from muranoapi.openstack.common.gettextutils import _ # noqa
|
||||
from muranoapi.openstack.common import log as logging
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_category_by_name(name):
|
||||
session = db_session.get_session()
|
||||
return session.query(models.Category).filter_by(name=name).first()
|
||||
|
||||
|
||||
def category_get_names():
|
||||
session = db_session.get_session()
|
||||
categories = []
|
||||
for row in session.query(models.Category.name).all():
|
||||
for name in row:
|
||||
categories.append(name)
|
||||
return categories
|
||||
|
||||
|
||||
def get_tag_by_name(name):
|
||||
session = db_session.get_session()
|
||||
return session.query(models.Tag).filter_by(name=name).first()
|
||||
|
||||
|
||||
def _package_get(package_id, session):
|
||||
package = session.query(models.Package).get(package_id)
|
||||
if not package:
|
||||
msg = _("Package id '{0}' is not found".format(package_id))
|
||||
LOG.error(msg)
|
||||
raise exc.HTTPNotFound(msg)
|
||||
return package
|
||||
|
||||
|
||||
def _authorize_package(package, context, allow_public=False):
|
||||
if context.is_admin:
|
||||
return
|
||||
|
||||
if package.owner_id != context.tenant:
|
||||
if not allow_public:
|
||||
msg = _("Package '{0}' is not owned by "
|
||||
"tenant '{1}'".format(package.id, context.tenant))
|
||||
LOG.error(msg)
|
||||
raise exc.HTTPForbidden(msg)
|
||||
if not package.is_public:
|
||||
msg = _("Package '{0}' is not public and not owned by "
|
||||
"tenant '{1}' ".format(package.id, context.tenant))
|
||||
LOG.error(msg)
|
||||
raise exc.HTTPForbidden(msg)
|
||||
|
||||
|
||||
def package_get(package_id, context):
|
||||
"""
|
||||
Return package details
|
||||
:param package_id: ID of a package, string
|
||||
:returns: detailed information about package, dict
|
||||
"""
|
||||
session = db_session.get_session()
|
||||
package = _package_get(package_id, session)
|
||||
_authorize_package(package, context, allow_public=True)
|
||||
return package
|
||||
|
||||
|
||||
def _get_categories(category_names):
|
||||
"""
|
||||
Return existing category objects or raise an exception
|
||||
:param category_names: name of categories to associate with package, list
|
||||
:returns: list of Category objects to associate with package, list
|
||||
"""
|
||||
categories = []
|
||||
for ctg_name in category_names:
|
||||
ctg_obj = get_category_by_name(ctg_name)
|
||||
if not ctg_obj:
|
||||
# it's not allowed to specify non-existent categories
|
||||
raise exc.HTTPBadRequest(
|
||||
"Category '{name}' doesn't exist".format(name=ctg_name))
|
||||
categories.append(ctg_obj)
|
||||
return categories
|
||||
|
||||
|
||||
def _get_tags(tag_names):
|
||||
"""
|
||||
Return existing tags object or create new ones
|
||||
:param tag_names: name of tags to associate with package, list
|
||||
:returns: list of Tag objects to associate with package, list
|
||||
"""
|
||||
tags = []
|
||||
if tag_names:
|
||||
for tag_name in tag_names:
|
||||
tag_obj = get_tag_by_name(tag_name)
|
||||
if tag_obj:
|
||||
tags.append(tag_obj)
|
||||
else:
|
||||
tag_record = models.Tag(name=tag_name)
|
||||
tags.append(tag_record)
|
||||
return tags
|
||||
|
||||
|
||||
def _do_replace(package, change):
|
||||
path = change['path'][0]
|
||||
value = change['value']
|
||||
calculate = {'categories': _get_categories,
|
||||
'tags': _get_tags}
|
||||
if path in ('categories', 'tags'):
|
||||
existing_items = getattr(package, path)
|
||||
|
||||
duplicates = list(set(i.name for i in existing_items) & set(value))
|
||||
unique_values = [x for x in value if x not in duplicates]
|
||||
items_to_replace = calculate[path](unique_values)
|
||||
|
||||
# NOTE(efedorova): Replacing duplicate entities is not allowed,
|
||||
# so need to remove anything, but duplicates
|
||||
# and append everything but duplicates
|
||||
for item in list(existing_items):
|
||||
if item.name not in duplicates:
|
||||
existing_items.remove(item)
|
||||
|
||||
existing_items.extend(items_to_replace)
|
||||
else:
|
||||
setattr(package, path, value)
|
||||
|
||||
return package
|
||||
|
||||
|
||||
def _do_add(package, change):
|
||||
# Only categories and tags support addition
|
||||
path = change['path'][0]
|
||||
value = change['value']
|
||||
|
||||
calculate = {'categories': _get_categories,
|
||||
'tags': _get_tags}
|
||||
items_to_add = calculate[path](value)
|
||||
for item in items_to_add:
|
||||
try:
|
||||
getattr(package, path).append(item)
|
||||
except AssertionError:
|
||||
msg = _('One of the specified {0} is already '
|
||||
'associated with a package. Doing nothing.')
|
||||
LOG.warning(msg.format(path))
|
||||
return package
|
||||
|
||||
|
||||
def _do_remove(package, change):
|
||||
# Only categories and tags support removal
|
||||
def find(seq, predicate):
|
||||
for elt in seq:
|
||||
if predicate(elt):
|
||||
return elt
|
||||
|
||||
path = change['path'][0]
|
||||
values = change['value']
|
||||
|
||||
current_values = getattr(package, path)
|
||||
for value in values:
|
||||
if value not in [i.name for i in current_values]:
|
||||
msg = _("Value '{0}' of property '{1}' "
|
||||
"does not exist.").format(value, path)
|
||||
LOG.error(msg)
|
||||
raise exc.HTTPNotFound(msg)
|
||||
if path == 'categories' and len(current_values) == 1:
|
||||
msg = _("At least one category should be assigned to the package")
|
||||
LOG.error(msg)
|
||||
raise exc.HTTPBadRequest(msg)
|
||||
item_to_remove = find(current_values, lambda i: i.name == value)
|
||||
current_values.remove(item_to_remove)
|
||||
return package
|
||||
|
||||
|
||||
def package_update(pkg_id, changes, context):
|
||||
"""
|
||||
Update package information
|
||||
:param changes: parameters to update
|
||||
:returns: detailed information about new package, dict
|
||||
"""
|
||||
|
||||
operation_methods = {'add': _do_add,
|
||||
'replace': _do_replace,
|
||||
'remove': _do_remove}
|
||||
session = db_session.get_session()
|
||||
pkg = _package_get(pkg_id, session)
|
||||
_authorize_package(pkg, context)
|
||||
|
||||
for change in changes:
|
||||
pkg = operation_methods[change['op']](pkg, change)
|
||||
|
||||
with session.begin():
|
||||
session.add(pkg)
|
||||
return pkg
|
@ -257,6 +257,20 @@ class Package(BASE, ModelBase):
|
||||
lazy='joined')
|
||||
class_definition = sa_orm.relationship("Class")
|
||||
|
||||
def to_dict(self):
|
||||
d = self.__dict__.copy()
|
||||
not_serializable = ['_sa_instance_state',
|
||||
'archive',
|
||||
'logo',
|
||||
'ui_definition']
|
||||
nested_objects = ['categories', 'tags']
|
||||
for key in not_serializable:
|
||||
if key in d.keys():
|
||||
del d[key]
|
||||
for key in nested_objects:
|
||||
d[key] = [a.name for a in d.get(key)]
|
||||
return d
|
||||
|
||||
|
||||
class Category(BASE, ModelBase):
|
||||
"""
|
||||
|
@ -63,7 +63,7 @@ def _create_facade_lazily():
|
||||
|
||||
if _FACADE is None:
|
||||
_FACADE = db_session.EngineFacade(
|
||||
CONF.database.connection,
|
||||
CONF.database.connection, sqlite_fk=True,
|
||||
**dict(CONF.database.iteritems())
|
||||
)
|
||||
return _FACADE
|
||||
|
@ -22,6 +22,8 @@ eventlet.patcher.monkey_patch(all=False, socket=True)
|
||||
|
||||
import datetime
|
||||
import errno
|
||||
import re
|
||||
import jsonschema
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
@ -35,6 +37,7 @@ import webob.exc
|
||||
from xml.dom import minidom
|
||||
from xml.parsers import expat
|
||||
|
||||
from muranoapi.api.v1 import schemas
|
||||
from muranoapi.openstack.common import exception
|
||||
from muranoapi.openstack.common.gettextutils import _
|
||||
from muranoapi.openstack.common import jsonutils
|
||||
@ -108,7 +111,7 @@ class Service(service.Service):
|
||||
eventlet.sleep(0.1)
|
||||
if not sock:
|
||||
raise RuntimeError(_("Could not bind to %(host)s:%(port)s "
|
||||
"after trying for 30 seconds") %
|
||||
"after trying for 30 seconds") %
|
||||
{'host': host, 'port': port})
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
# sockets can hang around forever without keepalive
|
||||
@ -233,7 +236,6 @@ class Debug(Middleware):
|
||||
|
||||
|
||||
class Router(object):
|
||||
|
||||
"""
|
||||
WSGI middleware that maps incoming requests to WSGI apps.
|
||||
"""
|
||||
@ -292,7 +294,9 @@ class Router(object):
|
||||
class Request(webob.Request):
|
||||
"""Add some Openstack API-specific logic to the base webob.Request."""
|
||||
|
||||
default_request_content_types = ('application/json', 'application/xml')
|
||||
default_request_content_types = ('application/json',
|
||||
'application/xml',
|
||||
'application/murano-packages-json-patch')
|
||||
default_accept_types = ('application/json', 'application/xml')
|
||||
default_accept_type = 'application/json'
|
||||
|
||||
@ -350,6 +354,7 @@ class Resource(object):
|
||||
may raise a webob.exc exception or return a dict, which will be
|
||||
serialized by requested content type.
|
||||
"""
|
||||
|
||||
def __init__(self, controller, deserializer=None, serializer=None):
|
||||
"""
|
||||
:param controller: object that implement methods created by routes lib
|
||||
@ -452,11 +457,11 @@ class JSONDictSerializer(DictSerializer):
|
||||
_dtime = obj - datetime.timedelta(microseconds=obj.microsecond)
|
||||
return _dtime.isoformat()
|
||||
return unicode(obj)
|
||||
|
||||
return jsonutils.dumps(data, default=sanitizer)
|
||||
|
||||
|
||||
class XMLDictSerializer(DictSerializer):
|
||||
|
||||
def __init__(self, metadata=None, xmlns=None):
|
||||
"""
|
||||
:param metadata: information needed to deserialize xml into
|
||||
@ -627,6 +632,7 @@ class RequestDeserializer(object):
|
||||
self.body_deserializers = {
|
||||
'application/xml': XMLDeserializer(),
|
||||
'application/json': JSONDeserializer(),
|
||||
'application/murano-packages-json-patch': JSONPatchDeserializer()
|
||||
}
|
||||
self.body_deserializers.update(body_deserializers or {})
|
||||
|
||||
@ -718,7 +724,6 @@ class TextDeserializer(ActionDispatcher):
|
||||
|
||||
|
||||
class JSONDeserializer(TextDeserializer):
|
||||
|
||||
def _from_json(self, datastring):
|
||||
try:
|
||||
return jsonutils.loads(datastring)
|
||||
@ -730,8 +735,142 @@ class JSONDeserializer(TextDeserializer):
|
||||
return {'body': self._from_json(datastring)}
|
||||
|
||||
|
||||
class XMLDeserializer(TextDeserializer):
|
||||
class JSONPatchDeserializer(TextDeserializer):
|
||||
allowed_operations = {"categories": ["add", "replace", "remove"],
|
||||
"tags": ["add", "replace", "remove"],
|
||||
"is_public": ["replace"],
|
||||
"enabled": ["replace"],
|
||||
"description": ["replace"],
|
||||
"name": ["replace"]}
|
||||
|
||||
def _from_json_patch(self, datastring):
|
||||
try:
|
||||
operations = jsonutils.loads(datastring)
|
||||
except ValueError:
|
||||
msg = _("cannot understand JSON")
|
||||
raise exception.MalformedRequestBody(reason=msg)
|
||||
|
||||
changes = []
|
||||
for raw_change in operations:
|
||||
if not isinstance(raw_change, dict):
|
||||
msg = _('Operations must be JSON objects.')
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
(op, path) = self._parse_json_schema_change(raw_change)
|
||||
|
||||
self._validate_path(path)
|
||||
change = {'op': op, 'path': path}
|
||||
|
||||
change['value'] = self._get_change_value(raw_change, op)
|
||||
self._validate_change(change)
|
||||
|
||||
changes.append(change)
|
||||
return changes
|
||||
|
||||
def _get_change_value(self, raw_change, op):
|
||||
if 'value' not in raw_change:
|
||||
msg = _('Operation "%s" requires a member named "value".')
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg % op)
|
||||
return raw_change['value']
|
||||
|
||||
def _get_change_operation(self, raw_change):
|
||||
try:
|
||||
return raw_change['op']
|
||||
except KeyError:
|
||||
msg = _("Unable to find '%s' in JSON Schema change") % 'op'
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
def _get_change_path(self, raw_change):
|
||||
try:
|
||||
return raw_change['path']
|
||||
except KeyError:
|
||||
msg = _("Unable to find '%s' in JSON Schema change") % 'path'
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
def _validate_change(self, change):
|
||||
change_path = change['path'][0]
|
||||
change_op = change['op']
|
||||
allowed_methods = self.allowed_operations.get(change_path)
|
||||
|
||||
if not allowed_methods:
|
||||
msg = _("Attribute '{0}' is invalid").format(change_path)
|
||||
raise webob.exc.HTTPForbidden(explanation=unicode(msg))
|
||||
|
||||
if change_op not in allowed_methods:
|
||||
msg = _("Method '{method}' is not allowed for a path with name "
|
||||
"'{name}'. Allowed operations are: '{ops}'").format(
|
||||
method=change_op,
|
||||
name=change_path,
|
||||
ops=', '.join(allowed_methods))
|
||||
|
||||
raise webob.exc.HTTPForbidden(explanation=unicode(msg))
|
||||
|
||||
property_to_update = {change_path: change['value']}
|
||||
|
||||
try:
|
||||
jsonschema.validate(property_to_update, schemas.PKG_UPDATE_SCHEMA)
|
||||
except jsonschema.ValidationError as e:
|
||||
LOG.exception(e)
|
||||
raise webob.exc.HTTPBadRequest(explanation=e.message)
|
||||
|
||||
def _decode_json_pointer(self, pointer):
|
||||
"""Parse a json pointer.
|
||||
|
||||
Json Pointers are defined in
|
||||
http://tools.ietf.org/html/draft-pbryan-zyp-json-pointer .
|
||||
The pointers use '/' for separation between object attributes, such
|
||||
that '/A/B' would evaluate to C in {"A": {"B": "C"}}. A '/'
|
||||
character
|
||||
in an attribute name is encoded as "~1" and a '~' character is
|
||||
encoded
|
||||
as "~0".
|
||||
"""
|
||||
self._validate_json_pointer(pointer)
|
||||
ret = []
|
||||
for part in pointer.lstrip('/').split('/'):
|
||||
ret.append(part.replace('~1', '/').replace('~0', '~').strip())
|
||||
return ret
|
||||
|
||||
|
||||
def _validate_json_pointer(self, pointer):
|
||||
"""Validate a json pointer.
|
||||
|
||||
Only limited form of json pointers is accepted.
|
||||
"""
|
||||
if not pointer.startswith('/'):
|
||||
msg = _('Pointer `%s` does not start with "/".') % pointer
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||
if re.search('/\s*?/', pointer[1:]):
|
||||
msg = _('Pointer `%s` contains adjacent "/".') % pointer
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||
if len(pointer) > 1 and pointer.endswith('/'):
|
||||
msg = _('Pointer `%s` end with "/".') % pointer
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||
if pointer[1:].strip() == '/':
|
||||
msg = _('Pointer `%s` does not contains valid token.') % pointer
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||
if re.search('~[^01]', pointer) or pointer.endswith('~'):
|
||||
msg = _('Pointer `%s` contains "~" not part of'
|
||||
' a recognized escape sequence.') % pointer
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
def _parse_json_schema_change(self, raw_change):
|
||||
op = self._get_change_operation(raw_change)
|
||||
path = self._get_change_path(raw_change)
|
||||
|
||||
path_list = self._decode_json_pointer(path)
|
||||
return op, path_list
|
||||
|
||||
def _validate_path(self, path):
|
||||
if len(path) > 1:
|
||||
msg = _('Nested paths are not allowed')
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
def default(self, datastring):
|
||||
return {'body': self._from_json_patch(datastring)}
|
||||
|
||||
|
||||
class XMLDeserializer(TextDeserializer):
|
||||
def __init__(self, metadata=None):
|
||||
"""
|
||||
:param metadata: information needed to deserialize xml into
|
||||
|
Loading…
Reference in New Issue
Block a user