HanamiMastery

Configure anything with dry-configurable

Episode #5

by Sebastian Wilgosz

Picture of the author

When you write ruby gems or even reusable components, there is often a need for configuring your projects to behave differently in different contexts.

For example, in our EventStoreClient gem, we've used to connect our apps to the external event store. We needed to pass the authorization data, as well as the URL of the store where it was hosted upon.

EventStoreClient.configure do |config|
  config.eventstore_url = ENV['EVENTSTORE_URL']
  config.eventstore_user = ENV['EVENTSTORE_USER']
  config.eventstore_password = ENV['EVENTSTORE_PASSWORD']
  config.verify_ssl = false # remove this line if your server does have the host verified
end

In the aws-sdk gem from another hand, you need to pass similar configuration options:

require 'aws-sdk'
require 'json'

creds = JSON.load(File.read('secrets.json'))
Aws.config[:credentials] = Aws::Credentials.new(
  creds['AccessKeyId'],
  creds['SecretAccessKey']
)

And, if you use Rails, this kind of configuration may be super familiar to you.

module Api
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 6.0
    config.api_only = true
  end
end

In fact, it's so common behavior, where a gem or component needs to be configured somehow, that it's surprising most of the projects come with the custom configuration implementations, not always thinking about being Thread-Safe, Type-safe, or even test the configuration engine properly!

I guess you can imagine what your project may do in such situation, can't you?

![[caitlin-wynne-4D953R0aRUo-unsplash(1)(1).jpg]]

But let's stay positive. You can avoid certain doom and I want to tell you how.

You can configure anyting, from initial stylings for your application, database connections, whatever you want.

In today's episode of Hanami Mastery, I want to show you the Dry-Configurable gem, which may be an amazing improvement in tn allowing you to add thread-safe configuration behavior to your classesita Shilnikov and Piotr Solnica, is one of such micro-libraries, that are close to perfection and can be injected almost everywhere*.

Usage

dry-configurable is super simple to use, but because of its narrowed responsibility, at the same time it's extremely stable and flexible to be applied in different contexts.

Just extend the mixin and use the setting macro to add configuration options:

You can use it either on ruby module on a class or an instance.

Using dry-configurable on module

On the module level, you need to extend the Dry::Configurable module and you can safely use all features it provides. Here I set the api_url to https://hanamimastery.com

require 'dry-configurable'

module App
  extend Dry::Configurable

  setting :api_url
end

App.config.api_url = 'https://hanamimastery.com'
App.config.api_url # => 'https://hanamimastery.com'

Using dry-configurable on class

On the class level, nothing changes, you can use it exactly in the same way.

require 'dry-configurable'

class App
  extend Dry::Configurable

  setting :api_url
end

App.config.api_url = 'https://example2.com'
App.config.api_url

Using dry-configurable on object

If you want to use it on the instance of the class, however, the only difference is that you need to include the Dry::Configurable module instead of extending it.

class App
  include Dry::Configurable

  setting :api_url
end

app1 = App.new
app1.config.api_url = 'https://example3.com'
app1.config.api_url
# => 'https://example3.com'
app2 = App.new
app2.config.api_url
# => nil

Simple? Simple.

Keep in mind, that this way you may have, for example,

  • multiple clients connecting to an external API, each client being completely independent!
  • multiple database connections

It's super useful, and I include Dry::Configurable in almost every gem or service I create, because of its flexibility and small size.

Features

Now let's go through some of the features dry_configurable allows you to use.

Basic setting

First of all, you can do a basic setting, which by default is set to nil. Then after setting the attribute to a specific value, this value will be returned.

require 'dry-configurable'

class App
  extend Dry::Configurable

  # Defaults to nil if no default value is given
  setting :adapter

  # Defaults to false
  setting :enabled
end

App.config.adapter # => nil
App.config.adapter = :http
App.config.adapter # => :http

You can set the default values for your settings, passing the default value as a second argument.

Quick note about the deprecation warning

NOTE: The syntax of configuring classes changed a little bit. If you see the deprecation warning; [dry-configurable] passing a constructor as a block is deprecated and will be removed in the next major version Provide a constructor: keyword argument instead You can switch to the new syntax

require 'dry-configurable'

class App
  extend Dry::Configurable

  # Defaults to nil if no default value is given
  setting :adapter

  # Defaults to false

  # Old, deprecated way to set defaults
  setting :enabled, false

  # Current approach
  setting :enabled, default: false
end

App.config.enabled # => false
App.config.enabled = true
App.config.enabled # => true

If you'd like to access the setting directly on the class or instance the attribute is defined on, set the reader option to true. Then you'll be able to access that setting directly, while it'll also be available via the configuration object.

require 'dry-configurable'

class App
  extend Dry::Configurable

  # Defaults to nil if no default value is given
  setting :adapter

  # Defaults to false
  setting :enabled, default: false, reader: true
end

App.enabled # => false
App.config.enabled # => false

App.config.enabled = true
App.config.enabled # => true
App.enabled # => true

Nested settings

With dry-configurable you can add nested settings, with all features supported in every deep level. You can nest as many levels as you want, it'll all work the same!

require 'dry-configurable'

class App
  extend Dry::Configurable

  # Pass a block for nested configuration (works to any depth)
  # Passing the reader option as true will create attr_reader method for the class
  # Passing the reader attributes works with nested configuration
  setting :repository, reader: true do
    # Can pass a default value
    setting :type, default: :local
    setting :encryption do
      setting :cipher, default: 'aes-256-cbc'
    end
  end
end

You can then use those settings by chaining the methods on the config object. And yes, the chaining works the same, if I'll set a reader option to true.

App.config.repository.type # => ":local"

App.config.repository.type = :remote
App.config.repository.type # => ":remote"

App.config.repository.enryption.cipher # =>  "aes-256-cbc"

App.repository.type # => :remote

Pre-Processed values

Finally, you can easily do the pre-processing of default, and passed setting values. I find it useful for example when I need to work with URLs. It's generally bad practice to work with plain ruby strings when it comes to URL definitions, so I often wrap such in a URI object.

require 'dry-configurable'

class App
  extend Dry::Configurable

  # Pre-process values
  setting(:url, default: 'https://hanamimastery.com') { |value| URI(value) }
end

App.config.url # => #<URI::HTTPS https://hanamimastery.com>

App.config.url = "https://subscribe.me"

App.config.url # => #<URI::HTTPS https://subscribe.me>

This makes sure I'll never have an invalid URL in my application. You can also do advanced Type checking, schema definitions, and so on, to be sure your app will never allow invalid configuration to be set.

The configure block

So far, in the examples above, I mostly set the values to our settings using the inline attribute assignment.

However, if your component has a lot of settings, or maybe the name is too long, you can save some of the keystrokes by using the configure method. It sends the config instance to the block you pass as an argument and then yields whatever is inside.

I find it very useful as it's convenient for me to configure my ruby classes this way. I've shown how it looks at the very beginning of the article, but here is another example, with some comments.

client = Client.configure do |config|
  config.adapter = :http
  config.url = 'https://hanamimastery.com'
end

client.config.adapter # => ":http"
client.config.repository.type # => :local

As you can see, the configure method returns the object it's called upon, so we can assign instances of the classes to variables for later usage and configure in the same operation.

While this is useful, it's only a useful syntax. I am surprised, however, that the official docs don't cover it! It's an opportunity to write a little contribution!

Using dry-configurable in the gem

Finally, I'll quickly show the complete use case and explain what problem dry-configurable actually solves.

It's basically an extra layer on your ENV variables, but it gives you much more power than relying on the ENV variables, because: you don't risk ENV var naming clash, you can do type validation for your setting, you can do values preprocessing.

#your gem source code
class MyGem
  extend Dry::Configurable
  setting :api_url do |value|
    URI(value)
  end
end

# in your for example rails app
# config/initializers/my_gem.rb

require 'my_gem'

MyGem.config.api_url = ENV['VAR_NOT_TIED_TO_GEM_IMPLEMENTATION']

This code already will fail in case of invalid configuration, like passing nil or invalid URL, which is a much more error-prone implementation.

Summary

dry-configurable is so simple, but at the same so useful, it's hard to believe it's not included in all ruby applications. Similar concepts are applied to dry-inflector gem - skinny, simple, resuable and this is why I love dry-rb for.

It's worth mentioning, that Hanami-RB 2.0 uses the dry-configurable for the framework, view, and controller settings. Give it a try in your projects and you'll not regret it.

I hope you've enjoyed this episode, and if you want to see more content in this fashion, subscribe to my Newsletter and follow me on twitter!

Special Thanks

I'd like to thank Bohdan V. for supporting this channel, and all other people encouraging me to continue with Hanami Mastery project - It really matters a lot!

Any support allows me to spend more time on creating this content, promoting great open source projects, so Thank you!.

Do you know great Ruby gems?

Add your suggestion to our discussion panel!

I'll gladly cover them in the future episodes! Thank you!

Suggest topicTweet #suggestion

May also interest you...

#12 Authorization with JWT in Hanami applications!
hanamiauthorizationdry-effects

Authorizating your API applications can be trivial, if you have proper tolls to do it. Here I showcase how authorization with JWT can be done effortless in any ruby application by using Hanami 2 as an example.

This episode is a special release, when Seb interviews Tim Riley and Luca Guidi from the Hanami Core Team, asking them questions collected from the community! This way we enjoy reaching our important milestone of releasing 50th Hanami Mastery episode!

Utility scripts in Ruby can be very powerful, but also very messy. In this episode I showcase dry-cli, to help you maintain advanced ruby CLI programs.

Validating data input is an extremely important problem to tackle in all applications. In Ruby ecosystem there is no better tool for that than dry-validation. Here is why!

Coffee buy button
Trusted & Supported by
AscendaLoyalty

1 / 1
Open Hanami Jobs

We use cookies to improve your experience on our site. To find out more, read our privacy policy.