Passing variables to config/environments/demo.rb from the Rails app - ruby-on-rails

I have been struggling with a problem for the past days in a Ruby on Rails App I'm currently working on. I have different countries and for each country we use different Amazon S3 buckets. Amazon S3 key credentials are stored as constants in config/environments/environment_name.rb(ex:demo.rb) There is no way for me to determine which country we are operating from the config file. I can determine which country we are operating from the controllers,models,views,etc but not from the config file. Is there a Ruby meta programming or some other kind of magic that I'm not aware of so that I want to say if we are working on UK as a country in the app, use UK's bucket credentials or Germany as a country, use Germany's bucket credentials? I can't think of a way to pass parameters to environment files from the app itself. Thank you very much in advance for all your helps.

Rather than actually pass the configuration details to whichever S3 client you're using at launch, you should probably select the relevant credentials for each request. Your config file can define them all in a hash like so:
# config/s3.rb
S3_BUCKETS => {
:us => 'our-files-us',
:gb => 'our-files-gb',
:tz => 'special-case'
}
Then you can select the credentials on request like so (in maybe your AppController):
bucket_name = S3_BUCKETS[I18n.locale]
# pass this info to your S3 client
Make sense?

Write a little middleware if you want to keep the knowledge of the per-country configuration out of the main application.
A middleware is extremely simple. A do-nothing middleware looks like this:
class DoesNothing
def initialize(app, *args)
#app = app
end
def call(env)
#app.call(env)
end
end
Rack powers applications through chaining a series of middlewares together... each one is given a reference to #app, which is the next link in the chain, and it must invoke #call on that application. The one at the end of the chain runs the app.
So in your case, you can do some additional configuration in here.
class PerCountryConfiguration
def initialize(app)
#app = app
end
def call(env)
case env["COUNTRY"]
when "AU"
Rails.application.config.s3_buckets = { ... }
when "US"
Rails.application.config.s3_buckets = { ... }
... etc
end
#app.call(env)
end
end
There are several ways to use the middleware, but since it depends on access to the Rails environment, you'll want to do it from inside Rails. Put it in your application.rb:
config.middleware.use PerCountryConfiguration
If you want to pass additional arguments to the constructor of your middleware, just list them after the class name:
config.middleware.use PerCountryConfiguration, :some_argument
You can also mount the middleware from inside of ApplicationController, which means all of the initializers and everything will have already been executed, so it may be too far along the chain.

Related

Connecting to Google Calendar's API through Rails

I'm creating a really simple Rails application with one specific purpose: add a Calendar event to a Google Calendar account.
I'm following the Ruby Guide from the Google Calendar API.
I was able to run the provided testing code (Ruby only, no framework) but I'm having a hard time accessing the credentials from a Rails project and I'm not sure the proper ("idiomatic"?) way to do it and organize the project.
Part of the process is using OAuth 2.0 since this goal requires access to Google User's private data (both read and write), I'm following the Using OAuth 2.0 for Web Services instructions.
Right now I have several different questions regarding best practices and/or the proper way to organize code:
Google provides a client_secret.json that have the credentials to access the application. Where should I keep it? Should I keep it in a .env file in the Development environment and (in my case) in Heroku's ENV VARS in the Production Environment?
I tried keeping the client_secret.json file in the project's root folder (same path as the Gemfile), added it to the .gitignore but I wasn't able to require "#{Rails.root}/client_secret.json":
/Users/jsoifer/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/activesupport-5.0.1/lib/active_support/dependencies.rb:293:in `require': No such file to load -- /Users/jsoifer/Developer/Tiny-Things/tiny-booking/client_secret.json (LoadError)
I created a services/ folder to put the Google Calendar related code inside, I wasn't sure if I should put is in a controller though. How should I organize this?
Important consideration:
I'm not using any other method of Authentication/Authorization such as Devise or others and I'm not planning to do so right now. I just want to get Google's Authorization token and create a Calendar event.
Github Project Link
I was able to figure this out and will post the answer to each of the questions below:
One of the possible locations for the client_secret.json file is config/client_secret.json.
When shipping to Production in Heroku, use ENV Vars.
Require is not the appropriate way to import the credentials in the json file.
Use Google::APIClient::ClientSecrets.load( File.join( Rails.root, 'config', 'client_secret.json' ) ) (assuming the file is indeed in config/.
There are several different alternatives as on how to organize code. I ended up creating a services folder and a google_calendar.rb class holding the authorization logic.
Here's the code:
app/services/google_calendar.rb
require 'google/api_client/client_secrets'
require 'google/apis/calendar_v3'
class GoogleCalendar
# Attributes Accessors (attr_writer + attr_reader)
attr_accessor :auth_client, :auth_uri, :code
def initialize
# ENV: Development
# Google's API Credentials are in ~/config/client_secret.json
client_secrets = Google::APIClient::ClientSecrets.load( File.join( Rails.root, 'config', 'client_secret.json' ) )
#auth_client = client_secrets.to_authorization
# Specify privileges and callback URL
#auth_client.update!(
:scope => 'https://www.googleapis.com/auth/calendar',
:redirect_uri => 'http://localhost:3000/oauth2callback'
)
# Build up the Redirecting URL
#auth_uri = #auth_client.authorization_uri.to_s
end
end
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
# Starting action in config/routes.rb
def welcome
# Redirect to Google Authorization Page
redirect_to GoogleCalendar.new.auth_uri
end
def token
# Get a auth_client object from Google API
#google_api = GoogleCalendar.new
#google_api.auth_client.code = params[:code] if params[:code]
response = #google_api.auth_client.fetch_access_token!
session[:access_token] = response['access_token']
# Whichever Controller/Action needed to handle what comes next
redirect_to new_event_path()
end
end

Pull some configuration variables from the DB to set dynamically in Rails environment?

Is it possible to set some configuration variables in production.rb dynamically using values from the database?
I'm building a multi-tenant app and each tenant has some environment specific information that needs to be set dynamically.
For example:
// production.rb
config.action_mailer.default_url_options = { :host => current_tenant.domain }
The current_tenant is a helper method is defined in ApplicationHelper so can't be accessed in production.rb
Here is current_tenant:
// application_helper.rb
def current_tenant
#current_tenant ||= Tenant.find_by(domain: Apartment::Tenant.current)
end
If not, is it possible to dynamically create secrets.yml using DB data? Since then I would be able to use ENV in production.rb
Maybe you can try this instead:
Create a helper method for your emails:
def build_tenant_url(route_name, *args)
opts = args.extract_options!
opts.merge!(host: #tenant ? #tenant.domain : your_domain)
#Here you can merge other options if you need to pass a token etc
Rails.application.routes_url_helpers.send(:"#{route_name}_url", *args, opts)
end
In your mailer, define the #tenant
Then in your email view use it
= link_to 'Edit', build_tenant_url(:edit_tenant, #tenant)
This setup runs when the app starts up, so if you want an option to be driven by some variable, that variable will need to be available to the Ruby code that runs at startup. There's various ways this could be done
the script (or whatever) which starts your rails server passes through a parameter
a value is written into a text file by some other process and read out again by the startup script
same again but with an environment variable
For example, if you were giving each tenant (group of users) their own subdomain on your app, then the subdomain name could be used as a key for the startup code to read the database and pull out some details for that tenant. This subdomain would then be passed through to the app when you spin it up.
EDIT - having said all this, i'm not sure if it's necessary for you to change the startup config at all. Can't you just look up the appropriate domain name whenever you send an email, rather than changing the default set in the config?

Where is a good place to initialize an API?

I wanted to use this api: https://github.com/coinbase/coinbase-ruby and the first step is to initialize the API, like this:
coinbase = Coinbase::Client.new(ENV['COINBASE_API_KEY'], ENV['COINBASE_API_SECRET'])
I was wondering what the best place to put this code is, and how would I access it if I put it "there"? I want this variable (coinbase) to be accessible ANYWHERE in the application.
Thanks!
The answer to this question really depends on your use case and your approach. My geral recommendation, however, is to create a Service Object (in the DDD sense) (see the section named "Domain Objects Should Not Know Anything About Infrastructure Underneath" in that link), that handles all communication with the Coinbase API. And then, within this service object, you can simply initialize the Coinbase::Client object once for however many times you call into it. Here's an example:
# app/services/coinbase_service.rb
class CoinbaseService
cattr_reader :coinbase_client, instance_accessor: false do
Coinbase::Client.new(ENV['COINBASE_API_KEY'], ENV['COINBASE_API_SECRET'])
end
def self.do_something
coinbase_client.do_something_in_their_api
end
def self.do_something_else
coinbase_client.do_something_else_in_their_api
end
end
So then you might do, e.g.:
# From MyController#action_1
if CoinbaseService.do_something
# ...
else
# ...
end
Or:
# From MyModel
def do_something
CoinbaseService.do_something_else
end
To get the service object working, you may need to add app/services to your autoload paths in application.rb file. I normally just add this:
# config/application.rb
config.autoload_paths += %W(#{config.root}/app)
I find this Service Object approach to be very beneficial organizationally, more efficient (only 1 invocation of the new Coinbase client needed), easier to test (easy to mock-out calls to Coinbase::Client), and simply joyful :).
One way to go about having a global variable can be done as similar as initializing redis in a Rails application by creating an initializer in config/initializers/coinbase.rb with:
$coinbase = Coinbase::Client.new(ENV['COINBASE_API_KEY'], ENV['COINBASE_API_SECRET'])
Now, you can access $coinbase anywhere in the application!
In the file config/initializers/coinbase.rb
Rails.application.config.after_initialize do
CoinbaseClient = Coinbase::Client.new(
Rails.application.credentials.coinbase[:api_key],
Rails.application.credentials.coinbase[:api_secret])
end
In place of the encrypted credentials, you could also use environment variables: ENV['COINBASE_API_KEY'], ENV['COINBASE_API_SECRET']
The above will make the constant CoinbaseClient available everywhere in your app. It will also ensure all your gems are loaded before the client is initialized.
Note: I am using Rails 6.1.4.4, and Ruby 2.7.5

HTTP request uuid or request start time in Ruby applications

I need to get a request uuid or time when server get a request. It's easy in Rails, but I'm working on a gem and I would like it to be more generic. So I would like it to work also with Sinatra and every other Ruby application which works in a http server.
This is another problem, it's a gem. I can't put Time.now at the beggining of my application controller. I need it to be generic, so it should work with different frameworks.
What would you propose?
You can implement a Rack middleware which you can use independently from your actual application framework (as long as it used rack, which is true for at least Rails, Sinatra, Padriono and most other Ruby web frameworks).
Rails already includes a middleware for adding a unique ID to a request of required in ActionDispatch::RequestId. Another alternative could be the rack-request-id gem.
A minimal versions of this midleware could look like this:
class RequestIdMiddleware
def initialize(app)
#app = app
end
def call(env)
env['request_id'] = env['HTTP_X_REQUEST_ID'] || SecureRandom.uuid
env['request_started_at'] = Time.now
#app.call(env)
end
end
You can then use this middleware in your config.ru or by adding this to your application.rb in Rails:
config.middleware.use RequestIdMiddleware

How to stub in Rails development environment?

I'm looking for a reliable way to dynamically stub certain methods in my development environment. One example use case is when I need to do development that normally requires access to the Facebook Graph APIs but I don't have Internet access. I'd like to be able to stub the calls to fb_graph methods so it looks as if I'm authenticated and have profile data. Ideally I could turn the stubs on or off with a minor config change.
Any ideas? I'm assuming something like mocha can handle this?
You can use the VCR gem which will record the results of an initial HTTP request into a yml file and then use the contents of that yml file on subsequent http requests. It can then be configured to ignore the VCR logic and always make HTTP requests, if so desired:
https://www.relishapp.com/myronmarston/vcr
Mocha can certainly do it. But it feels a bit strange.
You could also do something like dependency injection.
For instance:
class User < AR::Base
def find_friends
Facebook.find_friends(facebook_id)
end
end
class Facebook
def self.find_friends(id)
# connect to Facebook here
end
end
class FakeFacebook
def self.find_friends(id)
# a fake implementation here
end
end
And inside an initializer:
if Rails.env.development?
User::Facebook = FakeFacebook
end

Resources