Giter Site home page Giter Site logo

0exp / qonfig Goto Github PK

View Code? Open in Web Editor NEW
24.0 3.0 8.0 1003 KB

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.

License: MIT License

Ruby 99.97% Shell 0.03%
config ruby-config settings ruby-settings configurable ruby-configurable yaml-config json-config toml-config multi-config

qonfig's Introduction

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.

  • Support for: YAML, TOML, JSON, ENV, __END__-instructions;
  • Fully thread-safe;
  • Object-oriented behavior (config as an object, inhertance, composition, etc), with an abilities of lazy-instantiation;
  • Pluggable and extendable multi-functional API;
  • Developer-friendly DSL :)
# 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

  • setting(name, value = nil) - define setting with corresponding name and value;
  • setting(name) { setting(name, value = nil); ... } - define nested settings OR reopen existing nested setting and define some new nested settings;
  • re_setting(name, value = nil), re_setting(name) { ... } - re-define existing setting (or define new if the original does not exist);
  • accessing: access via method, access via index-method [], .dig, .slice, .slice_value, .subset;
# --- 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 []

  • without dot-notation:
# 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
  • with dot-notation:
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

  • without dot-notation:
# 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)
  • with dot-notation:
config.dig('vendor_api.domain') # => 'app.service.com' # (key exists)
config.dig('vendor_api.login') # => Qonfig::UnknownSettingError # (key does not exist)

.slice

  • without dot-notation:
# 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)
  • with dot-notation:
config.slice('vendor_api.login') # => { 'loign' => 'test_user' }
config.slice('vendor_api.port') # => Qonfig::UnknownSettingError # (key does not exist)

.slice_value

  • without dot-notaiton:
# 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)
  • with dot-notation:
config.slice_value('vendor_api.login') # => 'test_user'
config.slice_value('vendor_api.port') # => Qonfig::UnknownSettingError # (key does not exist)

.subset

  • without dot-notation:
# - 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' }
  • with dot-notation:
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

  • works via #to_h and #to_hash;
  • supported options:
    • key_transformer: - an optional proc that accepts setting key and makes your custom transformations;
    • value_transformer: - an optional proc that accepts setting value and makes your custom transformations;
    • dot_style: - (false by default) represent setting keys in dot-notation (transformations are supported too);
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

  • with key_transformer and/or value_transformer;
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

  • transformations are supported too (key_transformer and value_transformer);
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

  • class-level:
    • .configuration - settings definitions;
    • .configure - configuration;
    • .config - config object;
    • settings definitions are inheritable;
  • instance-level:
    • #configure - configuration;
    • #config - config object;
    • #shared_config - class-level config object;
# --- 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

  • without inheritance:
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'
  • with inheritance:
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


  • Qonfig::Compacted: represents the compacted config object with setting readers, setting writers and setting predicates only - and no any other useful instance-based functionality:
  • setting keys are represented as direct instace methods (#settings invokation does not need);
  • support for index-like access methods ([],[]=);
  • full support of Qonfig::DataSet definition DSL commands:
    • setting, re_setting doc
    • validate, add_validator doc
    • load_from_self doc, load_from_yaml doc, load_from_json doc, load_from_toml doc;
    • expose_self doc, expose_yaml doc, expose_json doc, expose_toml doc
    • values_file doc
  • support for validation of potential setting values .valid_with? documentation;
  • can be instantiated by:
    • by existing config object: Qonfig::DataSet#compacted or Qonfig::Compacted.build_from(config, &configuration);
    • from existing Qonfig::DataSet class: Qonfig::DataSet.build_compacted;
    • by direct instantiation: Qonfig::Compacted.new(settings_values = {}, &configuration);
    • by implicit instance building without explicit class definition Qonfig::Compacted.build(&dsl_commands);
  • you can define your own instance methods too;

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

  • Qonfig::DataSet#compacted
  • (or) Qonfig::Compacted.build_from(config)
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
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

  • #each_setting { |key, value| }
    • iterates over the root setting keys;
  • #deep_each_setting(yield_all: false) { |key, value| }
    • iterates over all setting keys (deep inside);
    • key object is represented as a string of .-joined setting key names;
    • yield_all: means "yield all config objects" (end values and root setting objects those have nested settings) (false by default);
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

  • #keys - returns a list of all config keys in dot-notation format;
    • all_variants: - get all possible variants of the config's keys sequences (false by default);
    • only_root: - get only the root config keys (false by default);
  • #root_keys - returns a list of root config keys (an alias for #keys(only_root: true));
# 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

  • method signature: #reload!(configurations = {}, &configuration);
# -- 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

  • set all config's settings to nil;
  • method signature: #clear!;
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

  • method signature: #freeze!;
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

  • DSL-method signature: freeze_state!
  • indicaes that all your config instances should be frozen;
  • freeze_state! DSL command is not inherited (your child and composed config classes will not have this declaration);
# --- 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

  • predicate form: ? at the end of setting name;
  • nil and false setting values indicates false;
  • other setting values indicates true;
  • setting roots always returns true;
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

  • supports dynamic array-like format and canonical dot-notation format;
  • returns true if the concrete key is exist;
  • returns false if the concrete key does not exist;
  • dynamic array-like format:
    • #key?(*key_path) / #option?(*key_path) / #setting?(*key_path);
    • *key_path - an array of symbols and strings that represents a path to the concrete setting key;
    • (for example, config.key?(:credentials, :user) tries to check that config.settings.credentials.user is exist);
  • dot-notation format:
    • #key?(key) / #option?(key) / #setting?(key);
    • key - string in dot-notated format
    • (for example: config.key?('credentials.user') tries to check that config.settings.crednetials.user is exist);
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

  • provides a way to run an arbitrary code with temporarily specified settings;
  • your arbitrary code can temporary change any setting too - all settings will be returned to the original state;
  • (it is convenient to run code samples by this way in tests (with substitued configs));
  • it is fully thread-safe :);
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:

  • if the setting key name at the current nesting level does not matter - use *;
  • if both the setting key name and nesting level does not matter - use #
  • examples:
    • db.settings.user - matches to db.settings.user setting;
    • db.settings.* - matches to all setting keys inside db.settings group of settings;
    • db.*.user - matches to all user setting keys at the first level of db group of settings;
    • #.user - matches to all user setting keys;
    • service.#.password - matches to all password setting keys at all levels of service group of settings;
    • # - matches to ALL setting keys;
    • * - matches to all setting keys at the root level;
    • and etc;

Import config settings

  • Qonfig::Imports - a special mixin that provides the convenient DSL to work with config import features (.import_settings method);
  • .import_settings - DSL method for importing configuration settings (from a config instance) as instance methods of a class;
  • (IMPORTANT) import_settings imports config settings as access methods to config's settings (creates attr_readers for your config);
  • generated methods can be used as predicates (with trailing ? symbol);
  • you can generate attr_accessors by specifying accessor: true option (be careful: you can get Qonfig::AmbiguousSettingValueError when you try to assign a value to config option which have nested settings);
  • signature: .import_settings(config_object, *setting_keys, mappings: {}, prefix: '', raw: false)
    • config_object - an instance of Qonfig::DataSet whose config settings should be imported;
    • *setting_keys - an array of dot-notaed config's setting keys that should be imported (dot-notaed key is a key that describes each part of nested setting key as a string separated by dot-symbol);
      • last part of dot-notated key will become a name of the setting access instance method;
    • mappings: - a map of keys that describes custom method names for each imported setting;
    • prefix: - prexifies setting access method name with custom prefix;
    • raw: - use nested settings as objects or hashify them (false by default (means "hashify nested settings"));
    • accessor: - generate attr_accessor for imported config settigns (false by default (means "generate attr_readers only"));

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)

  • last part of dot-notated key will become a name of the setting access instance method;
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)

  • mappings: defines a map of keys that describes custom method names for each imported setting;
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

  • prefix: - prexifies setting access method name with custom prefix;
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

  • generated methods can be used as predicates (with trailing ? symbol);
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

  • raw: false is used by default (hashify nested settings)
# 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

  • import root keys only: import_settings(config_object, '*');
  • import all keys: import_settings(config_object, '#');
  • import the subset of keys: import_settings(config_object, 'group.*.group.#') (pattern-mathcing usage);
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

  • works in .import_settings manner doc (see examples and documentation above :))
  • all config objects can export their settings to an arbitrary object as singleton methods;
  • (IMPORTANT) export_settings exports config settings as access methods to config's settings (creates attr_readers for your config);
  • generated methods can be used as predicates (with trailing ? symbol);
  • you can generate attr_accessors by specifying accessor: true option (be careful: you can get Qonfig::AmbiguousSettingValueError when you try to assign a value to config option which have nested settings);
  • signature: #export_settings(exportable_object, *setting_keys, mappings: {}, prefix: '', raw: false):
    • exportable_object - an arbitrary object for exporting;
    • *setting_keys - an array of dot-notaed config's setting keys that should be exported (dot-notaed key is a key that describes each part of nested setting key as a string separated by dot-symbol);
      • last part of dot-notated key will become a name of the setting access instance method;
    • mappings: - a map of keys that describes custom method names for each exported setting;
    • prefix: - prexifies setting access method name with custom prefix;
    • raw: - use nested settings as objects or hashify them (false by default (means "hashify nested settings"));
  • accessor: - generate attr_accessor for imported config settigns (false by default (means "generate attr_readers only"));
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:

  • validation is invoked on any mutation of any setting:
    • during dataset instantiation;
    • when assigning new values;
    • when calling #reload!;
    • when calling #clear!;
  • provides strict and non-strict behavior (strict: true and strict: false respectively):
    • strict: false ignores validations for settings with nil (allows nil value);
    • strict: true does not ignores validations for settings with nil;
    • strict: false is used by default;
  • provides a special key search pattern for matching setting key names;
  • you can validate potential setting values without any assignment (documentation)
  • uses the key search pattern for definging what the setting key should be validated;
  • you can define your own custom validation logic and validate dataset instance completely;
  • validation logic should return truthy or falsy value;
  • supprots two validation techniques (proc-based (documentation) and dataset-method-based (documentation)):
    • proc-based (setting validation) (documentation)
        validate('db.user', strict: true) do |value|
          value.is_a?(String)
        end
    • proc-based (dataset validation) (doc)
        validate(strict: false) do
          settings.user == User[1]
        end
    • dataset-method-based (setting validation) (documentation)
        validate 'db.user', by: :check_user, strict: true
      
        def check_user(value)
          value.is_a?(String)
        end
    • dataset-method-based (dataset validation) (documentation)
        validate by: :check_config, strict: false
      
        def check_config
          settings.user == User[1]
        end
  • provides a set of standard validations (documentation):
    • DSL: validate 'key.pattern', :predefned_validator;
    • supports strict behavior;
  • you can define your own predefined validators (class-related and global-related) (documentation);

Key search pattern

Key search pattern works according to the following rules:

  • works in RabbitMQ-like key pattern ruleses;
  • has a string format;
  • nested configs are defined by a set of keys separated by .-symbol;
  • if the setting key name at the current nesting level does not matter - use *;
  • if both the setting key name and nesting level does not matter - use #
  • examples:
    • db.settings.user - matches to db.settings.user setting;
    • db.settings.* - matches to all setting keys inside db.settings group of settings;
    • db.*.user - matches to all user setting keys at the first level of db group of settings;
    • #.user - matches to all user setting keys;
    • service.#.password - matches to all password setting keys at all levels of service group of settings;
    • # - matches to ALL setting keys;
    • * - matches to all setting keys at the root level;
    • and etc;

Proc-based validation

  • your proc should return truthy value or falsy value;
  • nil values are ignored by default;
  • set strict: true to disable nil ignorance (strict: false is used by default);
  • how to validate setting keys:
    • define proc with attribute: validate 'your.setting.path' do |value|; end
    • proc will receive setting value;
  • how to validate dataset instance:
    • define proc without setting key pattern: validate do; end;
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

  • method should return truthy value or falsy value;
  • nil values are ignored by default;
  • set strict: true to disable nil ignorance (strict: false is used by default);
  • how to validate setting keys:
    • define validation: validate 'db.*.user', by: :your_custom_method;
    • define your method with attribute: def your_custom_method(setting_value); end
  • how to validate config instance
    • define validation: validate by: :your_custom_method
    • define your method without attributes: def your_custom_method; end
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

  • DSL: validate 'key.pattern', :predefned_validator
  • nil values are ignored by default;
  • set strict: true to disable nil ignorance (strict: false is used by default);
  • predefined validators:
    • :not_nil
    • :integer
    • :float
    • :numeric
    • :big_decimal
    • :array
    • :hash
    • :string
    • :symbol
    • :text (string or symbol)
    • :boolean
    • :class
    • :module
    • :proc
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

  • DSL: .define_validator(name, &validation) { |value| ... } - create your own predefined validator;
  • class-level: define validators related only to the concrete config class;
  • global-level: define validators related to all config classes (Qonfig::DataSet.define_validator);
  • you can re-define any global and inherited validator (at class level);
  • you can re-define any already registered global validator on Qonfig::DataSet (at global-level);

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

  • (instance-level) #valid_with?(setting_values = {}, &configuration) - check that current config instalce will be valid with passed configurations;
  • (class-level) .valid_with?(setting_values = {}, &configuration) - check that potential config instancess will be valid with passed configurations;
  • makes no assignments;

#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

  • supports ERB;
  • :strict mode (fail behaviour when the required yaml file doesnt exist):
    • true (by default) - causes Qonfig::FileNotFoundError;
    • false - do nothing, ignore current command;
  • :replace_on_merge - whether the setting should be replaced on the key conflict, otherwise, it will be deep merged (default);
# 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

  • load configurations from YAML file in Rails-like manner (with environments);
  • works in load_from_yaml manner;
  • via: - how an environment will be determined:
    • :file_name
      • load configuration from YAML file that have an :env part in it's name;
    • :env_key
      • load configuration from YAML file;
      • concrete configuration should be defined in the root key with :env name;
  • env: - your environment name (must be a type of String, Symbol or Numeric);
  • strict: - requires the existence of the file and/or key with the name of the used environment:
    • true:
      • file should exist;
      • root key with :env name should exist (if via: :env_key is used);
      • raises Qonfig::ExposeError if file does not contain the required env key (if via: :env key is used);
      • raises Qonfig::FileNotFoundError if the required file does not exist;
    • false:
      • file is not required;
      • root key with :env name is not required (if via: :env_key is used);
  • :replace_on_merge - whether the setting should be replaced on the key conflict, otherwise, it will be deep merged (default);

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

  • supports ERB;
  • :strict mode (fail behaviour when the required yaml file doesnt exist):
    • true (by default) - causes Qonfig::FileNotFoundError;
    • false - do nothing, ignore current command;
  • :replace_on_merge - whether the setting should be replaced on the key conflict, otherwise, it will be deep merged (default);
// 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

  • load configurations from JSON file in Rails-like manner (with environments);
  • works in load_from_jsom/expose_yaml manner;
  • via: - how an environment will be determined:
    • :file_name
      • load configuration from JSON file that have an :env part in it's name;
    • :env_key
      • load configuration from JSON file;
      • concrete configuration should be defined in the root key with :env name;
  • env: - your environment name (must be a type of String, Symbol or Numeric);
  • strict: - requires the existence of the file and/or key with the name of the used environment:
    • true:
      • file should exist;
      • root key with :env name should exist (if via: :env_key is used);
      • raises Qonfig::ExposeError if file does not contain the required env key (if via: :env key is used);
      • raises Qonfig::FileNotFoundError if the required file does not exist;
    • false:
      • file is not required;
      • root key with :env name is not required (if via: :env_key is used);
  • :replace_on_merge - whether the setting should be replaced on the key conflict, otherwise, it will be deep merged (default);

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

  • :convert_values (false by default):
    • 't', 'T', 'true', 'TRUE' - covnerts to true;
    • 'f', 'F', 'false', 'FALSE' - covnerts to false;
    • 1, 23 and etc - converts to Integer;
    • 1.25, 0.26 and etc - converts to Float;
    • 1, 2, test, FALSE,Qonfig (strings without quotes that contains at least one comma) - converts to Array with recursively converted values;
    • '"please, test"', "'test, please'" (quoted strings) - converts to String without quotes;
  • :prefix - load ENV variables which names starts with a prefix:
    • nil (by default) - empty prefix;
    • Regexp - names that match the regexp pattern;
    • String - names which starts with a passed string;
  • :trim_prefix (false by default);
# 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__

  • aka load_from_self
  • :format - specify the format of data placed under the __END__ instruction:
    • format: :dynamic (default) - automatic format resolvation;
    • format: :yaml - YAML format;
    • format: :json - JSON format;
    • format: :toml - TOML format (via toml-plugin);
  • :replace_on_merge - whether the setting should be replaced on the key conflict, otherwise, it will be deep merged (default);
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__

  • aka expose_self;
  • works in expose_json and expose_yaml manner, but with __END__ instruction of the current file;
  • env: - your environment name (must be a type of String, Symbol or Numeric);
  • :format - specify the format of data placed under the __END__ instruction:
    • format: :dynamic (default) - automatic format resolvation;
    • format: :yaml - YAML format;
    • format: :json - JSON format;
    • format: :toml - TOML format (via toml-plugin);
  • :replace_on_merge - whether the setting should be replaced on the key conflict, otherwise, it will be deep merged (default);
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

  • defines a file that should be used for setting values initialization for your config object;
  • .values_file(file_path, format: :dynamic, strict: false, expose: nil)
    • file_path - full file path or :self (:self menas "load setting values from END data");
    • :format - defines the format of file (:dynamic means "try to automatically infer the file format") (:dynamic by default);
      • supports :yaml, :json, :toml (via Qonfig.plugin(:toml)), :dynamic (automatic format detection);
    • :strict - rerquires that file (or END-data) should exist (false by default);
    • :expose - what the environment-based subset of keys should be used (nil means "do not use any subset of keys") (nil by default);
  • extra keys that does not exist in your config will cause an exception Qonfig::SettingNotFound respectively;
  • initial values will be rewritten by values defined in your 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)

  • prvoides an ability to load predefined setting values from a yaml file;
  • #load_from_yaml(file_path, strict: true, expose: nil, &configurations)
    • file_path - full file path or :self (:self means "load setting values from END data");
    • :strict - rerquires that file (or END-data) should exist (true by default);
    • :expose - what the environment-based subset of keys should be used (nil means "do not use any subset of keys") (nil by default);
    • &configurations - do |config| ability :)

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)

  • prvoides an ability to load predefined setting values from a json file;
  • #load_from_json(file_path, strict: true, expose: nil, &configurations)
    • file_path - full file path or :self (:self means "load setting values from END data");
    • :strict - rerquires that file (or END-data) should exist (true by default);
    • :expose - what the environment-based subset of keys should be used (nil means "do not use any subset of keys") (nil by default);
    • &configurations - do |config| ability :)

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)

  • prvoides an ability to load predefined setting values from __END__ file section;
  • #load_from_self(strict: true, expose: nil, &configurations)
    • :format - defines the format of file (:dynamic means "try to automatically infer the file format") (:dynamic by default);
      • supports :yaml, :json, :toml (via Qonfig.plugin(:toml)), :dynamic (automatic format detection);
    • :strict - requires that END-data should exist (true by default);
    • :expose - what the environment-based subset of keys should be used (nil means "do not use any subset of keys") (nil by default);
    • &configurations - do |config| ability :)

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)

  • prvoides an ability to load predefined setting values from a file;
  • works in instance-based #load_from_yaml / #load_from_json / #load_from_self manner;
  • signature: #load_from_file(file_path, format: :dynamic, strict: true, expose: nil, &configurations):
    • file_path - full file path or :self (:self means "load setting values from END data");
    • :format - defines the format of file (:dynamic means "try to automatically infer the file format") (:dynamic by default);
      • supports :yaml, :json, :toml (via Qonfig.plugin(:toml)), :dynamic (automatic format detection);
    • :strict - rerquires that file (or END-data) should exist (true by default);
    • :expose - what the environment-based subset of keys should be used (nil means "do not use any subset of keys") (nil by default);
    • &configurations - do |config| ability :)
  • see examples for instance-based #load_from_yaml (doc) / #load_from_json (doc) / #load_from_self (doc);

Save to JSON file

  • #save_to_json - represents config object as a json structure and saves it to a file:
    • uses native ::JSON.generate under the hood;
    • writes new file (or rewrites existing file);
    • attributes:
      • :path - (required) - file path;
      • :options - (optional) - native ::JSON.generate options (from stdlib):
        • :indent - " " by default;
        • :space - " " by default/
        • :object_nl - "\n" by default;
      • &value_preprocessor - (optional) - value pre-processor;

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

  • #save_to_yaml - represents config object as a yaml structure and saves it to a file:
    • uses native ::Psych.dump under the hood;
    • writes new file (or rewrites existing file);
    • attributes:
      • :path - (required) - file path;
      • :options - (optional) - native ::Psych.dump options (from stdlib):
        • :indentation - 2 by default;
        • :line_width - -1 by default;
        • :canonical - false by default;
        • :header - false by default;
        • :symbolize_keys - (non-native option) - false by default;
      • &value_preprocessor - (optional) - value pre-processor;

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

  • toml (provides load_from_toml, save_to_toml, expose_toml);
  • pretty_print (beautified/prettified console output);
  • vault (provides load_from_vault, expose_vault)

Usage

  • show available plugins:
Qonfig.plugins # => ["pretty_print", "toml", ..., ...]
  • load specific plugin:
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')
  • show loaded plugins:
Qonfig.loaded_plugins # => ["pretty_print"]
# -- or --
Qonfig.enabled_plugins # => ["pretty_print"]

Plugins: toml

  • Qonfig.plugin(:toml)
  • adds support for toml format (specification);
  • depends on toml-rb gem (link) (tested on >= 2.0);
  • supports TOML 0.5.0 format (dependency lock) (toml-rb >= 2.0);
  • provides .load_from_toml (works in .load_from_yaml manner (doc));
  • provides .expose_toml (works in .expose_yaml manner (doc));
  • provides #save_to_toml (works in #save_to_yaml manner (doc)) (toml-rb has no native options);
  • provides format: :toml for .values_file (doc);
  • provides #load_from_toml (work in #load_from_yaml manner (doc));
# 1) require external dependency
require 'toml-rb'

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

# 3) use toml :)

Plugins: pretty_print

  • Qonfig.plugin(:pretty_print)
  • gives you really comfortable and beautiful console output;
  • represents all setting keys in dot-notation format;

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
  • before:
=> #<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
  • after:
=> #<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

  • Qonfig.plugin(:vault)
  • adds support for vault kv store, more info
  • depends on vault gem (link) (tested on >= 0.1);
  • provides .load_from_vault (works in .load_from_yaml manner (doc));
  • provides .expose_vault (works in .expose_yaml manner (doc));
# 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

  • General:
    • documentation rework;
  • Major:
    • support for Rails-like secrets;
    • support for persistent data storages (we want to store configs in multiple databases and files);
    • rails plugin;
    • support for pattern matching;
    • support for type checking (via rbs, typeprof, steep);
    • console utilities;
  • Minor:
    • An ability to flag Qonfig::Configurable's config object as compacted (Qonfig::Compacted);
    • Instance-based behavior for Vault plugin, also use instance of Vault client instead of Singleton;
    • External validation class with an importing api for better custom validations;
    • Setting value changement trace (in anyway_config manner);
    • Instantiation and reloading callbacks;
    • File geneartors (.rb-files with a pre-filled code (and (maybe) with a pre-generated yaml/json/etc files));
    • Setting value changement subscriptions and callbacks;

Build

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

Contributing

  • Fork it ( https://github.com/0exp/qonfig/fork )
  • Create your feature branch (git checkout -b feature/my-new-feature)
  • Commit your changes (git commit -am '[my-new-featre] Add some feature')
  • Push to the branch (git push origin feature/my-new-feature)
  • Create new Pull Request

License

Released under MIT License.

Authors

Rustam Ibragimov

qonfig's People

Contributors

0exp avatar akxcv avatar alexander-baz avatar anotherregulardude avatar nulldef avatar past-one avatar tycooon avatar vegetableprophet avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

qonfig's Issues

Import config options to a class instance as methods

For example:

class Config < Qonfig::DataSet
  setting :creds do
     setting :user, 'D@iVeR'
  end
  setting :api_enabled, true
end

AppConfig = Config.new

class Application
  include Qonfig::Importing
  # NOTE: EXPERIMENTAL API
  import_settings(AppConfig, 'creds.user', 'api_enabled')
end

# NOTE: instance methods mapped to config
app = Application.new
app.user # => 'D@iVeR'
app.api_enabled # => true

# NOTE: change config
AppConfig.settings.api_enabled
app.api_enabled # => false

[validation] add global interface for the custom global validators creation

For example:

Global validator:

Qonfig::Validator.add(:custom_text) do |value|
  value.is_a?(String) || value.is_a?(Symbol)
end

class Config < Qonfig::DataSet
  setting :que_adapter
  validate :que_adapter, :custom_text # usage of new created validator
end

Special validator (for the concrete config class only):

class Config < Qonfig::DataSet
end

Config.define_validator(:custom_text) do |value|
  value.is_a?(String)
end

class Config
   setting :super_text
   validate :super_text, :custom_text # cool!
end

class AnotherQonfig < Qonfig::DataSet
  setting :api_token
  validate :super_text, :custom_text # Qonfig::ValidatorArgumentError (nonexistent validator)
end

[valiation] Non-strict validation

  • rename strict: to ignore_nil:;
  • do not fail if config is invalid;
  • strict: should say fail or not;
  • validate! should return the list of validation errors;

[DSL/Instance-level] Settings file for loading setting values during instantiation and working with an instance

Example:

configs.yml

adapter: "sidekiq"

another_configs.yml

adapter: "cheto_drugoe"

class Config < Qonfig::DatSet
   settings_file "configs.yaml" # or .options_file

   setting :adapter
end

config = Config.new
config.adapter # => "sidekiq" (from "configs.yml)

Config.settings_file = "another_configs.yml"

config = Config.new
config.adapter # => "cheto_drugoe"

config = Config.new("configs.yml")
config.adapter # => "sidekiq"

config.settings_file = "another_configs.yml" # instance-level attribute (not a class-level)
config.reload!
config.adapter # => "cheto_drugoe"

Think about it ๐Ÿค”

[pretty-print] Issues with pretty_print plugin

Here is the example script:

require "qonfig"
require "pp"
require "set"

Qonfig.plugin(:pretty_print)

class Config < Qonfig::DataSet
  setting :ns do
    setting "foo.bar"
  end
end

pp Config.new # => Setting with <foo> key does not exist! (Qonfig::UnknownSettingError)
  1. A dot in namespaced setting key breaks the pretty_print.
  2. I have to manually require pp and set std libs which is not very handy.

#with(configurations = {}, &code) - An ability to run arbitary code with temporary settings

For example:

class Config < Qonfig::DataSet
  setting :api_enabled, true
  setting :token, 'test123'
end

config = Config.new
config.settings.api_enabled # => true

config.with(api_enabled: false) do
  config.settings.api_enabled # => false (changed value)
  config.settings.token = 'another_token'
  config.settings.token # => 'another_token' (changed value)
end

config.settings.api_enabled # => true (original value)
config.settings.token # => 'test23' (original value)

Requirements:

  • should be fully thread safe;

[conditional_expose] An ability to conditaionally expose values from files

For example:

class Config < Qonfig::DataSet
  setting :kek, expose_if: -> { some_code }
  # or
  setting :kek, expose_if: :some_method_name
  # ---
  values_file 'kek.yml' # or load_from_file (or yaml or json or etc)
end

# ---
## true for expose_if
config.kek # => '123' (some value from a kek.yml file)
## false for expose_if
config.kek # => nil (empty value)

Api review

Config composition

It is nice and flexible feature, but multiple inheritance (when you compose multiple configs inside one setting / root) is not a good pattern.

I suggest to deprecate compose api untill gem widely used and make mount-style api instead. I.e.

class Config < Qonfig::DataSet
  setting :foo, :bar
  setting :baz, MyOtherConfig
  # or mount :baz, MyOtherConfig
  # or compose :baz, MyOtherConfig
end

.freeze!

Use ::FrozenError as superclas for Qonfig::FrozenSettingsError or remove Qonfig::FrozenSettingsError

Nested config validation and configure not raises error

Summary

If we use methods like validate_with and configure with nested deep setting structure and custom validation methods, validation methods gets old settings payload and always returns true

Code

class FlatConfig < Qonfig::DataSet
  expose_yaml Rails.root.join('tmp/flat_config.yml'), via: :env_key, env: :default

  validate by: :some_validation_method

  def some_validation_method
    puts "try to validate settings.some_key = #{settings.some_key}"
    settings.some_key != 'bad'
  end
end

class NestedDeepConfig < Qonfig::DataSet
  expose_yaml Rails.root.join('tmp/deep_config.yml'), via: :env_key, env: :default

  validate by: :some_validation_method

  def some_validation_method
    puts "try to validate settings.some_key.some_key2 = #{settings.some_key.some_key2}"
    settings.some_key.some_key2 != 'bad' # HERE we have settings.some_key.some_key2 = good
  end
end
end

example 1 (flat)

    config = FlatConfig.new
    invalid_payload = {
      some_key: 'bad',
    }

    puts "valid_with? = #{config.valid_with?(invalid_payload)}"
    begin
      puts 'config.configure(invalid_payload)'
      config.configure(invalid_payload)
      puts 'Hello invalid config =('
      puts config.settings.some_key.to_s
    rescue StandardError => error
      puts error
    end

example 2 (nested)

    config = NestedDeepConfig.new
    invalid_payload = {
      some_key: {
        some_key2: 'bad',
      },
    }
    puts "valid_with? = #{config.valid_with?(invalid_payload)}"
    begin
      puts 'config.configure(invalid_payload)'
      config.configure(invalid_payload)
      puts 'Hello invalid config =('
      puts config.settings.some_key.some_key2.to_s
    rescue StandardError => error
      puts error
    end

tmp/flat_config.yml

default:
  some_key: 'good'

tmp/deep_config.yml

default:
  some_key:
    some_key2: 'good'

Expected behavior

  • example 1 (flat)
    • valid_with? = false
    • configure(invalid_payload) - raises error
  • example 2 (nested)
    • valid_with? = false
    • configure(invalid_payload) - raises error

Current behavior

  • example 1 (flat)
    • valid_with? = false
    • configure(invalid_payload) - raises error
  • example 2 (nested)
    • valid_with? = true
    • configure(invalid_payload) - no error raises =(

[instance] Setting key aliases

SUBJ

PoC:

class Config < Qonfig::DataSet
  setting :a, 22, as: :b # as: [:b, 'c', :d]
end

config = Config.new

config.a # => 22
config.b # => 22
config.c # => 22
config.d # => 22

config[:a] = 45
cofig.b # => 45
# and etc

[validation] Custom error messages

validate :some_option, :integer, error_message: 'Some error message'
config.settings.some_option = "text" # => Qonfig::ValidationError "Some error message"

[files] File Watchers

PoC: Change file -> refresh config instance :O

  • refresh only changed keys (yes)
  • affect only values_file? (dunno)
  • affect expose/load_from too? i think no

[freature] sequentional settings file merge

Imagine we have the following code:

    load_from_yaml root.join("config/redis/default.yml")
    load_from_yaml root.join("config/redis/#{env}.yml"), strict: false

may bee it would be nice to have this for file with the same nature/type/domain:

load_from_yaml "file1", "file2", "file3", "file4"

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.