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 +4 -4
- data/CHANGELOG.md +17 -1
- data/README.md +35 -8
- data/lib/unifig/config.rb +3 -0
- data/lib/unifig/errors.rb +12 -3
- data/lib/unifig/init.rb +106 -100
- data/lib/unifig/providers/local.rb +2 -9
- data/lib/unifig/providers.rb +14 -4
- data/lib/unifig/var.rb +40 -5
- data/lib/unifig/vars.rb +79 -0
- data/lib/unifig/version.rb +1 -1
- data/lib/unifig.rb +4 -1
- data/spec/unifig/config_spec.rb +10 -0
- data/spec/unifig/init_spec.rb +101 -16
- data/spec/unifig/providers_spec.rb +8 -0
- data/spec/unifig/var_spec.rb +14 -0
- data/spec/unifig/vars_spec.rb +41 -0
- metadata +20 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b77565f94f0a6c120bd7e8b2168fcd09186ca66fc0973a73cf0b6c238656eba7
|
4
|
+
data.tar.gz: e48489beb9e543a1c22e6e8e449124074a1d0eb957acc03f5e795eef81bd8387
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fd21aca175ebf8f3ab9254b4028147a6c3f138d0ea1c43fadf7457a8345acf935dc4044679a3fd7f7943c2479062da0f1e70eccb4acaae1b6cd21e0b89f7f08e
|
7
|
+
data.tar.gz: c2850b58cca17dfac8bc9e0d0dbbf0b875a32613558f5aedd8d5b92d30b465aa8a37babd8911e6d9478c46cf27e7c22d6d11f443c19895c2d471e2cc7ed45831
|
data/CHANGELOG.md
CHANGED
@@ -1,4 +1,19 @@
|
|
1
|
-
# [0.
|
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.
|
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.
|
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
|
-
```
|
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
|
-
```
|
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
|
-
```
|
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
|
-
|
11
|
+
MissingConfigError = Class.new(Error)
|
12
12
|
|
13
13
|
# Raised if there is no provider that matches the one given in the config.
|
14
|
-
|
14
|
+
MissingProviderError = Class.new(Error)
|
15
15
|
|
16
16
|
# Raised if a required var is blank.
|
17
|
-
|
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
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
-
|
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
|
-
|
57
|
-
raise
|
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
|
-
|
60
|
-
|
59
|
+
providers = Providers.list(config.providers)
|
60
|
+
return if providers.empty?
|
61
61
|
|
62
|
-
|
63
|
-
def exec!
|
64
|
-
providers = Providers.list(@config.providers)
|
65
|
-
return if providers.empty?
|
62
|
+
Vars.load!(yml, env)
|
66
63
|
|
67
|
-
|
64
|
+
fetch_from_providers!(providers)
|
68
65
|
|
69
|
-
|
70
|
-
vars = fetch_and_set_methods(provider, vars)
|
71
|
-
end
|
66
|
+
check_required_vars
|
72
67
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
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
|
-
|
81
|
-
|
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
|
-
|
80
|
+
Vars.write_results!(result, provider)
|
81
|
+
end
|
82
|
+
end
|
84
83
|
|
85
|
-
|
86
|
-
|
87
|
-
|
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
|
-
|
92
|
-
|
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
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
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
|
-
|
110
|
-
|
111
|
-
|
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
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
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
|
-
|
121
|
-
|
122
|
-
|
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
|
-
|
127
|
-
|
128
|
-
|
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
|
-
|
18
|
-
[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
|
data/lib/unifig/providers.rb
CHANGED
@@ -3,14 +3,24 @@
|
|
3
3
|
module Unifig
|
4
4
|
# @private
|
5
5
|
module Providers
|
6
|
-
# @raise [
|
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
|
12
|
-
|
13
|
-
|
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
|
-
#
|
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
|
-
|
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
|
data/lib/unifig/vars.rb
ADDED
@@ -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
|
data/lib/unifig/version.rb
CHANGED
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/
|
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/unifig/config_spec.rb
CHANGED
@@ -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]
|
data/spec/unifig/init_spec.rb
CHANGED
@@ -17,20 +17,7 @@ RSpec.shared_examples 'basic load tests' do
|
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
20
|
-
context '
|
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::
|
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::
|
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
|
data/spec/unifig/var_spec.rb
CHANGED
@@ -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.
|
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-
|
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
|