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..07436e96 100644 --- a/lecture_5/homework/Gemfile +++ b/lecture_5/homework/Gemfile @@ -35,7 +35,10 @@ gem 'active_model_serializers' group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console + gem 'awesome_print' gem 'byebug', platforms: %i[mri mingw x64_mingw] + gem 'database_cleaner' + gem 'factory_bot_rails' end group :development do diff --git a/lecture_5/homework/Gemfile.lock b/lecture_5/homework/Gemfile.lock index 411593e8..8baee5bf 100644 --- a/lecture_5/homework/Gemfile.lock +++ b/lecture_5/homework/Gemfile.lock @@ -49,6 +49,7 @@ GEM tzinfo (~> 1.1) arel (9.0.0) ast (2.4.0) + awesome_print (1.8.0) bootsnap (1.4.1) msgpack (~> 1.0) builder (3.2.3) @@ -57,8 +58,14 @@ GEM activesupport concurrent-ruby (1.1.5) crass (1.0.4) + database_cleaner (1.7.0) 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) globalid (0.4.2) activesupport (>= 4.2.0) @@ -177,8 +184,11 @@ PLATFORMS DEPENDENCIES active_model_serializers + awesome_print bootsnap (>= 1.1.0) byebug + database_cleaner + factory_bot_rails listen (>= 3.0.5, < 3.2) pg puma (~> 3.11) diff --git a/lecture_5/homework/app/controllers/buildings_controller.rb b/lecture_5/homework/app/controllers/buildings_controller.rb index 9b7e9677..a0a7bd4c 100644 --- a/lecture_5/homework/app/controllers/buildings_controller.rb +++ b/lecture_5/homework/app/controllers/buildings_controller.rb @@ -1,7 +1,25 @@ # frozen_string_literal: true class BuildingsController < ApplicationController - def index; end + def index + render json: buildings, status: 200 + end - def show; end + def show + render json: building + end + + private + + def buildings + @buildings ||= fetch_buildings.present? ? fetch_buildings : NoBuildings.new.buildings + end + + def fetch_buildings + BuildingsQueries.buildings(relation: Building) + end + + def building + @building ||= BuildingsQueries.building(relation: Building, id: 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..acab2d6a 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 diff --git a/lecture_5/homework/app/nulls/no_buildings.rb b/lecture_5/homework/app/nulls/no_buildings.rb new file mode 100644 index 00000000..4930fd08 --- /dev/null +++ b/lecture_5/homework/app/nulls/no_buildings.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class NoBuildings + def buildings + { message: 'There are no buildings' } + end +end diff --git a/lecture_5/homework/app/queries/buildings_queries.rb b/lecture_5/homework/app/queries/buildings_queries.rb new file mode 100644 index 00000000..0d0e605d --- /dev/null +++ b/lecture_5/homework/app/queries/buildings_queries.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class BuildingsQueries + def self.buildings(relation:) + relation.includes(:warriors).all + end + + def self.building(relation:, id:) + relation.find(id) + end + + def self.count_warriors_of_specified_type(relation:, building:, type:) + relation.find(building.id).warriors.where(type: type).count + end + + def self.update_siege_ability(relation:, building:, result:) + relation.find(building.id).update(siege_ability: result) + end +end diff --git a/lecture_5/homework/app/serializers/building_serializer.rb b/lecture_5/homework/app/serializers/building_serializer.rb index 899c9aa0..92430c53 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, :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..b6d1be4f 100644 --- a/lecture_5/homework/app/services/reports/siege_report.rb +++ b/lecture_5/homework/app/services/reports/siege_report.rb @@ -3,10 +3,69 @@ module Reports class SiegeReport def initialize(building:) + @building = building end def call - raise NotImprementedYet + no_army? ? no_siege_ability : compute_siege_ability + end + + private + + def no_army? + @building.warriors.empty? + end + + def no_siege_ability + save_siege_report_result(0) + @building.siege_ability = 0 + end + + def compute_siege_ability + daily_food_demand = compute_daily_food_demand + result = siege_ability(daily_food_demand) + save_siege_report_result(result) + end + + def compute_daily_food_demand + stronghold_staff + hussars + samurais + end + + def stronghold_staff + 10 + end + + def hussars + BuildingsQueries.count_warriors_of_specified_type( + relation: Building, + building: @building, + type: 'Warriors::Hussar' + ) * 2 + end + + def samurais + BuildingsQueries.count_warriors_of_specified_type( + relation: Building, + building: @building, + type: 'Warriors::Samurai' + ) + end + + def siege_ability(daily_food_demand) + granary / daily_food_demand + end + + def granary + @building.granary + end + + def save_siege_report_result(result) + BuildingsQueries.update_siege_ability( + relation: Building, + building: @building, + result: result + ) + @building.siege_ability = result end end end diff --git a/lecture_5/homework/config/initializers/active_model_serializers.rb b/lecture_5/homework/config/initializers/active_model_serializers.rb index 6f857680..56035bc5 100644 --- a/lecture_5/homework/config/initializers/active_model_serializers.rb +++ b/lecture_5/homework/config/initializers/active_model_serializers.rb @@ -1,3 +1,4 @@ # frozen_string_literal: true ActiveModelSerializers.config.adapter = :json_api +ActiveModel::Serializer.config.key_transform = :underscore diff --git a/lecture_5/homework/db/migrate/20190425195137_add_siege_ability_to_buildings.rb b/lecture_5/homework/db/migrate/20190425195137_add_siege_ability_to_buildings.rb new file mode 100644 index 00000000..ce361b71 --- /dev/null +++ b/lecture_5/homework/db/migrate/20190425195137_add_siege_ability_to_buildings.rb @@ -0,0 +1,5 @@ +class AddSiegeAbilityToBuildings < ActiveRecord::Migration[5.2] + def change + add_column :buildings, :siege_ability, :integer + end +end diff --git a/lecture_5/homework/db/migrate/20190428121458_compute_siege_abilities_of_buildings.rb b/lecture_5/homework/db/migrate/20190428121458_compute_siege_abilities_of_buildings.rb new file mode 100644 index 00000000..d9eb2f4d --- /dev/null +++ b/lecture_5/homework/db/migrate/20190428121458_compute_siege_abilities_of_buildings.rb @@ -0,0 +1,7 @@ +class ComputeSiegeAbilitiesOfBuildings < ActiveRecord::Migration[5.2] + def change + Building.find_each do |building| + Reports::SiegeReport.new(building: building).call + end + end +end diff --git a/lecture_5/homework/db/schema.rb b/lecture_5/homework/db/schema.rb index 13f968ab..80b5adc2 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_28_121458) 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" end create_table "clans", force: :cascade do |t| diff --git a/lecture_5/homework/spec/factories/buildings.rb b/lecture_5/homework/spec/factories/buildings.rb new file mode 100644 index 00000000..a379ba96 --- /dev/null +++ b/lecture_5/homework/spec/factories/buildings.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :building, class: Building do + sequence(:name) { |i| "Building_#{i}" } + sequence(:type) { |i| "Buildings::#{i.even? ? 'Walls' : 'Stronghold'}" } + granary { 100 } + end +end diff --git a/lecture_5/homework/spec/factories/clans.rb b/lecture_5/homework/spec/factories/clans.rb new file mode 100644 index 00000000..106801f7 --- /dev/null +++ b/lecture_5/homework/spec/factories/clans.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :clan, class: Clan do + sequence(:name) { |i| "Clan number #{i}" } + end +end diff --git a/lecture_5/homework/spec/factories/warriors.rb b/lecture_5/homework/spec/factories/warriors.rb new file mode 100644 index 00000000..b1c49f28 --- /dev/null +++ b/lecture_5/homework/spec/factories/warriors.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :warrior, class: Warrior do + association(:clan) + association(:building) + sequence(:type) { |i| i.even? ? 'Warriors::Hussar' : 'Warriors::Samurai' } + sequence(:name) { |i| "Warrior #{i}" } + preferred_weapon_kind { 'melee' } + armor_quality { 0 } + number_of_battles { 0 } + join_date { Time.now } + death_date { nil } + + trait :hussar do + type { 'Warriors::Hussar' } + end + + trait :samurai do + type { 'Warriors::Samurai' } + end + end +end diff --git a/lecture_5/homework/spec/rails_helper.rb b/lecture_5/homework/spec/rails_helper.rb new file mode 100644 index 00000000..429ba04c --- /dev/null +++ b/lecture_5/homework/spec/rails_helper.rb @@ -0,0 +1,69 @@ +# 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', __dir__) +# Prevent database truncation if the environment is production +abort('The Rails environment is running in production mode!') if Rails.env.production? +require 'rspec/rails' +# 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" + + config.include FactoryBot::Syntax::Methods + + # 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") + + config.before(:example) do + DatabaseCleaner.clean_with(:truncation) + end +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..e78f547f --- /dev/null +++ b/lecture_5/homework/spec/requests/buildings_spec.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Buildings API', type: :request do + describe 'GET /buildings' do + subject { get '/buildings' } + + it 'responds with 200' do + subject + expect(response).to have_http_status(200) + end + + context 'when there are no Buildings' do + it 'return "There are no buildings" message' do + subject + response_json = JSON.parse(response.body) + expect(response_json['message']).to include('There are no buildings') + end + end + + context 'with existing Buildings' do + before { create_list(:building, 4) } + + it 'responds with 200' do + subject + expect(response).to have_http_status(200) + end + + it 'includes Building data' do + subject + response_json = JSON.parse(response.body) + expect(response_json['data'].size).to eq(4) + end + end + end + + describe 'GET /buildings/:id' do + let(:building_id) { 1 } + subject { get "/buildings/#{building_id}" } + + context 'when Building is not found' do + it 'responds with 404' do + subject + expect(response).to have_http_status(404) + end + + it 'includes the "Couldn\'t find Building with \'id\'=1" message' do + subject + response_json = JSON.parse(response.body) + expect(response_json['error']).to include("Couldn't find Building with 'id'=1") + end + end + + context 'when Building is found' do + before { create(:building, id: building_id, name: 'Building_1') } + + it 'responds with 200' do + subject + expect(response).to have_http_status(200) + end + + it 'returns data of Building with id = 1' do + subject + response_json = JSON.parse(response.body) + expect(response_json['data']).to include( + 'id' => building_id.to_s, + 'attributes' => { + 'name' => 'Building_1', + 'siege_ability' => nil + } + ) + end + 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..4934e9f4 --- /dev/null +++ b/lecture_5/homework/spec/services/reports/siege_report_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Reports::SiegeReport, type: :service do + subject(:siege_report) { described_class.new(params).call } + + let(:params) { { building: building } } + let(:building) { create(:building, warriors: army) } + let(:army) { [] } + + describe '#call' do + context 'with building without army' do + it 'returns report with siege ability equals to 0' do + expect(siege_report).to eq(0) + end + end + + context 'with building which has an army' do + context 'when 10 samurais' do + let(:army) { create_list(:warrior, 10, :samurai) } + + it 'returns report with siege ability equals to 5' do + expect(siege_report).to eq(5) + end + end + + context 'when 10 hussars' do + let(:army) { create_list(:warrior, 10, :hussar) } + + it 'returns report with siege ability equals to 3' do + expect(siege_report).to eq(3) + end + end + + context 'when 5 hussars and 5 samurais' do + let(:army) { create_list(:warrior, 10) } + + it 'returns report with siege ability equals to 4' do + expect(siege_report).to eq(4) + end + end + + context 'when the number of warriors exceeds daily granary abilities' do + let(:army) { create_list(:warrior, 100) } + + it 'returns report with siege ability equals to 0' do + expect(siege_report).to eq(0) + end + end + end + + context 'with building which has an army but the granary is empty' do + let(:building) { create(:building, warriors: army, granary: 0) } + let(:army) { create_list(:warrior, 10) } + + it 'returns report with siege ability equals to 0' do + expect(siege_report).to eq(0) + 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..01f7c974 --- /dev/null +++ b/lecture_5/homework/spec/spec_helper.rb @@ -0,0 +1,96 @@ +# 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. + # # 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! + # + # # 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