From e5ece0a7a3556d3f5c1bfc5631f0918e2dd45031 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Mon, 10 Feb 2025 05:35:40 -0800 Subject: [PATCH] Refactor GridSummon and add documentation and tests --- app/models/grid_summon.rb | 98 ++++++++++++- spec/models/grid_summons_spec.rb | 235 +++++++++++++++++++++++++++++++ 2 files changed, 330 insertions(+), 3 deletions(-) create mode 100644 spec/models/grid_summons_spec.rb diff --git a/app/models/grid_summon.rb b/app/models/grid_summon.rb index 5b1ed59..6bb2e8f 100644 --- a/app/models/grid_summon.rb +++ b/app/models/grid_summon.rb @@ -1,5 +1,15 @@ # frozen_string_literal: true +## +# Model representing a grid summon within a party. +# +# A GridSummon is associated with a specific {Summon} and {Party} and is responsible for +# enforcing rules on positions, uncap levels, and transcendence steps based on the associated summon’s flags. +# +# @!attribute [r] summon +# @return [Summon] the associated summon. +# @!attribute [r] party +# @return [Party] the associated party. class GridSummon < ApplicationRecord belongs_to :summon, foreign_key: :summon_id, primary_key: :id @@ -8,14 +18,35 @@ class GridSummon < ApplicationRecord inverse_of: :summons validates_presence_of :party + # Validate that position is provided. + validates :position, presence: true validate :compatible_with_position, on: :create + + # Validate that uncap_level and transcendence_step are present and numeric. + validates :uncap_level, presence: true, numericality: { only_integer: true } + validates :transcendence_step, presence: true, numericality: { only_integer: true } + + # Custom validation to enforce maximum uncap_level based on the associated Summon’s flags. + validate :validate_uncap_level_based_on_summon_flags + validate :no_conflicts, on: :create + ## + # Returns the blueprint for rendering the grid summon. + # + # @return [GridSummonBlueprint] the blueprint class for grid summons. def blueprint GridSummonBlueprint end - # Returns conflicting summons if they exist + ## + # Returns any conflicting grid summon for the given party. + # + # If the associated summon has a limit, this method searches the party's grid summons to find + # any that conflict based on the summon ID. + # + # @param party [Party] the party in which to check for conflicts. + # @return [GridSummon, nil] the conflicting grid summon if found, otherwise nil. def conflicts(party) return unless summon.limit @@ -28,13 +59,74 @@ class GridSummon < ApplicationRecord private - # Validates whether there is a conflict with the party + ## + # Validates the uncap_level based on the associated Summon’s flags. + # + # This method delegates to specific validation methods for FLB, ULB, and transcendence limits. + # + # @return [void] + def validate_uncap_level_based_on_summon_flags + return unless summon + + validate_flb_limit + validate_ulb_limit + validate_transcendence_limits + end + + ## + # Validates that the uncap_level does not exceed 3 if the associated Summon does not have the FLB flag. + # + # @return [void] + def validate_flb_limit + return unless !summon.flb && uncap_level.to_i > 3 + + errors.add(:uncap_level, 'cannot be greater than 3 if summon does not have FLB') + end + + ## + # Validates that the uncap_level does not exceed 4 if the associated Summon does not have the ULB flag. + # + # @return [void] + def validate_ulb_limit + return unless !summon.ulb && uncap_level.to_i > 4 + + errors.add(:uncap_level, 'cannot be greater than 4 if summon does not have ULB') + end + + ## + # Validates the uncap_level and transcendence_step based on whether the associated Summon supports transcendence. + # + # If the summon does not support transcendence, the uncap_level must not exceed 5 and the transcendence_step must be 0. + # + # @return [void] + def validate_transcendence_limits + return if summon.transcendence + + errors.add(:uncap_level, 'cannot be greater than 5 if summon does not have transcendence') if uncap_level.to_i > 5 + + return unless transcendence_step.to_i.positive? + + errors.add(:transcendence_step, 'must be 0 if summon does not have transcendence') + end + + ## + # Validates that there are no conflicting grid summons in the party. + # + # If a conflict is found (i.e. another grid summon exists that conflicts with this one), + # an error is added to the :series attribute. + # + # @return [void] def no_conflicts # Check if the grid summon conflicts with any of the other grid summons in the party errors.add(:series, 'must not conflict with existing summons') unless conflicts(party).nil? end - # Validates whether the summon can be added to the desired position + ## + # Validates whether the grid summon can be added to the desired position. + # + # For positions 4 and 5, the associated summon must have subaura; otherwise, an error is added. + # + # @return [void] def compatible_with_position return unless [4, 5].include?(position.to_i) && !summon.subaura diff --git a/spec/models/grid_summons_spec.rb b/spec/models/grid_summons_spec.rb new file mode 100644 index 0000000..a16a2cf --- /dev/null +++ b/spec/models/grid_summons_spec.rb @@ -0,0 +1,235 @@ +# frozen_string_literal: true + +require 'rails_helper' + +# Define a dummy GridSummonBlueprint if it is not already defined. +class GridSummonBlueprint; end unless defined?(GridSummonBlueprint) + +RSpec.describe GridSummon, type: :model do + describe 'associations' do + it 'belongs to a party' do + association = described_class.reflect_on_association(:party) + expect(association).not_to be_nil + expect(association.macro).to eq(:belongs_to) + end + + it 'belongs to a summon' do + association = described_class.reflect_on_association(:summon) + expect(association).not_to be_nil + expect(association.macro).to eq(:belongs_to) + end + end + + describe 'validations' do + let(:party) { create(:party) } + let(:default_summon) { Summon.find_by!(granblue_id: '2040433000') } + + context 'with valid attributes' do + subject do + build(:grid_summon, + party: party, + summon: default_summon, + position: 1, + uncap_level: 3, + transcendence_step: 0, + main: false, + friend: false, + quick_summon: false) + end + + it 'is valid' do + expect(subject).to be_valid + end + end + + context 'with missing required attributes' do + it 'is invalid without a position' do + grid_summon = build(:grid_summon, + party: party, + summon: default_summon, + position: nil, + uncap_level: 3, + transcendence_step: 0) + expect(grid_summon).not_to be_valid + expect(grid_summon.errors[:position].join).to match(/can't be blank/) + end + + it 'is invalid without a party' do + grid_summon = build(:grid_summon, + party: nil, + summon: default_summon, + position: 1, + uncap_level: 3, + transcendence_step: 0) + grid_summon.validate + expect(grid_summon.errors[:party].join).to match(/must exist|can't be blank/) + end + + it 'is invalid without a summon' do + grid_summon = build(:grid_summon, + party: party, + summon: nil, + position: 1, + uncap_level: 3, + transcendence_step: 0) + expect { grid_summon.valid? }.to raise_error(NoMethodError) + end + end + + context 'with non-numeric values' do + it 'is invalid when uncap_level is non-numeric' do + grid_summon = build(:grid_summon, + party: party, + summon: default_summon, + position: 1, + uncap_level: 'three', + transcendence_step: 0) + expect(grid_summon).not_to be_valid + expect(grid_summon.errors[:uncap_level]).not_to be_empty + end + + it 'is invalid when transcendence_step is non-numeric' do + grid_summon = build(:grid_summon, + party: party, + summon: default_summon, + position: 1, + uncap_level: 3, + transcendence_step: 'one') + expect(grid_summon).not_to be_valid + expect(grid_summon.errors[:transcendence_step]).not_to be_empty + end + end + + context 'custom validations based on Summon flags' do + context 'when the summon does not have FLB flag' do + let(:summon_without_flb) { default_summon.tap { |s| s.flb = false } } + + it 'is invalid if uncap_level is greater than 3' do + grid_summon = build(:grid_summon, + party: party, + summon: summon_without_flb, + position: 1, + uncap_level: 4, + transcendence_step: 0) + expect(grid_summon).not_to be_valid + expect(grid_summon.errors[:uncap_level].join).to match(/cannot be greater than 3/) + end + + it 'is valid if uncap_level is 3 or less' do + grid_summon = build(:grid_summon, + party: party, + summon: summon_without_flb, + position: 1, + uncap_level: 3, + transcendence_step: 0) + expect(grid_summon).to be_valid + end + end + + context 'when the summon does not have ULB flag' do + let(:summon_without_ulb) do + default_summon.tap do |s| + s.ulb = false + s.flb = true + end + end + + it 'is invalid if uncap_level is greater than 4' do + grid_summon = build(:grid_summon, + party: party, + summon: summon_without_ulb, + position: 1, + uncap_level: 5, + transcendence_step: 0) + expect(grid_summon).not_to be_valid + expect(grid_summon.errors[:uncap_level].join).to match(/cannot be greater than 4/) + end + + it 'is valid if uncap_level is 4 or less' do + grid_summon = build(:grid_summon, + party: party, + summon: summon_without_ulb, + position: 1, + uncap_level: 4, + transcendence_step: 0) + expect(grid_summon).to be_valid + end + end + + context 'when the summon does not have transcendence flag' do + let(:summon_without_transcendence) do + # Ensure FLB and ULB are true so that only the transcendence rule applies. + default_summon.tap do |s| + s.transcendence = false + s.flb = true + s.ulb = true + end + end + + it 'is invalid if uncap_level is greater than 5' do + grid_summon = build(:grid_summon, + party: party, + summon: summon_without_transcendence, + position: 1, + uncap_level: 6, + transcendence_step: 0) + expect(grid_summon).not_to be_valid + expect(grid_summon.errors[:uncap_level].join).to match(/cannot be greater than 5/) + end + + it 'is invalid if transcendence_step is greater than 0' do + grid_summon = build(:grid_summon, + party: party, + summon: summon_without_transcendence, + position: 1, + uncap_level: 5, + transcendence_step: 1) + expect(grid_summon).not_to be_valid + expect(grid_summon.errors[:transcendence_step].join).to match(/must be 0/) + end + + it 'is valid if uncap_level is 5 or less and transcendence_step is 0' do + grid_summon = build(:grid_summon, + party: party, + summon: summon_without_transcendence, + position: 1, + uncap_level: 5, + transcendence_step: 0) + expect(grid_summon).to be_valid + end + end + end + end + + describe 'default values' do + let(:party) { create(:party) } + let(:summon) { Summon.find_by!(granblue_id: '2040433000') } + subject do + build(:grid_summon, + party: party, + summon: summon, + position: 1, + uncap_level: 3, + transcendence_step: 0) + end + + it 'defaults quick_summon to false' do + expect(subject.quick_summon).to be_falsey + end + + it 'defaults main to false' do + expect(subject.main).to be_falsey + end + + it 'defaults friend to false' do + expect(subject.friend).to be_falsey + end + end + + describe '#blueprint' do + it 'returns the GridSummonBlueprint constant' do + grid_summon = build(:grid_summon) + expect(grid_summon.blueprint).to eq(GridSummonBlueprint) + end + end +end