Can Struct.new take options as an argument? - ruby-on-rails

I Have a struct - let's say it looks like this:
class MyClass::Subclass < Struct.new(:model1, :model2)
def method1
end
def method2
if model1.active?
end
end
end
This is how I currently have a subclass setup. I am now at a point where I have to pass in a one-time options.
My thinking is that there must be something similar to what you can do in methods like:
class MyClass::Subclass < Struct.new(:model1, :model2, options = {})
def method1
if options["need_this"]
end
end
end
I keep on getting errors:
TypeError: {} is not a symbol
Is there something like options = {} that I can use in Structs? Sorry this may seem like a newb question but Ruby is not my main language.

I'm getting that what you are trying to do is add an optional list of options. (Please correct me if I'm wrong.) To do that, simply add the element. Then in your code, check to see whether or not a hash has been provided:
class MyClass::Subclass < Struct.new(:model1, :model2, :options)
def method1
if self.options && self.options["need_this"]
p 'need this'
end
end
end
x = MyClass::Subclass.new(:x, :y, {'need_this' => 'yesido'})
x.method1 # => 'need this'
You specify Struct parameters as symbols (hence your error), and you can pass any type of arguments you want to them — including none at all. So, while you can't specify a default value, you can check whether a value has been provided; if not, in this case, options will be nil. The example I've put in checks whether a hash has been provided for options, and if so, whether a need_this option has been included in the hash.
Here's the ruby doc on Struct. If you read the overview and the doc on the new method, you should have a clear picture of the Struct syntax.

Related

How can I run a private method in a module in rails console?

I have an error in a private method in a helper file. The helper file looks something like below with module HxHelper. I want to run method_2 in rails console to recreate the error in my local system.
module HxHelper
def method_1{
"key_1": "#h.htype"
"key_2": "value_2"
+ method_2}
end
private
def method_2{
"key1": "value_1"}
end
In my controller file I define a new class and include the previous helper file. I have tried the following.
Created an object of my class as obj = Class.new and obj.method_1. I get an error undefined method type in method_1. hype is attribute in house table.
Tried HxHeleper::method_1: Error - method_1 is not defined in HxHelper module.
Defining method_2 as self: Doesn't work.
Can someone help me understand what I am doing wrong?
Assuming we can get around your syntax issues, the standard answer to 'how do I call a private method in Ruby?' is with .send(), as in obj.send(:private_method).
After you manually debug this, learn to write automated tests. Rails has exemplary, industry-leading support for them. The tests will take over the role of experimentation and investigation that you are currently abusing the console for.
You have some syntax errors in this example. You want to use commas at the end of the lines of your hash, you can't add two hashes together, you instead need to merge. Merging will take the receiver (the thing you're calling merge on) and override any values from the argument. Additionally, when using : in your hash, your keys end up being symbols, which means you don't need the quotes.
This would be the proper way to define the helper module.
module HxHelper
def method_1
{
key_1: "#h.htype",
key_2: "value_2",
}.merge(method_2)
end
private
def method_2
{
key1: "value_1",
}
end
end
Then you can use it like this:
class Test
include HxHelper
end
t = Test.new
t.method_1
This will return:
{:key_1=>"#h.htype", :key_2=>"value_2", :key1=>"value_1"}
If you call t.method_2, you get an error about calling a private method.
If method_2's hash had a key of key_1 instead, your return value would be:
{:key_1=>"value_1", :key_2=>"value_2"}
because the :key_1 from the argument overrode the one on the receiver Hash.
If you wanted to call that private method, you could do:
t.send(:method_2)
If you had a method that took arguments, you just add them after the symbol of the method name:
private
def test(num1, num2)
return(num1 + num2)
end
send(:test, 1, 2)
This would return 3

Ruby variables visibility inside modules

TL;DR;
I need to make some ##vars of a static method (extends) in one module visible to a instance method in another module(includes).
How to accomplish that once only setting ##var=value was not enough to make it visible?
Maybe you can just read my capitalized comment bellow and jump to question 4.
Hi, I would like to add an method to my models to index some data in a mysql table with some full text search fields.
In order to accomplish that, I created the following module:
module ElasticFakeIndexing
module IndexingTarget
#instance method to be called on model to get data to save
def build_index_data
{
entity_id: self.id,
entity_type: self.class.name,
#UNABLE TO ACCESS IF SET ONLY WITH ##var=value. Why?
#AND ALMOST SURE THAT USING class_variable_set IS THE CAUSE OF CONFIGURATION OF ONE MODULE MESSING UP WITH ANOTHER'S
title: ##title_fields.collect{|prop| self.send(prop.to_sym)}.join(" || "),
description: ##description_fields.collect{|prop| self.send(prop.to_sym)}.join(" || "),
}
end
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
#class method to declare/call at a given model
def elastic_fake(options = {})
#Make sure we always get an array so we can use 'join'
title_arg = Array(options[:title])
ElasticFakeIndexing::IndexingTarget.class_variable_set(:##title_fields, title_arg)
description_arg = Array(options[:description])
ElasticFakeIndexing::IndexingTarget.class_variable_set(:##description_fields, description_arg)
extra_arg = Array(options[:extra])
ElasticFakeIndexing::IndexingTarget.class_variable_set(:##extra_args, extra_arg)
end
end
end
end
And I use it this way at my models:
class SomeModel < ApplicationRecord
#includes the module
include ElasticFakeIndexing::IndexingTarget
...
# 'static' method call to configure to all classes of this model
elastic_fake(title: "prop_a", description: ["prop_b", "prop_c", "prop_d"])
end
And at some point of my code something like this will be called:
index_data = some_model_instance.build_index_data
save_on_mysql_text_search_fields(index_data)
But I got some problems. And have some questions:
when I use/include my module in a second model, looks like the configuration of one model is being visible to the other. And I got 'invalid fields' like errors. I guess it happens because of this, for example:
ElasticFakeIndexing::IndexingTarget.class_variable_set(:##title_fields, title_arg)
But I got to this this because only set ##title_fields wasn't enough to make title_fields visible at build_index_data instance method. Why?
Why using only #title_fields isn't enough too to make it visible at build_index_data?
How to design it in a way that the set of fields are set in a 'static' variable for each model, and visible inside the instance method build_index_data? Or as a possible solution, the fields could live in a instance variable and be visible. But I think it should live in a 'static' variable because the fields will not change from one instance of the model to another...
Any thoughts? What am I missing about the variables scopes/visibility?
Thank you
Read the following articles on Ruby variables:
Ruby Variable Scope
Understanding Scope in Ruby
quick reminder: ##title_fields, class variable, must be initialized at creation time, while #title_fields, instance variable, hasn't such requirement.
Instead of relying on class variables I recommend using class side instance variables. Class variables will easily be overwritten between individual models including the module. Class side instance variables however are save.
Using some of the syntactic sugar (namely concern and class_attribute) rails offers you could write something like
module ElasticFakeIndexing
extend ActiveSupport::Concern
included do
class_attribute :title_fields,
:description_fields,
:extra_args
end
class_methods do
def elastic_fake(options = {})
...
self.title_fields = Array(options[:title])
...
end
end
def build_index_data
...
title: self.class.title_fields ...
...
end
end

Set dynamic values when generating setter methods using attr_accessor in ruby

Is there a better way to set values to setter methods when they are made dynamically using attr_accessor method? I need this for setting values for them from another model in rails. I'm trying to do something like below.
Model_class.all.each do |mdl|
attr_accessor(mdl.some_field)
end
Then I know that it creates a set of get and setter methods. What I want to do is, when these methods are get created, i want some value to be specified for setter method.Thanks in advance.
attr_accessor has no magic embedded. For each of params passed to it, it basically executes something like (the code is simplified and lacks necessary checks etc):
def attr_accessor(*vars)
vars.each do |var|
define_method var { instance_variable_get("##{var}") }
define_method "#{var}=" { |val| instance_variable_set("##{var}", val) }
end
end
That said, the attr_accessor :var1, :var2 DSL simply brings new 4 plain old good ruby methods. For what you are asking, one might take care about defining these methods (or some of them, or none,) themselves. For instance, for cumbersome setting with checks one might do:
attr_reader :variable # reader is reader, no magic
def variable=(val) do
raise ArgumentError, "You must be kidding" if val.nil?
#variable = val
end
The above is called as usual:
instance.variable = 42
#⇒ 42
instance.variable = nil
#⇒ ArgumentError: You must be kidding
Here is another possible implementation for this:
def attr_accessor(*args)
args.each do |attribute|
define_method(attribute.to_sym) { eval("##{attribute}") }
define_method((attribute.to_s + '=').to_sym) {|value| eval("##{attribute} = value") }
end
end

Pass a symbol to a method and call the corresponding method

In a Rails controller you can pass a symbol to the layout method that corresponds to a method in you controller that will return the layout name like this:
layout :my_method
def my_method
'layout_1'
end
I want to have a similar functionality to likewise pass a symbol to my classes method and that class should call the corresponding function and use its return value, like this
myClass.foo :my_method
def my_method
'layout_1'
end
I've read posts[1] that tell me I need to pass
myClass.foo(method(:my_method))
which I find ugly and inconvenient. How is rails here different allowing to pass just the symbol without any wrapper? Can this be achieved like Rails does it?
[1] How to implement a "callback" in Ruby?
If you want to only pass a :symbol into the method, then you have to make assumptions about which method named :symbol is the one you want called for you. Probably it's either defined in the class of the caller, or some outer scope. Using the binding_of_caller gem, we can snag that information easily and evaluate the code in that context.
This surely has security implications, but those issues are up to you! :)
require 'binding_of_caller'
class Test
def foo(sym)
binding.of_caller(1).eval("method(:#{sym})").call
end
end
class Other
def blork
t = Test.new
p t.foo(:bar)
p t.foo(:quxx)
end
def bar
'baz'
end
end
def quxx
'quxx'
end
o = Other.new
o.blork
> "baz"
> "quxx"
I still don't understand, what is author asking about. He's saying about "callbacks", but only wrote how he wants to pass parameter to some method. What that method(foo) should do - i have no idea.
So I tried to predict it's implementation. On class initialising it gets the name of method and create private method, that should be called somewhere under the hood. It possible not to create new method, but store method name in class variable and then call it somewhere.
module Foo
extend ActiveSupport::Concern
module ClassMethods
def foo(method_name)
define_method :_foo do
send method_name
end
end
end
end
class BaseClass
include Foo
end
class MyClass < BaseClass
foo :my_method
private
def my_method
"Hello world"
end
end
MyClass.new.send(:_foo)
#=> "Hello world"
And really, everything is much clearer when you're not just wondering how it works in rails, but viewing the source code: layout.rb

Ruby syntax, semantic questions def status=(status)

I was looking at this code and was trying to figure what def status=(status) means. I have never seen that before.
class Tweet
attr_accessor :status
def initialize(options={})
self.status = options[:status]
end
def public?
self.status && self.status[0] != "#"
end
def status=(status)
#status = status ? status[0...140] : status
end
end
I'll try answering this in layman's terms, since I didn't understand this when starting out.
Let's say you want the Tweet class to have an attribute status. Now you want to change that attribute, well you can't since it's hidden inside the class. The only way you can interact with anything inside a class is by creating a method to do so:
def status=(status)
#status = status # using # makes #status a class instance variable, so you can interact with this attribute in other methods inside this class
end
Great! Now I can do this:
tweet = Tweet.new
tweet.status = "200" # great this works
# now lets get the status back:
tweet.status # blows up!
We can't access the status variable since we haven't defined a method that does that.
def status
#status # returns whatever #status is, will return nil if not set
end
Now tweet.status will work as well.
There are shorthands for this:
attr_setter :status #like the first method
attr_reader :status # like the second one
attr_accessor :status # does both of the above
That is a setter - the method to be called when you say thing.status = whatever.
Without such a method, saying thing.status = whatever would be illegal, since that syntax is merely syntactic sugar for calling the setter.
It means exactly the same thing that def foo always means: define a method named foo.
def initialize
Defines a method named initialize.
def public?
Defines a method named public?
def status=
Defines a method named status=
That's it. There's absolutely nothing special going on here. There is no magic when defining a method whose name ends in an = sign.
The magic happens when calling a method whose name ends in an = sign. Basically, you are allowed to insert whitespace in between the = sign and the rest of the method name. So, instead of having to call the method like this
foo.status= 42
You can call it like this:
foo.status = 42
Which makes it look like an assignment. Note: it is also treated like an assignment in another way; just like with all other forms of assignments, assignment expressions evaluate to the value that is being assigned, which means that the return value of the method is ignored in this case.

Resources