interpol 0.3.6 → 0.4.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.
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