nero 0.5.0 → 0.7.0.rc2
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/.envrc +3 -1
- data/.yardopts +6 -0
- data/CHANGELOG.md +49 -1
- data/README.md +72 -49
- data/lib/nero/base_tag.rb +9 -0
- data/lib/nero/context.rb +17 -0
- data/lib/nero/deferred.rb +9 -0
- data/lib/nero/env_tag.rb +32 -0
- data/lib/nero/error.rb +22 -0
- data/lib/nero/format_tag.rb +16 -0
- data/lib/nero/parser.rb +139 -0
- data/lib/nero/proc_tag.rb +9 -0
- data/lib/nero/rails/credentials_tag.rb +24 -0
- data/lib/nero/rails/duration_tag.rb +26 -0
- data/lib/nero/rails/string_inquirer_tag.rb +16 -0
- data/lib/nero/rails.rb +35 -0
- data/lib/nero/railtie.rb +9 -0
- data/lib/nero/ref.rb +9 -0
- data/lib/nero/ref_tag.rb +9 -0
- data/lib/nero/result.rb +19 -0
- data/lib/nero/root_path_tag.rb +26 -0
- data/lib/nero/version.rb +1 -1
- data/lib/nero/visitor.rb +54 -0
- data/lib/nero.rb +28 -402
- data/rakelib/gem.rake +4 -10
- data/rakelib/yard.rake +12 -0
- metadata +23 -18
- data/lib/nero/util.rb +0 -29
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 69db9ab0dfd43a40d12a05bd3eb311d12a292a4ba7f1ce0c9f965e908344d659
|
|
4
|
+
data.tar.gz: 6c29320c5e9f8deafc4c3e207554f59df3e2a97240b73bcc3debefca0a6e3d04
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5351b2b97b1d49b3a405b5f829d0f716e190a9d9439dd5d54eb8ac673b11efaed1665e352a8973de4a80cde786fda5e0061646d63beafe2626f4fd95d25bf7df
|
|
7
|
+
data.tar.gz: 3a296f997e83ffd96a508553a638b74abbb0adef2e6487904a4c1be64809f2d5e62cfd7bc98ab0b1ee6e0092cf6888a01359209d540632325542e970102e11f3
|
data/.envrc
CHANGED
data/.yardopts
ADDED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
Total refactor and so quite some breaking changes.
|
|
4
|
+
|
|
5
|
+
## Breaking Changes
|
|
6
|
+
|
|
7
|
+
- Nero.load / Nero.load_file removed — replaced by Nero.parse and Nero.parse_file. These return values directly (or raise Nero::ParseError).
|
|
8
|
+
- Nero.configure / Nero.configuration removed — custom tags are now registered via a block passed to parse/parse_file/config_for:
|
|
9
|
+
```ruby
|
|
10
|
+
# Before
|
|
11
|
+
Nero.configure { |c| c.add_tag("rot/13", klass: RotTag[n: 13]) }
|
|
12
|
+
Nero.load("secret: !rot/13 uryyb")
|
|
13
|
+
|
|
14
|
+
# After
|
|
15
|
+
Nero.parse("secret: !rot/13 uryyb") { |c| c.add_tag("rot/13", RotTag.new(n: 13)) }
|
|
16
|
+
```
|
|
17
|
+
- `Nero::Config` removed — results are plain Hashes (string keys) instead of a Config subclass with DigExt/dig!.
|
|
18
|
+
- `Nero::BaseTag` API changed — tags now implement resolve(args, context:) instead of accessing coder/ctx. Tag options are passed via initialize instead of init_options.
|
|
19
|
+
- !ref syntax changed — from sequence-based !ref [base, url] to dot-notation !ref base.url.
|
|
20
|
+
- root: option replaces root:/env: and uses string keys (e.g. root: :development matches the string key "development").
|
|
21
|
+
- Keys are strings — previously keys were symbolized; now they stay as strings.
|
|
22
|
+
- !uri and !path tags removed — !uri is gone entirely; !path is no longer a built-in.
|
|
23
|
+
- `Nero::PathRootTag` renamed to `Nero::RootPathTag` with a new constructor API.
|
|
24
|
+
- !str/format changed — now also available as !format; the map form (fmt: key) is replaced by a sequence with named-parameter hashes: !format ["http://%<host>s", host: !ref host].
|
|
25
|
+
- load_config removed (was already deprecated).
|
|
26
|
+
- Nero::Util removed — deep_symbolize_keys and deep_transform_values are no longer needed.
|
|
27
|
+
- NERO_ENV_ALL_OPTIONAL env var no longer supported.
|
|
28
|
+
|
|
29
|
+
## [0.6.0] - 2025-04-10
|
|
30
|
+
|
|
31
|
+
### deprecations
|
|
32
|
+
|
|
33
|
+
- `Nero.load_config` - use `Nero.load_file` or `Nero.config_for`.
|
|
34
|
+
|
|
35
|
+
### other
|
|
36
|
+
|
|
37
|
+
- API docs live at https://eval.github.io/nero/
|
|
38
|
+
- Config for Rails
|
|
39
|
+
The `config.config_dir` is automatically setup, so `Nero.config_for` (formerly `Nero.load_config`) just works.
|
|
40
|
+
- `Nero::Config.dig!` ⛏️💥
|
|
41
|
+
Any (Hash-)result from `Nero.load/load_file/config_for` is now an instance of `Nero::Config`.
|
|
42
|
+
This class contains `dig!`, a fail-hard variant of `dig`:
|
|
43
|
+
```ruby
|
|
44
|
+
Nero.load(<<~Y).dig!(:smtp_settings, :hose) # 💥 typo
|
|
45
|
+
smtp_settings:
|
|
46
|
+
host: 127.0.0.1
|
|
47
|
+
port: 1025
|
|
48
|
+
Y
|
|
49
|
+
#=> 'Nero::DigExt#dig!': path not found [:smtp_settings, :hose] (ArgumentError)
|
|
50
|
+
```
|
|
3
51
|
|
|
4
52
|
## [0.5.0] - 2025-03-20
|
|
5
53
|
|
data/README.md
CHANGED
|
@@ -37,7 +37,7 @@ production:
|
|
|
37
37
|
* 💎 declarative YAML-tags for e.g. requiring and coercing env-vars
|
|
38
38
|
* 🛠️ add custom tags
|
|
39
39
|
* 🛤️ `Rails.application.config_for` drop-in
|
|
40
|
-
* ♻️
|
|
40
|
+
* ♻️ no dependencies
|
|
41
41
|
|
|
42
42
|
## Installation
|
|
43
43
|
|
|
@@ -47,6 +47,16 @@ Install the gem and add to the application's Gemfile by executing:
|
|
|
47
47
|
bundle add nero
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
+
## Configuration
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
parser = Nero::Parser.new do |config|
|
|
54
|
+
config.add_tag("str/upcase", ->(args, **) { args.join.upcase })
|
|
55
|
+
end
|
|
56
|
+
parser.parse("hello: !str/upcase world")
|
|
57
|
+
# => #<Nero::Result:0x00000001239f8de0 @errors=[], @value={"hello" => "WORLD"}>
|
|
58
|
+
```
|
|
59
|
+
|
|
50
60
|
## Usage
|
|
51
61
|
|
|
52
62
|
> [!WARNING]
|
|
@@ -56,7 +66,7 @@ bundle add nero
|
|
|
56
66
|
|
|
57
67
|
Given the following config:
|
|
58
68
|
```yaml
|
|
59
|
-
# config/
|
|
69
|
+
# config/app.yml
|
|
60
70
|
development:
|
|
61
71
|
# env-var with a fallback
|
|
62
72
|
secret: !env [SECRET, "dummy"]
|
|
@@ -73,7 +83,7 @@ Loading this config:
|
|
|
73
83
|
|
|
74
84
|
```ruby
|
|
75
85
|
# Loading development
|
|
76
|
-
Nero.
|
|
86
|
+
Nero.parse_file("config/app.yml", root: :development)
|
|
77
87
|
# ...and no ENV-vars were provided
|
|
78
88
|
#=> {secret: "dummy", debug?: false}
|
|
79
89
|
|
|
@@ -81,7 +91,7 @@ Nero.load_config("config/settings", root: :development)
|
|
|
81
91
|
#=> {secret: "dummy", debug?: true}
|
|
82
92
|
|
|
83
93
|
# Loading production
|
|
84
|
-
Nero.
|
|
94
|
+
Nero.parse_file("config/app.yml", root: :production)
|
|
85
95
|
# ...and no ENV-vars were provided
|
|
86
96
|
# raises error: key not found: "SECRET" (KeyError)
|
|
87
97
|
|
|
@@ -89,15 +99,14 @@ Nero.load_config("config/settings", root: :production)
|
|
|
89
99
|
#=> {secret: "s3cr3t", max_threads: 3}
|
|
90
100
|
```
|
|
91
101
|
> [!TIP]
|
|
92
|
-
>
|
|
102
|
+
> You can also use `Nero.config_for(:app)` (similar to [Rails.application.config_for](https://api.rubyonrails.org/classes/Rails/Application.html#method-i-config_for)).
|
|
103
|
+
> In Rails applications this gets configured for you. For other application you might need to adjust the `config_dir`:
|
|
93
104
|
```ruby
|
|
94
|
-
Nero.
|
|
95
|
-
config.config_dir = Rails.root / "config"
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
Nero.load_config(:settings, env: Rails.env)
|
|
105
|
+
Nero.config_for(:settings, env: Rails.env)
|
|
99
106
|
```
|
|
100
107
|
|
|
108
|
+
[API Documentation](https://eval.github.io/nero/).
|
|
109
|
+
|
|
101
110
|
### built-in tags
|
|
102
111
|
|
|
103
112
|
The following tags are provided:
|
|
@@ -169,9 +178,9 @@ $ env NERO_ENV_ALL_OPTIONAL=1 SECRET_KEY_BASE_DUMMY=1 rails asset:precompile
|
|
|
169
178
|
|
|
170
179
|
# pass it a map (including a key 'fmt') to use references
|
|
171
180
|
smtp_url: !str/format
|
|
172
|
-
|
|
173
|
-
user: !env SMTP_USER
|
|
174
|
-
|
|
181
|
+
- smtps://%<user>s:%<pass>s@smtp.gmail.com
|
|
182
|
+
- user: !env SMTP_USER
|
|
183
|
+
pass: !env SMTP_PASS
|
|
175
184
|
```
|
|
176
185
|
- `!ref`
|
|
177
186
|
Include values from elsewhere:
|
|
@@ -189,7 +198,7 @@ $ env NERO_ENV_ALL_OPTIONAL=1 SECRET_KEY_BASE_DUMMY=1 rails asset:precompile
|
|
|
189
198
|
- !ref[base, url]
|
|
190
199
|
|
|
191
200
|
# refs are resolved within the tree of the selected root.
|
|
192
|
-
# The following config won't work when doing `Nero.
|
|
201
|
+
# The following config won't work when doing `Nero.load_file("config/app.yml", root: :prod)`
|
|
193
202
|
dev:
|
|
194
203
|
max_threads: 5
|
|
195
204
|
prod:
|
|
@@ -199,13 +208,14 @@ $ env NERO_ENV_ALL_OPTIONAL=1 SECRET_KEY_BASE_DUMMY=1 rails asset:precompile
|
|
|
199
208
|
|
|
200
209
|
### custom tags
|
|
201
210
|
|
|
202
|
-
|
|
211
|
+
There's three ways to create your own tags.
|
|
212
|
+
|
|
213
|
+
For all these methods it's helpful to see the API-docs for [Nero::BaseTag](https://eval.github.io/nero/Nero/BaseTag.html).
|
|
203
214
|
|
|
204
|
-
1. a
|
|
215
|
+
1. **a proc**
|
|
205
216
|
```ruby
|
|
206
217
|
Nero.configure do |nero|
|
|
207
|
-
nero.add_tag("upcase") do |
|
|
208
|
-
# `tag` is a `Nero::BaseTag`.
|
|
218
|
+
nero.add_tag("upcase") do |args, context:|
|
|
209
219
|
# In YAML args are provided as scalar, seq or map:
|
|
210
220
|
# ---
|
|
211
221
|
# k: !upcase bar
|
|
@@ -217,42 +227,38 @@ Three ways to do this:
|
|
|
217
227
|
# k: !upcase
|
|
218
228
|
# bar: baz
|
|
219
229
|
#
|
|
220
|
-
|
|
221
|
-
case tag.args
|
|
230
|
+
case args
|
|
222
231
|
when Hash
|
|
223
|
-
|
|
232
|
+
args.each_with_object({}) {|(k,v), acc| acc[k] = v.upcase }
|
|
224
233
|
else
|
|
225
|
-
|
|
234
|
+
args.map(&:upcase)
|
|
226
235
|
end
|
|
227
236
|
|
|
228
|
-
# NOTE though
|
|
229
|
-
# as it allows for chaining:
|
|
237
|
+
# NOTE though a tag might just need one argument (ie scalar),
|
|
238
|
+
# it's helpful to accept a seq as it allows for chaining:
|
|
230
239
|
# a: !my/inc 4 # scalar suffices
|
|
231
|
-
# ...but when chaining, it
|
|
232
|
-
# a: !my/inc [!my/square 2]
|
|
240
|
+
# ...but when chaining, it needs to be a seq:
|
|
241
|
+
# a: !my/inc [ !my/square 2 ]
|
|
233
242
|
end
|
|
234
243
|
end
|
|
235
244
|
```
|
|
236
|
-
|
|
245
|
+
Blocks are passed instances of [Nero::BaseTag](https://eval.github.io/nero/Nero/BaseTag.html).
|
|
246
|
+
1. **re-use existing tag-class**
|
|
237
247
|
You can add an existing tag under a better fitting name this way.
|
|
238
248
|
Also: some tag-classes have options that allow for simple customizations (like `coerce` below):
|
|
239
249
|
```ruby
|
|
240
250
|
Nero.configure do |nero|
|
|
241
|
-
nero.add_tag("env/upcase", klass: Nero::EnvTag[coerce: :upcase])
|
|
242
|
-
|
|
243
251
|
# Alias for path/git_root:
|
|
244
|
-
nero.add_tag("path/project_root",
|
|
252
|
+
nero.add_tag("path/project_root", Nero::RootPathTag.new(".git"))
|
|
245
253
|
end
|
|
246
254
|
```
|
|
247
|
-
1. custom class
|
|
255
|
+
1. **custom class**
|
|
248
256
|
```ruby
|
|
249
257
|
class RotTag < Nero::BaseTag
|
|
250
258
|
# Configure:
|
|
251
259
|
# ```
|
|
252
|
-
# config.add_tag("rot/12",
|
|
253
|
-
# config.add_tag("rot/10",
|
|
254
|
-
# "#{secret} (try breaking this!)"
|
|
255
|
-
# end
|
|
260
|
+
# config.add_tag("rot/12", RotTag.new(12))
|
|
261
|
+
# config.add_tag("rot/10", RotTag.new(10))
|
|
256
262
|
# ```
|
|
257
263
|
#
|
|
258
264
|
# Usage in YAML:
|
|
@@ -260,35 +266,52 @@ Three ways to do this:
|
|
|
260
266
|
# secret: !rot/12 some message
|
|
261
267
|
# very_secret: !rot/10 [ !env [ MSG, some message ] ]
|
|
262
268
|
# ```
|
|
263
|
-
# => {secret: "EAyq yqEEmsq", very_secret: "Cywo woCCkqo
|
|
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
|
|
269
|
+
# => {secret: "EAyq yqEEmsq", very_secret: "Cywo woCCkqo)"}
|
|
271
270
|
|
|
272
271
|
def chars
|
|
273
272
|
@chars ||= (('a'..'z').to_a + ('A'..'Z').to_a + ('0'..'9').to_a)
|
|
274
273
|
end
|
|
275
274
|
|
|
276
|
-
def resolve(
|
|
275
|
+
def resolve(args, context:)
|
|
277
276
|
# Here we actually do the work: get the args, rotate strings and delegate to the block.
|
|
278
277
|
# `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
278
|
# String#tr replaces any character from the first collection with the same position in the other:
|
|
282
|
-
args.join.tr(chars.join, chars.rotate(
|
|
279
|
+
args.join.tr(chars.join, chars.rotate(@n).join)
|
|
283
280
|
end
|
|
284
281
|
end
|
|
285
282
|
```
|
|
286
283
|
|
|
287
284
|
## Development
|
|
288
285
|
|
|
289
|
-
|
|
286
|
+
```bash
|
|
287
|
+
# Setup
|
|
288
|
+
bin/setup # Make sure it exits with code 0
|
|
289
|
+
|
|
290
|
+
# Run tests
|
|
291
|
+
rake
|
|
292
|
+
```
|
|
290
293
|
|
|
291
|
-
|
|
294
|
+
Using [mise](https://mise.jdx.dev/) for env-vars is recommended.
|
|
295
|
+
|
|
296
|
+
### Releasing
|
|
297
|
+
|
|
298
|
+
1. Update `lib/bonchi/version.rb`
|
|
299
|
+
```
|
|
300
|
+
bin/rake 'gem:write_version[0.5.0]'
|
|
301
|
+
# commit&push
|
|
302
|
+
# check CI
|
|
303
|
+
```
|
|
304
|
+
1. Tag
|
|
305
|
+
```
|
|
306
|
+
gem_push=no bin/rake release
|
|
307
|
+
```
|
|
308
|
+
1. Release workflow from GitHub Actions...
|
|
309
|
+
- ...publishes to RubyGems (with Sigstore attestation)
|
|
310
|
+
- ...creates git GitHub release after successful publish
|
|
311
|
+
1. Update `version.rb` for next dev-cycle
|
|
312
|
+
```
|
|
313
|
+
bin/rake 'gem:write_version[0.6.0.dev]'
|
|
314
|
+
```
|
|
292
315
|
|
|
293
316
|
## Contributing
|
|
294
317
|
|
data/lib/nero/context.rb
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nero
|
|
4
|
+
class Context
|
|
5
|
+
attr_reader :errors, :environ, :dir
|
|
6
|
+
|
|
7
|
+
def initialize(environ:, errors:, dir: nil)
|
|
8
|
+
@environ = environ
|
|
9
|
+
@errors = errors
|
|
10
|
+
@dir = dir || File.realpath(".")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def add_error(message)
|
|
14
|
+
@errors << Error.new(message)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
data/lib/nero/env_tag.rb
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nero
|
|
4
|
+
class EnvTag < BaseTag
|
|
5
|
+
def initialize(coerce: nil, optional: false)
|
|
6
|
+
@coerce = coerce
|
|
7
|
+
@optional = optional
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def resolve(args, context:)
|
|
11
|
+
var_name = args[0]
|
|
12
|
+
default = args[1]
|
|
13
|
+
raw = context.environ[var_name]
|
|
14
|
+
|
|
15
|
+
if raw.nil? && default.nil?
|
|
16
|
+
context.add_error("environment variable #{var_name} is not set") unless @optional
|
|
17
|
+
return nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
value = raw || default.to_s
|
|
21
|
+
|
|
22
|
+
return value unless @coerce
|
|
23
|
+
|
|
24
|
+
begin
|
|
25
|
+
@coerce.call(value)
|
|
26
|
+
rescue ArgumentError => e
|
|
27
|
+
context.add_error("cannot coerce #{var_name}=#{value.inspect}: #{e.message}")
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
data/lib/nero/error.rb
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nero
|
|
4
|
+
class Error
|
|
5
|
+
attr_reader :message
|
|
6
|
+
|
|
7
|
+
def initialize(message)
|
|
8
|
+
@message = message
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def to_s = message
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class ParseError < StandardError
|
|
15
|
+
attr_reader :errors
|
|
16
|
+
|
|
17
|
+
def initialize(errors)
|
|
18
|
+
@errors = errors
|
|
19
|
+
super(errors.map(&:message).join(", "))
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nero
|
|
4
|
+
class FormatTag < BaseTag
|
|
5
|
+
def resolve(args, context:)
|
|
6
|
+
template = args[0]
|
|
7
|
+
opts = args.each_with_object({}) do |arg, h|
|
|
8
|
+
h.merge!(arg.transform_keys(&:to_sym)) if arg.is_a?(Hash)
|
|
9
|
+
end
|
|
10
|
+
format(template, opts)
|
|
11
|
+
rescue KeyError => e
|
|
12
|
+
context.add_error("format error: #{e.message}")
|
|
13
|
+
nil
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
data/lib/nero/parser.rb
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nero
|
|
4
|
+
class Parser
|
|
5
|
+
TO_INT = ->(v) { Integer(v) }
|
|
6
|
+
TO_FLOAT = ->(v) { Float(v) }
|
|
7
|
+
TO_BOOL = ->(v) { !%w[0 false no off].include?(v.downcase) }
|
|
8
|
+
TO_PATH = ->(v) { Pathname.new(v) }
|
|
9
|
+
|
|
10
|
+
def initialize(environ: ENV, root: nil, &block)
|
|
11
|
+
@environ = environ
|
|
12
|
+
@root = root&.to_s
|
|
13
|
+
@tags = {}
|
|
14
|
+
add_tag("env", EnvTag.new)
|
|
15
|
+
add_tag("env?", EnvTag.new(optional: true))
|
|
16
|
+
add_tag("env/int", EnvTag.new(coerce: TO_INT))
|
|
17
|
+
add_tag("env/int?", EnvTag.new(coerce: TO_INT, optional: true))
|
|
18
|
+
add_tag("env/integer", EnvTag.new(coerce: TO_INT))
|
|
19
|
+
add_tag("env/integer?", EnvTag.new(coerce: TO_INT, optional: true))
|
|
20
|
+
add_tag("env/bool", EnvTag.new(coerce: TO_BOOL))
|
|
21
|
+
add_tag("env/bool?", EnvTag.new(coerce: TO_BOOL, optional: true))
|
|
22
|
+
add_tag("env/boolean", EnvTag.new(coerce: TO_BOOL))
|
|
23
|
+
add_tag("env/boolean?", EnvTag.new(coerce: TO_BOOL, optional: true))
|
|
24
|
+
add_tag("env/float", EnvTag.new(coerce: TO_FLOAT))
|
|
25
|
+
add_tag("env/float?", EnvTag.new(coerce: TO_FLOAT, optional: true))
|
|
26
|
+
add_tag("env/path", EnvTag.new(coerce: TO_PATH))
|
|
27
|
+
add_tag("env/path?", EnvTag.new(coerce: TO_PATH, optional: true))
|
|
28
|
+
add_tag("str/format", FormatTag.new)
|
|
29
|
+
add_tag("format", FormatTag.new)
|
|
30
|
+
add_tag("ref", RefTag.new)
|
|
31
|
+
block&.call(self)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def add_tag(name, handler)
|
|
35
|
+
@tags[name] = handler.is_a?(Proc) ? ProcTag.new(handler) : handler
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def parse(yaml, dir: nil)
|
|
39
|
+
errors = []
|
|
40
|
+
tree = ::Psych.parse_stream(yaml)
|
|
41
|
+
ctx = Context.new(environ: @environ, errors: errors, dir: dir)
|
|
42
|
+
|
|
43
|
+
if @root
|
|
44
|
+
mark_inactive_roots(tree)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
visitor = Visitor.build(@tags, ctx)
|
|
48
|
+
result = visitor.accept(tree)
|
|
49
|
+
value = result.first
|
|
50
|
+
|
|
51
|
+
if @root
|
|
52
|
+
unless value.is_a?(Hash) && value.key?(@root)
|
|
53
|
+
ctx.add_error("root #{@root.inspect} not found in top-level keys")
|
|
54
|
+
return Result.new(nil, errors.freeze)
|
|
55
|
+
end
|
|
56
|
+
value = value[@root]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
value = resolve_refs(value, value, ctx) if contains_ref?(value)
|
|
60
|
+
Result.new(value, errors.freeze)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def parse_file(path)
|
|
64
|
+
dir = File.dirname(File.realpath(path))
|
|
65
|
+
parse(File.read(path), dir: dir)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def mark_inactive_roots(tree)
|
|
71
|
+
doc = tree.children.first
|
|
72
|
+
mapping = doc&.root
|
|
73
|
+
return unless mapping.is_a?(::Psych::Nodes::Mapping)
|
|
74
|
+
|
|
75
|
+
root_node = mapping.children.each_slice(2).find { |k, _| k.value == @root }&.last
|
|
76
|
+
used_anchors = root_node ? collect_aliases(root_node) : Set.new
|
|
77
|
+
|
|
78
|
+
mapping.children.each_slice(2) do |key_node, val_node|
|
|
79
|
+
next if key_node.value == @root
|
|
80
|
+
next if used_anchors.include?(val_node.anchor)
|
|
81
|
+
|
|
82
|
+
strip_custom_tags(val_node)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def collect_aliases(node, result = Set.new)
|
|
87
|
+
if node.is_a?(::Psych::Nodes::Alias)
|
|
88
|
+
result << node.anchor
|
|
89
|
+
elsif node.respond_to?(:children) && node.children
|
|
90
|
+
node.children.each { |child| collect_aliases(child, result) }
|
|
91
|
+
end
|
|
92
|
+
result
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def strip_custom_tags(node)
|
|
96
|
+
case node
|
|
97
|
+
when ::Psych::Nodes::Scalar, ::Psych::Nodes::Sequence, ::Psych::Nodes::Mapping
|
|
98
|
+
node.tag = nil if node.tag&.start_with?("!")
|
|
99
|
+
end
|
|
100
|
+
return unless node.respond_to?(:children) && node.children
|
|
101
|
+
node.children.each { |child| strip_custom_tags(child) }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def contains_ref?(value)
|
|
105
|
+
case value
|
|
106
|
+
when Ref, Deferred then true
|
|
107
|
+
when Hash then value.values.any? { |v| contains_ref?(v) }
|
|
108
|
+
when Array then value.any? { |v| contains_ref?(v) }
|
|
109
|
+
else false
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def resolve_refs(value, root, ctx, visited = Set.new)
|
|
114
|
+
case value
|
|
115
|
+
when Ref
|
|
116
|
+
key = value.path.join(".")
|
|
117
|
+
if visited.include?(key)
|
|
118
|
+
ctx.add_error("circular reference: #{key}")
|
|
119
|
+
return nil
|
|
120
|
+
end
|
|
121
|
+
target = value.path.reduce(root) { |h, k| h.is_a?(Hash) ? h[k] : nil }
|
|
122
|
+
if target.nil?
|
|
123
|
+
ctx.add_error("unknown ref #{key}")
|
|
124
|
+
return nil
|
|
125
|
+
end
|
|
126
|
+
resolve_refs(target, root, ctx, visited | [key])
|
|
127
|
+
when Deferred
|
|
128
|
+
resolved_args = resolve_refs(value.args, root, ctx, visited)
|
|
129
|
+
value.tag.resolve(resolved_args, context: ctx)
|
|
130
|
+
when Hash
|
|
131
|
+
value.transform_values { |v| resolve_refs(v, root, ctx, visited) }
|
|
132
|
+
when Array
|
|
133
|
+
value.map { |v| resolve_refs(v, root, ctx, visited) }
|
|
134
|
+
else
|
|
135
|
+
value
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nero
|
|
4
|
+
module Rails
|
|
5
|
+
class CredentialsTag < BaseTag
|
|
6
|
+
attr_reader :credentials
|
|
7
|
+
|
|
8
|
+
# TODO lookup?
|
|
9
|
+
def initialize(credentials)
|
|
10
|
+
@credentials = credentials
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def resolve(args, context:)
|
|
14
|
+
keys = args.map(&:to_sym)
|
|
15
|
+
#value = ::Rails.application.credentials.dig(*keys)
|
|
16
|
+
value = credentials.dig(*keys)
|
|
17
|
+
return value if value
|
|
18
|
+
|
|
19
|
+
context.add_error("credential #{args.join(".")} not found")
|
|
20
|
+
nil
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nero
|
|
4
|
+
module Rails
|
|
5
|
+
class DurationTag < BaseTag
|
|
6
|
+
UNITS = %w[seconds minutes hours days weeks months years].freeze
|
|
7
|
+
|
|
8
|
+
def resolve(args, context:)
|
|
9
|
+
amount = args[0]
|
|
10
|
+
unit = args[1]&.to_s
|
|
11
|
+
|
|
12
|
+
unless amount.is_a?(Numeric)
|
|
13
|
+
context.add_error("duration requires a numeric amount, got #{amount.inspect}")
|
|
14
|
+
return nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
unless UNITS.include?(unit)
|
|
18
|
+
context.add_error("duration unknown unit #{unit.inspect}, expected one of: #{UNITS.join(", ")}")
|
|
19
|
+
return nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
amount.public_send(unit)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nero
|
|
4
|
+
module Rails
|
|
5
|
+
class StringInquirerTag < BaseTag
|
|
6
|
+
def resolve(args, context:)
|
|
7
|
+
value = args[0]&.to_s
|
|
8
|
+
if value.nil? || value.empty?
|
|
9
|
+
context.add_error("str/inquirer requires a non-empty string argument")
|
|
10
|
+
return nil
|
|
11
|
+
end
|
|
12
|
+
ActiveSupport::StringInquirer.new(value)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|