diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 08ebed68..d2c14bdb 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -13,7 +13,7 @@ Metrics/AbcSize: # Offense count: 1 # Configuration parameters: CountComments. Metrics/ClassLength: - Max: 353 + Max: 230 # Offense count: 6 Metrics/CyclomaticComplexity: @@ -32,7 +32,7 @@ Metrics/MethodLength: # Offense count: 1 # Configuration parameters: CountComments. Metrics/ModuleLength: - Max: 394 + Max: 200 # Offense count: 4 Metrics/PerceivedComplexity: @@ -51,7 +51,6 @@ Style/Documentation: # Offense count: 2 Style/DoubleNegation: Exclude: - - 'lib/grape-swagger/doc_methods.rb' # Offense count: 3 # Configuration parameters: Exclude. @@ -65,7 +64,6 @@ Style/FileName: # Configuration parameters: NamePrefix, NamePrefixBlacklist. Style/PredicateName: Exclude: - - 'lib/grape-swagger/doc_methods.rb' # Offense count: 4 # Cop supports --auto-correct. @@ -73,4 +71,3 @@ Style/PredicateName: Style/RegexpLiteral: Exclude: - 'lib/grape-swagger.rb' - - 'lib/grape-swagger/doc_methods.rb' diff --git a/CHANGELOG.md b/CHANGELOG.md index abf9e732..9db049b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ -n.n.n / 2016-02-14 + +n.n.n / 2016-03-16 ================== +[#356](https://github.com/ruby-grape/grape-swagger/pull/356) + +- adds `consumes` setting +- refactoring + +[#354](https://github.com/ruby-grape/grape-swagger/pull/354) some improvements + +- fixes setting of `base_path` and `host`; +- adds possibility to configure the setting of `version` and `base_path` in documented path; +- adds `operationId` + +[#353](https://github.com/ruby-grape/grape-swagger/pull/353) resolves issue #352 ### 0.10.4 (Next) diff --git a/README.md b/README.md index 4ab26d31..24007fdb 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,7 @@ end ## Configure [host](#host) +[base_path](#base_path) [mount_path](#mount_path) [add_base_path](#add_base_path) [add_version](#add_version) @@ -337,6 +338,8 @@ desc 'Get all kittens!', { entity: Entities::Kitten, # or success http_codes: [[401, 'KittenBitesError', Entities::BadKitten]] # or failure # also explicit as hash: [{ code: 401, mssage: 'KittenBitesError', model: Entities::BadKitten }] + produces: [ "array", "of", "mime_types" ], + consumes: [ "array", "of", "mime_types" ] } get '/kittens' do ``` diff --git a/lib/grape-swagger.rb b/lib/grape-swagger.rb index d7b01fe5..1c663c59 100644 --- a/lib/grape-swagger.rb +++ b/lib/grape-swagger.rb @@ -2,10 +2,19 @@ require 'grape-swagger/version' require 'grape-swagger/endpoint' require 'grape-swagger/errors' -require 'grape-swagger/doc_methods/produces' + +# TODO: loading should be simplified by using a better/more clever dependincy structure +require 'grape-swagger/doc_methods/produces_consumes' require 'grape-swagger/doc_methods/data_type' require 'grape-swagger/doc_methods/extensions' +require 'grape-swagger/doc_methods/operation_id' +require 'grape-swagger/doc_methods/optional_object' +require 'grape-swagger/doc_methods/path_string' +require 'grape-swagger/doc_methods/tag_name_description' +require 'grape-swagger/doc_methods/parse_params' + require 'grape-swagger/doc_methods' + require 'grape-swagger/markdown/kramdown_adapter' require 'grape-swagger/markdown/redcarpet_adapter' diff --git a/lib/grape-swagger/doc_methods/operation_id.rb b/lib/grape-swagger/doc_methods/operation_id.rb new file mode 100644 index 00000000..1ee0b788 --- /dev/null +++ b/lib/grape-swagger/doc_methods/operation_id.rb @@ -0,0 +1,23 @@ +module GrapeSwagger + module DocMethods + class OperationId + class << self + def build(method, path = nil) + verb = method.to_s.downcase + + unless path.nil? + operation = path.split('/').map(&:capitalize).join + operation.gsub!(/\-(\w)/, &:upcase).delete!('-') if operation.include?('-') + operation.gsub!(/\_(\w)/, &:upcase).delete!('_') if operation.include?('_') + if path.include?('{') + operation.gsub!(/\{(\w)/, &:upcase) + operation.delete!('{').delete!('}') + end + end + + "#{verb}#{operation}" + end + end + end + end +end diff --git a/lib/grape-swagger/doc_methods/optional_object.rb b/lib/grape-swagger/doc_methods/optional_object.rb new file mode 100644 index 00000000..c146204a --- /dev/null +++ b/lib/grape-swagger/doc_methods/optional_object.rb @@ -0,0 +1,15 @@ +module GrapeSwagger + module DocMethods + class OptionalObject + class << self + def build(key, options, request = nil) + if options[key] + options[key].is_a?(Proc) ? options[key].call : options[key] + else + request + end + end + end + end + end +end diff --git a/lib/grape-swagger/doc_methods/parse_params.rb b/lib/grape-swagger/doc_methods/parse_params.rb new file mode 100644 index 00000000..00dfd7c4 --- /dev/null +++ b/lib/grape-swagger/doc_methods/parse_params.rb @@ -0,0 +1,102 @@ +module GrapeSwagger + module DocMethods + class ParseParams + class << self + def call(param, value, route) + @array_items = {} + path = route.route_path + method = route.route_method + + additional_documentation = value.is_a?(Hash) ? value[:documentation] : nil + data_type = GrapeSwagger::DocMethods::DataType.call(value) + + if additional_documentation && value.is_a?(Hash) + value = additional_documentation.merge(value) + end + + description = value.is_a?(Hash) ? value[:desc] || value[:description] : nil + required = value.is_a?(Hash) ? value[:required] : false + default_value = value.is_a?(Hash) ? value[:default] : nil + example = value.is_a?(Hash) ? value[:example] : nil + is_array = value.is_a?(Hash) ? (value[:is_array] || false) : false + values = value.is_a?(Hash) ? value[:values] : nil + name = (value.is_a?(Hash) && value[:full_name]) || param + enum_or_range_values = parse_enum_or_range_values(values) + + value_type = { value: value, data_type: data_type, path: path } + + parsed_params = { + in: param_type(value_type, param, method, is_array), + name: name, + description: description, + type: data_type, + required: required, + allowMultiple: is_array + } + + if GrapeSwagger::DocMethods::DataType::PRIMITIVE_MAPPINGS.key?(data_type) + parsed_params[:type], parsed_params[:format] = GrapeSwagger::DocMethods::DataType::PRIMITIVE_MAPPINGS[data_type] + end + + parsed_params[:items] = @array_items if @array_items.present? + + parsed_params[:defaultValue] = example if example + parsed_params[:defaultValue] = default_value if default_value && example.blank? + + parsed_params.merge!(enum_or_range_values) if enum_or_range_values + parsed_params + end + + def primitive?(type) + %w(object integer long float double string byte boolean date datetime).include? type.to_s.downcase + end + + private + + def param_type(value_type, param, method, is_array) + # TODO: use `value_type.dig():value, :documentation, :param_type)` instead req ruby2.3 + # + if value_type[:value].is_a?(Hash) && + value_type[:value].key?(:documentation) && + value_type[:value][:documentation].key?(:param_type) + + if is_array + @array_items = { 'type' => value_type[:data_type] } + + 'array' + end + else + case + when value_type[:path].include?("{#{param}}") + 'path' + when %w(POST PUT PATCH).include?(method) + primitive?(value_type[:data_type]) ? 'formData' : 'body' + else + 'query' + end + end + end + + def parse_enum_or_range_values(values) + case values + when Range + parse_range_values(values) if values.first.is_a?(Integer) + when Proc + values_result = values.call + if values_result.is_a?(Range) && values_result.first.is_a?(Integer) + parse_range_values(values_result) + else + { enum: values_result } + end + else + { enum: values } if values + end + end + + def parse_range_values(values) + { minimum: values.first, maximum: values.last } + end + end + end + end +end diff --git a/lib/grape-swagger/doc_methods/path_string.rb b/lib/grape-swagger/doc_methods/path_string.rb new file mode 100644 index 00000000..051607a5 --- /dev/null +++ b/lib/grape-swagger/doc_methods/path_string.rb @@ -0,0 +1,29 @@ +module GrapeSwagger + module DocMethods + class PathString + class << self + def build(path, options = {}) + # always removing format + path.sub!(/\(\.\w+?\)$/, '') + path.sub!('(.:format)', '') + + # ... format path params + path.gsub!(/:(\w+)/, '{\1}') + + # set item from path, this could be used for the definitions object + item = path.gsub(%r{/{(.+?)}}, '').split('/').last.singularize.underscore.camelize || 'Item' + + if options[:version] && options[:add_version] + path.sub!('{version}', options[:version]) + else + path.sub!('/{version}', '') + end + + path = "#{GrapeSwagger::DocMethods::OptionalObject.build(:base_path, options)}#{path}" if options[:add_base_path] + + [item, path.start_with?('/') ? path : "/#{path}"] + end + end + end + end +end diff --git a/lib/grape-swagger/doc_methods/produces.rb b/lib/grape-swagger/doc_methods/produces_consumes.rb similarity index 91% rename from lib/grape-swagger/doc_methods/produces.rb rename to lib/grape-swagger/doc_methods/produces_consumes.rb index 5fae1f63..66f4949e 100644 --- a/lib/grape-swagger/doc_methods/produces.rb +++ b/lib/grape-swagger/doc_methods/produces_consumes.rb @@ -1,6 +1,6 @@ module GrapeSwagger module DocMethods - class Produces + class ProducesConsumes class << self def call(*args) return ['application/json'] unless args.flatten.present? diff --git a/lib/grape-swagger/doc_methods/tag_name_description.rb b/lib/grape-swagger/doc_methods/tag_name_description.rb new file mode 100644 index 00000000..9cbc26ff --- /dev/null +++ b/lib/grape-swagger/doc_methods/tag_name_description.rb @@ -0,0 +1,26 @@ +module GrapeSwagger + module DocMethods + class TagNameDescription + class << self + def build(options = {}) + target_class = options[:target_class] + namespaces = target_class.combined_namespaces + namespace_routes = target_class.combined_namespace_routes + + namespace_routes.keys.map do |local_route| + next if namespace_routes[local_route].map(&:route_hidden).all? { |value| value.respond_to?(:call) ? value.call : value } + + original_namespace_name = target_class.combined_namespace_identifiers.key?(local_route) ? target_class.combined_namespace_identifiers[local_route] : local_route + description = namespaces[original_namespace_name] && namespaces[original_namespace_name].options[:desc] + description ||= "Operations about #{original_namespace_name.pluralize}" + + { + name: local_route, + description: description + } + end.compact + end + end + end + end +end diff --git a/lib/grape-swagger/endpoint.rb b/lib/grape-swagger/endpoint.rb index d1ddcf3f..b573c767 100644 --- a/lib/grape-swagger/endpoint.rb +++ b/lib/grape-swagger/endpoint.rb @@ -34,23 +34,13 @@ def swagger_object(target_class, request, options) swagger: '2.0', produces: content_types_for(target_class), authorizations: options[:authorizations], - host: optional_objects(:host, options, request.env['HTTP_HOST']), - basePath: optional_objects(:base_path, options, request.env['SCRIPT_NAME']), - tags: tag_name_description(options), + host: GrapeSwagger::DocMethods::OptionalObject.build(:host, options, request.env['HTTP_HOST']), + basePath: GrapeSwagger::DocMethods::OptionalObject.build(:base_path, options, request.env['SCRIPT_NAME']), + tags: GrapeSwagger::DocMethods::TagNameDescription.build(options), schemes: options[:scheme] }.delete_if { |_, value| value.blank? } end - # helper for swagger object - # gets host and base_path - def optional_objects(key, options, request = nil) - if options[key] - options[key].is_a?(Proc) ? options[key].call : options[key] - else - request - end - end - # building info object def info_object(infos) { @@ -75,9 +65,9 @@ def license_object(infos) # contact def contact_object(infos) { - contact_name: infos.delete(:contact_name), - contact_email: infos.delete(:contact_email), - contact_url: infos.delete(:contact_url) + name: infos.delete(:contact_name), + email: infos.delete(:contact_email), + url: infos.delete(:contact_url) }.delete_if { |_, value| value.blank? } end @@ -106,7 +96,7 @@ def path_item(routes, options) routes.each do |route| next if hidden?(route) - path = path_string(route, options) + @item, path = GrapeSwagger::DocMethods::PathString.build(route.route_path, options) @entity = route.route_entity || route.route_success # ... replacing version params through submitted version @@ -124,54 +114,19 @@ def path_item(routes, options) end end - def path_string(route, options) - path = route.route_path - # always removing format - path.sub!(/\(\.\w+?\)$/, '') - path.sub!('(.:format)', '') - # ... format params - path.gsub!(/:(\w+)/, '{\1}') - - # set item from path, this could be used for the definitions object - @item = path.gsub(%r{/{(.+?)}}, '').split('/').last.singularize.underscore.camelize || 'Item' - - if options[:version] && options[:add_version] - path.sub!('{version}', options[:version]) - else - path.sub!('/{version}', '') - end - - path = "/#{optional_objects(:base_path, options)}#{path}" if options[:add_base_path] - - path - end - def method_object(route, options, path) method = {} method[:description] = description_object(route, options[:markdown]) method[:headers] = route.route_headers if route.route_headers - method[:produces] = produces_object(route, options) + method[:produces] = produces_object(route, options[:produces] || options[:format]) + method[:consumes] = consumes_object(route, options[:format]) method[:parameters] = params_object(route) method[:responses] = response_object(route) method[:tags] = tag_object(route, options[:version]) - method[:operationId] = operation_id_object(route.route_method, path) + method[:operationId] = GrapeSwagger::DocMethods::OperationId.build(route.route_method, path) method.delete_if { |_, value| value.blank? } end - def operation_id_object(method, path = nil) - verb = method.to_s.downcase - unless path.nil? - operation = path.split('/').map(&:capitalize).join - operation.gsub!(/\-(\w)/, &:upcase).delete!('-') if operation.include?('-') - operation.gsub!(/\_(\w)/, &:upcase).delete!('_') if operation.include?('_') - if path.include?('{') - operation.gsub!(/\{(\w)/, &:upcase) - operation.delete!('{').delete!('}') - end - end - "#{verb}#{operation}" - end - def description_object(route, markdown) description = route.route_desc if route.route_desc.present? description = route.route_detail if route.route_detail.present? @@ -179,12 +134,21 @@ def description_object(route, markdown) description end - def produces_object(route, options) - mime_types = GrapeSwagger::DocMethods::Produces.call(options[:format]) + def consumes_object(route, format) + method = route.route_method.downcase.to_sym + # require 'pry'; binding.pry if [:post, :put].include?(method) + format = route.route_settings[:description][:consumes] if route.route_settings[:description] && route.route_settings[:description][:consumes] + mime_types = GrapeSwagger::DocMethods::ProducesConsumes.call(format) if [:post, :put].include?(method) + + mime_types + end + + def produces_object(route, format) + mime_types = GrapeSwagger::DocMethods::ProducesConsumes.call(format) route_mime_types = [:route_formats, :route_content_types, :route_produces].map do |producer| possible = route.send(producer) - GrapeSwagger::DocMethods::Produces.call(possible) if possible.present? + GrapeSwagger::DocMethods::ProducesConsumes.call(possible) if possible.present? end.flatten.compact.uniq route_mime_types.present? ? route_mime_types : mime_types @@ -230,7 +194,7 @@ def default_staus_codes def params_object(route) partition_params(route).map do |param, value| value = { required: false }.merge(value) if value.is_a?(Hash) - parse_params(param, value, route.route_path, route.route_method) + GrapeSwagger::DocMethods::ParseParams.call(param, value, route) end end @@ -272,8 +236,12 @@ def parse_response_params(params) params.each_with_object({}) do |x, memo| x[0] = x.last[:as] if x.last[:as] - if x.last[:using].present? || could_it_be_a_model?(x.last) - name = expose_params_from_model(x.last[:using] || x.last[:type]) + + model = x.last[:using] if x.last[:using].present? + model ||= x.last[:documentation][:type] if x.last[:documentation] && could_it_be_a_model?(x.last[:documentation]) + + if model + name = expose_params_from_model(model) memo[x.first] = if x.last[:documentation] && x.last[:documentation][:is_array] { 'type' => 'array', 'items' => { '$ref' => "#/definitions/#{name}" } } else @@ -287,7 +255,7 @@ def parse_response_params(params) end def expose_params_from_model(model) - model_name = model.name.demodulize.camelize + model_name = model.respond_to?(:name) ? model.name.demodulize.camelize : model.split('::').last # DONE: has to be adept, to be ready for grape-entity >0.5.0 # TODO: this should only be a temporary hack ;) @@ -306,10 +274,14 @@ def expose_params_from_model(model) end def could_it_be_a_model?(value) - value[:type] && + ( + value[:type].to_s.include?('Entity') || value[:type].to_s.include?('Entities') + ) || ( + value[:type] && value[:type].is_a?(Class) && - !primitive?(value[:type].name.downcase) && + !GrapeSwagger::DocMethods::ParseParams.primitive?(value[:type].name.downcase) && !value[:type] == Array + ) end def hidden?(route) @@ -320,116 +292,6 @@ def hidden?(route) false end - # original methods - # - def parse_params(param, value, path, method) - @array_items = {} - - additional_documentation = value.is_a?(Hash) ? value[:documentation] : nil - data_type = GrapeSwagger::DocMethods::DataType.call(value) - - if additional_documentation && value.is_a?(Hash) - value = additional_documentation.merge(value) - end - - description = value.is_a?(Hash) ? value[:desc] || value[:description] : nil - required = value.is_a?(Hash) ? value[:required] : false - default_value = value.is_a?(Hash) ? value[:default] : nil - example = value.is_a?(Hash) ? value[:example] : nil - is_array = value.is_a?(Hash) ? (value[:is_array] || false) : false - values = value.is_a?(Hash) ? value[:values] : nil - name = (value.is_a?(Hash) && value[:full_name]) || param - enum_or_range_values = parse_enum_or_range_values(values) - - value_type = { value: value, data_type: data_type, path: path } - - parsed_params = { - in: param_type(value_type, param, method, is_array), - name: name, - description: description, - type: data_type, - required: required, - allowMultiple: is_array - } - - if GrapeSwagger::DocMethods::DataType::PRIMITIVE_MAPPINGS.key?(data_type) - parsed_params[:type], parsed_params[:format] = GrapeSwagger::DocMethods::DataType::PRIMITIVE_MAPPINGS[data_type] - end - - parsed_params[:items] = @array_items if @array_items.present? - - parsed_params[:defaultValue] = example if example - parsed_params[:defaultValue] = default_value if default_value && example.blank? - - parsed_params.merge!(enum_or_range_values) if enum_or_range_values - parsed_params - end - - def param_type(value_type, param, method, is_array) - if value_type[:value].is_a?(Hash) && - value_type[:value].key?(:documentation) && - value_type[:value][:documentation].key?(:param_type) - - if is_array - @array_items = { 'type' => value_type[:data_type] } - - 'array' - end - else - case - when value_type[:path].include?("{#{param}}") - 'path' - when %w(POST PUT PATCH).include?(method) - primitive?(value_type[:data_type]) ? 'formData' : 'body' - else - 'query' - end - end - end - - def parse_enum_or_range_values(values) - case values - when Range - parse_range_values(values) if values.first.is_a?(Integer) - when Proc - values_result = values.call - if values_result.is_a?(Range) && values_result.first.is_a?(Integer) - parse_range_values(values_result) - else - { enum: values_result } - end - else - { enum: values } if values - end - end - - def parse_range_values(values) - { minimum: values.first, maximum: values.last } - end - - def primitive?(type) - %w(object integer long float double string byte boolean date dateTime).include? type - end - - def tag_name_description(options) - target_class = options[:target_class] - namespaces = target_class.combined_namespaces - namespace_routes = target_class.combined_namespace_routes - - namespace_routes.keys.map do |local_route| - next if namespace_routes[local_route].map(&:route_hidden).all? { |value| value.respond_to?(:call) ? value.call : value } - - original_namespace_name = target_class.combined_namespace_identifiers.key?(local_route) ? target_class.combined_namespace_identifiers[local_route] : local_route - description = namespaces[original_namespace_name] && namespaces[original_namespace_name].options[:desc] - description ||= "Operations about #{original_namespace_name.pluralize}" - - { - name: local_route, - description: description - } - end.compact - end - def tag_object(route, version) Array(route.route_path.split('{')[0].split('/').reject(&:empty?).delete_if { |i| ((i == route.route_prefix.to_s) || (i == version)) }.first) end diff --git a/spec/lib/endpoint_spec.rb b/spec/lib/endpoint_spec.rb index 24123258..1b79f011 100644 --- a/spec/lib/endpoint_spec.rb +++ b/spec/lib/endpoint_spec.rb @@ -3,19 +3,4 @@ describe Grape::Endpoint do subject { described_class.new(Grape::Util::InheritableSetting.new, {path: '/', method: :get}) } - describe 'operation_id_object' do - specify do - expect(subject.operation_id_object('GET')).to eql 'get' - expect(subject.operation_id_object('get')).to eql 'get' - expect(subject.operation_id_object(:get)).to eql 'get' - expect(subject.operation_id_object('GET', 'foo')).to eql 'getFoo' - expect(subject.operation_id_object('GET', '/foo')).to eql 'getFoo' - expect(subject.operation_id_object('GET', 'bar/foo')).to eql 'getBarFoo' - expect(subject.operation_id_object('GET', 'bar/foo{id}')).to eql 'getBarFooId' - expect(subject.operation_id_object('GET', '/bar_foo{id}')).to eql 'getBarFooId' - expect(subject.operation_id_object('GET', '/bar-foo{id}')).to eql 'getBarFooId' - expect(subject.operation_id_object('GET', '/simple_test/bar-foo{id}')).to eql 'getSimpleTestBarFooId' - end - end - end diff --git a/spec/lib/operation_id_spec.rb b/spec/lib/operation_id_spec.rb new file mode 100644 index 00000000..38479449 --- /dev/null +++ b/spec/lib/operation_id_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe GrapeSwagger::DocMethods::OperationId do + subject { described_class } + + specify { expect(subject).to eql GrapeSwagger::DocMethods::OperationId } + specify { expect(subject).to respond_to :build } + + describe 'build' do + specify do + expect(subject.build('GET')).to eql 'get' + expect(subject.build('get')).to eql 'get' + expect(subject.build(:get)).to eql 'get' + expect(subject.build('GET', 'foo')).to eql 'getFoo' + expect(subject.build('GET', '/foo')).to eql 'getFoo' + expect(subject.build('GET', 'bar/foo')).to eql 'getBarFoo' + expect(subject.build('GET', 'bar/foo{id}')).to eql 'getBarFooId' + expect(subject.build('GET', '/bar_foo{id}')).to eql 'getBarFooId' + expect(subject.build('GET', '/bar-foo{id}')).to eql 'getBarFooId' + expect(subject.build('GET', '/simple_test/bar-foo{id}')).to eql 'getSimpleTestBarFooId' + end + end + +end diff --git a/spec/lib/optional_object_spec.rb b/spec/lib/optional_object_spec.rb new file mode 100644 index 00000000..0420fa67 --- /dev/null +++ b/spec/lib/optional_object_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe GrapeSwagger::DocMethods::OptionalObject do + subject { described_class } + + specify { expect(subject).to eql GrapeSwagger::DocMethods::OptionalObject } + specify { expect(subject).to respond_to :build } + + describe 'build' do + let(:key) { :bar } + let(:request) { 'somes/request/string' } + + describe 'no option given for key' do + let(:options) { {foo: 'foo' }} + specify do + expect(subject.build(key, options)).to be_nil + expect(subject.build(key, options, request)).to eql request + end + end + + let(:value) { 'some optional value' } + + describe 'option is a string' do + let(:options) { {bar: value }} + specify do + expect(subject.build(key, options)).to eql value + expect(subject.build(key, options, request)).to eql value + end + end + + describe 'option is a proc' do + let(:options) { {bar: -> { value } }} + specify do + expect(subject.build(key, options)).to eql value + expect(subject.build(key, options, request)).to eql value + end + end + end + +end diff --git a/spec/lib/path_string_spec.rb b/spec/lib/path_string_spec.rb new file mode 100644 index 00000000..d068dba0 --- /dev/null +++ b/spec/lib/path_string_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +describe GrapeSwagger::DocMethods::PathString do + subject { described_class } + + specify { expect(subject).to eql GrapeSwagger::DocMethods::PathString } + specify { expect(subject).to respond_to :build } + + describe 'operation_id_object' do + describe 'version' do + describe 'defaults: not given, false' do + let(:options) {{ add_version: false }} + + specify do + expect(subject.build('/thing(.json)', options)).to eql ['Thing', '/thing'] + expect(subject.build('/thing/foo(.json)', options)).to eql ['Foo', '/thing/foo'] + expect(subject.build('/thing(.:format)', options)).to eql ['Thing', '/thing'] + expect(subject.build('/thing/foo(.:format)', options)).to eql ['Foo', '/thing/foo'] + expect(subject.build('/thing/:id', options)).to eql ['Thing', '/thing/{id}'] + expect(subject.build('/thing/foo/:id', options)).to eql ['Foo', '/thing/foo/{id}'] + end + end + + describe 'defaults: given, true' do + let(:options) {{ version: 'v1', add_version: true }} + + specify do + expect(subject.build('/{version}/thing(.json)', options)).to eql ['Thing', '/v1/thing'] + expect(subject.build('/{version}/thing/foo(.json)', options)).to eql ['Foo', '/v1/thing/foo'] + expect(subject.build('/{version}/thing(.:format)', options)).to eql ['Thing', '/v1/thing'] + expect(subject.build('/{version}/thing/foo(.:format)', options)).to eql ['Foo', '/v1/thing/foo'] + expect(subject.build('/{version}/thing/:id', options)).to eql ['Thing', '/v1/thing/{id}'] + expect(subject.build('/{version}/thing/foo/:id', options)).to eql ['Foo', '/v1/thing/foo/{id}'] + end + end + end + end +end diff --git a/spec/doc_methods/produces_spec.rb b/spec/lib/produces_consumes_spec.rb similarity index 97% rename from spec/doc_methods/produces_spec.rb rename to spec/lib/produces_consumes_spec.rb index 262177a5..fbabfbb4 100644 --- a/spec/doc_methods/produces_spec.rb +++ b/spec/lib/produces_consumes_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe GrapeSwagger::DocMethods::Produces do +describe GrapeSwagger::DocMethods::ProducesConsumes do describe ":json (default)" do subject { described_class.call } diff --git a/spec/lib/produces_spec.rb b/spec/lib/produces_spec.rb deleted file mode 100644 index 262177a5..00000000 --- a/spec/lib/produces_spec.rb +++ /dev/null @@ -1,98 +0,0 @@ -require 'spec_helper' - -describe GrapeSwagger::DocMethods::Produces do - describe ":json (default)" do - subject { described_class.call } - - specify do - expect(subject).to eql( ['application/json'] ) - end - end - - describe "accept symbols of" do - describe "single" do - subject { described_class.call(:xml) } - - specify do - expect(subject).to eql( ['application/xml'] ) - end - end - - describe "multiple" do - subject { described_class.call(:xml, :serializable_hash, :json, :binary, :txt) } - - specify do - expect(subject).to eql( [ - 'application/xml', - 'application/json', - 'application/octet-stream', - 'text/plain' - ] ) - end - end - end - - describe "accept mime_types of" do - describe "single" do - subject { described_class.call('application/xml') } - - specify do - expect(subject).to eql( ['application/xml'] ) - end - end - - describe "multiple" do - subject { described_class.call( - 'application/xml', - 'application/json', - 'application/octet-stream', - 'text/plain' - ) } - - specify do - expect(subject).to eql( [ - 'application/xml', - 'application/json', - 'application/octet-stream', - 'text/plain' - ] ) - end - end - end - - describe "mix it up" do - subject { described_class.call( - :xml, - :serializable_hash, - 'application/json', - 'application/octet-stream', - :txt - ) } - - specify do - expect(subject).to eql( [ - 'application/xml', - 'application/json', - 'application/octet-stream', - 'text/plain' - ] ) - end - - subject { described_class.call( [ - :xml, - :serializable_hash, - 'application/json', - 'application/octet-stream', - :txt - ] ) } - - specify do - expect(subject).to eql( [ - 'application/xml', - 'application/json', - 'application/octet-stream', - 'text/plain' - ] ) - end - end -end diff --git a/spec/support/api_swagger_v2_result.rb b/spec/support/api_swagger_v2_result.rb index 85fdd5a7..8acedce6 100644 --- a/spec/support/api_swagger_v2_result.rb +++ b/spec/support/api_swagger_v2_result.rb @@ -65,7 +65,7 @@ class ApiError < Grape::Entity "title"=>"The API title to be displayed on the API homepage.", "description"=>"A description of the API.", "termsOfServiceUrl"=>"www.The-URL-of-the-terms-and-service.com", - "contact"=>{"contact_name"=>"Contact name", "contact_email"=>"Contact@email.com", "contact_url"=>"Contact URL"}, + "contact"=>{"name"=>"Contact name", "email"=>"Contact@email.com", "url"=>"Contact URL"}, "license"=>{"name"=>"The name of the license.", "url"=>"www.The-URL-of-the-license.org"}, "version"=>"v1" }, @@ -104,6 +104,7 @@ class ApiError < Grape::Entity "401"=>{"description"=>"Unauthorized", "schema"=>{"$ref"=>"#/definitions/ApiError"}}}}, "post"=>{ "produces"=>["application/json"], + "consumes"=>["application/json"], "parameters"=>[ {"in"=>"formData", "name"=>"text", "description"=>"Content of something.", "type"=>"string", "required"=>true, "allowMultiple"=>false}, {"in"=>"body", "name"=>"links", "description"=>nil, "type"=>"Array", "required"=>true, "allowMultiple"=>true}], @@ -125,6 +126,7 @@ class ApiError < Grape::Entity "401"=>{"description"=>"Unauthorized"}}}, "put"=>{ "produces"=>["application/json"], + "consumes"=>["application/json"], "parameters"=>[ {"in"=>"path", "name"=>"id", "description"=>nil, "type"=>"integer", "required"=>true, "allowMultiple"=>false, "format"=>"int32"}, {"in"=>"formData", "name"=>"text", "description"=>"Content of something.", "type"=>"string", "required"=>false, "allowMultiple"=>false}, diff --git a/spec/support/the_api_entities.rb b/spec/support/the_api_entities.rb index a5246895..4d369b59 100644 --- a/spec/support/the_api_entities.rb +++ b/spec/support/the_api_entities.rb @@ -27,6 +27,11 @@ class UseResponse < Grape::Entity expose :description, documentation: { type: String } expose :items, as: '$responses', using: Entities::ResponseItem, documentation: { is_array: true } end + + class UseTemResponseAsType < Grape::Entity + expose :description, documentation: { type: String } + expose :responses, documentation: { type: Entities::ResponseItem, is_array: false } + end end end end diff --git a/spec/swagger_v2/api_swagger_v2_format-content_type_spec.rb b/spec/swagger_v2/api_swagger_v2_format-content_type_spec.rb index 02ea3908..218e6c6e 100644 --- a/spec/swagger_v2/api_swagger_v2_format-content_type_spec.rb +++ b/spec/swagger_v2/api_swagger_v2_format-content_type_spec.rb @@ -39,6 +39,14 @@ class ProducesApi < Grape::API { "declared_params" => declared(params) } end + desc 'This uses produces for produces', + failure: [{code: 400, model: Entities::ApiError}], + consumes: ["application/www_url_encoded"], + entity: Entities::UseResponse + post '/use_consumes' do + { "declared_params" => declared(params) } + end + add_swagger_documentation end end @@ -101,4 +109,16 @@ def app expect(subject['paths']['/use_produces']['get']['produces']).to eql(produced) end end + + describe "consumes" do + subject do + get '/swagger_doc/use_consumes' + JSON.parse(last_response.body) + end + + specify do + expect(subject['paths']['/use_consumes']['post']).to include('consumes') + expect(subject['paths']['/use_consumes']['post']['consumes']).to eql ["application/www_url_encoded"] + end + end end diff --git a/spec/swagger_v2/api_swagger_v2_response_spec.rb b/spec/swagger_v2/api_swagger_v2_response_spec.rb index d042e2b0..d765ffe6 100644 --- a/spec/swagger_v2/api_swagger_v2_response_spec.rb +++ b/spec/swagger_v2/api_swagger_v2_response_spec.rb @@ -22,6 +22,14 @@ class ResponseApi < Grape::API { "declared_params" => declared(params) } end + desc 'This returns something', + entity: Entities::UseTemResponseAsType, + failure: [{code: 400, message: 'NotFound', model: Entities::ApiError}] + get '/nested_type' do + { "declared_params" => declared(params) } + end + + add_swagger_documentation end end @@ -31,6 +39,42 @@ def app TheApi::ResponseApi end + describe "uses nested type as response object" do + subject do + get '/swagger_doc/nested_type' + JSON.parse(last_response.body) + end + specify do + expect(subject).to eql({ + "info"=>{"title"=>"API title", "version"=>"v1"}, + "swagger"=>"2.0", + "produces"=>["application/json"], + "host"=>"example.org", + "tags"=>[ + {"name"=>"params_response", "description"=>"Operations about params_responses"}, + {"name"=>"entity_response", "description"=>"Operations about entity_responses"}, + {"name"=>"nested_type", "description"=>"Operations about nested_types"} + ], + "schemes"=>["https", "http"], + "paths"=>{ + "/nested_type"=>{ + "get"=>{ + "produces"=>["application/json"], + "responses"=>{ + "200"=>{"description"=>"This returns something", "schema"=>{"$ref"=>"#/definitions/UseTemResponseAsType"}}, + "400"=>{"description"=>"NotFound", "schema"=>{"$ref"=>"#/definitions/ApiError"}} + }, + "tags"=>["nested_type"], + "operationId"=>"getNestedType" + }}}, + "definitions"=>{ + "ResponseItem"=>{"type"=>"object", "properties"=>{"id"=>{"type"=>"integer"}, "name"=>{"type"=>"string"}}}, + "UseTemResponseAsType"=>{"type"=>"object", "properties"=>{"description"=>{"type"=>"string"}, "responses"=>{"$ref"=>"#/definitions/ResponseItem"}}}, + "ApiError"=>{"type"=>"object", "properties"=>{"code"=>{"type"=>"integer"}, "message"=>{"type"=>"string"}}} + }}) + end + end + describe "uses entity as response object" do subject do get '/swagger_doc/entity_response' @@ -43,7 +87,11 @@ def app "swagger"=>"2.0", "produces"=>["application/json"], "host"=>"example.org", - "tags" => [{"name"=>"params_response", "description"=>"Operations about params_responses"}, {"name"=>"entity_response", "description"=>"Operations about entity_responses"}], + "tags" => [ + {"name"=>"params_response", "description"=>"Operations about params_responses"}, + {"name"=>"entity_response", "description"=>"Operations about entity_responses"}, + {"name"=>"nested_type", "description"=>"Operations about nested_types"} + ], "schemes"=>["https", "http"], "paths"=>{ "/entity_response"=>{ @@ -74,26 +122,23 @@ def app JSON.parse(last_response.body) end - - # usage of grape-entity 0.4.8 preduces a wrong definition for ParamsResponse, this one: - # "definitions" => { - # "ParamsResponse"=>{"properties"=>{"description"=>{"type"=>"string"}}}, - # "ApiError"=>{"type"=>"object", "properties"=>{"code"=>{"type"=>"integer"}, "message"=>{"type"=>"string"}}} - # } - # (`$response` property is missing) - specify do expect(subject).to eql({ "info"=>{"title"=>"API title", "version"=>"v1"}, "swagger"=>"2.0", "produces"=>["application/json"], "host"=>"example.org", - "tags" => [{"name"=>"params_response", "description"=>"Operations about params_responses"}, {"name"=>"entity_response", "description"=>"Operations about entity_responses"}], + "tags" => [ + {"name"=>"params_response", "description"=>"Operations about params_responses"}, + {"name"=>"entity_response", "description"=>"Operations about entity_responses"}, + {"name"=>"nested_type", "description"=>"Operations about nested_types"} + ], "schemes"=>["https", "http"], "paths"=>{ "/params_response"=>{ "post"=>{ "produces"=>["application/json"], + "consumes"=>["application/json"], "parameters"=>[ {"in"=>"formData", "name"=>"description", "description"=>nil, "type"=>"string", "required"=>false, "allowMultiple"=>false}, {"in"=>"formData", "name"=>"$responses", "description"=>nil, "type"=>"string", "required"=>false, "allowMultiple"=>true}], diff --git a/spec/swagger_v2/api_swagger_v2_spec.rb b/spec/swagger_v2/api_swagger_v2_spec.rb index 7d15cb74..4c0a5fd1 100644 --- a/spec/swagger_v2/api_swagger_v2_spec.rb +++ b/spec/swagger_v2/api_swagger_v2_spec.rb @@ -155,12 +155,12 @@ def app describe 'contact object' do let(:contact) { json['info']['contact'] } - it { expect(contact.keys).to include 'contact_name' } - it { expect(contact['contact_name']).to be_a String } - it { expect(contact.keys).to include 'contact_email' } - it { expect(contact['contact_email']).to be_a String } - it { expect(contact.keys).to include 'contact_url' } - it { expect(contact['contact_url']).to be_a String } + it { expect(contact.keys).to include 'name' } + it { expect(contact['name']).to be_a String } + it { expect(contact.keys).to include 'email' } + it { expect(contact['email']).to be_a String } + it { expect(contact.keys).to include 'url' } + it { expect(contact['url']).to be_a String } end end diff --git a/spec/swagger_v2/default_api_spec.rb b/spec/swagger_v2/default_api_spec.rb index a1faceb4..8194ee67 100644 --- a/spec/swagger_v2/default_api_spec.rb +++ b/spec/swagger_v2/default_api_spec.rb @@ -124,7 +124,7 @@ def app end it 'documents the contact email' do - expect(subject['contact']['contact_email']).to eql('support@test.com') + expect(subject['contact']['email']).to eql('support@test.com') end end end diff --git a/spec/swagger_v2/params_array_spec.rb b/spec/swagger_v2/params_array_spec.rb index 217f12b7..0e20ae96 100644 --- a/spec/swagger_v2/params_array_spec.rb +++ b/spec/swagger_v2/params_array_spec.rb @@ -38,24 +38,10 @@ def app end specify do - expect(subject).to eql({ - "info"=>{"title"=>"API title", "version"=>"v1"}, - "swagger"=>"2.0", - "produces"=>["application/json"], - "host"=>"example.org", - "tags" => [{"name"=>"groups", "description"=>"Operations about groups"}, {"name"=>"type_given", "description"=>"Operations about type_givens"}], - "schemes" => ["https", "http"], - "paths"=>{ - "/groups"=>{ - "post"=>{ - "produces"=>["application/json"], - "tags"=>["groups"], - "operationId"=>"postGroups", - "responses"=>{"201"=>{"description"=>"created Group"}}, - "parameters"=>[ - {"in"=>"formData", "name"=>"required_group[][required_param_1]", "description"=>nil, "type"=>"string", "required"=>true, "allowMultiple"=>true}, - {"in"=>"formData", "name"=>"required_group[][required_param_2]", "description"=>nil, "type"=>"string", "required"=>true, "allowMultiple"=>true} - ]}}}}) + expect(subject['paths']['/groups']['post']['parameters']).to eql([ + {"in"=>"formData", "name"=>"required_group[][required_param_1]", "description"=>nil, "type"=>"string", "required"=>true, "allowMultiple"=>true}, + {"in"=>"formData", "name"=>"required_group[][required_param_2]", "description"=>nil, "type"=>"string", "required"=>true, "allowMultiple"=>true} + ]) end end @@ -66,25 +52,12 @@ def app end specify do - expect(subject).to eql({ - "info"=>{"title"=>"API title", "version"=>"v1"}, - "swagger"=>"2.0", - "produces"=>["application/json"], - "host"=>"example.org", - "tags"=>[{"name"=>"groups", "description"=>"Operations about groups"}, {"name"=>"type_given", "description"=>"Operations about type_givens"}], - "schemes"=>["https", "http"], - "paths"=>{ - "/type_given"=>{ - "post"=>{ - "produces"=>["application/json"], - "parameters"=>[ - {"in"=>"formData", "name"=>"typed_group[][id]", "description"=>"integer given", "type"=>"integer", "required"=>true, "allowMultiple"=>true, "format"=>"int32"}, - {"in"=>"formData", "name"=>"typed_group[][name]", "description"=>"string given", "type"=>"string", "required"=>true, "allowMultiple"=>true}, - {"in"=>"formData", "name"=>"typed_group[][email]", "description"=>"email given", "type"=>"string", "required"=>false, "allowMultiple"=>true}, - {"in"=>"formData", "name"=>"typed_group[][others]", "description"=>nil, "type"=>"integer", "required"=>false, "allowMultiple"=>true, "format"=>"int32", "enum"=>[1, 2, 3]}], - "tags"=>["type_given"], - "operationId"=>"postTypeGiven", - "responses"=>{"201"=>{"description"=>"created TypeGiven"}}}}}}) + expect(subject['paths']['/type_given']['post']['parameters']).to eql([ + {"in"=>"formData", "name"=>"typed_group[][id]", "description"=>"integer given", "type"=>"integer", "required"=>true, "allowMultiple"=>true, "format"=>"int32"}, + {"in"=>"formData", "name"=>"typed_group[][name]", "description"=>"string given", "type"=>"string", "required"=>true, "allowMultiple"=>true}, + {"in"=>"formData", "name"=>"typed_group[][email]", "description"=>"email given", "type"=>"string", "required"=>false, "allowMultiple"=>true}, + {"in"=>"formData", "name"=>"typed_group[][others]", "description"=>nil, "type"=>"integer", "required"=>false, "allowMultiple"=>true, "format"=>"int32", "enum"=>[1, 2, 3]} + ]) end end end diff --git a/spec/swagger_v2/simple_mounted_api_spec.rb b/spec/swagger_v2/simple_mounted_api_spec.rb index 7fbf0637..a21ac0d7 100644 --- a/spec/swagger_v2/simple_mounted_api_spec.rb +++ b/spec/swagger_v2/simple_mounted_api_spec.rb @@ -109,6 +109,7 @@ def app "/items"=>{ "post"=>{ "produces"=>["application/json"], + "consumes"=>["application/json"], "parameters"=>[{"in"=>"formData", "name"=>"items[]", "description"=>"array of items", "type"=>"string", "required"=>false, "allowMultiple"=>true}], "tags"=>["items"], "operationId"=>"postItems", @@ -211,6 +212,7 @@ def app "/items"=>{ "post"=>{ "produces"=>["application/json"], + "consumes"=>["application/json"], "parameters"=>[{"in"=>"formData", "name"=>"items[]", "description"=>"array of items", "type"=>"string", "required"=>false, "allowMultiple"=>true}], "tags"=>["items"], "operationId"=>"postItems",