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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +36 -0
- data/README.md +130 -38
- data/lib/nero/version.rb +1 -1
- data/lib/nero.rb +266 -108
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4e28a4b8bcd989c20dd60dc592bfb31039314437b3d8ca5df33b79adefd5cb2e
|
4
|
+
data.tar.gz: f6eb0ec1e9ceb041088a7d2b850494e090f08d98f4e8369f82f0608a4449ab69
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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 (
|
19
|
+
# required env-var (not required during development)
|
18
20
|
secret: !env SECRET
|
19
|
-
|
21
|
+
|
22
|
+
# coercion
|
20
23
|
max_threads: !env/integer [MAX_THREADS, 5]
|
21
|
-
|
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`
|
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
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
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
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"
|
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
|
40
|
-
|
49
|
+
class Configuration
|
50
|
+
attr_reader :tags, :config_dir
|
41
51
|
|
42
|
-
def
|
43
|
-
@
|
52
|
+
def config_dir=(dir)
|
53
|
+
@config_dir = Pathname(dir).expand_path
|
44
54
|
end
|
45
55
|
|
46
|
-
def
|
47
|
-
|
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
|
-
|
52
|
-
|
53
|
-
|
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
|
-
|
79
|
-
|
80
|
-
# def to_boolean(s)
|
81
|
-
# end
|
82
|
-
# end
|
74
|
+
class BaseTag
|
75
|
+
include Resolvable
|
83
76
|
|
84
|
-
|
85
|
-
# extend TagHelpers
|
77
|
+
attr_reader :coder, :options, :ctx
|
86
78
|
|
87
|
-
|
88
|
-
|
89
|
-
|
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
|
-
|
92
|
-
|
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
|
-
|
96
|
-
|
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
|
-
|
100
|
-
|
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
|
-
|
104
|
-
|
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
|
-
|
170
|
+
@coercer ||= case @coerce
|
171
|
+
when Symbol then @coerce.to_proc
|
172
|
+
else
|
173
|
+
@coerce
|
118
174
|
end
|
175
|
+
end
|
119
176
|
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
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
|
-
|
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
|
-
|
138
|
-
|
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
|
-
|
142
|
-
|
143
|
-
|
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("
|
147
|
-
|
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("
|
151
|
-
|
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("
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
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
|
-
|
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.
|
182
|
-
|
183
|
-
|
184
|
-
|
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 :
|
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
|
-
|
318
|
+
config_file = resolve_file(file)
|
198
319
|
|
199
|
-
if
|
200
|
-
process_yaml(yaml_load_file(
|
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 #{
|
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,
|
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
|
-
|
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.
|
252
|
-
YAML.add_tag(
|
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
|
+
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-
|
10
|
+
date: 2025-03-20 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: zeitwerk
|