nero 0.2.2 → 0.4.0

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: 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