README.md in qonfig-0.12.0 vs README.md in qonfig-0.13.0
- old
+ new
@@ -1,9 +1,10 @@
# Qonfig · [![Gem Version](https://badge.fury.io/rb/qonfig.svg)](https://badge.fury.io/rb/qonfig) [![Build Status](https://travis-ci.org/0exp/qonfig.svg?branch=master)](https://travis-ci.org/0exp/qonfig) [![Coverage Status](https://coveralls.io/repos/github/0exp/qonfig/badge.svg?branch=master)](https://coveralls.io/github/0exp/qonfig?branch=master)
Config. Defined as a class. Used as an instance. Support for inheritance and composition.
-Lazy instantiation. Thread-safe. Command-style DSL. Extremely simple to define. Extremely simple to use. That's all.
+Lazy instantiation. Thread-safe. Command-style DSL. Validation layer. Support for **YAML**, **TOML**, **JSON**, **\_\_END\_\_**, **ENV**.
+Extremely simple to define. Extremely simple to use. That's all? **NOT** :)
## Installation
```ruby
gem 'qonfig'
@@ -19,31 +20,42 @@
require 'qonfig'
```
## Usage
-- [Definition and Settings Access](#definition-and-access)
-- [Configuration](#configuration)
-- [Inheritance](#inheritance)
-- [Composition](#composition)
-- [Hash representation](#hash-representation)
-- [Config reloading](#config-reloading) (reload config definitions and option values)
-- [Clear options](#clear-options) (set to nil)
-- [State freeze](#state-freeze)
-- [Settings as Predicates](#settings-as-predicates)
-- [Load from YAML file](#load-from-yaml-file)
-- [Expose YAML](#expose-yaml) (`Rails`-like environment-based YAML configs)
-- [Load from JSON file](#load-from-json-file)
-- [Load from ENV](#load-from-env)
-- [Load from \_\_END\_\_](#load-from-__end__) (aka `load_from_self`)
-- [Save to JSON file](#save-to-json-file) (`save_to_json`)
-- [Save to YAML file](#save-to-yaml-file) (`save_to_yaml`)
-- [Smart Mixin](#smart-mixin) (`Qonfig::Configurable`)
+- [Definition](#definition)
+ - [Definition and Settings Access](#definition-and-access)
+ - [Configuration](#configuration)
+ - [Inheritance](#inheritance)
+ - [Composition](#composition)
+ - [Hash representation](#hash-representation)
+ - [Smart Mixin](#smart-mixin) (`Qonfig::Configurable`)
+- [Interaction](#interaction)
+ - [Iteration over setting keys](#iteration-over-setting-keys) (`#each_setting`, `#deep_each_setting`)
+ - [Config reloading](#config-reloading) (reload config definitions and option values)
+ - [Clear options](#clear-options) (set to nil)
+ - [State freeze](#state-freeze)
+ - [Settings as Predicates](#settings-as-predicates)
+- [Validation](#validation)
+ - [Key search pattern](#key-search-pattern)
+ - [Proc-based validation](#proc-based-validation)
+ - [Method-based validation](#method-based-validation)
+ - [Predefined validations](#predefined-validations)
+- [Work with files](#work-with-files)
+ - [Load from YAML file](#load-from-yaml-file)
+ - [Expose YAML](#expose-yaml) (`Rails`-like environment-based YAML configs)
+ - [Load from JSON file](#load-from-json-file)
+ - [Load from ENV](#load-from-env)
+ - [Load from \_\_END\_\_](#load-from-__end__) (aka `load_from_self`)
+ - [Save to JSON file](#save-to-json-file) (`save_to_json`)
+ - [Save to YAML file](#save-to-yaml-file) (`save_to_yaml`)
- [Plugins](#plugins)
- - [toml](#plugin-toml) (provides `load_from_toml`, `save_to_toml`, `expose_toml`)
+ - [toml](#plugins-toml) (provides `load_from_toml`, `save_to_toml`, `expose_toml`)
---
+## Definition
+
### Definition and Access
```ruby
# --- definition ---
class Config < Qonfig::DataSet
@@ -272,10 +284,168 @@
}
```
---
+### 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;
+
+```ruby
+# --- 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)
+```
+
+```ruby
+# --- 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)
+```
+
+---
+
+
+## Interaction
+
+---
+
+### Iteration over setting keys
+
+- `#each_setting { |key, value| }`
+ - iterates over the root setting keys;
+- `#deep_each_setting { |key, value| }`
+ - iterates over all setting keys (deep inside);
+ - key object is represented as a string of `.`-joined keys;
+
+```ruby
+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
+
+# 1. #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' }
+
+# 2. #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' }
+```
+
+---
+
### Config reloading
```ruby
class Config < Qonfig::DataSet
setting :db do
@@ -421,10 +591,268 @@
config.settings.database.engine.driver? # => true (true => true)
```
---
+## Validation
+
+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](#key-search-patern)).
+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**:
+
+- is invoked on any mutation of any setting key
+ - during dataset instantiation;
+ - when assigning new values;
+ - when calling `#reload!`;
+ - when calling `#clear!`;
+
+- provides special [key search pattern](#key-search-pattern) for matching setting key names;
+- uses the [key search pattern](#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** and **dataset-method-based**)
+ - **proc-based** (`setting validation`)
+ ```ruby
+ validate 'db.user' do |value|
+ value.is_a?(String)
+ end
+ ```
+ - **proc-based** (`dataset validation`)
+ ```ruby
+ validate do
+ settings.user == User[1]
+ end
+ ```
+ - **dataset-method-based** (`setting validation`)
+ ```ruby
+ validate 'db.user', by: :check_user
+
+ def check_user(value)
+ value.is_a?(String)
+ end
+ ```
+ - **dataset-method-based** (`dataset validation`)
+ ```ruby
+ validate by: :check_config
+
+ def check_config
+ settings.user == User[1]
+ end
+ ```
+
+- provides a set of standard validations:
+ - `integer`
+ - `float`
+ - `numeric`
+ - `big_decimal`
+ - `boolean`
+ - `string`
+ - `symbol`
+ - `text` (string or symbol)
+ - `array`
+ - `hash`
+ - `proc`
+ - `class`
+ - `module`
+ - `not_nil`
+
+---
+
+### 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;
+- 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`
+
+```ruby
+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
+
+ # 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
+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`)
+```
+
+---
+
+### Method-based validation
+
+- method should return truthy value or falsy value;
+- 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`
+
+```ruby
+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
+
+ # 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
+
+ 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
+end
+
+config = Config.new
+
+config.settings.counts.google = 0 # => Qonfig::ValidationError (< 0)
+config.settings.counts.rambler = nil # => Qonfig::ValidationError (should be a numeric)
+config.settings.minimals.google = -1 # => Qonfig::ValidationError (< 0)
+config.settings.minimals.rambler = 'no' # => Qonfig::ValidationError (should be a numeric)
+config.settings.enabled = nil # => Qonfig::ValidationError (should be a boolean)
+```
+
+---
+
+### Predefined validations
+
+- DSL: `validate 'key.pattern', :predefned_validator`
+- predefined validators:
+ - `:not_nil`
+ - `:integer`
+ - `:float`
+ - `:numeric`
+ - `:big_decimal`
+ - `:array`
+ - `:hash`
+ - `:string`
+ - `:symbol`
+ - `:text` (`string` or `symbol`)
+ - `:boolean`
+ - `:class`
+ - `:module`
+ - `:proc`
+
+```ruby
+class Config < Qonfig::DataSet
+ setting :user
+ setting :password
+
+ setting :service do
+ setting :provider
+ setting :protocol
+ 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)
+```
+
+---
+
+## 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`;
@@ -920,120 +1348,10 @@
dynamic: 10
```
---
-### 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;
-
-```ruby
-# --- 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)
-```
-
-```ruby
-# --- 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)
-```
-
----
-
### Plugins
```ruby
# --- show names of registered plugins ---
Qonfig.plugins # => array of strings
@@ -1045,26 +1363,28 @@
---
### Plugins: toml
- adds support for `toml` format ([specification](https://github.com/toml-lang/toml));
-- depends on `toml-rb` gem;
-- provides `load_from_toml` (works in `load_from_yaml` manner [doc](#load-from-yaml-file));
-- provides `save_to_toml` (works in `save_to_yaml` manner [doc](#save-to-yaml-file)) (`toml-rb` has no native options);
-- provides `expose_toml` (works in `expose_yaml` manner [doc](#expose-yaml));
+- depends on `toml-rb` gem ([link](https://github.com/emancu/toml-rb));
+- supports TOML `0.4.0` format (dependency lock);
+- provides `load_from_toml` (works in `load_from_yaml` manner ([doc](#load-from-yaml-file)));
+- provides `save_to_toml` (works in `save_to_yaml` manner ([doc](#save-to-yaml-file))) (`toml-rb` has no native options);
+- provides `expose_toml` (works in `expose_yaml` manner ([doc](#expose-yaml)));
```ruby
+# 1) require external dependency
require 'toml-rb'
+
+# 2) enable plugin
Qonfig.plugin(:toml)
-# and use :)
+
+# 3) use :)
```
---
## Roadmap
-- support for TOML format;
-- explicit "settings" object;
-- validation layer;
- distributed configuration server;
- support for Rails-like secrets;
## Contributing