diff --git a/refstack/api/controllers/v1.py b/refstack/api/controllers/v1.py index bb5672da..549d1f0c 100644 --- a/refstack/api/controllers/v1.py +++ b/refstack/api/controllers/v1.py @@ -20,47 +20,74 @@ import pecan from pecan import rest from refstack import db +from refstack.common import validators logger = logging.getLogger(__name__) -class ResultsController(rest.RestController): +class RestControllerWithValidation(rest.RestController): + + """ + Controller provides validation for POSTed data + exposed endpoints: + POST base_url/ + GET base_url/ + GET base_url/schema + """ + + def __init__(self, validator): + self.validator = validator + + def get_item(self, item_id): + """Handler for getting item""" + raise NotImplemented + + def store_item(self, item_in_json): + """Handler for storing item. Should return new item id""" + raise NotImplemented + + @pecan.expose('json') + def get_one(self, arg): + """Return test results in JSON format. + :param arg: item ID in uuid4 format or action + """ + if self.validator.assert_id(arg): + return self.get_item(item_id=arg) + + elif arg == 'schema': + return self.validator.schema + + else: + pecan.abort(404) + + @pecan.expose('json') + def post(self, ): + """POST handler.""" + item = validators.safe_load_json_body(self.validator) + item_id = self.store_item(item) + pecan.response.status = 201 + return item_id + + +class ResultsController(RestControllerWithValidation): """/v1/results handler.""" - @pecan.expose('json') - def get(self, ): - """GET handler.""" - return {'Result': 'Ok'} - - @pecan.expose("json") - def get_one(self, test_id): - """Return test results in JSON format. - - :param test_id: ID of the test to get the JSON for. - """ - test_info = db.get_test(test_id) + def get_item(self, item_id): + """Handler for getting item""" + test_info = db.get_test(item_id) if not test_info: pecan.abort(404) - - test_list = db.get_test_results(test_id) + test_list = db.get_test_results(item_id) test_name_list = [test_dict[0] for test_dict in test_list] return {"cpid": test_info.cpid, "created_at": test_info.created_at, "duration_seconds": test_info.duration_seconds, "results": test_name_list} - @pecan.expose(template='json') - def post(self, ): - """POST handler.""" - try: - results = pecan.request.json - except ValueError: - return pecan.abort(400, - detail='Request body \'%s\' could not ' - 'be decoded as JSON.' - '' % pecan.request.body) - test_id = db.store_results(results) + def store_item(self, item_in_json): + """Handler for storing item. Should return new item id""" + test_id = db.store_results(item_in_json) return {'test_id': test_id} @@ -68,4 +95,4 @@ class V1Controller(object): """Version 1 API controller root.""" - results = ResultsController() + results = ResultsController(validators.TestResultValidator()) diff --git a/refstack/common/__init__.py b/refstack/common/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/refstack/common/validators.py b/refstack/common/validators.py new file mode 100644 index 00000000..a34e0f1c --- /dev/null +++ b/refstack/common/validators.py @@ -0,0 +1,121 @@ +# +# All Rights Reserved. +# +# 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. + +""" Validators module +""" + +import uuid + +import json +import jsonschema +import pecan +from pecan import request + +ext_format_checker = jsonschema.FormatChecker() + + +def is_uuid(inst): + """ Check that inst is a uuid_hex string. """ + try: + uuid.UUID(hex=inst) + except (TypeError, ValueError): + return False + return True + + +@jsonschema.FormatChecker.checks(ext_format_checker, + format='uuid_hex', + raises=(TypeError, ValueError)) +def checker_uuid(inst): + """Checker 'uuid_hex' format for jsonschema validator""" + return is_uuid(inst) + + +class Validator(object): + + """Base class for validators""" + + def validate(self, json_data): + """ + :param json_data: data for validation + """ + jsonschema.validate(json_data, self.schema) + + +class TestResultValidator(Validator): + + """Validator for incoming test results.""" + + def __init__(self): + + self.schema = { + 'type': 'object', + 'properties': { + 'cpid': { + 'type': 'string' + }, + 'duration_seconds': {'type': 'integer'}, + 'results': { + "type": "array", + "items": [{ + 'type': 'object', + 'properties': { + 'name': {'type': 'string'}, + 'uid': { + 'type': 'string', + 'format': 'uuid_hex' + } + } + }] + + } + }, + 'required': ['cpid', 'duration_seconds', 'results'], + 'additionalProperties': False + } + jsonschema.Draft4Validator.check_schema(self.schema) + self.validator = jsonschema.Draft4Validator( + self.schema, + format_checker=ext_format_checker + ) + + @staticmethod + def assert_id(_id): + """ Check that _id is a valid uuid_hex string. """ + return is_uuid(_id) + + +def safe_load_json_body(validator): + """ + Helper for load validated request body + :param validator: instance of Validator class + :return validated body + :raise ValueError, jsonschema.ValidationError + """ + body = '' + try: + body = json.loads(request.body) + except (ValueError, TypeError) as e: + pecan.abort(400, detail=e.message) + + try: + validator.validate(body) + except jsonschema.ValidationError as e: + pecan.abort(400, + detail=e.message, + title='Malformed json data, ' + 'see %s/schema' % request.path_url) + + return body diff --git a/requirements.txt b/requirements.txt index 7b9bc3eb..87ab01d3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,3 +8,4 @@ pecan>=0.8.2 pyOpenSSL==0.13 pycrypto==2.6 requests==1.2.3 +jsonschema>=2.0.0,<3.0.0 \ No newline at end of file diff --git a/specs/proposed/refstack-org-test-result-json-schema.rst b/specs/approved/refstack-org-test-result-json-schema.rst similarity index 96% rename from specs/proposed/refstack-org-test-result-json-schema.rst rename to specs/approved/refstack-org-test-result-json-schema.rst index f2655a3b..57bcddd8 100644 --- a/specs/proposed/refstack-org-test-result-json-schema.rst +++ b/specs/approved/refstack-org-test-result-json-schema.rst @@ -76,12 +76,12 @@ https://github.com/stackforge/refstack/blob/master/specs/approved/api-v1.md **failed response:** http:400 - Malformed data. { - 'message': 'malformed json data, see /v1/schema/results.json' + 'message': 'Malformed json data, see /v1/results/schema' } -**url:** get /v1/schema/results.json +**url:** get /v1/results/schema -**valid response:** http:200 results.json file +**valid response:** http:200 schema.json file No invalid responses. No accepted parameters.