Initialize a Ruby class from an arbitrary hash, but only keys with matching accessors
This is what I use (I call this idiom hash-init).
def initialize(object_attribute_hash = {})
object_attribute_hash.map { |(k, v)| send("#{k}=", v) }
end
If you are on Ruby 1.9 you can do it even cleaner (send allows private methods):
def initialize(object_attribute_hash = {})
object_attribute_hash.map { |(k, v)| public_send("#{k}=", v) }
end
This will raise a NoMethodError if you try to assign to foo and method "foo=" does not exist. If you want to do it clean (assign attrs for which writers exist) you should do a check
def initialize(object_attribute_hash = {})
object_attribute_hash.map do |(k, v)|
writer_m = "#{k}="
send(writer_m, v) if respond_to?(writer_m) }
end
end
however this might lead to situations where you feed your object wrong keys (say from a form) and instead of failing loudly it will just swallow them - painful debugging ahead. So in my book a NoMethodError is a better option (it signifies a contract violation).
If you just want a list of all writers (there is no way to do that for readers) you do
some_object.methods.grep(/\w=$/)
which is "get an array of method names and grep it for entries which end with a single equals sign after a word character".
If you do
eval("@#{opt} = \"#{val}\"")
and val comes from a web form - congratulations, you just equipped your app with a wide-open exploit.
iterating over values in a block
First of all, the b
parameter in the block is nil, so you will get a
NoMethodError: undefined method `one=' for nil:NilClass`
To fix this, you can change yield if block_given?
to yield(self) if block_given?
, which will pass self
as the first parameter to the block.
If you want the b.one = ..., b.two = ...
syntax, you should use an OpenStruct
:
require 'ostruct'
class BlockClass < OpenStruct
def initialize
super
yield(self) if block_given?
end
end
You can get a dump of the internal Hash
by calling marshal_dump
:
some_block = BlockClass.new {|b|
b.one = 1
b.two = 2
b.three = 3
}
some_block.marshal_dump # => {:one=>1, :two=>2, :three=>3}
You can then iterate over the values:
some_block.marshal_dump.each_pair do |k, v|
puts "the value of #{k} is #{v}"
end
Ruby - Overload solution for initialize with 3 params
This could be a solution, but well it's ugly!
def initialize(*args)
case (args.length)
when 1
super(XXX)
@y = args[0]
when 2
if args[0].kind_of?(A) then
super(args[0])
@x = args[0]
@y = args[1]
elsif args[0].kind_of?(B) then
super(XXX)
@y = args[0]
@z = args[1]
end
when 3
super(args[0])
@x = args[0]
@y = args[1]
@z = args[2]
end
end
Looping over attributes of object of a Non-Active-Record-Model
This is the best way I found
item=self.instance_values.symbolize_keys
item.each do |k,v|
...
..
end
There is a code to show it's usage here (look at the update in the question itself) - Grouping via an element in hash
How to Make a Ruby Class act Like a Hash with Setter
You declared set_prop
, but you're using []=
in tests. Did you mean to get this?
class MyClass
attr_accessor :my_hash
def initialize(hash={})
@my_hash = hash
end
def [](key)
my_hash[key]
end
def []=(key, value)
my_hash[key] = value
end
end
test = MyClass.new({:a => 3}) # success
test[:a] # success
test[:b] = 4 # success
test.my_hash # => {:a=>3, :b=>4}
Ruby Style: How to check whether a nested hash element exists
The most obvious way to do this is to simply check each step of the way:
has_children = slate[:person] && slate[:person][:children]
Use of .nil? is really only required when you use false as a placeholder value, and in practice this is rare. Generally you can simply test it exists.
Update: If you're using Ruby 2.3 or later there's a built-in
dig
method that does what's described in this answer.
If not, you can also define your own Hash "dig" method which can simplify this substantially:
class Hash
def dig(*path)
path.inject(self) do |location, key|
location.respond_to?(:keys) ? location[key] : nil
end
end
end
This method will check each step of the way and avoid tripping up on calls to nil. For shallow structures the utility is somewhat limited, but for deeply nested structures I find it's invaluable:
has_children = slate.dig(:person, :children)
You might also make this more robust, for example, testing if the :children entry is actually populated:
children = slate.dig(:person, :children)
has_children = children && !children.empty?
Set non-database attribute for rails model without `attr_accessor`
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.
Set a Ruby variable and never be able to change it again?
They are called constants. A constant in Ruby is defined by a UPPER_CASE
name.
VARIABLE = "foo"
It is worth to mention that, technically, in Ruby there is no way to prevent a variable to be changed. In fact, if you try to re-assign a value to a constant you will get a warning, not an error.
➜ ~ irb
2.1.5 :001 > VARIABLE = "foo"
=> "foo"
2.1.5 :002 > VARIABLE = "bar"
(irb):2: warning: already initialized constant VARIABLE
(irb):1: warning: previous definition of VARIABLE was here
=> "bar"
It's also worth to note that using constants will warn you if you try to replace the value of the constant, but not if you change the constant value in place.
2.1.5 :001 > VARIABLE = "foo"
=> "foo"
2.1.5 :002 > VARIABLE.upcase!
=> "FOO"
2.1.5 :003 > VARIABLE
=> "FOO"
In order to prevent changes to the value referenced by the constant, you can freeze the value once assigned.
2.1.5 :001 > VARIABLE = "foo".freeze
=> "foo"
2.1.5 :002 > VARIABLE.upcase!
RuntimeError: can't modify frozen String
from (irb):2:in `upcase!'
from (irb):2
from /Users/weppos/.rvm/rubies/ruby-2.1.5/bin/irb:11:in `<main>'
2.1.5 :003 > VARIABLE
=> "foo"
Here's an example inside a class.
class MyClass
MY_CONSTANT = "foo"
end
MyClass::MY_CONSTANT
# => "foo"
Does enum work only for integer fields in ruby on rails?
There is a way to make enum work with string values. You can do it like this:
enum duration_type: {
days: "days",
hours: "hours"
}
But it won't help to make the code work in this case. The problem here is that ActiveRecord expects enum
to be defined on the attribute and stored in the database as a column. It's not compatible with stores.
Here is an implementation of #days?
: click. As you can see, Rails is checking self[attr]
where attr
is the name of the enum (duration_type
in our case). And self[attr]
is equal to self.attributes[attr]
. For the model Setting
attributes contains only additional_settings
, so no value found, so self.attributes[:duration_type]
gives nil
.
There is a question why a.days!
work without exception in this case then, right? Well, it's tricky. Here is an implementation of this method: click. It's basically a call to update!(attr => value)
where attr
is duration_type
and value is enum's value. Under the hood update!
calls assign_attributes
like this: s.assign_attributes(duration_type: "days")
, - which is equal to s.duration_type = "days"
. And because attr accessor is defined for duration_type
(you specified it in store
call) it writes value to additional_settings
and saves it.
Here is a test to check how it works:
# frozen_string_literal: true
require "bundler/inline"
gemfile(true) do
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
gem "activerecord", "6.0.3"
gem "sqlite3"
gem "byebug"
end
require "active_record"
require "minitest/autorun"
require "logger"
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Schema.define do
create_table :settings do |t|
t.text :additional_settings
end
end
class Setting < ActiveRecord::Base
serialize :additional_settings, JSON
store :additional_settings,
accessors: %i[duration_type remind_before],
coder: JSON
enum duration_type: { days: "days", hours: "hours" }
end
class BugTest < Minitest::Test
def test_association_stuff
s = Setting.new
s.duration_type = :days
s.save!
puts s.attributes
end
end
Related Topics
Error Running Heckle? 'Current_Code': Undefined Method 'Translate' for Ruby2Ruby
Is There a Method to Limit/Clamp a Number
Could Not Find Rake-10.0.4 in Any of the Sources (Bundler::Gemnotfound)
Duplicating a Ruby Array of Strings
Getting Ruby Function Object Itself
How to Remove Xcode 4.2 and Install 4.1 to Develop Ruby/Rails on Osx Lion
Ruby on Rails Add a Column After a Specific Column Name
Find Case-Insensitive Word Matches in a Line
Create Paperclip Attachment from Rmagick Image
How to Add New View to Ruby on Rails Spree Commerce App
Ruby -V Dyld: Library Not Loaded: /Usr/Local/Lib/Libgmp.10.Dylib
Error "Undefinded Method "Load_Defaults" " When Trying to Deploy App on Heroku
Create a Human-Readable List with "And" Inserted Before the Last Element from a Ruby List
Need to Use Add_Index on Migration for Belongs_To/Has_Many Relationship? (Rails 3.2, Active Record)
Running into Issues with Rvm During Ruby Install (1.9.2)