Rails calling freeze constant - ruby-on-rails

I tried to do some code refactor inside of my webhook manager class. The idea was to use Builder pattern (hope I didn't get the names wrong) in which one class would distribute assignments to the others. That's why I build a class with constant variable WEBHOOK_DEFINITIONS and, depending on the arguments provided, would trigger the appropriate ActiveJob's. Like below:
# frozen_string_literal: true
class ManageWebhookData
WEBHOOK_DEFINITIONS = {
default: IdentityChecks::IdentityCheckUpdaterJob,
#there will be much more
(...)
}.freeze.with_indifferent_access
def initialize(webhook, name)
#webhook = webhook
#name = name
end
def call
WEBHOOK_DEFINITIONS[type].perform_later(webhook)
end
attr_reader :webhook, :name
private
def type
WEBHOOK_DEFINITIONS.key(name.downcase) ? name.downcase : :default
end
end
I don't know why but Rails somehow call WEBHOOK_DEFINITIONS even when it's freeze because I'm getting an error:
NameError (uninitialized constant IdentityCheckUpdaterJob):
app/services/manage_webhook_data.rb:5:in `<class:ManageWebhookData>'
Why is this happening?

It's not rails, it's ruby.
Constants are resolving on class load, you could check it yourself in console
> class Foo
> BAR = puts("I'm constant")
> end
I'm constant
Besides that, I'm not sure when Indifferent hash is called on freezed hash, it will be freezed too. I'd write instead
WEBHOOK_DEFINITIONS = {
default: IdentityChecks::IdentityCheckUpdaterJob,
#there will be much more
(...)
}.with_indifferent_access.freeze

Related

Rails Console Argument Error Creating a New object

I am new to Rails and Ruby development but I am trying to create an object called Currency which takes in two params and does some calculations on them. I am using attr_accessor to set up the params and I put the file inside the lib directory.
Whenever I run rails console and try to do c = Currency.new(100, "CAD") I get the following error:
ArgumentError: wrong number of arguments (given 2, expected 0)
from (irb):5:in `initialize'
from (irb):5:in `new'
from (irb):5
I did make sure to include the file in application.rb. Here is a skeleton of my class:
class Currency
class << self
attr_accessor :input_value, :currency_iso
USD_ISO = "USD"
USD_TO_DM = 2.8054
def converted_value
convert_to_dm
end
private
def convert_to_dm
#input_value / USD_TO_DM
end
end
end
I have looked all over and I am stumped on what this issue may be. I have tried with and without an initialize method and I have tried creating a more basic version.
The problem here is that you are defining the method as a class method. And you are not defining the initialize method with those two params. Let's check the code below:
class Currency
attr_accessor :input_value, :currency_iso
USD_ISO = "USD"
USD_TO_DM = 2.8054
def initialize(input_value, currency_iso)
#input_value = input_value
#currency_iso = currency_iso
end
def converted_value
convert_to_dm
end
private
def convert_to_dm
input_value / USD_TO_DM
end
end
Also, due to you have already defined the attr_accessor you don't need to use the # when calling those attributes.
I found this post. It can help you to understand better the difference between class method and instance method.

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

How can I share data with a concern in Rails 4.2?

I am attempting to use Rails Concerns (or even a bare Module mixin) to share methods across some of my models.
Given a simple model, I am storing some encoded data in one of the
fields:
class DataElement < ActiveRecord::Base
include EmbeddedData
ENCODED = %w(aliases)
end
I’ve then made a concern with the needed methods for managing the data:
module EmbeddedData
extend ActiveSupport::Concern
included do
after_find :decode_fields
before_save :encode_fields
#decoded = {}
end
def decoded(key, value = false)
#decoded[key][:value] if #decoded.has_key? key
end
def decode_fields
#decoded = {} if #decoded.nil?
ENCODED.each do |field|
if attributes[field]
#decoded[field] = {
value: JSON.parse(attributes[field]),
dirty: false
}
end
end
end
def encode_fields
ENCODED.each do |field|
if decoded[field] && decoded[field][:dirty]
attributes[field] = #decoded[field][:value].to_json
end
end
end
end
Given this setup, I get the error uninitialized constant EmbeddedData::ENCODED
If I change the reference to self::ENCODED in the Concern I get the error:
# is not a class/module
I've even tried making a method on the concern register_fields that I can then call from the model, but the model just throws an unknown method error.
Running out of ideas here and looking for help.
So it turns out the way to access the class constant is:
self.class::ENCODED

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.

Adding a method to an attribute in Ruby

How do you define a method for an attribute of an instance in Ruby?
Let's say we've got a class called HtmlSnippet, which extends ActiveRecord::Base of Rails and has got an attribute content. And, I want to define a method replace_url_to_anchor_tag! for it and get it called in the following way;
html_snippet = HtmlSnippet.find(1)
html_snippet.content = "Link to http://stackoverflow.com"
html_snippet.content.replace_url_to_anchor_tag!
# => "Link to <a href='http://stackoverflow.com'>http://stackoverflow.com</a>"
# app/models/html_snippet.rb
class HtmlSnippet < ActiveRecord::Base
# I expected this bit to do what I want but not
class << #content
def replace_url_to_anchor_tag!
matching = self.match(/(https?:\/\/[\S]+)/)
"<a href='#{matching[0]}'/>#{matching[0]}</a>"
end
end
end
As content is an instance of String class, redefine String class is one option. But I don't feel like to going for it because it overwrites behaviour of all instances of String;
class HtmlSnippet < ActiveRecord::Base
class String
def replace_url_to_anchor_tag!
...
end
end
end
Any suggestions please?
The reason why your code is not working is simple - you are working with #content which is nil in the context of execution (the self is the class, not the instance). So you are basically modifying eigenclass of nil.
So you need to extend the instance of #content when it's set. There are few ways, there is one:
class HtmlSnippet < ActiveRecord::Base
# getter is overrided to extend behaviour of freshly loaded values
def content
value = read_attribute(:content)
decorate_it(value) unless value.respond_to?(:replace_url_to_anchor_tag)
value
end
def content=(value)
dup_value = value.dup
decorate_it(dup_value)
write_attribute(:content, dup_value)
end
private
def decorate_it(value)
class << value
def replace_url_to_anchor_tag
# ...
end
end
end
end
For the sake of simplicity I've ommited the "nil scenario" - you should handle nil values differently. But that's quite simple.
Another thing is that you might ask is why I use dup in the setter. If there is no dup in the code, the behaviour of the following code might be wrong (obviously it depends on your requirements):
x = "something"
s = HtmlSnippet.find(1)
s.content = x
s.content.replace_url_to_anchor_tag # that's ok
x.content.replace_url_to_anchor_tag # that's not ok
Wihtout dup you are extending not only x.content but also original string that you've assigned.

Resources