rack-json_schema 1.0.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 +7 -0
- data/.gitignore +17 -0
- data/CHANGELOG.md +52 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +200 -0
- data/Rakefile +1 -0
- data/bin/specup +30 -0
- data/lib/rack-json_schema.rb +1 -0
- data/lib/rack/json_schema.rb +15 -0
- data/lib/rack/json_schema/base_request_handler.rb +64 -0
- data/lib/rack/json_schema/docs.rb +38 -0
- data/lib/rack/json_schema/error.rb +30 -0
- data/lib/rack/json_schema/error_handler.rb +19 -0
- data/lib/rack/json_schema/mock.rb +93 -0
- data/lib/rack/json_schema/request_validation.rb +184 -0
- data/lib/rack/json_schema/response_validation.rb +120 -0
- data/lib/rack/json_schema/schema.rb +55 -0
- data/lib/rack/json_schema/version.rb +5 -0
- data/rack-json_schema.gemspec +31 -0
- data/spec/fixtures/schema.json +152 -0
- data/spec/rack/spec/docs_spec.rb +93 -0
- data/spec/rack/spec/mock_spec.rb +126 -0
- data/spec/rack/spec_spec.rb +221 -0
- data/spec/spec_helper.rb +11 -0
- metadata +229 -0
@@ -0,0 +1,30 @@
|
|
1
|
+
module Rack
|
2
|
+
module JsonSchema
|
3
|
+
class Error < StandardError
|
4
|
+
# @return [Array] Rack response
|
5
|
+
def to_rack_response
|
6
|
+
[status, headers, [body]]
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
# @note Override this
|
12
|
+
def status
|
13
|
+
500
|
14
|
+
end
|
15
|
+
|
16
|
+
# @note Override this
|
17
|
+
def id
|
18
|
+
"internal_server_error"
|
19
|
+
end
|
20
|
+
|
21
|
+
def headers
|
22
|
+
{ "Content-Type" => "application/json" }
|
23
|
+
end
|
24
|
+
|
25
|
+
def body
|
26
|
+
MultiJson.encode({ id: id, message: message }, pretty: true) + "\n"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Rack
|
2
|
+
module JsonSchema
|
3
|
+
class ErrorHandler
|
4
|
+
# Behaves as a rack middleware
|
5
|
+
# @param app [Object] Rack application
|
6
|
+
def initialize(app)
|
7
|
+
@app = app
|
8
|
+
end
|
9
|
+
|
10
|
+
# Behaves as a rack middleware
|
11
|
+
# @param env [Hash] Rack env
|
12
|
+
def call(env)
|
13
|
+
@app.call(env)
|
14
|
+
rescue Rack::JsonSchema::Error => exception
|
15
|
+
exception.to_rack_response
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module Rack
|
2
|
+
module JsonSchema
|
3
|
+
class Mock
|
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
|
+
# @param env [Hash] Rack env
|
14
|
+
def call(env)
|
15
|
+
RequestHandler.call(app: @app, env: env, schema: @schema)
|
16
|
+
end
|
17
|
+
|
18
|
+
class RequestHandler < BaseRequestHandler
|
19
|
+
# @param app [Object] Rack application
|
20
|
+
def initialize(app: nil, **args)
|
21
|
+
@app = app
|
22
|
+
super(**args)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Returns dummy response if JSON schema is defined for the current link
|
26
|
+
# @return [Array] Rack response
|
27
|
+
def call
|
28
|
+
if has_link_for_current_action?
|
29
|
+
dummy_response
|
30
|
+
else
|
31
|
+
@app.call(@env)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def dummy_response
|
38
|
+
[dummy_status, dummy_headers, [dummy_body]]
|
39
|
+
end
|
40
|
+
|
41
|
+
def dummy_status
|
42
|
+
method == "POST" ? 201 : 200
|
43
|
+
end
|
44
|
+
|
45
|
+
def dummy_headers
|
46
|
+
{ "Content-Type" => "application/json" }
|
47
|
+
end
|
48
|
+
|
49
|
+
def dummy_body
|
50
|
+
document = ResponseGenerator.call(schema_for_current_link)
|
51
|
+
document = [document] if has_list_data?
|
52
|
+
MultiJson.encode(document, pretty: true) + "\n"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class ResponseGenerator
|
57
|
+
# Generates example response Hash from given schema
|
58
|
+
# @return [Hash]
|
59
|
+
# @example
|
60
|
+
# Rack::JsonSchema::Mock::ResponseGenerator(schema) #=> { "id" => 1, "name" => "example" }
|
61
|
+
def self.call(schema)
|
62
|
+
schema.properties.inject({}) do |result, (key, value)|
|
63
|
+
result.merge(
|
64
|
+
key => case
|
65
|
+
when !value.properties.empty?
|
66
|
+
call(value)
|
67
|
+
when !value.data["example"].nil?
|
68
|
+
value.data["example"]
|
69
|
+
when value.type.include?("null")
|
70
|
+
nil
|
71
|
+
else
|
72
|
+
raise ExampleNotFound, "No example found for #{schema.pointer}/#{key}"
|
73
|
+
end
|
74
|
+
)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
class Error < Error
|
80
|
+
end
|
81
|
+
|
82
|
+
class ExampleNotFound < Error
|
83
|
+
def id
|
84
|
+
"example_not_found"
|
85
|
+
end
|
86
|
+
|
87
|
+
def status
|
88
|
+
500
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,184 @@
|
|
1
|
+
module Rack
|
2
|
+
module JsonSchema
|
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
|
+
# @raise [Rack::JsonSchema::RequestValidation::Error] Raises if given request is invalid to JSON Schema
|
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 < BaseRequestHandler
|
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::JsonSchema::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_body? && !has_valid_json?
|
42
|
+
raise InvalidJson
|
43
|
+
when has_body? && has_schema? && !has_valid_parameter?
|
44
|
+
raise InvalidParameter, "Invalid request.\n#{schema_validation_error_message}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def has_valid_json?
|
51
|
+
parameters
|
52
|
+
true
|
53
|
+
rescue MultiJson::ParseError
|
54
|
+
false
|
55
|
+
end
|
56
|
+
|
57
|
+
# @return [true, false] True if request parameters are all valid
|
58
|
+
def has_valid_parameter?
|
59
|
+
schema_validation_result[0]
|
60
|
+
end
|
61
|
+
|
62
|
+
# @return [true, false] True if any schema is defined for the current action
|
63
|
+
def has_schema?
|
64
|
+
!!link.schema
|
65
|
+
end
|
66
|
+
|
67
|
+
# @return [true, false] True if request body is not empty
|
68
|
+
def has_body?
|
69
|
+
!body.empty?
|
70
|
+
end
|
71
|
+
|
72
|
+
# @return [true, false] True if no or matched content type given
|
73
|
+
def has_valid_content_type?
|
74
|
+
mime_type.nil? || Rack::Mime.match?(link.enc_type, mime_type)
|
75
|
+
end
|
76
|
+
|
77
|
+
# @return [Array] A result of schema validation for the current action
|
78
|
+
def schema_validation_result
|
79
|
+
@schema_validation_result ||= link.schema.validate(parameters)
|
80
|
+
end
|
81
|
+
|
82
|
+
# @return [Array] Errors of schema validation
|
83
|
+
def schema_validation_errors
|
84
|
+
schema_validation_result[1]
|
85
|
+
end
|
86
|
+
|
87
|
+
# @return [String] Joined error message to the result of schema validation
|
88
|
+
def schema_validation_error_message
|
89
|
+
::JsonSchema::SchemaError.aggregate(schema_validation_errors).join("\n")
|
90
|
+
end
|
91
|
+
|
92
|
+
# @return [String, nil] Request MIME Type specified in Content-Type header field
|
93
|
+
# @example
|
94
|
+
# mime_type #=> "application/json"
|
95
|
+
def mime_type
|
96
|
+
request.content_type.split(";").first if request.content_type
|
97
|
+
end
|
98
|
+
|
99
|
+
# @return [String] request body
|
100
|
+
def body
|
101
|
+
if instance_variable_defined?(:@body)
|
102
|
+
@body
|
103
|
+
else
|
104
|
+
@body = request.body.read
|
105
|
+
request.body.rewind
|
106
|
+
@body
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# @return [Hash] Request parameters decoded from JSON
|
111
|
+
# @raise [MultiJson::ParseError]
|
112
|
+
def parameters
|
113
|
+
@parameters ||= begin
|
114
|
+
if has_body?
|
115
|
+
MultiJson.decode(body)
|
116
|
+
else
|
117
|
+
{}
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# Base error class for Rack::JsonSchema::RequestValidation
|
124
|
+
class Error < Error
|
125
|
+
end
|
126
|
+
|
127
|
+
# Error class for case when no link defined for given request
|
128
|
+
class LinkNotFound < Error
|
129
|
+
def initialize
|
130
|
+
super("Not found")
|
131
|
+
end
|
132
|
+
|
133
|
+
def status
|
134
|
+
404
|
135
|
+
end
|
136
|
+
|
137
|
+
def id
|
138
|
+
"link_not_found"
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# Error class for invalid request content type
|
143
|
+
class InvalidContentType < Error
|
144
|
+
def initialize
|
145
|
+
super("Invalid content type")
|
146
|
+
end
|
147
|
+
|
148
|
+
def status
|
149
|
+
400
|
150
|
+
end
|
151
|
+
|
152
|
+
def id
|
153
|
+
"invalid_content_type"
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# Error class for invalid JSON
|
158
|
+
class InvalidJson < Error
|
159
|
+
def initialize
|
160
|
+
super("Request body wasn't valid JSON")
|
161
|
+
end
|
162
|
+
|
163
|
+
def status
|
164
|
+
400
|
165
|
+
end
|
166
|
+
|
167
|
+
def id
|
168
|
+
"invalid_json"
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# Error class for invalid request parameter
|
173
|
+
class InvalidParameter < Error
|
174
|
+
def status
|
175
|
+
400
|
176
|
+
end
|
177
|
+
|
178
|
+
def id
|
179
|
+
"invalid_parameter"
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module Rack
|
2
|
+
module JsonSchema
|
3
|
+
class ResponseValidation
|
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
|
+
# @raise [Rack::JsonSchema::ResponseValidation::Error]
|
14
|
+
# @param env [Hash] Rack env
|
15
|
+
def call(env)
|
16
|
+
@app.call(env).tap do |response|
|
17
|
+
Validator.call(env: env, response: response, schema: @schema)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class Validator < BaseRequestHandler
|
22
|
+
# @param env [Hash] Rack env
|
23
|
+
# @param response [Array] Rack response
|
24
|
+
# @param schema [JsonSchema::Schema] Schema object
|
25
|
+
def initialize(env: nil, response: nil, schema: nil)
|
26
|
+
@env = env
|
27
|
+
@response = response
|
28
|
+
@schema = schema
|
29
|
+
end
|
30
|
+
|
31
|
+
# Raises an error if any error detected, skipping validation for non-defined link
|
32
|
+
# @raise [Rack::JsonSchema::ResponseValidation::InvalidResponse]
|
33
|
+
def call
|
34
|
+
if has_link_for_current_action?
|
35
|
+
case
|
36
|
+
when !has_json_content_type?
|
37
|
+
raise InvalidResponseContentType
|
38
|
+
when !valid?
|
39
|
+
raise InvalidResponseType, validator.errors
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# @return [true, false] True if response Content-Type is for JSON
|
45
|
+
def has_json_content_type?
|
46
|
+
%r<\Aapplication/.*json> === headers["Content-Type"]
|
47
|
+
end
|
48
|
+
|
49
|
+
# @return [true, false] True if given data is valid to the JSON schema
|
50
|
+
def valid?
|
51
|
+
validator.validate(example_item)
|
52
|
+
end
|
53
|
+
|
54
|
+
# @return [Hash] Choose an item from response data, to be validated
|
55
|
+
def example_item
|
56
|
+
if has_list_data?
|
57
|
+
data.first
|
58
|
+
else
|
59
|
+
data
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# @return [Array, Hash] Response body data, decoded from JSON
|
64
|
+
def data
|
65
|
+
MultiJson.decode(body)
|
66
|
+
end
|
67
|
+
|
68
|
+
# @return [JsonSchema::Validator]
|
69
|
+
# @note The result is memoized for returning errors in invalid case
|
70
|
+
def validator
|
71
|
+
@validator ||= ::JsonSchema::Validator.new(schema_for_current_link)
|
72
|
+
end
|
73
|
+
|
74
|
+
# @return [Hash] Response headers
|
75
|
+
def headers
|
76
|
+
@response[1]
|
77
|
+
end
|
78
|
+
|
79
|
+
# @return [String] Response body
|
80
|
+
def body
|
81
|
+
result = ""
|
82
|
+
@response[2].each {|str| result << str }
|
83
|
+
result
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Base error class for Rack::JsonSchema::ResponseValidation
|
88
|
+
class Error < Error
|
89
|
+
end
|
90
|
+
|
91
|
+
class InvalidResponseType < Error
|
92
|
+
def initialize(errors)
|
93
|
+
super ::JsonSchema::SchemaError.aggregate(errors).join(" ")
|
94
|
+
end
|
95
|
+
|
96
|
+
def id
|
97
|
+
"invalid_response_type"
|
98
|
+
end
|
99
|
+
|
100
|
+
def status
|
101
|
+
500
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
class InvalidResponseContentType < Error
|
106
|
+
def initialize
|
107
|
+
super("Response Content-Type wasn't for JSON")
|
108
|
+
end
|
109
|
+
|
110
|
+
def id
|
111
|
+
"invalid_response_content_type"
|
112
|
+
end
|
113
|
+
|
114
|
+
def status
|
115
|
+
500
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|