apipie-rails 0.5.8 → 0.5.9

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 97d15ca9f95ac7fc79b848f3a3207c22c8e69f4c
4
- data.tar.gz: 5d0dcae7fea76412efec316d2774da51367cee44
3
+ metadata.gz: 67882facd3165a11bb36de196c2eaefd4444f698
4
+ data.tar.gz: 26ad0df6cd1f15d9f530838cf8dd9c7298dab91a
5
5
  SHA512:
6
- metadata.gz: 8716a1ebb09f66511232cdc45756cef0794791b618ae0dec4c1455e25cc194a5e5ab6ae9ec49c4845d6390f969785f4fe1f69b7d0e783c903cbce0f21c4f4b71
7
- data.tar.gz: 63a0482166b36b1d6c8e2d066b67cd76242495111a0a79296576a7505adf22070a8515ddef1d9136c91f2b502059e0a3a5b1a358ebc7e1cd4d75b0c8439b2537
6
+ metadata.gz: b3b202e8d5774e1f669b93059df6152a7006177e04cb1ca3cde7151207aa3318310f75a374f67fa38752f3c7f53c18c3a72d0db165655460da97ec362465382f
7
+ data.tar.gz: f7541b0e319499e074b7171dd286b33f73a7b398e8e04a41bd5242b824f08f8bc83adabdadb75605e8143a40800729369f170da9e2d384f8acebea23f1aea9a1
@@ -1,6 +1,14 @@
1
1
  Changelog
2
2
  ===========
3
3
 
4
+ v0.5.9
5
+ ------
6
+
7
+ - Support response validation [\#619](https://github.com/Apipie/apipie-rails/pull/619) ([abenari](https://github.com/abenari))
8
+ - Expect :number to have type 'numeric' [\#614](https://github.com/Apipie/apipie-rails/pull/614) ([akofink](https://github.com/akofink))
9
+ - New validator - DecimalValidator [\#431](https://github.com/Apipie/apipie-rails/pull/431) ([kiddrew](https://github.com/kiddrew))
10
+
11
+
4
12
  v0.5.8
5
13
  ------
6
14
 
data/README.rst CHANGED
@@ -842,6 +842,77 @@ The concern needs to be included to the controller after the methods are defined
842
842
  (either at the end of the class, or by using
843
843
  ``Controller.send(:include, Concerns::OauthConcern)``.
844
844
 
845
+
846
+ Response validation
847
+ -------------------
848
+
849
+ The swagger definitions created by Apipie can be used to auto-generate clients that access the
850
+ described APIs. Those clients will break if the responses returned from the API do not match
851
+ the declarations. As such, it is very important to include unit tests that validate the actual
852
+ responses against the swagger definitions.
853
+
854
+ The implemented mechanism provides two ways to include such validations in RSpec unit tests:
855
+ manual (using an RSpec matcher) and automated (by injecting a test into the http operations 'get', 'post',
856
+ raising an error if there is no match).
857
+
858
+ Example of the manual mechanism:
859
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
860
+
861
+ .. code:: ruby
862
+
863
+ require 'apipie/rspec/response_validation_helper'
864
+
865
+ RSpec.describe MyController, :type => :controller, :show_in_doc => true do
866
+
867
+ describe "GET stuff with response validation" do
868
+ render_views # this makes sure the 'get' operation will actually
869
+ # return the rendered view even though this is a Controller spec
870
+
871
+ it "does something" do
872
+ response = get :index, {format: :json}
873
+
874
+ # the following expectation will fail if the returned object
875
+ # does not match the 'returns' declaration in the Controller,
876
+ # or if there is no 'returns' declaration for the returned
877
+ # HTTP status code
878
+ expect(response).to match_declared_responses
879
+ end
880
+ end
881
+ end
882
+
883
+
884
+ Example of the automated mechanism:
885
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
886
+
887
+ .. code:: ruby
888
+
889
+ require 'apipie/rspec/response_validation_helper'
890
+
891
+ RSpec.describe MyController, :type => :controller, :show_in_doc => true do
892
+
893
+ describe "GET stuff with response validation" do
894
+ render_views
895
+ auto_validate_rendered_views
896
+
897
+ it "does something" do
898
+ get :index, {format: :json}
899
+ end
900
+ it "does something else" do
901
+ get :another_index, {format: :json}
902
+ end
903
+ end
904
+
905
+ describe "GET stuff without response validation" do
906
+ it "does something" do
907
+ get :index, {format: :json}
908
+ end
909
+ it "does something else" do
910
+ get :another_index, {format: :json}
911
+ end
912
+ end
913
+ end
914
+
915
+
845
916
  =========================
846
917
  Configuration Reference
847
918
  =========================
@@ -1163,6 +1234,17 @@ ArrayValidator
1163
1234
 
1164
1235
  Check if the parameter is an array
1165
1236
 
1237
+ DecimalValidator
1238
+ --------------
1239
+
1240
+ Check if the parameter is a decimal number
1241
+
1242
+ .. code:: ruby
1243
+
1244
+ param :latitude, :decimal, :desc => "Geographic latitude", :required => true
1245
+ param :longitude, :decimal, :desc => "Geographic longitude", :required => true
1246
+
1247
+
1166
1248
  Additional options
1167
1249
  ~~~~~~~~~~~~~~~~~
1168
1250
 
@@ -18,6 +18,17 @@ module Apipie
18
18
  app.to_swagger_json(version, resource_name, method_name, lang, clear_warnings)
19
19
  end
20
20
 
21
+ def self.json_schema_for_method_response(controller_name, method_name, return_code, allow_nulls)
22
+ # note: this does not support versions (only the default version is queried)!
23
+ version ||= Apipie.configuration.default_version
24
+ app.json_schema_for_method_response(version, controller_name, method_name, return_code, allow_nulls)
25
+ end
26
+
27
+ def self.json_schema_for_self_describing_class(cls, allow_nulls=true)
28
+ app.json_schema_for_self_describing_class(cls, allow_nulls)
29
+ end
30
+
31
+
21
32
  # all calls delegated to Apipie::Application instance
22
33
  def self.method_missing(method, *args, &block)
23
34
  app.respond_to?(method) ? app.send(method, *args, &block) : super
@@ -255,6 +255,16 @@ module Apipie
255
255
  @recorded_examples = nil
256
256
  end
257
257
 
258
+ def json_schema_for_method_response(version, controller_name, method_name, return_code, allow_nulls)
259
+ method = @resource_descriptions[version][controller_name].method_description(method_name)
260
+ raise NoDocumentedMethod.new(controller_name, method_name) if method.nil?
261
+ @swagger_generator.json_schema_for_method_response(method, return_code, allow_nulls)
262
+ end
263
+
264
+ def json_schema_for_self_describing_class(cls, allow_nulls)
265
+ @swagger_generator.json_schema_for_self_describing_class(cls, allow_nulls)
266
+ end
267
+
258
268
  def to_swagger_json(version, resource_name, method_name, lang, clear_warnings=false)
259
269
  return unless valid_search_args?(version, resource_name, method_name)
260
270
 
@@ -11,13 +11,14 @@ module Apipie
11
11
  :persist_show_in_doc, :authorize,
12
12
  :swagger_include_warning_tags, :swagger_content_type_input, :swagger_json_input_uses_refs,
13
13
  :swagger_suppress_warnings, :swagger_api_host, :swagger_generate_x_computed_id_field,
14
- :swagger_allow_additional_properties_in_response
14
+ :swagger_allow_additional_properties_in_response, :swagger_responses_use_refs
15
15
 
16
16
  alias_method :validate?, :validate
17
17
  alias_method :required_by_default?, :required_by_default
18
18
  alias_method :namespaced_resources?, :namespaced_resources
19
19
  alias_method :swagger_include_warning_tags?, :swagger_include_warning_tags
20
20
  alias_method :swagger_json_input_uses_refs?, :swagger_json_input_uses_refs
21
+ alias_method :swagger_responses_use_refs?, :swagger_responses_use_refs
21
22
  alias_method :swagger_generate_x_computed_id_field?, :swagger_generate_x_computed_id_field
22
23
 
23
24
  # matcher to be used in Dir.glob to find controllers to be reloaded e.g.
@@ -179,6 +180,7 @@ module Apipie
179
180
  @swagger_api_host = "localhost:3000"
180
181
  @swagger_generate_x_computed_id_field = false
181
182
  @swagger_allow_additional_properties_in_response = false
183
+ @swagger_responses_use_refs = true
182
184
  end
183
185
  end
184
186
  end
@@ -57,4 +57,30 @@ module Apipie
57
57
  "Invalid parameter '#{@param}' value #{@value.inspect}: #{@error}"
58
58
  end
59
59
  end
60
+
61
+ class ResponseDoesNotMatchSwaggerSchema < Error
62
+ def initialize(controller_name, method_name, response_code, error_messages, schema, returned_object)
63
+ @controller_name = controller_name
64
+ @method_name = method_name
65
+ @response_code = response_code
66
+ @error_messages = error_messages
67
+ @schema = schema
68
+ @returned_object = returned_object
69
+ end
70
+
71
+ def to_s
72
+ "Response does not match swagger schema (#{@controller_name}##{@method_name} #{@response_code}): #{@error_messages}\nSchema: #{JSON(@schema)}\nReturned object: #{@returned_object}"
73
+ end
74
+ end
75
+
76
+ class NoDocumentedMethod < Error
77
+ def initialize(controller_name, method_name)
78
+ @method_name = method_name
79
+ @controller_name = controller_name
80
+ end
81
+
82
+ def to_s
83
+ "There is no documented method #{@controller_name}##{@method_name}"
84
+ end
85
+ end
60
86
  end
@@ -75,6 +75,10 @@ module Apipie
75
75
  if param_desc[:type].first == :number && (key.to_s !~ /id$/ || !Apipie::Validator::NumberValidator.validate(value))
76
76
  param_desc[:type].shift
77
77
  end
78
+
79
+ if param_desc[:type].first == :decimal && (key.to_s !~ /id$/ || !Apipie::Validator::DecimalValidator.validate(value))
80
+ param_desc[:type].shift
81
+ end
78
82
  end
79
83
 
80
84
  if value.is_a? Hash
@@ -5,13 +5,14 @@ module Apipie
5
5
  include Apipie::DSL::Base
6
6
  include Apipie::DSL::Param
7
7
 
8
- attr_accessor :additional_properties
8
+ attr_accessor :additional_properties, :typename
9
9
 
10
- def initialize(method_description, scope, block)
10
+ def initialize(method_description, scope, block, typename)
11
11
  @method_description = method_description
12
12
  @scope = scope
13
13
  @param_group = {scope: scope}
14
14
  @additional_properties = false
15
+ @typename = typename
15
16
 
16
17
  self.instance_exec(&block) if block
17
18
 
@@ -67,6 +68,11 @@ module Apipie
67
68
  @is_array_of != false
68
69
  end
69
70
 
71
+ def typename
72
+ @response_object.typename
73
+ end
74
+
75
+
70
76
  def initialize(method_description, code, options, scope, block, adapter)
71
77
 
72
78
  @type_ref = options[:param_group]
@@ -93,7 +99,7 @@ module Apipie
93
99
  if adapter
94
100
  @response_object = adapter
95
101
  else
96
- @response_object = ResponseObject.new(method_description, scope, block)
102
+ @response_object = ResponseObject.new(method_description, scope, block, @type_ref)
97
103
  end
98
104
 
99
105
  @response_object.additional_properties ||= options[:additional_properties]
@@ -147,18 +147,19 @@ module Apipie
147
147
  class ResponseDescriptionAdapter
148
148
 
149
149
  def self.from_self_describing_class(cls)
150
- adapter = ResponseDescriptionAdapter.new
150
+ adapter = ResponseDescriptionAdapter.new(cls.to_s)
151
151
  props = cls.describe_own_properties
152
152
  adapter.add_property_descriptions(props)
153
153
  adapter
154
154
  end
155
155
 
156
- def initialize
156
+ def initialize(typename)
157
157
  @property_descs = []
158
158
  @additional_properties = false
159
+ @typename = typename
159
160
  end
160
161
 
161
- attr_accessor :additional_properties
162
+ attr_accessor :additional_properties, :typename
162
163
 
163
164
  def allow_additional_properties
164
165
  additional_properties
@@ -0,0 +1,194 @@
1
+ #----------------------------------------------------------------------------------------------
2
+ # response_validation_helper.rb:
3
+ #
4
+ # this is an rspec utility to allow validation of responses against the swagger schema generated
5
+ # from the Apipie 'returns' definition for the call.
6
+ #
7
+ #
8
+ # to use this file in a controller rspec you should
9
+ # require 'apipie/rspec/response_validation_helper' in the spec file
10
+ #
11
+ #
12
+ # this utility provides two mechanisms: matcher-based validation and auto-validation
13
+ #
14
+ # matcher-based: an rspec matcher allowing 'expect(response).to match_declared_responses'
15
+ # auto-validation: all responses returned from 'get', 'post', etc. are automatically tested
16
+ #
17
+ # ===================================
18
+ # Matcher-based validation - example
19
+ # ===================================
20
+ # Assume the file 'my_controller_spec.rb':
21
+ #
22
+ # require 'apipie/rspec/response_validation_helper'
23
+ #
24
+ # RSpec.describe MyController, :type => :controller, :show_in_doc => true do
25
+ #
26
+ # describe "GET stuff with response validation" do
27
+ # render_views # this makes sure the 'get' operation will actually
28
+ # # return the rendered view even though this is a Controller spec
29
+ #
30
+ # it "does something" do
31
+ # response = get :index, {format: :json}
32
+ #
33
+ # # the following expectation will fail if the returned object
34
+ # # does not match the 'returns' declaration in the Controller,
35
+ # # or if there is no 'returns' declaration for the returned
36
+ # # HTTP status code
37
+ # expect(response).to match_declared_responses
38
+ # end
39
+ # end
40
+ #
41
+ #
42
+ # ===================================
43
+ # Auto-validation
44
+ # ===================================
45
+ # To use auto-validation, at the beginning of the block in which you want to turn on validation:
46
+ # -) turn on view rendering (by stating 'render_views')
47
+ # -) turn on response validation by stating 'auto_validate_rendered_views'
48
+ #
49
+ # For example, assume the file 'my_controller_spec.rb':
50
+ #
51
+ # require 'apipie/rspec/response_validation_helper'
52
+ #
53
+ # RSpec.describe MyController, :type => :controller, :show_in_doc => true do
54
+ #
55
+ # describe "GET stuff with response validation" do
56
+ # render_views
57
+ # auto_validate_rendered_views
58
+ #
59
+ # it "does something" do
60
+ # get :index, {format: :json}
61
+ # end
62
+ # it "does something else" do
63
+ # get :another_index, {format: :json}
64
+ # end
65
+ # end
66
+ #
67
+ # describe "GET stuff without response validation" do
68
+ # it "does something" do
69
+ # get :index, {format: :json}
70
+ # end
71
+ # it "does something else" do
72
+ # get :another_index, {format: :json}
73
+ # end
74
+ # end
75
+ #
76
+ #
77
+ # Once this is done, responses from http operations ('get', 'post', 'delete', etc.)
78
+ # will fail the test if the response structure does not match the 'returns' declaration
79
+ # on the method (for the actual HTTP status code), or if there is no 'returns' declaration
80
+ # for the HTTP status code.
81
+ #----------------------------------------------------------------------------------------------
82
+
83
+
84
+ #----------------------------------------------------------------------------------------------
85
+ # Response validation: core logic (used by auto-validation and manual-validation mechanisms)
86
+ #----------------------------------------------------------------------------------------------
87
+ class ActionController::Base
88
+ module Apipie::ControllerValidationHelpers
89
+ # this method is injected into ActionController::Base in order to
90
+ # get access to the names of the current controller, current action, as well as to the response
91
+ def schema_validation_errors_for_response
92
+ unprocessed_schema = Apipie::json_schema_for_method_response(controller_name, action_name, response.code, true)
93
+
94
+ if unprocessed_schema.nil?
95
+ err = "no schema defined for #{controller_name}##{action_name}[#{response.code}]"
96
+ return [nil, [err], RuntimeError.new(err)]
97
+ end
98
+
99
+ schema = JSON.parse(JSON(unprocessed_schema))
100
+
101
+ error_list = JSON::Validator.fully_validate(schema, response.body, :strict => false, :version => :draft4, :json => true)
102
+
103
+ error_object = Apipie::ResponseDoesNotMatchSwaggerSchema.new(controller_name, action_name, response.code, error_list, schema, response.body)
104
+
105
+ [schema, error_list, error_object]
106
+ rescue Apipie::NoDocumentedMethod
107
+ [nil, [], nil]
108
+ end
109
+ end
110
+
111
+ include Apipie::ControllerValidationHelpers
112
+ end
113
+
114
+ module Apipie
115
+ def self.print_validation_errors(validation_errors, schema, response, error_object=nil)
116
+ Rails.logger.warn(validation_errors.to_s)
117
+ if Rails.env.test?
118
+ puts "schema validation errors:"
119
+ validation_errors.each { |e| puts "--> #{e.to_s}" }
120
+ puts "schema: #{schema.nil? ? '<none>' : JSON(schema)}"
121
+ puts "response: #{response.body}"
122
+ raise error_object if error_object
123
+ end
124
+ end
125
+ end
126
+
127
+ #---------------------------------
128
+ # Manual-validation (RSpec matcher)
129
+ #---------------------------------
130
+ RSpec::Matchers.define :match_declared_responses do
131
+ match do |actual|
132
+ (schema, validation_errors) = subject.send(:schema_validation_errors_for_response)
133
+ valid = (validation_errors == [])
134
+ Apipie::print_validation_errors(validation_errors, schema, response) unless valid
135
+
136
+ valid
137
+ end
138
+ end
139
+
140
+
141
+ #---------------------------------
142
+ # Auto-validation logic
143
+ #---------------------------------
144
+ module RSpec::Rails::ViewRendering
145
+ # Augment the RSpec DSL
146
+ module ClassMethods
147
+ def auto_validate_rendered_views
148
+ before do
149
+ @is_response_validation_on = true
150
+ end
151
+
152
+ after do
153
+ @is_response_validation_on = false
154
+ end
155
+ end
156
+ end
157
+ end
158
+
159
+
160
+ ActionController::TestCase::Behavior.instance_eval do
161
+ # instrument the 'process' method in ActionController::TestCase to enable response validation
162
+ module Apipie::ResponseValidationHelpers
163
+ @is_response_validation_on = false
164
+ def process(*args)
165
+ result = super(*args)
166
+ validate_response if @is_response_validation_on
167
+
168
+ result
169
+ end
170
+
171
+ def validate_response
172
+ controller.send(:validate_response_and_abort_with_info_if_errors)
173
+ end
174
+ end
175
+
176
+ prepend Apipie::ResponseValidationHelpers
177
+ end
178
+
179
+
180
+ class ActionController::Base
181
+ module Apipie::ControllerValidationHelpers
182
+ def validate_response_and_abort_with_info_if_errors
183
+
184
+ (schema, validation_errors, error_object) = schema_validation_errors_for_response
185
+
186
+ valid = (validation_errors == [])
187
+ if !valid
188
+ Apipie::print_validation_errors(validation_errors, schema, response, error_object)
189
+ end
190
+ end
191
+ end
192
+ end
193
+
194
+
@@ -24,6 +24,10 @@ module Apipie
24
24
  Apipie.configuration.swagger_json_input_uses_refs
25
25
  end
26
26
 
27
+ def responses_use_reference?
28
+ Apipie.configuration.swagger_responses_use_refs?
29
+ end
30
+
27
31
  def include_warning_tags?
28
32
  Apipie.configuration.swagger_include_warning_tags
29
33
  end
@@ -259,6 +263,10 @@ module Apipie
259
263
  remove_colons method.resource.controller.name + "::" + method.method
260
264
  end
261
265
 
266
+ def swagger_id_for_typename(typename)
267
+ typename
268
+ end
269
+
262
270
  def swagger_op_id_for_path(http_method, path)
263
271
  # using lowercase http method, because the 'swagger-codegen' tool outputs
264
272
  # strange method names if the http method is in uppercase
@@ -334,19 +342,42 @@ module Apipie
334
342
  # Responses
335
343
  #--------------------------------------------------------------------------
336
344
 
337
- def response_schema(response)
345
+ def json_schema_for_method_response(method, return_code, allow_nulls)
346
+ @definitions = {}
347
+ for response in method.returns
348
+ if response.code.to_s == return_code.to_s
349
+ schema = response_schema(response, allow_nulls) if response.code.to_s == return_code.to_s
350
+ schema[:definitions] = @definitions if @definitions != {}
351
+ return schema
352
+ end
353
+ end
354
+ nil
355
+ end
356
+
357
+ def json_schema_for_self_describing_class(cls, allow_nulls)
358
+ adapter = ResponseDescriptionAdapter.from_self_describing_class(cls)
359
+ response_schema(adapter, allow_nulls)
360
+ end
361
+
362
+ def response_schema(response, allow_nulls=false)
338
363
  begin
339
364
  # no need to warn about "missing default value for optional param" when processing response definitions
340
365
  prev_value = @disable_default_value_warning
341
366
  @disable_default_value_warning = true
342
- schema = json_schema_obj_from_params_array(response.params_ordered)
367
+
368
+ if responses_use_reference? && response.typename
369
+ schema = {"$ref" => gen_referenced_block_from_params_array(swagger_id_for_typename(response.typename), response.params_ordered, allow_nulls)}
370
+ else
371
+ schema = json_schema_obj_from_params_array(response.params_ordered, allow_nulls)
372
+ end
373
+
343
374
  ensure
344
375
  @disable_default_value_warning = prev_value
345
376
  end
346
377
 
347
378
  if response.is_array? && schema
348
379
  schema = {
349
- type: "array",
380
+ type: allow_nulls ? ["array","null"] : "array",
350
381
  items: schema
351
382
  }
352
383
  end
@@ -423,7 +454,7 @@ module Apipie
423
454
  # The core routine for creating a swagger parameter definition block.
424
455
  # The output is slightly different when the parameter is inside a schema block.
425
456
  #--------------------------------------------------------------------------
426
- def swagger_atomic_param(param_desc, in_schema, name)
457
+ def swagger_atomic_param(param_desc, in_schema, name, allow_nulls)
427
458
  def save_field(entry, openapi_key, v, apipie_key=openapi_key, translate=false)
428
459
  if v.key?(apipie_key)
429
460
  if translate
@@ -444,7 +475,7 @@ module Apipie
444
475
  end
445
476
 
446
477
  if swagger_def[:type] == "array"
447
- swagger_def[:items] = {type: "string"} # TODO: add support for arrays of non-string items
478
+ swagger_def[:items] = {type: "string"}
448
479
  end
449
480
 
450
481
  if swagger_def[:type] == "enum"
@@ -457,6 +488,21 @@ module Apipie
457
488
  warn_hash_without_internal_typespec(param_desc.name)
458
489
  end
459
490
 
491
+ if param_desc.is_array?
492
+ new_swagger_def = {
493
+ items: swagger_def,
494
+ type: 'array'
495
+ }
496
+ swagger_def = new_swagger_def
497
+ if allow_nulls
498
+ swagger_def[:type] = [swagger_def[:type], "null"]
499
+ end
500
+ end
501
+
502
+ if allow_nulls
503
+ swagger_def[:type] = [swagger_def[:type], "null"]
504
+ end
505
+
460
506
  if !in_schema
461
507
  swagger_def[:in] = param_desc.options.fetch(:in, @default_value_for_param_in)
462
508
  swagger_def[:required] = param_desc.required if param_desc.required
@@ -487,8 +533,8 @@ module Apipie
487
533
  end
488
534
 
489
535
 
490
- def json_schema_obj_from_params_array(params_array)
491
- (param_defs, required_params) = json_schema_param_defs_from_params_array(params_array)
536
+ def json_schema_obj_from_params_array(params_array, allow_nulls = false)
537
+ (param_defs, required_params) = json_schema_param_defs_from_params_array(params_array, allow_nulls)
492
538
 
493
539
  result = {type: "object"}
494
540
  result[:properties] = param_defs
@@ -498,17 +544,17 @@ module Apipie
498
544
  param_defs.length > 0 ? result : nil
499
545
  end
500
546
 
501
- def gen_referenced_block_from_params_array(name, params_array)
547
+ def gen_referenced_block_from_params_array(name, params_array, allow_nulls=false)
502
548
  return ref_to(:name) if @definitions.key(:name)
503
549
 
504
- schema_obj = json_schema_obj_from_params_array(params_array)
550
+ schema_obj = json_schema_obj_from_params_array(params_array, allow_nulls)
505
551
  return nil if schema_obj.nil?
506
552
 
507
553
  @definitions[name.to_sym] = schema_obj
508
554
  ref_to(name.to_sym)
509
555
  end
510
556
 
511
- def json_schema_param_defs_from_params_array(params_array)
557
+ def json_schema_param_defs_from_params_array(params_array, allow_nulls = false)
512
558
  param_defs = {}
513
559
  required_params = []
514
560
 
@@ -526,7 +572,7 @@ module Apipie
526
572
  param_type = swagger_param_type(param_desc)
527
573
 
528
574
  if param_type == "object" && param_desc.validator.params_ordered
529
- schema = json_schema_obj_from_params_array(param_desc.validator.params_ordered)
575
+ schema = json_schema_obj_from_params_array(param_desc.validator.params_ordered, allow_nulls)
530
576
  if param_desc.additional_properties
531
577
  schema[:additionalProperties] = true
532
578
  end
@@ -539,9 +585,18 @@ module Apipie
539
585
  schema = new_schema
540
586
  end
541
587
 
588
+ if allow_nulls
589
+ # ideally we would write schema[:type] = ["object", "null"]
590
+ # but due to a bug in the json-schema gem, we need to use anyOf
591
+ # see https://github.com/ruby-json-schema/json-schema/issues/404
592
+ new_schema = {
593
+ anyOf: [schema, {type: "null"}]
594
+ }
595
+ schema = new_schema
596
+ end
542
597
  param_defs[param_desc.name.to_sym] = schema if !schema.nil?
543
598
  else
544
- param_defs[param_desc.name.to_sym] = swagger_atomic_param(param_desc, true, nil)
599
+ param_defs[param_desc.name.to_sym] = swagger_atomic_param(param_desc, true, nil, allow_nulls)
545
600
  end
546
601
  end
547
602
 
@@ -619,7 +674,7 @@ module Apipie
619
674
  warn_param_ignored_in_form_data(desc.name)
620
675
  end
621
676
  else
622
- param_entry = swagger_atomic_param(desc, false, name)
677
+ param_entry = swagger_atomic_param(desc, false, name, false)
623
678
  if param_entry[:required]
624
679
  swagger_params_array.unshift(param_entry)
625
680
  else
@@ -398,6 +398,27 @@ module Apipie
398
398
  end
399
399
  end
400
400
 
401
+ class DecimalValidator < BaseValidator
402
+
403
+ def validate(value)
404
+ self.class.validate(value)
405
+ end
406
+
407
+ def self.build(param_description, argument, options, block)
408
+ if argument == :decimal
409
+ self.new(param_description)
410
+ end
411
+ end
412
+
413
+ def description
414
+ "Must be a decimal number."
415
+ end
416
+
417
+ def self.validate(value)
418
+ value.to_s =~ /\A^[-+]?[0-9]+([,.][0-9]+)?\Z$/
419
+ end
420
+ end
421
+
401
422
  class NumberValidator < BaseValidator
402
423
 
403
424
  def validate(value)
@@ -414,6 +435,10 @@ module Apipie
414
435
  "Must be a number."
415
436
  end
416
437
 
438
+ def expected_type
439
+ 'numeric'
440
+ end
441
+
417
442
  def self.validate(value)
418
443
  value.to_s =~ /\A(0|[1-9]\d*)\Z$/
419
444
  end
@@ -1,3 +1,3 @@
1
1
  module Apipie
2
- VERSION = '0.5.8'
2
+ VERSION = '0.5.9'
3
3
  end
@@ -140,6 +140,10 @@ class PetsController < ApplicationController
140
140
  param_group :pet_history
141
141
  end
142
142
  end
143
+ returns :code => 204 do
144
+ property :int_array, :array_of => Integer
145
+ property :enum_array, :array_of => ['v1','v2','v3']
146
+ end
143
147
  returns :code => :unprocessable_entity, :desc => "Fleas were discovered on the pet" do
144
148
  param_group :pet
145
149
  property :num_fleas, Integer, :desc => "Number of fleas on this pet"
@@ -244,6 +248,12 @@ class PetsController < ApplicationController
244
248
  render :json => result
245
249
  end
246
250
 
251
+ #-----------------------------------------------------------
252
+ # A method with no documentation
253
+ #-----------------------------------------------------------
254
+ def undocumented_method
255
+ render :json => {:result => "ok"}
256
+ end
247
257
 
248
258
  #-----------------------------------------------------------
249
259
  # A method which has a response with a missing field
@@ -335,7 +345,6 @@ class PetsController < ApplicationController
335
345
  render :json => result
336
346
  end
337
347
 
338
-
339
348
  #=======================================================================
340
349
  # Methods for testing array field responses
341
350
  #=======================================================================
@@ -26,6 +26,22 @@ Dummy::Application.routes.draw do
26
26
  get :contributors
27
27
  end
28
28
  end
29
+
30
+ get "/pets/return_and_validate_expected_response" => "pets#return_and_validate_expected_response"
31
+ get "/pets/return_and_validate_expected_array_response" => "pets#return_and_validate_expected_array_response"
32
+ get "/pets/return_and_validate_type_mismatch" => "pets#return_and_validate_type_mismatch"
33
+ get "/pets/return_and_validate_missing_field" => "pets#return_and_validate_missing_field"
34
+ get "/pets/return_and_validate_extra_property" => "pets#return_and_validate_extra_property"
35
+ get "/pets/return_and_validate_allowed_extra_property" => "pets#return_and_validate_allowed_extra_property"
36
+ get "/pets/sub_object_invalid_extra_property" => "pets#sub_object_invalid_extra_property"
37
+ get "/pets/sub_object_allowed_extra_property" => "pets#sub_object_allowed_extra_property"
38
+ get "/pets/return_and_validate_unexpected_array_response" => "pets#return_and_validate_unexpected_array_response"
39
+ get "/pets/return_and_validate_expected_response_with_null" => "pets#return_and_validate_expected_response_with_null"
40
+ get "/pets/return_and_validate_expected_response_with_null_object" => "pets#return_and_validate_expected_response_with_null_object"
41
+
42
+ get "/pets/returns_response_with_valid_array" => "pets#returns_response_with_valid_array"
43
+ get "/pets/returns_response_with_invalid_array" => "pets#returns_response_with_invalid_array"
44
+ get "/pets/undocumented_method" => "pets#undocumented_method"
29
45
  end
30
46
 
31
47
  apipie
@@ -0,0 +1,104 @@
1
+ require 'spec_helper'
2
+ require 'rack/utils'
3
+ require 'rspec/expectations'
4
+ require 'apipie/rspec/response_validation_helper'
5
+ require "json-schema"
6
+
7
+ RSpec.describe PetsController, :type => :controller do
8
+ before :each do
9
+ Apipie.configuration.swagger_allow_additional_properties_in_response = false
10
+ end
11
+
12
+ it "does not raise error when rendered output matches the described response" do
13
+ response = get :return_and_validate_expected_response, {format: :json}
14
+ expect(response).to match_declared_responses
15
+ end
16
+
17
+ it "does not raise error when rendered output (array) matches the described response" do
18
+ response = get :return_and_validate_expected_array_response, {format: :json}
19
+ expect(response).to match_declared_responses
20
+ end
21
+
22
+ it "does not raises error when rendered output includes null in the response" do
23
+ response = get :return_and_validate_expected_response_with_null, {format: :json}
24
+ expect(response).to match_declared_responses
25
+ end
26
+
27
+ it "does not raise error when rendered output includes null (instead of an object) in the response" do
28
+ response = get :return_and_validate_expected_response_with_null_object, {format: :json}
29
+ expect(response).to match_declared_responses
30
+ end
31
+
32
+ it "raises error when a response field has the wrong type" do
33
+ response = get :return_and_validate_type_mismatch, {format: :json}
34
+ expect(response).not_to match_declared_responses
35
+ end
36
+
37
+ it "raises error when a response has a missing field" do
38
+ response = get :return_and_validate_missing_field, {format: :json}
39
+ expect(response).not_to match_declared_responses
40
+ end
41
+
42
+ it "raises error when a response has an extra property and 'swagger_allow_additional_properties_in_response' is false" do
43
+ response = get :return_and_validate_extra_property, {format: :json}
44
+ expect(response).not_to match_declared_responses
45
+ end
46
+
47
+ it "raises error when a response has is array instead of object" do
48
+ # note: this action returns HTTP 201, not HTTP 200!
49
+ response = get :return_and_validate_unexpected_array_response, {format: :json}
50
+ expect(response).not_to match_declared_responses
51
+ end
52
+
53
+ it "does not raise error when a response has an extra property and 'swagger_allow_additional_properties_in_response' is true" do
54
+ Apipie.configuration.swagger_allow_additional_properties_in_response = true
55
+ response = get :return_and_validate_extra_property, {format: :json}
56
+ expect(response).to match_declared_responses
57
+ end
58
+
59
+ it "does not raise error when a response has an extra field and 'additional_properties' is specified in the response" do
60
+ Apipie.configuration.swagger_allow_additional_properties_in_response = false
61
+ response = get :return_and_validate_allowed_extra_property, {format: :json}
62
+ expect(response).to match_declared_responses
63
+ end
64
+
65
+ it "raises error when a response sub-object has an extra field and 'additional_properties' is not specified on it, but specified on the top level of the response" do
66
+ Apipie.configuration.swagger_allow_additional_properties_in_response = false
67
+ response = get :sub_object_invalid_extra_property, {format: :json}
68
+ expect(response).not_to match_declared_responses
69
+ end
70
+
71
+ it "does not rais error when a response sub-object has an extra field and 'additional_properties' is specified on it" do
72
+ Apipie.configuration.swagger_allow_additional_properties_in_response = false
73
+ response = get :sub_object_allowed_extra_property, {format: :json}
74
+ expect(response).to match_declared_responses
75
+ end
76
+
77
+ describe "auto validation" do
78
+ auto_validate_rendered_views
79
+ it "raises exception when a response field has the wrong type and auto validation is turned on" do
80
+ expect { get :return_and_validate_type_mismatch, {format: :json} }.to raise_error(Apipie::ResponseDoesNotMatchSwaggerSchema)
81
+ end
82
+
83
+ it "does not raise an exception when calling an undocumented method" do
84
+ expect { get :undocumented_method, {format: :json} }.not_to raise_error
85
+ end
86
+
87
+ end
88
+
89
+
90
+ describe "with array field" do
91
+ it "no error for valid response" do
92
+ response = get :returns_response_with_valid_array, {format: :json}
93
+ expect(response).to match_declared_responses
94
+ end
95
+
96
+ it "error if type of element in the array is wrong" do
97
+ response = get :returns_response_with_invalid_array, {format: :json}
98
+ expect(response).not_to match_declared_responses
99
+ end
100
+ end
101
+
102
+
103
+
104
+ end
@@ -12,8 +12,22 @@ describe "Swagger Responses" do
12
12
 
13
13
  let(:controller_class ) { described_class }
14
14
 
15
+ def get_ref(ref)
16
+ name = ref.split('#/definitions/')[1].to_sym
17
+ swagger[:definitions][name]
18
+ end
19
+
20
+ def resolve_refs(schema)
21
+ if schema['$ref']
22
+ return get_ref(schema['$ref'])
23
+ end
24
+ schema
25
+ end
26
+
15
27
  def swagger_response_for(path, code=200, method='get')
16
- swagger[:paths][path][method][:responses][code]
28
+ response = swagger[:paths][path][method][:responses][code]
29
+ response[:schema] = resolve_refs(response[:schema])
30
+ response
17
31
  end
18
32
 
19
33
  def swagger_params_for(path, method='get')
@@ -32,6 +46,64 @@ describe "Swagger Responses" do
32
46
 
33
47
 
34
48
 
49
+
50
+ #
51
+ # Matcher to validate the hierarchy of fields described in an internal 'returns' object (without checking their type)
52
+ #
53
+ # For example, code such as:
54
+ # returns_obj = Apipie.get_resource_description(...)._methods.returns.detect{|e| e.code=200})
55
+ # expect(returns_obj).to match_param_structure([:pet_name, :animal_type, :pet_measurements => [:weight, :height]])
56
+ #
57
+ # will verify that the payload structure described for the response of return code 200 is:
58
+ # {
59
+ # "pet_name": <any>,
60
+ # "animal_type": <any>,
61
+ # "pet_measurements": {
62
+ # "weight": <any>,
63
+ # "height": <any>
64
+ # }
65
+ # }
66
+ #
67
+ #
68
+ RSpec::Matchers.define :match_field_structure do |expected|
69
+ @last_message = nil
70
+
71
+ match do |actual|
72
+ deep_match?(actual, expected)
73
+ end
74
+
75
+ def deep_match?(actual, expected, breadcrumb=[])
76
+ num = 0
77
+ for pdesc in expected do
78
+ if pdesc.is_a? Symbol
79
+ return false unless fields_match?(actual.params_ordered[num], pdesc, breadcrumb)
80
+ elsif pdesc.is_a? Hash
81
+ return false unless fields_match?(actual.params_ordered[num], pdesc.keys[0], breadcrumb)
82
+ return false unless deep_match?(actual.params_ordered[num].validator, pdesc.values[0], breadcrumb + [pdesc.keys[0]])
83
+ end
84
+ num+=1
85
+ end
86
+ @fail_message = "expected property count#{breadcrumb == [] ? '' : ' of ' + (breadcrumb).join('.')} (#{actual.params_ordered.count}) to be #{num}"
87
+ actual.params_ordered.count == num
88
+ end
89
+
90
+ def fields_match?(param, expected_name, breadcrumb)
91
+ return false unless have_field?(param, expected_name, breadcrumb)
92
+ @fail_message = "expected #{(breadcrumb + [param.name]).join('.')} to eq #{(breadcrumb + [expected_name]).join('.')}"
93
+ param.name.to_s == expected_name.to_s
94
+ end
95
+
96
+ def have_field?(field, expected_name, breadcrumb)
97
+ @fail_message = "expected property #{(breadcrumb+[expected_name]).join('.')}"
98
+ !field.nil?
99
+ end
100
+
101
+ failure_message do |actual|
102
+ @fail_message
103
+ end
104
+ end
105
+
106
+
35
107
  describe PetsController do
36
108
 
37
109
 
@@ -57,7 +129,7 @@ describe "Swagger Responses" do
57
129
  schema = response[:schema]
58
130
  expect(schema[:type]).to eq("array")
59
131
 
60
- a_schema = schema[:items]
132
+ a_schema = resolve_refs(schema[:items])
61
133
  expect(a_schema).to have_field(:pet_name, 'string', {:description => 'Name of pet', :required => false})
62
134
  expect(a_schema).to have_field(:animal_type, 'string', {:description => 'Type of pet', :enum => ['dog','cat','iguana','kangaroo']})
63
135
  end
@@ -320,6 +392,24 @@ describe "Swagger Responses" do
320
392
  expect(pai_schema).to have_field(:avg_meals_per_day, 'number')
321
393
  end
322
394
 
395
+ it "should return code 204 with array of integer" do
396
+ returns_obj = subject.returns.detect{|e| e.code == 204 }
397
+
398
+ puts returns_obj.to_json
399
+ expect(returns_obj.code).to eq(204)
400
+ expect(returns_obj.is_array?).to eq(false)
401
+
402
+ expect(returns_obj).to match_field_structure([:int_array, :enum_array])
403
+ end
404
+ it 'should have the 204 response described in the swagger' do
405
+ response = swagger_response_for('/pets/{id}/extra_info', 204)
406
+
407
+ schema = response[:schema]
408
+ expect(schema).to have_field(:int_array, 'array', {items: {type: 'number'}})
409
+ expect(schema).to have_field(:enum_array, 'array', {items: {type: 'string', enum: ['v1','v2','v3']}})
410
+ end
411
+
412
+
323
413
  it "should return code matching :unprocessable_entity (422) with spread out 'pet' and 'num_fleas'" do
324
414
  returns_obj = subject.returns.detect{|e| e.code == 422 }
325
415
 
@@ -42,6 +42,12 @@ describe Apipie::Validator do
42
42
 
43
43
  end
44
44
 
45
+ describe 'NumberValidator' do
46
+ it 'should expect a Numeric type' do
47
+ validator = Apipie::Validator::BaseValidator.find(params_desc, :number, nil, nil)
48
+ expect(validator.expected_type).to eq('numeric')
49
+ end
50
+ end
45
51
  end
46
52
 
47
53
  describe 'ArrayClassValidator' do
@@ -34,62 +34,6 @@ module Rails4Compatibility
34
34
  end
35
35
 
36
36
 
37
- #
38
- # Matcher to validate the hierarchy of fields described in an internal 'returns' object (without checking their type)
39
- #
40
- # For example, code such as:
41
- # returns_obj = Apipie.get_resource_description(...)._methods.returns.detect{|e| e.code=200})
42
- # expect(returns_obj).to match_param_structure([:pet_name, :animal_type, :pet_measurements => [:weight, :height]])
43
- #
44
- # will verify that the payload structure described for the response of return code 200 is:
45
- # {
46
- # "pet_name": <any>,
47
- # "animal_type": <any>,
48
- # "pet_measurements": {
49
- # "weight": <any>,
50
- # "height": <any>
51
- # }
52
- # }
53
- #
54
- #
55
- RSpec::Matchers.define :match_field_structure do |expected|
56
- @last_message = nil
57
-
58
- match do |actual|
59
- deep_match?(actual, expected)
60
- end
61
-
62
- def deep_match?(actual, expected, breadcrumb=[])
63
- num = 0
64
- expected.each do |pdesc|
65
- if pdesc.is_a? Symbol
66
- return false unless matching_param(actual.params_ordered, pdesc, breadcrumb)
67
- elsif pdesc.is_a? Hash
68
- param = matching_param(actual.params_ordered, pdesc.keys[0], breadcrumb)
69
- return false unless param
70
- return false unless deep_match?(param.validator, pdesc.values[0], breadcrumb + [pdesc.keys[0]])
71
- end
72
- num+=1
73
- end
74
- @fail_message = "expected property count#{breadcrumb == [] ? '' : ' of ' + (breadcrumb).join('.')} (#{actual.params_ordered.count}) to be #{num}"
75
- actual.params_ordered.count == num
76
- end
77
-
78
- def matching_param(params, expected_name, breadcrumb)
79
- param = params.find { |p| p.name.to_s == expected_name.to_s }
80
- unless param
81
- @fail_message = "expected [#{ params.map(&:name).join(', ') }] to include #{(breadcrumb + [expected_name]).join('.')}"
82
- end
83
- param
84
- end
85
-
86
- failure_message do |actual|
87
- @fail_message
88
- end
89
- end
90
-
91
-
92
-
93
37
  #
94
38
  # Matcher to validate the properties (name, type and options) of a single field in the
95
39
  # internal representation of a swagger schema
@@ -112,12 +56,14 @@ RSpec::Matchers.define :have_field do |name, type, opts={}|
112
56
  @fail_message
113
57
  end
114
58
 
115
- match do |actual|
59
+ match do |unresolved|
60
+ actual = resolve_refs(unresolved)
116
61
  return fail("expected schema to have type 'object' (got '#{actual[:type]}')") if (actual[:type]) != 'object'
117
62
  return fail("expected schema to include param named '#{name}' (got #{actual[:properties].keys})") if (prop = actual[:properties][name]).nil?
118
63
  return fail("expected param '#{name}' to have type '#{type}' (got '#{prop[:type]}')") if prop[:type] != type
119
64
  return fail("expected param '#{name}' to have description '#{opts[:description]}' (got '#{prop[:description]}')") if opts[:description] && prop[:description] != opts[:description]
120
65
  return fail("expected param '#{name}' to have enum '#{opts[:enum]}' (got #{prop[:enum]})") if opts[:enum] && prop[:enum] != opts[:enum]
66
+ return fail("expected param '#{name}' to have items '#{opts[:items]}' (got #{prop[:items]})") if opts[:items] && prop[:items] != opts[:items]
121
67
  if !opts.include?(:required) || opts[:required] == true
122
68
  return fail("expected param '#{name}' to be required") unless actual[:required].include?(name)
123
69
  else
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: apipie-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.8
4
+ version: 0.5.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pavel Pokorny
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2018-04-27 00:00:00.000000000 Z
12
+ date: 2018-06-29 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -225,6 +225,7 @@ files:
225
225
  - lib/apipie/response_description_adapter.rb
226
226
  - lib/apipie/routes_formatter.rb
227
227
  - lib/apipie/routing.rb
228
+ - lib/apipie/rspec/response_validation_helper.rb
228
229
  - lib/apipie/see_description.rb
229
230
  - lib/apipie/static_dispatcher.rb
230
231
  - lib/apipie/swagger_generator.rb
@@ -302,6 +303,7 @@ files:
302
303
  - spec/lib/resource_description_spec.rb
303
304
  - spec/lib/swagger/openapi_2_0_schema.json
304
305
  - spec/lib/swagger/rake_swagger_spec.rb
306
+ - spec/lib/swagger/response_validation_spec.rb
305
307
  - spec/lib/swagger/swagger_dsl_spec.rb
306
308
  - spec/lib/validator_spec.rb
307
309
  - spec/lib/validators/array_validator_spec.rb
@@ -395,6 +397,7 @@ test_files:
395
397
  - spec/lib/resource_description_spec.rb
396
398
  - spec/lib/swagger/openapi_2_0_schema.json
397
399
  - spec/lib/swagger/rake_swagger_spec.rb
400
+ - spec/lib/swagger/response_validation_spec.rb
398
401
  - spec/lib/swagger/swagger_dsl_spec.rb
399
402
  - spec/lib/validator_spec.rb
400
403
  - spec/lib/validators/array_validator_spec.rb