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 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