A collection of middleware to help build services with JSON Schema.
Committee is tested on the following MRI versions:
- 2.0
- 2.1
- 2.2
- 2.3
- 2.4
use Committee::Middleware::RequestValidation, schema: JSON.parse(File.read(...))
This piece of middleware validates the parameters of incoming requests to make sure that they're formatted according to the constraints imposed by a particular schema.
Options:
allow_form_params
: Specifies that input can alternatively be specified asapplication/x-www-form-urlencoded
parameters when possible. This won't work for more complex schema validations.allow_query_params
: Specifies that query string parameters will be taken into consideration when doing validation (defaults totrue
).check_content_type
: Specifies thatContent-Type
should be verified according to JSON Hyper-schema definition. (defaults totrue
).coerce_date_times
: Convert the string with"format": "date-time"
parameter to DateTime object (default tofalse
).coerce_form_params
: Tries to convert POST data encoded into anapplication/x-www-form-urlencoded
body (where values are all strings) into concrete types required by the schema. This works fornull
(empty value),integer
(numeric value without decimals),number
(numeric value) andboolean
("true" is converted totrue
and "false" tofalse
). If coercion is not possible, the original value is passed unchanged to schema validation.coerce_query_params
: The same ascoerce_form_params
, but tries to coerceGET
parameters encoded in a request's query string.coerce_path_params
: The same ascoerce_form_params
, but tries to coerce parameters encoded in a request's URL path.coerce_recursive
: Coerce data in arrays and other nested objects (default totrue
).error_class
: Specifies the class to use for formatting and outputting validation errors (defaults toCommittee::ValidationError
)optimistic_json
: Will attempt to parse JSON in the request body even without aContent-Type: application/json
before falling back to other options (defaults tofalse
).prefix
: Mounts the middleware to respond at a configured prefix.raise
: Raise an exception on error instead of responding with a generic error body (defaults tofalse
).strict
: Puts the middleware into strict mode, meaning that paths which are not defined in the schema will be responded to with a 404 instead of being run (default tofalse
).
Some examples of use:
# missing required parameter
$ curl -X POST http://localhost:9292/account/app-transfers -H "Content-Type: application/json" -d '{"app":"heroku-api"}'
{"id":"invalid_params","message":"Require params: recipient."}
# missing required parameter (should have &query=...)
$ curl -X GET http://localhost:9292/search?category=all
{"id":"invalid_params","message":"Require params: query."}
# contains an unknown parameter
$ curl -X POST http://localhost:9292/account/app-transfers -H "Content-Type: application/json" -d '{"app":"heroku-api","recipient":"api@heroku.com","sender":"api@heroku.com"}'
{"id":"invalid_params","message":"Unknown params: sender."}
# invalid type
$ curl -X POST http://localhost:9292/account/app-transfers -H "Content-Type: application/json" -d '{"app":"heroku-api","recipient":7}'
{"id":"invalid_params","message":"Invalid type for key \"recipient\": expected 7 to be [\"string\"]."}
# invalid format (supports date-time, email, uuid)
$ curl -X POST http://localhost:9292/account/app-transfers -H "Content-Type: application/json" -d '{"app":"heroku-api","recipient":"api@heroku"}'
{"id":"invalid_params","message":"Invalid format for key \"recipient\": expected \"api@heroku\" to be \"email\"."
# invalid pattern
$ curl -X POST http://localhost:9292/apps -H "Content-Type: application/json" -d '{"name":"$#%"}'
{"id":"invalid_params","message":"Invalid pattern for key \"name\": expected $#% to match \"(?-mix:^[a-z][a-z0-9-]{3,30}$)\"."}
use Committee::Middleware::Stub, schema: JSON.parse(File.read(...))
This piece of middleware intercepts any routes that are in the JSON Schema, then builds and returns an appropriate response for them.
$ curl -X GET http://localhost:9292/apps
[
{
"archived_at":"2012-01-01T12:00:00Z",
"buildpack_provided_description":"Ruby/Rack",
"created_at":"2012-01-01T12:00:00Z",
"git_url":"git@heroku.com/example.git",
"id":"01234567-89ab-cdef-0123-456789abcdef",
"maintenance":false,
"name":"example",
"owner":[
{
"email":"username@example.com",
"id":"01234567-89ab-cdef-0123-456789abcdef"
}
],
"region":[
{
"id":"01234567-89ab-cdef-0123-456789abcdef",
"name":"us"
}
],
"released_at":"2012-01-01T12:00:00Z",
"repo_size":0,
"slug_size":0,
"stack":[
{
"id":"01234567-89ab-cdef-0123-456789abcdef",
"name":"cedar"
}
],
"updated_at":"2012-01-01T12:00:00Z",
"web_url":"http://example.herokuapp.com"
}
]
A bundled executable is also available to easily start up a server that will serve the stub for some particular JSON Schema file:
committee-stub -p <port> <path to JSON schema>
use Committee::Middleware::ResponseValidation, schema: JSON.parse(File.read(...))
This piece of middleware validates the contents of the response received from up the stack for any route that matches the JSON Schema. A hyper-schema link's targetSchema
property is used to determine what a valid response looks like.
Options:
error_class
: Specifies the class to use for formatting and outputting validation errors (defaults toCommittee::ValidationError
)prefix
: Mounts the middleware to respond at a configured prefix.raise
: Raise an exception on error instead of responding with a generic error body (defaults tofalse
).validate_errors
: Also validate non-2xx responses (defaults tofalse
).
Given a simple Sinatra app that responds for an endpoint in an incomplete fashion:
require "committee"
require "sinatra"
use Committee::Middleware::ResponseValidation, schema: JSON.parse(File.read("..."))
get "/apps" do
content_type :json
"[{}]"
end
The middleware will raise an error to indicate what the problems are:
# missing keys in response
$ curl -X GET http://localhost:9292/apps
{"id":"invalid_response","message":"Missing keys in response: archived_at, buildpack_provided_description, created_at, git_url, id, maintenance, name, owner:email, owner:id, region:id, region:name, released_at, repo_size, slug_size, stack:id, stack:name, updated_at, web_url."}
Committee will by default respond with a generic error JSON body for validation errors (when the raise
middleware option is false
).
Here's an example error to show the default format:
{
"id":"invalid_response",
"message":"Missing keys in response: archived_at, buildpack_provided_description, created_at, git_url, id, maintenance, name, owner:email, owner:id, region:id, region:name, released_at, repo_size, slug_size, stack:id, stack:name, updated_at, web_url."
}
You can customize this JSON body by setting the error_class
middleware option. The error_class
will be instantiated with: status
, id
, and message
.
status
: HTTP status codeid
: HTTP status name/stringmessage
: error message
Here's an example of a class to format errors according to JSON API:
module MyAPI
class ValidationError < Committee::ValidationError
def error_body
{
errors: [
{ status: id, detail: message }
]
}
end
def render
[
status,
{ "Content-Type" => "application/vnd.api+json" },
[JSON.generate(error_body)]
]
end
end
end
Committee ships with a small set of schema validation test assertions designed to be used along with rack-test
.
Here's a simple test to demonstrate:
describe Committee::Middleware::Stub do
include Committee::Test::Methods
include Rack::Test::Methods
def app
Sinatra.new do
get "/" do
content_type :json
JSON.generate({ "foo" => "bar" })
end
end
end
def schema_path
"./my-schema.json"
end
describe "GET /" do
it "conforms to schema" do
assert_schema_conform
end
end
end
Run tests with the following:
bundle install
bundle exec rake
Run a particular test suite or test:
bundle exec ruby -Ilib -Itest test/router_test.rb
bundle exec ruby -Ilib -Itest test/router_test.rb -n /prefix/
-
Update the version in
committee.gemspec
as appropriate for semantic versioning and add details toCHANGELOG
. -
Run the
release
task:bundle exec rake release