diff --git a/lecture_5/homework/.rspec b/lecture_5/homework/.rspec new file mode 100644 index 00000000..c99d2e73 --- /dev/null +++ b/lecture_5/homework/.rspec @@ -0,0 +1 @@ +--require spec_helper diff --git a/lecture_5/homework/Gemfile b/lecture_5/homework/Gemfile index f10bbf07..51662319 100644 --- a/lecture_5/homework/Gemfile +++ b/lecture_5/homework/Gemfile @@ -3,7 +3,7 @@ source 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" } -ruby '2.6.2' +ruby '2.5.3' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' gem 'rails', '~> 5.2.2' @@ -42,6 +42,7 @@ group :development do gem 'listen', '>= 3.0.5', '< 3.2' # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring gem 'rubocop', require: false + gem 'rubocop-rspec' gem 'spring' gem 'spring-watcher-listen', '~> 2.0.0' end @@ -50,5 +51,9 @@ group :test do gem 'rspec-rails' end +group :development, :test do + gem 'factory_bot_rails' +end + # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby] diff --git a/lecture_5/homework/Gemfile.lock b/lecture_5/homework/Gemfile.lock index 411593e8..208880eb 100644 --- a/lecture_5/homework/Gemfile.lock +++ b/lecture_5/homework/Gemfile.lock @@ -59,7 +59,13 @@ GEM crass (1.0.4) diff-lcs (1.3) erubi (1.8.0) + factory_bot (5.0.2) + activesupport (>= 4.2.0) + factory_bot_rails (5.0.2) + factory_bot (~> 5.0.2) + railties (>= 4.2.0) ffi (1.10.0) + ffi (1.10.0-x64-mingw32) globalid (0.4.2) activesupport (>= 4.2.0) i18n (1.6.0) @@ -83,14 +89,19 @@ GEM mini_portile2 (2.4.0) minitest (5.11.3) msgpack (1.2.9) + msgpack (1.2.9-x64-mingw32) nio4r (2.3.1) nokogiri (1.10.1) mini_portile2 (~> 2.4.0) + nokogiri (1.10.1-x64-mingw32) + mini_portile2 (~> 2.4.0) parallel (1.17.0) parser (2.6.2.0) ast (~> 2.4.0) pg (1.1.4) + pg (1.1.4-x64-mingw32) psych (3.1.0) + psych (3.1.0-x64-mingw32) puma (3.12.0) rack (2.0.6) rack-test (1.1.0) @@ -149,6 +160,8 @@ GEM rainbow (>= 2.2.2, < 4.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 1.6) + rubocop-rspec (1.32.0) + rubocop (>= 0.60.0) ruby-progressbar (1.10.0) ruby_dep (1.5.0) spring (2.0.2) @@ -167,6 +180,8 @@ GEM thread_safe (0.3.6) tzinfo (1.2.5) thread_safe (~> 0.1) + tzinfo-data (1.2019.1) + tzinfo (>= 1.0.0) unicode-display_width (1.5.0) websocket-driver (0.7.0) websocket-extensions (>= 0.1.0) @@ -174,23 +189,26 @@ GEM PLATFORMS ruby + x64-mingw32 DEPENDENCIES active_model_serializers bootsnap (>= 1.1.0) byebug + factory_bot_rails listen (>= 3.0.5, < 3.2) pg puma (~> 3.11) rails (~> 5.2.2) rspec-rails rubocop + rubocop-rspec spring spring-watcher-listen (~> 2.0.0) tzinfo-data RUBY VERSION - ruby 2.6.2p47 + ruby 2.5.3p105 BUNDLED WITH 2.0.1 diff --git a/lecture_5/homework/app/controllers/buildings_controller.rb b/lecture_5/homework/app/controllers/buildings_controller.rb index 9b7e9677..e46a59f6 100644 --- a/lecture_5/homework/app/controllers/buildings_controller.rb +++ b/lecture_5/homework/app/controllers/buildings_controller.rb @@ -1,7 +1,17 @@ # frozen_string_literal: true class BuildingsController < ApplicationController - def index; end + def index + render json: Building.all + end - def show; end + def show + render json: building, include: 'warrior' + end + + private + + def building + @building ||= Building.find(params[:id]) + end end diff --git a/lecture_5/homework/app/controllers/clans/warriors_controller.rb b/lecture_5/homework/app/controllers/clans/warriors_controller.rb index 6ccf3ff9..9ab1bbff 100644 --- a/lecture_5/homework/app/controllers/clans/warriors_controller.rb +++ b/lecture_5/homework/app/controllers/clans/warriors_controller.rb @@ -10,7 +10,7 @@ def index warriors = clan.warriors if params.key?(:alive) - if params[:alive].to_i == 0 + if params[:alive].to_i.zero? render json: warriors.dead else render json: warriors.alive @@ -47,7 +47,8 @@ def warrior end def warrior_params - params.permit(:name, :death_date, :armor_quality, :number_of_battles, :join_date) + params.permit(:name, :death_date, :armor_quality, + :number_of_battles, :join_date, :building_id) end end end diff --git a/lecture_5/homework/app/models/building.rb b/lecture_5/homework/app/models/building.rb index 23c41918..2d452e0a 100644 --- a/lecture_5/homework/app/models/building.rb +++ b/lecture_5/homework/app/models/building.rb @@ -5,4 +5,6 @@ class Building < ApplicationRecord validates :granary, presence: true, numericality: { greater_than_or_equal_to: 0, only_integer: true } + validates :siege_ability, presence: true, + numericality: { greater_than_or_equal_to: 0, only_integer: true } end diff --git a/lecture_5/homework/app/models/warrior.rb b/lecture_5/homework/app/models/warrior.rb index eacdc13f..b939adc5 100644 --- a/lecture_5/homework/app/models/warrior.rb +++ b/lecture_5/homework/app/models/warrior.rb @@ -14,4 +14,11 @@ class Warrior < ApplicationRecord scope :alive, -> { where('death_date IS NULL') } scope :dead, -> { where('death_date IS NOT NULL') } + + after_save :siege_report + after_destroy :siege_report + + def siege_report + return Reports::SiegeReport.new(building: building).call if building_id + end end diff --git a/lecture_5/homework/app/serializers/building_serializer.rb b/lecture_5/homework/app/serializers/building_serializer.rb index 899c9aa0..e1f1988d 100644 --- a/lecture_5/homework/app/serializers/building_serializer.rb +++ b/lecture_5/homework/app/serializers/building_serializer.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class BuildingSerializer < ActiveModel::Serializer - attributes :name + attributes :name, :granary, :siege_ability has_many :warriors end diff --git a/lecture_5/homework/app/services/reports/siege_report.rb b/lecture_5/homework/app/services/reports/siege_report.rb index 16a0b3a4..2368f318 100644 --- a/lecture_5/homework/app/services/reports/siege_report.rb +++ b/lecture_5/homework/app/services/reports/siege_report.rb @@ -3,10 +3,28 @@ module Reports class SiegeReport def initialize(building:) + @building = building + @warriors = building.warriors end def call - raise NotImprementedYet + siege = @warriors.present? ? @building.siege_ability = granary_ability : @building.siege_ability = 0 + @building.save + siege + end + + private + + def granary_request + daily_request = 10 + samurais = @warriors.where(type: 'Warriors::Samurai').count + hussars = @warriors.where(type: 'Warriors::Hussar').count * 2 + daily_request + samurais + hussars + end + + def granary_ability + granary = @building.granary + granary / granary_request end end end diff --git a/lecture_5/homework/config/database.yml b/lecture_5/homework/config/database.yml index 23483946..0a4d7b97 100644 --- a/lecture_5/homework/config/database.yml +++ b/lecture_5/homework/config/database.yml @@ -1,7 +1,9 @@ default: &default adapter: postgresql encoding: unicode - database: rorlevelup2019 + database: homework_5 + username: postgres + password: Potato development: <<: *default diff --git a/lecture_5/homework/db/migrate/20190416095847_add_siege_ability_on_building.rb b/lecture_5/homework/db/migrate/20190416095847_add_siege_ability_on_building.rb new file mode 100644 index 00000000..7c3cf2dc --- /dev/null +++ b/lecture_5/homework/db/migrate/20190416095847_add_siege_ability_on_building.rb @@ -0,0 +1,7 @@ +class AddSiegeAbilityOnBuilding < ActiveRecord::Migration[5.2] + def change + add_column :buildings, :siege_ability, :integer, default: 0, null: false + + Building.find_each { |building| Reports::SiegeReport.new(building: building).call } + end +end diff --git a/lecture_5/homework/db/schema.rb b/lecture_5/homework/db/schema.rb index 13f968ab..eaf004e0 100644 --- a/lecture_5/homework/db/schema.rb +++ b/lecture_5/homework/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_04_05_090544) do +ActiveRecord::Schema.define(version: 2019_04_16_095847) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -20,6 +20,8 @@ t.string "type", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.integer "granary", default: 0, null: false + t.integer "siege_ability", default: 0, null: false end create_table "clans", force: :cascade do |t| diff --git a/lecture_5/homework/spec/rails_helper.rb b/lecture_5/homework/spec/rails_helper.rb new file mode 100644 index 00000000..7331a82a --- /dev/null +++ b/lecture_5/homework/spec/rails_helper.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +# This file is copied to spec/ when you run 'rails generate rspec:install' +require 'spec_helper' +ENV['RAILS_ENV'] ||= 'test' +require File.expand_path('../../config/environment', __FILE__) +# Prevent database truncation if the environment is production +abort('The Rails environment is running in production mode!') if Rails.env.production? +require 'rspec/rails' +require 'support/factory_bot' +# Add additional requires below this line. Rails is not loaded until this point! + +# Requires supporting ruby files with custom matchers and macros, etc, in +# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are +# run as spec files by default. This means that files in spec/support that end +# in _spec.rb will both be required and run as specs, causing the specs to be +# run twice. It is recommended that you do not name files matching this glob to +# end with _spec.rb. You can configure this pattern with the --pattern +# option on the command line or in ~/.rspec, .rspec or `.rspec-local`. +# +# The following line is provided for convenience purposes. It has the downside +# of increasing the boot-up time by auto-requiring all files in the support +# directory. Alternatively, in the individual `*_spec.rb` files, manually +# require only the support files necessary. +# +# Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f } + +# Checks for pending migrations and applies them before tests are run. +# If you are not using ActiveRecord, you can remove these lines. +begin + ActiveRecord::Migration.maintain_test_schema! +rescue ActiveRecord::PendingMigrationError => e + puts e.to_s.strip + exit 1 +end +RSpec.configure do |config| + # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures + config.fixture_path = "#{::Rails.root}/spec/fixtures" + + # If you're not using ActiveRecord, or you'd prefer not to run each of your + # examples within a transaction, remove the following line or assign false + # instead of true. + config.use_transactional_fixtures = true + + # RSpec Rails can automatically mix in different behaviours to your tests + # based on their file location, for example enabling you to call `get` and + # `post` in specs under `spec/controllers`. + # + # You can disable this behaviour by removing the line below, and instead + # explicitly tag your specs with their type, e.g.: + # + # RSpec.describe UsersController, :type => :controller do + # # ... + # end + # + # The different available types are documented in the features, such as in + # https://relishapp.com/rspec/rspec-rails/docs + config.infer_spec_type_from_file_location! + + # Filter lines from Rails gems in backtraces. + config.filter_rails_from_backtrace! + # arbitrary gems may also be filtered via: + # config.filter_gems_from_backtrace("gem name") +end diff --git a/lecture_5/homework/spec/requests/buildings_spec.rb b/lecture_5/homework/spec/requests/buildings_spec.rb new file mode 100644 index 00000000..7038ef7f --- /dev/null +++ b/lecture_5/homework/spec/requests/buildings_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Buildings API', type: :request do + describe 'GET /buildings' do + it 'responds with 200' do + get '/buildings' + expect(response).to have_http_status(200) + end + end + + describe 'GET /buildings/:id' do + it 'response with 404' do + get '/buildings/1' + expect(response).to have_http_status(404) + end + end + + describe 'GET /buildings/:id' do + let(:building) { create(:building) } + + it 'response with 200' do + get "/buildings/#{building.id}" + expect(response).to have_http_status(200) + end + end +end diff --git a/lecture_5/homework/spec/services/reports/siege_report_spec.rb b/lecture_5/homework/spec/services/reports/siege_report_spec.rb new file mode 100644 index 00000000..1e367228 --- /dev/null +++ b/lecture_5/homework/spec/services/reports/siege_report_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Reports::SiegeReport, type: :service do + subject(:siege_report) { Reports::SiegeReport.new(building: building).call } + + let(:building) { create(:building) } + + describe '#call' do + context 'when building without warriors' do + it 'returns 0' do + expect(siege_report).to eq(0) + end + end + end + + describe '#call' do + context 'when building with one samurai' do + let(:warrior) { build(:warrior, building: building) } + + it 'returns 18' do + warrior.save + expect(siege_report).to eq(18) + end + end + end + + describe '#call' do + context 'when building with one hussar' do + let(:warrior) { build(:warrior, type: warrior_type, building: building) } + let(:warrior_type) { 'Warriors::Hussar' } + + it 'returns 16' do + warrior.save + expect(siege_report).to eq(16) + end + end + end + + describe '#call' do + context 'when building with two types of warriors' do + let(:warrior1) { build(:warrior, building: building) } + let(:warrior2) { build(:warrior, name: hussar_name, type: warrior_type, building: building) } + let(:hussar_name) { 'Frog Hussar' } + let(:warrior_type) { 'Warriors::Hussar' } + + it 'returns 15' do + warrior1.save + warrior2.save + expect(siege_report).to eq(15) + end + end + end +end diff --git a/lecture_5/homework/spec/spec_helper.rb b/lecture_5/homework/spec/spec_helper.rb new file mode 100644 index 00000000..68b030f2 --- /dev/null +++ b/lecture_5/homework/spec/spec_helper.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +# This file was generated by the `rails generate rspec:install` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. +# +# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +RSpec.configure do |config| + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + +# The settings below are suggested to provide a good initial experience +# with RSpec, but feel free to customize to your heart's content. +=begin + # This allows you to limit a spec run to individual examples or groups + # you care about by tagging them with `:focus` metadata. When nothing + # is tagged with `:focus`, all examples get run. RSpec also provides + # aliases for `it`, `describe`, and `context` that include `:focus` + # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + config.filter_run_when_matching :focus + + # Allows RSpec to persist some state between runs in order to support + # the `--only-failures` and `--next-failure` CLI options. We recommend + # you configure your source control system to ignore this file. + config.example_status_persistence_file_path = "spec/examples.txt" + + # Limits the available syntax to the non-monkey patched syntax that is + # recommended. For more details, see: + # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ + # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ + # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode + config.disable_monkey_patching! + + # This setting enables warnings. It's recommended, but in some cases may + # be too noisy due to issues in dependencies. + config.warnings = true + + # Many RSpec users commonly either run the entire suite or an individual + # file, and it's useful to allow more verbose output when running an + # individual spec file. + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = "doc" + end + + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + config.profile_examples = 10 + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed +=end +end diff --git a/lecture_5/homework/spec/support/factory_bot.rb b/lecture_5/homework/spec/support/factory_bot.rb new file mode 100644 index 00000000..94cf464c --- /dev/null +++ b/lecture_5/homework/spec/support/factory_bot.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +RSpec.configure do |config| + config.include FactoryBot::Syntax::Methods +end + +FactoryBot.define do + factory :building do + name { 'Western Walls' } + type { 'Buildings::Walls' } + granary { 200 } + end + + factory :clan do + name { 'Froggy' } + end + + factory :warrior do + association(:clan) + name { 'Frog Samurai' } + end +end