0exp / qonfig

Powerful configuration Ruby-framework with a support for many commonly used config formats with a multi-functional API, developer-friendly DSL and object-oriented behavior.
MIT License
23 stars 8 forks source link
config configurable json-config multi-config ruby-config ruby-configurable ruby-settings settings toml-config yaml-config

Qonfig · Gem Version Coverage Status

Powerful configuration Ruby-framework with a support for many commonly used config formats with a multi-functional API, developer-friendly DSL and object-oriented behavior.

# in the past...:

Config. Defined as a class. Used as an instance. Support for inheritance and composition.
Lazy instantiation. Thread-safe. Command-style DSL. Validation layer. **Dot-notation**)
And pretty-print :) Support for **YAML**, **TOML**, **JSON**, **\_\_END\_\_**, **ENV**.
Extremely simple to define. Extremely simple to use. That's all? **Not** :)

Installation

gem 'qonfig'
$ bundle install
# --- or ---
$ gem install 'qonfig'
require 'qonfig'

Usage

Definition


Definition and Access

# --- definition ---
class Config < Qonfig::DataSet
  # nil by default
  setting :project_id

  # nested setting
  setting :vendor_api do
    setting :host, 'vendor.service.com'
  end

  setting :enable_graphql, false

  # nested setting reopening
  setting :vendor_api do
    setting :user, 'simple_user'
  end

  # re-definition of existing setting (drop the old - make the new)
  re_setting :vendor_api do
    setting :domain, 'api.service.com'
    setting :login, 'test_user'
  end

  # deep nesting
  setting :credentials do
    setting :user do
      setting :login, 'D@iVeR'
      setting :password, 'test123'
    end
  end
end

config = Config.new # your configuration object instance

access via method

# get option value via method
config.settings.project_id # => nil
config.settings.vendor_api.domain # => 'app.service.com'
config.settings.vendor_api.login # => 'test_user'
config.settings.enable_graphql # => false

access via index-method []

# get option value via index (with indifferent (string / symbol / mixed) access)
config.settings[:project_id] # => nil
config.settings[:vendor_api][:domain] # => 'app.service.com'
config.settings[:vendor_api][:login] # => 'test_user'
config.settings[:enable_graphql] # => false

# get option value via index (with indifferent (string / symbol / mixed) access)
config.settings['project_id'] # => nil
config.settings['vendor_api']['domain'] # => 'app.service.com'
config.settings['vendor_api']['login'] # => 'test_user'
config.settings['enable_graphql'] # => false

# dig to value
config.settings[:vendor_api, :domain] # => 'app.service.com'
config.settings[:vendor_api, 'login'] # => 'test_user'

# get option value directly via index (with indifferent access)
config['project_id'] # => nil
config['enable_graphql'] # => false
config[:project_id] # => nil
config[:enable_graphql] # => false
config.settings['vendor_api.domain'] # => 'app.service.com'
config.settings['vendor_api.login'] # => 'test_user'

config['vendor_api.domain'] # => 'app.service.com'
config['vendor_api.login'] # => 'test_user'

.dig

# get option value in Hash#dig manner (and fail when the required key does not exist);
config.dig(:vendor_api, :domain) # => 'app.service.com' # (key exists)
config.dig(:vendor_api, :login) # => Qonfig::UnknownSettingError # (key does not exist)
config.dig('vendor_api.domain') # => 'app.service.com' # (key exists)
config.dig('vendor_api.login') # => Qonfig::UnknownSettingError # (key does not exist)

.slice

# get a hash slice of setting options (and fail when the required key does not exist);
config.slice(:vendor_api) # => { 'vendor_api' => { 'domain' => 'app_service', 'login' => 'test_user' } }
config.slice(:vendor_api, :login) # => { 'login' => 'test_user' }
config.slice(:project_api) # => Qonfig::UnknownSettingError # (key does not exist)
config.slice(:vendor_api, :port) # => Qonfig::UnknownSettingError # (key does not exist)
config.slice('vendor_api.login') # => { 'loign' => 'test_user' }
config.slice('vendor_api.port') # => Qonfig::UnknownSettingError # (key does not exist)

.slice_value

# get value from the slice of setting options using the given key set
# (and fail when the required key does not exist) (works in slice manner);

config.slice_value(:vendor_api) # => { 'domain' => 'app_service', 'login' => 'test_user' }
config.slice_value(:vendor_api, :login) # => 'test_user'
config.slice_value(:project_api) # => Qonfig::UnknownSettingError # (key does not exist)
config.slice_value(:vendor_api, :port) # => Qonfig::UnknownSettingError # (key does not exist)
config.slice_value('vendor_api.login') # => 'test_user'
config.slice_value('vendor_api.port') # => Qonfig::UnknownSettingError # (key does not exist)

.subset

# - get a subset (a set of sets) of config settings represented as a hash;
# - each key (or key set) represents a requirement of a certain setting key;

config.subset(:vendor_api, :enable_graphql)
# => { 'vendor_api' => { 'login' => ..., 'domain' => ... }, 'enable_graphql' => false }

config.subset(:project_id, [:vendor_api, :domain], [:credentials, :user, :login])
# => { 'project_id' => nil, 'domain' => 'app.service.com', 'login' => 'D@iVeR' }
config.subset('project_id', 'vendor_api.domain', 'credentials.user.login')
# => { 'project_id' => nil, 'domain' => 'app.service.com', 'login' => 'D@iVeR' }

Configuration

class Config < Qonfig::DataSet
  setting :testing do
    setting :engine, :rspec
    setting :parallel, true
  end

  setting :geo_api do
    setting :provider, :google_maps
  end

  setting :enable_middlewares, false
end

config = Config.new

configure via proc

config.configure do |conf|
  conf.enable_middlewares = true
  conf.geo_api.provider = :yandex_maps
  conf.testing.engine = :mini_test
end

configure via settings object (by option name)

config.settings.enable_middlewares = false
config.settings.geo_api.provider = :apple_maps
config.settings.testing.engine = :ultra_test

configure via settings object (by setting key)

config.settings[:enable_middlewares] = true
config.settings[:geo_api][:provider] = :rambler_maps
config.settings[:testing][:engine] = :mega_test

instant configuration via proc

config = Config.new do |conf|
  conf.enable_middlewares = false
  conf.geo_api.provider = :amazon_maps
  conf.testing.engine = :crypto_test
end

using a hash

config = Config.new(
  testing: { engine: :mini_test, parallel: false },
  geo_api: { provider: :rambler_maps },
  enable_middlewares: true
)
config.configure(enable_middlewares: false)

using both hash and proc (proc has higher priority)

config = Config.new(enable_middlewares: true) do |conf|
  conf.testing.parallel = true
end

config.configure(geo_api: { provider: nil }) do |conf|
  conf.testing.engine = :rspec
end

Inheritance

class CommonConfig < Qonfig::DataSet
  setting :uploader, :fog
end

class ProjectConfig < CommonConfig
  setting :auth_provider, :github
end

project_config = ProjectConfig.new

# inherited setting
project_config.settings.uploader # => :fog

# own setting
project_config.settings.auth_provider # => :github

Composition

class SharedConfig < Qonfig::DataSet
  setting :logger, Logger.new
end

class ServerConfig < Qonfig::DataSet
  setting :port, 12345
  setting :address, '0.0.0.0'
end

class DatabaseConfig < Qonfig::DataSet
  setting :user, 'test'
  setting :password, 'testpaswd'
end

class ProjectConfig < Qonfig::DataSet
  compose SharedConfig

  setting :server do
    compose ServerConfig
  end

  setting :db do
    compose DatabaseConfig
  end
end

project_config = ProjectConfig.new

# fields from SharedConfig
project_config.settings.logger # => #<Logger:0x66f57048>

# fields from ServerConfig
project_config.settings.server.port # => 12345
project_config.settings.server.address # => '0.0.0.0'

# fields from DatabaseConfig
project_config.settings.db.user # => 'test'
project_config.settings.db.password # => 'testpaswd'

Hash representation

class Config < Qonfig::DataSet
  setting :serializers do
    setting :json do
      setting :engine, :ok
    end

    setting :hash do
      setting :engine, :native
    end
  end

  setting :adapter do
    setting :default, :memory_sync
  end

  setting :logger, Logger.new(STDOUT)
end

Default behavior (without-options)

Config.new.to_h
# =>
{
  "serializers": {
    "json" => { "engine" => :ok },
    "hash" => { "engine" => :native },
  },
  "adapter" => { "default" => :memory_sync },
  "logger" => #<Logger:0x4b0d79fc>
}

With transformations

key_transformer = -> (key) { "#{key}!!" }
value_transformer = -> (value) { "#{value}??" }

Config.new.to_h(key_transformer: key_transformer, value_transformer: value_transformer)
# =>
{
  "serializers!!": {
    "json!!" => { "engine!!" => "ok??" },
    "hash!!" => { "engine!!" => "native??" },
  },
  "adapter!!" => { "default!!" => "memory_sync??" },
  "logger!!" => "#<Logger:0x00007fcde799f158>??"
}

Dot-style format

Config.new.to_h(dot_style: true)
# =>
{
  "serializers.json.engine" => :ok,
  "serializers.hash.engine" => :native,
  "adapter.default" => :memory_sync,
  "logger" => #<Logger:0x4b0d79fc>,
}
transformer = -> (value) { "$$#{value}$$" }

Config.new.to_h(dot_style: true, key_transformer: transformer, value_transformer: transformer)

# => "#<Logger:0x00007fcde799f158>??"
{
  "$$serializers.json.engine$$" => "$$ok$$",
  "$$serializers.hash.engine$$" => "$$native$$",
  "$$adapter.default$$" => "$$memory_sync$$",
  "$$logger$$" => "$$#<Logger:0x00007fcde799f158>$$",
}

Smart Mixin

# --- usage ---

class Application
  # make configurable
  include Qonfig::Configurable

  configuration do
    setting :user
    setting :password
  end
end

app = Application.new

# class-level config
Application.config.settings.user # => nil
Application.config.settings.password # => nil

# instance-level config
app.config.settings.user # => nil
app.config.settings.password # => nil

# access to the class level config from an instance
app.shared_config.settings.user # => nil
app.shared_config.settings.password # => nil

# class-level configuration
Application.configure do |conf|
  conf.user = '0exp'
  conf.password = 'test123'
end

# instance-level configuration
app.configure do |conf|
  conf.user = 'admin'
  conf.password = '123test'
end

# class has own config object
Application.config.settings.user # => '0exp'
Application.config.settings.password # => 'test123'

# instance has own config object
app.config.settings.user # => 'admin'
app.config.settings.password # => '123test'

# access to the class level config from an instance
app.shared_config.settings.user # => '0exp'
app.shared_config.settings.password # => 'test123'

# and etc... (all Qonfig-related features)
# --- inheritance ---

class BasicApplication
  # make configurable
  include Qonfig::Configurable

  configuration do
    setting :user
    setting :pswd
  end

  configure do |conf|
    conf.user = 'admin'
    conf.pswd = 'admin'
  end
end

class GeneralApplication < BasicApplication
  # extend inherited definitions
  configuration do
    setting :db do
      setting :adapter
    end
  end

  configure do |conf|
    conf.user = '0exp' # .user inherited from BasicApplication
    conf.pswd = '123test' # .pswd inherited from BasicApplication
    conf.db.adapter = 'pg'
  end
end

BasicApplication.config.to_h
{ 'user' => 'admin', 'pswd' => 'admin' }

GeneralApplication.config.to_h
{ 'user' => '0exp', 'pswd' => '123test', 'db' => { 'adapter' => 'pg' } }

# and etc... (all Qonfig-related features)

Instantiation without class definition

config = Qonfig::DataSet.build do
  setting :user, 'D@iVeR'
  setting :password, 'test123'

  def custom_method
    'custom_result'
  end
end

config.is_a?(Qonfig::DataSet) # => true

config.settings.user # => 'D@iVeR'
config.settings.password # => 'test123'
config.custom_method # => 'custom_result'
class GeneralConfig < Qonfig::DataSet
  setting :db_adapter, :postgresql
end

config = Qonfig::DataSet.build(GeneralConfig) do
  setting :web_api, 'api.google.com'
end

config.is_a?(Qonfig::DataSet) # => true

config.settings.db_adapter # => :postgresql
config.settings.web_api # => "api.google.com"

Compacted config



Definition and instantiation

by raw initialization

class Config < Qonfig::Compacted
  setting :api, 'google.com'
  setting :enabled, true
  setting :queue do
    setting :engine, :sidekiq
  end
end

config = Config.new(api: 'yandex.ru') do |conf|
  conf.enabled = false
end

config.api # => 'yandex.ru'
config.enabled # => false
config.queue.engine # => :sidekiq

by existing Qonfig::DataSet class

class Config < Qonfig::DataSet
  setting :api, 'google.com'
  setting :enabled, true
end

config = Config.build_compacted # builds Qonfig::Compacted instance

config.api # => 'google.com'
config.enabled # => true

by existing Qonfig::DataSet instance

class Config < Qonfig::DataSet
  setting :api, 'google.com'
  setting :enabled, true
end

config = Config.new

compacted_config = config.compacted
# --- or ---
compacted_config = Qonfig::Compacted.build_from(config)

compacted_config.api # => 'google.com'
compacted_config.enabled # => true

instantiation without class definition

config = Qonfig::Compacted.build do
  setting :api, 'google.ru'
  setting :enabled, true
end

config.api # => 'google.ru'
config.enabled # => true

validation API (see full documentation):

# custom validators
Qonfig::Compacted.define_validator(:version_check) do |value|
  value.is_a?(Integer) && value < 100
end

class Config < Qonfig::Compacted
  setting :api, 'google.ru'
  setting :enabled, true
  setting :version, 2
  setting :queue { setting :engine, :sidekiq }

  # full support of original validation api
  validate :api, :string, strict: true
  validate :enabled, :boolean, strict: true
  validate :version, :version_check # custom validator
  validate 'queue.#', :symbol
end

# potential values validation
Config.valid_with?(api: :yandex) # => false
Config.valid_with?(enabled: nil) # => false
Config.valid_with?(version: nil) # => false
Config.valid_with?(api: 'yandex.ru', enabled: false, version: 3) # => true

config = Config.new

# instance validation
config.api = :yandex # => Qonfig::ValidationError (should be a type of string)
config.version = nil # => Qonfig::ValidationError (can not be nil)
config.queue.engine = 'sneakers' # => Qonfig::ValidationError (should be a type of symbol)

Setting readers and writers

class Config < Qonfig::Compcated
  setting :api, 'google.ru'
  setting :enabled, true
  setting :queue do
    setting :engine, :sidekiq
    setting :workers_count, 10
  end
end

config = Config.new

reading (by setting name and index method with dot-notation support and indifferent access)

# by setting name
config.api # => 'google.ru'
config.enabled # => true
config.queue.engine # => :sidekiq
config.queue.workers_count # => 10

# by index method with dot-notation support and indiffernt access
config[:api] # => 'google.ru'
config['enabled'] # => true
config[:queue][:engine] # => :sidekiq
config['queue.workers_count'] # => 10

writing (by setting name and index method with dot-notation support and indifferent access)

# by setting name
config.api = 'yandex.ru'
config.queue.engine = :sidekiq
# and etc

# by index method with dot-notaiton support and indifferent access
config['api'] = 'yandex.ru'
config['queue.engine'] = :sidekiq
config[:queue][:workers_count] = 5

predicates (see full documentation)

class Config < Qonfig::Compcated
  setting :enabled, true
  setting :api, 'yandex.ru'
  setting :queue do
    setting :engine, :sidekiq
  end
end

config = Config.new

config.enabled? # => true
config.enabled = nil
config.enabled? # => false

config.queue.engine? # => true
config.queue.engine =  nil
config.queue.engine? # => false

config.queue? # => true

Interaction


Iteration over setting keys

class Config < Qonfig::DataSet
  setting :db do
    setting :creds do
      setting :user, 'D@iVeR'
      setting :password, 'test123',
      setting :data, test: false
    end
  end

  setting :telegraf_url, 'udp://localhost:8094'
  setting :telegraf_prefix, 'test'
end

config = Config.new

.each_setting

config.each_setting { |key, value| { key => value } }

# result of each step:
{ 'db' => <Qonfig::Settings:0x00007ff8> }
{ 'telegraf_url' => 'udp://localhost:8094' }
{ 'telegraf_prefix' => 'test' }

.deep_each_setting

config.deep_each_setting { |key, value| { key => value } }

# result of each step:
{ 'db.creds.user' => 'D@iveR' }
{ 'db.creds.password' => 'test123' }
{ 'db.creds.data' => { test: false } }
{ 'telegraf_url' => 'udp://localhost:8094' }
{ 'telegraf_prefix' => 'test' }

.deep_each_setting(yield_all: true)

config.deep_each_setting(yield_all: true) { |key, value| { key => value } }

# result of each step:
{ 'db' => <Qonfig::Settings:0x00007ff8> } # (yield_all: true)
{ 'db.creds' => <Qonfig::Settings:0x00002ff1> } # (yield_all: true)
{ 'db.creds.user' => 'D@iVeR' }
{ 'db.creds.password' => 'test123' }
{ 'db.crds.data' => { test: false } }
{ 'telegraf_url' => 'udp://localhost:8094' }
{ 'telegraf_prefix' => 'test' }

List of config keys

# NOTE: suppose we have the following config

class Config < Qonfig::DataSet
  setting :credentials do
    setting :social do
      setting :service, 'instagram'
      setting :login, '0exp'
    end

    setting :admin do
      setting :enabled, true
    end
  end

  setting :server do
    setting :type, 'cloud'
    setting :options do
      setting :os, 'CentOS'
    end
  end
end

config = Config.new

Default behavior

config.keys

# the result:
[
  "credentials.social.service",
  "credentials.social.login",
  "credentials.admin.enabled",
  "server.type",
  "server.options.os"
]

All key variants

config.keys(all_variants: true)

# the result:
[
  "credentials",
  "credentials.social",
  "credentials.social.service",
  "credentials.social.login",
  "credentials.admin",
  "credentials.admin.enabled",
  "server",
  "server.type",
  "server.options",
  "server.options.os"
]

Only root keys

config.keys(only_root: true)

# the result:
['credentials', 'server']
config.root_keys

# the result:
['credentials', 'server']

Config reloading

# -- config example ---

class Config < Qonfig::DataSet
  setting :db do
    setting :adapter, 'postgresql'
  end

  setting :logger, Logger.new(STDOUT)
end

config = Config.new

config.settings.db.adapter # => 'postgresql'
config.settings.logger # => #<Logger:0x00007ff9>
# --- redefine some settings (or add a new one) --

config.configure { |conf| conf.logger = nil } # redefine some settings (will be reloaded)

# re-define and append settings
class Config
  setting :db do
    setting :adapter, 'mongoid' # re-define defaults
  end

  setting :enable_api, false # append new setting
end
# --- reload ---

# reload settings
config.reload!

config.settings.db.adapter # => 'mongoid'
config.settings.logger # => #<Logger:0x00007ff9> (reloaded from defaults)
config.settings.enable_api # => false (new setting)

# reload with instant configuration
config.reload!(db: { adapter: 'oracle' }) do |conf|
  conf.enable_api = true # changed instantly
end

config.settings.db.adapter # => 'oracle'
config.settings.logger = # => #<Logger:0x00007ff9>
config.settings.enable_api # => true # value from instant change

Clear options

class Config
  setting :database do
    setting :user
    setting :password
  end

  setting :web_api do
    setting :endpoint
  end
end

config = Config.new do |conf|
  conf.database.user = '0exp'
  conf.database.password = 'test123'

  conf.web_api.endpoint = '/api/'
end

config.settings.database.user # => '0exp'
config.settings.database.password # => 'test123'
config.settings.web_api.endpoint # => '/api'

# clear all options
config.clear!

config.settings.database.user # => nil
config.settings.database.password # => nil
config.settings.web_api.endpoint # => nil

Frozen state

Instance-level

class Config < Qonfig::DataSet
  setting :logger, Logger.new(STDOUT)
  setting :worker, :sidekiq
  setting :db do
    setting :adapter, 'postgresql'
  end
end

config = Config.new
config.freeze!

config.settings.logger = Logger.new(StringIO.new) # => Qonfig::FrozenSettingsError
config.settings.worker = :que # => Qonfig::FrozenSettingsError
config.settings.db.adapter = 'mongoid' # => Qonfig::FrozenSettingsError

config.reload! # => Qonfig::FrozenSettingsError
config.clear! # => Qonfig::FrozenSettingsError

Definition-level

# --- base class ---
class Config < Qonfig::DataSet
  setting :test, true
  freeze_state!
end

config = Config.new
config.frozen? # => true
config.settings.test = false # => Qonfig::FrozenSettingsError

# --- child class ---
class InheritedConfig < Config
end

inherited_config = InheritedConfig.new
config.frozen? # => false
config.settings.test = false # ok :)

Settings as Predicates

class Config < Qonfig::DataSet
  setting :database do
    setting :user
    setting :host, 'google.com'

    setting :engine do
      setting :driver, 'postgres'
    end
  end
end

config = Config.new

# predicates
config.settings.database.user? # => false (nil => false)
config.settings.database.host? # => true ('google.com' => true)
config.settings.database.engine.driver? # => true ('postgres' => true)

# setting roots always returns true
config.settings.database? # => true
config.settings.database.engine? # => ture

config.configure do |conf|
  conf.database.user = '0exp'
  conf.database.host = false
  conf.database.engine.driver = true
end

# predicates
config.settings.database.user? # => true ('0exp' => true)
config.settings.database.host? # => false (false => false)
config.settings.database.engine.driver? # => true (true => true)

Setting key existence

class Config < Qonfig::DataSet
  setting :credentials do
    setting :user, 'D@iVeR'
    setting :password, 'test123'
  end
end

config = Config.new

# --- array-like format ---
config.key?('credentials', 'user') # => true
config.key?('credentials', 'token') # => false (key does not exist)

# --- dot-notation format ---
config.key?('credentials.user') # => true
config.key?('credentials.token') # => false (key does not exist)

config.key?('credentials') # => true
config.key?('que_adapter') # => false (key does not exist)

# aliases
config.setting?('credentials') # => true
config.option?(:credentials, :password) # => true
config.option?('credentials.password') # => true

Run arbitrary code with temporary settings

class Config < Qonfig::DataSet
  setting :queue do
    setting :adapter, :sidekiq
    setting :options, {}
  end
end

config = Config.new

# run a block of code with temporary queue.adapter setting
config.with(queue: { adapter: 'que' }) do
  # your changed settings
  config.settings.queue.adapter # => 'que'

  # you can temporary change settings by your code too
  config.settings.queue.options = { concurrency: 10 }

  # ...your another code...
end

# original settings has not changed :)
config.settings.queue.adapter # => :sidekiq
config.settings.queue.options # => {}

Import settings / Export settings

Sometimes the nesting of configs in your project is quite high, and it makes you write the rather "cumbersome" code (config.settings.web_api.credentials.account.auth_token for example). Frequent access to configs in this way is inconvinient - so developers wraps such code by methods or variables. In order to make developer's life easer Qonfig provides a special Import API simplifies the config importing (gives you .import_settings DSL) and gives an ability to instant config setting export from a config object (gives you #export_settings config's method).

You can use RabbitMQ-like pattern matching in setting key names:


Import config settings

Suppose we have a config with deeply nested keys:

# NOTE: (Qonfig::DataSet.build creates a class and instantly instantiates it)
AppConfig = Qonfig::DataSet.build do
  setting :web_api do
    setting :credentials do
      setting :account do
        setting :login, 'DaiveR'
        setting :auth_token, 'IAdkoa0@()1239uA'
      end
    end
  end

  setting :graphql_api, false
end

Let's see what we can to do :)

Import a set of setting keys (simple dot-noated key list)

class ServiceObject
  include Qonfig::Imports

  import_settings(AppConfig,
    'web_api.credentials.account.login',
    'web_api.credentials.account'
  )
end

service = ServiceObject.new

service.login # => "D@iVeR"
service.account # => { "login" => "D@iVeR", "auth_token" => IAdkoa0@()1239uA" }

Import with custom method names (mappings)

class ServiceObject
  include Qonfig::Imports

  import_settings(AppConfig, mappings: {
    account_data: 'web_api.credentials.account', # NOTE: name access method with "account_data"
    secret_token: 'web_api.credentials.account.auth_token' # NOTE: name access method with "secret_token"
  })
end

service = ServiceObject.new

service.account_data # => { "login" => "D@iVeR", "auth_token" => "IAdkoa0@()1239uA" }
service.auth_token # => "IAdkoa0@()1239uA"

Prexify method name

class ServiceObject
  include Qonfig::Imports

  import_settings(AppConfig,
    'web_api.credentials.account',
    mappings: { secret_token: 'web_api.credentials.account.auth_token' },
    prefix: 'config_'
  )
end

service = ServiceObject.new

service.config_account # => { login" => "D@iVeR", "auth_token" => "IAdkoa0@()1239uA" }
service.config_secret_token # => "IAdkoa0@()1239uA"

Support for predicate-like methods

class ServiceObject
  include Qonfig::Imports

  import_settings(AppConfig,
    'web_api.credentials.account',
    mappings: { secret_token: 'web_api.credentials.account.auth_token' },
  )
end

service = ServiceObject.new

service.account? # => true
service.secret_token? # => true

Import nested settings as raw Qonfig::Settings objects

# NOTE: import nested settings as raw objects (raw: true)
class ServiceObject
  include Qonfig::Imports

  import_settings(AppConfig, 'web_api.credentials', raw: true)
end

service = ServiceObject.new

service.credentials # => <Qonfig::Settings:0x00007ff8>
service.credentials.account.login # => "D@iVeR"
service.credentials.account.auth_token # => "IAdkoa0@()1239uA"
# NOTE: import nested settings as converted-to-hash objects (raw: false) (default behavior)
class ServiceObject
  include Qonfig::Imports

  import_settings(AppConfig, 'web_api.credentials', raw: false)
end

service = ServiceObject.new

service.credentials # => { "account" => { "login" => "D@iVeR", "auth_token" => "IAdkoa0@()1239uA"} }

Import with pattern-matching

class ServiceObject
  include Qonfig::Imports

  # import all settings from web_api.credentials subset
  import_settings(AppConfig, 'web_api.credentials.#')
  # generated instance methods:
  #   => service.account
  #   => service.login
  #   => service.auth_token

  # import only the root keys from web_api.credentials.account subset
  import_settings(AppConfig, 'web_api.credentials.account.*')
  # generated instance methods:
  #   => service.login
  #   => service.auth_token

  # import only the root keys
  import_settings(AppConfig, '*')
  # generated instance methods:
  #   => service.web_api
  #   => service.graphql_api

  # import ALL keys
  import_settings(AppConfig, '#')
  # generated instance methods:
  #   => service.web_api
  #   => service.credentials
  #   => service.account
  #   => service.login
  #   => service.auth_token
  #   => service.graphql_api
end

Export config settings

class Config < Qonfig::DataSet
  setting :web_api do
    setting :credentials do
      setting :account do
        setting :login, 'DaiveR'
        setting :auth_token, 'IAdkoa0@()1239uA'
      end
    end
  end

  setting :graphql_api, false
end

class ServiceObject; end

config = Config.new
service = ServiceObject.new

service.config_account # => NoMethodError
# NOTE: export settings as access methods to config's settings
config.export_settings(service, 'web_api.credentials.account', prefix: 'config_')

service.config_account # => { "login" => "D@iVeR", "auth_token" => "IAdkoa0@()1239uA" }
# NOTE: export settings with pattern matching
config.export_settings(service, '*') # export root settings

service.web_api # => { 'credentials' => { 'account' => { ... } }, 'graphql_api' => false }
service.graphql_api # => false
# NOTE: predicates
config.export_settings(service, '*')

config.web_api? # => true
config.graphql_api? # => false

Validation


Introduction

Qonfig provides a lightweight DSL for defining validations and works in all cases when setting values are initialized or mutated. Settings are validated as keys (matched with a specific string pattern). You can validate both a set of keys and each key separately. If you want to check the config object completely you can define a custom validation.

Features:


Key search pattern

Key search pattern works according to the following rules:


Proc-based validation

class Config < Qonfig::DataSet
  setting :db do
    setting :user, 'D@iVeR'
    setting :password, 'test123'
  end

  setting :service do
    setting :address, 'google.ru'
    setting :protocol, 'https'

    setting :creds do
      seting :admin, 'D@iVeR'
    end
  end

  setting :enabled, false
  setting :token, '1a2a3a', strict: true

  # validates:
  #   - db.password
  validate 'db.password' do |value|
    value.is_a?(String)
  end

  # validates:
  #   - service.address
  #   - service.protocol
  #   - service.creds.user
  validate 'service.#' do |value|
    value.is_a?(String)
  end

  # validates:
  #   - dataset instance
  validate do # NOTE: no setting key pattern
    settings.enabled == false
  end

  # do not ignore `nil` (strict: true)
  validate(:token, strict: true) do
    value.is_a?(String)
  end
end

config = Config.new
config.settings.db.password = 123 # => Qonfig::ValidationError (should be a string)
config.settings.service.address = 123 # => Qonfig::ValidationError (should be a string)
config.settings.service.protocol = :http # => Qonfig::ValidationError (should be a string)
config.settings.service.creds.admin = :billikota # => Qonfig::ValidationError (should be a string)
config.settings.enabled = true # => Qonfig::ValidationError (isnt `true`)

config.settings.db.password = nil # ok, nil is ignored (non-strict behavior)
config.settings.token = nil # => Qonfig::ValidationError (nil is not ignored, strict behavior) (should be a type of string)

Method-based validation

class Config < Qonfig::DataSet
  setting :services do
    setting :counts do
      setting :google, 2
      setting :rambler, 3
    end

    setting :minimals do
      setting :google, 1
      setting :rambler, 0
    end
  end

  setting :enabled, true
  setting :timeout, 12345, strict: true

  # validates:
  #   - services.counts.google
  #   - services.counts.rambler
  #   - services.minimals.google
  #   - services.minimals.rambler
  validate 'services.#', by: :check_presence

  # validates:
  #   - dataset instance
  validate by: :check_state # NOTE: no setting key pattern

  # do not ignore `nil` (strict: true)
  validate :timeout, strict: true, by: :check_timeout

  def check_presence(value)
    value.is_a?(Numeric) && value > 0
  end

  def check_state
    settings.enabled.is_a?(TrueClass) || settings.enabled.is_a?(FalseClass)
  end

  def check_timeout(value)
    value.is_a?(Numeric)
  end
end

config = Config.new

config.settings.counts.google = 0 # => Qonfig::ValidationError (< 0)
config.settings.minimals.google = -1 # => Qonfig::ValidationError (< 0)
config.settings.minimals.rambler = 'no' # => Qonfig::ValidationError (should be a numeric)

config.settings.counts.rambler = nil # ok, nil is ignored (default non-strict behavior)
config.settings.enabled = nil # ok, nil is ignored (default non-strict behavior)
config.settings.timeout = nil # => Qonfig::ValidationError (nil is not ignored, strict behavior) (should be a type of numeric)

Predefined validations

class Config < Qonfig::DataSet
  setting :user, 'empty'
  setting :password, 'empty'

  setting :service do
    setting :provider, :empty
    setting :protocol, :empty
    setting :on_fail, -> { puts 'atata!' }
  end

  setting :ignorance, false

  validate 'user', :string
  validate 'password', :string
  validate 'service.provider', :text
  validate 'service.protocol', :text
  validate 'service.on_fail', :proc
  validate 'ignorance', :not_nil
end

config = Config.new do |conf|
  conf.user = 'D@iVeR'
  conf.password = 'test123'
  conf.service.provider = :google
  conf.service.protocol = :https
end # NOTE: all right :)

config.settings.ignorance = nil # => Qonfig::ValidationError (cant be nil)

Custom predefined validators

Define your own class-level validator

class Config < Qonfig::DataSet
  # NOTE: definition
  define_validator(:user_type) { |value| value.is_a?(User) }

  setting :admin # some key

  # NOTE: usage
  validate :admin, :user_type
end

Define new global validator

Qonfig::DataSet.define_validator(:secured_value) do |value|
  value == '***'
end

class Config < Qonfig::DataSet
  setting :password
  validate :password, :secured_value
end

Re-definition of existing validators in child classes

class Config < Qonfig::DataSet
  # NOTE: redefine existing :text validator only in Config class
  define_validator(:text) { |value| value.is_a?(String) }

  # NOTE: some custom validator that can be redefined in child classes
  define_validator(:user) { |value| value.is_a?(User) }
end

class SubConfig < Qonfig
  define_validator(:user) { |value| value.is_a?(AdminUser) } # NOTE: redefine inherited :user validator
end

Re-definition of existing global validators

# NOTE: redefine already existing :numeric validator
Qonfig::DataSet.define_validator(:numeric) do |value|
  value.is_a?(Numeric) || (value.is_a?(String) && value.match?(/\A\d+\.*\d+\z/))
end

Validation of potential setting values

valid_with? (instance-level)

class Config < Qonfig::DataSet
  setting :enabled, false
  setting :queue do
    setting :adapter, 'sidekiq'
  end

  validate :enabled, :boolean
  validate 'queue.adapter', :string
end

config = Config.new

config.valid_with?(enabled: true, queue: { adapter: 'que' }) # => true
config.valid_with?(enabled: 123) # => false (should be a type of boolean)
config.valid_with?(enabled: true, queue: { adapter: Sidekiq }) # => false (queue.adapter should be a type of string)

# do-config notation is supported too
config.valid_with?(enabled: true) do |conf|
  conf.queue.adapter = :sidekiq
end
# => false (queue.adapter should be a type of string)

.valid_with? (class-level)

class Config < Qonfig::DataSet
  setting :enabled, false
  setting :queue do
    setting :adapter, 'sidekiq'
  end

  validate :enabled, :boolean
  validate 'queue.adapter', :string
end

Config.valid_with?(enabled: true, queue: { adapter: 'que' }) # => true
Config.valid_with?(enabled: 123) # => false (should be a type of boolean)
Config.valid_with?(enabled: true, queue: { adapter: Sidekiq }) # => false (queue.adapter should be a type of string)

# do-config notation is supported too
Config.valid_with?(enabled: true) do |config|
  config.queue.adapter = :sidekiq
end
# => false (queue.adapter should be a type of string)

Work with files


Load from YAML file

# travis.yml

sudo: false
language: ruby
rvm:
  - ruby-head
  - jruby-head
# project.yml

enable_api: false
Sidekiq/Scheduler:
  enable: true
# ruby_data.yml

version: <%= RUBY_VERSION %>
platform: <%= RUBY_PLATFORM %>
class Config < Qonfig::DataSet
  setting :ruby do
    load_from_yaml 'ruby_data.yml'
  end

  setting :travis do
    load_from_yaml 'travis.yml'
  end

  load_from_yaml 'project.yml'
end

config = Config.new

config.settings.travis.sudo # => false
config.settings.travis.language # => 'ruby'
config.settings.travis.rvm # => ['ruby-head', 'jruby-head']
config.settings.enable_api # => false
config.settings['Sidekiq/Scheduler']['enable'] #=> true
config.settings.ruby.version # => '2.5.1'
config.settings.ruby.platform # => 'x86_64-darwin17'
# --- strict mode ---
class Config < Qonfig::DataSet
  setting :nonexistent_yaml do
    load_from_yaml 'nonexistent_yaml.yml', strict: true # true by default
  end

  setting :another_key
end

Config.new # => Qonfig::FileNotFoundError

# --- non-strict mode ---
class Config < Qonfig::DataSet
  settings :nonexistent_yaml do
    load_from_yaml 'nonexistent_yaml.yml', strict: false
  end

  setting :another_key
end

Config.new.to_h # => { "nonexistent_yaml" => {}, "another_key" => nil }

Expose YAML

Environment is defined as a root key of YAML file

# config/project.yml

default: &default
  enable_api_mode: true
  google_key: 12345
  window:
    width: 100
    height: 100

development:
  <<: *default

test:
  <<: *default
  sidekiq_instrumentation: false

staging:
  <<: *default
  google_key: 777
  enable_api_mode: false

production:
  google_key: asd1-39sd-55aI-O92x
  enable_api_mode: true
  window:
    width: 50
    height: 150
class Config < Qonfig::DataSet
  expose_yaml 'config/project.yml', via: :env_key, env: :production # load from production env

  # NOTE: in rails-like application you can use this:
  expose_yaml 'config/project.yml', via: :env_key, env: Rails.env
end

config = Config.new

config.settings.enable_api_mode # => true (from :production subset of keys)
config.settings.google_key # => asd1-39sd-55aI-O92x (from :production subset of keys)
config.settings.window.width # => 50 (from :production subset of keys)
config.settings.window.height # => 150 (from :production subset of keys)

Environment is defined as a part of YAML file name

# config/sidekiq.staging.yml

web:
  username: staging_admin
  password: staging_password
# config/sidekiq.production.yml

web:
  username: urj1o2
  password: u192jd0ixz0
class SidekiqConfig < Qonfig::DataSet
  # NOTE: file name should be described WITHOUT environment part (in file name attribute)
  expose_yaml 'config/sidekiq.yml', via: :file_name, env: :staging # load from staging env

  # NOTE: in rails-like application you can use this:
  expose_yaml 'config/sidekiq.yml', via: :file_name, env: Rails.env
end

config = SidekiqConfig.new

config.settings.web.username # => staging_admin (from sidekiq.staging.yml)
config.settings.web.password # => staging_password (from sidekiq.staging.yml)

Load from JSON file

// options.json

{
  "user": "0exp",
  "password": 12345,
  "rubySettings": {
    "allowedVersions": ["2.3", "2.4.2", "1.9.8"],
    "gitLink": null,
    "withAdditionals": false
  }
}
class Config < Qonfig::DataSet
  load_from_json 'options.json'
end

config = Config.new

config.settings.user # => '0exp'
config.settings.password # => 12345
config.settings.rubySettings.allowedVersions # => ['2.3', '2.4.2', '1.9.8']
config.settings.rubySettings.gitLink # => nil
config.settings.rubySettings.withAdditionals # => false
# --- strict mode ---
class Config < Qonfig::DataSet
  setting :nonexistent_json do
    load_from_json 'nonexistent_json.json', strict: true # true by default
  end

  setting :another_key
end

Config.new # => Qonfig::FileNotFoundError

# --- non-strict mode ---
class Config < Qonfig::DataSet
  settings :nonexistent_json do
    load_from_json 'nonexistent_json.json', strict: false
  end

  setting :another_key
end

Config.new.to_h # => { "nonexistent_json" => {}, "another_key" => nil }

Expose JSON

Environment is defined as a root key of JSON file

// config/project.json

{
  "development": {
    "api_mode_enabled": true,
    "logging": false,
    "db_driver": "sequel",
    "throttle_requests": false,
    "credentials": {}
  },
  "test": {
    "api_mode_enabled": true,
    "logging": false,
    "db_driver": "in_memory",
    "throttle_requests": false,
    "credentials": {}
  },
  "staging": {
    "api_mode_enabled": true,
    "logging": true,
    "db_driver": "active_record",
    "throttle_requests": true,
    "credentials": {}
  },
  "production": {
    "api_mode_enabled": true,
    "logging": true,
    "db_driver": "rom",
    "throttle_requests": true,
    "credentials": {}
  }
}
class Config < Qonfig::DataSet
  expose_json 'config/project.json', via: :env_key, env: :production # load from production env

  # NOTE: in rails-like application you can use this:
  expose_json 'config/project.json', via: :env_key, env: Rails.env
end

config = Config.new

config.settings.api_mode_enabled # => true (from :production subset of keys)
config.settings.logging # => true (from :production subset of keys)
config.settings.db_driver # => "rom" (from :production subset of keys)
config.settings.throttle_requests # => true (from :production subset of keys)
config.settings.credentials # => {} (from :production subset of keys)

Environment is defined as a part of JSON file name

// config/sidekiq.staging.json
{
  "web": {
    "username": "staging_admin",
    "password": "staging_password"
  }
}
// config/sidekiq.production.json
{
  "web": {
    "username": "urj1o2",
    "password": "u192jd0ixz0"
  }
}
class SidekiqConfig < Qonfig::DataSet
  # NOTE: file name should be described WITHOUT environment part (in file name attribute)
  expose_json 'config/sidekiq.json', via: :file_name, env: :staging # load from staging env

  # NOTE: in rails-like application you can use this:
  expose_json 'config/sidekiq.json', via: :file_name, env: Rails.env
end

config = SidekiqConfig.new

config.settings.web.username # => "staging_admin" (from sidekiq.staging.json)
config.settings.web.password # => "staging_password" (from sidekiq.staging.json)

Load from ENV

# some env variables
ENV['QONFIG_BOOLEAN'] = 'true'
ENV['QONFIG_INTEGER'] = '0'
ENV['QONFIG_STRING'] = 'none'
ENV['QONFIG_ARRAY'] = '1, 2.5, t, f, TEST'
ENV['QONFIG_MESSAGE'] = '"Hello, Qonfig!"'
ENV['RUN_CI'] = '1'

class Config < Qonfig::DataSet
  # nested
  setting :qonfig do
    load_from_env convert_values: true, prefix: 'QONFIG' # or /\Aqonfig.*\z/i
  end

  setting :trimmed do
    load_from_env convert_values: true, prefix: 'QONFIG_', trim_prefix: true # trim prefix
  end

  # on the root
  load_from_env
end

config = Config.new

# customized
config.settings['qonfig']['QONFIG_BOOLEAN'] # => true ('true' => true)
config.settings['qonfig']['QONFIG_INTEGER'] # => 0 ('0' => 0)
config.settings['qonfig']['QONFIG_STRING'] # => 'none'
config.settings['qonfig']['QONFIG_ARRAY'] # => [1, 2.5, true, false, 'TEST']
config.settings['qonfig']['QONFIG_MESSAGE'] # => 'Hello, Qonfig!'
config.settings['qonfig']['RUN_CI'] # => Qonfig::UnknownSettingError

# trimmed (and customized)
config.settings['trimmed']['BOOLEAN'] # => true ('true' => true)
config.settings['trimmed']['INTEGER'] # => 0 ('0' => 0)
config.settings['trimmed']['STRING'] # => 'none'
config.settings['trimmed']['ARRAY'] # => [1, 2.5, true, false, 'TEST']
config.settings['trimmed']['MESSAGE'] # => 'Hello, Qonfig!'
config.settings['trimmed']['RUN_CI'] # => Qonfig::UnknownSettingError

# default
config.settings['QONFIG_BOOLEAN'] # => 'true'
config.settings['QONFIG_INTEGER'] # => '0'
config.settings['QONFIG_STRING'] # => 'none'
config.settings['QONFIG_ARRAY'] # => '1, 2.5, t, f, TEST'
config.settings['QONFIG_MESSAGE'] # => '"Hello, Qonfig!"'
config.settings['RUN_CI'] # => '1'

Load from __END__

class Config < Qonfig::DataSet
  load_from_self # on the root (:dynamic format is used by default)

  setting :nested do
    load_from_self, format: :yaml # with explicitly identified YAML format
  end
end

config = Config.new

# on the root
config.settings.ruby_version # => '2.5.1'
config.settings.secret_key # => 'top-mega-secret'
config.settings.api_host # => 'super.puper-google.com'
config.settings.connection_timeout.seconds # => 10
config.settings.connection_timeout.enabled # => false

# nested
config.settings.nested.ruby_version # => '2.5.1'
config.settings.nested.secret_key # => 'top-mega-secret'
config.settings.nested.api_host # => 'super.puper-google.com'
config.settings.nested.connection_timeout.seconds # => 10
config.settings.nested.connection_timeout.enabled # => false

__END__

ruby_version: <%= RUBY_VERSION %>
secret_key: top-mega-secret
api_host: super.puper-google.com
connection_timeout:
  seconds: 10
  enabled: false

Expose __END__

class Config < Qonfig::DataSet
  expose_self env: :production, format: :yaml # with explicitly identified YAML format

  # NOTE: for Rails-like applications you can use this:
  expose_self env: Rails.env
end

config = Config.new

config.settings.log # => true (from :production environment)
config.settings.api_enabled # => true (from :production environment)
config.settings.creds.user # => "D@iVeR" (from :production environment)
config.settings.creds.password # => "test123" (from :production environment)

__END__

default: &default
  log: false
  api_enabled: true
  creds:
    user: admin
    password: 1234

development:
  <<: *default
  log: true

test:
  <<: *default
  log: false

staging:
  <<: *default

production:
  <<: *default
  log: true
  creds:
    user: D@iVeR
    password: test123

Default setting values file

Default behavior

# sidekiq.yml

adapter: sidekiq
options:
  processes: 10
class Config < Qonfig::DataSet
  values_file 'sidekiq.yml', format: :yaml

  setting :adapter, 'que'
  setting :options do
    setting :processes, 2
    setting :threads, 5
    setting :protected, false
  end
end

config = Config.new

config.settings.adapter # => "sidekiq" (from sidekiq.yml)
config.settings.options.processes # => 10 (from sidekiq.yml)
config.settings.options.threads # => 5 (original value)
config.settings.options.protected # => false (original value)

Load values from __END__-data

class Config < Qonfig::DataSet
  values_file :self, format: :yaml

  setting :user
  setting :password
  setting :enabled, true
end

config = Config.new

config.settings.user # => "D@iVeR" (from __END__ data)
config.settings.password # => "test123" (from __END__ data)
config.settings.enabled # => true (original value)

__END__

user: 'D@iVeR'
password: 'test123'

Setting values with environment separation

# sidekiq.yml

development:
  adapter: :in_memory
  options:
    threads: 10

production:
  adapter: :sidekiq
  options:
    threads: 150
class Config < Qonfig::DataSet
  values_file 'sidekiq.yml', format: :yaml, expose: :development

  setting :adapter
  setting :options do
    setting :threads
  end
end

config = Config.new

config.settings.adapter # => 'in_memory' (development keys subset)
config.settings.options.threads # => 10 (development keys subset)

File does not exist

# non-strict behavior (default)
class Config < Qonfig::DataSet
  values_file 'sidekiq.yml'
end

config = Config.new # no error

# strict behavior (strict: true)
class Config < Qonfig::DataSet
  values_file 'sidekiq.yml', strict: true
end

config = Config.new # => Qonfig::FileNotFoundError

Load setting values from YAML file (by instance)

Default behavior

# config.yml

domain: google.ru
creds:
  auth_token: test123
class Config < Qonfig::DataSet
  seting :domain, 'test.com'
  setting :creds do
    setting :auth_token, 'test'
  end
end

config = Config.new
config.settings.domain # => "test.com"
config.settings.creds.auth_token # => "test"

# load new values
config.load_from_yaml('config.yml')

config.settings.domain # => "google.ru" (from config.yml)
config.settings.creds.auth_token # => "test123" (from config.yml)

Load from __END__

class Config < Qonfig::DataSet
  seting :domain, 'test.com'
  setting :creds do
    setting :auth_token, 'test'
  end
end

config = Config.new
config.settings.domain # => "test.com"
config.settings.creds.auth_token # => "test"

# load new values
config.load_from_yaml(:self)
config.settings.domain # => "yandex.ru" (from __END__-data)
config.settings.creds.auth_token # => "CK0sIdA" (from __END__-data)

__END__

domain: yandex.ru
creds:
  auth_token: CK0sIdA

Setting values with environment separation

# config.yml

development:
  domain: dev.google.ru
  creds:
    auth_token: kekpek

production:
  domain: google.ru
  creds:
    auth_token: Asod1
class Config < Qonfig::DataSet
  setting :domain, 'test.com'
  setting :creds do
    setting :auth_token
  end
end

config = Config.new

# load new values (expose development settings)
config.load_from_yaml('config.yml', expose: :development)

config.settings.domain # => "dev.google.ru" (from config.yml)
config.settings.creds.auth_token # => "kek.pek" (from config.yml)

Load setting values from JSON file (by instance)

Default behavior

// config.json

{
  "domain": "google.ru",
  "creds": {
    "auth_token": "test123"
  }
}
class Config < Qonfig::DataSet
  seting :domain, 'test.com'
  setting :creds do
    setting :auth_token, 'test'
  end
end

config = Config.new
config.settings.domain # => "test.com"
config.settings.creds.auth_token # => "test"

# load new values
config.load_from_json('config.json')

config.settings.domain # => "google.ru" (from config.json)
config.settings.creds.auth_token # => "test123" (from config.json)

Load from __END__

class Config < Qonfig::DataSet
  seting :domain, 'test.com'
  setting :creds do
    setting :auth_token, 'test'
  end
end

config = Config.new
config.settings.domain # => "test.com"
config.settings.creds.auth_token # => "test"

# load new values
config.load_from_json(:self)
config.settings.domain # => "yandex.ru" (from __END__-data)
config.settings.creds.auth_token # => "CK0sIdA" (from __END__-data)

__END__

{
  "domain": "yandex.ru",
  "creds": {
    "auth_token": "CK0sIdA"
  }
}

Setting values with environment separation

// config.json

{
  "development": {
    "domain": "dev.google.ru",
    "creds": {
      "auth_token": "kekpek"
    }
  },
  "production": {
    "domain": "google.ru",
    "creds": {
      "auth_token": "Asod1"
    }
  }
}
class Config < Qonfig::DataSet
  setting :domain, 'test.com'
  setting :creds do
    setting :auth_token
  end
end

config = Config.new

# load new values (from development subset)
config.load_from_json('config.json', expose: :development)

config.settings.domain # => "dev.google.ru" (from config.json)
config.settings.creds.auth_token # => "kek.pek" (from config.json)

Load setting values from __END__ (by instance)

Default behavior

class Config < Qonfig::DataSet
  setting :account, 'test'
  setting :options do
    setting :login, '0exp'
    setting :password, 'test123'
  end
end

config = Config.new
config.settings.account # => "test" (original value)
config.settings.options.login # => "0exp" (original value)
config.settings.options.password # => "test123" (original value)

# load new values
config.load_from_self(format: :yaml)
# or config.load_from_self

config.settings.account # => "real" (from __END__-data)
config.settings.options.login # => "D@iVeR" (from __END__-data)
config.settings.options.password # => "azaza123" (from __END__-data)

__END__

account: real
options:
  login: D@iVeR
  password: azaza123

Setting values with envvironment separation

class Config < Qonfig::DataSet
  setting :domain, 'test.google.ru'
  setting :options do
    setting :login, 'test'
    setting :password, 'test123'
  end
end

config = Config.new
config.settings.domain # => "test.google.ru" (original value)
config.settings.options.login # => "test" (original value)
config.settings.options.password # => "test123" (original value)

# load new values
config.load_from_self(format: :json, expose: :production)
# or config.load_from_self(expose: production)

config.settings.domain # => "prod.google.ru" (from __END__-data)
config.settings.options.login # => "prod" (from __END__-data)
config.settings.options.password # => "prod123" (from __END__-data)

__END__

{
  "development": {
    "domain": "dev.google.ru",
    "options": {
      "login": "dev",
      "password": "dev123"
    }
  },
  "production": {
    "domain": "prod.google.ru",
    "options": {
      "login": "prod",
      "password": "prod123"
    }
  }
}

Load setting values from file manually (by instance)


Save to JSON file

Without value preprocessing (standard usage)

class AppConfig < Qonfig::DataSet
  setting :server do
    setting :address, 'localhost'
    setting :port, 12_345
  end

  setting :enabled, true
end

config = AppConfig.new

# NOTE: save to json file
config.save_to_json(path: 'config.json')
{
 "sentry": {
  "address": "localhost",
  "port": 12345
 },
 "enabled": true
}

With value preprocessing and custom options

class AppConfig < Qonfig::DataSet
  setting :server do
    setting :address, 'localhost'
    setting :port, 12_345
  end

  setting :enabled, true
  setting :dynamic, -> { 1 + 2 }
end

config = AppConfig.new

# NOTE: save to json file with custom options (no spaces / no new line / no indent; call procs)
config.save_to_json(path: 'config.json', options: { indent: '', space: '', object_nl: '' }) do |value|
  value.is_a?(Proc) ? value.call : value
end
// no spaces / no new line / no indent / calculated "dynamic" setting key
{"sentry":{"address":"localhost","port":12345},"enabled":true,"dynamic":3}

Save to YAML file

Without value preprocessing (standard usage)

class AppConfig < Qonfig::DataSet
  setting :server do
    setting :address, 'localhost'
    setting :port, 12_345
  end

  setting :enabled, true
end

config = AppConfig.new

# NOTE: save to yaml file
config.save_to_yaml(path: 'config.yml')
---
server:
  address: localhost
  port: 12345
enabled: true

With value preprocessing and custom options

class AppConfig < Qonfig::DataSet
  setting :server do
    setting :address, 'localhost'
    setting :port, 12_345
  end

  setting :enabled, true
  setting :dynamic, -> { 5 + 5 }
end

config = AppConfig.new

# NOTE: save to yaml file with custom options (add yaml version header; call procs)
config.save_to_yaml(path: 'config.yml', options: { header: true }) do |value|
  value.is_a?(Proc) ? value.call : value
end
# yaml version header / calculated "dynamic" setting key
%YAML 1.1
---
server:
  address: localhost
  port: 12345
enabled: true
dynamic: 10

Plugins


Usage

Qonfig.plugins # => ["pretty_print", "toml", ..., ...]
Qonfig.plugin(:pretty_print) # or Qonfig.plugin('pretty_print')
# -- or --
Qonfig.enable(:pretty_print) # or Qonfig.enable('pretty_print')
# -- or --
Qonfig.load(:pretty_print) # or Qonfig.load('pretty_print')
Qonfig.loaded_plugins # => ["pretty_print"]
# -- or --
Qonfig.enabled_plugins # => ["pretty_print"]

Plugins: toml

# 1) require external dependency
require 'toml-rb'

# 2) enable plugin
Qonfig.plugin(:toml)

# 3) use toml :)

Plugins: pretty_print

Example:

class Config < Qonfig::DataSet
  setting :api do
    setting :domain, 'google.ru'
    setting :creds do
      setting :account, 'D@iVeR'
      setting :password, 'test123'
    end
  end

  setting :log_requests, true
  setting :use_proxy, true
end

config = Config.new
=> #<Config:0x00007f9b6c01dab0
 @__lock__=
  #<Qonfig::DataSet::Lock:0x00007f9b6c01da60
   @access_lock=#<Thread::Mutex:0x00007f9b6c01da38>,
   @arbitary_lock=#<Thread::Mutex:0x00007f9b6c01d9e8>,
   @definition_lock=#<Thread::Mutex:0x00007f9b6c01da10>>,
 @settings=
  #<Qonfig::Settings:0x00007f9b6c01d858
   @__lock__=
    #<Qonfig::Settings::Lock:0x00007f9b6c01d808
     @access_lock=#<Thread::Mutex:0x00007f9b6c01d7b8>,
     @definition_lock=#<Thread::Mutex:0x00007f9b6c01d7e0>,
     @merge_lock=#<Thread::Mutex:0x00007f9b6c01d790>>,
   @__mutation_callbacks__=
    #<Qonfig::Settings::Callbacks:0x00007f9b6c01d8d0
     @callbacks=[#<Proc:0x00007f9b6c01d8f8@/Users/daiver/Projects/qonfig/lib/qonfig/settings/builder.rb:39>],
     @lock=#<Thread::Mutex:0x00007f9b6c01d880>>,
   @__options__=
    {"api"=>
      #<Qonfig::Settings:0x00007f9b6c01d498
# ... and etc
=> #<Config:0x00007f9b6c01dab0
 api.domain: "google.ru",
 api.creds.account: "D@iVeR",
 api.creds.password: "test123",
 log_requests: true,
 use_proxy: true>

# -- or --

=> #<Config:0x00007f9b6c01dab0 api.domain: "google.ru", api.creds.account: "D@iVeR", api.creds.password: "test123", log_requests: true, use_proxy: true>

Plugins: vault

# 1) require external dependency
require 'vault'

# 2) Setup vault client

Vault.address = 'http://localhost:8200'
Vault.token = 'super-duper-token-here'

# 3) enable plugin
Qonfig.plugin(:vault)

# 3) use vault :)

Roadmap

Build

bin/rspec -w # test the core functionality and plugins
bin/rspec -n # test only the core functionality

Contributing

License

Released under MIT License.

Authors

Rustam Ibragimov