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