diff --git a/.overcommit.yml b/.overcommit.yml index 97dbf68b..b38ef191 100644 --- a/.overcommit.yml +++ b/.overcommit.yml @@ -19,6 +19,7 @@ PreCommit: RuboCop: enabled: true on_warn: fail + command: ['bundle', 'exec', 'rubocop'] Fasterer: enabled: true on_warn: fail diff --git a/Gemfile b/Gemfile index d00a4565..15307ede 100644 --- a/Gemfile +++ b/Gemfile @@ -19,6 +19,7 @@ gem 'hamlit', '~> 3.0.3' gem 'hamlit-rails', '~> 0.2.3' gem 'html2haml', '~> 2.2.0' gem 'importmap-rails', '~> 1.1.5' +gem 'interactor', '~> 3.1.2' gem 'jbuilder', '~> 2.11.5' gem 'jquery-rails', '~> 4.5.1' gem 'nokogiri', '~> 1.15.4' diff --git a/Gemfile.lock b/Gemfile.lock index f020e819..8082a67a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -67,7 +67,7 @@ GEM minitest (>= 5.1) tzinfo (~> 2.0) ast (2.4.2) - autoprefixer-rails (10.4.7.0) + autoprefixer-rails (10.4.13.0) execjs (~> 2) bcrypt (3.1.18) bindex (0.8.1) @@ -93,10 +93,10 @@ GEM concurrent-ruby (1.2.2) crass (1.0.6) date (3.3.3) - debug (1.7.1) + debug (1.7.2) irb (>= 1.5.0) reline (>= 0.3.1) - devise (4.9.0) + devise (4.9.2) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0) @@ -119,7 +119,7 @@ GEM factory_bot_rails (6.2.0) factory_bot (~> 6.2.0) railties (>= 5.0.0) - fasterer (0.10.0) + fasterer (0.10.1) colorize (~> 0.7) ruby_parser (>= 3.19.1) ffaker (2.21.0) @@ -158,14 +158,15 @@ GEM haml (>= 4.0, < 6) nokogiri (>= 1.6.0) ruby_parser (~> 3.5) - i18n (1.12.0) + i18n (1.13.0) concurrent-ruby (~> 1.0) - importmap-rails (1.1.5) + importmap-rails (1.1.6) actionpack (>= 6.0.0) railties (>= 6.0.0) iniparse (1.5.0) + interactor (3.1.2) io-console (0.6.0) - irb (1.6.2) + irb (1.6.4) reline (>= 0.3.0) jbuilder (2.11.5) actionview (>= 5.0.0) @@ -178,7 +179,7 @@ GEM listen (3.8.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) - loofah (2.19.1) + loofah (2.20.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) lumberjack (1.2.8) @@ -213,12 +214,12 @@ GEM childprocess (>= 0.6.3, < 5) iniparse (~> 1.4) rexml (~> 3.2) - pagy (6.0.2) - parallel (1.22.1) - parser (3.2.1.0) + pagy (6.0.3) + parallel (1.23.0) + parser (3.2.2.1) ast (~> 2.4.1) pg (1.4.6) - popper_js (2.11.6) + popper_js (2.11.7) pry (0.14.2) coderay (~> 1.1) method_source (~> 1.0) @@ -275,8 +276,8 @@ GEM rb-fsevent (0.11.2) rb-inotify (0.10.1) ffi (~> 1.0) - regexp_parser (2.7.0) - reline (0.3.2) + regexp_parser (2.8.0) + reline (0.3.3) io-console (~> 0.5) require_all (3.0.0) responders (3.1.0) @@ -287,12 +288,12 @@ GEM rspec-core (~> 3.12.0) rspec-expectations (~> 3.12.0) rspec-mocks (~> 3.12.0) - rspec-core (3.12.1) + rspec-core (3.12.2) rspec-support (~> 3.12.0) - rspec-expectations (3.12.2) + rspec-expectations (3.12.3) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) - rspec-mocks (3.12.3) + rspec-mocks (3.12.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) rspec-rails (6.0.1) @@ -314,26 +315,26 @@ GEM rubocop-ast (>= 1.24.1, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.27.0) + rubocop-ast (1.28.1) parser (>= 3.2.1.0) - rubocop-capybara (2.17.1) + rubocop-capybara (2.18.0) rubocop (~> 1.41) rubocop-md (1.2.0) rubocop (>= 1.0) - rubocop-performance (1.16.0) + rubocop-performance (1.17.1) rubocop (>= 1.7.0, < 2.0) rubocop-ast (>= 0.4.0) - rubocop-rails (2.18.0) + rubocop-rails (2.19.1) activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.33.0, < 2.0) rubocop-rake (0.6.0) rubocop (~> 1.0) - rubocop-rspec (2.18.1) + rubocop-rspec (2.20.0) rubocop (~> 1.33) rubocop-capybara (~> 2.17) - ruby-progressbar (1.11.0) - ruby_parser (3.19.2) + ruby-progressbar (1.13.0) + ruby_parser (3.20.0) sexp_processor (~> 4.16) sassc (2.4.0) ffi (~> 1.9) @@ -389,7 +390,7 @@ GEM websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) - zeitwerk (2.6.7) + zeitwerk (2.6.8) PLATFORMS x86_64-linux @@ -412,6 +413,7 @@ DEPENDENCIES hamlit-rails (~> 0.2.3) html2haml (~> 2.2.0) importmap-rails (~> 1.1.5) + interactor (~> 3.1.2) jbuilder (~> 2.11.5) jquery-rails (~> 4.5.1) nokogiri (~> 1.15.4) diff --git a/app/assets/stylesheets/car_card.scss b/app/assets/stylesheets/car_card.scss index 1e5f8de0..3565bc2a 100644 --- a/app/assets/stylesheets/car_card.scss +++ b/app/assets/stylesheets/car_card.scss @@ -1,3 +1,3 @@ .car-card { - border-radius: 20px; + border-radius: 200px; } diff --git a/app/controllers/cars_controller.rb b/app/controllers/cars_controller.rb index 151a0919..3c544204 100644 --- a/app/controllers/cars_controller.rb +++ b/app/controllers/cars_controller.rb @@ -9,6 +9,7 @@ def index sort @total_cars_count = @cars.count @pagy, @cars = pagy @cars + @cars end def show @@ -20,11 +21,19 @@ def show def search return unless params['filter_params'].present? && params['filter_params'].keys.any? - @cars = CarsService::SearchService.new(params: params['filter_params']).call + @cars = search_cars.cars end def sort - @cars = CarsService::SortService.new(params: params[:sort_by]).call if valid_sort_params? + @cars = sort_cars.cars if valid_sort_params? + end + + def search_cars + Cars::Searcher.call(params: params['filter_params'], cars: @cars) + end + + def sort_cars + Cars::Sorter.call(params: params[:sort_by], cars: @cars) end def valid_sort_params? diff --git a/app/interactors/cars/searcher.rb b/app/interactors/cars/searcher.rb new file mode 100644 index 00000000..e30fcb62 --- /dev/null +++ b/app/interactors/cars/searcher.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +module Cars + class Searcher + include Interactor + def call + search_by_make + search_by_model + search_by_year_from + search_by_year_to + search_by_price_from + search_by_price_to + + context.cars + end + + private + + def search_by_make + return context.cars if context.params['make'].blank? + + context.cars = context.cars.filter_by_make(context.params['make']) + end + + def search_by_model + return context.cars if context.params['model'].blank? + + context.cars = context.cars.filter_by_model(context.params['model']) + end + + def search_by_year_from + return context.cars if context.params['year_from'].blank? + + context.cars = context.cars.filter_by_year_from(context.params['year_from'].to_i) + end + + def search_by_year_to + return context.cars if context.params['year_to'].blank? + + context.cars = context.cars.filter_by_year_to(context.params['year_to'].to_i) + end + + def search_by_price_from + return context.cars if context.params['price_from'].blank? + + context.cars = context.cars.filter_by_price_from(context.params['price_from'].to_i) + end + + def search_by_price_to + return context.cars if context.params['price_to'].blank? + + context.cars = context.cars.filter_by_price_to(context.params['price_to'].to_i) + end + end +end diff --git a/app/interactors/cars/sorter.rb b/app/interactors/cars/sorter.rb new file mode 100644 index 00000000..a17b97da --- /dev/null +++ b/app/interactors/cars/sorter.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Cars + class Sorter + include Interactor + + attr_reader :data + + def call + context.cars = context.cars.filter_sort_by(context.params) + end + end +end diff --git a/app/models/user.rb b/app/models/user.rb index a31a557d..cd37473d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,13 +1,13 @@ # frozen_string_literal: true class User < ApplicationRecord - has_many :searches, dependent: :delete_all - - validates :email, presence: true, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP } - validates :password, length: { within: 8..128 } - # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable + + validates :email, presence: true + validates :email, uniqueness: true + + has_many :searches, dependent: :delete_all end diff --git a/app/queries/cars_service/search_service.rb b/app/queries/cars_service/search_service.rb deleted file mode 100644 index d00c2ef9..00000000 --- a/app/queries/cars_service/search_service.rb +++ /dev/null @@ -1,58 +0,0 @@ -# frozen_string_literal: true - -module CarsService - class SearchService - def initialize(params:, data: Car.all) - @params = params - @data = data - end - - def call - search_by_make - search_by_model - search_by_year_from - search_by_year_to - search_by_price_from - search_by_price_to - @data - end - - private - - def search_by_make - return @data if @params['make'].blank? - - @data = @data.filter_by_make(@params['make']) - end - - def search_by_model - return @data if @params['model'].blank? - - @data = @data.filter_by_model(@params['model']) - end - - def search_by_year_from - return @data if @params['year_from'].blank? - - @data = @data.filter_by_year_from(@params['year_from'].to_i) - end - - def search_by_year_to - return @data if @params['year_to'].blank? - - @data = @data.filter_by_year_to(@params['year_to'].to_i) - end - - def search_by_price_from - return @data if @params['price_from'].blank? - - @data = @data.filter_by_price_from(@params['price_from'].to_i) - end - - def search_by_price_to - return @data if @params['price_to'].blank? - - @data = @data.filter_by_price_to(@params['price_to'].to_i) if @params['year_to'].present? - end - end -end diff --git a/app/queries/cars_service/sort_service.rb b/app/queries/cars_service/sort_service.rb deleted file mode 100644 index 8ce83abb..00000000 --- a/app/queries/cars_service/sort_service.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -module CarsService - class SortService - attr_reader :data - - def initialize(params:, data: Car.all) - @params = params - @data = data - end - - def call - @data.filter_sort_by(@params) - end - end -end diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 881e614f..44318d5c 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -178,7 +178,7 @@ # ==> Configuration for :validatable # Range for password length. - config.password_length = 6..128 + config.password_length = 8..128 # Email regex used to validate email formats. It simply asserts that # one (and only one) @ exists in the given string. This is mainly diff --git a/spec/queries/cars_service/search_service_spec.rb b/spec/interactors/cars/searcher_spec.rb similarity index 67% rename from spec/queries/cars_service/search_service_spec.rb rename to spec/interactors/cars/searcher_spec.rb index fd8fced6..2b6c111b 100644 --- a/spec/queries/cars_service/search_service_spec.rb +++ b/spec/interactors/cars/searcher_spec.rb @@ -2,13 +2,13 @@ require 'rails_helper' -RSpec.describe CarsService::SearchService, type: :service do +RSpec.describe Cars::Searcher, type: :service do describe '.call' do - subject(:cars) { described_class.new(params:, data: Car.all).call } + subject(:context) { described_class.call(params:, cars: Car.all) } - let!(:car1) { create(:car, :nissan_leaf) } - let!(:car2) { create(:car, :zaz_sens) } - let!(:car3) { create(:car, :opel_kadett) } + let!(:nissan_leaf) { create(:car, :nissan_leaf) } + let!(:zaz_sens) { create(:car, :zaz_sens) } + let!(:opel_kadett) { create(:car, :opel_kadett) } context 'when filtering by all proper params' do let(:params) do @@ -17,7 +17,7 @@ end it 'returns filtered data' do - expect(cars.first).to eq(car2) + expect(context.cars.first).to eq(zaz_sens) end end @@ -25,7 +25,7 @@ let(:params) { { 'make' => 'Zaz' } } it 'returns filtered data' do - expect(cars.first).to eq(car2) + expect(context.cars.first).to eq(zaz_sens) end end @@ -33,7 +33,7 @@ let(:params) { { 'model' => 'Leaf' } } it 'returns filtered data' do - expect(cars.first.model).to eq('Leaf') + expect(context.cars.first.model).to eq('Leaf') end end @@ -41,7 +41,7 @@ let(:params) { { 'year_from' => 1985, 'year_to' => 2012 } } it 'returns filtered data' do - expect(cars).to include(car1, car3) + expect(context.cars).to include(nissan_leaf, opel_kadett) end end @@ -49,7 +49,7 @@ let(:params) { { 'year_from' => '2013' } } it 'returns filtered data' do - expect(cars).to include(car2) + expect(context.cars).to include(zaz_sens) end end @@ -57,7 +57,7 @@ let(:params) { { 'year_to' => '2012' } } it 'returns filtered data' do - expect(cars).to include(car1, car3) + expect(context.cars).to include(nissan_leaf, opel_kadett) end end @@ -65,7 +65,7 @@ let(:params) { { 'price_from' => 3000, 'price_to' => 15_000 } } it 'returns filtered data' do - expect(cars).to include(car1, car2) + expect(context.cars).to include(nissan_leaf, zaz_sens) end end @@ -73,7 +73,7 @@ let(:params) { { 'price_from' => 10_000 } } it 'returns filtered data' do - expect(cars).to include(car1) + expect(context.cars).to include(nissan_leaf) end end @@ -81,7 +81,7 @@ let(:params) { { 'price_to' => 10_000 } } it 'returns filtered data' do - expect(cars).to include(car2, car3) + expect(context.cars).to include(zaz_sens, opel_kadett) end end end diff --git a/spec/queries/cars_service/sort_service_spec.rb b/spec/interactors/cars/sorter_spec.rb similarity index 53% rename from spec/queries/cars_service/sort_service_spec.rb rename to spec/interactors/cars/sorter_spec.rb index 8c214cae..f4a62df5 100644 --- a/spec/queries/cars_service/sort_service_spec.rb +++ b/spec/interactors/cars/sorter_spec.rb @@ -2,19 +2,19 @@ require 'rails_helper' -RSpec.describe CarsService::SortService, type: :service do +RSpec.describe Cars::Sorter, type: :service do describe '.call' do - subject(:cars) { described_class.new(params:, data: Car.all).call } + subject(:context) { described_class.call(params:, cars: Car.all) } - let(:car1) { create(:car, :zaz_sens) } - let(:car2) { create(:car, :opel_kadett) } - let(:car3) { create(:car, :nissan_leaf) } + let(:zaz_sens) { create(:car, :zaz_sens) } + let(:opel_kadett) { create(:car, :opel_kadett) } + let(:nissan_leaf) { create(:car, :nissan_leaf) } context 'when sorting by created_at ASC' do let(:params) { { 'created_at' => 'ASC' } } it 'returns sorted data' do - expect(cars).to eq([car1, car2, car3]) + expect(context.cars).to eq([zaz_sens, opel_kadett, nissan_leaf]) end end @@ -22,7 +22,7 @@ let(:params) { { 'created_at' => 'DESC' } } it 'returns sorted data' do - expect(cars).to eq([car1, car2, car3].reverse) + expect(context.cars).to eq([zaz_sens, opel_kadett, nissan_leaf].reverse) end end @@ -30,7 +30,7 @@ let(:params) { { 'price' => 'ASC' } } it 'returns sorted data' do - expect(cars).to eq([car2, car1, car3]) + expect(context.cars).to eq([opel_kadett, zaz_sens, nissan_leaf]) end end @@ -38,7 +38,7 @@ let(:params) { { 'price' => 'DESC' } } it 'returns sorted data' do - expect(cars).to eq([car2, car1, car3].reverse) + expect(context.cars).to eq([opel_kadett, zaz_sens, nissan_leaf].reverse) end end end diff --git a/spec/models/car_spec.rb b/spec/models/car_spec.rb index 5c267e73..a3732883 100644 --- a/spec/models/car_spec.rb +++ b/spec/models/car_spec.rb @@ -225,70 +225,73 @@ end describe 'scopes' do - let!(:car1) do + let!(:opel_kadett) do create(:car, :opel_kadett) end - let!(:car2) do + let!(:honda_civic) do create(:car, :honda_civic) end - let!(:car3) do + let!(:toyota_corolla) do create(:car, :toyota_corolla) end - let!(:car4) do + let!(:zaz_sens) do create(:car, :zaz_sens) end context 'when filter by make' do it 'returns car with the specific make' do - expect(described_class.filter_by_make('Opel')).to match_array([car1]) + expect(described_class.filter_by_make('Opel')).to contain_exactly(opel_kadett) end end context 'when filter by model' do it 'returns car with the specific model' do - expect(described_class.filter_by_model('Civic')).to match_array([car2]) + expect(described_class.filter_by_model('Civic')).to contain_exactly(honda_civic) end end context 'when filter by year from' do it 'returns car with the specific year from' do - expect(described_class.filter_by_year_from(2004)).to match_array([car3, car4]) + expect(described_class.filter_by_year_from(2004)).to contain_exactly(toyota_corolla, zaz_sens) end end context 'when filter by year to' do it 'returns car with the specific year to' do - expect(described_class.filter_by_year_to(2002)).to match_array([car1, car2]) + expect(described_class.filter_by_year_to(2002)).to contain_exactly(opel_kadett, honda_civic) end end context 'when filter by price from' do it 'returns car with the specific price to' do - expect(described_class.filter_by_price_from(1000)).to match_array([car2, car3, car4]) + expect(described_class.filter_by_price_from(1000)).to contain_exactly(honda_civic, toyota_corolla, zaz_sens) end end context 'when filter by price to' do it 'returns car with the specific price to' do - expect(described_class.filter_by_price_to(1000)).to match_array([car1, car2]) + expect(described_class.filter_by_price_to(1000)).to contain_exactly(opel_kadett, honda_civic) end end context 'when filter sort by' do it 'return cars sorted by price ASC' do - expect(described_class.filter_sort_by('price ASC')).to eq([car1, car2, car4, car3]) + expect(described_class.filter_sort_by('price ASC')).to eq([opel_kadett, honda_civic, zaz_sens, toyota_corolla]) end it 'return cars sorted by price DESC' do - expect(described_class.filter_sort_by('price DESC')).to eq([car1, car2, car4, car3].reverse) + expect(described_class.filter_sort_by('price DESC')).to eq([opel_kadett, honda_civic, zaz_sens, + toyota_corolla].reverse) end it 'return cars sorted by created_at ASC' do - expect(described_class.filter_sort_by('created_at ASC')).to eq([car1, car2, car3, car4]) + expect(described_class.filter_sort_by('created_at ASC')).to eq([opel_kadett, honda_civic, toyota_corolla, + zaz_sens]) end it 'return cars sorted by created_at DESC' do - expect(described_class.filter_sort_by('created_at DESC')).to eq([car1, car2, car3, car4].reverse) + expect(described_class.filter_sort_by('created_at DESC')).to eq([opel_kadett, honda_civic, toyota_corolla, + zaz_sens].reverse) end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 3f833dd9..8d28c3b9 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -31,7 +31,7 @@ it { is_expected.not_to allow_value('test').for(:email) } context 'when password is too short' do - subject(:new_user) { build(:user, password: '1234567') } + subject(:new_user) { build(:user, password: '1234567', password_confirmation: '1234567') } it { is_expected.not_to be_valid }