How to use ActiveSupport::Configurable with Rails Engine - ruby-on-rails

I want to give my rails engine gem a proper configuration possibilities.
Something that looks like this in initializers/my_gem.rb (link to the current initializer):
MyGem.configure do |config|
config.awesome_var = true
# config.param_name = :page
end
So I've looked around for any clues in other gems and the best I cloud find was this kaminari/config.rb.
But it looks so hacky that I think there must be a better way.

The source file for ActiveSupport::Configurable got decent documentation:
https://github.com/rails/rails/blob/master/activesupport/lib/active_support/configurable.rb
I like to put the configuration into it's own class within the engine (like kaminari does):
class MyGem
def self.configuration
#configuration ||= Configuration.new
end
def self.configure
yield configuration
end
end
class MyGem::Configuration
include ActiveSupport::Configurable
config_accessor(:foo) { "use a block to set default value" }
config_accessor(:bar) # no default (nil)
end
Now I can configure the engine with this API:
MyGem.configure do |config|
config.bar = 'baz'
end
And access the configuration with
MyGem.configuration.bar

try this out
I hope this is simple and clear.
Example Code

Related

Why object_id changes after app initialization?

Rails 6.1.4.1
I'm setting values from an initializer file and when I request a page, I find the object_id of the configuration changed (and the values lost) unless I do require 'my_library' in the initializer file. I don't understand why?
app/lib/feature_flags.rb:
class FeatureFlags
class Configuration
include ActiveSupport::Configurable
config_accessor(:my_key) { false }
end
class << self
def configuration
#configuration ||= Configuration.new
end
def configure
yield configuration
end
def enabled?(feature)
puts "#{__FILE__} FeatureFlags.configuration.object_id = #{FeatureFlags.configuration.object_id}"
configuration[feature]
end
end
end
config/initializers/feature_flags.rb:
# require 'feature_flag' # If I uncomment this line, the problem is solved
puts "#{__FILE__} FeatureFlags.configuration.object_id = #{FeatureFlags.configuration.object_id}"
FeatureFlags.configure do |config|
config.my_key = true
end
Output:
1. Run the rails server:
config/initializers/feature_flags.rb FeatureFlags.configuration.object_id = 14720
2. Request some page:
app/lib/feature_flags.rb FeatureFlags.configuration.object_id = 22880
My questions are:
Why do I need to require 'feature_flags' in the initializer for the object_id not to change? I thought Zeitwerk was taking care of this.
Is that how I'm supposed to do (to do it right)?
Thanks for your help!
I found my answer here: https://edgeguides.rubyonrails.org/autoloading_and_reloading_constants.html#use-case-1-during-boot-load-reloadable-code
Why is it not working: because FeatureFlags is a reloadable class, it is replaced by a new object uppon request.
How am I supposed to do it right: wrap my initialization code in a to_prepare block:
Rails.application.config.to_prepare do
FeatureFlags.configure do |config|
config.my_key = true
end
end

How can I avoid a constant when I want a global object in rails?

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)

Rails module autoloading preserve state

I have set up a couple of modules that provide similar functionality, and I have another module that collects all of these, so that at runtime it is possible to determine what functionality is available.
This works fine, but whenever the code gets reloaded, Rails' autoloading functionality clobbers the instance variables on the collection module.
The code looks something like this:
module ServiceCollection
def self.available_services
#available_services ||= []
end
end
module ServiceProvider
extend ActiveSupport::Concern
included do |includer|
ServiceCollection.available_services.push(includer)
end
end
module MyService
include ServiceProvider
#some functionality here
end
Calling ServiceCollection.available_services will return the list of modules that include ServiceProvider, however on reload, the instance variable #available_services will be reset, and subsequent calls return an empty array.
Is there an easy way to get around this?
You can force the load of the modules putting a require call in your application.rb file.
Something like:
Dir["#{File.expand_path('../..', __FILE__)}/extras/*.rb"].each { |rb| require rb }
Thanks to #thaleshcv for the answer, but I decided to go a different route, partially inspired by his answer.
I changed the implementation of ServiceCollection to something like this:
module ServiceCollection
def self.available_services
Dir[Rails.root.join("app", "services", "*.rb")].each do |filename|
model_name = Pathname.new(filename.to_s).basename.to_s.chomp('.rb').camelcase
begin
Module.const_get(model_name)
rescue Exception => e
#Log something or whatever
end
end
#available_services ||= []
end
end
Calling Module.const_get triggers Rails' autoloading functionality. Assuming that I follow the module/class/filename naming convention, then this should work fine. Here's a gist that extracts this functionality into a module

Method visible everywhere in Rails

How can I make this method, which outputs a yellow line in the log file, accessible from everywhere (Models, Controllers, Views) in my Rails app?
def my_log(text, file = "", line = "")
text.to_s.chomp.gsub!(/%/, "%%")
Rails.logger.debug(sprintf("\033[32m#{file}#{line}\033[0m\033[1m\033[33m#{text}\033[0m"))
end
You could define it in Kernel (NOT recommended):
module Kernel
def my_log(..)
..
end
end
... if you really want it available anywhere.
Or, place something like this in lib/util.rb:
module Util
def self.my_log(..)
..
end
end
... and make sure to require 'util' in your config/application.rb and then you can call this anywhere:
Util.my_log(..)
why not create an initializer and write this method to the rails module?
# config/initializers.rb
module Rails
def self.log_with_colour(message, level = :debug)
text.to_s.chomp.gsub!(/%/, "%%")
logger.send(level, sprintf("\033[32m#{__FILE__}#{__LINE__}\033[0m\033[1m\033[33m#{message}\033[0m"))
end
end
in your code you can then call Rails.log_with_colour("hello") or Rails.log_with_colour("Hello again", :info)
I put stuff like this in config/initializers/app_methods.rb. They don't need to be scoped inside a class or module. Feels a bit hacky but i never had any problems.
Add it as an instance and class method in Object
class Object
def self.my_log(...)
...
end
def my_log(...)
Object.my_log(...)
end
end

How do you create a configurable Ruby on Rails plugin?

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.

Resources