How to transform a string into a variable/field? - ruby-on-rails

I'm new to Ruby and I would like to find out what the best way of doing things is.
Assume the following scenario:
I have a text field where the user can input strings. Based on what the user inputs (after validation) I would like to access different fields of an instance variable.
Example: #zoo is an instance variable. The user inputs "monkey" and I would like to access #zoo.monkey. How can I do that in Ruby?
One idea that crossed my mind is to have a hash:
zoo_hash = { "monkey" => #zoo.monkey, ... }
but I was wondering if there is a better way to do this?
Thanks!

#zoo.attributes gives you a hash of the object attributes. So you can access them like
#zoo.attributes['monkey']
This will give nil if the attribute is not present. Calling a method which doesn't exist will throw NoMethodError

In your controller you could use the public_send (or even send) method like this:
def your_action
#zoo.public_send(params[:your_field])
end
Obviously this is no good, since someone can post somehing like delete_all as the method name, so you must sanitize the value you get from the form. As a simple example:
ALLOWED_METHODS = [:monkey, :tiger]
def your_action
raise unless ALLOWED_METHODS.include?(params[:your_field])
#zoo.public_send(params[:your_field])
end

There is much better way to do this - you should use Object#send or (even better, because it raises error if you try to call private or protected method) Object#public_send, like this:
message = 'monkey'
#zoo.public_send( message )

You could implement method_missing in your class and have it interrogate #zoo for a matching method. Documentation: http://ruby-doc.org/core-1.9.3/BasicObject.html#method-i-method_missing
require 'ostruct' # only necessary for my example
class ZooKeeper
def initialize
#zoo = OpenStruct.new(monkey: 'chimp')
end
def method_missing(method, *args)
if #zoo.respond_to?(method)
return #zoo.send(method)
else
super
end
end
end
keeper = ZooKeeper.new
keeper.monkey #=> "chimp"
keeper.lion #=> NoMethodError: undefined method `lion'

Related

Disable all links of User after deactivating in Rails5

I want to disable all link of users at a time after deactivating users. So, for that I wrote a code like this
def link_to(*user)
if user_link_disabled?(user.id)
return nil
else
super
end
end
def user_link_disabled?(user_id)
User.where(activation: false).pluck(:name).include?(user_id)
end
But I am getting this error
undefined method `id' for #<Array:0x007efee4667d00>
Could anyone please help me on this?
I would add a column to your users model:
deactivated => type boolean
user.deactivated? #will return true or false
In your view you can then use link_to_unless
link_to_unless(user.deactivated, name, options = {}, html_options = {}, &block)
I don't know the scope because you didnt display anymore info but it could just be user_id in your if statement or try id[params[:id] instead of user.id but I'm not sure with out more context.
IN following method
def user_link_disabled?(user_id)
User.where(activation: false).pluck(:name).include?(user_id)
end
You are going to pluck name from user table records but you are checking include? for user.id, I think you should pluck id instead of name.
First of all, I am not gonna comment on your preferred code/method for overriding the the link_to helper. There is not much context available for that.
But to solve the particular error you are getting:
Your are defining method like this def link_to(*user) .
Here *user means it is expecting an Array as argument to the method and using the Ruby splat(*), it is converting it to normal arguments.
So if you call this as link_to [1,2,3], it will be same as calling a method with 3 arguments. That is link_to (1,2,3) but the argument user will be an Array.
So in here if user_link_disabled?(user.id), you are calling a id on a Array data type. That's why you are getting an error.
Depending on your use, either remove the * from method definition,
or
Use looping, if you are going to pass multiple users data to method, like:
def link_to(*user)
user.each do |u|
if user_link_disabled?(u.id)
return nil
else
super
end
end
end
As I mentioned in beginning, I don't know much about the context. So can not comment about the right way but if I may suggest, then I would suggest to use a custom helper for all user routes. like below pseudo code:
def link_to_user(user)
deactivated = user.deactivated?
if deactivated
# render some disabled link
else
# render link
end
end

Monkeypatch ActiveRecord::FinderMethods

I'm trying to monkey patch ActiveRecord::FinderMethods in order to use hashed ids for my models. So for example User.find(1) becomes User.find("FEW"). Sadly my overwritten method doesn't get called. Any ideas how to overwrite the find_one method?
module ActiveRecord
module FinderMethods
alias_method :orig_find_one, :find_one
def find_one(id)
if id.is_a?(String)
orig_find_one decrypt_id(id)
else
orig_find_one(id)
end
end
end
end
Here's an article that discusses how to actually do what you want by overriding the User.primary_key method like:
class User
self.primary_key = 'hashed_id'
end
Which would allow you to call User.find and pass it the "hashed_id":
http://ruby-journal.com/how-to-override-default-primary-key-id-in-rails/
So, it's possible.
That said, I would recommend against doing that, and instead using something like User.find_by_hashed_id. The only difference is that this method will return nil when a result is not found instead of throwing an ActiveRecord::RecordNotFound exception. You could throw this manually in your controller:
def show
#user = User.find_by_hashed_id(hashed_id)
raise ActiveRecord::RecordNotFound.new if #user.nil?
... continue processing ...
end
Finally, one other note to make this easier on you -- Rails also has a method you can override in your model, to_param, to tell it what property to use when generating routes. By default, of course, it users the id, but you would probably want to use the hashed_id.
class User
def to_param
self.hashed_id
end
end
Now, in your controller, params[:id] will contain the hashed_id instead of the id.
def show
#user = User.find_by_hashed_id(params[:id])
raise ActiveRecord::RecordNotFound.new if #user.nil?
... continue processing ...
end
I agree that you should be careful when doing this, but it is possible.
If you have a method decode_id that converts a hashed ID back to the original id, then the following will work:
In User.rb
# Extend AR find method to allow finding records by an encoded string id:
def self.find(*ids)
return super if ids.length > 1
# Note the short-circuiting || to fall-back to default behavior
find_by(id: decode_id(ids[0])) || super
end
Just make sure that decode_id returns nil if it's passed an invalid hash. This way you can find by Hashed ID and standard ID, so if you had a user with id 12345, then the following:
User.find(12345)
User.find("12345")
User.find(encode_id(12345))
Should all return the same user.

Preferred Workflow for Ruby-YAML interaction

I have just started working on a project where there is a lot of interaction between Ruby and 5-6 levels deep YAML files. Since Ruby will respond with NoMethodError: undefined method '[]' for nil:NilClass when you are trying to access a key that doesn't exist there are lots of methods with the following setup:
def retrieve_som_data(key1, key2)
results = []
if data(key1, key2)
if data_set_2(key, key2)["my_key"]
results = data_set_2(key, key2)["my_other_key"]
end
end
return results.clone
end
This looks horrible, so I am looking at a way to refactor it. I have tried working on a version where I would replace a method like this:
def data(key1, key2)
if data = names_data(key1)
return data[key2]
end
end
with this instead:
def data(key1, key2)
names_data(key1).fetch(key2)
end
This raises a more specific error KeyError which can than be rescued and acted on in any method calling .data(), but this also doesn't seem like a good solution readability wise.
I'd love to get some input on how you are handling situations where you are trying to access YAML_DATA[key][key1][key2][key3][key4] and take into account that any of the provided keys could hit something thats nil.
What are your preferred workflows for this?
If you're using rails, they've added a method, try to Object. For nil objects this will always return nil, rather than throwing so you could do something along the lines of this:
def get_yaml_obj(yaml_data, key1, key2, key3)
yaml_data.try(:[], key1).try(:[], key2).try(:[], key3)
end
Or if you have an arbitary number of keys:
def get_data(yaml_data, keys)
keys.each do |key|
yaml_data = yaml_data.try(:[], key)
end
yaml_data
end

Ruby 2 Keyword Arguments and ActionController::Parameters

I have a rails 4 application that is running on ruby 2.1. I have a User model that looks something like
class User < ActiveModel::Base
def self.search(query: false, active: true, **extra)
# ...
end
end
As you can see in the search method I am attempting to use the new keyword arguments feature of ruby 2.
The problem is that when I call this code from in my controller all values get dumped into query.
params
{"action"=>"search", "controller"=>"users", query: "foobar" }
Please note that this is a ActionController::Parameters object and not a hash as it looks
UsersController
def search
#users = User.search(params)
end
I feel that this is because params is a ActionController::Parameters object and not a hash. However even calling to_h on params when passing it in dumps everything into query instead of the expected behavior. I think this is because the keys are now strings instead of symbols.
I know that I could build a new hash w/ symbols as the keys but this seems to be more trouble than it's worth. Ideas? Suggestions?
Keywords arguments must be passed as hash with symbols, not strings:
class Something
def initialize(one: nil)
end
end
irb(main):019:0> Something.new("one" => 1)
ArgumentError: wrong number of arguments (1 for 0)
ActionController::Parameters inherits from ActiveSupport::HashWithIndifferentAccess which defaults to string keys:
a = HashWithIndifferentAccess.new(one: 1)
=> {"one"=>1}
To make it symbols you can call symbolize_keys method. In your case: User.search(params.symbolize_keys)
I agree with Morgoth, however, with rails ~5 you will get a Deprecation Warning because ActionController::Parameters no longer inherits from hash. So instead you can do:
params.to_unsafe_hash.symbolize_keys
or if you have nested params as is often the case when building api endpoints:
params.to_unsafe_hash.deep_symbolize_keys
You might add a method to ApplicationController that looks something like this:
def unsafe_keyworded_params
#_unsafe_keyworded_params ||= params.to_unsafe_hash.deep_symbolized_keys
end
You most likely do need them to be symbols. Try this:
def search
#users = User.search(params.inject({}){|para,(k,v)| para[k.to_sym] = v; para}
end
I know it's not the ideal solution, but it is a one liner.
In this particular instance I think you're better off passing the params object and treating it as such rather than trying to be clever with the new functionality in Ruby 2.
For one thing, reading this is a lot clearer about where the variables are coming from and why they might be missing/incorrect/whatever:
def search(params)
raise ArgumentError, 'Required arguments are missing' unless params[:query].present?
# ... do stuff ...
end
What you're trying to do (in my opinion) only clouds the issue and confuses things when trying to debug problems:
def self.search(query: false, active: true, **extra)
# ...
end
# Method explicitly asks for particular arguments, but then you call it like this:
User.search(params)
Personally, I think that code is a bit smelly.
However ... personal opinion aside, how I would fix it would be to monkey-patch the ActionController::Parameters class and add a #to_h method which structured the data as you need it to pass to a method like this.
Using to_unsafe_hash is unsafe because it includes params that are not permitted. (See ActionController::Parameters#permit) A better approach is to use to_hash:
params.to_hash.symbolize_keys
or if you have nested params:
params.to_hash.deep_symbolize_keys
Reference: https://api.rubyonrails.org/classes/ActionController/Parameters.html#method-i-to_hash

How do you pass a method with arguments to #to_xml?

How can I pass a method with an argument to #to_xml?
#object.to_xml(:methods => [:a_method_with_args] )
Is there a way to do this? What is the correct syntax?
Thanks.
to_xml is supposed to express your model's state. and as such it shouldn't need any external 'location' arguments. if this is really what you need it looks like you need a 'give me an xml representation of my model when on location X'. I guess you can just add a 'set_default_location' to your model and change the price_points_for_location to have a default value for the argument:
attr_writer :default_location
def price_points_for_location(location = #default_location)
...
end
You could try redefining the to_xml method like
def to_xml(location)
# do your stuff
super()
end
But not sure it would work that well. Other option would be to create some new XML view method for your model, like:
def as_xml(location)
self.price_points_for_location(location)
self.to_xml
end
Thanks for the answers, they look like good options. What I actually ended up doing is using a proc. I was aware that I could use procs with to_xml, but it seems that you can't access the current object in the array when iterating over multiple objects. To get around this I did something like this:
price_points = #items.map { |item| item.price_points_for_location(location) }
price_point = Proc.new {|options| options[:builder].tag!('price_points', price_points.shift) }
#items.to_xml(:procs => [price_point])

Resources