ANinjaSloth
ANinjaSloth

Reputation: 39

How to call a class method in \lib from model in rails?

I'm trying to generate stats for a character created by a form. The user inputs the name, race, class, alignment, and whether or not the stats will be generated randomly, or prioritized (values being assigned from highest to lowest). The form works flawlessly, as I can see the output in a view.

What I am now trying to do is call a method from a class in /lib in the model that will generate the stats; however, I keep getting the following error (I can't post pictures):

NoMethodError in CharactersController#create 


undefined method `[]' for nil:NilClass


Extracted source (around line #14): 


12  before_save do
13      generate_stats
14      self.strength = @character_stats[:strength]
15      self.dexterity = @character_stats[:dexterity]
16      self.constitution = @character_stats[:constitution]
17      self.intelligence = @character_stats[:intelligence]

Here is a copy of some of my code:

In controllers\characters_controller.rb

class CharactersController < ApplicationController
    def create
        @character = Character.new(character_info_params)
        @character.name = params[:character][:name].capitalize

        @character.alignment = "#{params[:character][:alignment_lr]} #{params[:character][:alignment_ud]}"


        if @character.save
            redirect_to @character
        else
            render 'new'
        end
    end
    private

    def character_info_params
        params.require(:character).permit(:name, :race, :class_, :alignment)
    end
end

In models\character.rb

class Character < ActiveRecord::Base
    require 'random_stats_generator'    
    attr_accessor :rand_stat_gen

    def generate_stats
        if @rand_stat_gen == true
            @character_stats_inst = RandomStatGenerator.new
            @character_stats = @character_stats_inst.generate
        end
    end

    before_save do
        generate_stats
        self.strength = @character_stats[:strength]
        self.dexterity = @character_stats[:dexterity]
        self.constitution = @character_stats[:constitution]
        self.intelligence = @character_stats[:intelligence]
        self.wisdom = @character_stats[:wisdom]
        self.charisma = @character_stats[:charisma]
    end

#validation passed this point

end

In initializers\stat_builders.rb

require "./lib/random_stat_generator.rb"

In lib/random_stat_generator.rb

class RandomStatGenerator

    def initialize
        @strength = :strength
        @dexterity = :dexterity
        @constitution = :constitution
        @intelligence = :intelligence
        @wisdom = :wisdom
        @charisma = :charisma
        @character_stats = HashWithIndifferentAccess.new()
    end

    def self.generate
        roll_stats
    end

    def roll(stat)
        @roll_value_1 = (1 + (rand(6)))
        @roll_value_2 = (1 + (rand(6)))
        @roll_value_3 = (1 + (rand(6)))
        @roll_value_4 = (1 + (rand(6)))
        @roll_array = [@roll_value_1,@roll_value_2,@roll_value_3,@roll_value_4]
        @roll_array = @roll_array.sort_by {|x| x }
        @roll_array = @roll_array.reverse
        stat = @roll_array[0] + @roll_array[1] + @roll_array[2]
    end

    def roll_stats
        @strength = roll(@strength)
        @dexterity = roll(@dexterity)
        @constitution = roll(@constitution)
        @intelligence = roll(@intelligence)
        @wisdom = roll(@wisdom)
        @charisma = roll(@charisma)

        @character_stats[:strength] = @strength
        @character_stats[:dexterity] = @dexterity
        @character_stats[:constitution] = @constitution
        @character_stats[:intelligence] = @intelligence
        @character_stats[:wisdom] = @wisdom
        @character_stats[:charisma] = @charisma

        return @character_stats
    end
end

To me, it looks like the method isn't returning anything, or isn't being called at all.

I've tried a lot of solutions that I've come across online, none of them working. There may be some things that don't really make sense that are left over from these solutions. I'm only just starting with rails, so I'm still trying to get used to everything.

Thanks a lot for your help.

Upvotes: 0

Views: 1572

Answers (2)

max
max

Reputation: 102250

Ruby has really powerful functions for manipulating both hashes and arrays. Typing out duplicate assignments like:

self.strength = @character_stats[:strength]
self.dexterity = @character_stats[:dexterity]
self.constitution = @character_stats[:constitution]

Is pretty dull. So instead we can simply rewrite the methods to pass hashes around.

class RandomStatGenerator

  # This is just a constant containing all the stats we want to generate.
  STATS = [:strength, :dexterity, :constitution, :intelligence, :wisdom, :charisma]

  # Create a hash with random roll values for each stat
  def self.roll_stats
    # This is kind of scary looking but actually just creates an 
    # hash from an array of keys
    Hash[STATS.map {|k| [k, self.roll ] } ]
  end

  private

  def self.roll
    # Create an array with 4 elements (nil) 
    ary = Array.new(4)
    # We then replace the nil value with a random value 1-6
    ary = ary.map do 
      (1 + (rand(6)))
    end
    # sort it and drop the lowest roll. return the sum of all rolls.
    ary.sort.drop(1).sum

    # a ruby ninja writes it like this
    Array.new(4).map { 1 + rand(6) }.sort.drop(1).sum
  end
end

Output:

irb(main):032:0> RandomStatGenerator.roll_stats
=> {:strength=>14, :dexterity=>14, :constitution=>14, :intelligence=>13, :wisdom=>10, :charisma=>9}

But if you don't intend to actually create instances of a class, than you should use a module instead.

Rails models can either be created with a hash or you can replace its values with a hash:

Character.new(RandomStatGenerator.roll_stats)
@character.assign_attributes(RandomStatGenerator.roll_stats)

So we can use this in Character#generate_stats:

def generate_stats
  assign_attributes(RandomStatGenerator.roll_stats)
end

You should use ActiveModel callbacks with extreme prejudice. It is often quite a challenge to regulate where in your application and when in the model lifetime. Since before_save runs after validations means that any validations like validates_presence_of :constitution will fail.

In your case it might be better to simply do it in the controller or use:

before_validation :generate_stats, if: -> { new_record? && @rand_stat_gen }

Upvotes: 1

Cyril Duchon-Doris
Cyril Duchon-Doris

Reputation: 14029

I would like to suggest the following organisation fo your library

# Use a module at top level
module RandomStatGenerator

    STATS = [:strength, :dexterity, :constitution, :intelligence, :wisdom, :charisma]

    # Use a class Stats if you need to but I don't see why...
    class Stats
        def initialize
            RandomStatGenerator::STATS.each do |stat|
                # Below line will do @stat = :stat
                instance_variable_set("@#{stat.to_s}", stat)
            @character_stats = HashWithIndifferentAccess.new()
        end

        def roll_stats
            @character_stats = RandomStatGenerator.roll_stats
        end
    end

    module_function
    # below lines will be considered as module functions 
    # => call RandomStatGenerator.function_name

    def roll
        roll_value_1 = (1 + (rand(6)))
        roll_value_2 = (1 + (rand(6)))
        roll_value_3 = (1 + (rand(6)))
        roll_value_4 = (1 + (rand(6)))
        roll_array = [roll_value_1,roll_value_2,roll_value_3,roll_value_4]
        roll_array = roll_array.sort_by {|x| x }
        roll_array = roll_array.reverse
        roll_array[0] + roll_array[1] + roll_array[2]
    end

    def roll_stats
        character_stats = {}
        STATS.each do |stat|
            character_stats[stat] = RandomStatGenerator.roll
        end

        return character_stats
    end
end

Then in your character.rb

def generate_stats
    @character_stats = RandomStatGenerator.roll_stats
end

Upvotes: 1

Related Questions