rack-json_schema 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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