📜 ⬆️ ⬇️

Configuring Ruby Module

I think you are familiar with the configure method that many gems provide for configuration. For example, carrierwave configuration:

CarrierWave.configure do |config| config.storage = :file config.enable_processing = false end 

How to implement it in your module?


Fast and dirty


Let's start with the falling tests.
')
 # configure.rb require 'minitest/autorun' class ConfigurationTest < MiniTest::Test def test_configure_block MyModule.configure do |config| config.name = "TestName" config.per_page = 25 end assert_equal "TestName", MyModule.config.name assert_equal 25, MyModule.config.per_page assert_equal "TestName", MyModule.config[:name] assert_equal 25, MyModule.config[:per_page] end end 

 ➜ Projects ruby configure.rb Run options: --seed 25758 # Running: E Finished in 0.001166s, 857.6329 runs/s, 0.0000 assertions/s. 1) Error: ConfigurationTest#test_configure_block: NameError: uninitialized constant ConfigurationTest::MyModule configure.rb:5:in `test_configure_block' 1 runs, 0 assertions, 0 failures, 1 errors, 0 skips 

Now, when we have falling tests we will start implementation of functionality. First, let's declare the module containing the configure method.

 module MyModule def self.configure end end 

We need a place to store our configuration. I think the module variable is good for this.

 module MyModule def self.configure @config ||= {} end def self.config @config end end 

There is a problem here. We will not be able to store the configuration in the hash. For now, I'll replace the hash with OpenStruct, which corresponds to the functionality that we are going to end up with. After that, I can already call the block inside the method and pass it the storage as an argument.

 require 'minitest/autorun' require 'ostruct' module MyModule def self.configure @config ||= OpenStruct.new yield(@config) if block_given? @config end def self.config @config || configure end end 

The necessary functionality is ready. Tests pass.

 ➜ Projects ruby configure.rb Run options: --seed 8967 # Running: . Finished in 0.001607s, 622.2775 runs/s, 2489.1101 assertions/s. 1 runs, 4 assertions, 0 failures, 0 errors, 0 skips 


Refactoring


It's time to refactor this solution. Two problems are immediately visible:


Add tests to be sure that when calling a non-existing configuration method, we will get an exception.

 def test_set_not_exists_attribute assert_raises NoMethodError do MyModule.configure do |config| config.unknown_attribute = "TestName" end end end def test_get_not_exists_attribute assert_raises NoMethodError do MyModule.config.unknown_attribute end end 

We have two ways to fix this. The first is to use Struct with a white list of available configuration methods.

 module MyModule CONFIG_ATTRIBUTES = %i(name per_page) def self.configure @config ||= Struct.new(*CONFIG_ATTRIBUTES).new yield(@config) if block_given? @config end def self.config @config || configure end end 

Everything looks great. Tests pass, the code is simple and readable. But I forgot one important detail . Default configuration values. For them, you need to add another test.

 def test_default_values MyModule.configure do |config| config.name = "TestName" end assert_equal 10, MyModule.config.per_page end 

To avoid overwriting the configuration values ​​in different tests , add a reset to the previous configuration before running each test . I will add a reset method directly in the test class, because it is needed only for test needs and there is no need to make it part of the public API.

 module ::MyModule def self.reset @config = nil end end def setup MyModule.reset end 

Let's return to solving the problem with default values. The simplest solution would be:

 self.config ||= begin config = Struct.new(*CONFIG_ATTRIBUTES).new config.per_page = 10 config end 

Hmm, the code starts to smack . Default values ​​can be much more complicated. This code will be difficult to maintain. I think we can do better. Let's replace Struct with a class . In a class, we can set default values ​​directly in the initializer. Such code will be easy to read and expand.

 module MyModule class Configuration attr_accessor :name, :per_page def initialize @per_page = 10 end def [](value) self.public_send(value) end end def self.configure @config ||= Configuration.new yield(@config) if block_given? @config end def self.config @config || configure end end 

I like this solution. It is still very simple and readable . It is also quite flexible . We can set complex defaults and, if necessary, put them into separate methods. We also have two ways to get configuration values: using the method and via subscript.

This is all I wanted to share today. Sources are available here: goo.gl/feCwCC

UPD:

northbear proposed an alternative in which Struct with an initializer is used instead of a class.

 module Sample DefaultConfig = Struct.new(:a, :b) do def initialize self.a = 10 self.b = 'test' end end def self.configure @config = DefaultConfig.new yield(@config) if block_given? @config end def self.config @config || configure end end 

In this case, there is no need to use public_send to define a subscript, which makes the code even easier. Thank you, northbear !

Source: https://habr.com/ru/post/242729/


All Articles