diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..371e8fe --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +max_line_length = 120 + +[*.md] +indent_style = space +indent_size = 4 diff --git a/.rspec b/.rspec index 8c18f1a..7a588d9 100644 --- a/.rspec +++ b/.rspec @@ -1,2 +1,4 @@ +--require spec_helper.rb --format documentation --color +--warnings diff --git a/.rubocop.yml b/.rubocop.yml index dee73d8..a372e91 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,16 +1,20 @@ AllCops: + NewCops: enable Exclude: - vendor/**/* - TargetRubyVersion: 2.4 + TargetRubyVersion: 2.7 inherit_from: .rubocop_todo.yml Layout/EmptyLinesAroundArguments: Enabled: false -Layout/IndentHash: +Layout/FirstHashElementIndentation: EnforcedStyle: consistent +Layout/MultilineMethodCallIndentation: + EnforcedStyle: indented + Metrics/BlockLength: Exclude: - spec/**/* diff --git a/Gemfile b/Gemfile index b03157b..907ae0d 100644 --- a/Gemfile +++ b/Gemfile @@ -5,4 +5,11 @@ source 'https://rubygems.org' # Specify your gem's dependencies in grape-swagger-representable.gemspec gemspec -gem 'grape-swagger', git: 'https://github.com/ruby-grape/grape-swagger.git' +gem 'multi_json' +gem 'pry-byebug' unless RUBY_PLATFORM.eql?('java') || RUBY_ENGINE.eql?('rbx') +gem 'rack-test' +gem 'rake' +gem 'rspec', '~> 3.0' +gem 'rubocop', '~> 1.63.4' + +gem 'grape-swagger', github: 'ruby-grape/grape-swagger' diff --git a/README.md b/README.md index 66a0782..9ce581e 100644 --- a/README.md +++ b/README.md @@ -31,4 +31,3 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERN ## License The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). - diff --git a/bin/pry b/bin/pry index 5af8e51..b9119d7 100755 --- a/bin/pry +++ b/bin/pry @@ -2,14 +2,10 @@ # frozen_string_literal: true require 'bundler/setup' -require 'grape-swagger/representable' +require_relative '../lib/grape-swagger/representable' # You can add fixtures and/or initialization code here to make experimenting # with your gem easier. You can also use a different console, if you like. -# (If you use this, don't forget to add pry to your Gemfile!) -# require "pry" -# Pry.start - require 'pry' Pry.start diff --git a/grape-swagger-representable.gemspec b/grape-swagger-representable.gemspec index fb8b844..34c39b6 100644 --- a/grape-swagger-representable.gemspec +++ b/grape-swagger-representable.gemspec @@ -1,8 +1,6 @@ # frozen_string_literal: true -lib = File.expand_path('lib', __dir__) -$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) -require 'grape-swagger/representable/version' +require_relative 'lib/grape-swagger/representable/version' Gem::Specification.new do |s| s.name = 'grape-swagger-representable' @@ -11,7 +9,6 @@ Gem::Specification.new do |s| s.email = ['kirik910@gmail.com'] s.summary = 'Grape swagger adapter to support representable object parsing' - s.homepage = 'https://github.com/Bugagazavr/grape-swagger-representable' s.license = 'MIT' s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } @@ -19,18 +16,20 @@ Gem::Specification.new do |s| s.executables = s.files.grep(%r{^exe/}) { |f| File.basename(f) } s.require_paths = ['lib'] - s.add_runtime_dependency 'grape-swagger', '>= 0.31.0' - s.add_runtime_dependency 'representable' - - s.add_development_dependency 'bundler', '~> 1.12' - s.add_development_dependency 'multi_json' - s.add_development_dependency 'pry' unless RUBY_PLATFORM.eql?('java') || RUBY_ENGINE.eql?('rbx') - s.add_development_dependency 'pry-byebug' unless RUBY_PLATFORM.eql?('java') || RUBY_ENGINE.eql?('rbx') - s.add_development_dependency 'rack-cors' - s.add_development_dependency 'rack-test' - s.add_development_dependency 'rake', '~> 10.0' - s.add_development_dependency 'redcarpet' unless RUBY_PLATFORM.eql?('java') || RUBY_ENGINE.eql?('rbx') - s.add_development_dependency 'rouge' unless RUBY_PLATFORM.eql?('java') || RUBY_ENGINE.eql?('rbx') - s.add_development_dependency 'rspec', '~> 3.0' - s.add_development_dependency 'rubocop' + github_uri = "https://github.com/ruby-grape/#{s.name}" + + s.homepage = github_uri + + s.metadata = { + 'rubygems_mfa_required' => 'true', + 'bug_tracker_uri' => "#{github_uri}/issues", + 'documentation_uri' => "http://www.rubydoc.info/gems/#{s.name}/#{s.version}", + 'homepage_uri' => s.homepage, + 'source_code_uri' => github_uri + } + + s.required_ruby_version = '>= 2.7', '< 4' + + s.add_runtime_dependency 'grape-swagger', '~> 2.0' + s.add_runtime_dependency 'representable', '~> 3.2' end diff --git a/lib/grape-swagger-representable.rb b/lib/grape-swagger-representable.rb index 3fd783b..d7020fc 100644 --- a/lib/grape-swagger-representable.rb +++ b/lib/grape-swagger-representable.rb @@ -1,3 +1,3 @@ # frozen_string_literal: true -require 'grape-swagger/representable' +require_relative 'grape-swagger/representable' diff --git a/lib/grape-swagger/representable.rb b/lib/grape-swagger/representable.rb index 0503d24..0ee21e4 100644 --- a/lib/grape-swagger/representable.rb +++ b/lib/grape-swagger/representable.rb @@ -3,12 +3,12 @@ require 'grape-swagger' require 'representable' -require 'grape-swagger/representable/version' -require 'grape-swagger/representable/parser' +require_relative 'representable/version' +require_relative 'representable/parser' module GrapeSwagger module Representable end end -GrapeSwagger.model_parsers.register(::GrapeSwagger::Representable::Parser, ::Representable::Decorator) +GrapeSwagger.model_parsers.register(GrapeSwagger::Representable::Parser, Representable::Decorator) diff --git a/lib/grape-swagger/representable/parser.rb b/lib/grape-swagger/representable/parser.rb index 60996f2..b1f1b45 100644 --- a/lib/grape-swagger/representable/parser.rb +++ b/lib/grape-swagger/representable/parser.rb @@ -3,8 +3,7 @@ module GrapeSwagger module Representable class Parser - attr_reader :model - attr_reader :endpoint + attr_reader :model, :endpoint def initialize(model, endpoint) @model = model @@ -61,7 +60,9 @@ def parse_representer_property(property) end end - def representer_mapping(representer, documentation, property, is_a_collection = false, is_a_decorator = false, nested = nil) + def representer_mapping( + representer, documentation, property, is_a_collection = false, is_a_decorator = false, nested = nil + ) if nested.nil? && is_a_decorator name = endpoint.send(:expose_params_from_model, representer) @@ -105,13 +106,17 @@ def parse_representer(representer) properties = representer.map.each_with_object({}) do |value, property| property_name = value[:as].try(:call) || value.name hidden_property = value[:documentation]&.[](:hidden) + next if hidden_property && (hidden_property.is_a?(Proc) ? hidden_property.call : hidden_property) + property[property_name] = parse_representer_property(value) end - required = representer.map - .select { |value| value[:documentation] && value[:documentation][:required] } - .map { |value| value[:as] || value.name } + required = + representer + .map + .select { |value| value[:documentation] && value[:documentation][:required] } + .map { |value| value[:as] || value.name } [properties, required] end @@ -125,15 +130,17 @@ def combine(representer, nested) overrided = (attributes.keys & nested_attributes.keys) - final_required = (required + nested_required) - .uniq - .select { |k| (overrided.include?(k) && nested_required.include?(k)) || !overrided.include?(k) } + final_required = + (required + nested_required) + .uniq + .select { |k| (overrided.include?(k) && nested_required.include?(k)) || !overrided.include?(k) } [final_attributes, final_required] end def with_required(hash, required) return hash if required.empty? + hash[:required] = required hash end diff --git a/spec/grape-swagger/representable_spec.rb b/spec/grape-swagger/representable_spec.rb index 45b93bf..83a79f7 100644 --- a/spec/grape-swagger/representable_spec.rb +++ b/spec/grape-swagger/representable_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe GrapeSwagger::Representable do it 'has a version number' do expect(GrapeSwagger::Representable::VERSION).not_to be nil diff --git a/spec/grape-swagger/representers/response_inline_representer_spec.rb b/spec/grape-swagger/representers/response_inline_representer_spec.rb index 432b3a9..8a98811 100644 --- a/spec/grape-swagger/representers/response_inline_representer_spec.rb +++ b/spec/grape-swagger/representers/response_inline_representer_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe 'responseInlineModel' do before :all do module ThisInlineApi @@ -50,7 +48,7 @@ class ResponseModelApi < Grape::API is_array: true, http_codes: [{ code: 200, message: 'OK', model: Representers::Something }] get '/something' do - something = OpenStruct.new text: 'something' + something = Struct.new('Something', :text).new('something') Representers::Something.new(something).to_hash end @@ -66,10 +64,10 @@ class ResponseModelApi < Grape::API end get '/something/:id' do if params[:id] == 1 - something = OpenStruct.new text: 'something' + something = Struct.new('Something', :text).new('something') Representers::Something.new(something).to_hash else - error = OpenStruct.new code: 'some_error', message: 'Some error' + error = Struct.new('SomeError', :code, :message).new('some_error', 'Some error') Representers::Error.new(error).to_hash end end @@ -94,7 +92,7 @@ def app 'description' => 'OK', 'schema' => { 'type' => 'array', - 'items' => { '$ref' => '#/definitions/Something' } + 'items' => { '$ref' => '#/definitions/ThisInlineApi_Representers_Something' } } } ) @@ -104,40 +102,51 @@ def app expect(subject['paths']['/something/{id}']['get']['responses']).to eq( '200' => { 'description' => 'OK', - 'schema' => { '$ref' => '#/definitions/Something' } + 'schema' => { '$ref' => '#/definitions/ThisInlineApi_Representers_Something' } }, '403' => { 'description' => 'Refused to return something', - 'schema' => { '$ref' => '#/definitions/Error' } + 'schema' => { '$ref' => '#/definitions/ThisInlineApi_Representers_Error' } } ) - expect(subject['definitions'].keys).to include 'Error' - expect(subject['definitions']['Error']).to eq( + expect(subject['definitions'].keys).to include 'ThisInlineApi_Representers_Error' + expect(subject['definitions']['ThisInlineApi_Representers_Error']).to eq( 'type' => 'object', - 'description' => 'This returns something', + 'description' => 'ThisInlineApi_Representers_Error model', 'properties' => { 'code' => { 'type' => 'string', 'description' => 'Error code', 'default' => 403 }, 'message' => { 'type' => 'string', 'description' => 'Error message' } } ) - expect(subject['definitions'].keys).to include 'Something' - expect(subject['definitions']['Something']).to eq( + expect(subject['definitions'].keys).to include 'ThisInlineApi_Representers_Something' + expect(subject['definitions']['ThisInlineApi_Representers_Something']).to eq( 'type' => 'object', - 'description' => 'This returns something', + 'description' => 'ThisInlineApi_Representers_Something model', 'properties' => { 'text' => { 'description' => 'Content of something.', 'type' => 'string' }, 'alias' => { 'description' => 'Aliased.', 'type' => 'string' }, - 'kind' => { '$ref' => '#/definitions/Kind', 'description' => 'The kind of this something.' }, + 'kind' => { + '$ref' => '#/definitions/ThisInlineApi_Representers_Kind', + 'description' => 'The kind of this something.' + }, 'kind2' => { 'type' => 'object', 'properties' => { - 'id' => { 'description' => 'Title of the kind.', 'type' => 'integer', 'format' => 'int32', 'example' => 123 }, + 'id' => { + 'description' => 'Title of the kind.', + 'type' => 'integer', + 'format' => 'int32', + 'example' => 123 + }, 'name' => { 'description' => 'Kind name.', 'type' => 'string' } }, 'description' => 'Secondary kind.' }, - 'kind3' => { '$ref' => '#/definitions/Kind', 'description' => 'Tertiary kind.' }, + 'kind3' => { + '$ref' => '#/definitions/ThisInlineApi_Representers_Kind', + 'description' => 'Tertiary kind.' + }, 'kind4' => { 'description' => '', 'properties' => { @@ -171,9 +180,17 @@ def app 'required' => ['kind4'] ) - expect(subject['definitions'].keys).to include 'Kind' - expect(subject['definitions']['Kind']).to eq( - 'type' => 'object', 'properties' => { 'id' => { 'description' => 'Title of the kind.', 'type' => 'integer', 'format' => 'int32', 'example' => 123 } } + expect(subject['definitions'].keys).to include 'ThisInlineApi_Representers_Kind' + expect(subject['definitions']['ThisInlineApi_Representers_Kind']).to eq( + 'type' => 'object', + 'properties' => { + 'id' => { + 'description' => 'Title of the kind.', + 'type' => 'integer', + 'format' => 'int32', + 'example' => 123 + } + } ) end end diff --git a/spec/grape-swagger/representers/response_representer_spec.rb b/spec/grape-swagger/representers/response_representer_spec.rb index 8cfd2ac..4abd3dc 100644 --- a/spec/grape-swagger/representers/response_representer_spec.rb +++ b/spec/grape-swagger/representers/response_representer_spec.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'spec_helper' - describe 'responseModel' do before :all do module ThisApi @@ -55,7 +53,7 @@ class ResponseModelApi < Grape::API is_array: true, http_codes: [{ code: 200, message: 'OK', model: Representers::Something }] get '/something' do - something = OpenStruct.new text: 'something' + something = Struct.new('Something', :text).new('something') Representers::Something.new(something).to_hash end @@ -71,10 +69,10 @@ class ResponseModelApi < Grape::API end get '/something/:id' do if params[:id] == 1 - something = OpenStruct.new text: 'something' + something = Struct.new('Something', :text).new('something') Representers::Something.new(something).to_hash else - error = OpenStruct.new code: 'some_error', message: 'Some error' + error = Struct.new('SomeError', :code, :message).new('some_error', 'Some error') Representers::Error.new(error).to_hash end end @@ -99,7 +97,7 @@ def app 'description' => 'OK', 'schema' => { 'type' => 'array', - 'items' => { '$ref' => '#/definitions/Something' } + 'items' => { '$ref' => '#/definitions/ThisApi_Representers_Something' } } } ) @@ -107,12 +105,20 @@ def app it 'should document specified models with hidden property' do allow(ThisApi::Representers::Error).to receive(:developer?).and_return(true) - expect(subject['definitions']['Error']).to eq( + expect(subject['definitions']['ThisApi_Representers_Error']).to eq( 'type' => 'object', - 'description' => 'This returns something', - 'properties' => { 'code' => { 'description' => 'Error code', 'type' => 'string' }, - 'message' => { 'description' => 'Error message', 'type' => 'string' }, - 'developer_message' => { 'description' => 'Developer hidden error message', 'type' => 'string' } } + 'description' => 'ThisApi_Representers_Error model', + 'properties' => { + 'code' => { + 'description' => 'Error code', 'type' => 'string' + }, + 'message' => { + 'description' => 'Error message', 'type' => 'string' + }, + 'developer_message' => { + 'description' => 'Developer hidden error message', 'type' => 'string' + } + } ) end @@ -120,46 +126,57 @@ def app expect(subject['paths']['/something/{id}']['get']['responses']).to eq( '200' => { 'description' => 'OK', - 'schema' => { '$ref' => '#/definitions/Something' } + 'schema' => { '$ref' => '#/definitions/ThisApi_Representers_Something' } }, '403' => { 'description' => 'Refused to return something', - 'schema' => { '$ref' => '#/definitions/Error' } + 'schema' => { '$ref' => '#/definitions/ThisApi_Representers_Error' } } ) - expect(subject['definitions'].keys).to include 'Error' - expect(subject['definitions']['Error']).to eq( + expect(subject['definitions'].keys).to include 'ThisApi_Representers_Error' + expect(subject['definitions']['ThisApi_Representers_Error']).to eq( 'type' => 'object', - 'description' => 'This returns something', + 'description' => 'ThisApi_Representers_Error model', 'properties' => { 'code' => { 'description' => 'Error code', 'type' => 'string' }, 'message' => { 'description' => 'Error message', 'type' => 'string' } } ) - expect(subject['definitions'].keys).to include 'Something' - expect(subject['definitions']['Something']).to eq( + expect(subject['definitions'].keys).to include 'ThisApi_Representers_Something' + expect(subject['definitions']['ThisApi_Representers_Something']).to eq( 'type' => 'object', - 'description' => 'This returns something', - 'properties' => - { 'text' => { 'type' => 'string', 'description' => 'Content of something.' }, - 'alias' => { 'type' => 'string', 'description' => 'Aliased.' }, - 'kind' => { '$ref' => '#/definitions/Kind', 'description' => 'The kind of this something.' }, - 'kind2' => { '$ref' => '#/definitions/Kind', 'description' => 'Secondary kind.' }, - 'kind3' => { '$ref' => '#/definitions/Kind', 'description' => 'Tertiary kind.' }, - 'tags' => { 'type' => 'array', 'items' => { '$ref' => '#/definitions/Tag' }, 'description' => 'Tags.' }, - 'relation' => { '$ref' => '#/definitions/Relation', 'description' => 'A related model.' } } + 'description' => 'ThisApi_Representers_Something model', + 'properties' => { + 'text' => { 'type' => 'string', 'description' => 'Content of something.' }, + 'alias' => { 'type' => 'string', 'description' => 'Aliased.' }, + 'kind' => { + '$ref' => '#/definitions/ThisApi_Representers_Kind', + 'description' => 'The kind of this something.' + }, + 'kind2' => { '$ref' => '#/definitions/ThisApi_Representers_Kind', 'description' => 'Secondary kind.' }, + 'kind3' => { '$ref' => '#/definitions/ThisApi_Representers_Kind', 'description' => 'Tertiary kind.' }, + 'tags' => { + 'type' => 'array', + 'items' => { '$ref' => '#/definitions/ThisApi_Representers_Tag' }, + 'description' => 'Tags.' + }, + 'relation' => { + '$ref' => '#/definitions/ThisApi_Representers_Relation', + 'description' => 'A related model.' + } + } ) - expect(subject['definitions'].keys).to include 'Kind' - expect(subject['definitions']['Kind']).to eq( + expect(subject['definitions'].keys).to include 'ThisApi_Representers_Kind' + expect(subject['definitions']['ThisApi_Representers_Kind']).to eq( 'type' => 'object', 'properties' => { 'title' => { 'type' => 'string', 'description' => 'Title of the kind.', 'example' => 123 } } ) - expect(subject['definitions'].keys).to include 'Relation' - expect(subject['definitions']['Relation']).to eq( + expect(subject['definitions'].keys).to include 'ThisApi_Representers_Relation' + expect(subject['definitions']['ThisApi_Representers_Relation']).to eq( 'type' => 'object', 'properties' => { 'name' => { 'type' => 'string', 'description' => 'Name', 'example' => 'A relation' } } ) - expect(subject['definitions'].keys).to include 'Tag' - expect(subject['definitions']['Tag']).to eq( + expect(subject['definitions'].keys).to include 'ThisApi_Representers_Tag' + expect(subject['definitions']['ThisApi_Representers_Tag']).to eq( 'type' => 'object', 'properties' => { 'name' => { 'type' => 'string', 'description' => 'Name' } } ) end @@ -205,7 +222,7 @@ class ResponseEntityApi < Grape::API is_array: true, entity: Representers::SomeEntity get '/some_entity' do - something = OpenStruct.new text: 'something' + something = Struct.new('Something', :text).new('something') Representers::SomeEntity.new(something).to_hash end @@ -225,20 +242,55 @@ def app it 'it prefer entity over others' do expect(subject['definitions']).to eql( - 'Kind' => { 'type' => 'object', 'properties' => { 'id' => { 'description' => 'Title of the kind.', 'type' => 'integer', 'format' => 'int32' } } }, - 'Tag' => { 'type' => 'object', 'properties' => { 'name' => { 'description' => 'Name', 'type' => 'string' } } }, - 'Relation' => { 'type' => 'object', 'properties' => { 'name' => { 'description' => 'Name', 'type' => 'string' } } }, - 'SomeEntity' => { + 'TheseApi_Representers_Kind' => { + 'type' => 'object', + 'properties' => { + 'id' => { + 'description' => 'Title of the kind.', + 'type' => 'integer', + 'format' => 'int32' + } + } + }, + 'TheseApi_Representers_Tag' => { + 'type' => 'object', + 'properties' => { + 'name' => { + 'description' => 'Name', + 'type' => 'string' + } + } + }, + 'TheseApi_Representers_Relation' => { + 'type' => 'object', + 'properties' => { + 'name' => { + 'description' => 'Name', + 'type' => 'string' + } + } + }, + 'TheseApi_Representers_SomeEntity' => { 'type' => 'object', 'properties' => { 'text' => { 'description' => 'Content of something.', 'type' => 'string' }, - 'kind' => { '$ref' => '#/definitions/Kind', 'description' => 'The kind of this something.' }, - 'kind2' => { '$ref' => '#/definitions/Kind', 'description' => 'Secondary kind.' }, - 'kind3' => { '$ref' => '#/definitions/Kind', 'description' => 'Tertiary kind.' }, - 'tags' => { 'type' => 'array', 'items' => { '$ref' => '#/definitions/Tag' }, 'description' => 'Tags.' }, - 'relation' => { '$ref' => '#/definitions/Relation', 'description' => 'A related model.' } + 'kind' => { + '$ref' => '#/definitions/TheseApi_Representers_Kind', + 'description' => 'The kind of this something.' + }, + 'kind2' => { '$ref' => '#/definitions/TheseApi_Representers_Kind', 'description' => 'Secondary kind.' }, + 'kind3' => { '$ref' => '#/definitions/TheseApi_Representers_Kind', 'description' => 'Tertiary kind.' }, + 'tags' => { + 'type' => 'array', + 'items' => { '$ref' => '#/definitions/TheseApi_Representers_Tag' }, + 'description' => 'Tags.' + }, + 'relation' => { + '$ref' => '#/definitions/TheseApi_Representers_Relation', + 'description' => 'A related model.' + } }, - 'description' => 'This returns something' + 'description' => 'TheseApi_Representers_SomeEntity model' } ) end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 8eda29e..af0292a 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -$LOAD_PATH.unshift File.expand_path('../lib', __dir__) - -require 'grape-swagger/representable' +require_relative '../lib/grape-swagger/representable' require 'grape' require 'representable/json'