Reopening class which includes Mongoid::Document - ruby-on-rails

I have a simple class:
class User
include Mongoid::Document
field :name
end
And I would like to reopen it to add a Mongoid callback:
class User
before_create :do_this
def do_this
# do it...
end
end
Unfortunately I got the error: undefined method 'before_create' for User:Class
Any idea how to do this ? Should I use a mixin pattern instead of re-opening ?
UPDATE: I can't change the original class definition, since it's in a shared library. And the load order is tricky, because it's in Rails. The original class is in a file loaded in autoload_path. Where should I reopen it ? And I would rather use a module rather than reopening, but I'm not sure it's possible to include my module "from the outside" !
UPDATE 2: You are right, it's just a load order problem. So now my question becomes: Since Rails' autoload is lazy, how can I force Rails to load my reopening file after it loads the original class file ? :)

Your code above worked for me in the console. I suspect the second class declaration is being loaded first. You might try printing out a message immediately inside each class declaration e.g.
class User
puts "First"
...
end
...
class User
puts "Second"
...
end
and verifying that they load in the correct order.
Also, if you do have access to the first class declaration, you might use a mixin if possible, as it keeps everything for the User class in a single location.
UPDATE: Can you first load/require the shared User class to ensure it is loaded? That is:
require 'app/models/user'
class User
before_create :do_something
def do_something
...
end
end

Related

Dynamic concerns with inheritance not loading twice, but only once

We are loading code dynamically with concerns, based on some environment variables, which works pretty nice.
Something like this:
# User class
class User
include DynamicConcern
end
module DynamicConcern
extend ActiveSupport::Concern
included do
if "Custom::#{ENV["CUSTOMER_NAME"].camelize}::#{self.name}Concern".safe_constantize
include "Custom::#{ENV["CUSTOMER_NAME"].camelize}::#{self.name}Concern".constantize
end
end
end
# custom code
module Custom::Custom123::UserConcern
extend ActiveSupport::Concern
included do
...
end
end
We are using this since years and it worked absolutely fine in models. Some days ago we tried to use the same approach with Controllers, but realized that this approach doesn'
t work fine with inheritance, where the parent class inherits the concern as well as the inherited class:
class ApplicationController < ActionController::Base
# this gets loaded and includes the right dynamic module
include DynamicConcern
end
class ShopController < ApplicationController
# this is NOT getting loaded again and skipped,
# since it has been loaded already in the parent controller
include DynamicConcern
end
Is there a way to tell rails that it should include/evaluade the concern a second time, since the second time it would have another class name which would include another module?
I'm not looking for other solutions, since a lot of our code is based on this approach and I think it's possible to solve this without rewriting everything.
Thanks!
You are only trying to dynamically include modules based on the class name.
It's not necessary to make a concern but it can be a normal class, and the include action can be a normal method. Every time you want to call it, just call it like any other method.
Because you have already written your code with ActiveSupport::Concern in an include fashion. I guess the following refactor may work even though I cannot guarantee it. The idea is simple:
Just make it a normal method with the target class as the parameter. You can include it (it automatically calls dynamic_include in included hook).
If the module is already included in the ancestor hierarchy chain, just invoke the dynamic_include will immediately call the method and do the dynamic includes.
Please give it a try and let me know if it works for your scenarios.
module DynamicConcern
extend ActiveSupport::Concern
included do
def self.dynamic_include(klass)
if "Custom::#{ENV["CUSTOMER_NAME"].camelize}::#{klass.name}Concern".safe_constantize
klass.include "Custom::#{ENV["CUSTOMER_NAME"].camelize}::#{klass.name}Concern".constantize
end
end
dynamic_include(self)
end
end
class ApplicationController < ActionController::Base
# this gets loaded and includes the right dynamic module
include DynamicConcern
end
class ShopController < ApplicationController
# this is NOT getting loaded again and skipped,
# since it has been loaded already in the parent controller
dynamic_include(self)
end
Actually it's a feature of Rails that the same module doesn't get loaded multiple times.
We started to use the normal ruby module inclution hooks and it worked fine!
module CustomConcern
def self.included(base)
custom_class_lookup_paths = [
"#{HOSTNAME.camelize}::Models::#{base.name}PrependConcern",
"#{HOSTNAME.camelize}::Controllers::#{base.name}PrependConcern"
].map{|class_string| class_string.safe_constantize }.compact
custom_class_lookup_paths.each do |class_string|
base.send :include, class_string
end
end

Accessing class property to define instance methods in rails concern

I'm trying my hands on metaprogramming after a long pause. I found a few questions but could not get an input to solve my problem so I hope someone can enlighten me.
In a rails 5 app, I am trying to write a concern that provides a class method to set configuration options. With those options, I want to define instance methods.
module Base64Attachable
extend ActiveSupport::Concern
class_methods do
attr_reader :base64_attachable_property
private
def base64_attachable(property)
#base64_attachable_property = property
end
end
included do
# ?
end
end
The concern above is used inside a User model:
class User < ApplicationRecord
include Base64Attachable
base64_attachable :image
end
In my understanding, the concern sets up the class method that is being called in the user model. However I do not seem to be able to get the base64_attachable_property inside the included block to define further methods based on the value of it. I thought I would find anything I need in self.class inside the included block, but that's not the case.
The aim in this case is to use define_method to define setters, getters and other methods for image in the user model.
What am I missing here?
The included block is run at the moment the concern is included on the class, the base64_attachable :image line is not run yet.
I'd suggest you follow what official gems does. Check ActiveStorage for example https://github.com/rails/rails/blob/530f7805ed5790af1d472a041bc74089dc183f47/activestorage/lib/active_storage/attached/model.rb#L35. It defines the methods that depends on that property right inside the class method (it uses class_eval, but I guess you can use define_method too):
def has_one_attached(name, dependent: :purge_later)
generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1
def #{name}
#active_storage_attached_#{name} ||= ActiveStorage::Attached::One.new("#{name}", self)
end
def #{name}=(attachable)
attachment_changes["#{name}"] =
if attachable.nil?
ActiveStorage::Attached::Changes::DeleteOne.new("#{name}", self)
else
ActiveStorage::Attached::Changes::CreateOne.new("#{name}", self, attachable)
end
end
CODE
has_one :"#{name}_attachment", ......etc....

Ruby: undefined method for not initialized

Note: There are numerous answers explaining that you can get this error when you subclass ActiveRecord::Base and add an #initialize without super. No answer explains what is actually happening.
I am working in someone else's code and I have an HTTParty service in a Rails app with the following class hierarchy. Note the subclass #initialize with a differing signature to the parent class.
module A
class Base
include HTTParty
...
end
end
module A
class User < Base
def initialize(user)
#user = user
end
end
end
module A
class PublicUser < User
def initialize(opts = {})
#limit = opts[:limit]
# no call to super
end
end
end
Locally there are no problems with this, but in SemaphoreCI the following results:
A::PublicUser.new(limit: 1).some_method
undefined method `some_method' for #<A::PublicUser not initialized>
I can't find any documentation about the "not initialized" message. What causes this sort of failure?
OK, I got it. I also tagged your question with ruby-on-rails, since plain good ruby would rare give such a weird behaviour.
You have experienced two different issues, more or less unrelated.
#<A::PublicUser not initialized> is a result of (sic!) calling inspect on A::PublicUser. So, ruby tries to format an error message and—voilà—the class is printed out that way.
Rails messes with you, as well as with constant lookup. A::Base name conflicts with ActiveRecord::Base, and guess what is resolved when class User < Base is met. To replicate this behaviour you might open a console and do: class Q < ActiveRecord::Base; end; Q.allocate, resulting in #<Q not initialized>. (Do you already love Rails as I do?)
To fix this, either explicitly specify class User < A::Base or rename Base to MyBase. Sorry for suggesting that.

rails - instance model include module on variable condition

I need to know if I can include a module to an instantiated model.
What works today :
in the controller
#m = MyModel.create(params)
in the model
class Doc < ActiveRecord::Base
after_save :set_include
def set_include
if bool
self.class.send(:include, Module1)
else
self.class.send(:include, Module2)
end
end
end
and this works, but I'm afraid that self.class actually include the module for the class model an not the instantiated model
In this case, this will work.
The module methods are call after the object is saved.
But in many case, the controller will call some modules methods.
I thought of called the method set_include (up there) in a before_action of the controller.
But I really thinks that is not a good idea...
Any idea how I can really do that with in a good way ?
thanks !
Answer to your direct question is no. Your code only appears to be working and is actually not modifying instance of a class, but the class itself. So all instances of it will be getting this "benefit". Probably not what you wanted. Let me demonstrate with simple ruby example: https://repl.it/BnLO
What you can do instead is use extend with instance like: https://repl.it/BnLO/2
Applied to your code it would be:
class Doc < ActiveRecord::Base
after_save :set_include
def set_include
if bool
extend(Module1)
else
extend(Module2)
end
end
end
Also, self is not necessary. https://repl.it/BnLO/3
You need to use instance class (a.k.a eigenklass):
def set_include
singleton_class.instance_eval do
include bool ? Module1 : Module2
end
end
However the fact that you want to do this is suspicious and might lead to a disaster. So the question is: what are you really trying to achieve here - there surely is the better way of doing so.

Why do functions from my Rails plugin not work without specifically requiring?

I need some help with my plugin. I want to extend ActiveRecord::Base with a method that initializes another method that can be called in the controller.
It will look like this:
class Article < ActiveRecord::Base
robot_catch :title, :text
...
end
My attempt at extending the ActiveRecord::Base class with robot_catch method looks like following. The function will initialize the specified attributes (in this case :title and :text) in a variable and use class_eval to make the robot? function available for the user to call it in the controller:
module Plugin
module Base
extend ActiveSupport::Concern
module ClassMethods
def robot_catch(*attr)
##robot_params = attr
self.class_eval do
def robot?(params_hash)
# Input is the params hash, and this function
# will check if the some hashed attributes in this hash
# correspond to the attribute values as expected,
# and return true or false.
end
end
end
end
end
end
ActiveRecord::Base.send :include, Plugin::Base
So, in the controller, this could be done:
class ArticlesController < ApplicationController
...
def create
#article = Article.new(params[:article])
if #article.robot? params
# Do not save this in database, but render
# the page as if it would have succeeded
...
end
end
end
My question is whether if I am right that robot_catch is class method. This function is to be called inside a model, as shown above. I wonder if I am extending the ActiveRecord::Base the right way. The robot? function is an instance method without any doubt.
I am using Rails 3.2.22 and I installed this plugin as a gem in another project where I want to use this functionality.
Right now, it only works if I specifically require the gem in the model. However, I want it the functionality to be included as a part of ActiveRecord::Base without requiring it, otherwise I'd have to require it in every model I want to use it, not particularly DRY. Shouldn't the gem be automatically loaded into the project on Rails start-up?
EDIT: Maybe callbacks (http://api.rubyonrails.org/classes/ActiveSupport/Callbacks/ClassMethods.html) would be a solution to this problem, but I do not know how to use it. It seems a bit obscure.
First, I would suggest you make sure that none of the many many built in Rails validators meet your needs.
Then if that's the case, what you actually want is a custom validator.
Building a custom validator is not as simple as it might seem, the basic class you'll build will have this structure:
class SpecialValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
# Fill this with your validation logic
# Add to record.errors if validation fails
end
end
Then in your model:
class Article < ActiveRecord::Base
validates :title, :text, special: true
end
I would strongly suggest making sure what you want is not already built, chances are it is. Then use resources like this or ruby guides resources to continue going down the custom validator route.
Answer
I found out the solution myself. Bundler will not autoload dependencies from a gemspec that my project uses, so I had to require all third party gems in an engine.rb file in the lib/ directory of my app in order to load the gems. Now everything is working as it should.
Second: the robot_catch method is a class method.

Resources