Passing in a hash to a class without using an initialization method - ruby-on-rails

I was under the, apparently incorrect, impression that when I pass a hash into a class the class requires an initialization method like this:
class Dog
attr_reader :sound
def initialize(params = {})
#sound = params[:sound]
end
end
dog = Dog.new({sound:"woof"})
puts dog.sound
But I've run into a bit of code (for creating a password digest) that works within a rails application that doesn't use an initialization method and seems to work just fine and it's kind of confuses me because when I try this anywhere else it doesn't seem to work. Here is the sample code that works (allows me to pass in a hash and initializes without an initialization method):
class User < ActiveRecord::Base
attr_reader :password
validates :email, :password_digest, presence: true
validates :password, length: { minimum: 6, allow_nil: true }
def password=(pwd)
#password = pwd
self.password_digest = BCrypt::Password.create(pwd)
end
end
NOTE: In the create action I pass in a hash via strong params from a form that, at the end of the day, looks something like this {"email"=>"joeblow#gmail.com", "password"=>"holymolycanoli”}
In this bit of code there is no initialization method. When I try something like this (passing in a hash without an initialization method) in pry or in a repl it doesn't seem to work (for instance the following code does not work):
class Dog
attr_reader :sound
def sound=(pwd)
#sound = pwd
end
end
dog = Dog.new({sound:"woof"})
puts dog.sound
The error I get is:
wrong number of arguments (1 for 0)
Is it rails that allows me to pass in hashes like this or ActiveRecord? I'm confused as to why it works within rails within this context but generates an error outside of rails. Why does this work in rails?

If you look at the top you have this:
class Dog < ActiveRecord::Base
This causes your class Dog to inherit from ActiveRecord::Base
when it does so it gains a bunch of methods that allows you to set things up.
Now when you call for example:
Dog.create(password: 'some_password', username: 'some_username')
Your calling a method on the class object that then returns an instance of the class.
so taking your example
class Dog
attr_reader :sound
def sound=(pwd)
#sound = pwd
end
def self.create data_hash
new_dog = self.new #create new instance of dog class
new_dog.sound = data_hash[:sound] #set instance of dog classes sound
new_dog # return instance of dog class
end
end
It's essentially what we would term a factory method, a method that takes in data and returns an object based on that data.
Now I have no doubt that ActiveRecord::Base is doing something more complicated than that but that is essentially what it's doing at the most basic of levels.
I'd also like to point out that when inheriting from ActiveRecord::Base your also inheriting its 'initialize' method so you don't have to set one yourself.
The class knows what attribute methods to create based on the schema you set when you did the DB migrations for a table that matches (through rail's conventions) the class.

A lot of things happen when you subclass ActiveRecord::Base. Before looking at other issues I'm guessing that Dog is a rails ActiveRecord model and you just forgot to add
class Dog < ActiveRecord::Base

Related

Rails - attr_accessor :- read the attribute value instead of the function overriding it

I have a model Customer that accepts a virtual attribute opening_balance. Code looks something like this:
model Customer < ApplicationRecord
attr_accessor :opening_balance
has_one :ledger_account
after_create :open_ledger_account
def opening_balance
ledger_account.opening_balance if ledger_account.present?
end
private
def open_ledger_account
create_ledger_account!(opening_balance: self.opening_balance)
end
But the issue here is self.opening_balance is calling the method defined in the class not the value stored in attr_accessor's opening_balance attribute.
I tried one more solution:
def open_ledger_account
create_ledger_account!(opening_balance: self.read_attribute(:opening_balance))
end
But this also doesn't work.
How to read the value stored in the actual attribute? Any help would be appreciated. I am using rails 5.1.
Thanks!
attr_accessor defines a instance variable and a method to access it (read/write).
So the easy way is to write:
def open_ledger_account
create_ledger_account!(opening_balance: #opening_balance)
end
The read_attribute would only work if opening_balance was an attribute in the database on Customer.
First you have to understand that attr_accessor does not define instance variables. It just creates setter and getter methods. What attr_accessor :name does is:
class Person
def name
#name
end
def name=(value)
#name = value
end
end
Now you can access the instance variable from the outside:
p = Person.new
p.name = 'Jane'
puts p.name
And you can also access the instance variable from the inside by using the getter method instead of #name:
class Person
attr_accessor :name
def hello
"hello my name is: #{name}"
end
end
attr_accessor does not "define" a instance variable. There is no declaration of members/attributes in Ruby like in for example Java. An instance variables is declared when it is first set. Accessing an instance variable that has not been assigned a value returns nil.
So whats happing here:
class Customer < ApplicationRecord
attr_accessor :opening_balance
# ...
def opening_balance
ledger_account.opening_balance if ledger_account.present?
end
end
Is that you are overwriting the getter created by attr_accessor. If you want to access the instance variable itself just use #opening_balance.
However...
You should just use delegate instead:
class Customer < ApplicationRecord
has_one :ledger_account
delegate :opening_balance, to: :ledger_account
end

Set non-database attribute for rails model without `attr_accessor`

In PHP, I can set an attribute (that is not a column in database) to a model. E.g.(PHP code),
$user = new User;
$user->flag = true;
But in rails, when I set any attribute that doesn't exist in database, it will throw an error undefined method flag. There is attr_accessor method, but what will happen if I need about ten temp attributes?
but what will happen if I need about ten temp attributes?
#app/models/user.rb
class User < ActiveRecord::Base
attr_accessor :flag, :other_attribute, :other_attribute2, :etc...
end
attr_accessor creates "virtual" attributes in Rails -- they don't exist in the database, but are present in the model.
As with attributes from the db, attr_accessor just creates a set of setter & getter (instance) methods in your class, which you can call & interact with when the class is initialized:
#app/models/user.rb
class User < ActiveRecord::Base
attr_accessor :flag
# getter
def flag
#flag
end
# setter
def flag=(val)
#flag = val
end
end
This is expected because it's how ActiveRecord works by design. If you need to set arbitrary attributes, then you have to use a different kind of objects.
For example, Ruby provides a library called OpenStruct that allows you to create objects where you can assign arbitrary key/values. You may want to use such library and then convert the object into a corresponding ActiveRecord instance only if/when you need to save to the database.
Don't try to model ActiveRecord to behave as you just described because it was simply not designed to behave in that way. That would be a cargo culting error from your current PHP knowledge.
As the guys explained, attr_accessor is just a quick setter and getter.
We can set our Model attr_accessor on record initializing to be a Ruby#Hash for example using ActiveRecord#After_initilize method so we get more flexibility on temporarily storing values (idea credit to this answer).
Something like:
class User < ActiveRecord::Base
attr_accessor :vars
after_initialize do |user|
self.vars = Hash.new
end
end
Now you could do:
user = User.new
#set
user.vars['flag'] = true
#get
user.vars['flag']
#> true
All that attr_accessor does is add getter and setter methods which use an instance variable, eg this
attr_accessor :flag
will add these methods:
def flag
#flag
end
def flag=(val)
#flag = val
end
You can write these methods yourself if you want, and have them do something more interesting than just storing the value in an instance var, if you want.
If you need temp attributes you can add them to the singleton object.
instance = Model.new
class << instance
attr_accessor :name
end

Rails STI class auto initialize

I'm trying to make a STI Base model which changes automatically to inherited class like that:
#models/source/base.rb
class Source::Base < ActiveRecord::Base
after_initialize :detect_type
private
def detect_type
if (/(rss)$/ ~= self.url)
self.type = 'Source::RSS'
end
end
end
#models/source/rss.rb
class Source::RSS < Source::Base
def get_content
puts 'Got content from RSS'
end
end
And i want such behavior:
s = Source::Base.new(:url => 'http://stackoverflow.com/rss')
s.get_content #=> Got content from RSS
s2 = Source::Base.first # url is also ending rss
s2.get_content #=> Got content from RSS
There are (at least) three ways to do this:
1. Use a Factory method
#Alejandro Babio's answer is a good example of this pattern. It has very few downsides, but you have to remember to always use the factory method. This can be challenging if third-party code is creating your objects.
2. Override Source::Base.new
Ruby (for all its sins) will let you override new.
class Source::Base < ActiveRecord::Base
def self.new(attributes)
base = super
return base if base.type == base.real_type
base.becomes(base.real_type)
end
def real_type
# type detection logic
end
end
This is "magic", with all of the super cool and super confusing baggage that can bring.
3. Wrap becomes in a conversion method
class Source::Base < ActiveRecord::Base
def become_real_type
return self if self.type == self.real_type
becomes(real_type)
end
def real_type
# type detection logic
end
end
thing = Source::Base.new(params).become_real_type
This is very similar to the factory method, but it lets you do the conversion after object creation, which can be helpful if something else is creating the object.
Another option would be to use a polymorphic association, your classes could look like this:
class Source < ActiveRecord::Base
belongs_to :content, polymorphic: true
end
class RSS < ActiveRecord::Base
has_one :source, as: :content
validates :source, :url, presence: true
end
When creating an instance you'd create the the source, then create and assign a concrete content instance, thus:
s = Source.create
s.content = RSS.create url: exmaple.com
You'd probably want to accepts_nested_attributes_for to keep things simpler.
Your detect_type logic would sit either in a controller, or a service object. It could return the correct class for the content, e.g. return RSS if /(rss)$/ ~= self.url.
With this approach you could ask for Source.all includes: :content, and when you load the content for each Source instance, Rails' polymorphism will instanciate it to the correct type.
If I were you I would add a class method that returns the right instance.
class Source::Base < ActiveRecord::Base
def self.new_by_url(params)
type = if (/(rss)$/ ~= params[:url])
'Source::RSS'
end
raise 'invalid type' unless type
type.constantize.new(params)
end
end
Then you will get the behavior needed:
s = Source::Base.new_by_url(:url => 'http://stackoverflow.com/rss')
s.get_content #=> Got content from RSS
And s will be an instance of Source::RSS.
Note: after read your comment about becomes: its code uses klass.new. And new is a class method. After initialize, your object is done and it is a Source::Base, and there are no way to change it.

Rails model -- non persistent class member or property?

Is it possible / advisable to have a member of a class that is not persisted to the database for a rails model?
I want to store the last type the user selects in a session variable. Since I cant set the session variable from my model, I want to store the value in a "dummy" class member that just passes the value back to the controller.
Can you have such a class member?
Adding non-persisted attributes to a Rails model is just like any other Ruby class:
class User < ActiveRecord::Base
attr_accessor :someattr
end
me = User.new(name: 'Max', someattr: 'bar')
me.someattr # "bar"
me.someattr = 'foo'
The extended explanation:
In Ruby all instance variables are private and do not need to be defined before assignment.
attr_accessor creates a setter and getter method:
class User < ActiveRecord::Base
def someattr
#someattr
end
def someattr=(value)
#someattr = value
end
end
There is one special thing going on here; Rails takes the hash you pass to User.new and maps the values to attributes. You could simulate this behavior in a plain ruby class with something like:
class Foo
attr_accessor :bar
def initialize(hash)
hash.keys.each do |key|
setter = "#{key}=".intern
self.send(setter, hash[key]) if self.respond_to? setter
end
end
end
> Foo.new(bar: 'baz')
=> <Foo:0x0000010112aa50 #bar="baz">
Classes in Ruby can also be re-opened at any point, ActiveRecord uses this ability to "auto-magically" add getters and setters to your models based on its database columns (ActiveRecord figures out which attributes to add based on the database schema).
Yes you can, the code below allows you to set my_class_variable and inside the model reference it as #my_class_variable
class MyCLass < ActiveRecord::Base
attr_accessor :my_class_variable
def do_something_with_it
#my_class_variable + 10
end

Rails model attr_accessor attribute not saving?

Here is the structure I'm working with:
app/models/model.rb
class Model < ActiveRecord::Base
attr_accessor :some_var
end
app/models/model_controller.rb
class ModelsController < ApplicationController
def show
#model = Model.find(params[:id])
#other_var
if #model.some_var.nil?
#model.some_var = "some value"
#other_var = "some value"
else
#other_var = #model.some_var
end
end
end
Whenever I run this code (e.g. the show method), the if clause is evaluated to be true (e.g. #model.some_var == nil). How do I get around this? Is there something wrong in my assumption of how attr_accessor works?
attr_accessor is a built-in Ruby macro which will define a setter and a getter for an instance variable of an object, and doesn't have anything to do with database columns with ActiveRecord instances. For example:
class Animal
attr_accessor :legs
end
a = Animal.new
a.legs = 4
a.legs #=> 4
If you want it to be saved to the database, you need to define a column in a migration. Then ActiveRecord will create the accessor methods automatically, and you can (should) remove your attr_accessor declaration.

Resources