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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: bf3a96b091fcf262c279bfd214379e55ed856c7a
4
- data.tar.gz: 313b5fd8342e4ddd245fced70fad0d348db2092f
3
+ metadata.gz: a554761d92c1fbeb80f0983023a2cc61970cb9f6
4
+ data.tar.gz: 578034987add0a2733821b66a3d7fa9743e9aaac
5
5
  SHA512:
6
- metadata.gz: 2ef09a48686f6e9818fbd4e2c8aef235b8d45981a26f4703e54efe051deb7f057a57dee84ab6d93cb3fb41e327691dd717db083b41835ef50d3e5796170d6368
7
- data.tar.gz: f625b179cae1770d3877b050492d3e93fa9b05e226a4f84ad359a5acaad62b176277cf0eb8b1afd862bd8c77b69bed2d866a2247b441e4eda909cd779a922f60
6
+ metadata.gz: c12e918bd0224e9968dc856e0bd3fcc89293b1133bd1d6ed35c6c49d02ee8bff9ed988078f3281fd34947b99b128f8453bf22350eb820fad334262613e807e47
7
+ data.tar.gz: c95c7728c4d01ffb2066b3193102e08a44ef9202c9da36b343027352f7e77d1f76219a39762e23c954b6b34ad4f4afa6f2e69a81636e65436eb095fbd16c6809
data/CHANGELOG.md CHANGED
@@ -1,3 +1,6 @@
1
+ ## v0.1.0
2
+ * Rebuilt entire code based on JSON schema
3
+
1
4
  ## v0.0.5
2
5
  * Change RESTful resource API (#get, #post, #put, and #delete)
3
6
 
data/README.md CHANGED
@@ -1,135 +1,11 @@
1
1
  # Rack::Spec
2
- Spec based web-application middleware for Rack.
2
+ Generate API server from [JSON Schema](http://json-schema.org/).
3
3
 
4
- * Rack::Spec - all-in-one middleware
5
- * Rack::Spec::Validation - validates requests along given specifications
6
- * Rack::Spec::ExceptionHandler - rescues exceptions raised from Rack::Spec::Validation
7
- * Rack::Spec::Restful - provides strongly-conventional RESTful API endpoints
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, spec: YAML.load_file("spec.yml")
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 "rack/builder"
2
- require "rack/spec/exception_handler"
3
- require "rack/spec/exceptions/base"
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
- def call(env)
34
- @app.call(env)
35
- end
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
@@ -1,5 +1,5 @@
1
1
  module Rack
2
- class Spec
3
- VERSION = "0.0.5"
2
+ module Spec
3
+ VERSION = "0.1.0"
4
4
  end
5
5
  end
data/rack-spec.gemspec CHANGED
@@ -1,7 +1,6 @@
1
- # coding: utf-8
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 'rack/spec/version'
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 "activesupport"
22
- spec.add_dependency "addressable"
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-json_matcher", "0.1.3"
27
+ spec.add_development_dependency "rspec-console"
30
28
  end