unifig 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2daa78635ee2bccb9232401f2d8c26ad3845069ec0445ffbadf2657ac0ea85bd
4
- data.tar.gz: 196d2431dc1025377ca12c5108dc6398c7c473b11d3daaf204dd3621a13cf4f7
3
+ metadata.gz: b77565f94f0a6c120bd7e8b2168fcd09186ca66fc0973a73cf0b6c238656eba7
4
+ data.tar.gz: e48489beb9e543a1c22e6e8e449124074a1d0eb957acc03f5e795eef81bd8387
5
5
  SHA512:
6
- metadata.gz: 81359820cd6783317b0697ca4bac3161cd59650e76449d60a17e3b69eab1e5f46f7910643af53229859ebaa32886c9724728fbd490ddb7d76142593f1bae483a
7
- data.tar.gz: d2f52f43b879df6941bcc790e22de92bc4ee75d5a0115b59b3b38454882481e525ff01847f09e009d6f3b0407fe66814426764981112a89d87b27996474b3cd5
6
+ metadata.gz: fd21aca175ebf8f3ab9254b4028147a6c3f138d0ea1c43fadf7457a8345acf935dc4044679a3fd7f7943c2479062da0f1e70eccb4acaae1b6cd21e0b89f7f08e
7
+ data.tar.gz: c2850b58cca17dfac8bc9e0d0dbbf0b875a32613558f5aedd8d5b92d30b465aa8a37babd8911e6d9478c46cf27e7c22d6d11f443c19895c2d471e2cc7ed45831
data/CHANGELOG.md CHANGED
@@ -1,4 +1,19 @@
1
- # [0.2.0][] (TBD)
1
+ # [0.3.0][] (2022-07-31)
2
+
3
+ ## Changed
4
+
5
+ - Renamed some errors so they all end in `Error`.
6
+
7
+ ## Added
8
+
9
+ - Raise an error if a two or more variable names result in the same method name.
10
+ - Any `nil` values are now handled here so providers don't have to worry about it.
11
+ - Variable substituion
12
+ - `Unifig::Vars` will now contain information about the loaded variables.
13
+
14
+ # [0.2.0][] (2022-07-24)
15
+
16
+ ## Changed
2
17
 
3
18
  - An environment is no longer required.
4
19
 
@@ -6,5 +21,6 @@
6
21
 
7
22
  Initial release.
8
23
 
24
+ [0.3.0]: https://github.com/AaronLasseigne/unifig/compare/v0.2.0...v0.3.0
9
25
  [0.2.0]: https://github.com/AaronLasseigne/unifig/compare/v0.1.0...v0.2.0
10
26
  [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.0'
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.0'
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)
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.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,60 @@
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
+ end
37
+
38
+ # The name of the method this variable can be found using.
39
+ #
40
+ # @return [Symbol]
14
41
  def method
15
42
  @method ||= name.to_s.downcase.tr('-', '_').to_sym
16
43
  end
17
44
 
45
+ # @private
18
46
  def local_value
19
- @local_value ||= env_config(:value) || config[:value]
47
+ @local_value ||= env_config(:value) || @config[:value]
20
48
  end
21
49
 
50
+ # Returns whether or not this is a required variable.
51
+ #
52
+ # @return [Boolean]
22
53
  def required?
23
54
  return @required if defined?(@required)
24
55
 
25
56
  optional = env_config(:optional)
26
- optional = config[:optional] if optional.nil?
57
+ optional = @config[:optional] if optional.nil?
27
58
  optional = false if optional.nil?
28
59
  @required = !optional
29
60
  end
@@ -31,7 +62,11 @@ module Unifig
31
62
  private
32
63
 
33
64
  def env_config(key)
34
- config.dig(:envs, env, key)
65
+ @config.dig(:envs, @env, key)
66
+ end
67
+
68
+ def blank?(value)
69
+ value.nil? || (value.respond_to?(:to_str) && value.to_str.strip.empty?)
35
70
  end
36
71
  end
37
72
  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.0'
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
@@ -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:
@@ -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,20 @@ 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
+ end
29
+
16
30
  describe '#local_value' do
17
31
  context 'with no value' do
18
32
  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.0
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-07-31 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