Drying up Rails controllers - ruby-on-rails

I have an application in Ruby/Rails where I am going to have to connect to a third-party application (Xero accounting) in order to send/pull data. This happens in several controllers thoughout the application. However, whilst the specifics are different, the actual connection is the same, and looks like this:
require 'xero_gateway'
xero_config = YAML.load_file("#{Rails.root}/config/xero.yml")["testing"]
xero_private_key = "#{ENV['HOME']}/certs/breathehr_xero.key" || ENV["xero_private_key"]
xero_gateway = XeroGateway::PrivateApp.new(xero_config['consumer_key'], xero_config['consumer_secret'], xero_private_key)
The next step of the code might be fetch something, like an invoice, as in:
xero_gateway.get_invoice('INV-001')
Or, in another controller, it might be to create a contact, such as:
xero_gateway.build_contact
Is there somewhere I can put the connection code so that I can end up just calling xero_gateway in each specific controller? It doesn't feel right to be repeating the same code again and again each time I have to authenticate and connect.

To build on #Dave Newton's answer:
You would create a "provider" class that marries an object factory to some configuration:
File: lib/xero_gateway_provider.rb
require 'xero_gateway'
class XeroGatewayProvider
cattr_accessor :default_private_key, :default_consumer_key, :default_consumer_secret
def initialize(overrides = {})
#private_key = overrides[:private_key] || self.class.default_private_key
#consumer_key = overrides[:consumer_key] || self.class.default_consumer_key
#consumer_secret = overrides[:consumer_secret] || self.class.default_consumer_secret
end
def create_private_app
XeroGateway::PrivateApp.new(#consumer_key, #consumer_secret, #private_key)
end
end
Then you could create a Rails initializer: config/initializers/xero_gateway_provider.rb
require 'xero_gateway_provider'
conf = YAML.load_file("#{Rails.root}/config/xero.yml")[Rails.env]
XeroGatewayProvider.default_private_key = "#{ENV['HOME']}/certs/breathehr_xero.key" || ENV["xero_private_key"]
XeroGatewayProvider.default_consumer_key = conf["consumer_key"]
XeroGatewayProvider.default_consumer_secret = conf["consumer_secret"]
And to use it:
# Using default configs
provider = XeroGatewayProvider.new;
private_app = provider.create_private_app
private_app.get_invoice("...")
# Using overrides
provider = XeroGatewayProvider.new :consumer_key => '...', :consumer_secret => '...';
private_app = provider.create_private_app
private_app.get_invoice("...")
Edit: Just realized there is no point to instantiating XeroGatewayProvider if it uses class level properties, so I made them defaults allowing you to configure each provider individually.
Also #Gareth Burrows comment on where to put and name the class, I think this would fit just fine in the lib/ directory. See the edits to the post for specifics.

You could put it just about anywhere, including:
A utility class used by your actions, or
A base action class (meh), or
A module included in your actions (meh).
I lean towards a utility class, because:
It's easy to instantiate or mock/stub it for testing, and
It doesn't make the surface area of your action class any bigger

You can create a regular ruby class in the models folder called Xero or something and do this code in the initializer.
require 'xero_gateway'
class Xero
def initialize
xero_config = YAML.load_file("#{Rails.root}/config/xero.yml")["testing"]
xero_private_key = "#{ENV['HOME']}/certs/breathehr_xero.key" || ENV["xero_private_key"]
xero_gateway = XeroGateway::PrivateApp.new(xero_config['consumer_key'], xero_config['consumer_secret'], xero_private_key)
end
end
And then just call:
xero_gateway = Xero.new
Another option is to create an initializer in the initializers/ folder.
xero_gateway.rb
And put the initialization code in there. This way it will be parsed only on application startup.

Related

Correct syntax to call a mailer with interpolation

I have a method to dynamically call a mailer that look like this :
def mail
application = mail[:application].capitalize
class_template = mail[:template].split('/').[0]+ 'Mailer'
template = mail[:template].split('/').second
email_params = mail[:params].values.map(&:inspect).join(', ')
"#{application}::#{class_template}.#{template}(#{email_params}).deliver_now"
# Here I have something like : "Application::TemplatetMailer.test_name(\"John\", \"Doe\").deliver_now"
end
How can I have something like :
Application::TemplatetMailer.test(\"John\", \"Doe\").deliver_now
instead of
"Application::TemplatetMailer.test(\"John\", \"Doe\").deliver_now"
You can look up arbitrary constants with constantize:
mailer = [ application, class_template ].join('::').constantize
mailer.send(template, *email_params).deliver_now
Be extremely careful with what access you allow to end users. Do not expose this in a way that allows them to make arbitrary method calls on arbitrary classes. Having an allow-list of classes and methods is way safer.

Update attribute in associated model in Rails when parent is updated

I have two models User and Assignment. Whenever User is updated I want to update the url attribute in Assignment.
How do I do this?
class User
has_many :assignments
...
end
class Assignment
belongs_to :user
before_save :set_url
def set_sandbox_url
language = 'www'
project = 'example'
base_url = "https://#{language}.#{project}.org/"
sandbox_url = "#{base_url}/User:#{user.username}/#{article_title}"
end
I agree with Tamer Shlash that there is not really a benefit in storing this URL in the database because it can be easily generated each time you need it.
But apart from that, I would like to answer your question. Your callback to regenerate the URL doesn't work for various reasons. First, you want to update the URL when the user changes therefore the user needs to have a callback defined. Second, the naming is not correct. The callback as it is currently written would try to run a set_url method but the method is actually called set_sandbox_url. And third, sandbox_url = will just assign the new URL to a local variable sandbox_url but it would not update the instance variable #sandbox_url.
I would do something like this:
# in app/models/user.rb
after_save :update_assignment_url
private
def update_assignment_url
assignments.each(&:update_url) if username_previously_changed?
end
# in app/models/assignments.rb
def update_url
language = 'www'
project = 'example'
base_url = "https://#{language}.#{project}.org/"
sandbox_url = "#{base_url}/User:#{user.username}/#{article_title}"
update!(sandbox_url: sandbox_url)
end
Note: because you build the URL by simply concatenating strings I suggest making sure that these strings (especially values provided by the user like username and article_title) only include characters that are valid in an URL (for example by using String#parameterize).
You might want to read about Dirty Attributes too which provided the used username_previously_changed? method.

How to pass arguments to initialize method from a class?

I am using Push8 gem for Apple push notification which accepts .P8 certificates.The problem is that I have two bundle_id for two separate apps and need to send Push Notifications to both of them. The Push8 gem accepts bundle_id ENV['APN_BUNDLE_ID'] params automatically from the application.yml file. However, I want it to use ENV['APN_VENDOR_BUNDLE_ID'] as well for a different APP to send Push Notification.
my code to send Push notification is here
def self.send_notification_ios(device_id, notification_id)
send = Notification.where(id: notification_id).first
if Rails.env == 'development'
apn = P8push::Client.development
else
apn = P8push::Client.production
end
token = device_id
notification = P8push::Notification.new(device: token)
notification.alert = send.template.message % { txnid: send.order.txnid }
notification.sound = 'sosumi.aiff'
apn.push(notification)
end
Here If the send.end_user_type is "User" I want to use the Bundle id APN_BUNDLE_ID as the topic, for rest as want to use APN_VENDOR_BUNDLE_ID. But I dont know how to pass APN_VENDOR_BUNDLE_ID as a param to initialize method in the client.rb file of the gem. Hence it always accepts APN_BUNDLE_ID as the topic and hence throws the error topic disallowed.
Here is the client.rb file for the gem:
https://github.com/andrewarrow/p8push/blob/master/lib/p8push/client.rb
The link for the gem is https://github.com/andrewarrow/p8push
If the initialize method makes no accommodations for customizing that attribute you've got two choices: Monkey-patch it to make it do what you want, which is messy, or subclass it and use that instead.
The sub-class solution looks like this:
class UserAwareClient < P8Push::Client
def self.development(user_type)
client = self.new(user_type)
client.jwt_uri = APPLE_DEVELOPMENT_JWT_URI
client
end
def self.production(user_type)
client = self.new(user_type)
client.jwt_uri = APPLE_PRODUCTION_JWT_URI
client
end
def initialize(user_type)
# Initialize as the parent class would
super
# Then detect the user_type argument and decide how to configure it
#private_key =
case (user_type)
when 'User'
File.read(ENV['APN_PRIVATE_KEY'])
else
File.read(ENV['APN_VENDOR_BUNDLE_ID'])
end
end
end
end
Then you create that:
apn = UserAwareClient.development(user_type)
This gem could be made a lot more flexible with a few pull-requests to make your life easier, so consider that, too.

Object.const_get and Rails - cutting off parent module names

I have the following code to pull out and instantiate Rails controllers:
def get_controller(route)
name = route.requirements[:controller]
return if name.nil?
if name.match(/\//)
controller_name = name.split('/').map(&:camelize).join('::')
else
controller_name = name.camelize
end
p controller_name: controller_name
controller = Object.const_get("#{controller_name}Controller")
p controller: controller
controller.new
end
some routes are single names - "users", "friends", "neighbors", "politicians", etc...
other routes are nested, such as "admin/pets", "admin/shopping_lists", "admin/users", etc...
The above code works (in that it properly builds and instantiates the controller) in most of the cases mentioned, except for one - in this example, "admin/users"
from the puts statements, I'm getting the following:
{:controller_name=>"Admin::Users"}
{:controller => UsersController}
You'll notice that the namespace Admin is getting cut off. My guess is that since this is only the case for controllers which share a name in multiple namespaces (users and admin/users), it has something do to with Rails autoloading (?). Any idea about what is causing this?
As per the comment from lx00st, I should also point out that I've tried various forms of getting these constants, another attempt was as follows:
sections = name.split('/')
sections.map(&:camelize).inject(Object) do |obj, const|
const += "Controller" if const == sections.last
obj.const_get(const)
end
The same problem was encountered with this approach.
This was solved by an answer from user apneadiving which can be found here
Be aware that there are vicious cases in Rails development mode. In order to gain speed, the strict minimum is loaded. Then Rails looks for classes definitions when needed.
But this sometimes fails big time example, when you have say ::User already loaded, and then look for ::Admin::User. Rails would not look for it, it will think ::User does the trick.
This can be solved using require_dependency statements in your code.
Personally I think this is a bug, not a feature, but...so it goes. It solves the problem.
First of all, this code is superfluous:
if name.match(/\//)
controller_name = name.split('/').map(&:camelize).join('::')
else
controller_name = name.camelize
end
The only string would perfectly handle both cases:
controller_name = name.split('/').map(&:camelize).join('::')
Then, you probably want to handle namespaces properly:
n_k = controller_name.split('::')
klazz = n_k.last
namespace_object = if n_k.length == 1
Object
else
Kernel.const_get(n_k[0..-2].join('::'))
end
controller = namespace_object.const_get("#{klazz}Controller")
Hope that helps.

Accessing application variables from a Rails plugin

I have a variable that I need globally available throughout my app (so I've set #account in the applicationController).
However, a plugin that I have needs access to the same variable.
Note: This variable is distinct on each request.
What is the best way of creating this architecture?
Maybe something like this will work:
class Account
def self.current
#current
# or: Thread.current[:current_account]
end
def self.current=(a)
#current = a
# or: Thread.current[:current_account] = a # ..if you want to be thread-safe.
end
...
end
# your controller's before_filter:
def assign_account
...
Account.current = #account # But remember to set nil if none found!
end
# Any code in your app (view, model, almost anything):
<%= Account.current.name if Account.current %>
Setting #account in your app controller doesn't make it globally available throughout the app - models can't access it for example. Any instance var set in the controller will be available only in the controller or views. If the plugins have controller and view code then this code should be able to access the variable in the normal way, as long as the variable is set before the plugin controller code runs for example.
If you provide more details about what you want to do (ie where/how you want to access #account) then someone may be able to suggest a good approach.

Resources