apipie-rails 0.5.8 → 0.5.9

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