Rails include behaviour? - ruby-on-rails

On this project setup :
RoR : 6.0.3.4
Ruby: 2.7.1p83
I have this bit of code serving as a Nokogiri helper - within app > helpers > nepper > helpers > nokogiri.rb (a bit redundant fo'sure) :
# frozen_string_literal: true
module Nepper
module Helpers
# Dedicated to Nokogiri utilities
module Nokogiri
# HTML::Document extension
class ::Nokogiri::HTML::Document
def has?(css_selector)
# css_selector (symbole): const name
css(css_selectors.const_get(css_selector)).present?
end
def text_at(css_selector)
# css_selector (symbole): const name
elt = css(css_selectors.const_get(css_selector))
# raise TypeError 'No text on node' unless elt.text.present?
elt.text
end
def href_at(css_selector)
# css_selector (symbole): const name
elt = css(css_selectors.const_get(css_selector)).first
# raise TypeError 'Not a link' unless elt[:href].present?
elt[:href]
end
end
# css selectors constants shortcut
def css_selectors
Nepper::Constants::CssSelector
end
end
end
end
and within a model declaration, something looking like this
class Card
include Mongoid::Document
include Nepper::Helpers::Nokogiri
# [...]
end
My issue is that, within rails console, I get undefined method for Nokogiri::HTML::Document error for css_selectors whenever an instance of the class is using any of the previous added methods.
More troublesome : It worked just fine yesterday right before pushing the code.
I could put the method within the Nokogiri::HTML::Document method implementation, but I want to keep this bit of code accessible whenether a file includes it, without having to use a Nokogiri instance nor duplicating the code.
So ... What's going on with this weird load behaviour ?
I don't get why the Nokogiri::HTML::Document instances don't access the module method anymore ... both should exist once this code is included isn't it ?
Edit : Btw I had the same issue with TypeError being errored as undefined - for why I had to comment those raise error parts (also weird). This is as if some fo the Main object modules are not loaded prior to those methods calls.

Related

Rails Active support concern included do : "uninitialized constant" error when using "includer" constant

Disclaimer : I know that it's a very bad pattern, I just want to understand why I have the following behaviour and the mechanics behind it.
Lets have one class with some constants / methods
one concern
one monkey patch that include that concern into my first class
I put all of the following in one rails model (under app/modela/a.rb)
class A
MY_CONST = "const"
def self.hello
"world"
end
end
module MyConcern
extend ActiveSupport::Concern
included do
byebug # Here I want to try many things
end
end
class A
include MyConcern
end
Then I open my console and run the following to load my class :
[1] pry(main) > A
First I'm my A Class is loaded, then the MyConcern module, then the monkey patch.
When I enter my byebug I have some weird behaviour
(byebug) self
A # Important : I'm in the scope of my A class
(byebug) hello
"world" # so far so good, my A class is loaded and have a world class method
(byebug) A::MY_CONST
"const" # Obiously this is working
(byebug) self::MY_CONST
"const" # When I Explicitly access it through `self`, it's also working
(byebug) MY_CONST
*** NameError Exception: uninitialized constant MyConcern::MY_CONST
nil # Here for some reason, and that's what I want to understand, I can't access my const, even if `self == A`
I want' to know why constant are not working the same way. Why ruby isn't able to find this const. My scope is my A class, self == A, so why can't I access my constant?
ps : Rails.version => "6.0.0"

How to detect if ruby code is invoked via Ruby on Rails

I wrote a small .rb tool that used the "blank?" method. I want my program to continue to work if invoked directly by ruby. I Monkey Patched Object with code below but I don't want to monkey patch when running under Rails. What can I do?
class Object
def blank?
respond_to?(:empty?) ? (respond_to?(:strip) ? strip.empty? : !!empty?) : !self
end
end
The first thing to keep in mind is that monkey-patching a class directly — that is, opening a class to define a new method — is discouraged. It works, but it's not very flexible and it's considered a code smell.
A more sensible approach to monkey-patching is to define your methods in a mixin and then including it in a class.
This also allows you to conditionally include the mixin. For example, a common requirement in Ruby Gems is to only implement or define something if another library is (already) loaded. A common way to do this is to check if a constant from that library is defined. For example, in your case you could do this:
module PresenceExtensions
def blank?
respond_to?(:empty?) ? (respond_to?(:strip) ? strip.empty? : !!empty?) : !self
end
end
unless Module.const_defined?("Rails") || Object.method_defined?(:blank?)
Object.include PresenceExtensions
end
Another common technique is to try to load a gem and then add your alternative monkey-patch only if the gem is not available:
begin
require "active_support/core_ext/object/blank"
rescue LoadError
Object.include PresenceExtensions
end
This technique has the advantage that will tell you immediately if the gem is not available, so that you don't have to worry about load order.
Rails by default should not load a file at runtime unless it is expected to through some kind of configuration either by default or an initializer etc. If this class definition just sits inside your lib/monkey.rb for example, Rails won't auto-load it unless you tell it to.
You can test this out in your rails console if you are using pry.
Just do:
rails c
# inside your console:
show-method Object.blank?
# this should show you the actual method definition which should be somthing
# like:
From: /Users/myself/.rvm/gems/ruby-2.5.1/gems/activesupport-4.2.10/lib/active_support/core_ext/object/blank.rb # line 16:
Owner: Object
Visibility: public
Number of lines: 3
def blank?
respond_to?(:empty?) ? !!empty? : !self
end
But if rails had loaded your lib file, you would see instead that definition which you can force in the console with require
require './lib/monkey.rb'
show-method Object.blank?
From: /Users/myself/some/rails/project/lib/monkey.rb # line 2:
Owner: Object
Visibility: public
Number of lines: 4
def blank?
puts "this is a monkey patch"
respond_to?(:empty?) ? (respond_to?(:strip) ? strip.empty? : !!empty?) : !self
end

Rails: Reference an ActiveRecord model over a module with the same name

I have the following standard Rails ActiveRecord Foo defined:
# app/models/foo.rb
class Foo < ApplicationRecord
end
And I'm trying to call Foo.find(..) from within a hierarchy that contains a module also named Foo..
# lib/commands/bar.rb
module Commands
module Bar
module Create
class Command
def initialize(params)
...
Foo.find(params[:foo_id]
...
end
end
end
end
end
# lib/commands/foo.rb
module Commands
module Foo
module Create
class Command
...
end
end
end
end
Ruby/Rails is finding Commands::Foo instead of my Foo Model and throwing undefined method 'find' for Commands::Foo:Module.. how can I point at the correct ActiveModel implementation?
The obvious answer is to rename Commands::Foo.. to Commands::Foos.. but I'm curious to know if there's another way :o)
If you want to avoid the clash then you should rename the modules. The existing structure is unwieldy and will present similar problems to all future maintainers.
The best solution that I find in your code is to ensure you call the appropriate module and method via its full path:
2.3.3 :007 > ::Commands::Foo::Create::Command.new
"Commands::Foo::Command reached"
=> #<Commands::Foo::Create::Command:0x007ffa1b05e2f0>
2.3.3 :008 > ::Commands::Bar::Create::Command.new
"Commands::Bar::Command reached"
=> #<Commands::Bar::Create::Command:0x007ffa1b04f110>
You shouldn't try to override or modify internal Rails calls, because then you've modified the framework to fit code, which leads to unpredictable side effects.
You can try to call::Foo in Commands::Foo, it should go with your Foo model

Ruby - Check if controller defined

I am using Solidus with Ruby on Rails to create a webshop and I have multiple modules for that webshop.
So, I defined a me controller into an module called 'solidus_jwt_auth' with the followin code:
module Spree
module Api
class MeController < Spree::Api::BaseController
def index
...
end
def orders
...
end
def addresses
...
end
end
end
end
I want to extend this in another module called 'solidus_prescriptions' so I created a decorator for this with the following code me_decorator:
if defined? Spree::Api::MeController.class
Spree::Api::MeController.class_eval do
def prescriptions
...
end
def create_prescription
...
end
private
def prescription_params
params.require(:prescription).permit(
*Spree::CustomerPrescription.permitted_attributes
)
end
end
end
And for this I wrote unit tests in solidus_prescription module and integration tests in webshop. The unit tests are working fine, but the integration tests are giving the following error:
Error:
MeEndpointsTest#test_me/prescriptions_post_endpoint_throws_an_error_when_wrong_params:
AbstractController::ActionNotFound: The action 'create_prescription' could not be found for Spree::Api::MeController
test/integration/me_endpoints_test.rb:68:in `block in '
Which means that he can not find the MeController defined in another module. How can I make the check if the MeController is defined since the code bellow does not help me with anything:
if defined? Spree::Api::MeController.class
end
This worked in the end:
def class_defined?(klass)
Object.const_get(klass)
rescue
false
end
if class_defined? 'Spree::Api::MeController'
....
end
if defined? should do exactly what you want it to do in theory. The problem is you're checking if defined? Spree::Api::MeController.class. The #class of your class is Class. So what you're really getting is if defined? Class which will always be true!
This issue is most likely not that the conditional is failing but that it's never getting read. Rails lazy loads most of the code you write, meaning the file is not read until it's called somewhere in execution.
The decorator module should just contain the methods you want to add, without the conditionals or the use of class_eval. Then in the original class you can include it.
module Spree
module Api
class MeController < Spree::Api::BaseController
include MeDecorator
end
end
end
If for any reason you're not certain MeDecorator will be defined, don't use defined?, because defined? MeDecorator will not actually go looking for it if it's not defined and load the necessary file. It will return nil if the constant has no value. Just rescue a NameError
module Spree
module Api
class MeController < Spree::Api::BaseController
begin
include MeDecorator
rescue NameError => e
logger.error e
end
end
end
end

Rails: trouble including modules via a plugin

I'm trying to create a rails plugin and the problem I'm facing is that the app won't include my modules when migrating the plugin.
Here's what I have so far:
1. A file lib/patch/settings_helper_patch.rb with extension code
2. An init.rb file with require_dependency 'patch/settings_helper_patch'
3. Some code in settings_helper_patch.rb which is as follows:
module ValidateIssuePatch
module Patch
module SettingsHelperPatch
def self.included(base)
base.send(:include, InstanceMethods)
end
module InstanceMethods
def issue_options
#some code here
end
end
end
end
end
unless SettingsHelper.included_modules.include?(ValidateIssuePatch::Patch::SettingsHelperPatch)
SettingsHelper.send(:include, ValidateIssuePatch::Patch::SettingsHelperPatch)
end
After I migrate the plugin, I wish to use the issue_options method, but I get undefined local variable or method error.
If I run SettingsHelper.included_modules.include?(ValidateIssuePatch::Patch::SettingsHelperPatch) from the console, I get uninitialized constant Patch::SettingsHelperPatch.
However, if I call ValidateIssuePatch from the console, I get => ValidateIssuePatch in response.
Can anyone tell me what is the magic I'm missing here?
Firstly, if your module is only going to have instance methods, I would recommend using the following easy-to-follow syntax:
module ValidateIssuePatch
module Patch
module SettingsHelperPatch
def issue_options
# code
end
end
end
end
SettingsHelper.include(ValidateIssuePatch::Patch::SettingsHelperPatch)
Secondly, the reason why ValidateIssuePatch might be defined is that some other file has it which is being required properly. This file isn't being executed in any way. I would raise an error somewhere that, when raised, will verify that the code is / isn't being executed. Something like the following:
module ValidateIssuePatch
module Patch
module SettingsHelperPatch
raise "All good" # remove this afterwards
def issue_options
# code
end
end
end
end
SettingsHelper.include(ValidateIssuePatch::Patch::SettingsHelperPatch)
Chances are that the error won't be raised and it'll confirm that your file isn't being required - either not at all or not in the right order.
To further verify this, simply open up your console and do the following with your existing code:
ValidateIssuePatch::Patch::SettingsHelperPatch #=> error
require path_of_file
ValidateIssuePatch::Patch::SettingsHelperPatch #=> no more error
Finally, why do you check for the module already being included in SettingsHelper? (referring to the unless condition) Your code should be including the module only once, not "maybe only once".

Resources