unifig 0.2.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2daa78635ee2bccb9232401f2d8c26ad3845069ec0445ffbadf2657ac0ea85bd
4
- data.tar.gz: 196d2431dc1025377ca12c5108dc6398c7c473b11d3daaf204dd3621a13cf4f7
3
+ metadata.gz: 89378b794de9cdd9511c52a145c4458317713e357634690201b3af379fd19e23
4
+ data.tar.gz: 51642a4f0f6e7bf60fbc6715427678344b81b21c8b3cbe8bbd51d392169c0413
5
5
  SHA512:
6
- metadata.gz: 81359820cd6783317b0697ca4bac3161cd59650e76449d60a17e3b69eab1e5f46f7910643af53229859ebaa32886c9724728fbd490ddb7d76142593f1bae483a
7
- data.tar.gz: d2f52f43b879df6941bcc790e22de92bc4ee75d5a0115b59b3b38454882481e525ff01847f09e009d6f3b0407fe66814426764981112a89d87b27996474b3cd5
6
+ metadata.gz: 5c0ff6dd223e7999c825c2f63894ade93b687927b3473b277a65f382526cb811f8588726b530ec7d5ed4dda8fc33d2b849453b17edf562a6c8850e828d60b1f3
7
+ data.tar.gz: fed3b1b88b3f3e2b8397e29d6cd191047e8239a63983e34fb5b34d61b3eee7f1c65906fbcd0ab6e32fb1f19f83a5badfeafc3f9ee6cc05c074e5c0877eae1b37
data/CHANGELOG.md CHANGED
@@ -1,4 +1,32 @@
1
- # [0.2.0][] (TBD)
1
+ # [0.3.2][] (2022-08-01)
2
+
3
+ ## Fixed
4
+
5
+ - Provider in Unifig::Var returned incorrectly
6
+ - Fix early calls to Unifig::Vars.list
7
+
8
+ # [0.3.1][] (2022-07-31)
9
+
10
+ ## Fixed
11
+
12
+ - Frozen strings returned from providers caused an error.
13
+
14
+ # [0.3.0][] (2022-07-31)
15
+
16
+ ## Changed
17
+
18
+ - Renamed some errors so they all end in `Error`.
19
+
20
+ ## Added
21
+
22
+ - Raise an error if a two or more variable names result in the same method name.
23
+ - Any `nil` values are now handled here so providers don't have to worry about it.
24
+ - Variable substituion
25
+ - `Unifig::Vars` will now contain information about the loaded variables.
26
+
27
+ # [0.2.0][] (2022-07-24)
28
+
29
+ ## Changed
2
30
 
3
31
  - An environment is no longer required.
4
32
 
@@ -6,5 +34,8 @@
6
34
 
7
35
  Initial release.
8
36
 
37
+ [0.3.2]: https://github.com/AaronLasseigne/unifig/compare/v0.3.1...v0.3.2
38
+ [0.3.1]: https://github.com/AaronLasseigne/unifig/compare/v0.3.0...v0.3.1
39
+ [0.3.0]: https://github.com/AaronLasseigne/unifig/compare/v0.2.0...v0.3.0
9
40
  [0.2.0]: https://github.com/AaronLasseigne/unifig/compare/v0.1.0...v0.2.0
10
41
  [0.1.0]: https://github.com/AaronLasseigne/unifig/compare/v0.0.0...v0.1.0
data/README.md CHANGED
@@ -7,16 +7,24 @@ Unifig is a pluggable system for loading external variables from one or more pro
7
7
 
8
8
  ## Installation
9
9
 
10
+ If you are using a framework you should install the associated gem:
11
+
12
+ | Framework | Gem |
13
+ | --------- | ---------------- |
14
+ | Rails | [unifig-rails][] |
15
+
16
+ If you want to use Unifig outside of a framework listed above you can manually add it to your project.
17
+
10
18
  Add it to your Gemfile:
11
19
 
12
20
  ``` rb
13
- gem 'unifig', '~> 0.2.0'
21
+ gem 'unifig', '~> 0.3.2'
14
22
  ```
15
23
 
16
24
  Or install it manually:
17
25
 
18
26
  ``` sh
19
- $ gem install unifig --version '~> 0.2.0'
27
+ $ gem install unifig --version '~> 0.3.2'
20
28
  ```
21
29
 
22
30
  This project uses [Semantic Versioning][].
@@ -61,6 +69,17 @@ HELLO:
61
69
  value: "dlrow"
62
70
  ```
63
71
 
72
+ ### Variable Substitutions
73
+
74
+ Variables can be used in other variables with `${VAR}`.
75
+
76
+ ```rb
77
+ USER:
78
+ SERVICE_USERNAME: "${USER}_at_service"
79
+ ```
80
+
81
+ Order of the variables does not matter but cyclical dependencies will throw an error.
82
+
64
83
  ### Loading
65
84
 
66
85
  Loading a configuration is handled through the `Unifig::Init` class.
@@ -70,7 +89,7 @@ All variables are assumed to be required (not `nil` or a blank string).
70
89
  Variables can be made optional by using the `:optional` setting.
71
90
  If there is a required variable without a value, Unifig will throw an error when loading.
72
91
 
73
- ``` rb
92
+ ```rb
74
93
  Unifig::Init.load(<<~YAML, env: :production)
75
94
  config:
76
95
  providers: local
@@ -99,7 +118,7 @@ YAML
99
118
 
100
119
  If we replaced `:development` with `:production` inside the `load` call we'd get:
101
120
 
102
- ``` rb
121
+ ```rb
103
122
  > Unifig.host?
104
123
  # true
105
124
  > Unifig.host
@@ -112,7 +131,7 @@ If we replaced `:development` with `:production` inside the `load` call we'd get
112
131
 
113
132
  You can load from a configuration file by using `load_file`.
114
133
 
115
- ``` rb
134
+ ```rb
116
135
  Unifig::Init.load_file('unifig.yml', env: :production)
117
136
  ```
118
137
 
@@ -131,9 +150,15 @@ This is the case for the overall configuration as well as any variable configura
131
150
 
132
151
  ### Providers
133
152
 
134
- | Provider | Gem |
135
- | -------- | -------- |
136
- | Local | Built-in |
153
+ | Provider | Gem |
154
+ | -------- | -------------- |
155
+ | Local | Built-in |
156
+ | ENV | [unifig-env][] |
157
+
158
+ ### Unifig::Vars
159
+
160
+ After loading the configuration you can use `Unifig::Vars` to check on what happened.
161
+ It will return a list of `Unifig::Var`s with information such as which provider supplied the value.
137
162
 
138
163
  ## Contributing
139
164
 
@@ -145,10 +170,12 @@ A [complete list of contributors][] is available on GitHub.
145
170
  Unifig is licensed under [the MIT License][].
146
171
 
147
172
  [Unifig]: https://github.com/AaronLasseigne/unifig
173
+ [unifig-rails]: https://github.com/AaronLasseigne/unifig-rails
148
174
  [Semantic Versioning]: http://semver.org/spec/v2.0.0.html
149
175
  [GitHub releases]: https://github.com/AaronLasseigne/unifig/releases
150
176
  [YAML configuration]: #yaml-configuration
151
177
  [API Documentation]: http://rubydoc.info/github/AaronLasseigne/unifig
152
178
  [our contribution guidelines]: CONTRIBUTING.md
179
+ [unifig-env]: https://github.com/AaronLasseigne/unifig-env
153
180
  [complete list of contributors]: https://github.com/AaronLasseigne/unifig/graphs/contributors
154
181
  [the MIT License]: LICENSE.txt
data/lib/unifig/config.rb CHANGED
@@ -3,7 +3,10 @@
3
3
  module Unifig
4
4
  # @private
5
5
  class Config
6
+ # @raise [MissingConfigError] - No config section was provided.
6
7
  def initialize(config, env: nil)
8
+ raise MissingConfigError, 'no configuration provided' unless config
9
+
7
10
  @env_config = config.slice(:providers)
8
11
  @env = env
9
12
 
data/lib/unifig/errors.rb CHANGED
@@ -8,11 +8,20 @@ module Unifig
8
8
  YAMLSyntaxError = Class.new(Error)
9
9
 
10
10
  # Raised if there is no config at the start of the YAML.
11
- MissingConfig = Class.new(Error)
11
+ MissingConfigError = Class.new(Error)
12
12
 
13
13
  # Raised if there is no provider that matches the one given in the config.
14
- MissingProvider = Class.new(Error)
14
+ MissingProviderError = Class.new(Error)
15
15
 
16
16
  # Raised if a required var is blank.
17
- MissingRequired = Class.new(Error)
17
+ MissingRequiredError = Class.new(Error)
18
+
19
+ # Raised if a variable produces a duplicate method name.
20
+ DuplicateNameError = Class.new(Error)
21
+
22
+ # Raised if substitutions result in a cyclical dependency.
23
+ CyclicalSubstitutionError = Class.new(Error)
24
+
25
+ # Raised if a substitution does not exist.
26
+ MissingSubstitutionError = Class.new(Error)
18
27
  end
data/lib/unifig/init.rb CHANGED
@@ -4,128 +4,134 @@ require 'yaml'
4
4
 
5
5
  module Unifig
6
6
  # Initializes Unifig with methods based on YAML.
7
- class Init
8
- # Loads a string of YAML to configure Unifig.
9
- #
10
- # @example
11
- # Unifig::Init.load(<<~YML, env: :development)
12
- # config:
13
- # envs:
14
- # development:
15
- # providers: local
16
- #
17
- # FOO_BAR:
18
- # value: "baz"
19
- # YML
20
- #
21
- # @param str [String] A YAML config.
22
- # @param env [Symbol] An environment name to load.
23
- #
24
- # @raise [YAMLSyntaxError] - Invalid YAML
25
- # @raise (see #initialize)
26
- # @raise (see Unifig::Providers.list)
27
- def self.load(str, env: nil)
28
- yml = Psych.load(str, symbolize_names: true)
29
- new(yml, env: env).exec!
30
- rescue Psych::SyntaxError, Psych::BadAlias => e
31
- raise YAMLSyntaxError, e.message
32
- end
7
+ module Init
8
+ class << self
9
+ # Loads a string of YAML to configure Unifig.
10
+ #
11
+ # @example
12
+ # Unifig::Init.load(<<~YML, env: :development)
13
+ # config:
14
+ # envs:
15
+ # development:
16
+ # providers: local
17
+ #
18
+ # FOO_BAR:
19
+ # value: "baz"
20
+ # YML
21
+ #
22
+ # @param str [String] A YAML config.
23
+ # @param env [Symbol] An environment name to load.
24
+ #
25
+ # @raise [YAMLSyntaxError] - Invalid YAML
26
+ # @raise (see .exec!)
27
+ def load(str, env: nil)
28
+ yml = Psych.load(str, symbolize_names: true)
29
+ exec!(yml, env: env)
30
+ rescue Psych::SyntaxError, Psych::BadAlias => e
31
+ raise YAMLSyntaxError, e.message
32
+ end
33
33
 
34
- # Loads a YAML file to configure Unifig.
35
- #
36
- # @example
37
- # Unifig::Init.load_file('config.yml', env: :development)
38
- #
39
- # @param file_path [String] The path to a YAML config file.
40
- # @param env [Symbol] An environment name to load.
41
- #
42
- # @raise (see Unifig::Init.load)
43
- def self.load_file(file_path, env: nil)
44
- # Ruby 2.7 Psych.load_file doesn't support the :symbolize_names flag.
45
- # After Ruby 2.7 this can be changed to Psych.load_file if that's faster.
46
- load(File.read(file_path), env: env)
47
- end
34
+ # Loads a YAML file to configure Unifig.
35
+ #
36
+ # @example
37
+ # Unifig::Init.load_file('config.yml', env: :development)
38
+ #
39
+ # @param file_path [String] The path to a YAML config file.
40
+ # @param env [Symbol] An environment name to load.
41
+ #
42
+ # @raise (see Unifig::Init.load)
43
+ def load_file(file_path, env: nil)
44
+ # Ruby 2.7 Psych.load_file doesn't support the :symbolize_names flag.
45
+ # After Ruby 2.7 this can be changed to Psych.load_file if that's faster.
46
+ load(File.read(file_path), env: env)
47
+ end
48
48
 
49
- # @private
50
- #
51
- # @raise [MissingConfig] - No config section was provided in the YAML.
52
- def initialize(yml, env: nil)
53
- @yml = yml
54
- @env = env
49
+ private
55
50
 
56
- config = @yml.delete(:config)
57
- raise MissingConfig unless config
51
+ # @raise [MissingRequiredError] - One or more required variables are missing values.
52
+ # @raise (see Unifig::Config#initialize)
53
+ # @raise (see Unifig::Providers.list)
54
+ # @raise (see Unifig::Var.load!)
55
+ # @raise (see .complete_substitutions!)
56
+ def exec!(yml, env: nil)
57
+ config = Config.new(yml.delete(:config), env: env)
58
58
 
59
- @config = Config.new(config, env: @env)
60
- end
59
+ providers = Providers.list(config.providers)
60
+ return if providers.empty?
61
61
 
62
- # @private
63
- def exec!
64
- providers = Providers.list(@config.providers)
65
- return if providers.empty?
62
+ Vars.load!(yml, env)
66
63
 
67
- vars = vars_and_set_local
64
+ fetch_from_providers!(providers)
68
65
 
69
- providers.each do |provider|
70
- vars = fetch_and_set_methods(provider, vars)
71
- end
66
+ check_required_vars
72
67
 
73
- required_vars, optional_vars = vars.values.partition(&:required?)
74
- if required_vars.any?
75
- raise MissingRequired, <<~MSG
76
- Missing Required Vars: #{required_vars.map(&:name).join(', ')}
77
- MSG
68
+ complete_substitutions!
69
+
70
+ missing_vars, vars = Vars.list.partition { |var| var.value.nil? }
71
+ attach_methods!(vars)
72
+ attach_missing_optional_methods!(missing_vars)
78
73
  end
79
74
 
80
- attach_optional_methods(optional_vars)
81
- end
75
+ def fetch_from_providers!(providers)
76
+ providers.each do |provider|
77
+ remaining_vars = Vars.list.filter_map { |var| var.name if var.value.nil? }
78
+ result = provider.retrieve(remaining_vars)
82
79
 
83
- private
80
+ Vars.write_results!(result, provider.name)
81
+ end
82
+ end
84
83
 
85
- def vars_and_set_local
86
- vars = {}
87
- local_values = {}
88
- @yml.each do |name, local_config|
89
- local_config = {} if local_config.nil?
84
+ def check_required_vars
85
+ missing_required_vars = Vars.list.select { |var| var.required? && var.value.nil? }
86
+ return if missing_required_vars.empty?
90
87
 
91
- vars[name] = Var.new(name, local_config, @env)
92
- local_values[name] = vars[name].local_value
88
+ raise MissingRequiredError, <<~MSG
89
+ variables without a value: #{missing_required_vars.map(&:name).join(', ')}
90
+ MSG
93
91
  end
94
- Unifig::Providers::Local.load(local_values)
95
- vars
96
- end
97
92
 
98
- def fetch_and_set_methods(provider, vars)
99
- values = provider.retrieve(vars.keys)
100
- values.each do |name, value|
101
- next values.delete(name) if blank_string?(value)
102
-
103
- attach_method(vars[name], value)
104
- attach_predicate(vars[name], true)
93
+ # @raise [CyclicalSubstitutionError] - Subtitutions resulted in a cyclical dependency.
94
+ # @raise [MissingSubstitutionError] - A substitution does not exist.
95
+ def complete_substitutions!
96
+ Vars.tsort.each do |name|
97
+ var = Vars[name]
98
+ next unless var.value.is_a?(String)
99
+
100
+ var.value = var.value.gsub(/\${[^}]+}/) do |match|
101
+ name = match[2..-2].to_sym
102
+ Vars[name].value
103
+ end
104
+ end
105
+ rescue TSort::Cyclic => e
106
+ names = e.message.scan(/:([^ \],]+)/).flatten
107
+
108
+ raise CyclicalSubstitutionError, "cyclical dependency: #{names.join(', ')}"
105
109
  end
106
- vars.slice(*(vars.keys - values.keys)) # switch to except after 2.7
107
- end
108
110
 
109
- def blank_string?(value)
110
- value.respond_to?(:to_str) && value.to_str.strip.empty?
111
- end
111
+ def attach_methods!(vars)
112
+ vars.each do |var|
113
+ attach_method!(var)
114
+ attach_predicate!(var, true)
115
+ end
116
+ end
112
117
 
113
- def attach_optional_methods(vars)
114
- vars.each do |var|
115
- attach_method(var, nil)
116
- attach_predicate(var, false)
118
+ def attach_missing_optional_methods!(vars)
119
+ vars.each do |var|
120
+ attach_method!(var)
121
+ attach_predicate!(var, false)
122
+ end
117
123
  end
118
- end
119
124
 
120
- def attach_method(var, value)
121
- Unifig.define_singleton_method(var.method) do
122
- value
125
+ def attach_method!(var)
126
+ Unifig.define_singleton_method(var.method) do
127
+ var.value
128
+ end
123
129
  end
124
- end
125
130
 
126
- def attach_predicate(var, bool)
127
- Unifig.define_singleton_method(:"#{var.method}?") do
128
- bool
131
+ def attach_predicate!(var, bool)
132
+ Unifig.define_singleton_method(:"#{var.method}?") do
133
+ bool
134
+ end
129
135
  end
130
136
  end
131
137
  end
@@ -8,17 +8,10 @@ module Unifig
8
8
  :local
9
9
  end
10
10
 
11
- # @private
12
- def self.load(data)
13
- @data = data
14
- end
15
-
16
11
  def self.retrieve(var_names)
17
- local_values = var_names.to_h do |name|
18
- [name, @data[name]]
12
+ var_names.to_h do |name|
13
+ [name, Vars[name].local_value]
19
14
  end
20
- local_values.compact!
21
- local_values
22
15
  end
23
16
  end
24
17
  end
@@ -3,14 +3,24 @@
3
3
  module Unifig
4
4
  # @private
5
5
  module Providers
6
- # @raise [MissingProvider] - The given provider is not in the list of available providers.
6
+ # @raise [MissingProviderError] - The given provider is not in the list of available providers.
7
7
  def self.list(providers = nil)
8
8
  return all if providers.nil?
9
9
 
10
10
  providers.map do |provider|
11
- all.detect { |pp| pp.name == provider }.tap do |found|
12
- raise MissingProvider, %("#{provider}" is not in the list of available providers) unless found
13
- end
11
+ all
12
+ .detect { |pp| pp.name == provider }
13
+ .tap do |found|
14
+ unless found
15
+ msg = %("#{provider}" is not in the list of available providers)
16
+
17
+ dym = DidYouMean::SpellChecker.new(dictionary: all.map(&:name))
18
+ correction = dym.correct(provider).first
19
+ msg += "\nDid you mean? #{correction}" if correction
20
+
21
+ raise MissingProviderError, msg
22
+ end
23
+ end
14
24
  end
15
25
  end
16
26
 
data/lib/unifig/var.rb CHANGED
@@ -1,29 +1,62 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Unifig
4
- # @private
4
+ # A variable created after loading a configuration.
5
5
  class Var
6
+ # @private
6
7
  def initialize(name, config, env)
7
8
  @name = name
8
9
  @config = config
9
10
  @env = env
11
+ @value = nil
12
+ @provider = nil
10
13
  end
11
14
 
12
- attr_reader :name, :config, :env
15
+ # The variable name.
16
+ #
17
+ # @return [Symbol]
18
+ attr_reader :name
13
19
 
20
+ # The provider that supplied the value.
21
+ #
22
+ # @return [Symbol]
23
+ attr_reader :provider # rubocop:disable Style/BisectedAttrAccessor
24
+
25
+ # @private
26
+ attr_writer :provider # rubocop:disable Style/BisectedAttrAccessor
27
+
28
+ # The value of the variable.
29
+ #
30
+ # @return [Object]
31
+ attr_reader :value
32
+
33
+ # @private
34
+ def value=(obj)
35
+ value = blank?(obj) ? nil : obj
36
+ value = value.dup.freeze unless value.frozen?
37
+ @value = value
38
+ end
39
+
40
+ # The name of the method this variable can be found using.
41
+ #
42
+ # @return [Symbol]
14
43
  def method
15
44
  @method ||= name.to_s.downcase.tr('-', '_').to_sym
16
45
  end
17
46
 
47
+ # @private
18
48
  def local_value
19
- @local_value ||= env_config(:value) || config[:value]
49
+ @local_value ||= env_config(:value) || @config[:value]
20
50
  end
21
51
 
52
+ # Returns whether or not this is a required variable.
53
+ #
54
+ # @return [Boolean]
22
55
  def required?
23
56
  return @required if defined?(@required)
24
57
 
25
58
  optional = env_config(:optional)
26
- optional = config[:optional] if optional.nil?
59
+ optional = @config[:optional] if optional.nil?
27
60
  optional = false if optional.nil?
28
61
  @required = !optional
29
62
  end
@@ -31,7 +64,11 @@ module Unifig
31
64
  private
32
65
 
33
66
  def env_config(key)
34
- config.dig(:envs, env, key)
67
+ @config.dig(:envs, @env, key)
68
+ end
69
+
70
+ def blank?(value)
71
+ value.nil? || (value.respond_to?(:to_str) && value.to_str.strip.empty?)
35
72
  end
36
73
  end
37
74
  end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Unifig
4
+ # Information about variables after loading a configuration.
5
+ module Vars
6
+ class << self
7
+ include TSort
8
+
9
+ # @raise [DuplicateNameError] - A variable produces a duplicate method name.
10
+ # @private
11
+ def load!(yml, env)
12
+ vars = yml.to_h do |name, config|
13
+ [name, Var.new(name, config || {}, env)]
14
+ end
15
+
16
+ vars
17
+ .values
18
+ .group_by(&:method)
19
+ .each do |method_name, list|
20
+ next unless list.size > 1
21
+
22
+ names = list.map { |var| %("#{var.name}") }.join(', ')
23
+ raise DuplicateNameError, "variables all result in the same method name (Unifig.#{method_name}): #{names}"
24
+ end
25
+
26
+ @map = vars
27
+ end
28
+
29
+ # @private
30
+ def write_results!(results, provider)
31
+ results.each do |name, value|
32
+ @map[name].value = value
33
+ @map[name].provider = provider unless @map[name].value.nil?
34
+ end
35
+ end
36
+
37
+ # Returns a list the variables.
38
+ #
39
+ # @return [Array<Unifig::Var>]
40
+ def list
41
+ (@map || {}).values
42
+ end
43
+
44
+ # Retrieve a variable by name unless it does not exist.
45
+ #
46
+ # @param name [Symbol] The name of the variable.
47
+ #
48
+ # @return [Unifig::Var or nil]
49
+ def [](name)
50
+ @map ||= {}
51
+ @map[name]
52
+ end
53
+
54
+ private
55
+
56
+ def tsort_each_node(&block)
57
+ @map.each_key(&block)
58
+ end
59
+
60
+ def tsort_each_child(node, &block)
61
+ var = @map.fetch(node) { raise_missing_substituion_error(node) }
62
+
63
+ return unless var.value.is_a?(String)
64
+
65
+ var.value.scan(/\${([^}]+)}/).flatten.map(&:to_sym).each(&block)
66
+ end
67
+
68
+ def raise_missing_substituion_error(name)
69
+ msg = "variable not found: #{name}"
70
+
71
+ dym = DidYouMean::SpellChecker.new(dictionary: @map.keys)
72
+ correction = dym.correct(name).first
73
+ msg += "\nDid you mean? #{correction}" if correction
74
+
75
+ raise MissingSubstitutionError, msg
76
+ end
77
+ end
78
+ end
79
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Unifig
4
- VERSION = '0.2.0'
4
+ VERSION = '0.3.2'
5
5
  end
data/lib/unifig.rb CHANGED
@@ -1,13 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'tsort'
4
+
3
5
  require_relative 'unifig/version'
4
6
 
5
7
  require_relative 'unifig/errors'
6
8
  require_relative 'unifig/config'
7
9
  require_relative 'unifig/var'
8
- require_relative 'unifig/init'
10
+ require_relative 'unifig/vars'
9
11
  require_relative 'unifig/providers'
10
12
  require_relative 'unifig/providers/local'
13
+ require_relative 'unifig/init'
11
14
 
12
15
  # Handle all your configuration variables.
13
16
  module Unifig
data/spec/spec_helper.rb CHANGED
@@ -43,7 +43,7 @@ RSpec.configure do |config|
43
43
 
44
44
  def self.retrieve(var_names)
45
45
  var_names.to_h do |var_name|
46
- [var_name, 42]
46
+ [var_name, '42'.freeze]
47
47
  end
48
48
  end
49
49
  end
@@ -13,6 +13,16 @@ RSpec.describe Unifig::Config do
13
13
  }
14
14
  end
15
15
 
16
+ describe '#new' do
17
+ context 'without a config' do
18
+ let(:config_hash) { nil }
19
+
20
+ it 'raises an error' do
21
+ expect { config }.to raise_error Unifig::MissingConfigError
22
+ end
23
+ end
24
+ end
25
+
16
26
  describe '#providers' do
17
27
  it 'returns a list of providers for the selected env' do
18
28
  expect(config.providers).to eql %i[local]
@@ -17,20 +17,7 @@ RSpec.shared_examples 'basic load tests' do
17
17
  end
18
18
  end
19
19
 
20
- context 'without a config' do
21
- let(:str) do
22
- <<~YML
23
- ONE:
24
- value: 1
25
- YML
26
- end
27
-
28
- it 'throws an error' do
29
- expect { subject }.to raise_error Unifig::MissingConfig
30
- end
31
- end
32
-
33
- context 'with a config' do
20
+ context 'with a valid config' do
34
21
  let(:str) do
35
22
  <<~YML
36
23
  config:
@@ -73,7 +60,7 @@ RSpec.describe Unifig::Init do
73
60
  it 'returns the values from the providers in order' do
74
61
  load
75
62
 
76
- expect(Unifig.one).to be 42
63
+ expect(Unifig.one).to eql '42'
77
64
  expect(Unifig.two).to be 2
78
65
  end
79
66
  end
@@ -179,7 +166,7 @@ RSpec.describe Unifig::Init do
179
166
  end
180
167
 
181
168
  it 'throws an error' do
182
- expect { load }.to raise_error Unifig::MissingRequired
169
+ expect { load }.to raise_error Unifig::MissingRequiredError
183
170
  end
184
171
  end
185
172
 
@@ -195,7 +182,105 @@ RSpec.describe Unifig::Init do
195
182
  end
196
183
 
197
184
  it 'throws an error' do
198
- expect { load }.to raise_error Unifig::MissingRequired
185
+ expect { load }.to raise_error Unifig::MissingRequiredError
186
+ end
187
+ end
188
+ end
189
+
190
+ context 'with substitutions' do
191
+ let(:str) do
192
+ <<~YML
193
+ config:
194
+ providers: local
195
+
196
+ NAME:
197
+ value: "world"
198
+ GREETING:
199
+ value: "Hello, ${NAME}!"
200
+ YML
201
+ end
202
+
203
+ it 'replaces the substitution' do
204
+ load
205
+
206
+ expect(Unifig.greeting).to eql 'Hello, world!'
207
+ end
208
+
209
+ context 'when they are out of order' do
210
+ let(:str) do
211
+ <<~YML
212
+ config:
213
+ providers: local
214
+
215
+ GREETING:
216
+ value: "Hello, ${NAME}!"
217
+ NAME:
218
+ value: "world"
219
+ YML
220
+ end
221
+
222
+ it 'replaces the substitution' do
223
+ load
224
+
225
+ expect(Unifig.greeting).to eql 'Hello, world!'
226
+ end
227
+ end
228
+
229
+ context 'when they chain' do
230
+ let(:str) do
231
+ <<~YML
232
+ config:
233
+ providers: local
234
+
235
+ INTRO:
236
+ value: "${GREETING} I'm Aaron."
237
+ NAME:
238
+ value: "world"
239
+ GREETING:
240
+ value: "Hello, ${NAME}!"
241
+ YML
242
+ end
243
+
244
+ it 'replaces the substitutions in order' do
245
+ load
246
+
247
+ expect(Unifig.intro).to eql "Hello, world! I'm Aaron."
248
+ end
249
+ end
250
+
251
+ context 'when they cause a cycle' do
252
+ let(:str) do
253
+ <<~YML
254
+ config:
255
+ providers: local
256
+
257
+ A:
258
+ value: "${B}"
259
+ B:
260
+ value: "${C}"
261
+ C:
262
+ value: "${A}"
263
+ YML
264
+ end
265
+
266
+ it 'raises an error' do
267
+ expect { load }.to raise_error Unifig::CyclicalSubstitutionError, 'cyclical dependency: A, B, C'
268
+ end
269
+ end
270
+
271
+ context 'when they do not exist' do
272
+ let(:str) do
273
+ <<~YML
274
+ config:
275
+ providers: local
276
+
277
+ A:
278
+ value: "${B}"
279
+ YML
280
+ end
281
+
282
+ it 'raises an error' do
283
+ expect { load }.to raise_error Unifig::MissingSubstitutionError
199
284
  end
200
285
  end
201
286
  end
@@ -32,5 +32,13 @@ RSpec.describe Unifig::Providers do
32
32
  list = described_class.list([Unifig::Providers::FortyTwo.name, Unifig::Providers::Local.name])
33
33
  expect(list).to eql [Unifig::Providers::FortyTwo, Unifig::Providers::Local]
34
34
  end
35
+
36
+ context 'with an invalid provider' do
37
+ it 'raises an error' do
38
+ expect do
39
+ described_class.list([Unifig::Providers::Local.name, :invalid])
40
+ end.to raise_error Unifig::MissingProviderError
41
+ end
42
+ end
35
43
  end
36
44
  end
@@ -13,6 +13,27 @@ RSpec.describe Unifig::Var do
13
13
  end
14
14
  end
15
15
 
16
+ describe '#value=' do
17
+ it 'writes the value' do
18
+ var.value = 'a'
19
+
20
+ expect(var.value).to eql 'a'
21
+ end
22
+
23
+ it 'write blank strings as nil' do
24
+ var.value = ' '
25
+
26
+ expect(var.value).to be_nil
27
+ end
28
+
29
+ it 'freezes the value if no frozen' do
30
+ var.value = 'a'
31
+
32
+ expect(var.value).to eql 'a'
33
+ expect(var.value).to be_frozen
34
+ end
35
+ end
36
+
16
37
  describe '#local_value' do
17
38
  context 'with no value' do
18
39
  it 'returns nil' do
@@ -0,0 +1,41 @@
1
+ RSpec.describe Unifig::Vars do
2
+ let(:yml) do
3
+ {
4
+ one: {
5
+ value: 1
6
+ },
7
+ two: {
8
+ value: 2
9
+ }
10
+ }
11
+ end
12
+ let(:env) { :development }
13
+ let(:results) { { one: 1, two: 2 } }
14
+
15
+ before do
16
+ described_class.load!(yml, env)
17
+ described_class.write_results!(results, :local)
18
+ end
19
+
20
+ describe '.list' do
21
+ it 'returns all of the vars' do
22
+ vars = described_class.list
23
+
24
+ expect(vars.size).to be 2
25
+ vars.each.with_index(1) do |var, value|
26
+ expect(var).to be_an_instance_of Unifig::Var
27
+ expect(var.value).to be value
28
+ expect(var.provider).to be :local
29
+ end
30
+ end
31
+ end
32
+
33
+ describe '.[]' do
34
+ it 'returns the Var based on the name' do
35
+ var = described_class[:one]
36
+
37
+ expect(var).to be_an_instance_of Unifig::Var
38
+ expect(var.value).to be 1
39
+ end
40
+ end
41
+ end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: unifig
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aaron Lasseigne
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-07-24 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2022-08-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: tsort
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 0.1.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 0.1.0
13
27
  description: A pluggable system for loading external variables from one or more providers
14
28
  (e.g. ENV).
15
29
  email:
@@ -29,12 +43,14 @@ files:
29
43
  - lib/unifig/providers.rb
30
44
  - lib/unifig/providers/local.rb
31
45
  - lib/unifig/var.rb
46
+ - lib/unifig/vars.rb
32
47
  - lib/unifig/version.rb
33
48
  - spec/spec_helper.rb
34
49
  - spec/unifig/config_spec.rb
35
50
  - spec/unifig/init_spec.rb
36
51
  - spec/unifig/providers_spec.rb
37
52
  - spec/unifig/var_spec.rb
53
+ - spec/unifig/vars_spec.rb
38
54
  homepage: https://github.com/AaronLasseigne/unifig
39
55
  licenses:
40
56
  - MIT
@@ -69,3 +85,4 @@ test_files:
69
85
  - spec/unifig/init_spec.rb
70
86
  - spec/unifig/providers_spec.rb
71
87
  - spec/unifig/var_spec.rb
88
+ - spec/unifig/vars_spec.rb