nero 0.2.2 → 0.4.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: 4a87cd3e7e7ebac49f614c881c242634459409e9578686de3afb50d1f771d293
4
- data.tar.gz: 0c8c81360ec1fc2d66460bf4849d162fc773a15bd5088913bdf00be6ff190533
3
+ metadata.gz: 9822fcc078e4b2c0e9f049da7cf6f37a98a5b9cf607434057f266a393154296e
4
+ data.tar.gz: b71a5be94f469b564e3a80c80a0545246afbc053f619201f8a0c6c0923a776a4
5
5
  SHA512:
6
- metadata.gz: 14d8c46a0a012c020b64ba2c4a850ba8b8881f6ab96c41b2ffc12bfe3311240348fc95d0da17f5adcff54c2a54768efbaebae66a51d3fb29b8fb217aa3f439e8
7
- data.tar.gz: 3d5f679720c0c89c019000fde2892b01d8fe7ce2cf9648c2a47db74c3541cd97ac685a2934b7c9082a051c80a57377dd9cf4ae251c41fddd00c1dc80819a4a00
6
+ metadata.gz: 863f2c0fbeb21286a0d0f51219e07351a93489688eb7c49be5c1c720a3e414d8e43129e2c0e4d23c6e44213e8547a03c28df128210026414e9e3e209bb7ef472
7
+ data.tar.gz: c431e49a4d5d422b554a891973c0e24b7c1810dd922492d56b39ca6157dc70e3110114d7474d0d2ed3b5076f2bea63d8493b2cd9db64c3354ea051022f8887cb
data/Appraisals ADDED
@@ -0,0 +1,7 @@
1
+ appraise "psych-3" do
2
+ gem "psych", "< 4"
3
+ end
4
+
5
+ appraise "psych-4" do
6
+ gem "psych", "< 5"
7
+ end
data/CHANGELOG.md CHANGED
@@ -1,4 +1,57 @@
1
1
  ## [Unreleased]
2
+ ...
3
+
4
+ ## [0.4.0] - 2025-02-15
5
+
6
+ - Add `!ref`-tag:
7
+ ```ruby
8
+ Nero.load(<<~YAML)
9
+ min_threads: !env [MIN_THREADS, !ref [max_threads]]
10
+ max_threads: 5
11
+ end
12
+ # => {min_threads: 5, max_threads: 5}
13
+ ```
14
+ - Support Psych v3
15
+ ...so it can used with Rails v6
16
+
17
+ ## [0.3.0] - 2025-02-02
18
+
19
+ - Add configuration
20
+ For custom tags:
21
+ ```ruby
22
+ Nero.configure do |nero|
23
+ nero.add_tag("duration") do |coder|
24
+ num, duration = coder.seq
25
+ mult = case duration
26
+ when /^seconds?/ then 1
27
+ when /^minutes?$/ then 60
28
+ when /^hours?$/ then 60 *60
29
+ when /^days?$/ then 24 * 60 * 60
30
+ else
31
+ raise ArgumentError, "Unknown duration #{coder.seq.inspect}"
32
+ end
33
+ num * mult
34
+ end
35
+ end
36
+ ```
37
+ ...and config_dir:
38
+ ```ruby
39
+ Nero.configure {|nero| nero.config_dir = Rails.root / "config" }
40
+ ```
41
+ - Allow for a `Rails.application.config_for` like experience
42
+ ```ruby
43
+ Nero.configure {|nero| nero.config_dir = Rails.root / "config" }
44
+
45
+ Nero.load_config(:stripe, root: Rails.env)
46
+ # Returns content of Rails.root / "config/stripe.yml"
47
+ ```
48
+ - Add `Nero.load` like `YAML.load`
49
+ ```ruby
50
+ Nero.load(<<~YAML)
51
+ cache_ttl: !duration [1, day]
52
+ end
53
+ # => {cache_ttl: 86400}
54
+ ```
2
55
 
3
56
  ## [0.1.0] - 2025-01-24
4
57
 
data/README.md CHANGED
@@ -2,37 +2,33 @@
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/nero.svg)](https://badge.fury.io/rb/nero)
4
4
 
5
- Nero is a RubyGem that offers predefined tags and allows you to effortlessly create custom ones for YAML configuration files.
5
+ Nero is a RubyGem that offers declarative YAML-tags to simplify config files, e.g. for requiring and coercion of env-vars.
6
+ Additionally, it allows you to create your own.
6
7
 
7
- E.g. instead of having the following settings file in your Ruby/Rails project:
8
+ **Sample:**
8
9
 
9
10
  ```yaml
10
11
  development:
11
- # env-var with a fallback
12
- secret: <%= ENV.fetch("SECRET", "dummy") %>
13
- # NOTE *any* value provided is taken as `true`
14
- debug?: <%= !!ENV["DEBUG"] %>
15
- production:
16
- # NOTE we can't fail-fast on ENV-var absence (i.e. use `ENV.fetch`),
17
- # as it would require the env-var for development as well
18
- secret: <%= ENV["SECRET"] %>
19
- max_threads: <%= ENV.fetch("MAX_THREADS", 5).to_i %>
20
- ```
21
-
22
- ...turn it into this:
23
- ```yaml
24
- development:
25
- # env-var with a fallback
12
+ # env-var with default value
26
13
  secret: !env [SECRET, "dummy"]
27
- # Though the default is false, explicitly providing "false"/"off"/"n"/"no" is also possible.
14
+ # optional env-var with coercion
28
15
  debug?: !env/bool? DEBUG
29
16
  production:
30
- # fail-fast on absence of SECRET
17
+ # required env-var (only when getting the production-root)
31
18
  secret: !env SECRET
32
- # always an integer
19
+ # int coercion
33
20
  max_threads: !env/integer [MAX_THREADS, 5]
21
+ # something custom
22
+ cache_ttl: !duration [2, hours]
34
23
  ```
35
24
 
25
+ ## Highlights
26
+
27
+ * 💎 declarative YAML-tags for e.g. requiring and coercing env-vars
28
+ * 🛠️ add custom tags
29
+ * 🛤️ `Rails.application.config_for` stand-in
30
+ * ♻️ Zeitwerk-only dependency
31
+
36
32
  ## Installation
37
33
 
38
34
  Install the gem and add to the application's Gemfile by executing:
@@ -52,7 +48,7 @@ Given the following config:
52
48
  development:
53
49
  # env-var with a fallback
54
50
  secret: !env [SECRET, "dummy"]
55
- # Though the default is false, explicitly providing "false"/"off"/"n"/"no" is also possible.
51
+ # Though the default is false, explicitly providing "false"/"off"/"n"/"no" also works.
56
52
  debug?: !env/bool? DEBUG
57
53
  production:
58
54
  # fail-fast on absence of SECRET
@@ -65,7 +61,7 @@ Loading this config:
65
61
 
66
62
  ```ruby
67
63
  # Loading development
68
- Nero.load_config(Pathname.pwd / "config/settings.yml", root: :development)
64
+ Nero.load_config("config/settings", root: :development)
69
65
  # ...and no ENV-vars were provided
70
66
  #=> {secret: "dummy", debug?: false}
71
67
 
@@ -73,7 +69,7 @@ Nero.load_config(Pathname.pwd / "config/settings.yml", root: :development)
73
69
  #=> {secret: "dummy", debug?: true}
74
70
 
75
71
  # Loading production
76
- Nero.load_config(Pathname.pwd / "config/settings.yml", root: :production)
72
+ Nero.load_config("config/settings", root: :production)
77
73
  # ...and no ENV-vars were provided
78
74
  # raises error: key not found: "SECRET" (KeyError)
79
75
 
@@ -132,17 +128,42 @@ The following tags are provided:
132
128
  - smtps://%s:%s@smtp.gmail.com
133
129
  - !env SMTP_USER
134
130
  - !env SMTP_PASS
135
- # using references
131
+
132
+ # pass it a map (including a key 'fmt') to use references
136
133
  smtp_url: !str/format
137
134
  fmt: smtps://%<user>s:%<pass>s@smtp.gmail.com
138
135
  user: !env SMTP_USER
139
136
  pass: !env SMTP_PASS
140
137
  ```
138
+ - `!ref`
139
+ Include values from elsewhere:
140
+ ```yaml
141
+ # simple
142
+ min_threads: !env [MIN_THREADS, !ref [max_threads]]
143
+ max_threads: 5
144
+
145
+ # oauth_callback -refs-> base.url -refs-> base.host
146
+ base:
147
+ host: !env [HOST]
148
+ url: !str/format ['https://%s', !ref[base, host]]
149
+ oauth_callback: !str/format
150
+ - '%s/oauth/callback'
151
+ - !ref[base, url]
152
+
153
+ # refs are resolved within the tree of the selected root.
154
+ # The following config won't work when doing `Nero.load_config(:app, root: :prod)`
155
+ dev:
156
+ max_threads: 5
157
+ prod:
158
+ max_threads: !env[MAX_THREADS, !ref[dev, max_threads]]
159
+ ```
160
+ NOTE future version should raise properly over ref-ing a non-existing path.
161
+
141
162
 
142
- TBD Add one yourself:
163
+ Add one yourself:
143
164
  ```ruby
144
- Nero.configure do
145
- add_tag("foo") do |coder|
165
+ Nero.configure do |nero|
166
+ nero.add_tag("foo") do |coder|
146
167
  # coder.type is one of :scalar, :seq or :map
147
168
  # e.g. respective YAML:
148
169
  # ---
@@ -156,7 +177,18 @@ Nero.configure do
156
177
  #
157
178
  # Find the value in the respective attribute, e.g. `coder.scalar`:
158
179
  coder.scalar.upcase
180
+
181
+ # NOTE when needing just one argument, supporting both scalar and seq allows for chaining:
182
+ # a: !my/inc 4 # scalar suffices
183
+ # ...but when chaining, a seq is required:
184
+ # a: !my/inc [!my/square 2]
159
185
  end
186
+
187
+ # Other configuration options:
188
+ #
189
+ # `config_dir` (default: Pathname.pwd) - path used for expanding non-Pathnames passed to `load_config`, e.g.
190
+ # `Nero.load_config(:app)` loads file `Pathname.pwd / "app.yml"`.
191
+ nero.config_dir = Rails.root / "config"
160
192
  end
161
193
  ```
162
194
 
@@ -0,0 +1,10 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rake", "~> 13.0"
6
+ gem "rspec", "~> 3.0"
7
+ gem "standard", "~> 1.3"
8
+ gem "psych", "< 4"
9
+
10
+ gemspec path: "../"
@@ -0,0 +1,10 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rake", "~> 13.0"
6
+ gem "rspec", "~> 3.0"
7
+ gem "standard", "~> 1.3"
8
+ gem "psych", "< 5"
9
+
10
+ gemspec path: "../"
data/lib/nero/version.rb CHANGED
@@ -3,5 +3,5 @@
3
3
  module Nero
4
4
  # NOTE this is written upon release via:
5
5
  # $ rake gem:build[version=0.3.0]
6
- VERSION = "0.2.2"
6
+ VERSION = "0.4.0"
7
7
  end
data/lib/nero.rb CHANGED
@@ -5,6 +5,7 @@ loader = Zeitwerk::Loader.for_gem
5
5
  loader.setup
6
6
 
7
7
  require "uri" # why needed?
8
+ require "yaml"
8
9
 
9
10
  # TODO fail on unknown tag
10
11
  # TODO show missing env's at once
@@ -30,7 +31,10 @@ module Nero
30
31
  end
31
32
  end
32
33
  extend Resolvable
33
- private_class_method :try_resolve, :gen_resolve_tryer, :deep_resolve
34
+ private_class_method \
35
+ :deep_resolve,
36
+ :gen_resolve_tryer,
37
+ :try_resolve
34
38
 
35
39
  class TagResolver
36
40
  include Resolvable
@@ -41,7 +45,7 @@ module Nero
41
45
 
42
46
  def resolve(ctx)
43
47
  resolve_nested!(ctx)
44
- ctx[:resolvers][@coder.tag].call(@coder)
48
+ ctx[:tags][@coder.tag].call(@coder, ctx)
45
49
  end
46
50
 
47
51
  def resolve_nested!(ctx)
@@ -54,106 +58,197 @@ module Nero
54
58
  end
55
59
  end
56
60
 
57
- def self.add_resolver(name, &block)
58
- (@resolvers ||= {})["!#{name}"] = block
59
- end
60
-
61
- def self.env_fetch(k, fallback = nil, all_optional: "dummy")
62
- fallback ||= all_optional if ENV["NERO_ENV_ALL_OPTIONAL"]
61
+ class Configuration
62
+ attr_reader :tags
63
+ attr_accessor :config_dir
63
64
 
64
- fallback.nil? ? ENV.fetch(k) : ENV.fetch(k, fallback)
65
+ def add_tag(name, &block)
66
+ (@tags ||= {})["!#{name}"] = block
67
+ end
65
68
  end
66
- private_class_method :env_fetch
67
69
 
68
- add_resolver("env/integer") do |coder|
69
- Integer(env_fetch(*(coder.scalar || coder.seq), all_optional: "999"))
70
+ def self.configuration
71
+ @configuration ||= Configuration.new
70
72
  end
71
73
 
72
- add_resolver("env/integer?") do |coder|
73
- Integer(ENV[coder.scalar]) if ENV[coder.scalar]
74
+ def self.configure
75
+ yield configuration if block_given?
74
76
  end
75
77
 
76
- add_resolver("env/bool") do |coder|
77
- re_true = /y|Y|yes|Yes|YES|true|True|TRUE|on|On|ON/
78
- re_false = /n|N|no|No|NO|false|False|FALSE|off|Off|OFF/
78
+ # helpers for configuration
79
+ # module TagHelpers
80
+ # def to_boolean(s)
81
+ # end
82
+ # end
79
83
 
80
- coerce = ->(s) do
81
- case s
82
- when TrueClass, FalseClass then s
83
- when re_true then true
84
- when re_false then false
85
- else
86
- raise "bool value should be one of y(es)/n(o), on/off, true/false (got #{s.inspect})"
84
+ def self.add_default_tags!
85
+ # extend TagHelpers
86
+
87
+ configure do |config|
88
+ config.add_tag("ref") do |coder, ctx|
89
+ # validate: non-empty coder.seq, only strs, path must exists in ctx[:config]
90
+
91
+ path = coder.seq.map(&:to_sym)
92
+ deep_resolve(ctx[:config].dig(*path), **ctx)
87
93
  end
88
- end
89
94
 
90
- coerce[env_fetch(*(coder.scalar || coder.seq), all_optional: "false")]
91
- end
95
+ config.add_tag("env/integer") do |coder|
96
+ Integer(env_fetch(*(coder.scalar || coder.seq), all_optional: "999"))
97
+ end
92
98
 
93
- add_resolver("env/bool?") do |coder|
94
- re_true = /y|Y|yes|Yes|YES|true|True|TRUE|on|On|ON/
95
- re_false = /n|N|no|No|NO|false|False|FALSE|off|Off|OFF/
99
+ config.add_tag("env/integer?") do |coder|
100
+ Integer(ENV[coder.scalar]) if ENV[coder.scalar]
101
+ end
96
102
 
97
- coerce = ->(s) do
98
- case s
99
- when TrueClass, FalseClass then s
100
- when re_true then true
101
- when re_false then false
102
- else
103
- raise "bool value should be one of y(es)/n(o), on/off, true/false (got #{s.inspect})"
103
+ config.add_tag("env/bool") do |coder|
104
+ re_true = /y|Y|yes|Yes|YES|true|True|TRUE|on|On|ON/
105
+ re_false = /n|N|no|No|NO|false|False|FALSE|off|Off|OFF/
106
+
107
+ coerce = ->(s) do
108
+ case s
109
+ when TrueClass, FalseClass then s
110
+ when re_true then true
111
+ when re_false then false
112
+ else
113
+ raise "bool value should be one of y(es)/n(o), on/off, true/false (got #{s.inspect})"
114
+ end
115
+ end
116
+
117
+ coerce[env_fetch(*(coder.scalar || coder.seq), all_optional: "false")]
104
118
  end
105
- end
106
119
 
107
- ENV[coder.scalar] ? coerce[ENV[coder.scalar]] : false
108
- end
120
+ config.add_tag("env/bool?") do |coder|
121
+ re_true = /y|Y|yes|Yes|YES|true|True|TRUE|on|On|ON/
122
+ re_false = /n|N|no|No|NO|false|False|FALSE|off|Off|OFF/
123
+
124
+ coerce = ->(s) do
125
+ case s
126
+ when TrueClass, FalseClass then s
127
+ when re_true then true
128
+ when re_false then false
129
+ else
130
+ raise "bool value should be one of y(es)/n(o), on/off, true/false (got #{s.inspect})"
131
+ end
132
+ end
133
+
134
+ ENV[coder.scalar] ? coerce[ENV[coder.scalar]] : false
135
+ end
136
+
137
+ config.add_tag("env") do |coder|
138
+ env_fetch(*(coder.scalar || coder.seq))
139
+ end
140
+
141
+ config.add_tag("env?") do |coder|
142
+ fetch_args = coder.scalar ? [coder.scalar, nil] : coder.seq
143
+ ENV.fetch(*fetch_args)
144
+ end
145
+
146
+ config.add_tag("path") do |coder|
147
+ Pathname.new(coder.scalar || coder.seq.join("/"))
148
+ end
149
+
150
+ config.add_tag("uri") do |coder|
151
+ URI(coder.scalar || coder.seq.join)
152
+ end
109
153
 
110
- add_resolver("env") do |coder|
111
- env_fetch(*(coder.scalar || coder.seq))
154
+ config.add_tag("str/format") do |coder|
155
+ case coder.type
156
+ when :seq
157
+ sprintf(*coder.seq)
158
+ when :map
159
+ m = Util.deep_symbolize_keys(coder.map)
160
+ fmt = m.delete(:fmt)
161
+ sprintf(fmt, m)
162
+ else
163
+ coder.scalar
164
+ end
165
+ end
166
+ end
112
167
  end
168
+ private_class_method :add_default_tags!
169
+
170
+ def self.reset_configuration!
171
+ @configuration = nil
172
+
173
+ configure do |config|
174
+ config.config_dir = Pathname.pwd
175
+ end
113
176
 
114
- add_resolver("env?") do |coder|
115
- fetch_args = coder.scalar ? [coder.scalar, nil] : coder.seq
116
- ENV.fetch(*fetch_args)
177
+ add_default_tags!
117
178
  end
179
+ reset_configuration!
180
+
181
+ def self.env_fetch(k, fallback = nil, all_optional: "dummy")
182
+ fallback ||= all_optional if ENV["NERO_ENV_ALL_OPTIONAL"]
118
183
 
119
- add_resolver("path") do |coder|
120
- Pathname.new(coder.scalar || coder.seq.join("/"))
184
+ fallback.nil? ? ENV.fetch(k) : ENV.fetch(k, fallback)
121
185
  end
186
+ private_class_method :env_fetch
122
187
 
123
- add_resolver("uri") do |coder|
124
- URI(coder.scalar || coder.seq.join)
188
+ @yaml_options = {
189
+ permitted_classes: [Symbol, TagResolver],
190
+ aliases: true
191
+ }
192
+
193
+ def self.load_config(file, root: nil, env: nil)
194
+ root ||= env
195
+ add_tags!
196
+
197
+ file = resolve_file(file)
198
+
199
+ if file.exist?
200
+ process_yaml(yaml_load_file(file, @yaml_options), root:)
201
+ else
202
+ raise "Can't find file #{file}"
203
+ end
125
204
  end
126
205
 
127
- add_resolver("str/format") do |coder|
128
- case coder.type
129
- when :seq
130
- sprintf(*coder.seq)
131
- when :map
132
- m = Util.deep_symbolize_keys(coder.map)
133
- fmt = m.delete(:fmt)
134
- sprintf(fmt, m)
206
+ def self.resolve_file(file)
207
+ case file
208
+ when Pathname then file
209
+ # TODO expand full path
135
210
  else
136
- coder.scalar
211
+ configuration.config_dir / "#{file}.yml"
137
212
  end
138
213
  end
214
+ private_class_method :resolve_file
139
215
 
140
- def self.load_config(file, root: nil)
216
+ def self.load(raw, root: nil, env: nil)
217
+ root ||= env
141
218
  add_tags!
142
219
 
143
- if file.exist?
144
- unresolved = Util.deep_symbolize_keys(YAML.load_file(file,
145
- permitted_classes: [Symbol, TagResolver], aliases: true)).then do
146
- root ? _1[root.to_sym] : _1
147
- end
220
+ process_yaml(yaml_load(raw, @yaml_options), root:)
221
+ end
222
+
223
+ def self.process_yaml(yaml, root: nil)
224
+ unresolved = Util.deep_symbolize_keys(yaml).then do
225
+ root ? _1[root.to_sym] : _1
226
+ end
148
227
 
149
- deep_resolve(unresolved, resolvers: @resolvers)
228
+ deep_resolve(unresolved, tags: configuration.tags, config: unresolved)
229
+ end
230
+ private_class_method :process_yaml
231
+
232
+ def self.yaml_load_file(file, opts = {})
233
+ if Psych::VERSION < "4"
234
+ YAML.load_file(file)
150
235
  else
151
- raise "Can't find file #{file}"
236
+ YAML.load_file(file, **opts)
237
+ end
238
+ end
239
+ private_class_method :yaml_load_file
240
+
241
+ def self.yaml_load(file, opts = {})
242
+ if Psych::VERSION < "4"
243
+ YAML.load(file)
244
+ else
245
+ YAML.load(file, **opts)
152
246
  end
153
247
  end
248
+ private_class_method :yaml_load
154
249
 
155
250
  def self.add_tags!
156
- @resolvers.keys.each do
251
+ configuration.tags.keys.each do
157
252
  YAML.add_tag(_1, TagResolver)
158
253
  end
159
254
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nero
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gert Goet
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-01-29 00:00:00.000000000 Z
10
+ date: 2025-02-15 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: zeitwerk
@@ -23,6 +23,20 @@ dependencies:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
25
  version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: appraisal
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
26
40
  description: |
27
41
  Some convenient YAML-tags...
28
42
  - to get environment values: env, env?, env/integer, env/integer?, env/bool, env/bool?.
@@ -39,10 +53,13 @@ files:
39
53
  - ".envrc"
40
54
  - ".rspec"
41
55
  - ".standard.yml"
56
+ - Appraisals
42
57
  - CHANGELOG.md
43
58
  - LICENSE.txt
44
59
  - README.md
45
60
  - Rakefile
61
+ - gemfiles/psych_3.gemfile
62
+ - gemfiles/psych_4.gemfile
46
63
  - lib/nero.rb
47
64
  - lib/nero/util.rb
48
65
  - lib/nero/version.rb