Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +6 -2
  3. data/.jrubyrc +1 -0
  4. data/.rspec +1 -1
  5. data/.rubocop.yml +15 -0
  6. data/.travis.yml +43 -4
  7. data/CHANGELOG.md +121 -0
  8. data/Gemfile +4 -2
  9. data/LICENSE.txt +1 -1
  10. data/README.md +1060 -19
  11. data/Rakefile +18 -4
  12. data/bin/console +5 -11
  13. data/bin/rspec +55 -0
  14. data/bin/setup +1 -0
  15. data/gemfiles/with_external_deps.gemfile +8 -0
  16. data/gemfiles/without_external_deps.gemfile +5 -0
  17. data/lib/qonfig.rb +22 -2
  18. data/lib/qonfig/command_set.rb +67 -0
  19. data/lib/qonfig/commands.rb +15 -0
  20. data/lib/qonfig/commands/add_nested_option.rb +45 -0
  21. data/lib/qonfig/commands/add_option.rb +41 -0
  22. data/lib/qonfig/commands/base.rb +12 -0
  23. data/lib/qonfig/commands/compose.rb +37 -0
  24. data/lib/qonfig/commands/expose_yaml.rb +159 -0
  25. data/lib/qonfig/commands/load_from_env.rb +95 -0
  26. data/lib/qonfig/commands/load_from_env/value_converter.rb +84 -0
  27. data/lib/qonfig/commands/load_from_json.rb +56 -0
  28. data/lib/qonfig/commands/load_from_self.rb +73 -0
  29. data/lib/qonfig/commands/load_from_yaml.rb +58 -0
  30. data/lib/qonfig/configurable.rb +116 -0
  31. data/lib/qonfig/data_set.rb +213 -0
  32. data/lib/qonfig/data_set/class_builder.rb +27 -0
  33. data/lib/qonfig/data_set/validator.rb +7 -0
  34. data/lib/qonfig/dsl.rb +122 -0
  35. data/lib/qonfig/errors.rb +111 -0
  36. data/lib/qonfig/loaders.rb +9 -0
  37. data/lib/qonfig/loaders/basic.rb +38 -0
  38. data/lib/qonfig/loaders/json.rb +24 -0
  39. data/lib/qonfig/loaders/yaml.rb +24 -0
  40. data/lib/qonfig/plugins.rb +65 -0
  41. data/lib/qonfig/plugins/abstract.rb +13 -0
  42. data/lib/qonfig/plugins/access_mixin.rb +38 -0
  43. data/lib/qonfig/plugins/registry.rb +125 -0
  44. data/lib/qonfig/plugins/toml.rb +26 -0
  45. data/lib/qonfig/plugins/toml/commands/expose_toml.rb +146 -0
  46. data/lib/qonfig/plugins/toml/commands/load_from_toml.rb +49 -0
  47. data/lib/qonfig/plugins/toml/data_set.rb +19 -0
  48. data/lib/qonfig/plugins/toml/dsl.rb +27 -0
  49. data/lib/qonfig/plugins/toml/loaders/toml.rb +24 -0
  50. data/lib/qonfig/plugins/toml/tomlrb_fixes.rb +92 -0
  51. data/lib/qonfig/plugins/toml/uploaders/toml.rb +25 -0
  52. data/lib/qonfig/settings.rb +457 -0
  53. data/lib/qonfig/settings/builder.rb +18 -0
  54. data/lib/qonfig/settings/key_guard.rb +71 -0
  55. data/lib/qonfig/settings/lock.rb +60 -0
  56. data/lib/qonfig/uploaders.rb +10 -0
  57. data/lib/qonfig/uploaders/base.rb +18 -0
  58. data/lib/qonfig/uploaders/file.rb +55 -0
  59. data/lib/qonfig/uploaders/json.rb +35 -0
  60. data/lib/qonfig/uploaders/yaml.rb +93 -0
  61. data/lib/qonfig/version.rb +7 -1
  62. data/qonfig.gemspec +29 -17
  63. metadata +122 -16
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
---
2
2
SHA256:
3
- metadata.gz: 640c084d4fd0dc167cfb9b52d12ae4d2f70382ec4d2f576703fc8be3a748fc08
4
- data.tar.gz: a36a0d612d46c542e9e20609a57c6fcf9ea244fce30fdbf295af8bc130ac9223
3
+ metadata.gz: 62b74a92c4c74b79f222e029726296a63b51ba3a57886a00911a34ffbcbced25
4
+ data.tar.gz: 33fa91631b6554dec3547c9c31faddf805bc4a56becbe7042e1942c19c493f16
5
5
SHA512:
6
- metadata.gz: 3922f86f65e5d9486074c9ca425821a1d6d6ccb8e9655089c20e95696ece61a99a00e18aa19078b6fa71f069acad5558dcf3a622944c2b72348996774a8e79fc
7
- data.tar.gz: f71c819eafab31c35d1faa5cfeaa6a28c9fcee4898d2dc492385391c04e4c50c7376ca2f5a45560d86b25fa98bfab486122f970d9d86c70c77b318a98ab79ccb
6
+ metadata.gz: 152acb89e834fccaaed1b4bfae379f8263d795b2e4e7b7913f6a2b98fd1bfebf984dec1c82a3568ddc08a3e16fdec2be7783f1f583ef09a3dc6b1386f9afb18f
7
+ data.tar.gz: c468d5a5c289dc8b4b7b337e62314dbbf9d388b5dd6df1248a14e957a97be9937d7f24c47aa8634ffe415dbd092ed4f3c5b447036770b171fb6d87454eceb4e8
data/.gitignore CHANGED
@@ -6,6 +6,10 @@
6
6
/pkg/
7
7
/spec/reports/
8
8
/tmp/
9
-
10
- # rspec failure tracking
11
9
.rspec_status
10
+ Gemfile.lock
11
+ /.idea
12
+ .ruby-version
13
+ /.vscode/
14
+ /spec/artifacts/
15
+ /gemfiles/*.gemfile.lock
data/.jrubyrc ADDED
@@ -0,0 +1 @@
1
+ debug.fullTrace=true
data/.rspec CHANGED
@@ -1,3 +1,3 @@
1
- --format documentation
2
1
--color
2
+ --format progress
3
3
--require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,15 @@
1
+ inherit_gem:
2
+ armitage-rubocop:
3
+ - lib/rubocop.general.yml
4
+ - lib/rubocop.rspec.yml
5
+
6
+ AllCops:
7
+ TargetRubyVersion: 2.6.3
8
+ Include:
9
+ - lib/**/*.rb
10
+ - spec/**/*.rb
11
+ - Gemfile
12
+ - Rakefile
13
+ - qonfig.gemspec
14
+ - bin/console
15
+ - bin/rspec
data/.travis.yml CHANGED
@@ -1,5 +1,44 @@
1
- sudo: false
2
1
language: ruby
3
- rvm:
4
- - 2.5.1
5
- before_install: gem install bundler -v 1.16.1
2
+ matrix:
3
+ fast_finish: true
4
+ include:
5
+ - rvm: 2.3.8
6
+ gemfile: gemfiles/with_external_deps.gemfile
7
+ env: TEST_PLUGINS=true
8
+ - rvm: 2.4.6
9
+ gemfile: gemfiles/with_external_deps.gemfile
10
+ env: TEST_PLUGINS=true
11
+ - rvm: 2.5.5
12
+ gemfile: gemfiles/with_external_deps.gemfile
13
+ env: TEST_PLUGINS=true
14
+ - rvm: 2.6.3
15
+ gemfile: gemfiles/with_external_deps.gemfile
16
+ env: TEST_PLUGINS=true
17
+ - rvm: ruby-head
18
+ gemfile: gemfiles/with_external_deps.gemfile
19
+ env: TEST_PLUGINS=true
20
+ - rvm: jruby-head
21
+ gemfile: gemfiles/with_external_deps.gemfile
22
+ env: TEST_PLUGINS=true
23
+ - rvm: 2.3.8
24
+ gemfile: gemfiles/without_external_deps.gemfile
25
+ - rvm: 2.4.6
26
+ gemfile: gemfiles/without_external_deps.gemfile
27
+ - rvm: 2.5.5
28
+ gemfile: gemfiles/without_external_deps.gemfile
29
+ - rvm: 2.6.3
30
+ gemfile: gemfiles/without_external_deps.gemfile
31
+ - rvm: ruby-head
32
+ gemfile: gemfiles/without_external_deps.gemfile
33
+ - rvm: jruby-head
34
+ gemfile: gemfiles/without_external_deps.gemfile
35
+ allow_failures:
36
+ - rvm: ruby-head
37
+ - rvm: jruby-head
38
+ sudo: false
39
+ cache: bundler
40
+ before_install: gem install bundler
41
+ script: bundle exec rspec
42
+ notifications:
43
+ slack:
44
+ secure: I03SDv+IrKy3IkeGjjHUJ9VneFMiYpLzIgPOixGFO5zGVXgulwmun82KsrPSW5HkK1UEQCahfoXt1hNECPwxcdsY01q6LBYJQx1jD0q17bXHllN/q0C6lx3peRN0KqNAAOU3/mHZxLt3HFV+N3HsSnoxDMuSYJgKbjCL/QVG2L2UYT9vi+JRNM/thj8R6MWKWlOHemik40GbLr2anCOCqiALzxnJzh5nJyGj+9SGvjhHfQq/fAIrg1+Scu+UchG8d+1yS5JWsGTt1/JF08i+Ux4ceTQ7GNBNeA5cj/xuUzvRx6A85renxyTiZMKIL0+jeceUm8c+/46XFcq0F7/kJB36lwjFhX1JRphcu/VouRdEwW/BvH74wnyHtygqOpk0LH4shp7A1DIS1DlnBbeJxrR5hdMDnmV85kTU6w6H0BbncsYY/pH1fji1kgH6jCsdwqDlq4wLB8x8I+eZABBsfb/r55z/HnBmmxlvR7Rt3X5/yR3gJrsgRrDBBZ5LwYy1RSCDu76vMQqWGJKG03FdfqSNqmC5V4MC5Ez8yjGDW0Yle09mwvsL2c6fDXyDPCeB/gwNk+FvgLRXnv7C4BaBNEoG4JnCL/dwauoJNg0lB6uF34BEmYAhZIrdY1CI6BqPWnaVMJfcJSYekBVXDIELDnTFRWjaGU1y8dM6lNzDmYE=
data/CHANGELOG.md ADDED
@@ -0,0 +1,121 @@
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ ## [0.12.0] - 2019-07-19
5
+ ### Added
6
+ - Support for **TOML** (`.toml`) format
7
+ - realized as a plugin (`Qonfig.plugin(:toml)`);
8
+ - provides `#save_to_toml`, `#load_from_toml`, `#expose_toml` methods and works in `#*_yaml`-like manner);
9
+ - Custom `bin/rspec` command:
10
+ - `bin/rspec -n` - run tests without plugin tests;
11
+ - `bin/rspec -w` - run all tests;
12
+ - Added more convinient aliases for `Qonfig::DataSet` instances:
13
+ - `#save_to_yaml` => `#dump_to_yaml`;
14
+ - `#save_to_json` => `#dump_to_json`;
15
+ - `#save_to_toml` => `#dump_to_toml`;
16
+ ### Changed
17
+ - Actualized development dependencies;
18
+
19
+ ## [0.11.0] - 2019-05-15
20
+ ### Added
21
+ - `#save_to_json` - save configurations to a json file (uses native `::JSON.generate` under the hood);
22
+ - `#save_to_yaml` - save configurations to a yaml file (uses native `::Psych.dump` under the hood);
23
+
24
+ ### Changed
25
+ - new `#to_h` signature: `#to_h(key_transformer:, value_transformer:)`
26
+ - `:key_transformer` - proc object used for key pre-processing (`-> (key) { key }` by default);
27
+ - `:value_transformer` - proc object used for value pre-processing (`-> (value) { value }` by default);
28
+
29
+ ## [0.10.0] - 2019-02-26
30
+ ### Added
31
+ - `#slice_value` - get a slice of config options as a hash set and fetch the required value using the given key set;
32
+
33
+ ## [0.9.0] - 2018-11-28
34
+ ### Added
35
+ - `#slice` - get a slice of config options as a hash set (works in a `#dig` manner);
36
+
37
+ ## [0.8.0] - 2018-11-21
38
+ ### Changed
39
+ - `expose_yaml`, `load_from_yaml`, `load_from_json` and `load_from_self` treats empty hash (`{}`)
40
+ as an option with empty hash value (previously treated as a nested setting without options);
41
+
42
+ ## [0.7.0] - 2018-10-20
43
+ ### Added
44
+ - `expose_yaml` - a command that provides an ability to define config settings
45
+ by loading them from a yaml file where the concrete settings depends on the chosen environment;
46
+
47
+ ## [0.6.0] - 2018-08-22
48
+ ### Added
49
+ - `#shared_config` - instance method that provides an access to the class level config
50
+ object from `Qonfig::Configurable` instances;
51
+
52
+ ## [0.5.0] - 2018-07-27
53
+ ### Added
54
+ - `load_from_json`- a command that provides an ability to define config settings
55
+ by loading them from a json file (in `load_from_yaml` manner);
56
+
57
+ ### Changed
58
+ - Support for Ruby 2.2 has ended;
59
+
60
+ ## [0.4.0] - 2018-06-24
61
+ ### Added
62
+ - Introduce Plugin Ecosystem (`Qonfig::Plugins`):
63
+ - load plugin: `Qonfig.plugin('plugin_name')` or `Qonfig.plugin(:plugin_name)`;
64
+ - get registered plugins: `Qonfig.plugins #=> array of strings`
65
+
66
+ ## [0.3.0] - 2018-06-13
67
+ ### Added
68
+ - Improved configuration process: `#configure` can take a hash as a configuration `[option key => option]`
69
+ map of values;
70
+
71
+ ### Changed
72
+ - `#clear!` causes `Qonfig::FrozenSettingsError` if config object is frozen;
73
+
74
+ ## [0.2.0] - 2018-06-07
75
+ ### Added
76
+ - Instant configuration via block `config = Config.new { |conf| <<your configuration code>> }`;
77
+ - `.load_from_env` command - an ability to define config settings by loading them from ENV variable;
78
+ - `.load_from_yaml` command - an ability to define config settings by loading them from a yaml file;
79
+ - `.load_from_self` command - an ability to load config definitions form the YAML
80
+ instructions written in the file where the config class is defined (`__END__` section);
81
+ - `#reload!` - an ability to reload config isntance after any config class changes and updates;
82
+ - `#clear!` - an ability to set all options to `nil`;
83
+ - `#dig` - an ability to fetch setting values in `Hash#dig` manner
84
+ (fails with `Qonfig::UnknownSettingError` when the required key does not exist);
85
+ - Settings as Predicates - an ability to check the boolean nature of the config setting by appending
86
+ the question mark symbol (`?`) at the end of setting name:
87
+ - `nil` and `false` setting values indicates `false`;
88
+ - other setting values indicates `true`;
89
+ - setting roots always returns `true`;
90
+ - examples:
91
+ - `config.settings.database.user # => nil`;
92
+ - `config.settings.database.user? # => false`;
93
+ - `config.settings.database.host # => 'google.com'`;
94
+ - `config.settings.database.host? # => true`;
95
+ - `config.settings.database? # => true (setting with nested option (setting root))`
96
+ - Support for ERB instructions in YAML;
97
+ - Support for `HashWithIndifferentAccess`-like behaviour;
98
+ - `Qonfig::Settings` instance method redefinition protection: the setting key can not
99
+ have a name that matches an any instance method name of `Qonfig::Settings`;
100
+ - Added `Qonfig::Configurable` mixin - configuration behaviour for any classes and modules
101
+ and their instances:
102
+ - all `Qonfig`-related features;
103
+ - different class-level and instance-level config objects;
104
+ - working class-level inheritance :);
105
+ - Full thread-safe implementation;
106
+
107
+ ### Changed
108
+ - Superclass of `Qonfig::FrozenSettingsError` (it was `Qonfig::Error` before):
109
+ - `ruby >= 2.5` - inherited from `::FrozenError`;
110
+ - `ruby < 2.5` - inherited from `::RuntimeError`;
111
+ - `.setting` will raise exceptions immediately:
112
+ - `.setting(key, ...) { ... }` - if setting key has incompatible type;
113
+ - `.compose(config_class)`- if composed config class is not a subtype of `Qonfig::DataSet`;
114
+
115
+ ### Fixed
116
+ - Recoursive hash representation with deep nested `Qonfig::Settings` values (infinite loop);
117
+ - Fixed re-assignment of the options with nested options (losing the nested options
118
+ due to the instance configuration). Now it causes `Qonfig::AmbigousSettingValueError`.
119
+
120
+ ## [0.1.0] - 2018-05-18
121
+ - Release :)
data/Gemfile CHANGED
@@ -1,6 +1,8 @@
1
- source "https://rubygems.org"
1
+ # frozen_string_literal: true
2
2
3
- git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
3
+ source 'https://rubygems.org'
4
+
5
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
4
6
5
7
# Specify your gem's dependencies in qonfig.gemspec
6
8
gemspec
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
The MIT License (MIT)
2
2
3
- Copyright (c) 2018 Rustam Ibragimov
3
+ Copyright (c) 2018-2019 Rustam Ibragimov
4
4
5
5
Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,43 +1,1084 @@
1
- # Qonfig
1
+ # 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)
2
2
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/qonfig`. To experiment with that code, run `bin/console` for an interactive prompt.
4
-
3
+ Config. Defined as a class. Used as an instance. Support for inheritance and composition.
4
+ Lazy instantiation. Thread-safe. Command-style DSL. Extremely simple to define. Extremely simple to use. That's all.
5
- TODO: Delete this and the text above, and describe your gem
6
5
7
6
## Installation
8
7
9
- Add this line to your application's Gemfile:
10
-
11
8
```ruby
12
9
gem 'qonfig'
13
10
```
14
11
15
- And then execute:
12
+ ```shell
13
+ $ bundle install
14
+ # --- or ---
15
+ $ gem install 'qonfig'
16
+ ```
17
+
18
+ ```ruby
19
+ require 'qonfig'
20
+ ```
21
+
22
+ ## Usage
16
23
17
- $ bundle
24
+ - [Definition and Settings Access](#definition-and-access)
25
+ - [Configuration](#configuration)
26
+ - [Inheritance](#inheritance)
27
+ - [Composition](#composition)
28
+ - [Hash representation](#hash-representation)
29
+ - [Config reloading](#config-reloading) (reload config definitions and option values)
30
+ - [Clear options](#clear-options) (set to nil)
31
+ - [State freeze](#state-freeze)
32
+ - [Settings as Predicates](#settings-as-predicates)
33
+ - [Load from YAML file](#load-from-yaml-file)
34
+ - [Expose YAML](#expose-yaml) (`Rails`-like environment-based YAML configs)
35
+ - [Load from JSON file](#load-from-json-file)
36
+ - [Load from ENV](#load-from-env)
37
+ - [Load from \_\_END\_\_](#load-from-__end__) (aka `load_from_self`)
38
+ - [Save to JSON file](#save-to-json-file) (`save_to_json`)
39
+ - [Save to YAML file](#save-to-yaml-file) (`save_to_yaml`)
40
+ - [Smart Mixin](#smart-mixin) (`Qonfig::Configurable`)
41
+ - [Plugins](#plugins)
42
+ - [toml](#plugin-toml) (provides `load_from_toml`, `save_to_toml`, `expose_toml`)
43
+ ---
18
44
19
- Or install it yourself as:
45
+ ### Definition and Access
20
46
21
- $ gem install qonfig
47
+ ```ruby
48
+ # --- definition ---
49
+ class Config < Qonfig::DataSet
50
+ # nil by default
51
+ setting :project_id
22
52
23
- ## Usage
53
+ # nested setting
54
+ setting :vendor_api do
55
+ setting :host, 'app.service.com'
56
+ end
57
+
58
+ setting :enable_graphql, false
59
+
60
+ # nested setting reopening
61
+ setting :vendor_api do
62
+ setting :user, 'test_user'
63
+ end
64
+ end
65
+
66
+ config = Config.new # your configuration object instance
67
+
68
+ # --- setting access ---
69
+
70
+ # get option value via method
71
+ config.settings.project_id # => nil
72
+ config.settings.vendor_api.host # => 'app.service.com'
73
+ config.settings.vendor_api.user # => 'test_user'
74
+ config.settings.enable_graphql # => false
75
+
76
+ # get option value via index (with indifferent (string / symbol / mixed) access)
77
+ config.settings[:project_id] # => nil
78
+ config.settings[:vendor_api][:host] # => 'app.service.com'
79
+ config.settings[:vendor_api][:user] # => 'test_user'
80
+ config.settings[:enable_graphql] # => false
81
+
82
+ # get option value via index (with indifferent (string / symbol / mixed) access)
83
+ config.settings['project_id'] # => nil
84
+ config.settings['vendor_api']['host'] # => 'app.service.com'
85
+ config.settings['vendor_api']['user'] # => 'test_user'
86
+ config.settings['enable_graphql'] # => false
87
+
88
+ # get option value directly via index (with indifferent access)
89
+ config['project_id'] # => nil
90
+ config['enable_graphql'] # => false
91
+ config[:project_id] # => nil
92
+ config[:enable_graphql] # => false
93
+
94
+ # get option value in Hash#dig manner (and fail when the required key does not exist)
95
+ config.dig(:vendor_api, :host) # => 'app.service.com' # (key exists)
96
+ config.dig(:vendor_api, :port) # => Qonfig::UnknownSettingError # (key does not exist)
97
+
98
+ # get a hash slice of setting options (and fail when the required key does not exist)
99
+ config.slice(:vendor_api) # => { 'vendor_api' => { 'host' => 'app_service', 'user' => 'test_user' } }
100
+ config.slice(:vendor_api, :user) # => { 'user' => 'test_user' }
101
+ config.slice(:project_api) # => Qonfig::UnknownSettingError # (key does not exist)
102
+ config.slice(:vendor_api, :port) # => Qonfig::UnknownSettingError # (key does not exist)
103
+
104
+ # 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)
105
+ config.slice_value(:vendor_api) # => { 'host' => 'app_service', 'user' => 'test_user' }
106
+ config.slice_value(:vendor_api, :user) # => 'test_user'
107
+ config.slice_value(:project_api) # => Qonfig::UnknownSettingError # (key does not exist)
108
+ config.slice_value(:vendor_api, :port) # => Qonfig::UnknownSettingError # (key does not exist)
109
+ ```
110
+
111
+ ---
112
+
113
+ ### Configuration
114
+
115
+ ```ruby
116
+ class Config < Qonfig::DataSet
117
+ setting :testing do
118
+ setting :engine, :rspec
119
+ setting :parallel, true
120
+ end
121
+
122
+ setting :geo_api do
123
+ setting :provider, :google_maps
124
+ end
125
+
126
+ setting :enable_middlewares, false
127
+ end
128
+
129
+ config = Config.new
130
+
131
+ # configure via proc
132
+ config.configure do |conf|
133
+ conf.enable_middlewares = true
134
+ conf.geo_api.provider = :yandex_maps
135
+ conf.testing.engine = :mini_test
136
+ end
137
+
138
+ # configure via settings object (by option name)
139
+ config.settings.enable_middlewares = false
140
+ config.settings.geo_api.provider = :apple_maps
141
+ config.settings.testing.engine = :ultra_test
142
+
143
+ # configure via settings object (by setting key)
144
+ config.settings[:enable_middlewares] = true
145
+ config.settings[:geo_api][:provider] = :rambler_maps
146
+ config.settings[:testing][:engine] = :mega_test
147
+
148
+ # instant configuration via proc
149
+ config = Config.new do |conf|
150
+ conf.enable_middlewares = false
151
+ conf.geo_api.provider = :amazon_maps
152
+ conf.testing.engine = :crypto_test
153
+ end
154
+
155
+ # using a hash
156
+ config = Config.new(
157
+ testing: { engine: :mini_test, parallel: false },
158
+ geo_api: { provider: :rambler_maps },
159
+ enable_middlewares: true
160
+ )
161
+ config.configure(enable_middlewares: false)
162
+
163
+ # using both hash and proc (proc has higher priority)
164
+ config = Config.new(enable_middlewares: true) do |conf|
165
+ conf.testing.parallel = true
166
+ end
167
+
168
+ config.configure(geo_api: { provider: nil }) do |conf|
169
+ conf.testing.engine = :rspec
170
+ end
171
+ ```
172
+
173
+ ---
174
+
175
+ ### Inheritance
176
+
177
+ ```ruby
178
+ class CommonConfig < Qonfig::DataSet
179
+ setting :uploader, :fog
180
+ end
181
+
182
+ class ProjectConfig < CommonConfig
183
+ setting :auth_provider, :github
184
+ end
185
+
186
+ project_config = ProjectConfig.new
187
+
188
+ # inherited setting
189
+ project_config.settings.uploader # => :fog
190
+
191
+ # own setting
192
+ project_config.settings.auth_provider # => :github
193
+ ```
194
+
195
+ ---
196
+
197
+ ### Composition
198
+
199
+ ```ruby
200
+ class SharedConfig < Qonfig::DataSet
201
+ setting :logger, Logger.new
202
+ end
203
+
204
+ class ServerConfig < Qonfig::DataSet
205
+ setting :port, 12345
206
+ setting :address, '0.0.0.0'
207
+ end
208
+
209
+ class DatabaseConfig < Qonfig::DataSet
210
+ setting :user, 'test'
211
+ setting :password, 'testpaswd'
212
+ end
213
+
214
+ class ProjectConfig < Qonfig::DataSet
215
+ compose SharedConfig
216
+
217
+ setting :server do
218
+ compose ServerConfig
219
+ end
220
+
221
+ setting :db do
222
+ compose DatabaseConfig
223
+ end
224
+ end
225
+
226
+ project_config = ProjectConfig.new
227
+
228
+ # fields from SharedConfig
229
+ project_config.settings.logger # => #<Logger:0x66f57048>
230
+
231
+ # fields from ServerConfig
232
+ project_config.settings.server.port # => 12345
233
+ project_config.settings.server.address # => '0.0.0.0'
234
+
235
+ # fields from DatabaseConfig
236
+ project_config.settings.db.user # => 'test'
237
+ project_config.settings.db.password # => 'testpaswd'
238
+ ```
239
+
240
+ ---
241
+
242
+ ### Hash representation
243
+
244
+ ```ruby
245
+ class Config < Qonfig::DataSet
246
+ setting :serializers do
247
+ setting :json do
248
+ setting :engine, :ok
249
+ end
250
+
251
+ setting :hash do
252
+ setting :engine, :native
253
+ end
254
+ end
255
+
256
+ setting :adapter do
257
+ setting :default, :memory_sync
258
+ end
259
+
260
+ setting :logger, Logger.new(STDOUT)
261
+ end
262
+
263
+ Config.new.to_h
264
+
265
+ {
266
+ "serializers": {
267
+ "json" => { "engine" => :ok },
268
+ "hash" => { "engine" => :native },
269
+ },
270
+ "adapter" => { "default" => :memory_sync },
271
+ "logger" => #<Logger:0x4b0d79fc>
272
+ }
273
+ ```
274
+
275
+ ---
276
+
277
+ ### Config reloading
278
+
279
+ ```ruby
280
+ class Config < Qonfig::DataSet
281
+ setting :db do
282
+ setting :adapter, 'postgresql'
283
+ end
284
+
285
+ setting :logger, Logger.new(STDOUT)
286
+ end
287
+
288
+ config = Config.new
289
+
290
+ config.settings.db.adapter # => 'postgresql'
291
+ config.settings.logger # => #<Logger:0x00007ff9>
292
+
293
+ config.configure { |conf| conf.logger = nil } # redefine some settings (will be reloaded)
294
+
295
+ # re-define and append settings
296
+ class Config
297
+ setting :db do
298
+ setting :adapter, 'mongoid' # re-define defaults
299
+ end
300
+
301
+ setting :enable_api, false # append new setting
302
+ end
303
+
304
+ # reload settings
305
+ config.reload!
306
+
307
+ config.settings.db.adapter # => 'mongoid'
308
+ config.settings.logger # => #<Logger:0x00007ff9> (reloaded from defaults)
309
+ config.settings.enable_api # => false (new setting)
310
+
311
+ # reload with instant configuration
312
+ config.reload!(db: { adapter: 'oracle' }) do |conf|
313
+ conf.enable_api = true # changed instantly
314
+ end
315
+
316
+ config.settings.db.adapter # => 'oracle'
317
+ config.settings.logger = # => #<Logger:0x00007ff9>
318
+ config.settings.enable_api # => true # value from instant change
319
+ ```
320
+
321
+ ---
322
+
323
+ ### Clear options
324
+
325
+ ```ruby
326
+ class Config
327
+ setting :database do
328
+ setting :user
329
+ setting :password
330
+ end
331
+
332
+ setting :web_api do
333
+ setting :endpoint
334
+ end
335
+ end
336
+
337
+ config = Config.new do |conf|
338
+ conf.database.user = '0exp'
339
+ conf.database.password = 'test123'
340
+
341
+ conf.web_api.endpoint = '/api/'
342
+ end
343
+
344
+ config.settings.database.user # => '0exp'
345
+ config.settings.database.password # => 'test123'
346
+ config.settings.web_api.endpoint # => '/api'
347
+
348
+ # clear all options
349
+ config.clear!
350
+
351
+ config.settings.database.user # => nil
352
+ config.settings.database.password # => nil
353
+ config.settings.web_api.endpoint # => nil
354
+ ```
355
+
356
+ ---
357
+
358
+ ### State freeze
359
+
360
+ ```ruby
361
+ class Config < Qonfig::DataSet
362
+ setting :logger, Logger.new(STDOUT)
363
+ setting :worker, :sidekiq
364
+ setting :db do
365
+ setting :adapter, 'postgresql'
366
+ end
367
+ end
368
+
369
+ config = Config.new
370
+ config.freeze!
371
+
372
+ config.settings.logger = Logger.new(StringIO.new) # => Qonfig::FrozenSettingsError
373
+ config.settings.worker = :que # => Qonfig::FrozenSettingsError
374
+ config.settings.db.adapter = 'mongoid' # => Qonfig::FrozenSettingsError
375
+
376
+ config.reload! # => Qonfig::FrozenSettingsError
377
+ config.clear! # => Qonfig::FrozenSettingsError
378
+ ```
379
+
380
+ ---
381
+
382
+ ### Settings as Predicates
383
+
384
+ - predicate form: `?` at the end of setting name;
385
+ - `nil` and `false` setting values indicates `false`;
386
+ - other setting values indicates `true`;
387
+ - setting roots always returns `true`;
388
+
389
+ ```ruby
390
+ class Config < Qonfig::DataSet
391
+ setting :database do
392
+ setting :user
393
+ setting :host, 'google.com'
394
+
395
+ setting :engine do
396
+ setting :driver, 'postgres'
397
+ end
398
+ end
399
+ end
400
+
401
+ config = Config.new
402
+
403
+ # predicates
404
+ config.settings.database.user? # => false (nil => false)
405
+ config.settings.database.host? # => true ('google.com' => true)
406
+ config.settings.database.engine.driver? # => true ('postgres' => true)
407
+
408
+ # setting roots always returns true
409
+ config.settings.database? # => true
410
+ config.settings.database.engine? # => ture
411
+
412
+ config.configure do |conf|
413
+ conf.database.user = '0exp'
414
+ conf.database.host = false
415
+ conf.database.engine.driver = true
416
+ end
417
+
418
+ # predicates
419
+ config.settings.database.user? # => true ('0exp' => true)
420
+ config.settings.database.host? # => false (false => false)
421
+ config.settings.database.engine.driver? # => true (true => true)
422
+ ```
423
+
424
+ ---
425
+
426
+ ### Load from YAML file
427
+
428
+ - supports `ERB`;
429
+ - `:strict` mode (fail behaviour when the required yaml file doesnt exist):
430
+ - `true` (by default) - causes `Qonfig::FileNotFoundError`;
431
+ - `false` - do nothing, ignore current command;
432
+
433
+ ```yaml
434
+ # travis.yml
435
+
436
+ sudo: false
437
+ language: ruby
438
+ rvm:
439
+ - ruby-head
440
+ - jruby-head
441
+ ```
442
+
443
+ ```yaml
444
+ # project.yml
445
+
446
+ enable_api: false
447
+ Sidekiq/Scheduler:
448
+ enable: true
449
+ ```
450
+
451
+ ```yaml
452
+ # ruby_data.yml
453
+
454
+ version: <%= RUBY_VERSION %>
455
+ platform: <%= RUBY_PLATFORM %>
456
+ ```
457
+
458
+ ```ruby
459
+ class Config < Qonfig::DataSet
460
+ setting :ruby do
461
+ load_from_yaml 'ruby_data.yml'
462
+ end
463
+
464
+ setting :travis do
465
+ load_from_yaml 'travis.yml'
466
+ end
467
+
468
+ load_from_yaml 'project.yml'
469
+ end
470
+
471
+ config = Config.new
472
+
473
+ config.settings.travis.sudo # => false
474
+ config.settings.travis.language # => 'ruby'
475
+ config.settings.travis.rvm # => ['ruby-head', 'jruby-head']
476
+ config.settings.enable_api # => false
477
+ config.settings['Sidekiq/Scheduler']['enable'] #=> true
478
+ config.settings.ruby.version # => '2.5.1'
479
+ config.settings.ruby.platform # => 'x86_64-darwin17'
480
+ ```
481
+
482
+ ```ruby
483
+ # --- strict mode ---
484
+ class Config < Qonfig::DataSet
485
+ setting :nonexistent_yaml do
486
+ load_from_yaml 'nonexistent_yaml.yml', strict: true # true by default
487
+ end
488
+
489
+ setting :another_key
490
+ end
491
+
492
+ Config.new # => Qonfig::FileNotFoundError
493
+
494
+ # --- non-strict mode ---
495
+ class Config < Qonfig::DataSet
496
+ settings :nonexistent_yaml do
497
+ load_from_yaml 'nonexistent_yaml.yml', strict: false
498
+ end
499
+
500
+ setting :another_key
501
+ end
502
+
503
+ Config.new.to_h # => { "nonexistent_yaml" => {}, "another_key" => nil }
504
+ ```
505
+
506
+ ---
507
+
508
+ ### Expose YAML
509
+
510
+ - load configurations from YAML file in Rails-like manner (with environments);
511
+ - works in `load_from_yaml` manner;
512
+ - `via:` - how an environment will be determined:
513
+ - `:file_name`
514
+ - load configuration from YAML file that have an `:env` part in it's name;
515
+ - `:env_key`
516
+ - load configuration from YAML file;
517
+ - concrete configuration should be defined in the root key with `:env` name;
518
+ - `env:` - your environment name (must be a type of `String`, `Symbol` or `Numeric`);
519
+ - `strict:` - requires the existence of the file and/or key with the name of the used environment:
520
+ - `true`:
521
+ - file should exist;
522
+ - root key with `:env` name should exist (if `via: :env_key` is used);
523
+ - raises `Qonfig::ExposeError` if file does not contain the required env key (if `via: :env` key is used);
524
+ - raises `Qonfig::FileNotFoundError` if the required file does not exist;
525
+ - `false`:
526
+ - file is not required;
527
+ - root key with `:env` name is not required (if `via: :env_key` is used);
528
+
529
+ #### Environment is defined as a root key of YAML file
530
+
531
+ ```yaml
532
+ # config/project.yml
533
+
534
+ default: &default
535
+ enable_api_mode: true
536
+ google_key: 12345
537
+ window:
538
+ width: 100
539
+ height: 100
24
540
25
- TODO: Write usage instructions here
541
+ development:
542
+ <<: *default
26
543
27
- ## Development
544
+ test:
545
+ <<: *default
546
+ sidekiq_instrumentation: false
547
+
548
+ staging:
549
+ <<: *default
550
+ google_key: 777
551
+ enable_api_mode: false
552
+
553
+ production:
554
+ google_key: asd1-39sd-55aI-O92x
555
+ enable_api_mode: true
556
+ window:
557
+ width: 50
558
+ height: 150
559
+ ```
560
+
561
+ ```ruby
562
+ class Config < Qonfig::DataSet
563
+ expose_yaml 'config/project.yml', via: :env_key, env: :production # load from production env
564
+
565
+ # NOTE: in rails-like application you can use this:
566
+ expose_yaml 'config/project.yml', via: :env_key, env: Rails.env
567
+ end
568
+
569
+ config = Config.new
570
+
571
+ config.settings.enable_api_mode # => true (from :production subset of keys)
572
+ config.settings.google_key # => asd1-39sd-55aI-O92x (from :production subset of keys)
573
+ config.settings.window.width # => 50 (from :production subset of keys)
574
+ config.settings.window.height # => 150 (from :production subset of keys)
575
+ ```
576
+
577
+ #### Environment is defined as a part of YAML file name
578
+
579
+ ```yaml
580
+ # config/sidekiq.staging.yml
581
+
582
+ web:
583
+ username: staging_admin
584
+ password: staging_password
585
+ ```
586
+
587
+ ```yaml
588
+ # config/sidekiq.production.yml
589
+
590
+ web:
591
+ username: urj1o2
592
+ password: u192jd0ixz0
593
+ ```
594
+
595
+ ```ruby
596
+ class SidekiqConfig < Qonfig::DataSet
597
+ # NOTE: file name should be described WITHOUT environment part (in file name attribute)
598
+ expose_yaml 'config/sidekiq.yml', via: :file_name, env: :staging # load from staging env
599
+
600
+ # NOTE: in rails-like application you can use this:
601
+ expose_yaml 'config/sidekiq.yml', via: :file_name, env: Rails.env
602
+ end
603
+
604
+ config = SidekiqConfig.new
605
+
606
+ config.settings.web.username # => staging_admin (from sidekiq.staging.yml)
607
+ config.settings.web.password # => staging_password (from sidekiq.staging.yml)
608
+ ```
609
+
610
+ ---
611
+
612
+ ### Load from JSON file
613
+
614
+ - `:strict` mode (fail behaviour when the required yaml file doesnt exist):
615
+ - `true` (by default) - causes `Qonfig::FileNotFoundError`;
616
+ - `false` - do nothing, ignore current command;
617
+
618
+ ```json
619
+ // options.json
620
+
621
+ {
622
+ "user": "0exp",
623
+ "password": 12345,
624
+ "rubySettings": {
625
+ "allowedVersions": ["2.3", "2.4.2", "1.9.8"],
626
+ "gitLink": null,
627
+ "withAdditionals": false
628
+ }
629
+ }
630
+ ```
631
+
632
+ ```ruby
633
+ class Config < Qonfig::DataSet
634
+ load_from_json 'options.json'
635
+ end
636
+
637
+ config = Config.new
638
+
639
+ config.settings.user # => '0exp'
640
+ config.settings.password # => 12345
641
+ config.settings.rubySettings.allowedVersions # => ['2.3', '2.4.2', '1.9.8']
642
+ config.settings.rubySettings.gitLink # => nil
643
+ config.settings.rubySettings.withAdditionals # => false
644
+ ```
645
+
646
+ ```ruby
647
+ # --- strict mode ---
648
+ class Config < Qonfig::DataSet
649
+ setting :nonexistent_json do
650
+ load_from_json 'nonexistent_json.json', strict: true # true by default
651
+ end
652
+
653
+ setting :another_key
654
+ end
655
+
656
+ Config.new # => Qonfig::FileNotFoundError
657
+
658
+ # --- non-strict mode ---
659
+ class Config < Qonfig::DataSet
660
+ settings :nonexistent_json do
661
+ load_from_json 'nonexistent_json.json', strict: false
662
+ end
663
+
664
+ setting :another_key
665
+ end
666
+
667
+ Config.new.to_h # => { "nonexistent_json" => {}, "another_key" => nil }
668
+ ```
669
+
670
+ ---
671
+
672
+ ### Load from ENV
673
+
674
+ - `:convert_values` (`false` by default):
675
+ - `'t'`, `'T'`, `'true'`, `'TRUE'` - covnerts to `true`;
676
+ - `'f'`, `'F'`, `'false'`, `'FALSE'` - covnerts to `false`;
677
+ - `1`, `23` and etc - converts to `Integer`;
678
+ - `1.25`, `0.26` and etc - converts to `Float`;
679
+ - `1, 2, test`, `FALSE,Qonfig` (strings without quotes that contains at least one comma) -
680
+ converts to `Array` with recursively converted values;
681
+ - `'"please, test"'`, `"'test, please'"` (quoted strings) - converts to `String` without quotes;
682
+ - `:prefix` - load ENV variables which names starts with a prefix:
683
+ - `nil` (by default) - empty prefix;
684
+ - `Regexp` - names that match the regexp pattern;
685
+ - `String` - names which starts with a passed string;
686
+ - `:trim_prefix` (`false` by default);
687
+
688
+ ```ruby
689
+ # some env variables
690
+ ENV['QONFIG_BOOLEAN'] = 'true'
691
+ ENV['QONFIG_INTEGER'] = '0'
692
+ ENV['QONFIG_STRING'] = 'none'
693
+ ENV['QONFIG_ARRAY'] = '1, 2.5, t, f, TEST'
694
+ ENV['QONFIG_MESSAGE'] = '"Hello, Qonfig!"'
695
+ ENV['RUN_CI'] = '1'
696
+
697
+ class Config < Qonfig::DataSet
698
+ # nested
699
+ setting :qonfig do
700
+ load_from_env convert_values: true, prefix: 'QONFIG' # or /\Aqonfig.*\z/i
701
+ end
702
+
703
+ setting :trimmed do
704
+ load_from_env convert_values: true, prefix: 'QONFIG_', trim_prefix: true # trim prefix
705
+ end
706
+
707
+ # on the root
708
+ load_from_env
709
+ end
710
+
711
+ config = Config.new
712
+
713
+ # customized
714
+ config.settings['qonfig']['QONFIG_BOOLEAN'] # => true ('true' => true)
715
+ config.settings['qonfig']['QONFIG_INTEGER'] # => 0 ('0' => 0)
716
+ config.settings['qonfig']['QONFIG_STRING'] # => 'none'
717
+ config.settings['qonfig']['QONFIG_ARRAY'] # => [1, 2.5, true, false, 'TEST']
718
+ config.settings['qonfig']['QONFIG_MESSAGE'] # => 'Hello, Qonfig!'
719
+ config.settings['qonfig']['RUN_CI'] # => Qonfig::UnknownSettingError
720
+
721
+ # trimmed (and customized)
722
+ config.settings['trimmed']['BOOLEAN'] # => true ('true' => true)
723
+ config.settings['trimmed']['INTEGER'] # => 0 ('0' => 0)
724
+ config.settings['trimmed']['STRING'] # => 'none'
725
+ config.settings['trimmed']['ARRAY'] # => [1, 2.5, true, false, 'TEST']
726
+ config.settings['trimmed']['MESSAGE'] # => 'Hello, Qonfig!'
727
+ config.settings['trimmed']['RUN_CI'] # => Qonfig::UnknownSettingError
728
+
729
+ # default
730
+ config.settings['QONFIG_BOOLEAN'] # => 'true'
731
+ config.settings['QONFIG_INTEGER'] # => '0'
732
+ config.settings['QONFIG_STRING'] # => 'none'
733
+ config.settings['QONFIG_ARRAY'] # => '1, 2.5, t, f, TEST'
734
+ config.settings['QONFIG_MESSAGE'] # => '"Hello, Qonfig!"'
735
+ config.settings['RUN_CI'] # => '1'
736
+ ```
737
+
738
+ ---
739
+
740
+ ### Load from \_\_END\_\_
741
+
742
+ - aka `load_from_self`
743
+
744
+ ```ruby
745
+ class Config < Qonfig::DataSet
746
+ load_from_self # on the root
747
+
748
+ setting :nested do
749
+ load_from_self # nested
750
+ end
751
+ end
752
+
753
+ config = Config.new
754
+
755
+ # on the root
756
+ config.settings.ruby_version # => '2.5.1'
757
+ config.settings.secret_key # => 'top-mega-secret'
758
+ config.settings.api_host # => 'super.puper-google.com'
759
+ config.settings.connection_timeout.seconds # => 10
760
+ config.settings.connection_timeout.enabled # => false
761
+
762
+ # nested
763
+ config.settings.nested.ruby_version # => '2.5.1'
764
+ config.settings.nested.secret_key # => 'top-mega-secret'
765
+ config.settings.nested.api_host # => 'super.puper-google.com'
766
+ config.settings.nested.connection_timeout.seconds # => 10
767
+ config.settings.nested.connection_timeout.enabled # => false
768
+
769
+ __END__
770
+
771
+ ruby_version: <%= RUBY_VERSION %>
772
+ secret_key: top-mega-secret
773
+ api_host: super.puper-google.com
774
+ connection_timeout:
775
+ seconds: 10
776
+ enabled: false
777
+ ```
778
+
779
+ ---
780
+
781
+ ### Save to JSON file
782
+
783
+ - `#save_to_json` - represents config object as a json structure and saves it to a file:
784
+ - uses native `::JSON.generate` under the hood;
785
+ - writes new file (or rewrites existing file);
786
+ - attributes:
787
+ - `:path` - (required) - file path;
788
+ - `:options` - (optional) - native `::JSON.generate` options (from stdlib):
789
+ - `:indent` - `" "` by default;
790
+ - `:space` - `" "` by default/
791
+ - `:object_nl` - `"\n"` by default;
792
+ - `&value_preprocessor` - (optional) - value pre-processor;
793
+
794
+ #### Without value preprocessing (standard usage)
795
+
796
+ ```ruby
797
+ class AppConfig < Qonfig::DataSet
798
+ setting :server do
799
+ setting :address, 'localhost'
800
+ setting :port, 12_345
801
+ end
802
+
803
+ setting :enabled, true
804
+ end
805
+
806
+ config = AppConfig.new
807
+
808
+ # NOTE: save to json file
809
+ config.save_to_json(path: 'config.json')
810
+ ```
811
+
812
+ ```json
813
+ {
814
+ "sentry": {
815
+ "address": "localhost",
816
+ "port": 12345
817
+ },
818
+ "enabled": true
819
+ }
820
+ ```
821
+
822
+ #### With value preprocessing and custom options
823
+
824
+ ```ruby
825
+ class AppConfig < Qonfig::DataSet
826
+ setting :server do
827
+ setting :address, 'localhost'
828
+ setting :port, 12_345
829
+ end
830
+
831
+ setting :enabled, true
832
+ setting :dynamic, -> { 1 + 2 }
833
+ end
834
+
835
+ config = AppConfig.new
836
+
837
+ # NOTE: save to json file with custom options (no spaces / no new line / no indent; call procs)
838
+ config.save_to_json(path: 'config.json', options: { indent: '', space: '', object_nl: '' }) do |value|
839
+ value.is_a?(Proc) ? value.call : value
840
+ end
841
+ ```
842
+
843
+ ```json
844
+ // no spaces / no new line / no indent / calculated "dynamic" setting key
845
+ {"sentry":{"address":"localhost","port":12345},"enabled":true,"dynamic":3}
846
+ ```
847
+
848
+ ---
849
+
850
+ ### Save to YAML file
851
+
852
+ - `#save_to_yaml` - represents config object as a yaml structure and saves it to a file:
853
+ - uses native `::Psych.dump` under the hood;
854
+ - writes new file (or rewrites existing file);
855
+ - attributes:
856
+ - `:path` - (required) - file path;
857
+ - `:options` - (optional) - native `::Psych.dump` options (from stdlib):
858
+ - `:indentation` - `2` by default;
859
+ - `:line_width` - `-1` by default;
860
+ - `:canonical` - `false` by default;
861
+ - `:header` - `false` by default;
862
+ - `:symbolize_keys` - (non-native option) - `false` by default;
863
+ - `&value_preprocessor` - (optional) - value pre-processor;
864
+
865
+ #### Without value preprocessing (standard usage)
866
+
867
+ ```ruby
868
+ class AppConfig < Qonfig::DataSet
869
+ setting :server do
870
+ setting :address, 'localhost'
871
+ setting :port, 12_345
872
+ end
873
+
874
+ setting :enabled, true
875
+ end
876
+
877
+ config = AppConfig.new
878
+
879
+ # NOTE: save to yaml file
880
+ config.save_to_yaml(path: 'config.yml')
881
+ ```
882
+
883
+ ```yaml
884
+ ---
885
+ server:
886
+ address: localhost
887
+ port: 12345
888
+ enabled: true
889
+ ```
890
+
891
+ #### With value preprocessing and custom options
892
+
893
+ ```ruby
894
+ class AppConfig < Qonfig::DataSet
895
+ setting :server do
896
+ setting :address, 'localhost'
897
+ setting :port, 12_345
898
+ end
899
+
900
+ setting :enabled, true
901
+ setting :dynamic, -> { 5 + 5 }
902
+ end
903
+
904
+ config = AppConfig.new
905
+
906
+ # NOTE: save to yaml file with custom options (add yaml version header; call procs)
907
+ config.save_to_yaml(path: 'config.yml', options: { header: true }) do |value|
908
+ value.is_a?(Proc) ? value.call : value
909
+ end
910
+ ```
911
+
912
+ ```yaml
913
+ # yaml version header / calculated "dynamic" setting key
914
+ %YAML 1.1
915
+ ---
916
+ server:
917
+ address: localhost
918
+ port: 12345
919
+ enabled: true
920
+ dynamic: 10
921
+ ```
922
+
923
+ ---
924
+
925
+ ### Smart Mixin
926
+
927
+ - class-level:
928
+ - `.configuration` - settings definitions;
929
+ - `.configure` - configuration;
930
+ - `.config` - config object;
931
+ - settings definitions are inheritable;
932
+ - instance-level:
933
+ - `#configure` - configuration;
934
+ - `#config` - config object;
935
+ - `#shared_config` - class-level config object;
936
+
937
+ ```ruby
938
+ # --- usage ---
939
+
940
+ class Application
941
+ # make configurable
942
+ include Qonfig::Configurable
943
+
944
+ configuration do
945
+ setting :user
946
+ setting :password
947
+ end
948
+ end
949
+
950
+ app = Application.new
951
+
952
+ # class-level config
953
+ Application.config.settings.user # => nil
954
+ Application.config.settings.password # => nil
955
+
956
+ # instance-level config
957
+ app.config.settings.user # => nil
958
+ app.config.settings.password # => nil
959
+
960
+ # access to the class level config from an instance
961
+ app.shared_config.settings.user # => nil
962
+ app.shared_config.settings.password # => nil
963
+
964
+ # class-level configuration
965
+ Application.configure do |conf|
966
+ conf.user = '0exp'
967
+ conf.password = 'test123'
968
+ end
969
+
970
+ # instance-level configuration
971
+ app.configure do |conf|
972
+ conf.user = 'admin'
973
+ conf.password = '123test'
974
+ end
975
+
976
+ # class has own config object
977
+ Application.config.settings.user # => '0exp'
978
+ Application.config.settings.password # => 'test123'
979
+
980
+ # instance has own config object
981
+ app.config.settings.user # => 'admin'
982
+ app.config.settings.password # => '123test'
983
+
984
+ # access to the class level config from an instance
985
+ app.shared_config.settings.user # => '0exp'
986
+ app.shared_config.settings.password # => 'test123'
987
+
988
+ # and etc... (all Qonfig-related features)
989
+ ```
990
+
991
+ ```ruby
992
+ # --- inheritance ---
993
+
994
+ class BasicApplication
995
+ # make configurable
996
+ include Qonfig::Configurable
997
+
998
+ configuration do
999
+ setting :user
1000
+ setting :pswd
1001
+ end
1002
+
1003
+ configure do |conf|
1004
+ conf.user = 'admin'
1005
+ conf.pswd = 'admin'
1006
+ end
1007
+ end
1008
+
1009
+ class GeneralApplication < BasicApplication
1010
+ # extend inherited definitions
1011
+ configuration do
1012
+ setting :db do
1013
+ setting :adapter
1014
+ end
1015
+ end
1016
+
1017
+ configure do |conf|
1018
+ conf.user = '0exp' # .user inherited from BasicApplication
1019
+ conf.pswd = '123test' # .pswd inherited from BasicApplication
1020
+ conf.db.adapter = 'pg'
1021
+ end
1022
+ end
1023
+
1024
+ BasicApplication.config.to_h
1025
+ { 'user' => 'admin', 'pswd' => 'admin' }
1026
+
1027
+ GeneralApplication.config.to_h
1028
+ { 'user' => '0exp', 'pswd' => '123test', 'db' => { 'adapter' => 'pg' } }
1029
+
1030
+ # and etc... (all Qonfig-related features)
1031
+ ```
1032
+
1033
+ ---
1034
+
1035
+ ### Plugins
1036
+
1037
+ ```ruby
1038
+ # --- show names of registered plugins ---
1039
+ Qonfig.plugins # => array of strings
1040
+
1041
+ # --- load specific plugin ---
1042
+ Qonfig.plugin(:plugin_name) # or Qonfig.plugin('plugin_name')
1043
+ ```
1044
+
1045
+ ---
1046
+
1047
+ ### Plugins: toml
1048
+
1049
+ - adds support for `toml` format ([specification](https://github.com/toml-lang/toml));
1050
+ - depends on `toml-rb` gem;
1051
+ - provides `load_from_toml` (works in `load_from_yaml` manner [doc](#load-from-yaml-file));
1052
+ - provides `save_to_toml` (works in `save_to_yaml` manner [doc](#save-to-yaml-file)) (`toml-rb` has no native options);
1053
+ - provides `expose_toml` (works in `expose_yaml` manner [doc](#expose-yaml));
1054
+
1055
+ ```ruby
1056
+ require 'toml-rb'
1057
+ Qonfig.plugin(:toml)
1058
+ # and use :)
1059
+ ```
1060
+ ---
28
1061
29
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
1062
+ ## Roadmap
30
1063
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
1064
+ - support for TOML format;
1065
+ - explicit "settings" object;
1066
+ - validation layer;
1067
+ - distributed configuration server;
1068
+ - support for Rails-like secrets;
32
1069
33
1070
## Contributing
34
1071
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/qonfig. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
1072
+ - Fork it ( https://github.com/0exp/qonfig/fork )
1073
+ - Create your feature branch (`git checkout -b feature/my-new-feature`)
1074
+ - Commit your changes (`git commit -am 'Add some feature'`)
1075
+ - Push to the branch (`git push origin feature/my-new-feature`)
1076
+ - Create new Pull Request
36
1077
37
1078
## License
38
1079
39
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
1080
+ Released under MIT License.
40
1081
41
- ## Code of Conduct
1082
+ ## Authors
42
1083
43
- Everyone interacting in the Qonfig project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/qonfig/blob/master/CODE_OF_CONDUCT.md).
1084
+ [Rustam Ibragimov](https://github.com/0exp)
data/Rakefile CHANGED
@@ -1,6 +1,20 @@
1
- require "bundler/gem_tasks"
1
+ # frozen_string_literal: true
2
- require "rspec/core/rake_task"
3
2
4
- RSpec::Core::RakeTask.new(:spec)
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+ require 'rubocop'
6
+ require 'rubocop-rspec'
7
+ require 'rubocop-performance'
8
+ require 'rubocop/rake_task'
5
9
6
- task :default => :spec
10
+ RuboCop::RakeTask.new(:rubocop) do |t|
11
+ config_path = File.expand_path(File.join('.rubocop.yml'), __dir__)
12
+
13
+ t.options = ['--config', config_path]
14
+ t.requires << 'rubocop-rspec'
15
+ t.requires << 'rubocop-performance'
16
+ end
17
+
18
+ RSpec::Core::RakeTask.new(:rspec)
19
+
20
+ task default: :rspec
data/bin/console CHANGED
@@ -1,14 +1,8 @@
1
1
#!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
3
- require "bundler/setup"
4
- require "qonfig"
4
+ require 'bundler/setup'
5
+ require 'qonfig'
5
6
6
- # You can add fixtures and/or initialization code here to make experimenting
7
- # with your gem easier. You can also use a different console, if you like.
7
+ require 'pry'
8
+ Pry.start
8
-
9
- # (If you use this, don't forget to add pry to your Gemfile!)
10
- # require "pry"
11
- # Pry.start
12
-
13
- require "irb"
14
- IRB.start(__FILE__)
data/bin/rspec ADDED
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'pathname'
5
+ require 'optparse'
6
+
7
+ module QonfigSpecRunner
8
+ extend self
9
+
10
+ expand_gemfile_path = lambda do |gemfile_name|
11
+ File.expand_path(File.join('..', 'gemfiles', gemfile_name), __dir__)
12
+ end
13
+
14
+ GEMFILES = {
15
+ with_external_deps: expand_gemfile_path.call('with_external_deps.gemfile'),
16
+ without_external_deps: expand_gemfile_path.call('without_external_deps.gemfile')
17
+ }.freeze
18
+
19
+ def run!
20
+ OptionParser.new do |opts|
21
+ opts.banner = 'Usage: bin/rspec [options]'
22
+
23
+ opts.on(
24
+ '-w', '--with-plugins',
25
+ 'Run tests with test for plugins'
26
+ ) { run_with_specs_for_plugins! }
27
+
28
+ opts.on(
29
+ '-n', '--without-plugins',
30
+ 'Run tests without tests for plugins'
31
+ ) { run_without_specs_for_plugins! }
32
+ end.parse!
33
+ end
34
+
35
+ private
36
+
37
+ def run_with_specs_for_plugins!
38
+ ENV['TEST_PLUGINS'] = 'true'
39
+ ENV['BUNDLE_GEMFILE'] = GEMFILES[:with_external_deps]
40
+ run_tests!
41
+ end
42
+
43
+ def run_without_specs_for_plugins!
44
+ ENV['BUNDLE_GEMFILE'] = GEMFILES[:without_external_deps]
45
+ run_tests!
46
+ end
47
+
48
+ def run_tests!
49
+ require 'rubygems'
50
+ require 'bundler/setup'
51
+ load Gem.bin_path('rspec-core', 'rspec')
52
+ end
53
+ end
54
+
55
+ QonfigSpecRunner.run!
data/bin/setup CHANGED
@@ -1,4 +1,5 @@
1
1
#!/usr/bin/env bash
2
+
2
3
set -euo pipefail
3
4
IFS=#x27;\n\t'
4
5
set -vx
data/gemfiles/with_external_deps.gemfile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # @since 0.12.0 (qonfig: :toml plugin)
6
+ gem 'toml-rb', '>= 1'
7
+
8
+ gemspec path: '..'
data/gemfiles/without_external_deps.gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec path: '..'
data/lib/qonfig.rb CHANGED
@@ -1,5 +1,25 @@
1
- require "qonfig/version"
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require 'json'
5
+ require 'erb'
2
6
3
7
module Qonfig
4
- # Your code goes here...
8
+ require_relative 'qonfig/errors'
9
+ require_relative 'qonfig/loaders'
10
+ require_relative 'qonfig/uploaders'
11
+ require_relative 'qonfig/commands'
12
+ require_relative 'qonfig/command_set'
13
+ require_relative 'qonfig/settings'
14
+ require_relative 'qonfig/dsl'
15
+ require_relative 'qonfig/data_set'
16
+ require_relative 'qonfig/configurable'
17
+ require_relative 'qonfig/plugins'
18
+
19
+ # @api public
20
+ # @since 0.4.0
21
+ extend Plugins::AccessMixin
22
+
23
+ # @since 0.12.0
24
+ register_plugin('toml', Qonfig::Plugins::TOML)
5
25
end
data/lib/qonfig/command_set.rb ADDED
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ # @since 0.1.0
5
+ class Qonfig::CommandSet
6
+ # @return [Array<Qonfig::Commands::Base>]
7
+ #
8
+ # @api private
9
+ # @since 0.1.0
10
+ attr_reader :commands
11
+
12
+ # @api private
13
+ # @since 0.1.0
14
+ def initialize
15
+ @commands = []
16
+ @access_lock = Mutex.new
17
+ end
18
+
19
+ # @param command [Qonfig::Commands::Base]
20
+ # @return [void]
21
+ #
22
+ # @api private
23
+ # @since 0.1.0
24
+ def add_command(command)
25
+ thread_safe { commands << command }
26
+ end
27
+ alias_method :<<, :add_command
28
+
29
+ # @param block [Proc]
30
+ # @return [Enumerable]
31
+ #
32
+ # @api private
33
+ # @since 0.1.0
34
+ def each(&block)
35
+ thread_safe { block_given? ? commands.each(&block) : commands.each }
36
+ end
37
+
38
+ # @param command_set [Qonfig::CommandSet]
39
+ # @return [void]
40
+ #
41
+ # @api private
42
+ # @since 0.1.0
43
+ def concat(command_set)
44
+ thread_safe { commands.concat(command_set.commands) }
45
+ end
46
+
47
+ # @return [Qonfig::CommandSet]
48
+ #
49
+ # @api private
50
+ # @since 0.2.0
51
+ def dup
52
+ thread_safe do
53
+ self.class.new.tap { |duplicate| duplicate.concat(self) }
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ # @param block [Proc]
60
+ # @return [Object]
61
+ #
62
+ # @api private
63
+ # @since 0.2.0
64
+ def thread_safe(&block)
65
+ @access_lock.synchronize(&block)
66
+ end
67
+ end
data/lib/qonfig/commands.rb ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ # @since 0.1.0
5
+ module Qonfig::Commands
6
+ require_relative 'commands/base'
7
+ require_relative 'commands/add_option'
8
+ require_relative 'commands/add_nested_option'
9
+ require_relative 'commands/compose'
10
+ require_relative 'commands/load_from_yaml'
11
+ require_relative 'commands/load_from_json'
12
+ require_relative 'commands/load_from_self'
13
+ require_relative 'commands/load_from_env'
14
+ require_relative 'commands/expose_yaml'
15
+ end
data/lib/qonfig/commands/add_nested_option.rb ADDED
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ # @since 0.1.0
5
+ class Qonfig::Commands::AddNestedOption < Qonfig::Commands::Base
6
+ # @return [Symbol, String]
7
+ #
8
+ # @api private
9
+ # @since 0.1.0
10
+ attr_reader :key
11
+
12
+ # @return [Class<Qonfig::DataSet>]
13
+ #
14
+ # @api private
15
+ # @since 0.2.0
16
+ attr_reader :nested_data_set_klass
17
+
18
+ # @param key [Symbol, String]
19
+ # @param nested_definitions [Proc]
20
+ #
21
+ # @raise [Qonfig::ArgumentError]
22
+ # @raise [Qonfig::CoreMethodIntersectionError]
23
+ #
24
+ # @api private
25
+ # @since 0.1.0
26
+ def initialize(key, nested_definitions)
27
+ Qonfig::Settings::KeyGuard.prevent_incomparabilities!(key)
28
+
29
+ @key = key
30
+ @nested_data_set_klass = Class.new(Qonfig::DataSet).tap do |data_set|
31
+ data_set.instance_eval(&nested_definitions)
32
+ end
33
+ end
34
+
35
+ # @param settings [Qonfig::Settings]
36
+ # @return [void]
37
+ #
38
+ # @api private
39
+ # @since 0.1.0
40
+ def call(settings)
41
+ nested_settings = nested_data_set_klass.new.settings
42
+
43
+ settings.__define_setting__(key, nested_settings)
44
+ end
45
+ end
data/lib/qonfig/commands/add_option.rb ADDED
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ # @since 0.1.0
5
+ class Qonfig::Commands::AddOption < Qonfig::Commands::Base
6
+ # @return [Symbol, String]
7
+ #
8
+ # @api private
9
+ # @since 0.1.0
10
+ attr_reader :key
11
+
12
+ # @return [Object]
13
+ #
14
+ # @api private
15
+ # @since 0.1.0
16
+ attr_reader :value
17
+
18
+ # @param key [Symbol, String]
19
+ # @param value [Object]
20
+ #
21
+ # @raise [Qonfig::ArgumentError]
22
+ # @raise [Qonfig::CoreMethodIntersectionError]
23
+ #
24
+ # @api private
25
+ # @since 0.1.0
26
+ def initialize(key, value)
27
+ Qonfig::Settings::KeyGuard.prevent_incomparabilities!(key)
28
+
29
+ @key = key
30
+ @value = value
31
+ end
32
+
33
+ # @param settings [Qonfig::Settings]
34
+ # @return [void]
35
+ #
36
+ # @api private
37
+ # @since 0.1.0
38
+ def call(settings)
39
+ settings.__define_setting__(key, value)
40
+ end
41
+ end
data/lib/qonfig/commands/base.rb ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ # @since 0.1.0
5
+ class Qonfig::Commands::Base
6
+ # @param settings [Qonfig::Settings]
7
+ # @return [void]
8
+ #
9
+ # @api private
10
+ # @since 0.1.0
11
+ def call(settings); end
12
+ end
data/lib/qonfig/commands/compose.rb ADDED
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ # @since 0.1.0
5
+ class Qonfig::Commands::Compose < Qonfig::Commands::Base
6
+ # @return [Qonfig::DataSet]
7
+ #
8
+ # @api private
9
+ # @since 0.1.0
10
+ attr_reader :data_set_klass
11
+
12
+ # @param data_set_klass [Qonfig::DataSet]
13
+ #
14
+ # @raise [Qonfig::ArgumentError]
15
+ #
16
+ # @api private
17
+ # @since 0.1.0
18
+ def initialize(data_set_klass)
19
+ raise(
20
+ Qonfig::ArgumentError,
21
+ 'Composed config class should be a subtype of Qonfig::DataSet'
22
+ ) unless data_set_klass.is_a?(Class) && data_set_klass < Qonfig::DataSet
23
+
24
+ @data_set_klass = data_set_klass
25
+ end
26
+
27
+ # @param settings [Qonfig::Settings]
28
+ # @return [void]
29
+ #
30
+ # @api private
31
+ # @since 0.1.0
32
+ def call(settings)
33
+ composite_settings = data_set_klass.new.settings
34
+
35
+ settings.__append_settings__(composite_settings)
36
+ end
37
+ end
data/lib/qonfig/commands/expose_yaml.rb ADDED
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ # @since 0.7.0
5
+ class Qonfig::Commands::ExposeYAML < Qonfig::Commands::Base
6
+ # @return [Hash]
7
+ #
8
+ # @api private
9
+ # @since 0.7.0
10
+ EXPOSERS = { file_name: :file_name, env_key: :env_key }.freeze
11
+
12
+ # @return [Hash]
13
+ #
14
+ # @api private
15
+ # @since 0.7.0
16
+ EMPTY_YAML_DATA = {}.freeze
17
+
18
+ # @return [String]
19
+ #
20
+ # @api private
21
+ # @since 0.7.0
22
+ attr_reader :file_path
23
+
24
+ # @return [Boolean]
25
+ #
26
+ # @api private
27
+ # @since 0.7.0
28
+ attr_reader :strict
29
+
30
+ # @return [Symbol]
31
+ #
32
+ # @api private
33
+ # @since 0.7.0
34
+ attr_reader :via
35
+
36
+ # @return [Symbol, String]
37
+ #
38
+ # @api private
39
+ # @since 0.7.0
40
+ attr_reader :env
41
+
42
+ # @param file_path [String]
43
+ # @option strict [Boolean]
44
+ # @option via [Symbol]
45
+ # @option env [String, Symbol]
46
+ #
47
+ # @api private
48
+ # @since 0.7.0
49
+ def initialize(file_path, strict: true, via:, env:)
50
+ unless env.is_a?(Symbol) || env.is_a?(String) || env.is_a?(Numeric)
51
+ raise Qonfig::ArgumentError, ':env should be a string or a symbol'
52
+ end
53
+
54
+ raise Qonfig::ArgumentError, ':env should be provided' if env.to_s.empty?
55
+ raise Qonfig::ArgumentError, 'used :via is unsupported' unless EXPOSERS.key?(via)
56
+
57
+ @file_path = file_path
58
+ @strict = strict
59
+ @via = via
60
+ @env = env
61
+ end
62
+
63
+ # @param settings [Qonfig::Settings]
64
+ # @return [void]
65
+ #
66
+ # @api private
67
+ # @since 0.7.0
68
+ def call(settings)
69
+ case via
70
+ when EXPOSERS[:file_name]
71
+ expose_file_name!(settings)
72
+ when EXPOSERS[:env_key]
73
+ expose_env_key!(settings)
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+ # @param settings [Qonfig::Settings]
80
+ # @return [void]
81
+ #
82
+ # @api private
83
+ # @since 0.7.0
84
+ # rubocop:disable Metrics/AbcSize
85
+ def expose_file_name!(settings)
86
+ # NOTE: transform file name (insert environment name into the file name)
87
+ # from: path/to/file/file_name.file_extension
88
+ # to: path/to/file/file_name.env_name.file_extension
89
+
90
+ pathname = Pathname.new(file_path)
91
+ dirname = pathname.dirname
92
+ extname = pathname.extname.to_s
93
+ basename = pathname.basename.to_s.sub!(extname, '')
94
+ envname = [env.to_s, extname].reject(&:empty?).join('')
95
+ envfile = [basename, envname].reject(&:empty?).join('.')
96
+ realfile = dirname.join(envfile).to_s
97
+
98
+ yaml_data = load_yaml_data(realfile)
99
+ yaml_based_settings = build_data_set_class(yaml_data).new.settings
100
+
101
+ settings.__append_settings__(yaml_based_settings)
102
+ end
103
+ # rubocop:enable Metrics/AbcSize
104
+
105
+ # @param settings [Qonfig::Settings]
106
+ # @return [void]
107
+ #
108
+ # @raise [Qonfig::ExposeError]
109
+ # @raise [Qonfig::IncompatibleYAMLStructureError]
110
+ #
111
+ # @api private
112
+ # @since 0.7.0
113
+ # rubocop:disable Metrics/AbcSize
114
+ def expose_env_key!(settings)
115
+ yaml_data = load_yaml_data(file_path)
116
+ yaml_data_slice = yaml_data[env] || yaml_data[env.to_s] || yaml_data[env.to_sym]
117
+ yaml_data_slice = EMPTY_YAML_DATA.dup if yaml_data_slice.nil? && !strict
118
+
119
+ raise(
120
+ Qonfig::ExposeError,
121
+ "#{file_path} file does not contain settings with <#{env}> environment key!"
122
+ ) unless yaml_data_slice
123
+
124
+ raise(
125
+ Qonfig::IncompatibleYAMLStructureError,
126
+ 'YAML content should have a hash-like structure'
127
+ ) unless yaml_data_slice.is_a?(Hash)
128
+
129
+ yaml_based_settings = build_data_set_class(yaml_data_slice).new.settings
130
+
131
+ settings.__append_settings__(yaml_based_settings)
132
+ end
133
+ # rubocop:enable Metrics/AbcSize
134
+
135
+ # @param file_path [String]
136
+ # @return [Hash]
137
+ #
138
+ # @raise [Qonfig::IncompatibleYAMLStructureError]
139
+ #
140
+ # @api private
141
+ # @since 0.7.0
142
+ def load_yaml_data(file_path)
143
+ Qonfig::Loaders::YAML.load_file(file_path, fail_on_unexist: strict).tap do |yaml_data|
144
+ raise(
145
+ Qonfig::IncompatibleYAMLStructureError,
146
+ 'YAML content should have a hash-like structure'
147
+ ) unless yaml_data.is_a?(Hash)
148
+ end
149
+ end
150
+
151
+ # @param yaml_data [Hash]
152
+ # @return [Class<Qonfig::DataSet>]
153
+ #
154
+ # @api private
155
+ # @since 0.7.0
156
+ def build_data_set_class(yaml_data)
157
+ Qonfig::DataSet::ClassBuilder.build_from_hash(yaml_data)
158
+ end
159
+ end
data/lib/qonfig/commands/load_from_env.rb ADDED
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ # @since 0.2.0
5
+ class Qonfig::Commands::LoadFromENV < Qonfig::Commands::Base
6
+ require_relative 'load_from_env/value_converter'
7
+
8
+ # @return [Boolean]
9
+ #
10
+ # @api private
11
+ # @since 0.2.0
12
+ attr_reader :convert_values
13
+
14
+ # @return [Regexp]
15
+ #
16
+ # @api private
17
+ # @since 0.2.0
18
+ attr_reader :prefix_pattern
19
+
20
+ # @return [Boolean]
21
+ #
22
+ # @api private
23
+ # @since 0.2.0
24
+ attr_reader :trim_prefix
25
+
26
+ # @return [Regexp]
27
+ #
28
+ # @api private
29
+ # @since 0.2.0
30
+ attr_reader :trim_pattern
31
+
32
+ # @option convert_values [Boolean]
33
+ # @opion prefix [NilClass, String, Regexp]
34
+ #
35
+ # @raise [Qonfig::ArgumentError]
36
+ #
37
+ # @api private
38
+ # @since 0.2.0
39
+ def initialize(convert_values: false, prefix: nil, trim_prefix: false)
40
+ unless convert_values.is_a?(FalseClass) || convert_values.is_a?(TrueClass)
41
+ raise Qonfig::ArgumentError, ':convert_values option should be a boolean'
42
+ end
43
+
44
+ unless prefix.is_a?(NilClass) || prefix.is_a?(String) || prefix.is_a?(Regexp)
45
+ raise Qonfig::ArgumentError, ':prefix option should be a nil / string / regexp'
46
+ end
47
+
48
+ unless trim_prefix.is_a?(FalseClass) || trim_prefix.is_a?(TrueClass)
49
+ raise Qonfig::ArgumentError, ':trim_refix options should be a boolean'
50
+ end
51
+
52
+ @convert_values = convert_values
53
+ @prefix_pattern = prefix.is_a?(Regexp) ? prefix : /\A#{Regexp.escape(prefix.to_s)}.*\z/m
54
+ @trim_prefix = trim_prefix
55
+ @trim_pattern = prefix.is_a?(Regexp) ? prefix : /\A(#{Regexp.escape(prefix.to_s)})/m
56
+ end
57
+
58
+ # @param settings [Qonfig::Settings]
59
+ # @return [void]
60
+ #
61
+ # @api private
62
+ # @since 0.2.0
63
+ def call(settings)
64
+ env_data = extract_env_data
65
+
66
+ env_based_settings = build_data_set_class(env_data).new.settings
67
+
68
+ settings.__append_settings__(env_based_settings)
69
+ end
70
+
71
+ private
72
+
73
+ # @return [Hash]
74
+ #
75
+ # @api private
76
+ # @since 0.2.0
77
+ def extract_env_data
78
+ ENV.each_with_object({}) do |(key, value), env_data|
79
+ next unless key.match(prefix_pattern)
80
+ key = key.sub(trim_pattern, '') if trim_prefix
81
+ env_data[key] = value
82
+ end.tap do |env_data|
83
+ ValueConverter.convert_values!(env_data) if convert_values
84
+ end
85
+ end
86
+
87
+ # @param env_data [Hash]
88
+ # @return [Class<Qonfig::DataSet>]
89
+ #
90
+ # @api private
91
+ # @since 0.2.0
92
+ def build_data_set_class(env_data)
93
+ Qonfig::DataSet::ClassBuilder.build_from_hash(env_data)
94
+ end
95
+ end
data/lib/qonfig/commands/load_from_env/value_converter.rb ADDED
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qonfig
4
+ # @api private
5
+ # @since 0.2.0
6
+ module Commands::LoadFromENV::ValueConverter
7
+ # @return [Regexp]
8
+ #
9
+ # @api private
10
+ # @since 0.2.0
11
+ INTEGER_PATTERN = /\A\d+\z/.freeze
12
+
13
+ # @return [Regexp]
14
+ #
15
+ # @api private
16
+ # @since 0.2.0
17
+ FLOAT_PATTERN = /\A\d+\.\d+\z/.freeze
18
+
19
+ # @return [Regexp]
20
+ #
21
+ # @api private
22
+ # @since 0.2.0
23
+ TRUE_PATTERN = /\A(t|true)\z/i.freeze
24
+
25
+ # @return [Regexp]
26
+ #
27
+ # @api private
28
+ # @since 0.2.0
29
+ FALSE_PATTERN = /\A(f|false)\z/i.freeze
30
+
31
+ # @return [Regexp]
32
+ #
33
+ # @api private
34
+ # @since 0.2.0
35
+ ARRAY_PATTERN = /\A[^'"].*\s*,\s*.*[^'"]\z/.freeze
36
+
37
+ # @return [Regexp]
38
+ #
39
+ # @api private
40
+ # @since 0.2.0
41
+ QUOTED_STRING_PATTERN = /\A['"].*['"]\z/.freeze
42
+
43
+ class << self
44
+ # @param env_data [Hash]
45
+ # @return [void]
46
+ #
47
+ # @api private
48
+ # @since 0.2.0
49
+ def convert_values!(env_data)
50
+ env_data.each_pair do |key, value|
51
+ env_data[key] = convert_value(value)
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ # @param value [Object]
58
+ # @return [Object]
59
+ #
60
+ # @api private
61
+ # @since 0.2.0
62
+ def convert_value(value)
63
+ return value unless value.is_a?(String)
64
+
65
+ case value
66
+ when INTEGER_PATTERN
67
+ Integer(value)
68
+ when FLOAT_PATTERN
69
+ Float(value)
70
+ when TRUE_PATTERN
71
+ true
72
+ when FALSE_PATTERN
73
+ false
74
+ when ARRAY_PATTERN
75
+ value.split(/\s*,\s*/).map(&method(:convert_value))
76
+ when QUOTED_STRING_PATTERN
77
+ value.gsub(/(\A['"]|['"]\z)/, '')
78
+ else
79
+ value
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
data/lib/qonfig/commands/load_from_json.rb ADDED
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ # @since 0.5.0
5
+ class Qonfig::Commands::LoadFromJSON < Qonfig::Commands::Base
6
+ # @return [String]
7
+ #
8
+ # @api private
9
+ # @since 0.5.0
10
+ attr_reader :file_path
11
+
12
+ # @return [Boolean]
13
+ #
14
+ # @api private
15
+ # @sicne 0.5.0
16
+ attr_reader :strict
17
+
18
+ # @param file_path [String]
19
+ # @option strict [Boolean]
20
+ #
21
+ # @api private
22
+ # @since 0.5.0
23
+ def initialize(file_path, strict: true)
24
+ @file_path = file_path
25
+ @strict = strict
26
+ end
27
+
28
+ # @param settings [Qonfig::Settings]
29
+ # @return [void]
30
+ #
31
+ # @api private
32
+ # @since 0.5.0
33
+ def call(settings)
34
+ json_data = Qonfig::Loaders::JSON.load_file(file_path, fail_on_unexist: strict)
35
+
36
+ raise(
37
+ Qonfig::IncompatibleJSONStructureError,
38
+ 'JSON object should have a hash-like structure'
39
+ ) unless json_data.is_a?(Hash)
40
+
41
+ json_based_settings = build_data_set_class(json_data).new.settings
42
+
43
+ settings.__append_settings__(json_based_settings)
44
+ end
45
+
46
+ private
47
+
48
+ # @param json_data [Hash]
49
+ # @return [Class<Qonfig::DataSet>]
50
+ #
51
+ # @api private
52
+ # @since 0.5.0
53
+ def build_data_set_class(json_data)
54
+ Qonfig::DataSet::ClassBuilder.build_from_hash(json_data)
55
+ end
56
+ end
data/lib/qonfig/commands/load_from_self.rb ADDED
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ # @since 0.2.0
5
+ class Qonfig::Commands::LoadFromSelf < Qonfig::Commands::Base
6
+ # @return [String]
7
+ #
8
+ # @api private
9
+ # @since 0.2.0
10
+ attr_reader :caller_location
11
+
12
+ # @param caller_location [String]
13
+ #
14
+ # @api private
15
+ # @since 0.2.0
16
+ def initialize(caller_location)
17
+ @caller_location = caller_location
18
+ end
19
+
20
+ # @param settings [Qonfig::Settings]
21
+ # @return [void]
22
+ #
23
+ # @api private
24
+ # @since 0.2.0
25
+ def call(settings)
26
+ yaml_data = load_self_placed_yaml_data
27
+
28
+ yaml_based_settings = build_data_set_klass(yaml_data).new.settings
29
+
30
+ settings.__append_settings__(yaml_based_settings)
31
+ end
32
+
33
+ private
34
+
35
+ # @return [Hash]
36
+ #
37
+ # @raise [Qonfig::SelfDataNotFound]
38
+ # @raise [Qonfig::IncompatibleYAMLStructureError]
39
+ #
40
+ # @api private
41
+ # @since 0.2.0
42
+ def load_self_placed_yaml_data
43
+ caller_file = caller_location.split(':').first
44
+
45
+ raise(
46
+ Qonfig::SelfDataNotFoundError,
47
+ "Caller file does not exist! (location: #{caller_location})"
48
+ ) unless File.exist?(caller_file)
49
+
50
+ data_match = IO.read(caller_file).match(/\n__END__\n(?<end_data>.*)/m)
51
+ raise Qonfig::SelfDataNotFoundError, '__END__ data not found!' unless data_match
52
+
53
+ end_data = data_match[:end_data]
54
+ raise Qonfig::SelfDataNotFoundError, '__END__ data not found!' unless end_data
55
+
56
+ yaml_data = Qonfig::Loaders::YAML.load(end_data)
57
+ raise(
58
+ Qonfig::IncompatibleYAMLStructureError,
59
+ 'YAML content should have a hash-like structure'
60
+ ) unless yaml_data.is_a?(Hash)
61
+
62
+ yaml_data
63
+ end
64
+
65
+ # @param self_placed_yaml_data [Hash]
66
+ # @return [Class<Qonfig::DataSet>]
67
+ #
68
+ # @api private
69
+ # @since 0.2.0
70
+ def build_data_set_klass(self_placed_yaml_data)
71
+ Qonfig::DataSet::ClassBuilder.build_from_hash(self_placed_yaml_data)
72
+ end
73
+ end
data/lib/qonfig/commands/load_from_yaml.rb ADDED
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ # @since 0.2.0
5
+ class Qonfig::Commands::LoadFromYAML < Qonfig::Commands::Base
6
+ # @return [String]
7
+ #
8
+ # @api private
9
+ # @since 0.2.0
10
+ attr_reader :file_path
11
+
12
+ # @return [Boolean]
13
+ #
14
+ # @api private
15
+ # @since 0.2.0
16
+ attr_reader :strict
17
+
18
+ # @param file_path [String]
19
+ # @option strict [Boolean]
20
+ #
21
+ # @api private
22
+ # @since 0.2.0
23
+ def initialize(file_path, strict: true)
24
+ @file_path = file_path
25
+ @strict = strict
26
+ end
27
+
28
+ # @param settings [Qonfig::Settings]
29
+ # @return [void]
30
+ #
31
+ # @raise [Qonfig::IncompatibleYAMLStructureError]
32
+ #
33
+ # @api private
34
+ # @since 0.2.0
35
+ def call(settings)
36
+ yaml_data = Qonfig::Loaders::YAML.load_file(file_path, fail_on_unexist: strict)
37
+
38
+ raise(
39
+ Qonfig::IncompatibleYAMLStructureError,
40
+ 'YAML content should have a hash-like structure'
41
+ ) unless yaml_data.is_a?(Hash)
42
+
43
+ yaml_based_settings = build_data_set_class(yaml_data).new.settings
44
+
45
+ settings.__append_settings__(yaml_based_settings)
46
+ end
47
+
48
+ private
49
+
50
+ # @param yaml_data [Hash]
51
+ # @return [Class<Qonfig::DataSet>]
52
+ #
53
+ # @api private
54
+ # @since 0.2.0
55
+ def build_data_set_class(yaml_data)
56
+ Qonfig::DataSet::ClassBuilder.build_from_hash(yaml_data)
57
+ end
58
+ end
data/lib/qonfig/configurable.rb ADDED
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api public
4
+ # @since 0.2.0
5
+ module Qonfig::Configurable
6
+ class << self
7
+ # @param base_klass [Class]
8
+ # @return [void]
9
+ #
10
+ # @api private
11
+ # @since 0.2.0
12
+ def included(base_klass)
13
+ base_klass.instance_variable_set(:@__qonfig_access_lock__, Mutex.new)
14
+ base_klass.instance_variable_set(:@__qonfig_definition_lock__, Mutex.new)
15
+ base_klass.instance_variable_set(:@__qonfig_config_klass__, Class.new(Qonfig::DataSet))
16
+ base_klass.instance_variable_set(:@__qonfig_config__, nil)
17
+
18
+ base_klass.extend(ClassMethods)
19
+ base_klass.include(InstanceMethods)
20
+ base_klass.singleton_class.prepend(ClassInheritance)
21
+
22
+ super
23
+ end
24
+ end
25
+
26
+ # @api private
27
+ # @since 0.2.0
28
+ module ClassInheritance
29
+ # @param child_klass [Class]
30
+ # @return [void]
31
+ #
32
+ # @api private
33
+ # @since 0.2.0
34
+ def inherited(child_klass)
35
+ inherited_config_klass = Class.new(@__qonfig_config_klass__)
36
+
37
+ child_klass.instance_variable_set(:@__qonfig_definition_lock__, Mutex.new)
38
+ child_klass.instance_variable_set(:@__qonfig_access_lock__, Mutex.new)
39
+ child_klass.instance_variable_set(:@__qonfig_config_klass__, inherited_config_klass)
40
+ child_klass.instance_variable_set(:@__qonfig_config__, nil)
41
+
42
+ super
43
+ end
44
+ end
45
+
46
+ # @api private
47
+ # @since 0.2.0
48
+ module ClassMethods
49
+ # @param block [Proc]
50
+ # @return [void]
51
+ #
52
+ # @api public
53
+ # @since 0.2.0
54
+ def configuration(&block)
55
+ @__qonfig_definition_lock__.synchronize do
56
+ @__qonfig_config_klass__.instance_eval(&block) if block_given?
57
+ end
58
+ end
59
+
60
+ # @param options_map [Hash]
61
+ # @param block [Proc]
62
+ # @return [void]
63
+ #
64
+ # @api public
65
+ # @since 0.2.0
66
+ def configure(options_map = {}, &block)
67
+ @__qonfig_access_lock__.synchronize do
68
+ config.configure(options_map, &block)
69
+ end
70
+ end
71
+
72
+ # @return [Qonfig::DataSet]
73
+ #
74
+ # @api public
75
+ # @since 0.2.0
76
+ def config
77
+ @__qonfig_definition_lock__.synchronize do
78
+ @__qonfig_config__ ||= @__qonfig_config_klass__.new
79
+ end
80
+ end
81
+ end
82
+
83
+ # @api private
84
+ # @since 0.2.0
85
+ module InstanceMethods
86
+ # @return [Qonfig::DataSet]
87
+ #
88
+ # @api public
89
+ # @since 0.2.0
90
+ def config
91
+ self.class.instance_variable_get(:@__qonfig_definition_lock__).synchronize do
92
+ @__qonfig_config__ ||= self.class.instance_variable_get(:@__qonfig_config_klass__).new
93
+ end
94
+ end
95
+
96
+ # @return [Qonfig::DataSet]
97
+ #
98
+ # @api public
99
+ # @since 0.6.0
100
+ def shared_config
101
+ self.class.config
102
+ end
103
+
104
+ # @param options_map [Hash]
105
+ # @param block [Proc]
106
+ # @return [void]
107
+ #
108
+ # @api public
109
+ # @since 0.2.0
110
+ def configure(options_map = {}, &block)
111
+ self.class.instance_variable_get(:@__qonfig_access_lock__).synchronize do
112
+ config.configure(options_map, &block)
113
+ end
114
+ end
115
+ end
116
+ end
data/lib/qonfig/data_set.rb ADDED
@@ -0,0 +1,213 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api public
4
+ # @since 0.1.0
5
+ class Qonfig::DataSet
6
+ require_relative 'data_set/class_builder'
7
+ require_relative 'data_set/validator'
8
+
9
+ # @since 0.1.0
10
+ extend Qonfig::DSL
11
+
12
+ # @return [Qonfig::Settings]
13
+ #
14
+ # @api private
15
+ # @since 0.1.0
16
+ attr_reader :settings
17
+
18
+ # @param options_map [Hash]
19
+ # @param configurations [Proc]
20
+ #
21
+ # @api public
22
+ # @since 0.1.0
23
+ def initialize(options_map = {}, &configurations)
24
+ @__access_lock__ = Mutex.new
25
+ @__definition_lock__ = Mutex.new
26
+
27
+ thread_safe_definition { load!(options_map, &configurations) }
28
+ end
29
+
30
+ # @return [void]
31
+ #
32
+ # @api public
33
+ # @since 0.1.0
34
+ def freeze!
35
+ thread_safe_access { settings.__freeze__ }
36
+ end
37
+
38
+ # @return [void]
39
+ #
40
+ # @api public
41
+ # @since 0.2.0
42
+ def frozen?
43
+ thread_safe_access { settings.__is_frozen__ }
44
+ end
45
+
46
+ # @param options_map [Hash]
47
+ # @param configurations [Proc]
48
+ # @return [void]
49
+ #
50
+ # @raise [Qonfig::FrozenSettingsError]
51
+ #
52
+ # @api public
53
+ # @since 0.2.0
54
+ def reload!(options_map = {}, &configurations)
55
+ thread_safe_definition do
56
+ raise Qonfig::FrozenSettingsError, 'Frozen config can not be reloaded' if frozen?
57
+ load!(options_map, &configurations)
58
+ end
59
+ end
60
+
61
+ # @param options_map [Hash]
62
+ # @return [void]
63
+ #
64
+ # @api public
65
+ # @since 0.1.0
66
+ def configure(options_map = {})
67
+ thread_safe_access do
68
+ settings.__apply_values__(options_map)
69
+ yield(settings) if block_given?
70
+ end
71
+ end
72
+
73
+ # @option key_transformer [Proc]
74
+ # @option value_transformer [Proc]
75
+ # @return [Hash]
76
+ #
77
+ # @api public
78
+ # @since 0.1.0
79
+ def to_h(
80
+ key_transformer: Qonfig::Settings::BASIC_SETTING_KEY_TRANSFORMER,
81
+ value_transformer: Qonfig::Settings::BASIC_SETTING_VALUE_TRANSFORMER
82
+ )
83
+ thread_safe_access do
84
+ settings.__to_hash__(
85
+ transform_key: key_transformer,
86
+ transform_value: value_transformer
87
+ )
88
+ end
89
+ end
90
+ alias_method :to_hash, :to_h
91
+
92
+ # @option path [String]
93
+ # @option options [Hash<Symbol|String,Any>] Native (ruby-stdlib) ::JSON#generate attributes
94
+ # @param value_processor [Block]
95
+ # @return [void]
96
+ #
97
+ # @api public
98
+ # @since 0.11.0
99
+ def save_to_json(path:, options: Qonfig::Uploaders::JSON::DEFAULT_OPTIONS, &value_processor)
100
+ thread_safe_access do
101
+ Qonfig::Uploaders::JSON.upload(settings, path: path, options: options, &value_processor)
102
+ end
103
+ end
104
+ alias_method :dump_to_json, :save_to_json
105
+
106
+ # @option path [String]
107
+ # @option symbolize_keys [Boolean]
108
+ # @option options [Hash<Symbol|String,Any>] Native (ruby-stdlib) ::YAML#dump attributes
109
+ # @param value_processor [Block]
110
+ # @return [void]
111
+ #
112
+ # @api public
113
+ # @since 0.11.0
114
+ def save_to_yaml(
115
+ path:,
116
+ symbolize_keys: false,
117
+ options: Qonfig::Uploaders::YAML::DEFAULT_OPTIONS,
118
+ &value_processor
119
+ )
120
+ thread_safe_access do
121
+ Qonfig::Uploaders::YAML.upload(
122
+ settings,
123
+ path: path,
124
+ options: options.merge(symbolize_keys: symbolize_keys),
125
+ &value_processor
126
+ )
127
+ end
128
+ end
129
+ alias_method :dump_to_yaml, :save_to_yaml
130
+
131
+ # @param key [String, Symbol]
132
+ # @return [Object]
133
+ #
134
+ # @api public
135
+ # @since 0.2.0
136
+ def [](key)
137
+ thread_safe_access { settings[key] }
138
+ end
139
+
140
+ # @param keys [Array<String, Symbol>]
141
+ # @return [Object]
142
+ #
143
+ # @api public
144
+ # @since 0.2.0
145
+ def dig(*keys)
146
+ thread_safe_access { settings.__dig__(*keys) }
147
+ end
148
+
149
+ # @param keys [Array<String, Symbol>]
150
+ # @return [Hash]
151
+ #
152
+ # @api public
153
+ # @since 0.9.0
154
+ def slice(*keys)
155
+ thread_safe_access { settings.__slice__(*keys) }
156
+ end
157
+
158
+ # @param keys [Array<String, Symbol>]
159
+ # @return [Hash,Any]
160
+ #
161
+ # @api public
162
+ # @since 0.10.0
163
+ def slice_value(*keys)
164
+ thread_safe_access { settings.__slice_value__(*keys) }
165
+ end
166
+
167
+ # @return [void]
168
+ #
169
+ # @api public
170
+ # @since 0.2.0
171
+ def clear!
172
+ thread_safe_access { settings.__clear__ }
173
+ end
174
+
175
+ private
176
+
177
+ # @return [Qonfig::Settings]
178
+ #
179
+ # @api private
180
+ # @since 0.2.0
181
+ def build_settings
182
+ Qonfig::Settings::Builder.build(self.class.commands.dup)
183
+ end
184
+
185
+ # @param options_map [Hash]
186
+ # @param configurations [Proc]
187
+ # @return [void]
188
+ #
189
+ # @api private
190
+ # @since 0.2.0
191
+ def load!(options_map = {}, &configurations)
192
+ @settings = build_settings
193
+ configure(options_map, &configurations)
194
+ end
195
+
196
+ # @param instructions [Proc]
197
+ # @return [Object]
198
+ #
199
+ # @api private
200
+ # @since 0.2.0
201
+ def thread_safe_access(&instructions)
202
+ @__access_lock__.synchronize(&instructions)
203
+ end
204
+
205
+ # @param instructions [Proc]
206
+ # @return [Object]
207
+ #
208
+ # @api private
209
+ # @since 0.2.0
210
+ def thread_safe_definition(&instructions)
211
+ @__definition_lock__.synchronize(&instructions)
212
+ end
213
+ end
data/lib/qonfig/data_set/class_builder.rb ADDED
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ # @since 0.2.0
5
+ module Qonfig::DataSet::ClassBuilder
6
+ class << self
7
+ # @param hash [Hash]
8
+ # @return [Class<Qonfig::DataSet>]
9
+ #
10
+ # @see Qonfig::DataSet
11
+ #
12
+ # @api private
13
+ # @since 0.2.0
14
+ def build_from_hash(hash)
15
+ Class.new(Qonfig::DataSet).tap do |data_set_klass|
16
+ hash.each_pair do |key, value|
17
+ if value.is_a?(Hash) && value.any?
18
+ sub_data_set_klass = build_from_hash(value)
19
+ data_set_klass.setting(key) { compose sub_data_set_klass }
20
+ else
21
+ data_set_klass.setting key, value
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
data/lib/qonfig/data_set/validator.rb ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api public
4
+ # @since 0.?.0
5
+ class Qonfig::DataSet::Validator
6
+ # TODO: think about validation layer
7
+ end
data/lib/qonfig/dsl.rb ADDED
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @api private
4
+ # @since 0.1.0
5
+ module Qonfig::DSL
6
+ class << self
7
+ # @param child_klass [Qonfig::DataSet]
8
+ # @return [void]
9
+ #
10
+ # @api private
11
+ # @since 0.1.0
12
+ def extended(child_klass)
13
+ child_klass.instance_variable_set(:@commands, Qonfig::CommandSet.new)
14
+
15
+ child_klass.singleton_class.prepend(Module.new do
16
+ def inherited(child_klass)
17
+ child_klass.instance_variable_set(:@commands, Qonfig::CommandSet.new)
18
+ child_klass.commands.concat(commands)
19
+ super
20
+ end
21
+ end)
22
+ end
23
+ end
24
+
25
+ # @return [Qonfig::CommandSet]
26
+ #
27
+ # @api private
28
+ # @since 0.1.0
29
+ def commands
30
+ @commands
31
+ end
32
+
33
+ # @param key [Symbol, String]
34
+ # @param initial_value [Object]
35
+ # @param nested_settings [Proc]
36
+ # @return [void]
37
+ #
38
+ # @see Qonfig::Commands::AddNestedOption
39
+ # @see Qonfig::Commands::AddOption
40
+ #
41
+ # @api public
42
+ # @since 0.1.0
43
+ def setting(key, initial_value = nil, &nested_settings)
44
+ if block_given?
45
+ commands << Qonfig::Commands::AddNestedOption.new(key, nested_settings)
46
+ else
47
+ commands << Qonfig::Commands::AddOption.new(key, initial_value)
48
+ end
49
+ end
50
+
51
+ # @param data_set_klass [Class<Qonfig::DataSet>]
52
+ # @return [void]
53
+ #
54
+ # @see Qonfig::Comamnds::Compose
55
+ #
56
+ # @api private
57
+ # @sine 0.1.0
58
+ def compose(data_set_klass)
59
+ commands << Qonfig::Commands::Compose.new(data_set_klass)
60
+ end
61
+
62
+ # @param file_path [String]
63
+ # @option strict [Boolean]
64
+ # @return [void]
65
+ #
66
+ # @see Qonfig::Commands::LoadFromYAML
67
+ #
68
+ # @api public
69
+ # @since 0.2.0
70
+ def load_from_yaml(file_path, strict: true)
71
+ commands << Qonfig::Commands::LoadFromYAML.new(file_path, strict: strict)
72
+ end
73
+
74
+ # @return [void]
75
+ #
76
+ # @see Qonfig::Commands::LoadFromSelf
77
+ #
78
+ # @api public
79
+ # @since 0.2.0
80
+ def load_from_self
81
+ caller_location = caller(1, 1).first
82
+ commands << Qonfig::Commands::LoadFromSelf.new(caller_location)
83
+ end
84
+
85
+ # @option convert_values [Boolean]
86
+ # @option prefix [NilClass, String, Regexp]
87
+ # @return [void]
88
+ #
89
+ # @see Qonfig::Commands::LoadFromENV
90
+ #
91
+ # @api public
92
+ # @since 0.2.0
93
+ def load_from_env(convert_values: false, prefix: nil, trim_prefix: false)
94
+ commands << Qonfig::Commands::LoadFromENV.new(
95
+ convert_values: convert_values,
96
+ prefix: prefix,
97
+ trim_prefix: trim_prefix
98
+ )
99
+ end
100
+
101
+ # @param file_path [String]
102
+ # @option strict [Boolean]
103
+ # @return [void]
104
+ #
105
+ # @api public
106
+ # @since 0.5.0
107
+ def load_from_json(file_path, strict: true)
108
+ commands << Qonfig::Commands::LoadFromJSON.new(file_path, strict: strict)
109
+ end
110
+
111
+ # @param file_path [String]
112
+ # @option strict [Boolean]
113
+ # @option via [Symbol]
114
+ # @option env [Symbol, String]
115
+ # @return [void]
116
+ #
117
+ # @api public
118
+ # @since 0.7.0
119
+ def expose_yaml(file_path, strict: true, via:, env:)
120
+ commands << Qonfig::Commands::ExposeYAML.new(file_path, strict: strict, via: via, env: env)
121
+ end
122
+ end
data/lib/qonfig/errors.rb ADDED
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qonfig
4
+ # @api public
5
+ # @since 0.1.0
6
+ Error = Class.new(StandardError)
7
+
8
+ # @api public
9
+ # @since 0.1.0
10
+ ArgumentError = Class.new(ArgumentError)
11
+
12
+ # @api public
13
+ # @since 0.12.0
14
+ PluginError = Class.new(Error)
15
+
16
+ # @api public
17
+ # @since 0.11.0
18
+ IncorrectHashTransformationError = Class.new(ArgumentError)
19
+
20
+ # @api public
21
+ # @since 0.11.0
22
+ IncorrectKeyTransformerError = Class.new(IncorrectHashTransformationError)
23
+
24
+ # @api public
25
+ # @since 0.11.0
26
+ IncorrectValueTransformerError = Class.new(IncorrectHashTransformationError)
27
+
28
+ # @see Qonfig::Settings
29
+ #
30
+ # @api public
31
+ # @since 0.1.0
32
+ UnknownSettingError = Class.new(Error)
33
+
34