In my Rails6/Grape API app my controller starting to look a little bit heavy - there is a too much logic going so I wrap webhook distribution data into two services Activities::WebhookData and Journeys::WebhookData like below:
endpoint
post do
name = CmsClient.fetch_model_name(model_id)
#some other logic
name == 'Journey' ? ::Journeys::WebhookData.new(params).call : ::Activities::WebhookData.new(params).call
end
Journeys::WebhookData
module Journeys
class WebhookData
def initialize(webhook)
#webhook = webhook
end
attr_accessor :webhook
def call
case webhook[:event_type]
when 'publish'
JourneyWorker.perform_async(webhook)
when 'delete'
HideJourneyWorker.perform_async(webhook)
end
end
end
end
Activities::WebhookData
module Activities
class WebhookData
def initialize(webhook)
#webhook = webhook
end
attr_accessor :webhook
def call
case webhook[:event_type]
when 'publish'
ActivityWorker.perform_async(webhook)
when 'delete'
DeleteActivityWorker.perform_async(webhook)
end
end
end
end
As you see both are pretty much the same, is there a better way to merge these two services into one instead?
I don't advice you to move that to a single class, because both classes have a different way to interpret the webhook data they receive. If you do so you're going to replace that with some if/else conditions.
You can create a class where you define the call method to fetch the values from a hash given the event_type in the attr_reader, then create two other classes inheriting from the first one and just define a hash of its own with the according classes where you're going to invoke perform_async on:
class Webhook
def call
event_types[webhook[:event_type]].public_send(:perform_async, webhook)
end
protected
def initialize(webhook)
#webhook = webhook
end
private
attr_reader :webhook
def event_types
raise NotImplementedError
end
end
module Journeys
class WebhookData < Webhook
def event_types
{ 'publish' => JourneyWorker, 'delete' => HideJourneyWorker }
end
end
end
module Activities
class WebhookData < Webhook
def event_types
{ 'publish' => ActivityWorker, 'delete' => DeleteActivityWorker }
end
end
end
Related
I have and active admin resource. How i can dynamic extend resource. I try do it like this:
ActiveAdmin.register Order do
include UpdatePriceBlock
price_blocks_names names: [:last, :actual]
end
module UpdatePriceBlock
extend ActiveSupport::Concern
def price_blocks_names(options = {})
#price_blocks_names ||= options[:names]
end
def self.included(base)
#price_blocks_names.each do |name|
base.send :member_action, name, method: :get do
end
end
end
end
Now I has an error:
undefined method `price_blocks_names' for #<ActiveAdmin::ResourceDSL
This is a possible way, I don't know yet how you could keep the names inside the active admin register block. Add the price_blocks_names to your model:
class Order < ApplicationRecord
def self.price_bloks_names
%i(last actual)
end
end
And then place this in config/initializers/active_admin_update_price_block.rb
module ActiveAdminUpdatePriceBlock
def self.extended(base)
base.instance_eval do
self.controller.resource_class.price_bloks_names.each do |name|
member_action name, method: :get do
raise resource.inspect
end
end
end
end
end
Now you can extend, but the configuration needs to reside in the model as a class method this way. Haven't found a cleaner way so far.
ActiveAdmin.register Order do
extend UpdatePriceBlock
end
I think I found it:
ActiveAdmin.register Order do
controller do
include UpdatePriceBlock
end
end
What's going on:
Within the register Order do block, self is a special Active Admin thing:
ActiveAdmin.register Order do
puts "What's self here? #{self}"
end
=>
What's self here? #<ActiveAdmin::ResourceDSL:0x000000012b948230>
Within the controller do block, it's the controller class (so, pretty much the same as the body of a class definition):
ActiveAdmin.register Order do
controller do
puts "What's self here? #{self}"
include UpdatePriceBlock
end
end
=> What's self here? Admin::OrdersController
Within a member_action block, it's an instance of the controller, just like in a regular Rails controller action:
ActiveAdmin.register Order do
member_action :action do
puts "What's self here? #{self}"
end
end
=> What's self here? #<Admin::OrdersController:0x00000001259e7e80>
Let's say there are two classes:
1.
class Fax
def initialize(number)
**code**
end
def send!
**code**
end
end
class FaxJob
def perform
Fax.new(number).send!
end
end
In the FaxJobSpec, I need to confirm that
FaxJob.perform_now(number) run the Fax.new(number).send!
You should use a double.
it 'sends fax!' do
fax_instance = instance_double(Fax)
allow(Fax).to(receive(:new).and_return(fax_instance))
allow(fax_instance).to(receive(:send!))
FaxJob.perform_now(number)
expect(fax_instance).to(have_received(:send!))
end
You can avoid having to allow instance and class with a minor refactor to your Fax class:
class Fax
def self.send!(number)
new(number).send!
end
end
FaxJob:
class FaxJob
def perform
Fax.send!(number)
end
end
And then your test:
it 'sends fax!' do
allow(Fax).to(receive(:send!).and_call_original)
FaxJob.perform_now(number)
expect(Fax).to(have_received(:send!).with(number))
end
If you are really into DRY, this should work too:
it 'sends fax!' do
expect(Fax).to(receive(:send!).with(number))
FaxJob.perform_now(number)
end
I am not really found of this latter one because it does not respect the AAA (arrange, act, assert) and it compromises readability, imo.
I am on rails 4.2.10. I need to trigger a job using sidekiq in after_save method. But the job is triggered, before the object is committed into the database, so I get the error, object not found with id=xyz.
So, I need to use
after_commit :method_name, :on => [:create, :update]
But the changes that I made in object doesn't show up in above method. I have an attribute email. When I was calling above method after_save, email_changed? return true. But if I call the same method using after_commit, email_changed? returns `false.
Is it because I am using object.save method and not create method?
Below is the method, which I am calling to trigger the job:
def update_or_create_user
if email_changed?
ServiceUpdateDataJob.perform_later action: 'update', data: {type: 'user', user_id: self.id}
end
true
end
I recognize this isn't exactly an answer to your question as stated. However...
IMO, you're overloading your model's responsibilities. I suggest you create a service that triggers the job when your model is saved. It might look something like:
class FooService
attr_accessor :unsaved_record
class << self
def call(unsaved_record)
new(unsaved_record).call
end
end
def initialize(unsaved_record)
#unsaved_record = unsaved_record
end
def call
kick_off_job if unsaved_record.save
!unsaved_record.new_record?
end
private
def kick_off_job
# job logic
end
end
You might use the service in a controller something like:
class FooController < ApplicationController
def create
#new_record = ModelName.new(record_params)
if FooService.call(#new_record)
# do successful save stuff
else
# do unsuccessful save stuff
end
end
...
end
I have two models, restaurant and cuisine with a many to many association. And I have this in my app/admin/restaurant.rb
ActiveAdmin.register Restaurant do
scope("All"){|scope| scope.order("created_at desc")}
Cuisine.all.each do |c|
scope(c.name) { |scope| scope.joins(:cuisines).where("cuisines.id=?",c.id)}
end
end
The problem is whenever I delete or add a new cuisine the scopes do not change until I make a change to my admin/restaurant.rb file. How can I fix this issue?
I was able to fix this by adding in my admin/restaurant.rb
controller do
before_filter :update_scopes, :only => :index
def update_scopes
resource = active_admin_config
Cuisine.order("created_at ASC").each do |m|
next if resource.scopes.any? { |scope| scope.name == m.name}
resource.scopes << (ActiveAdmin::Scope.new m.name do |restaurants|
restaurants.joins(:cuisines).where("cuisines.id=?", m.id)
end)
end
resource.scopes.delete_if do |scope|
!(Cuisine.all.any? { |m| scope.name == m.name })
end
resource.scopes.unshift(ActiveAdmin::Scope.new "All" do |restaurants| restaurants end)
end
found the solution here
I'm not sure of a way to defines scopes dynamically, at least using the scope method.
The alternative to the scope method is defining a class method, which accomplishes the same thing so far as I know.
In other words,
scope("All"){|scope| scope.order("created_at desc")}
is the same as
# in a Class
class << self
def All
order("created_at desc")
end
end
You can dynamically create class methods using this method (taken from ruby-defining-class-methods:
class Object
def meta_def name, &blk
(class << self; self; end).instance_eval { define_method name.to_s, &blk }
end
end
I'll use the following to remove the generated class methods:
class Object
def meta_undef name
(class << self; self; end).class_eval { remove_method name.to_sym }
end
end
These methods can be called from the save and destroy hooks on your models, i.e.:
# in a Model
def save(*args)
self.class.meta_def(name) do
joins(:cuisines).where("cuisines.id=?",c.id)
end
super(*args)
end
def destroy(*args)
self.class.meta_undef(name)
super(*args)
end
Then whenever a record is created or removed, the scopes will be updated. There are pros and cons of this approach. Clearly it's nice to define methods on the fly, but this is vulnerable to remote code execution.
Personally I'd probably hold off from dynamically defining class methods (i.e. scopes) and just make one that accepts an argument. Example:
# This is with standard ActiveRecord, not sure about ActiveAdmin
class Restaurant < ActiveRecord::Base
def self.All
order("created_at desc")
end
end
class Cuisine < ActiveRecord::Base
def self.by_name(name)
Restaurant.all.joins(:cuisines).where("cuisines.name=?", name)
end
end
Cuisine.by_name("something")
Restaurant.all.All
Restaurant.All
edit in response to your comment:
load(file) will re-load the source. So you could try the following:
# in a model
def save(*args)
load(Rails.root.join("app", "models", "THIS_MODEL_FILE.rb")
super
end
def destroy(*args)
load(Rails.root.join("app", "models", "THIS_MODEL_FILE.rb")
super
end
Under the hood, save is called for both create and update. So overriding it and destroy covers all the CRUD operations.
The reason I didn't initially recommend this approach is that I haven't personally used it. I'd be curious to know how it works.
Is it possible to add a callback to a single ActiveRecord instance? As a further constraint this is to go on a library so I don't have control over the class (except to monkey-patch it).
This is more or less what I want to do:
def do_something_creazy
message = Message.new
message.on_save_call :do_even_more_crazy_stuff
end
def do_even_more_crazy_stuff(message)
puts "Message #{message} has been saved! Hallelujah!"
end
You could do something like that by adding a callback to the object right after creating it and like you said, monkey-patching the default AR before_save method:
def do_something_ballsy
msg = Message.new
def msg.before_save(msg)
puts "Message #{msg} is saved."
# Calls before_save defined in the model
super
end
end
For something like this you can always define your own crazy handlers:
class Something < ActiveRecord::Base
before_save :run_before_save_callbacks
def before_save(&block)
#before_save_callbacks ||= [ ]
#before_save_callbacks << block
end
protected
def run_before_save_callbacks
return unless #before_save_callbacks
#before_save_callbacks.each do |callback|
callback.call
end
end
end
This could be made more generic, or an ActiveRecord::Base extension, whatever suits your problem scope. Using it should be easy:
something = Something.new
something.before_save do
Rails.logger.warn("I'm saving!")
end
I wanted to use this approach in my own project to be able to inject additional actions into the 'save' action of a model from my controller layer. I took Tadman's answer a stage further and created a module that can be injected into active model classes:
module InstanceCallbacks
extend ActiveSupport::Concern
CALLBACKS = [:before_validation, :after_validation, :before_save, :before_create, :after_create, :after_save, :after_commit]
included do
CALLBACKS.each do |callback|
class_eval <<-RUBY, __FILE__, __LINE__
#{callback} :run_#{callback}_instance_callbacks
def run_#{callback}_instance_callbacks
return unless #instance_#{callback}_callbacks
#instance_#{callback}_callbacks.each do |callback|
callback.call
end
end
def #{callback}(&callback)
#instance_#{callback}_callbacks ||= []
#instance_#{callback}_callbacks << callback
end
RUBY
end
end
end
This allows you to inject a full set of instance callbacks into any model just by including the module. In this case:
class Message
include InstanceCallbacks
end
And then you can do things like:
m = Message.new
m.after_save do
puts "In after_save callback"
end
m.save!
To add to bobthabuilda's answer - instead of defining the method on the objects metaclass, extend the object with a module:
def do_something_ballsy
callback = Module.new do
def before_save(msg)
puts "Message #{msg} is saved."
# Calls before_save defined in the model
super
end
end
msg = Message.new
msg.extend(callback)
end
This way, you can define multiple callbacks, and they will be executed in the opposite order you added them.
The following will allow you to use an ordinary before_save construction, i.e. calling it on the class, only in this case, you call it on the instance's metaclass so that no other instances of Message shall be affected. (Tested in Ruby 1.9, Rails 3.13)
msg = Message.new
class << msg
before_save -> { puts "Message #{self} is saved" } # Here, `self` is the msg instance
end
Message.before_save # Calling this with no args will ensure that it gets added to the callbacks chain (but only for your instance)
Test it thus:
msg.save # will run the before_save callback above
Message.new.save # will NOT run the before_save callback above