Initialize a Ruby Class from an Arbitrary Hash, But Only Keys with Matching Accessors

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



Leave a reply



Submit