interpol 0.3.6 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -25,6 +25,8 @@ definitions:
25
25
  * `Interpol::Sinatra::RequestParamsParser` validates and parses
26
26
  a sinatra `params` hash based on your endpoint params schema
27
27
  definitions.
28
+ * `Interpol::RequestBodyValidator` is a rack middleware that validates
29
+ and parses request bodies based on your schema definitions.
28
30
 
29
31
  You can use any of these tools individually or some combination of all
30
32
  of them.
@@ -145,22 +147,30 @@ Interpol.default_configuration do |config|
145
147
  # Needed by all tools.
146
148
  config.endpoint_definition_files = Dir["config/endpoints/*.yml"]
147
149
 
148
- # Determines which versioned endpoint definition Interpol uses
150
+ # Determines which versioned response endpoint definition Interpol uses
149
151
  # for a request. You can also use a block form, which yields
150
152
  # the rack env hash and the endpoint object as arguments.
151
153
  # This is useful when you need to extract the version from a
152
154
  # request header (e.g. Accept) or from the request URI.
153
155
  #
154
- # Needed by Interpol::StubApp, Interpol::ResponseSchemaValidator
155
- # and Interpol::Sinatra::RequestParamsParser.
156
- config.api_version '1.0'
156
+ # Needed by Interpol::StubApp and Interpol::ResponseSchemaValidator.
157
+ config.response_version '1.0'
158
+
159
+ # Determines which versioned response endpoint definition Interpol uses
160
+ # for a request. You can also use a block form, which yields
161
+ # the rack env hash and the endpoint object as arguments.
162
+ # This is useful when you need to extract the version from a
163
+ # request header (e.g. Content-Type) or from the request URI.
164
+ #
165
+ # Needed by Interpol::Sinatra::RequestParamsParser.
166
+ config.request_version '1.0'
157
167
 
158
168
  # Determines the stub app response when the requested version is not
159
169
  # available. This block will be eval'd in the context of a
160
170
  # sinatra application, so you can use sinatra helpers like `halt` here.
161
171
  #
162
- # Needed by Interpol::StubApp and Interpol::Sinatra::RequestParamsParser.
163
- config.on_unavailable_request_version do |requested_version, available_versions|
172
+ # Used by Interpol::StubApp and Interpol::Sinatra::RequestParamsParser.
173
+ config.on_unavailable_sinatra_request_version do |requested_version, available_versions|
164
174
  message = JSON.dump(
165
175
  "message" => "Not Acceptable",
166
176
  "requested_version" => requested_version,
@@ -170,18 +180,33 @@ Interpol.default_configuration do |config|
170
180
  halt 406, message
171
181
  end
172
182
 
183
+ # Determines the response when the requested version is not available.
184
+ #
185
+ # Used by Interpol::RequestBodyValidator.
186
+ config.on_unavailable_request_version do |env, requested_version, available_versions|
187
+ [406, { 'Content-Type' => 'text/plain' }, ['Wrong Version!']]
188
+ end
189
+
173
190
  # Determines which responses will be validated against the endpoint
174
191
  # definition when you use Interpol::ResponseSchemaValidator. The
175
192
  # validation is meant to run against the "happy path" response.
176
193
  # For responses like "404 Not Found", you probably don't want any
177
- # validation performed. The default validate_if hook will cause
194
+ # validation performed. The default validate_response_if hook will cause
178
195
  # validation to run against any 2xx response except 204 ("No Content").
179
196
  #
180
197
  # Used by Interpol::ResponseSchemaValidator.
181
- config.validate_if do |env, status, headers, body|
198
+ config.validate_response_if do |env, status, headers, body|
182
199
  headers['Content-Type'] == my_custom_mime_type
183
200
  end
184
201
 
202
+ # Determines which request bodies to validate.
203
+ #
204
+ # Used by Interpol::RequestBodyValidator.
205
+ config.validate_request_if do |env|
206
+ env.fetch('CONTENT_TYPE').to_s.include?('json') &&
207
+ %w[ POST PUT ].include?(env.fetch('REQUEST_METHOD'))
208
+ end
209
+
185
210
  # Determines how Interpol::ResponseSchemaValidator handles
186
211
  # invalid data. By default it will raise an error, but you can
187
212
  # make it print a warning instead.
@@ -216,6 +241,14 @@ Interpol.default_configuration do |config|
216
241
  config.on_invalid_sinatra_request_params do |error|
217
242
  halt 400, JSON.dump(:error => error.message)
218
243
  end
244
+
245
+ # Determines how to respond when the request body is invalid
246
+ # based on your schema definition.
247
+ #
248
+ # Used by Interpol::RequestBodyValidator.
249
+ config.on_invalid_request_body do |env, error|
250
+ [400, { 'Content-Type' => 'text/plain' }, [error.message]]
251
+ end
219
252
  end
220
253
 
221
254
  ```
@@ -275,7 +308,7 @@ require 'interpol/stub_app'
275
308
  # config or if you have not set a default config.
276
309
  stub_app = Interpol::StubApp.build do |app|
277
310
  app.endpoint_definition_files = Dir["config/endpoints_definitions/*.yml"]
278
- app.api_version do |env|
311
+ app.response_version do |env|
279
312
  RequestVersion.extract_from(env['HTTP_ACCEPT'])
280
313
  end
281
314
  end
@@ -300,7 +333,7 @@ unless ENV['RACK_ENV'] == 'production'
300
333
  # config or if you have not set a default config.
301
334
  use Interpol::ResponseSchemaValidator do |config|
302
335
  config.endpoint_definition_files = Dir["config/endpoints_definitions/*.yml"]
303
- config.api_version do |env|
336
+ config.response_version do |env|
304
337
  RequestVersion.extract_from(env['HTTP_ACCEPT'])
305
338
  end
306
339
  end
@@ -384,6 +417,38 @@ class MySinatraApp < Sinatra::Base
384
417
  end
385
418
  ```
386
419
 
420
+ ### Interpol::RequestBodyValidator
421
+
422
+ This rack middleware validates request body (e.g. for POST or PUT
423
+ requests) based on your endpoint request schema definitions.
424
+ It also makes the parsed request body available as
425
+ `interpol.parsed_body` in the rack env hash.
426
+
427
+ ``` ruby
428
+ require 'sinatra/base'
429
+ require 'interpol/request_body_validator'
430
+
431
+ class MySinatraApp < Sinatra::Base
432
+ # The block is only necessary if you want to override the
433
+ # default config or have not set a default config.
434
+ use Interpol::RequestBodyValidator do |config|
435
+ config.on_invalid_request_body do |error|
436
+ [400, { 'Content-Type' => 'text/plain' }, [error.message]]
437
+ end
438
+ end
439
+
440
+ helpers do
441
+ def parsed_body
442
+ env.fetch('interpol.parsed_body')
443
+ end
444
+ end
445
+
446
+ put '/users/:user_id' do
447
+ User.create_or_replace(parsed_body.user_id, parsed_body.attributes)
448
+ end
449
+ end
450
+ ```
451
+
387
452
  ## Contributing
388
453
 
389
454
  1. Fork it
data/Rakefile CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env rake
2
+ require File.expand_path('../config/setup_load_paths', __FILE__)
2
3
  require "bundler/gem_tasks"
3
4
 
4
5
  require 'rspec/core/rake_task'
@@ -40,9 +41,6 @@ end
40
41
 
41
42
  desc "Import the twitter bootstrap assets from the compass_twitter_bootstrap gem"
42
43
  task :import_bootstrap_assets do
43
- require 'bundler'
44
- Bundler.setup
45
-
46
44
  # when the gem installed as a :git gem, it has "-" as a separator;
47
45
  # when it's installed as a released rubygem, it has "_" as a separator.
48
46
  gem_lib_path = $LOAD_PATH.grep(/compass[-_]twitter[-_]bootstrap/).first
@@ -56,33 +56,69 @@ module Interpol
56
56
  @endpoints = endpoints.extend(DefinitionFinder)
57
57
  end
58
58
 
59
+ [:request, :response].each do |type|
60
+ class_eval <<-EOEVAL, __FILE__, __LINE__ + 1
61
+ def #{type}_version(version = nil, &block)
62
+ if [version, block].compact.size.even?
63
+ raise ConfigurationError.new("#{type}_version requires a static version " +
64
+ "or a dynamic block, but not both")
65
+ end
66
+
67
+ @#{type}_version_block = block || lambda { |*a| version }
68
+ end
69
+
70
+ def #{type}_version_for(rack_env, *extra_args)
71
+ @#{type}_version_block.call(rack_env, *extra_args).to_s
72
+ end
73
+ EOEVAL
74
+ end
75
+
59
76
  def api_version(version=nil, &block)
60
- if [version, block].compact.size.even?
61
- raise ConfigurationError.new("api_version requires a static version " +
62
- "or a dynamic block, but not both")
63
- end
77
+ warn "WARNING: Interpol's #api_version config option is deprecated. " +
78
+ "Instead, use separate #request_version and #response_version " +
79
+ "config options."
80
+
81
+ request_version(version, &block)
82
+ response_version(version, &block)
83
+ end
64
84
 
65
- @api_version_block = block || lambda { |*a| version }
85
+ def validate_response_if(&block)
86
+ @validate_response_if_block = block
66
87
  end
67
88
 
68
- def api_version_for(rack_env, endpoint=nil)
69
- @api_version_block.call(rack_env, endpoint).to_s
89
+ def validate_response?(*args)
90
+ @validate_response_if_block.call(*args)
70
91
  end
71
92
 
72
93
  def validate_if(&block)
73
- @validate_if_block = block
94
+ warn "WARNING: Interpol's #validate_if config option is deprecated. " +
95
+ "Instead, use #validate_response_if."
96
+
97
+ validate_response_if(&block)
74
98
  end
75
99
 
76
- def validate?(*args)
77
- @validate_if_block.call(*args)
100
+ def validate_request?(env)
101
+ @validate_request_if_block.call(env)
102
+ end
103
+
104
+ def validate_request_if(&block)
105
+ @validate_request_if_block = block
106
+ end
107
+
108
+ def on_unavailable_sinatra_request_version(&block)
109
+ @unavailable_sinatra_request_version_block = block
110
+ end
111
+
112
+ def sinatra_request_version_unavailable(execution_context, *args)
113
+ execution_context.instance_exec(*args, &@unavailable_sinatra_request_version_block)
78
114
  end
79
115
 
80
116
  def on_unavailable_request_version(&block)
81
117
  @unavailable_request_version_block = block
82
118
  end
83
119
 
84
- def request_version_unavailable(execution_context, *args)
85
- execution_context.instance_exec(*args, &@unavailable_request_version_block)
120
+ def request_version_unavailable(*args)
121
+ @unavailable_request_version_block.call(*args)
86
122
  end
87
123
 
88
124
  def on_invalid_sinatra_request_params(&block)
@@ -93,6 +129,14 @@ module Interpol
93
129
  execution_context.instance_exec(*args, &@invalid_sinatra_request_params_block)
94
130
  end
95
131
 
132
+ def on_invalid_request_body(&block)
133
+ @invalid_request_body_block = block
134
+ end
135
+
136
+ def request_body_invalid(*args)
137
+ @invalid_request_body_block.call(*args)
138
+ end
139
+
96
140
  def filter_example_data(&block)
97
141
  filter_example_data_blocks << block
98
142
  end
@@ -136,23 +180,52 @@ module Interpol
136
180
  }.join("\n\n")
137
181
  end
138
182
 
183
+ def rack_json_response(status, hash)
184
+ json = JSON.dump(hash)
185
+
186
+ [status, { 'Content-Type' => 'application/json',
187
+ 'Content-Length' => json.bytesize }, [json]]
188
+ end
189
+
139
190
  def register_default_callbacks
140
- api_version do
141
- raise ConfigurationError, "api_version has not been configured"
191
+ request_version do
192
+ raise ConfigurationError, "request_version has not been configured"
142
193
  end
143
194
 
144
- validate_if do |env, status, headers, body|
195
+ response_version do
196
+ raise ConfigurationError, "response_version has not been configured"
197
+ end
198
+
199
+ validate_response_if do |env, status, headers, body|
145
200
  headers['Content-Type'].to_s.include?('json') &&
146
201
  status >= 200 && status <= 299 && status != 204 # No Content
147
202
  end
148
203
 
149
- on_unavailable_request_version do |requested, available|
150
- message = "The requested API version is invalid. " +
204
+ validate_request_if do |env|
205
+ env.fetch('CONTENT_TYPE').to_s.include?('json') &&
206
+ %w[ POST PUT ].include?(env.fetch('REQUEST_METHOD'))
207
+ end
208
+
209
+ on_unavailable_request_version do |env, requested, available|
210
+ message = "The requested request version is invalid. " +
151
211
  "Requested: #{requested}. " +
152
212
  "Available: #{available}"
213
+
214
+ rack_json_response(406, :error => message)
215
+ end
216
+
217
+ on_unavailable_sinatra_request_version do |requested, available|
218
+ message = "The requested request version is invalid. " +
219
+ "Requested: #{requested}. " +
220
+ "Available: #{available}"
221
+
153
222
  halt 406, JSON.dump(:error => message)
154
223
  end
155
224
 
225
+ on_invalid_request_body do |env, error|
226
+ rack_json_response(400, :error => error.message)
227
+ end
228
+
156
229
  on_invalid_sinatra_request_params do |error|
157
230
  halt 400, JSON.dump(:error => error.message)
158
231
  end
@@ -71,10 +71,12 @@ module Interpol
71
71
  find_definition!(version, 'response').first.example_status_code
72
72
  end
73
73
 
74
- def available_versions
75
- @all_definitions.inject(Set.new) do |set, definition|
76
- set << definition.version
77
- end.to_a
74
+ def available_request_versions
75
+ available_versions_matching &:request?
76
+ end
77
+
78
+ def available_response_versions
79
+ available_versions_matching &:response?
78
80
  end
79
81
 
80
82
  def definitions
@@ -95,6 +97,12 @@ module Interpol
95
97
 
96
98
  private
97
99
 
100
+ def available_versions_matching
101
+ @all_definitions.each_with_object(Set.new) do |definition, set|
102
+ set << definition.version if yield definition
103
+ end.to_a
104
+ end
105
+
98
106
  def route_regex
99
107
  @route_regex ||= begin
100
108
  regex_string = route.split('/').map do |path_part|
@@ -163,6 +171,10 @@ module Interpol
163
171
  message_type == "request"
164
172
  end
165
173
 
174
+ def response?
175
+ message_type == "response"
176
+ end
177
+
166
178
  def endpoint_name
167
179
  @endpoint.name
168
180
  end
@@ -175,7 +187,9 @@ module Interpol
175
187
  end
176
188
 
177
189
  def description
178
- "#{endpoint_name} (v. #{version}, mt. #{message_type}, sc. #{status_codes})"
190
+ subdescription = "#{message_type} v. #{version}"
191
+ subdescription << " for status: #{status_codes}" if message_type == 'response'
192
+ "#{endpoint_name} (#{subdescription})"
179
193
  end
180
194
 
181
195
  def status_codes
@@ -0,0 +1,82 @@
1
+ require 'interpol'
2
+ require 'interpol/dynamic_struct'
3
+
4
+ module Interpol
5
+ # Validates and parses a request body according to the endpoint
6
+ # schema definitions.
7
+ class RequestBodyValidator
8
+ def initialize(app, &block)
9
+ @config = Configuration.default.customized_duplicate(&block)
10
+ @app = app
11
+ end
12
+
13
+ def call(env)
14
+ if @config.validate_request?(env)
15
+ handler = Handler.new(env, @config)
16
+
17
+ handler.validate do |error_response|
18
+ return error_response
19
+ end
20
+
21
+ env['interpol.parsed_body'] = handler.parse
22
+ end
23
+
24
+ @app.call(env)
25
+ end
26
+
27
+ # Handles request body validation for a single request.
28
+ class Handler
29
+ attr_reader :env, :config
30
+
31
+ def initialize(env, config)
32
+ @env = env
33
+ @config = config
34
+ end
35
+
36
+ def parse
37
+ DynamicStruct.new(parsed_body)
38
+ end
39
+
40
+ def validate(&block)
41
+ endpoint_definition(&block).validate_data!(parsed_body)
42
+ rescue Interpol::ValidationError => e
43
+ yield @config.request_body_invalid(env, e)
44
+ end
45
+
46
+ private
47
+
48
+ def request_method
49
+ env.fetch('REQUEST_METHOD')
50
+ end
51
+
52
+ def path
53
+ env.fetch('PATH_INFO')
54
+ end
55
+
56
+ def parsed_body
57
+ @parsed_body ||= JSON.parse(unparsed_body)
58
+ end
59
+
60
+ def unparsed_body
61
+ @unparsed_body ||= begin
62
+ input = env.fetch('rack.input')
63
+ input.read.tap { input.rewind }
64
+ end
65
+ end
66
+
67
+ def endpoint_definition(&block)
68
+ config.endpoints.find_definition(request_method, path, 'request', nil) do |endpoint|
69
+ available = endpoint.available_request_versions
70
+
71
+ @config.request_version_for(env, endpoint).tap do |requested|
72
+ unless available.include?(requested)
73
+ yield @config.request_version_unavailable(env, requested, available)
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ end
80
+ end
81
+ end
82
+
@@ -15,7 +15,9 @@ module Interpol
15
15
 
16
16
  def call(env)
17
17
  status, headers, body = @app.call(env)
18
- return status, headers, body unless @config.validate?(env, status, headers, body)
18
+ unless @config.validate_response?(env, status, headers, body)
19
+ return status, headers, body
20
+ end
19
21
 
20
22
  handler = @handler_class.new(status, headers, body, env, @config)
21
23
  handler.validate!
@@ -69,9 +71,13 @@ module Interpol
69
71
  def validator
70
72
  @validator ||= @config.endpoints.
71
73
  find_definition(request_method, path, 'response', status) do |endpoint|
72
- @config.api_version_for(env, endpoint)
74
+ @config.response_version_for(env, endpoint, response_triplet)
73
75
  end
74
76
  end
77
+
78
+ def response_triplet
79
+ [status, headers, extracted_body]
80
+ end
75
81
  end
76
82
 
77
83
  # Private: Subclasses Handler in order to convert validation errors
@@ -83,14 +83,14 @@ module Interpol
83
83
 
84
84
  definition = config.endpoints.find_definition \
85
85
  request.env.fetch('REQUEST_METHOD'), request.path, 'request', nil do |endpoint|
86
- available_versions ||= endpoint.available_versions
87
- config.api_version_for(request.env, endpoint).tap do |_version|
86
+ available_versions ||= endpoint.available_request_versions
87
+ config.request_version_for(request.env, endpoint).tap do |_version|
88
88
  version ||= _version
89
89
  end
90
90
  end
91
91
 
92
92
  if definition == DefinitionFinder::NoDefinitionFound
93
- config.request_version_unavailable(app, version, available_versions)
93
+ config.sinatra_request_version_unavailable(app, version, available_versions)
94
94
  end
95
95
 
96
96
  definition
@@ -53,10 +53,11 @@ module Interpol
53
53
  end
54
54
 
55
55
  def example_and_version_for(endpoint, app)
56
- version = config.api_version_for(app.request.env, endpoint)
56
+ version = config.response_version_for(app.request.env, endpoint)
57
57
  example = endpoint.find_example_for!(version, 'response')
58
58
  rescue NoEndpointDefinitionFoundError
59
- config.request_version_unavailable(app, version, endpoint.available_versions)
59
+ config.sinatra_request_version_unavailable(app, version,
60
+ endpoint.available_response_versions)
60
61
  else
61
62
  example = example.apply_filters(config.filter_example_data_blocks, app.request.env)
62
63
  return example, version
@@ -1,3 +1,3 @@
1
1
  module Interpol
2
- VERSION = "0.3.6"
2
+ VERSION = "0.4.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: interpol
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.6
4
+ version: 0.4.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,8 +9,24 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-10-03 00:00:00.000000000 Z
12
+ date: 2012-10-17 00:00:00.000000000 Z
13
13
  dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rack
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
14
30
  - !ruby/object:Gem::Dependency
15
31
  name: json-schema
16
32
  requirement: !ruby/object:Gem::Requirement
@@ -18,7 +34,7 @@ dependencies:
18
34
  requirements:
19
35
  - - ~>
20
36
  - !ruby/object:Gem::Version
21
- version: 1.0.8
37
+ version: 1.0.10
22
38
  type: :runtime
23
39
  prerelease: false
24
40
  version_requirements: !ruby/object:Gem::Requirement
@@ -26,7 +42,7 @@ dependencies:
26
42
  requirements:
27
43
  - - ~>
28
44
  - !ruby/object:Gem::Version
29
- version: 1.0.8
45
+ version: 1.0.10
30
46
  - !ruby/object:Gem::Dependency
31
47
  name: nokogiri
32
48
  requirement: !ruby/object:Gem::Requirement
@@ -177,6 +193,7 @@ files:
177
193
  - lib/interpol/each_with_object.rb
178
194
  - lib/interpol/endpoint.rb
179
195
  - lib/interpol/errors.rb
196
+ - lib/interpol/request_body_validator.rb
180
197
  - lib/interpol/request_params_parser.rb
181
198
  - lib/interpol/response_schema_validator.rb
182
199
  - lib/interpol/sinatra/request_params_parser.rb
@@ -221,18 +238,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
221
238
  - - ! '>='
222
239
  - !ruby/object:Gem::Version
223
240
  version: '0'
224
- segments:
225
- - 0
226
- hash: 4001494580876368258
227
241
  required_rubygems_version: !ruby/object:Gem::Requirement
228
242
  none: false
229
243
  requirements:
230
244
  - - ! '>='
231
245
  - !ruby/object:Gem::Version
232
246
  version: '0'
233
- segments:
234
- - 0
235
- hash: 4001494580876368258
236
247
  requirements: []
237
248
  rubyforge_project:
238
249
  rubygems_version: 1.8.24