How to add values in global Hash from method? RoR - ruby-on-rails

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.

Related

How can I securely store a ruby hash in 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").

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 do you define a method with a "." that extends the functionality of a preexisting attribute?

So, this is what I have so far:
# #return [Array<Hash>] of variables ([Placeholders, actual variables])
def variables
# #return [Hash] of all variables.
# If actual variables are named teh same as the placeholders, the placeholders will be over written.
# This implementation of merging hashes of an array will work even if we add more elements to the
# variables array.
self.class.send(:define_method, "variables.flatten") do
return self.variables.inject{|placeholders, vars| placeholders.merge(vars || {})}
end
return [placeholder_variables, self.variable_data]
end
I want to be able to do:
my_object.variables # => return the Array<Hash>
but also be able to do
my_object.variables.flatten # => return a single merged hash
the issue when I just do
def variables.flatten
is that when I run the console, I get an error message saying that variables is not defined (or just whatever I put before .flatten)
is there a way I can define my custom .flatten method only on the variables attribute? (normal <Array>.flatten doesn't work here, cause I need the hashes merged. )
Direct answer: define the method in its own module, then include it in the array you return:
module SpecialFlatten
def flatten
first.update last
end
end
...and later...
def variables
vals = [placeholder_variables, self.variable_data]
return vals.extend(SpecialFlatten)
end
But in this case, a better option might be to do this:
def variables(*flags)
if flags.include? :flatten
placeholder_variables.update self.variable_data
else
[placeholder_variables, self.variable_data]
end
end
I don't think it's possible to define the "variables.flatten" method. To prove that let's check by this example:
require 'ruby_parser'
RubyParser.new.parse(<<-EOF)
my_object.variables.flatten
EOF
#=> s(:call,
s(:call, s(:call, nil, :my_object, s(:arglist)), :variables, s(:arglist)),
:flatten,
s(:arglist))
As you can see variables and flatten are different tokens and they can't merge into one token, that's a parse issue.
But one thing maybe you're missing - that's the chainability. The method variables returns the array of variables and if they are responding to flatten method, the result will be flat. If they're not responding to that method, you should implement flatten method for these objects.
You can define a singleton method on the returning value.
def variables
result = [placeholder_variables, self.variable_data]
# redefine method flatten on the result array
def result.flatten
# did not understand what you are trying to do here
# self is the array here
inject{|placeholders, vars| placeholders.merge(vars || {})}
end
return result # return the array with added method
end
However, this is almost always a bad idea.
The Array#flatten method is part of Ruby core, so I would recommend not changing it, especially if you are working on a team with other developers. It seems like you are trying to conform to an existing interface. I'd look for alternatives, but you could override the flatten method on the return value of the variables method.
def variables
[placeholder_variables, self.class.variable_data].tap do |vars|
vars.instance_eval do
def flatten
inject{|result, vars_hash| result.merge!(vars_hash)}
end
end
end
end

Rails - Model changes not being saved or preserved between controller and model

I'd appreciate any help I can get with a somewhat strange phenonemon going on in my code. The controller's create method is (roughly) as follows:
def create
#session ||= Session.new
#session.date = params[:date]
#session.generate_string
#session.save
# etc
end
And the model:
class Session < ActiveRecord::Base # table is 'sessions' with 3 columns :id, :str, :date
include MyHelper
def generate_string(num_chars)
#str ||= ""
num_chars.to_i.times do
#str += some_method_in_MyHelper() # method returns a string
end
end
end
With some logging I found out that although the generate_string is working correctly, the resulting #session (in the controller) has the date set as expected but the value of str is a blank string. Sure enough, when the .save is hit, the database is told to insert a record consisting of a blank string and the correct date.
I found this Why do my changes to model instances not get saved sometimes in Rails 3? that suggests I should be using the "self" prefix instead of #. This seems to make the code work as expected, but seems strange because I thought self.xxx referred to the class, not the class instance. I'd be grateful if anyone could clarify what's going on - thanks!
self refers to the instance when used inside an instance method. It refers to the class outside an instance method, when it (self) is the class that's being defined.
# is an instance variable, which is different than an ActiveRecord column.
In order to store it in the str field to be saved to the database, you need to use self.str method. I think this is what you are looking for
class Session < ActiveRecord::Base # table is 'sessions' with 3 columns :id, :str, :date
include MyHelper
def generate_string(num_chars)
str = ""
num_chars.to_i.times do
str += some_method_in_MyHelper() # method returns a string
end
self.str = str # store it in attribute to be saved into db
end
end
Notice I removed the instance variable #str and changed it to local variable str instead because it seems like you want to generate a new string everytime this method is called? Also, this variable caching is useless
#session ||= Session.new
because instance variables only stick around for a single request. It should be
#session = Session.new

Resources