rack-spec 0.0.5 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|