rack-spec 0.0.5 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +3 -0
- data/README.md +6 -130
- data/lib/rack/spec.rb +6 -36
- data/lib/rack/spec/request_validation.rb +153 -0
- data/lib/rack/spec/schema.rb +55 -0
- data/lib/rack/spec/version.rb +2 -2
- data/rack-spec.gemspec +5 -7
- data/spec/fixtures/schema.json +115 -0
- data/spec/rack/spec/request_validation_spec.rb +117 -0
- data/spec/spec_helper.rb +1 -4
- metadata +31 -60
- data/lib/rack/spec/exception_handler.rb +0 -15
- data/lib/rack/spec/exceptions/base.rb +0 -8
- data/lib/rack/spec/exceptions/validation_error.rb +0 -43
- data/lib/rack/spec/restful.rb +0 -95
- data/lib/rack/spec/spec.rb +0 -30
- data/lib/rack/spec/validation.rb +0 -21
- data/lib/rack/spec/validator_factory.rb +0 -19
- data/lib/rack/spec/validators/base.rb +0 -44
- data/lib/rack/spec/validators/maximum_length_validator.rb +0 -13
- data/lib/rack/spec/validators/maximum_validator.rb +0 -13
- data/lib/rack/spec/validators/minimum_length_validator.rb +0 -13
- data/lib/rack/spec/validators/minimum_validator.rb +0 -13
- data/lib/rack/spec/validators/null_validator.rb +0 -11
- data/lib/rack/spec/validators/only_validator.rb +0 -13
- data/lib/rack/spec/validators/parameters_validator.rb +0 -26
- data/lib/rack/spec/validators/required_validator.rb +0 -13
- data/lib/rack/spec/validators/type_validator.rb +0 -36
- data/spec/fixtures/spec.yml +0 -36
- data/spec/rack/spec_spec.rb +0 -221
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a554761d92c1fbeb80f0983023a2cc61970cb9f6
|
4
|
+
data.tar.gz: 578034987add0a2733821b66a3d7fa9743e9aaac
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c12e918bd0224e9968dc856e0bd3fcc89293b1133bd1d6ed35c6c49d02ee8bff9ed988078f3281fd34947b99b128f8453bf22350eb820fad334262613e807e47
|
7
|
+
data.tar.gz: c95c7728c4d01ffb2066b3193102e08a44ef9202c9da36b343027352f7e77d1f76219a39762e23c954b6b34ad4f4afa6f2e69a81636e65436eb095fbd16c6809
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -1,135 +1,11 @@
|
|
1
1
|
# Rack::Spec
|
2
|
-
|
2
|
+
Generate API server from [JSON Schema](http://json-schema.org/).
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
4
|
+
## RequestValidation
|
5
|
+
* Raise `Rack::Spec::RequestValidation::LinkNotFound` when given request is not defined in schema
|
6
|
+
* Raise `Rack::Spec::RequestValidation::InvalidContentType` for invalid content type
|
7
|
+
* Raise `Rack::Spec::RequestValidation::InvalidParameter` for invalid request parameter
|
8
8
|
|
9
9
|
```ruby
|
10
|
-
use Rack::Spec,
|
11
|
-
run ->(env) { [404, {}, ["Not Found"]] }
|
12
|
-
```
|
13
|
-
|
14
|
-
```yaml
|
15
|
-
# spec.yml
|
16
|
-
meta:
|
17
|
-
baseUri: http://api.example.com/
|
18
|
-
|
19
|
-
endpoints:
|
20
|
-
/recipes:
|
21
|
-
GET:
|
22
|
-
parameters:
|
23
|
-
page:
|
24
|
-
type: integer
|
25
|
-
minimum: 1
|
26
|
-
maximum: 10
|
27
|
-
private:
|
28
|
-
type: boolean
|
29
|
-
rank:
|
30
|
-
type: float
|
31
|
-
time:
|
32
|
-
type: iso8601
|
33
|
-
kind:
|
34
|
-
type: string
|
35
|
-
only:
|
36
|
-
- mono
|
37
|
-
- di
|
38
|
-
- tri
|
39
|
-
POST:
|
40
|
-
parameters:
|
41
|
-
title:
|
42
|
-
type: string
|
43
|
-
minimumLength: 3
|
44
|
-
maximumLength: 10
|
45
|
-
required: true
|
46
|
-
```
|
47
|
-
|
48
|
-
## Rack::Spec::Validation
|
49
|
-
Rack::Spec::Validation is a rack-middleware and works as a validation layer for your rack-application.
|
50
|
-
It loads spec definition (= a pure Hash object in specific format) to validate each request.
|
51
|
-
If the request is not valid on your definition, it will raise Rack::Spec::Exceptions::ValidationError.
|
52
|
-
Rack::Spec::ExceptionHandler is a utility rack-middleware to rescue validation error and return 400.
|
53
|
-
|
54
|
-
```ruby
|
55
|
-
use Rack::Spec::ExceptionHandler
|
56
|
-
use Rack::Spec::Validation, spec: YAML.load_file("spec.yml")
|
57
|
-
```
|
58
|
-
|
59
|
-
### Custom Validator
|
60
|
-
Custom validator can be defined by inheriting Rack::Spec::Validators::Base.
|
61
|
-
The following FwordValidator rejects any parameter starting with "F".
|
62
|
-
See [lib/rack/spec/validators](https://github.com/r7kamura/rack-spec/tree/master/lib/rack/spec/validators) for more examples.
|
63
|
-
|
64
|
-
```ruby
|
65
|
-
# Example:
|
66
|
-
#
|
67
|
-
# parameters:
|
68
|
-
# title:
|
69
|
-
# fword: false
|
70
|
-
#
|
71
|
-
class FwordValidator < Rack::Spec::Validators::Base
|
72
|
-
register_as "fword"
|
73
|
-
|
74
|
-
def valid?
|
75
|
-
value.nil? || !value.start_with?("F")
|
76
|
-
end
|
77
|
-
end
|
78
|
-
```
|
79
|
-
|
80
|
-
### Exception Handling
|
81
|
-
Replace Rack::Spec::ExceptionHandler to customize error behavior.
|
82
|
-
|
83
|
-
```ruby
|
84
|
-
use MyExceptionHandler # Rack::Spec::Exceptions::ValidationError must be rescued
|
85
|
-
use Rack::Spec::Validation, spec: YAML.load_file("spec.yml")
|
86
|
-
```
|
87
|
-
|
88
|
-
## Rack::Spec::Restful
|
89
|
-
Rack::Spec::Restful provides strongly-conventional RESTful API endpoints as a rack-middleware.
|
90
|
-
|
91
|
-
### Convention
|
92
|
-
It recognizes a preferred instruction from the request method & path, then tries to call it.
|
93
|
-
|
94
|
-
| verb | path | instruction |
|
95
|
-
| ---- | ---- | ---- |
|
96
|
-
| GET | /recipes | Recipe.get(params) |
|
97
|
-
| GET | /recipes/{id} | Recipe.get(params) |
|
98
|
-
| POST | /recipes | Recipe.post(params) |
|
99
|
-
| PUT | /recipes/{id} | Recipe.put(params) |
|
100
|
-
| DELETE | /recipes/{id} | Recipe.delete(params) |
|
101
|
-
|
102
|
-
### Example
|
103
|
-
You must implement correspondent class & methods for your API.
|
104
|
-
|
105
|
-
```ruby
|
106
|
-
class Recipe
|
107
|
-
def self.index(params)
|
108
|
-
order(params[:order]).page(params[:page])
|
109
|
-
end
|
110
|
-
|
111
|
-
def self.show(id, params)
|
112
|
-
find(id)
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
|
-
require "rack"
|
117
|
-
require "rack/spec"
|
118
|
-
require "yaml"
|
119
|
-
|
120
|
-
use Rack::Spec::Restful, spec: YAML.load_file("spec.yml")
|
121
|
-
run ->(env) do
|
122
|
-
[404, {}, ["Not Found"]]
|
123
|
-
end
|
124
|
-
```
|
125
|
-
|
126
|
-
## Development
|
127
|
-
```sh
|
128
|
-
# setup
|
129
|
-
git clone git@github.com:r7kamura/rack-spec.git
|
130
|
-
cd rack-spec
|
131
|
-
bundle install
|
132
|
-
|
133
|
-
# testing
|
134
|
-
bundle exec rspec
|
10
|
+
use Rack::Spec::RequestValidation, schema: JSON.parse("schema.json")
|
135
11
|
```
|
data/lib/rack/spec.rb
CHANGED
@@ -1,37 +1,7 @@
|
|
1
|
-
require "
|
2
|
-
require "
|
3
|
-
require "
|
4
|
-
require "rack/spec/exceptions/validation_error"
|
5
|
-
require "rack/spec/restful"
|
6
|
-
require "rack/spec/spec"
|
7
|
-
require "rack/spec/validation"
|
8
|
-
require "rack/spec/validators/base"
|
9
|
-
require "rack/spec/validators/null_validator"
|
10
|
-
require "rack/spec/validator_factory"
|
11
|
-
require "rack/spec/validators/maximum_length_validator"
|
12
|
-
require "rack/spec/validators/maximum_validator"
|
13
|
-
require "rack/spec/validators/minimum_length_validator"
|
14
|
-
require "rack/spec/validators/minimum_validator"
|
15
|
-
require "rack/spec/validators/null_validator"
|
16
|
-
require "rack/spec/validators/only_validator"
|
17
|
-
require "rack/spec/validators/parameters_validator"
|
18
|
-
require "rack/spec/validators/required_validator"
|
19
|
-
require "rack/spec/validators/type_validator"
|
20
|
-
require "rack/spec/version"
|
21
|
-
|
22
|
-
module Rack
|
23
|
-
class Spec
|
24
|
-
def initialize(app, options)
|
25
|
-
@app = Rack::Builder.app do
|
26
|
-
use Rack::Spec::ExceptionHandler
|
27
|
-
use Rack::Spec::Validation, options
|
28
|
-
use Rack::Spec::Restful, options
|
29
|
-
run app
|
30
|
-
end
|
31
|
-
end
|
1
|
+
require "json"
|
2
|
+
require "json_schema"
|
3
|
+
require "multi_json"
|
32
4
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
end
|
37
|
-
end
|
5
|
+
require "rack/spec/request_validation"
|
6
|
+
require "rack/spec/schema"
|
7
|
+
require "rack/spec/version"
|
@@ -0,0 +1,153 @@
|
|
1
|
+
module Rack
|
2
|
+
module Spec
|
3
|
+
class RequestValidation
|
4
|
+
# Behaves as a rack-middleware
|
5
|
+
# @param app [Object] Rack application
|
6
|
+
# @param schema [Hash] Schema object written in JSON schema format
|
7
|
+
# @raise [JsonSchema::SchemaError]
|
8
|
+
def initialize(app, schema: nil)
|
9
|
+
@app = app
|
10
|
+
@schema = Schema.new(schema)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Behaves as a rack-middleware
|
14
|
+
# @param env [Hash] Rack env
|
15
|
+
def call(env)
|
16
|
+
Validator.call(env: env, schema: @schema)
|
17
|
+
@app.call(env)
|
18
|
+
end
|
19
|
+
|
20
|
+
class Validator
|
21
|
+
# Utility wrapper method
|
22
|
+
def self.call(**args)
|
23
|
+
new(**args).call
|
24
|
+
end
|
25
|
+
|
26
|
+
# @param env [Hash] Rack env
|
27
|
+
# @param schema [JsonSchema::Schema] Schema object
|
28
|
+
def initialize(env: nil, schema: nil)
|
29
|
+
@env = env
|
30
|
+
@schema = schema
|
31
|
+
end
|
32
|
+
|
33
|
+
# Raises an error if any error detected
|
34
|
+
# @raise [Rack::Spec::RequestValidation::Error]
|
35
|
+
def call
|
36
|
+
case
|
37
|
+
when !has_link_for_current_action?
|
38
|
+
raise LinkNotFound
|
39
|
+
when has_body? && !has_valid_content_type?
|
40
|
+
raise InvalidContentType
|
41
|
+
when has_schema? && !has_valid_parameter?
|
42
|
+
raise InvalidParameter
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
# @return [true, false] True if request parameters are all valid
|
49
|
+
def has_valid_parameter?
|
50
|
+
valid, errors = link.schema.validate(parameters)
|
51
|
+
valid
|
52
|
+
rescue MultiJson::ParseError
|
53
|
+
false
|
54
|
+
end
|
55
|
+
|
56
|
+
# @return [true, false] True if any schema is defined for the current action
|
57
|
+
def has_schema?
|
58
|
+
!!link.schema
|
59
|
+
end
|
60
|
+
|
61
|
+
# @return [true, false] True if request body is not empty
|
62
|
+
def has_body?
|
63
|
+
!body.empty?
|
64
|
+
end
|
65
|
+
|
66
|
+
# @return [true, false] True if no or matched content type given
|
67
|
+
def has_valid_content_type?
|
68
|
+
content_type.nil? || Rack::Mime.match?(link.enc_type, content_type)
|
69
|
+
end
|
70
|
+
|
71
|
+
# @return [true, false] True if link is defined for the current action
|
72
|
+
def has_link_for_current_action?
|
73
|
+
!!link
|
74
|
+
end
|
75
|
+
|
76
|
+
# @return [JsonSchema::Schema::Link, nil] Link for the current action
|
77
|
+
def link
|
78
|
+
if instance_variable_defined?(:@link)
|
79
|
+
@link
|
80
|
+
else
|
81
|
+
@link = @schema.link_for(method: method, path: path)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Treats env as a utility object to easily extract method and path
|
86
|
+
# @return [Rack::Request]
|
87
|
+
def request
|
88
|
+
@request ||= Rack::Request.new(@env)
|
89
|
+
end
|
90
|
+
|
91
|
+
# @return [String] HTTP request method
|
92
|
+
# @example
|
93
|
+
# method #=> "GET"
|
94
|
+
def method
|
95
|
+
request.request_method
|
96
|
+
end
|
97
|
+
|
98
|
+
# @return [String] Request path
|
99
|
+
# @example
|
100
|
+
# path #=> "/recipes"
|
101
|
+
def path
|
102
|
+
request.path_info
|
103
|
+
end
|
104
|
+
|
105
|
+
# @return [String] Request content type
|
106
|
+
# @example
|
107
|
+
# path #=> "application/json"
|
108
|
+
def content_type
|
109
|
+
request.content_type
|
110
|
+
end
|
111
|
+
|
112
|
+
# @return [String] request body
|
113
|
+
def body
|
114
|
+
if instance_variable_defined?(:@body)
|
115
|
+
@body
|
116
|
+
else
|
117
|
+
@body = request.body.read
|
118
|
+
request.body.rewind
|
119
|
+
@body
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# @return [Hash] Request parameters decoded from JSON
|
124
|
+
# @raise [MultiJson::ParseError]
|
125
|
+
def parameters
|
126
|
+
@parameters ||= begin
|
127
|
+
if has_body?
|
128
|
+
MultiJson.decode(body)
|
129
|
+
else
|
130
|
+
{}
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Base error class for Rack::Spec::RequestValidation
|
137
|
+
class Error < StandardError
|
138
|
+
end
|
139
|
+
|
140
|
+
# Error class for case when no link defined for given request
|
141
|
+
class LinkNotFound < Error
|
142
|
+
end
|
143
|
+
|
144
|
+
# Error class for invalid request content type
|
145
|
+
class InvalidContentType < Error
|
146
|
+
end
|
147
|
+
|
148
|
+
# Error class for invalid request parameter
|
149
|
+
class InvalidParameter < Error
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Rack
|
2
|
+
module Spec
|
3
|
+
|
4
|
+
# Utility wrapper class for JsonSchema::Schema
|
5
|
+
class Schema
|
6
|
+
# Recursively extracts all links in given JSON schema
|
7
|
+
# @param json_schema [JsonSchema::Schema]
|
8
|
+
# @return [Array] An array of JsonSchema::Schema::Link
|
9
|
+
def self.extract_links(json_schema)
|
10
|
+
links = json_schema.links.select {|link| link.method && link.href }
|
11
|
+
links + json_schema.properties.map {|key, schema| extract_links(schema) }.flatten
|
12
|
+
end
|
13
|
+
|
14
|
+
# @param schema [Hash]
|
15
|
+
# @raise [JsonSchema::SchemaError]
|
16
|
+
# @example
|
17
|
+
# hash = JSON.parse("schema.json")
|
18
|
+
# schema = Rack::Spec::Schema.new(hash)
|
19
|
+
def initialize(schema)
|
20
|
+
@json_schema = JsonSchema.parse!(schema).tap(&:expand_references!)
|
21
|
+
end
|
22
|
+
|
23
|
+
# @param method [String] Uppercase HTTP method name (e.g. GET, POST)
|
24
|
+
# @param path [String] Path string, which may include URI template
|
25
|
+
# @return [JsonSchema::Scheam::Link, nil] Link defined for the given method and path
|
26
|
+
# @example
|
27
|
+
# schema.has_link_for?(method: "GET", path: "/recipes/{+id}") #=> nil
|
28
|
+
def link_for(method: nil, path: nil)
|
29
|
+
links_indexed_by_method[method].find do |link|
|
30
|
+
%r<^#{link.href.gsub(/\{(.*?)\}/, "[^/]+")}$> === path
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# @return [Array] All links defined in given JSON schema
|
37
|
+
# @example
|
38
|
+
# schema.links #=> [#<JsonSchema::Schema::Link>]
|
39
|
+
def links
|
40
|
+
@links ||= self.class.extract_links(@json_schema)
|
41
|
+
end
|
42
|
+
|
43
|
+
# @return [Hash] A key-value pair of HTTP method and an Array of links
|
44
|
+
# @note This Hash always returns an Array for any key
|
45
|
+
# @example
|
46
|
+
# schema.links_indexed_by_method #=> { "GET" => [#<JsonSchema::Schema::Link>] }
|
47
|
+
def links_indexed_by_method
|
48
|
+
@links_indexed_by_method ||= links.inject(Hash.new {|hash, key| hash[key] = [] }) do |result, link|
|
49
|
+
result[link.method.to_s.upcase] << link
|
50
|
+
result
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/lib/rack/spec/version.rb
CHANGED
data/rack-spec.gemspec
CHANGED
@@ -1,7 +1,6 @@
|
|
1
|
-
|
2
|
-
lib = File.expand_path('../lib', __FILE__)
|
1
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
2
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
-
require
|
3
|
+
require "rack/spec/version"
|
5
4
|
|
6
5
|
Gem::Specification.new do |spec|
|
7
6
|
spec.name = "rack-spec"
|
@@ -18,13 +17,12 @@ Gem::Specification.new do |spec|
|
|
18
17
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
18
|
spec.require_paths = ["lib"]
|
20
19
|
|
21
|
-
spec.add_dependency "
|
22
|
-
spec.add_dependency "
|
23
|
-
spec.add_dependency "rack"
|
20
|
+
spec.add_dependency "json_schema"
|
21
|
+
spec.add_dependency "multi_json"
|
24
22
|
spec.add_development_dependency "bundler", "~> 1.5"
|
25
23
|
spec.add_development_dependency "pry"
|
26
24
|
spec.add_development_dependency "rack-test"
|
27
25
|
spec.add_development_dependency "rake"
|
28
26
|
spec.add_development_dependency "rspec", "2.14.1"
|
29
|
-
spec.add_development_dependency "rspec-
|
27
|
+
spec.add_development_dependency "rspec-console"
|
30
28
|
end
|