nero 0.4.0 → 0.5.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.
Files changed (6) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +36 -0
  3. data/README.md +130 -38
  4. data/lib/nero/version.rb +1 -1
  5. data/lib/nero.rb +266 -108
  6. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9822fcc078e4b2c0e9f049da7cf6f37a98a5b9cf607434057f266a393154296e
4
- data.tar.gz: b71a5be94f469b564e3a80c80a0545246afbc053f619201f8a0c6c0923a776a4
3
+ metadata.gz: 4e28a4b8bcd989c20dd60dc592bfb31039314437b3d8ca5df33b79adefd5cb2e
4
+ data.tar.gz: f6eb0ec1e9ceb041088a7d2b850494e090f08d98f4e8369f82f0608a4449ab69
5
5
  SHA512:
6
- metadata.gz: 863f2c0fbeb21286a0d0f51219e07351a93489688eb7c49be5c1c720a3e414d8e43129e2c0e4d23c6e44213e8547a03c28df128210026414e9e3e209bb7ef472
7
- data.tar.gz: c431e49a4d5d422b554a891973c0e24b7c1810dd922492d56b39ca6157dc70e3110114d7474d0d2ed3b5076f2bea63d8493b2cd9db64c3354ea051022f8887cb
6
+ metadata.gz: 98dc2c475e0e164f47d1738dbe32d6dfeeba6b531f6db0b396a25d3e29219c312ac0750206fddea1d9ee450e9205425a5f0342c6b5b13d38867a364e1219bed0
7
+ data.tar.gz: e9daf1ee287679b10d4ea744c41d3f93bf50570394eb261f7c8b3587a1008efe57b58b7957141aeea48229b65efdaa614f23c181fbf08659a0a878a7a944e773
data/CHANGELOG.md CHANGED
@@ -1,6 +1,42 @@
1
1
  ## [Unreleased]
2
2
  ...
3
3
 
4
+ ## [0.5.0] - 2025-03-20
5
+
6
+ - tag-classes
7
+ Added [`Nero::BaseTag`](https://rubydoc.info/github/eval/nero/main/Nero/BaseTag) that is the basis of all existing tags.
8
+ This means that building upon existing tags is easier and custom tags can be more powerful.
9
+
10
+ Create new tags can be done in 3 ways:
11
+ By block (as before, but slightly changed interface):
12
+ ```ruby
13
+ Nero.configure do |nero|
14
+ nero.add_tag("foo") do |tag|
15
+ # tag of type Nero::BaseTag
16
+ end
17
+ end
18
+ ```
19
+ By re-using existing tags via options:
20
+ ```ruby
21
+ nero.add_tag("env/upcase", klass: Nero::EnvTag[coerce: :upcase])
22
+ ```
23
+ Finally, by subclassing [Nero::BaseTag](https://rubydoc.info/github/eval/nero/main/Nero/BaseTag). See the section ["custom tags"](https://github.com/eval/nero?tab=readme-ov-file#custom-tags) from the README.
24
+
25
+ - `!env/float` and `!env/float?`
26
+ - `!env/git_root` and `!env/rails_root`
27
+ Construct a path relative to some root-path:
28
+ ```yaml
29
+ asset_path: !path/rails_root [ public/assets ]
30
+ ```
31
+ Easy to use for your own tags:
32
+ ```ruby
33
+ config.add_tag("path/project_root", klass: Nero::PathRootTag[containing: '.git']) do |path|
34
+ # possible post-processing
35
+ end
36
+ ```
37
+ - [#2](https://github.com/eval/nero/pull/2) Add irb to gemfile (@dlibanori)
38
+ - [#3](https://github.com/eval/nero/pull/3) Fix missing require (@dlibanori)
39
+
4
40
  ## [0.4.0] - 2025-02-15
5
41
 
6
42
  - Add `!ref`-tag:
data/README.md CHANGED
@@ -11,14 +11,24 @@ Additionally, it allows you to create your own.
11
11
  development:
12
12
  # env-var with default value
13
13
  secret: !env [SECRET, "dummy"]
14
+
14
15
  # optional env-var with coercion
15
16
  debug?: !env/bool? DEBUG
17
+
16
18
  production:
17
- # required env-var (only when getting the production-root)
19
+ # required env-var (not required during development)
18
20
  secret: !env SECRET
19
- # int coercion
21
+
22
+ # coercion
20
23
  max_threads: !env/integer [MAX_THREADS, 5]
21
- # something custom
24
+
25
+ # refer to other keys
26
+ min_threads: !env/integer [MIN_THREADS, !ref max_threads ]
27
+
28
+ # descriptive names
29
+ asset_folder: !path/rails_root [ public/assets ]
30
+
31
+ # easy to add custom tags
22
32
  cache_ttl: !duration [2, hours]
23
33
  ```
24
34
 
@@ -26,7 +36,7 @@ production:
26
36
 
27
37
  * 💎 declarative YAML-tags for e.g. requiring and coercing env-vars
28
38
  * 🛠️ add custom tags
29
- * 🛤️ `Rails.application.config_for` stand-in
39
+ * 🛤️ `Rails.application.config_for` drop-in
30
40
  * ♻️ Zeitwerk-only dependency
31
41
 
32
42
  ## Installation
@@ -42,6 +52,8 @@ bundle add nero
42
52
  > [!WARNING]
43
53
  > It's early days - the API and included tags will certainly change. Check the CHANGELOG when upgrading.
44
54
 
55
+ ### loading a config
56
+
45
57
  Given the following config:
46
58
  ```yaml
47
59
  # config/settings.yml
@@ -76,6 +88,17 @@ Nero.load_config("config/settings", root: :production)
76
88
  # ...with ENV {"SECRET" => "s3cr3t", "MAX_THREADS" => "3"}
77
89
  #=> {secret: "s3cr3t", max_threads: 3}
78
90
  ```
91
+ > [!TIP]
92
+ > The following configuration would make `Nero.load_config` a drop-in replacement for [Rails.application.config_for](https://api.rubyonrails.org/classes/Rails/Application.html#method-i-config_for):
93
+ ```ruby
94
+ Nero.configure do |config|
95
+ config.config_dir = Rails.root / "config"
96
+ end
97
+
98
+ Nero.load_config(:settings, env: Rails.env)
99
+ ```
100
+
101
+ ### built-in tags
79
102
 
80
103
  The following tags are provided:
81
104
  - `!env KEY`, `!env? KEY`
@@ -90,10 +113,11 @@ The following tags are provided:
90
113
  secret: !env? SECRET
91
114
  ```
92
115
  - to coerce env-values:
93
- - `env/integer`, `env/integer?`:
116
+ - `env/integer`, `env/integer?`, `env/float`, `env/float?`:
94
117
  ```yaml
95
118
  port: !env/integer [PORT, 3000]
96
119
  threads: !env/integer? THREADS # nil when not provided
120
+ threshold: !env/float CUTOFF
97
121
  ```
98
122
  - `env/bool`, `env/bool?`:
99
123
  ```yaml
@@ -104,6 +128,11 @@ The following tags are provided:
104
128
  # ...or false:
105
129
  debug?: !env/bool? DEBUG
106
130
  ```
131
+ > [!TIP]
132
+ > Make all env-var's optional by providing `ENV["NERO_ENV_ALL_OPTIONAL"]`, e.g.
133
+ ```shell
134
+ $ env NERO_ENV_ALL_OPTIONAL=1 SECRET_KEY_BASE_DUMMY=1 rails asset:precompile
135
+ ```
107
136
  - `!path`
108
137
  Create a [Pathname](https://rubyapi.org/3.4/o/pathname):
109
138
  ```yaml
@@ -113,6 +142,15 @@ The following tags are provided:
113
142
  - !env PROJECT_ROOT
114
143
  - /public/assets
115
144
  ```
145
+ - `!path/git_root`, `!path/rails_root`
146
+ Create a Pathname relative to some root-path.
147
+ The root-path is expected to be an existing ancestor folder of the yaml-config being parsed.
148
+ It's found by traversing up and checking for the presence of specific files/folders, e.g. '.git' (`!path/git_root`) or 'config.ru' (`!path/rails_root`).
149
+ While the root-path needs to exist, the resulting Pathname doesn't need to.
150
+ ```yaml
151
+ project_root: !path/git_root
152
+ config_folder: !path/rails_root [ config ]
153
+ ```
116
154
  - `!uri`
117
155
  Create a [URI](https://rubyapi.org/3.4/o/uri):
118
156
  ```yaml
@@ -139,7 +177,7 @@ The following tags are provided:
139
177
  Include values from elsewhere:
140
178
  ```yaml
141
179
  # simple
142
- min_threads: !env [MIN_THREADS, !ref [max_threads]]
180
+ min_threads: !env/integer [MIN_THREADS, !ref [max_threads]]
143
181
  max_threads: 5
144
182
 
145
183
  # oauth_callback -refs-> base.url -refs-> base.host
@@ -158,39 +196,93 @@ The following tags are provided:
158
196
  max_threads: !env[MAX_THREADS, !ref[dev, max_threads]]
159
197
  ```
160
198
  NOTE future version should raise properly over ref-ing a non-existing path.
161
-
162
199
 
163
- Add one yourself:
164
- ```ruby
165
- Nero.configure do |nero|
166
- nero.add_tag("foo") do |coder|
167
- # coder.type is one of :scalar, :seq or :map
168
- # e.g. respective YAML:
169
- # ---
170
- # !foo bar
171
- # ---
172
- # !foo
173
- # - bar
174
- # ---
175
- # !foo
176
- # bar: baz
177
- #
178
- # Find the value in the respective attribute, e.g. `coder.scalar`:
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]
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"
192
- end
193
- ```
200
+ ### custom tags
201
+
202
+ Three ways to do this:
203
+
204
+ 1. a block
205
+ ```ruby
206
+ Nero.configure do |nero|
207
+ nero.add_tag("upcase") do |tag|
208
+ # `tag` is a `Nero::BaseTag`.
209
+ # In YAML args are provided as scalar, seq or map:
210
+ # ---
211
+ # k: !upcase bar
212
+ # ---
213
+ # k: !upcase [bar] # equivalent to:
214
+ # k: !upcase
215
+ # - bar
216
+ # ---
217
+ # k: !upcase
218
+ # bar: baz
219
+ #
220
+ # Find these args via `tag.args` (Array or Hash):
221
+ case tag.args
222
+ when Hash
223
+ tag.args.each_with_object({}) {|(k,v), acc| acc[k] = v.upcase }
224
+ else
225
+ tag.args.map(&:upcase)
226
+ end
227
+
228
+ # NOTE though you might just need one argument, it's helpful to accept a seq nonetheless
229
+ # as it allows for chaining:
230
+ # a: !my/inc 4 # scalar suffices
231
+ # ...but when chaining, it comes as a seq:
232
+ # a: !my/inc [!my/square 2]
233
+ end
234
+ end
235
+ ```
236
+ 1. re-use existing tag-class
237
+ You can add an existing tag under a better fitting name this way.
238
+ Also: some tag-classes have options that allow for simple customizations (like `coerce` below):
239
+ ```ruby
240
+ Nero.configure do |nero|
241
+ nero.add_tag("env/upcase", klass: Nero::EnvTag[coerce: :upcase])
242
+
243
+ # Alias for path/git_root:
244
+ nero.add_tag("path/project_root", klass: Nero::PathRootTag[containing: '.git'])
245
+ end
246
+ ```
247
+ 1. custom class
248
+ ```ruby
249
+ class RotTag < Nero::BaseTag
250
+ # Configure:
251
+ # ```
252
+ # config.add_tag("rot/12", klass: RotTag[n: 12])
253
+ # config.add_tag("rot/10", klass: RotTag[n: 10]) do |secret|
254
+ # "#{secret} (try breaking this!)"
255
+ # end
256
+ # ```
257
+ #
258
+ # Usage in YAML:
259
+ # ```
260
+ # secret: !rot/12 some message
261
+ # very_secret: !rot/10 [ !env [ MSG, some message ] ]
262
+ # ```
263
+ # => {secret: "EAyq yqEEmsq", very_secret: "Cywo woCCkqo (try breaking this!)"}
264
+
265
+ # By overriding `init_options` we can restrict/require options,
266
+ # provide default values and do any other setup.
267
+ # By default an option is available via `options[:foo]`.
268
+ def init_options(n: 10)
269
+ super # no specific assignments, so available via `options[:n]`.
270
+ end
271
+
272
+ def chars
273
+ @chars ||= (('a'..'z').to_a + ('A'..'Z').to_a + ('0'..'9').to_a)
274
+ end
275
+
276
+ def resolve(**) # currently no keywords are passed, but `**` allows for future ones.
277
+ # Here we actually do the work: get the args, rotate strings and delegate to the block.
278
+ # `args` are the resolved nested args (so e.g. `!env MSG` is already resolved).
279
+ # `config` is the tag's config, and contains e.g. the block.
280
+ block = config.fetch(:block, :itself.to_proc)
281
+ # String#tr replaces any character from the first collection with the same position in the other:
282
+ args.join.tr(chars.join, chars.rotate(options[:n]).join).then(&block)
283
+ end
284
+ end
285
+ ```
194
286
 
195
287
  ## Development
196
288
 
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.4.0"
6
+ VERSION = "0.5.0"
7
7
  end
data/lib/nero.rb CHANGED
@@ -4,8 +4,9 @@ require "zeitwerk"
4
4
  loader = Zeitwerk::Loader.for_gem
5
5
  loader.setup
6
6
 
7
- require "uri" # why needed?
7
+ require "uri"
8
8
  require "yaml"
9
+ require "pathname"
9
10
 
10
11
  # TODO fail on unknown tag
11
12
  # TODO show missing env's at once
@@ -16,7 +17,7 @@ module Nero
16
17
  module Resolvable
17
18
  def try_resolve(ctx, object)
18
19
  if object.respond_to?(:resolve)
19
- object.resolve(ctx)
20
+ object.resolve(**ctx)
20
21
  else
21
22
  object
22
23
  end
@@ -29,6 +30,15 @@ module Nero
29
30
  def deep_resolve(object, **ctx)
30
31
  Util.deep_transform_values(object, &gen_resolve_tryer(ctx))
31
32
  end
33
+
34
+ def resolve_nested!(coder, ctx = {})
35
+ case coder.type
36
+ when :seq
37
+ coder.seq.map!(&gen_resolve_tryer(ctx))
38
+ when :map
39
+ coder.map = deep_resolve(coder.map, **ctx)
40
+ end
41
+ end
32
42
  end
33
43
  extend Resolvable
34
44
  private_class_method \
@@ -36,37 +46,23 @@ module Nero
36
46
  :gen_resolve_tryer,
37
47
  :try_resolve
38
48
 
39
- class TagResolver
40
- include Resolvable
49
+ class Configuration
50
+ attr_reader :tags, :config_dir
41
51
 
42
- def init_with(coder)
43
- @coder = coder
52
+ def config_dir=(dir)
53
+ @config_dir = Pathname(dir).expand_path
44
54
  end
45
55
 
46
- def resolve(ctx)
47
- resolve_nested!(ctx)
48
- ctx[:tags][@coder.tag].call(@coder, ctx)
49
- end
56
+ def add_tag(name, klass: BaseTag, &block)
57
+ klass, klass_options = klass
50
58
 
51
- def resolve_nested!(ctx)
52
- case @coder.type
53
- when :seq
54
- @coder.seq.map!(&gen_resolve_tryer(ctx))
55
- when :map
56
- @coder.map = deep_resolve(@coder.map, **ctx)
59
+ (@tags ||= {})[name] = {klass:}.tap do |h|
60
+ h[:block] = block if block
61
+ h[:options] = klass_options if klass_options
57
62
  end
58
63
  end
59
64
  end
60
65
 
61
- class Configuration
62
- attr_reader :tags
63
- attr_accessor :config_dir
64
-
65
- def add_tag(name, &block)
66
- (@tags ||= {})["!#{name}"] = block
67
- end
68
- end
69
-
70
66
  def self.configuration
71
67
  @configuration ||= Configuration.new
72
68
  end
@@ -75,92 +71,221 @@ module Nero
75
71
  yield configuration if block_given?
76
72
  end
77
73
 
78
- # helpers for configuration
79
- # module TagHelpers
80
- # def to_boolean(s)
81
- # end
82
- # end
74
+ class BaseTag
75
+ include Resolvable
83
76
 
84
- def self.add_default_tags!
85
- # extend TagHelpers
77
+ attr_reader :coder, :options, :ctx
86
78
 
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]
79
+ def self.[](**options)
80
+ [self, options]
81
+ end
82
+
83
+ # used by YAML
84
+ def init_with(coder)
85
+ @coder = coder
86
+ end
87
+
88
+ def init(ctx:, options:)
89
+ init_ctx(ctx)
90
+ init_options(**options)
91
+ end
92
+
93
+ def init_ctx(ctx)
94
+ @ctx = ctx
95
+ end
96
+
97
+ def init_options(**options)
98
+ @options = options
99
+ end
90
100
 
91
- path = coder.seq.map(&:to_sym)
92
- deep_resolve(ctx[:config].dig(*path), **ctx)
101
+ def tag_name
102
+ coder.tag[1..]
103
+ end
104
+
105
+ def args
106
+ @args ||= begin
107
+ resolve_nested!(coder, {})
108
+ case coder.type
109
+ when :map then Util.deep_symbolize_keys(coder.map)
110
+ else
111
+ Array(coder.public_send(coder.type))
112
+ end
93
113
  end
114
+ end
94
115
 
95
- config.add_tag("env/integer") do |coder|
96
- Integer(env_fetch(*(coder.scalar || coder.seq), all_optional: "999"))
116
+ def config
117
+ ctx.dig(:tags, tag_name)
118
+ end
119
+
120
+ def resolve(**)
121
+ if (block = config[:block])
122
+ if block.parameters.map(&:last).include?(:coder)
123
+ # legacy
124
+ block.call(coder, ctx)
125
+ else
126
+ block.call(self)
127
+ end
128
+ else
129
+ args
97
130
  end
131
+ end
132
+ end
98
133
 
99
- config.add_tag("env/integer?") do |coder|
100
- Integer(ENV[coder.scalar]) if ENV[coder.scalar]
134
+ # Requires an env-var to be available and coerces the value.
135
+ # When tag-name ends with "?", the env-var is optional.
136
+ #
137
+ # Given config:
138
+ # config.add_tag("env/upcase", klass: Nero::EnvTag[coerce: :upcase])
139
+ # config.add_tag("env/upcase?", klass: Nero::EnvTag[coerce: :upcase])
140
+
141
+ # Then YAML => result:
142
+ # "--- env/upcase [MSG, Hello World]" => "HELLO WORLD"
143
+ # "--- env/upcase MSG" => raises when not ENV.has_key? "MSG"
144
+ # "--- env/upcase? MSG" => nil
145
+ #
146
+ # Args supported:
147
+ # - scalar
148
+ # name of env-var, e.g. `!env HOME`
149
+ # - seq
150
+ # name of env-var and fallback, e.g. `!env [HOME, /root]`
151
+
152
+ # Options:
153
+ # - coerce - symbol or proc to be applied to value of env-var.
154
+ # when using coerce, the block is ignoerd.
155
+ #
156
+ class EnvTag < BaseTag
157
+ def resolve(**)
158
+ if coercer
159
+ coercer.call(env_value) unless env_value.nil?
160
+ elsif ctx.dig(:tags, tag_name, :block)
161
+ super
162
+ else
163
+ env_value
101
164
  end
165
+ end
102
166
 
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
167
+ def coercer
168
+ return unless @coerce
116
169
 
117
- coerce[env_fetch(*(coder.scalar || coder.seq), all_optional: "false")]
170
+ @coercer ||= case @coerce
171
+ when Symbol then @coerce.to_proc
172
+ else
173
+ @coerce
118
174
  end
175
+ end
119
176
 
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
177
+ def init_options(coerce: nil)
178
+ @coerce = coerce
179
+ end
180
+
181
+ def optional
182
+ tag_name.end_with?("?") || !!ENV["NERO_ENV_ALL_OPTIONAL"]
183
+ end
184
+ alias_method :optional?, :optional
133
185
 
134
- ENV[coder.scalar] ? coerce[ENV[coder.scalar]] : false
186
+ def env_value
187
+ self.class.env_value(*args, optional:)
188
+ end
189
+
190
+ def self.env_value(k, fallback = nil, optional: false)
191
+ if fallback.nil? && !optional
192
+ ENV.fetch(k)
193
+ else
194
+ ENV.fetch(k, fallback)
135
195
  end
196
+ end
197
+
198
+ def self.coerce_bool(v)
199
+ return false unless v
200
+
201
+ re_true = /y|Y|yes|Yes|YES|true|True|TRUE|on|On|ON/
202
+ re_false = /n|N|no|No|NO|false|False|FALSE|off|Off|OFF/
136
203
 
137
- config.add_tag("env") do |coder|
138
- env_fetch(*(coder.scalar || coder.seq))
204
+ case v
205
+ when TrueClass, FalseClass then v
206
+ when re_true then true
207
+ when re_false then false
208
+ else
209
+ raise "bool value should be one of y(es)/n(o), on/off, true/false (got #{v.inspect})"
139
210
  end
211
+ end
212
+ end
140
213
 
141
- config.add_tag("env?") do |coder|
142
- fetch_args = coder.scalar ? [coder.scalar, nil] : coder.seq
143
- ENV.fetch(*fetch_args)
214
+ # Construct path relative to some root-path.
215
+ # Root-paths are expected to be ancestors of the yaml-file being parsed.
216
+ # They are found by traversing up and checking for specific files/folders, e.g. '.git' or 'Gemfile'.
217
+ # Any argument is appended to the root-path, constructing a path-instance that may exist.
218
+ class PathRootTag < BaseTag
219
+ # Config:
220
+ # config.add_tag("path/git_root", klass: PathRootTag[containing: ".git"])
221
+ # config.add_tag("path/rails_root", klass: PathRootTag[containing: "Gemfile"])
222
+ #
223
+ # YAML:
224
+ # project_root: !path/git_root
225
+ # config_path: !path/git_root [ config ]
226
+ def init_options(containing:)
227
+ super
228
+ end
229
+
230
+ def resolve(**)
231
+ # TODO validate upfront
232
+ raise <<~ERR unless root_path
233
+ #{tag_name}: failed to find root-path (ie an ancestor of #{ctx[:yaml_file]} containing #{options[:containing].inspect}).
234
+ ERR
235
+ root_path.join(*args).then(&config.fetch(:block, :itself.to_proc))
236
+ end
237
+
238
+ def root_path
239
+ find_up(ctx[:yaml_file], options[:containing])
240
+ end
241
+
242
+ def find_up(path, containing)
243
+ (path = path.parent) until path.root? || (path / containing).exist?
244
+ path unless path.root?
245
+ end
246
+ end
247
+
248
+ def self.add_default_tags!
249
+ configure do |config|
250
+ config.add_tag("ref") do |tag|
251
+ # validate: non-empty coder.seq, only strs, path must exists in ctx[:config]
252
+
253
+ path = tag.args.map(&:to_sym)
254
+ deep_resolve(tag.ctx[:yaml].dig(*path), **{})
144
255
  end
145
256
 
146
- config.add_tag("path") do |coder|
147
- Pathname.new(coder.scalar || coder.seq.join("/"))
257
+ config.add_tag("env", klass: EnvTag)
258
+ config.add_tag("env?", klass: EnvTag)
259
+ config.add_tag("env/float", klass: EnvTag[coerce: :to_f])
260
+ config.add_tag("env/float?", klass: EnvTag[coerce: :to_f])
261
+
262
+ config.add_tag("env/integer", klass: EnvTag[coerce: :to_i])
263
+ config.add_tag("env/integer?", klass: EnvTag[coerce: :to_i])
264
+
265
+ config.add_tag("env/bool", klass: EnvTag) do |tag|
266
+ EnvTag.coerce_bool(tag.env_value)
267
+ end
268
+ config.add_tag("env/bool?", klass: EnvTag) do |tag|
269
+ EnvTag.coerce_bool(tag.env_value)
148
270
  end
149
271
 
150
- config.add_tag("uri") do |coder|
151
- URI(coder.scalar || coder.seq.join)
272
+ config.add_tag("path") do |tag|
273
+ Pathname.new(tag.args.join("/"))
152
274
  end
275
+ config.add_tag("path/git_root", klass: PathRootTag[containing: ".git"])
276
+ config.add_tag("path/rails_root", klass: PathRootTag[containing: "config.ru"])
153
277
 
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)
278
+ config.add_tag("uri") do |tag|
279
+ URI.join(*tag.args.join)
280
+ end
281
+
282
+ config.add_tag("str/format") do |tag|
283
+ case tag.args
284
+ when Hash
285
+ fmt = tag.args.delete(:fmt)
286
+ sprintf(fmt, tag.args)
162
287
  else
163
- coder.scalar
288
+ sprintf(*tag.args)
164
289
  end
165
290
  end
166
291
  end
@@ -178,28 +303,24 @@ module Nero
178
303
  end
179
304
  reset_configuration!
180
305
 
181
- def self.env_fetch(k, fallback = nil, all_optional: "dummy")
182
- fallback ||= all_optional if ENV["NERO_ENV_ALL_OPTIONAL"]
183
-
184
- fallback.nil? ? ENV.fetch(k) : ENV.fetch(k, fallback)
306
+ def self.yaml_options
307
+ {
308
+ permitted_classes: [Symbol] + configuration.tags.values.map { _1[:klass] },
309
+ aliases: true
310
+ }
185
311
  end
186
- private_class_method :env_fetch
187
-
188
- @yaml_options = {
189
- permitted_classes: [Symbol, TagResolver],
190
- aliases: true
191
- }
312
+ private_class_method :yaml_options
192
313
 
193
- def self.load_config(file, root: nil, env: nil)
314
+ def self.load_config(file, root: nil, env: nil, resolve: true)
194
315
  root ||= env
195
316
  add_tags!
196
317
 
197
- file = resolve_file(file)
318
+ config_file = resolve_file(file)
198
319
 
199
- if file.exist?
200
- process_yaml(yaml_load_file(file, @yaml_options), root:)
320
+ if config_file.exist?
321
+ process_yaml(yaml_load_file(config_file, yaml_options), root:, config_file:, resolve:)
201
322
  else
202
- raise "Can't find file #{file}"
323
+ raise "Can't find file #{config_file}"
203
324
  end
204
325
  end
205
326
 
@@ -213,22 +334,37 @@ module Nero
213
334
  end
214
335
  private_class_method :resolve_file
215
336
 
216
- def self.load(raw, root: nil, env: nil)
337
+ def self.load(raw, root: nil, env: nil, resolve: true)
217
338
  root ||= env
218
339
  add_tags!
219
340
 
220
- process_yaml(yaml_load(raw, @yaml_options), root:)
341
+ process_yaml(yaml_load(raw, yaml_options), root:, resolve:)
221
342
  end
222
343
 
223
- def self.process_yaml(yaml, root: nil)
344
+ def self.process_yaml(yaml, root: nil, resolve: true, config_file: nil)
345
+ config_file ||= (Pathname.pwd / __FILE__)
346
+
224
347
  unresolved = Util.deep_symbolize_keys(yaml).then do
225
348
  root ? _1[root.to_sym] : _1
226
349
  end
350
+ ctx = {tags: configuration.tags, yaml: unresolved, yaml_file: config_file}
351
+ init_tags!(collect_tags(unresolved), ctx:)
227
352
 
228
- deep_resolve(unresolved, tags: configuration.tags, config: unresolved)
353
+ return unresolved unless resolve
354
+
355
+ # NOTE originally ctx was passed at this point. Maybe delete this.
356
+ deep_resolve(unresolved, **{})
229
357
  end
230
358
  private_class_method :process_yaml
231
359
 
360
+ def self.init_tags!(tags, ctx:)
361
+ tags.each do |tag|
362
+ options = ctx.dig(:tags, tag.tag_name, :options) || {}
363
+ tag.init(ctx:, options:)
364
+ end
365
+ end
366
+ private_class_method :init_tags!
367
+
232
368
  def self.yaml_load_file(file, opts = {})
233
369
  if Psych::VERSION < "4"
234
370
  YAML.load_file(file)
@@ -248,11 +384,33 @@ module Nero
248
384
  private_class_method :yaml_load
249
385
 
250
386
  def self.add_tags!
251
- configuration.tags.keys.each do
252
- YAML.add_tag(_1, TagResolver)
387
+ configuration.tags.each do |tag_name, tag|
388
+ YAML.add_tag("!#{tag_name}", tag[:klass])
253
389
  end
254
390
  end
255
391
  private_class_method :add_tags!
392
+
393
+ def self.collect_tags(obj)
394
+ case obj
395
+ when Hash
396
+ obj.each_value.flat_map { collect_tags(_1) }.compact
397
+ when Nero::BaseTag
398
+ [obj] +
399
+ case obj.coder.type
400
+ when :seq
401
+ collect_tags(obj.coder.seq)
402
+ when :map
403
+ collect_tags(obj.coder.map)
404
+ else
405
+ []
406
+ end
407
+ when Array
408
+ obj.flat_map { collect_tags(_1) }.compact
409
+ else
410
+ []
411
+ end
412
+ end
413
+ private_class_method :collect_tags
256
414
  end
257
415
 
258
416
  loader.eager_load if ENV.key?("CI")
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.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gert Goet
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-02-15 00:00:00.000000000 Z
10
+ date: 2025-03-20 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: zeitwerk