How can I securely store a ruby hash in Rails? - ruby-on-rails

I need to map to different logic based on a unique id parameter. I essentially want to do the following:
{
'id123' => send(:foo),
'id789' => send(:bar)
}
I wouldn't want to hardcode the variables, and storing each ID in a separate environment variable is tedious and difficult to keep track of. The best solution I can think of is converting the hash to a string to store as an env var, then converting back to a hash when I initialize the app but I'm wondering if there is a better way?

You could use Rails custom configuration with Rails::Application.config_for:
# config/id_to_method_mapping.yml:
id123: "foo"
id789: "bar"
# config/application.rb
module MyApp
class Application < Rails::Application
config.id_to_method_mapping = config_for(:id_to_method_mapping)
end
end
# your controller
method_name = Rails.configuration.id_to_method_mapping[params[:id]]
send(method_name)
You can .gitignore the config file if you don't want it to be in the codebase.

Not sure if I've understood your question correctly but for your own solution of converting the hash, you can use Object Marshaling in Ruby to serialize any object (e.g. Hash). That's what some libraries like Sidekiq do and store the serialized object, for example in Redis.
class Demo
def initialize(name)
#name = name
end
def to_s
p "Serializing #{#name}"
end
end
obj = Demo.new("something...")
obj_d = Marshal.dump(o)
obj2 = Marshal.load(obj_d)
obj2.to_s
And you can always use eval to dynamically evaluate an expression: eval("obj2.to_s").

Related

Rails ActiveAttr Gem, manipulation of attributes within Class?

I have a Rails 5 class which includes ActiveAttr::Model, ActiveAttr:MassAssignment and ActiveAttr::AttributeDefaults.
It defines a couple of attributes using the method attribute and has some instance methods. I have some trouble manipulating the defined attributes. My problem is how to set an attribute value within the initializer. Some code:
class CompanyPresenter
include ActiveAttr::Model
include ActiveAttr::MassAssignment
include ActiveAttr::AttributeDefaults
attribute :identifier
# ...
attribute :street_address
attribute :postal_code
attribute :city
attribute :country
# ...
attribute :logo
attribute :schema_org_identifier
attribute :productontology
attribute :website
def initialize(attributes = nil, options = {})
super
fetch_po_field
end
def fetch_po_field
productontology = g_i_f_n('ontology') if identifier
end
def uri
#uri ||= URI.parse(website)
end
# ...
end
As I have written it, the method fetch_po_field does not work, it thinks that productontology is a local variable (g_i_f_n(...) is defined farther down, it works and its return value is correct). The only way I have found to set this variable is to write self.productontology instead. Moreover, the instance variable #uri is not defined as an attribute, instead it is written down only in this place and visible from outside.
Probably I have simply forgotten the basics of Ruby and Rails, I've done this for so long with ActiveRecord and ActiveModel. Can anybody explain why I need to write self.productontology, using #productontology doesn't work, and why my predecessor who wrote the original code mixed the # notation in #uri with the attribute-declaration style? I suppose he must have had some reason to do it like this.
I am also happy with any pointers to documentation. I haven't been able to find docs for ActiveAttr showing manipulation of instance variables in methods of an ActiveAttr class.
Thank you :-)
To start you most likely don't need the ActiveAttr gem as it really just replicates APIs that are already available in Rails 5.
See https://api.rubyonrails.org/classes/ActiveModel.html.
As I have written it, the method fetch_po_field does not work, it thinks that productontology is a local variable.
This is really just a Ruby thing and has nothing to do with the Rails Attributes API or the ActiveAttr gem.
When using assignment you must explicitly set the recipient unless you want to set a local variable. This line:
self.productontology = g_i_f_n('ontology') if identifier
Is actually calling the setter method productontology= on self using the rval as the argument.
Can anybody explain why I need to write self.productontology, using
#productontology doesn't work
Consider this plain old ruby example:
class Thing
def initialize(**attrs)
#storage = attrs
end
def foo
#storage[:foo]
end
def foo=(value)
#storage[:foo] = value
end
end
irb(main):020:0> Thing.new(foo: "bar").foo
=> "bar"
irb(main):021:0> Thing.new(foo: "bar").instance_variable_get("#foo")
=> nil
This looks quite a bit different then the standard accessors you create with attr_accessor. Instead of storing the "attributes" in one instance variable per attribute we use a hash as the internal storage and create accessors to expose the stored values.
The Rails attributes API does the exact same thing except its not just a simple hash and the accessors are defined with metaprogramming. Why? Because Ruby does not let you track changes to simple instance variables. If you set #foo = "bar" there is no way the model can track the changes to the attribute or do stuff like type casting.
When you use attribute :identifier you're writing both the setter and getter instance methods as well as some metadata about the attribute like its "type", defaults etc. which are stored in the class.

How self[:name] works rails

I created a reader method in my users model
def name
self[:name]
end
I'm having a hard time understanding self[:name]
it looks like I'm accessing a value with a key in a Hash but from what i can tell its not a Hash.
I have also tried to create classes in ruby to emulate this but cant get them to work so i"m not sure whether this is ruby or rails thing that I'm not understanding.
ActiveRecord supplies a [] method:
[](attr_name)
Returns the value of the attribute identified by attr_name after it has been typecast...
So saying self[:name] is just a round-about way to access the name attribute of your model.
[] is a method like any other in Ruby, you can define your own in any class you want:
class C
def [](k)
# do whatever you want
end
end
c = C.new
c[:pancakes]
ActiveRecord is used with data that is, more or less, a Hash backed by a relational database so saying model[:attribute_name] is fairly natural. Hence the existence of the [] method.

Rails return JSON serialized attribute with_indifferent_access

I previously had:
serialize :params, JSON
But this would return the JSON and convert hash key symbols to strings. I want to reference the hash using symbols, as is most common when working with hashes. I feed it symbols, Rails returns strings. To avoid this, I created my own getter/setter. The setter is simple enough (JSON encode), the getter is:
def params
read_attribute(:params) || JSON.parse(read_attribute(:params).to_json).with_indifferent_access
end
I couldn't reference params directly because that would cause a loop, so I'm using read_attribute, and now my hash keys can be referenced with symbols or strings. However, this does not update the hash:
model.params.merge!(test: 'test')
puts model.params # => returns default params without merge
Which makes me think the hash is being referenced by copy.
My question is twofold. Can I extend active record JSON serialization to return indifferent access hash (or not convert symbols to strings), and still have hash work as above with merge? If not, what can I do to improve my getter so that model.params.merge! works?
I was hoping for something along the lines of (which works):
def params_merge!(hash)
write_attribute(:params, read_attribute(:params).merge(hash))
end
# usage: model.params_merge!(test: 'test')
Better yet, just get Rails to return a hash with indifferent access or not convert my symbols into strings! Appreciate any help.
use the built-in serialize method :
class Whatever < ActiveRecord::Base
serialize :params, HashWithIndifferentAccess
end
see ActiveRecord::Base docs on serialization for more info.
Posting comment as answer, per #fguillen's request... Caveat: I am not typically a Rubyist… so this may not be idiomatic or efficient. Functionally, it got me what I wanted. Seems to work in Rails 3.2 and 4.0...
In application_helper.rb:
module ApplicationHelper
class JSONWithIndifferentAccess
def self.load(str)
obj = HashWithIndifferentAccess.new(JSON.load(str))
#...or simply: obj = JSON.load(str, nil, symbolize_names:true)
obj.freeze #i also want it set all or nothing, not piecemeal; ymmv
obj
end
def self.dump(obj)
JSON.dump(obj)
end
end
end
In my model, I have a field called rule_spec, serialized into a text field:
serialize :rule_spec, ApplicationHelper::JSONWithIndifferentAccess
Ultimately, I realized I just wanted symbols, not indifferent access, but by tweaking the load method you can get either behavior.
Using HashWithIndifferentAccess is great, but it still acts like a Hash, and it can only serialize as YAML in the database.
My preference, using Postgres 9.3 and higher, is to use the json column type in Postgres. This means that when the table is read, ActiveRecord will get a Hash directly from Postgres.
create_table "gadgets" do |t|
t.json "info"
end
ActiveRecord serialize requires that you provide it a single class that is both responsible for reading/writing the data and serializing/deserializing it.
So you can create an object that does the job by inheriting from HashWithIndifferentAccess, or my preference, Hashie::Mash. Then you implement the serialization as the dump and load class methods.
class HashieMashStoredAsJson < Hashie::Mash
def self.dump(obj)
ActiveSupport::JSON.encode(obj.to_h)
end
def self.load(raw_hash)
new(raw_hash || {})
end
end
In your model, you can specify this class for serialization.
class Gadget < ActiveRecord::Base
serialize :info, HashieMashStoredAsJson
# This allows the field to be set as a Hash or anything compatible with it.
def info=(new_value)
self[:info] = HashieMashStoredAsJson.new new_value
end
end
If you don't use the json column type in Postgres, the implementation changes slightly
Full code and documentation here: using a JSON column type and using a string column type.
I ended up using a variation on bimsapi's solution that you can use not only with simple un-nested JSON but any JSON.
Once this is loaded...
module JsonHelper
class JsonWithIndifferentAccess
def self.load(str)
self.indifferent_access JSON.load(str)
end
def self.dump(obj)
JSON.dump(obj)
end
private
def self.indifferent_access(obj)
if obj.is_a? Array
obj.map!{|o| self.indifferent_access(o)}
elsif obj.is_a? Hash
obj.with_indifferent_access
else
obj
end
end
end
end
then instead of calling
JSON.load(http_response)
you just call
JsonHelper::JsonWithIndifferentAccess.load(http_response)
Does the same thing but all the nested hashes are indifferent access.
Should serve you well but think a little before making it your default approach for all parsing as massive JSON payloads will add significant ruby operations on top of the native JSON parser which is optimised in C and more fully designed for performance.

How to add values in global Hash from method? RoR

I have some problem. I have many strings with keys and their belongings, keys are always the same. String looks like "key1=value1;key2=value2..." . So I made global Hash with arrays as values and want to store all data from strings into that hash so I made function:
<%
$all={}
for len in (0..$authors.length-1)
$all[$authors[len]] = Array.new #authors are defined and filled earlier
end
def add_hash(from)
the_from = from.to_s.split(";")
for one in the_from
splitted = one.split("=")
for j in (0..$authors.length.to_i-1)
if($authors[j] == splitted[0])
$all[$authors[j]] << splitted[1]
end
end
end
end
%>
but it doesn't seem to work, is something wrong in my code? (note: I can only use Ruby on Rails code)
Just for lolz)), cause of
note: I can only use Ruby on Rails code
place it in lolo.rb in initializer folder of rails app
require 'singleton'
class Lolo < Hash
include Singleton
def initialize
super([])
end
def add src
src.to_s.split(";").each do |item|
splitted = item.split("=")
self[splitted[0]] = splitted[1]
end
end
end
in any place call all =Lolo.instance to access hash, and all.add("key1=value1;key2=value2") to add elements, all.keys is authors list
and don't use global vars cause it could cause a lot of problem
Using global variable is a bad practice. Still if you want to use there is no problem.
In your code accessing hash variable using key as string is not allowed. So change the key to symbol by using to_sym
(ie) $all[$authors[len].to_sym] similarly $all[$authors[j].to_sym]
This may work.

active record to hash inside itself

So this is a bit of a silly one and is more lack of programming knowledge rather than anything ruby or rails specific.
If i wanted to turn an ordinary class into hash its not so bad. I already have one:
class CustomRequest
require 'json'
require 'net/http'
attr_accessor :url, :depth, :status
def initialize(url,depth)
#url = url
#depth = depth
end
def make_me_hash_and_send
#myReq = {:url => #url, :depth =>#depth}
myJsonReq = #myReq
puts myJsonReq
res = Net::HTTP.post_form(URI.parse('http://127.0.0.1:3008/user_requests/add.json'),
myJsonReq)
end
end
Simply generates the hash from the internal variables that are passed in the constructor. I want to do the same for active record but the abstractness of it isn't making it easy.
Lets say I have this class
def turn_my_insides_to_hash
#How do I take the actual record variables
# nd generate a hash from them.
#Is it something like
#myHash = {:myString = self.myString
:myInt => self.myInt }
end
I may be looking at this the wrong way. I know Outside of the class I could simply say
#x = Result.find(passed_id).to_hash
and then do what I want to it. But I would rather call something liks
#x = Result.send
(which turns the result's variables into hash and sends them)
I already have the send part, just need to know how to turn variables into hash from inside class.
You could try use JSON instead of YAML:
Result.find(passed_id).to_json
or
Result.find(passed_id).attributes.to_json
also you can use options like :except and :only for to_json method.
Result.find(passed_id).attributes.to_json(:only => ['status', 'message'])
record.serializable_hash
http://api.rubyonrails.org/v4.0.12/classes/ActiveModel/Serialization.html#method-i-serializable_hash
I write something more because SO ask me to do so.

Resources