diff --git a/README.md b/README.md
index 68ec8c3..a7566b6 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@ Tornado-JSON is a small extension of [Tornado](http://www.tornadoweb.org/en/stab
Some of the key features the included modules provide:
-* Input and output [JSON Schema](http://json-schema.org/) validation by decorating RequestHandlers with `io_schema`
+* Input and output [JSON Schema](http://json-schema.org/) validation by decorating RequestHandlers with `schema.validate`
* Automated *route generation* with `routes.get_routes(package)`
* *Automated Public API documentation* using schemas and provided descriptions
* Standardized output using the [JSend](http://labs.omniti.com/labs/jsend) specification
diff --git a/README.rst b/README.rst
index 3a06c94..80c85f7 100644
--- a/README.rst
+++ b/README.rst
@@ -22,7 +22,7 @@ in the documentation.
Some of the key features the included modules provide:
- Input and output `JSON Schema `__ validation
- by decorating RequestHandlers with ``io_schema``
+ by decorating RequestHandlers with ``schema.validate``
- Automated *route generation* with ``routes.get_routes(package)``
- *Automated Public API documentation* using schemas and provided
descriptions
diff --git a/demos/helloworld/helloworld/api.py b/demos/helloworld/helloworld/api.py
index cd0d639..25488b5 100755
--- a/demos/helloworld/helloworld/api.py
+++ b/demos/helloworld/helloworld/api.py
@@ -1,7 +1,7 @@
from tornado import gen
from tornado_json.requesthandlers import APIHandler
-from tornado_json.utils import io_schema
+from tornado_json import schema
class HelloWorldHandler(APIHandler):
@@ -17,12 +17,12 @@ class HelloWorldHandler(APIHandler):
"doc": "Shouts hello to the world!",
}
- # Decorate any HTTP methods with the `io_schema` decorator
+ # Decorate any HTTP methods with the `schema.validate` decorator
# to validate input to it and output from it as per the
# the schema for the method defined in `apid`
# Simply use `return` rather than `self.write` to write back
# your output.
- @io_schema
+ @schema.validate
def get(self):
return "Hello world!"
@@ -43,13 +43,13 @@ class AsyncHelloWorld(APIHandler):
def hello(self, callback=None):
callback("Hello (asynchronous) world!")
- @io_schema
+ @schema.validate
@gen.coroutine
def get(self):
# Asynchronously yield a result from a method
res = yield gen.Task(self.hello)
- # When using the io_schema decorator asynchronously,
+ # When using the `schema.validate` decorator asynchronously,
# we can return the output desired by raising
# `tornado.gen.Return(value)` which returns a
# Future that the decorator will yield.
@@ -97,9 +97,9 @@ class PostIt(APIHandler):
"""
}
- @io_schema
+ @schema.validate
def post(self):
- # io_schema will JSON-decode `self.request.body` for us
+ # `schema.validate` will JSON-decode `self.request.body` for us
# and set self.body as the result, so we can use that here
return {
"message": "{} was posted.".format(self.body["title"])
@@ -124,7 +124,7 @@ class Greeting(APIHandler):
# arguments; here, you can GET /api/greeting/John/Smith and you will
# get a response back that says, "Greetings, John Smith!"
# You can match the regex equivalent of `\w+`.
- @io_schema
+ @schema.validate
def get(self, fname, lname):
return "Greetings, {} {}!".format(fname, lname)
@@ -135,6 +135,9 @@ class FreeWilledHandler(APIHandler):
# if you want your handlers to do something more custom,
# they definitely can.
def get(self):
+ # If you don't know where `self.success` comes from, it is defined
+ # in the `JSendMixin` mixin in tornado_json.jsend. `APIHandler`
+ # inherits from this and thus gets the methods.
self.success("I don't need no stinkin' schema validation.")
# If you're feeling really bold, you could even skip JSend
# altogether and do the following EVIL thing:
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 4b868a5..19f0338 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -4,6 +4,16 @@ Changelog
_
---------
+
+v0.20 - Refactor of ``utils`` module
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Functions that did not belong in ``utils`` were moved to more relevant modules. This change changes the interface for Tornado-JSON in quite a big way. The following changes were made (that are not backwards compatible).
+
+* ``api_assert`` and ``APIError`` were moved to ``tornado_json.exceptions``
+* ``io_schema`` was renamed ``validate`` and moved to ``tornado_json.schema``
+
+
v0.14 - Bugfixes thanks to 100% coverage
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/docs/index.rst b/docs/index.rst
index ec7bf18..5075198 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -16,7 +16,7 @@ in the documentation.
Some of the key features the included modules provide:
- Input and output `JSON Schema `__ validation
- by decorating RequestHandlers with ``io_schema``
+ by decorating RequestHandlers with ``schema.validate``
- Automated *route generation* with ``routes.get_routes(package)``
- *Automated Public API documentation* using schemas and provided
descriptions
diff --git a/docs/requesthandler_guidelines.rst b/docs/requesthandler_guidelines.rst
index abfb7de..0bef351 100644
--- a/docs/requesthandler_guidelines.rst
+++ b/docs/requesthandler_guidelines.rst
@@ -29,7 +29,7 @@ for an example. Here is an example for POST:
``doc`` is the **public** accompanying documentation that will be
available on the wiki.
-Use the ``io_schema`` decorator on methods which will automatically
+Use the ``schema.validate`` decorator on methods which will automatically
validate the request body and output against the schemas in
``apid[method_name]``. Additionally, ``return`` the data from the
request handler, rather than writing it back (the decorator will take
@@ -38,7 +38,7 @@ care of that).
.. code:: python
class ExampleHandler(APIHandler):
- @io_schema
+ @schema.validate
def post(self):
...
return data
@@ -48,15 +48,15 @@ Assertions
----------
-Use ``utils.api_assert`` to fail when some the client does not meet some
+Use ``exceptions.api_assert`` to fail when some the client does not meet some
API pre-condition/requirement, e.g., an invalid or incomplete request is
made. When using an assertion is not suitable,
-``raise APIError( ... )``; don't use JSend ``fail`` directly.
+``raise APIError( ... )``; don't use ``self.fail`` directly.
.. code:: python
class ExampleHandler(APIHandler):
- @io_schema
+ @schema.validate
def post(self):
...
api_assert(condition, status_code, log_message=log_message)
diff --git a/docs/using_tornado_json.rst b/docs/using_tornado_json.rst
index 13b94df..8262936 100644
--- a/docs/using_tornado_json.rst
+++ b/docs/using_tornado_json.rst
@@ -58,13 +58,13 @@ helloworld/api.py
Now comes the fun part where we develop the actual web app. We'll import
``APIHandler`` (this is the handler you should subclass for API routes),
-and the ``io_schema`` decorator which will validate input and output
+and the ``schema.validate`` decorator which will validate input and output
schema for us.
.. code:: python
from tornado_json.requesthandlers import APIHandler
- from tornado_json.utils import io_schema
+ from tornado_json import schema
class HelloWorldHandler(APIHandler):
"""Hello!"""
@@ -95,12 +95,12 @@ back. Notice that rather than using ``self.write`` as we usually would,
we simply return the data we want to write back, which will then be
validated against the output schema and be written back according to the
`JSend `__ specification. The
-``io_schema`` decorator handles all of this so be sure to decorate any
+``schema.validate`` decorator handles all of this so be sure to decorate any
HTTP methods with it.
.. code:: python
- @io_schema
+ @schema.validate
def get(self):
return "Hello world!"
diff --git a/tests/func_test.py b/tests/func_test.py
index 2f59769..21f7d9b 100644
--- a/tests/func_test.py
+++ b/tests/func_test.py
@@ -5,7 +5,7 @@
try:
sys.path.append('.')
from tornado_json import routes
- from tornado_json import utils
+ from tornado_json import schema
from tornado_json import application
from tornado_json import requesthandlers
sys.path.append('demos/helloworld')
@@ -68,11 +68,11 @@ class ExplodingHandler(requesthandlers.APIHandler):
},
}
- @utils.io_schema
+ @schema.validate
def get(self):
return "I am not the handler you are looking for."
- @utils.io_schema
+ @schema.validate
def post(self):
return "Fission mailed."
diff --git a/tests/test_tornado_json.py b/tests/test_tornado_json.py
index d953ad9..955cb93 100644
--- a/tests/test_tornado_json.py
+++ b/tests/test_tornado_json.py
@@ -6,7 +6,8 @@
try:
sys.path.append('.')
from tornado_json import routes
- from tornado_json import utils
+ from tornado_json import schema
+ from tornado_json import exceptions
from tornado_json import jsend
sys.path.append('demos/helloworld')
import helloworld
@@ -28,7 +29,7 @@ class Request(object):
request = Request()
def fail(message):
- raise utils.APIError(message)
+ raise exceptions.APIError(message)
def success(self, message):
raise SuccessException
@@ -78,15 +79,15 @@ class TestUtils(TestTornadoJSONBase):
"""Tests the utils module"""
def test_api_assert(self):
- """Test utils.api_assert"""
- with pytest.raises(utils.APIError):
- utils.api_assert(False, 400)
+ """Test exceptions.api_assert"""
+ with pytest.raises(exceptions.APIError):
+ exceptions.api_assert(False, 400)
- utils.api_assert(True, 400)
+ exceptions.api_assert(True, 400)
class TerribleHandler(MockRequestHandler):
- """This 'handler' is used in test_io_schema"""
+ """This 'handler' is used in test_validate"""
apid = {
"get": {
@@ -105,17 +106,17 @@ class TerribleHandler(MockRequestHandler):
},
}
- @utils.io_schema
+ @schema.validate
def get(self):
return "I am not the handler you are looking for."
- @utils.io_schema
+ @schema.validate
def post(self):
return "Fission mailed."
class ReasonableHandler(MockRequestHandler):
- """This 'handler' is used in test_io_schema"""
+ """This 'handler' is used in test_validate"""
apid = {
"get": {
@@ -138,23 +139,23 @@ class ReasonableHandler(MockRequestHandler):
},
}
- @utils.io_schema
+ @schema.validate
def get(self, fname, lname):
return "I am the handler you are looking for, {} {}".format(
fname, lname)
- @utils.io_schema
+ @schema.validate
def post(self):
# Test that self.body is available as expected
assert self.body == {"I am a": "JSON object"}
return "Mail received."
- # DONE: Test io_schema functionally instead; pytest.raises does
+ # DONE: Test validate functionally instead; pytest.raises does
# not seem to be catching errors being thrown after change
# to async compatible code.
# The following test left here as antiquity.
- # def test_io_schema(self):
- # """Tests the utils.io_schema decorator"""
+ # def test_validate(self):
+ # """Tests the schema.validate decorator"""
# th = self.TerribleHandler()
# rh = self.ReasonableHandler()
diff --git a/tornado_json/__init__.py b/tornado_json/__init__.py
index cdaba45..c9152ac 100644
--- a/tornado_json/__init__.py
+++ b/tornado_json/__init__.py
@@ -5,4 +5,4 @@
# Alternatively, just put the version in a text file or something to avoid
# this.
-__version__ = '0.14'
+__version__ = '0.20'
diff --git a/tornado_json/exceptions.py b/tornado_json/exceptions.py
new file mode 100644
index 0000000..1538c6f
--- /dev/null
+++ b/tornado_json/exceptions.py
@@ -0,0 +1,18 @@
+from tornado.web import HTTPError
+
+
+class APIError(HTTPError):
+
+ """Equivalent to ``RequestHandler.HTTPError`` except for in name"""
+
+
+def api_assert(condition, *args, **kwargs):
+ """Assertion to fail with if not ``condition``
+
+ Asserts that ``condition`` is ``True``, else raises an ``APIError``
+ with the provided ``args`` and ``kwargs``
+
+ :type condition: bool
+ """
+ if not condition:
+ raise APIError(*args, **kwargs)
diff --git a/tornado_json/requesthandlers.py b/tornado_json/requesthandlers.py
index fbaabf3..a0dc91f 100644
--- a/tornado_json/requesthandlers.py
+++ b/tornado_json/requesthandlers.py
@@ -4,7 +4,7 @@
from jsonschema import ValidationError
from tornado_json.jsend import JSendMixin
-from tornado_json.utils import APIError
+from tornado_json.exceptions import APIError
class BaseHandler(RequestHandler):
diff --git a/tornado_json/routes.py b/tornado_json/routes.py
index 5c4e5ec..9624fc5 100644
--- a/tornado_json/routes.py
+++ b/tornado_json/routes.py
@@ -72,7 +72,7 @@ def extract_method(wrapped_method):
:rtype: any([types.FunctionType, types.MethodType])
"""
- # If method was decorated with io_schema, the original method
+ # If method was decorated with validate, the original method
# is available as orig_func thanks to our container decorator
return wrapped_method.orig_func if \
hasattr(wrapped_method, "orig_func") else wrapped_method
diff --git a/tornado_json/schema.py b/tornado_json/schema.py
new file mode 100644
index 0000000..3ecc7c4
--- /dev/null
+++ b/tornado_json/schema.py
@@ -0,0 +1,90 @@
+import json
+import jsonschema
+
+from tornado import gen
+from tornado.concurrent import Future
+
+from tornado_json.utils import container
+
+
+@container
+def validate(rh_method):
+ """Decorator for RequestHandler schema validation
+
+ This decorator:
+
+ - Validates request body against input schema of the method
+ - Calls the ``rh_method`` and gets output from it
+ - Validates output against output schema of the method
+ - Calls ``JSendMixin.success`` to write the validated output
+
+ :type rh_method: function
+ :param rh_method: The RequestHandler method to be decorated
+ :returns: The decorated method
+ :raises ValidationError: If input is invalid as per the schema or malformed
+ :raises TypeError: If the output is invalid as per the schema or malformed
+ """
+
+ @gen.coroutine
+ def _wrapper(self, *args, **kwargs):
+ # Get name of method
+ method_name = rh_method.__name__
+
+ # Special case for GET, DELETE requests (since there is no data to
+ # validate)
+ if method_name not in ["get", "delete"]:
+ # If input is not valid JSON, fail
+ try:
+ # TODO: Assuming UTF-8 encoding for all requests,
+ # find a nice way of determining this from charset
+ # in headers if provided
+ encoding = "UTF-8"
+ input_ = json.loads(self.request.body.decode(encoding))
+ except ValueError as e:
+ raise jsonschema.ValidationError(
+ "Input is malformed; could not decode JSON object."
+ )
+
+ # Validate the received input
+ jsonschema.validate(
+ input_,
+ type(self).apid[method_name]["input_schema"]
+ )
+ else:
+ input_ = None
+
+ # A json.loads'd version of self.request["body"] is now available
+ # as self.body
+ setattr(self, "body", input_)
+ # Call the requesthandler method
+ output = rh_method(self, *args, **kwargs)
+ # If the rh_method returned a Future a la `raise Return(value)`
+ # we grab the output.
+ if isinstance(output, Future):
+ output = yield output
+
+ # We wrap output in an object before validating in case
+ # output is a string (and ergo not a validatable JSON object)
+ try:
+ jsonschema.validate(
+ {"result": output},
+ {
+ "type": "object",
+ "properties": {
+ "result": type(self)
+ .apid[method_name]["output_schema"]
+ },
+ "required": ["result"]
+ }
+ )
+ except jsonschema.ValidationError as e:
+ # We essentially re-raise this as a TypeError because
+ # we don't want this error data passed back to the client
+ # because it's a fault on our end. The client should
+ # only see a 500 - Internal Server Error.
+ raise TypeError(str(e))
+
+ # If no ValidationError has been raised up until here, we write
+ # back output
+ self.success(output)
+ return _wrapper
diff --git a/tornado_json/utils.py b/tornado_json/utils.py
index ec37104..ce1cea8 100644
--- a/tornado_json/utils.py
+++ b/tornado_json/utils.py
@@ -1,30 +1,4 @@
-# TODO: Better organization for contents of this module
-
-import json
-import logging
from functools import wraps
-from jsonschema import validate, ValidationError
-
-from tornado import gen
-from tornado.web import HTTPError
-from tornado.concurrent import Future
-
-
-class APIError(HTTPError):
-
- """Equivalent to ``RequestHandler.HTTPError`` except for in name"""
-
-
-def api_assert(condition, *args, **kwargs):
- """Assertion to fail with if not ``condition``
-
- Asserts that ``condition`` is ``True``, else raises an ``APIError``
- with the provided ``args`` and ``kwargs``
-
- :type condition: bool
- """
- if not condition:
- raise APIError(*args, **kwargs)
def container(dec):
@@ -43,86 +17,3 @@ def meta_decorator(f):
decorator.orig_func = f
return decorator
return meta_decorator
-
-
-@container
-def io_schema(rh_method):
- """Decorator for RequestHandler schema validation
-
- This decorator:
-
- - Validates request body against input schema of the method
- - Calls the ``rh_method`` and gets output from it
- - Validates output against output schema of the method
- - Calls ``JSendMixin.success`` to write the validated output
-
- :type rh_method: function
- :param rh_method: The RequestHandler method to be decorated
- :returns: The decorated method
- :raises ValidationError: If input is invalid as per the schema or malformed
- :raises TypeError: If the output is invalid as per the schema or malformed
- """
-
- @gen.coroutine
- def _wrapper(self, *args, **kwargs):
- # Get name of method
- method_name = rh_method.__name__
-
- # Special case for GET, DELETE requests (since there is no data to
- # validate)
- if method_name not in ["get", "delete"]:
- # If input is not valid JSON, fail
- try:
- # TODO: Assuming UTF-8 encoding for all requests,
- # find a nice way of determining this from charset
- # in headers if provided
- encoding = "UTF-8"
- input_ = json.loads(self.request.body.decode(encoding))
- except ValueError as e:
- raise ValidationError(
- "Input is malformed; could not decode JSON object."
- )
-
- # Validate the received input
- validate(
- input_,
- type(self).apid[method_name]["input_schema"]
- )
- else:
- input_ = None
-
- # A json.loads'd version of self.request["body"] is now available
- # as self.body
- setattr(self, "body", input_)
- # Call the requesthandler method
- output = rh_method(self, *args, **kwargs)
- # If the rh_method returned a Future a la `raise Return(value)`
- # we grab the output.
- if isinstance(output, Future):
- output = yield output
-
- # We wrap output in an object before validating in case
- # output is a string (and ergo not a validatable JSON object)
- try:
- validate(
- {"result": output},
- {
- "type": "object",
- "properties": {
- "result": type(self)
- .apid[method_name]["output_schema"]
- },
- "required": ["result"]
- }
- )
- except ValidationError as e:
- # We essentially re-raise this as a TypeError because
- # we don't want this error data passed back to the client
- # because it's a fault on our end. The client should
- # only see a 500 - Internal Server Error.
- raise TypeError(str(e))
-
- # If no ValidationError has been raised up until here, we write
- # back output
- self.success(output)
- return _wrapper