Suppose I need to parse some configuration to instanciate some Service Singletons (that could be used with or without Rails).
A sample code example:
#services/my_service.rb
module MyService
#config = nil
def self.load_config(config)
#config = config
end
When using with Rail (or Capistrano, SInatra, etc.) I would use an initializer to boot up the service
#initializers/svc.rb
MyService.load_config(Rails.application.secrets.my_service.credentials)
But when used specifically with Rails, on every rails console restart!, this #config variable is cleared which is a problem...
Are there
after-reload! hooks that I could use to re-run the initializer ?
other types of variables that would be preserved during a restart!
that I could use here ?
You could define config method as:
def config
#config ||= Rails.application.secrets.my_service.credentials
end
And call this method instead of #config, so when the config variable is unset, it will be set again, otherwise it will return the value.
Seeing that people are still reading this, here is implementation I ended up with
# config/initializers/0_service_activation.rb
# Activation done at the end of file
module ServiceActivation
def self.with_reload
ActiveSupport::Reloader.to_prepare do
yield
end
end
module Slack
def self.service
::SlackConnector
end
def self.should_be_activated?
Utility.production? ||
Utility.staging? ||
(
Utility.development? &&
ENV['ENABLE_SLACK'] == 'true'
)
end
def self.activate
slack = service
slack.webhook = Rails.application.secrets.slack&.dig(:slack_webhooks)
Rails.application.secrets&.dig(:slack, :intercept_channel).try do |channel|
slack.intercept_channel = channel if channel.present?
end
slack.activate
slack
end
end
[
ServiceActivation::Intercom,
ServiceActivation::Slack,
ServiceActivation::Google
] .each do |activator|
ServiceActivation.with_reload do
activator.activate if activator.should_be_activated?
activator.service.status_report
end
end
I am not showing my connector class SlackConnector, but basically you can probably guess the interface from the way it is called. You need to set the webhook url, and do other stuff. The implementation is decoupled, so it's possible to use the same SlackConnector in Rails and in Capistrano for deployment, so it's basically in the lib/ folder
Related
I am want to add S3 file storage to my rails 5 application. Since I am using heroku I used their tutorial which says to just create a constant named S3_BUCKET in your config/initializers/aws.rb and you can use that constant everywhere.
The heroku code looks like this:
#config/initializers/aws.rb
S3_BUCKET = Aws::S3::Resource.new.bucket(ENV['S3_BUCKET'])
The problem with this is that I have to override this constant for the specs to work.
I have this alternative solution (which is sadly not working):
#lib/aws_helpers/s3.rb
module AWSHelpers
module S3
class << self
attr_accessor :configuration
def configure
self.configuration ||= Configuration.new
yield(configuration)
end
def bucket
#bucket ||= Aws::S3::Resource.new.bucket(configuration.s3_bucket)
end
end
class Configuration
attr_accessor :s3_bucket, :aws_access_key_id, :aws_secret_access_key_id
end
end
end
#config/initializers/aws.rb
AWSHelpers::S3.configure do |config|
config.s3_bucket = ENV['S3_BUCKET']
config.aws_access_key_id = ENV['AWS_ACCESS_KEY_ID']
config.aws_secret_access_key_id = ENV['AWS_SECRET_ACCESS_KEY']
end
What I want to be able to do in a controller is:
AWSHelpers::S3.bucket.object(a_key)
Unfortunately this fails and tells me that bucket method can't return anything because the configuration is nil.
What I know:
the aws initializer gets executed and when I add puts in all the methods I can see the correct output when my server starts via rails s
the controller knows about the service module or it would not even get to the bucket method
the code works when I dump the content of config/initializers/aws.rb into the controller
I really would like to know why the code above is not working. It seems to set up everything correctly and then when I want to use it in the controller suddenly things are as if i'd never called configure.
I am also open to other suggestions on how to do this. Just using the constant is not an option because it has to be changed for the specs to work.
This code might look strange, but it's actually exactly what you'd want in this situation. Keep in mind this situation is a bit irregular, it's for configuring a plugin that has external API dependencies and associated keys that must be populated before the rest of the code works.
It's ugly from an implementation perspective, but from a usability perspective it's nice. You can just do AWSHelpers::S3.configure do |config| as shown in the initializer. All that code is to make those semantics work properly.
This is a cheap trick, but it works.
in config/application.rb:
module YourApp
class Application < Rails::Application
def s3_bucket
#s3_bucket ||= begin
# your stuff
end
end
end
end
Rails.application.s3_bucket.object(a_key)
So far I always used "puts" to add custom logging infos to my code. But now it is kind of a pain. When I run rspec for exemple I'm not interested in all the verbose that I added with puts. So I installed the "logging and logging-rails gem" because its installation is real fast and satisfying.
It works well when I call logger from models and controller, but not when I'm using logger inside libraries. I get that error : NameError - undefined local variable or method `logger' for CustomLib:Class.
The easiest thing I succeeded is to call 'Rails.logger' instead of just 'logger'. But in that way in my logfile, the class referring to that line will be 'Rails' but I want 'CustomLib'. For models and controller the right classname is displayed without any intervention from myself.
# config/environnement/test.rb
# Set the logging destination(s)
config.log_to = %w[stdout]
config.log_level = :info
# Show the logging configuration on STDOUT
config.show_log_configuration = false
# lib/custom_lib.rb
class CustomLib
def initialize
Rails.logger.info 'foo'
end
end
When I will use or test my customlib class I'll get :
[2019-06-21T16:26:41] INFO Rails : foo
Instead I would like to see :
[2019-06-21T16:26:41] INFO CustomLib : foo
I'm a bit lost in all that log management in rails, I have no idea what to try next to reach that goal...
Edit
When I put a byebug just before the "logger.info 'foo' " line and enter into it via 'step', I got two different results if whether I'm in a model/controller or a custom lib.
# In custom lib, step enters this file "gems/logging-2.2.2/lib/logging/logger.rb"
# And Rails.logger returns an object like this one beloow
Logging::Logger:0x000055a2182f8f40
#name="Rails",
#parent=#<Logging::RootLogger:0x000055a2182e7ee8
#name="root",
#level=1>,
# In model/controller, step enters this file "gems/logging-rails-0.6.0/lib/logging/rails/mixin.rb"
# And Rails.logger returns an object like this one beloow
Logging::Logger:0x0000557aed75d7b8
#name="Controller",
#parent=#<Logging::RootLogger:0x0000557aedfcf630
#name="root",
#level=0>,
At the end I found a better way, I just need to include Logging.globally on top of the module where I want that behavior:
# lib/custom_lib.rb
class CustomLib
include Logging.globally
def initialize
logger.info 'foo'
end
end
The Rails in the log line is the progname of the logger. You could create a new logger that sets the progname to 'CustomLib'with (say) Rails.logger.clone.tap {|l| l.progname = 'CustomLib' }, but that isn't really the purpose of progname, which is to specify the name of the program, not the name of a class.
You could instead include the class name in the log line:
Rails.logger.info "[#{self.class}] - some message"
Or, for a bit more effort, you could define your own formatter. Here's one with a convenience method for wrapping an existing logger:
class ClassNameFormatter
def self.wrap_logger(klass, logger)
logger.clone.tap { |l| l.formatter = new(klass, logger.formatter) }
end
def initialize(klass, formatter=nil)
#klass = klass
#formatter = formatter ||= Logger::Formatter.new
end
def call(severity, timestamp, progname, msg)
#formatter.call severity, timestamp, progname, "[#{#klass.name}] - #{msg}"
end
end
To use it:
class CustomLib
def initialize
#logger = ClassNameFormatter.wrap_logger self.class, Rails.logger
end
def call
#logger.info 'test log please ignore'
end
end
I'm having some trouble understanding how to clone/duplicate a config object that uses ActiveSupport::Configurable. I'm using the ActiveSupport::Configurable module to create a simple configuration object. In some automated tests, I want to set aside the current configuration, and create a new config object with different value. I have a method called remember_config which just does
module App
class Config
include ActiveSupport::Configurable
def self.remember_config
#previous_config = #config
#config = #config.clone
end
end
end
The trouble occurs when I use it.
App.config.foo = 1
App.config.foo # Returns 1
App.previous_config # Returns nil
App.remember_config
App.config.foo # Still returns 1
App.previous_config.foo # eturns 1
App.config.foo = 2
App.config.foo # Returns 2
App.config.previous_config.foo # Also returns 2, but I'd like it to return 1!
Why does the last line return 2 instead of 1? The #previous_config and config objects have different object_ids. Maybe it's not a clone problem. Maybe it's an issue with the way the ActiveSupport::Configurable magic works.
Try using dup function rather then the clone function.
module App
class Config
include ActiveSupport::Configurable
def self.remember_config
#previous_config = #config
#config = #config.dup
end
end
end
But you can not use class as singleton class.
I have read this question in some length, particularly this answer. This may be the same, too. I'm of the opinion that they are for an older version of rack+rails than I am using now.
I have a rack middleware:
config.middleware.insert_before(Rack::Runtime, Rack::Rewrite) do
r301 %r{^/reviews/company/(\d+)}, lambda { |match, _rack_env|
company = Company.find_by_id(match[1])
case company.reviews.count
when 0
"/company-reviews"
when 1..3
"/#{company.slug}/reviews/"
# set no_index = true
else
"/#{company.slug}/reviews/"
# set no_index = false
end
}
end
Within those non-zero clauses, I would like to set a no_index variable to be available in the controller.
module ApplicationHelper
def application_meta_tags
#application_meta_tags.merge(
'no-index' => no_index_from_rack
)
end
end
Inside of the lambda in rack, I can do
request = Rack::Request.new(env)
request.session['no-index']=true
but it doesn't not appear in the controller scope.
request.session.keys
# [
# [0] "session_id",
# [1] "_csrf_token"
# ]
Since similar-looking answers have not worked, I wonder is this due to
I didn't implement them correctly
They were done inside the lambda scope
something else...
I am open to altogether-different strategies to pass data between rack and rails.
Update
I am currently using 'ENV' and/or Rails.configuration but this is not session-based, and I must un-set the variable after every use. Even then, I suspect that a race condition may nip me.
Is this a place I can set headers that will be later available to Rails? I'm trying to understand what is the right concept for passing data between these apps / contexts.
You definitely should not use ENV or Rails.configuration because they are global variables, and as you know global variables are evil. And as you said you will have race conditions.
If there is no reason to store the no_index boolean in the session, you should directly use the env variable :
Your middleware :
class Middleware
def initialize(app)
#app = app
end
def call(env)
env['app.no_index'] = true
#app.call(env)
end
end
Your controller/view :
class Controller
def new
env['app.no_index'] # is true
end
end
i am trying to create my first rails plugin, and i want it to be configurable, that is to say, i want to be able to set a variable in the environment.rb file or something.
UPDATE: i'm trying to do something like what's done here: http://soakedandsoaped.com/articles/read/exception-notifier-ruby-on-rails-plugin. i have tried mimicking their code, but i can't get it working.
i have the plugin working with the value hard-coded, but everything i have tried so far for making it configurable hasn't worked.
Here's some of the code:
#vendor/plugin/markup/lib/markup_helper.rb
module MarkupHelper
def stylesheet_cache_link_tag(*sources)
cache = assests_cache_dir ? assests_cache_dir : ""
options = sources.extract_options!.stringify_keys
cached_name = options.delete("cached_name")
stylesheet_link_tag(sources, :cache=> File.join(cache, cached_name))
end
def javascript_cache_include_tag(*sources)
cache = assests_cache_dir ? assests_cache_dir : ""
options = sources.extract_options!.stringify_keys
cached_name = options.delete("cached_name")
javascript_include_tag(sources, :cache=> File.join(cache, cached_name))
end
end
#something like the following in config/environment.rb or probably config/environments/production.rb
MarkupConfig.assests_cache_dir = "cache"
i want assests_cache_dir to default to "cache" but be able to set in an environment config file. i have googled a long time on this, and can't find anything discussing this. How can i accomplish this?
module MarkupHelper
mattr_accessor :assets_cache_dir
self.assets_cache_dir = "cache"
def assets_cache_dir
MarkupHelper.assets_cache_dir
end
end
Then in environment.rb (or development.rb/test.rb/production.rb if you want different values for each environment):
MarkupHelper.assets_cache_dir = "my-value"
Although the approach used by tomafro is quite easy to use, another approach is to use a database.yml-style configuration file that can be split according to environments:
module MyPlugin
class Configuration
# == Constants ==========================================================
CONFIG_FILES = [
"#{RAILS_ROOT}/config/myplugin.yml",
"#{RAILS_ROOT}/config/myplugin.yaml"
].freeze
DEFAULT_CONFIGURATION = {
:url => DEFAULT_HOSTNAME
}.freeze
# == Class Methods ======================================================
# :nodoc:
def self.config_file_found
CONFIG_FILES.find do |path|
File.exist?(path)
end
end
# Returns the default path to the configuration file
def self.default_path
config_file_found or CONFIG_FILES.first
end
# == Instance Methods ===================================================
# Creates a new MyPlugin::Configuration instance by reading from the
# configuration file.
# +env+ The Rails environment to load
def initialize(env)
config_file = self.class.config_file_found
#env_config = DEFAULT_CONFIGURATION
if (#config = (config_file and YAML.load(File.open(config_file))))
[ #config['defaults'], #config[env] ].each do |options|
if (options)
#env_config = #env_config.merge(options.symbolize_keys)
end
end
end
end
# Will return +true+ if a configuration file was found and loaded, or
# +false+ otherwise.
def exists?
#env_config != DEFAULT_CONFIGURATION
end
# Returns a particular configuration option.
def [](key)
#env_config[key.to_sym]
end
end
def self.config
#config ||= Configuration.new(Rails.env)
end
end
You would use this as:
settting = MyPlugin.config[:param_name]
You can also write utility methods to fetch particular values, or use OpenStruct instead of a configuration Hash. This is posted merely as an example of another design pattern.