README.md in qonfig-0.12.0 vs README.md in qonfig-0.13.0

- old
+ new

@@ -1,9 +1,10 @@ # Qonfig &middot; [![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