How to get a rails engine to interact with main App - ruby-on-rails

I am creating a rails engine used as an interface to a payment service. I have to handle the use cases where the payment service is sending me message outside of any transaction.
I will take the following use case :
The engine receive a message that subscription FooBar42 has been correctly billed(through it's own routes). What does my engine do next?
If i start to call models specific to my app, my engine is only good for this app.
Only example i could find is Devise, but in devise, it just add methods to your user model and the engine handle how the user is stored and all the code.
How do i create a reusable system where my engine can call/trigger code in the main app ?
Do i override engine controller ? Do i generate a service object with empty methods that will be used as an engine-app communication system?

You need to have a way to configure your engine either in an initializer or through calling class methods in your model, much like how you configure Devise to use your User model when you call devise in the body of the User class:
class User < ActiveRecord::Base
devise :database_authenticatable
end
For one of my engines I use an initializer that stores the classes the engine interacts with using a config object.
Given a config object:
class Configuration
attr_reader :models_to_interact_with
def initialize
#models_to_interact_with = []
end
end
You can then provide a config hook as a module method in your main engine file in lib:
require 'my_engine/configuration'
module MyEngine
mattr_reader :config
def self.configure(&block)
self.config ||= Configuration.new
block.call self.config
end
end
Now when you're in the main app, you can create an initializer in config/initializers/my_engine.rb and add the models to your Configuration:
MyEngine.configure do |config|
config.models_to_interact_with << User
config.models_to_interact_with << SomeOtherModel
end
Now you have access to this from you engine without having to hardcode the model in your engine:
# in some controller in your engine:
def payment_webhook
MyEngine.config.models_to_interact_with.each do |model|
model.send_notification_or_something!
end
end
Hope that answers your question.

Related

Overriding a gem's controller method in Rails 5

I'm using a (private) gem which defines a Rails 5.2 controller. I want to override the private params permitting method in the mentioned controller to add some different params.
I tried to reference the Rails Engine Guide but it doesn't actually show how to override a controller method (only a model, and the same approach doesn't seem to work for controllers.)
Update: Here's the decorator pattern I tried based on the aforementioned Rails guide.
Load the decorators (Is the /lib folder even loaded by Rails anymore?):
# MyApp/lib/private_gem/engine.rb
module PrivateGem
class Engine < ::Rails::Engine
isolate_namespace PrivateGem
config.to_prepare do
Dir.glob(Rails.root + "app/decorators/**/*_decorator*.rb").each do |c|
require_dependency(c)
end
end
end
end
The Decorator:
# MyApp/app/decorators/controllers/private_gem/users_controller_decorator.rb
PrivateGem::UsersController.class_eval do
private
# Only allow a trusted parameter "white list" through.
def user_params
params.require(:user).permit(:existing_param, :new_param)
end
end
I have a working solution that I don't like. First, I load the existing class and then redefine the private method.
# MyApp/controllers/private_gem/users_controller.rb
load PrivateGem::Engine.root.join('app/controllers/private_gem/users_controller.rb')
PrivateGem::UsersController.class_eval do
private
# Only allow a trusted parameter "white list" through.
def user_params
params.require(:user).permit(:existing_param, :new_param)
end
end
Is this really the best way to do this? Maybe it belongs in a different place? Should I somehow use ActiveSupport::Concern? It doesn't seem like idiomatic Rails to me but I have a lot to learn about how Rails is initialized and how Engines and Concerns work.
Thanks!

Rails Route Helper Methods included into Service Object not working properly

I wanted to make my route helpers available in my service object.
Ex:
blog_path(blog) #make available in service object
The issue is that I am using passenger, so the application is relative to the domain.
Ex: Instead of the path loading: www.my_domain.com/blog/1, passenger loads the path with: www.my_domain.com/this_app/blog/1.
Currently my route helper in my service object is rendering the first version and not the second version.
Here is what my service object looks like:
class BuildLink
include ActionView::Helpers::TagHelper
include ActionView::Helpers::UrlHelper
RouteHelpers = Rails.application.routes.url_helpers
attr_accessor :blog
def initialize(blog)
#blog = blog
end
def init
content_tag(:li, link_to(“Show Blog“, RouteHelpers.blog_path(blog)))
end
end
The route works locally because on localhost I do not have a relative path. But when I put it in production it does not work because passenger is expecting the application name as the relative path, but the service object is not including the application name within the url it generates.
That relative path works everywhere else in my application, it just doesn't properly generate the relative path within the service object.
The issue is that actionview-related methods are not available to POROs.
In order to get all the great stuff from actionview: you need to utilize the view_context keyword. Then: you can simply call upon actionview-related methods from your view_context:
class BuildLink
attr_accessor :blog, :view_context
def initialize(blog, view_context)
#blog = blog
#view_context = view_context
end
def init
content_tag(:li, link_to(“Show Blog“, view_context.blog_path(blog)))
end
end
So for example: from your controller you would call upon this PORO like so:
BuildLink.new(#blog, view_context).init
For more information, see below references:
Rails doc on view_context
Utilization of view_context via presenter pattern, shown in this article
Railscast which talks through utilizing view_context via presenter pattern

Devise sign_in resource works even without importing module

So, I was customizing devise in a custom sign up page which required me to sign_in a user after creating the account along with some other operations. After creating the resource I did
sign_in resource if resource.active_for_authentication?
and it signs in the user. My controller inherits the
ApplicationController
and I haven't included any modules like this
include Devise::Controllers::SignInOut
How did rails know about the
sign_in
method
Basic answer - the module Devise::Controllers::Helpers (which includes Devise::Controllers::SignInOut that you discovered) is automatically included in ApplicationController by one of the Devise initializer named "devise.url_helpers". Initializer is included by adding Devise gem, and its content is run during rails Application startup.
Going deeper
Devise is a Rails Engine - you can check this article for brief review.
Engines can be considered miniature applications that provide
functionality to their host applications. A Rails application is
actually just a "supercharged" engine, with the Rails::Application
class inheriting a lot of its behavior from Rails::Engine.
...
Engines are also closely related to plugins.
Then, you will find the following call at rails.rb of Devise (this rails.rb is required by gem root devise.rb file - see below) here:
initializer "devise.url_helpers" do
Devise.include_helpers(Devise::Controllers)
end
To state, initializer here is not a method definition, but actual calling a class method with string name parameter and block parameter. It's executed on class load (i.e. as a result of loading a class by require). Same time, passed block serves as parameter to this call, and in this particular case is saved to be executed later - see below explanation of initializers.
Side note on engines (in fact railtie) initializer concept
Initializer is a concept by one of the Rails basic class Railtie. Concept is described here:
Initializers - To add an initialization step from your Railtie to Rails boot process, you just need to create an initializer block:
# class MyRailtie < Rails::Railtie
# initializer "my_railtie.configure_rails_initialization" do
# # some initialization behavior
# end
# end
The implementation of initializers logic is part of Initilizable module which is included into Railtie class. Specific initializer class method basically adds passed block to class initializers array source:
def initializer(name, opts = {}, &blk)
...
initializers << Initializer.new(name, nil, opts, &blk)
end
It's not executed immediately. It's run by executing run method on initializers in specific order by run_initializers call, which is also part of Initializable module. This method is available for rails Application with inherits from Engine (which includes Initializable module).
def run_initializers(group=:default, *args)
return if instance_variable_defined?(:#ran)
initializers.tsort_each do |initializer|
initializer.run(*args) if initializer.belongs_to?(group)
end
#ran = true
end
This run_initializers method is triggered by initialize! call (see below a bit later) of the Application.
Side note on collecting all initializers by Rails Application.
Meanwhile, initializers here is an overloaded method in Application class:
def initializers #:nodoc:
Bootstrap.initializers_for(self) +
railties_initializers(super) +
Finisher.initializers_for(self)
end
This method will load all initializers of the application for further ordering and running.
Inside, railties_initializers will call ordered_railties, which will use railties getter of Engine class (which Application is inherited from). This getter is the following
def railties
#railties ||= Railties.new
end
Railties (plural) service class is different from Railtie. It actually just collects all railties by looking at all subclasses of both Engine and Railtie classes.
def initialize
#_all ||= ::Rails::Railtie.subclasses.map(&:instance) +
::Rails::Engine.subclasses.map(&:instance)
end
Finally, subclasses is a method from extension of Ruby base Class, which Rails extend for its convenience
def subclasses
subclasses, chain = [], descendants
chain.each do |k|
subclasses << k unless chain.any? { |c| c > k }
end
subclasses
end
end
Back to running initialiers by Application. As mentioned above, run_initializers is called by initialize! call of the Application class:
def initialize!(group=:default) #:nodoc:
raise "Application has been already initialized." if #initialized
run_initializers(group, self)
#initialized = true
self
end
Which for the Rails app is triggered by Rails.application.initialize! call in environment.rb file - see generator source
How those initializers got added to the running queue? This happens by adding Devise gem (e.g. by Bundle.require), which loads lib/devise.rb gem root file, and which has following require at the very bottom:
require 'devise/rails'
As this loads Devise class, it will be discovered by Railties class by looking at subclasses for Engine.
Back to Devise devise.url_helpers initializer
If you look at include_helpers call, this is what it does:
def self.include_helpers(scope)
ActiveSupport.on_load(:action_controller) do
include scope::Helpers if defined?(scope::Helpers)
include scope::UrlHelpers
end
ActiveSupport.on_load(:action_view) do
include scope::UrlHelpers
end
end
ActiveSupport on_load call is a Rails feature to lazy_load components. source:
# lazy_load_hooks allows Rails to lazily load a lot of components
and thus # making the app boot faster.
In this case, those include commands for controller will be executed when controller is loaded, but not on server start. Check this or any other articles on the concept.
And this is the place where that lazy block is run - source:
module ActionController
# Action Controllers are the core of a web request in \Rails. They are made up of one or more actions that are executed
# on request and then either it renders a template or redirects to another action. An action is defined as a public method
# on the controller, which will automatically be made accessible to the web-server through \Rails Routes.
#
# By default, only the ApplicationController in a \Rails application inherits from <tt>ActionController::Base</tt>. All other
# controllers in turn inherit from ApplicationController. This gives you one class to configure things such as
# request forgery protection and filtering of sensitive request parameters.
...
class Base < Metal
...
ActiveSupport.run_load_hooks(:action_controller, self)
end
end
BTW, your ApplicationController generated by Rails is inherited from ActionController::Base

Can't access model from inside ActionController method added by a Rails engine

I am developing a Rails engine to be packaged as a gem. In my engine's main module file, I have:
module Auditor
require 'engine' if defined?(Rails) && Rails::VERSION::MAJOR == 3
require 'application_controller'
end
module ActionController
module Auditor
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def is_audited
include ActionController::Auditor::InstanceMethods
before_filter :audit_request
end
end
module InstanceMethods
def audit_request
a = AuditorLog.new
a.save!
end
end
end
end
ActionController::Base.send(:include, ActionController::Auditor)
where AuditorLog is a model also provided by the engine. (My intent is to have "is_audited" added to the controllers in an application using this engine which will cause audit logging of the details of the request.)
The problem I have is that when this code gets called from an application where the engine is being used, the AuditorLog model isn't accessible. It looks like Ruby thinks it should be a class in ActionController:
NameError (uninitialized constant
ActionController::Auditor::InstanceMethods::AuditorLog)
rather than a model from my engine.
Can anyone point me in the right direction? This is my first time creating an engine and attempting to package it as a gem; I've searched for examples of this and haven't had much luck. My approach to adding this capability to the ActionController class was based on what mobile_fu does, so please let me know if I'm going about this all wrong.
Use ::AuditorLog to access the ActiveRecord class (unless you have it in a module or namespace, in which case you'll need to include the module name).

Determine the domain in an ActiveRecord model

I am in the middle of migrating my application from using subdirectories for userspace to subdomains (ie. domain.com/~user to user.domain.com). I've got a method in my user class currently to get the "home" URL for each user:
class User
def home_url
"~#{self.username}"
# How I'd like to do it for subdomains:
#"http://#{self.username}.#{SubdomainFu.host_without_subdomain(request.host)}"
end
end
I'd like to update this for subdomains, but without hardcoding the domain into the method. As you can see, I am using the subdomain-fu plugin, which provides some methods that I could use to do this, except that they need access to request, which is not available to the model.
I know it's considered bad form to make request available in a model, so I'd like to avoid doing that, but I'm not sure if there's a good way to do this. I could pass the domain along every time the model is initialized, I guess, but I don't think this is a good solution, because I'd have to remember to do so every time a class is initialized, which happens often.
The model shouldn't know about the request, you're right. I would do something like this:
# app/models/user.rb
class User
def home_url(domain)
"http://#{username}.#{domain}"
end
end
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# ...
def domain
SubdomainFu.host_without_subdomain(request.host)
end
# Make domain available to all views too
helper_method :domain
end
# where you need it (controller or view)
user.home_url(domain)
If there is such a thing as a canonical user home URL, I would make a configurable default domain (e.g. YourApp.domain) that you can use if you call User#home_url without arguments. This allows you to construct a home URL in places where, conceptually, the "current domain" does not exist.
While molf's answer is good, it did not solve my specific problem as there were some instances where other models needed to call User#home_url, and so there would be a lot of methods I'd have to update in order to pass along the domain.
Instead, I took inspiration from his last paragraph and added a base_domain variable to my app's config class, which is the set in a before_filter in ApplicationController:
module App
class << self
attr_accessor :base_domain
end
end
class ApplicationController < ActionController::Base
before_filter :set_base_domain
def set_base_domain
App.base_domain = SubdomainFu.host_without_subdomain(request.host)
end
end
And thus, when I need to get the domain in a model, I can just use App.base_domain.

Resources