nero 0.6.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/CHANGELOG.md +26 -1
- data/README.md +51 -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 +4 -5
- 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 -574
- data/rakelib/gem.rake +4 -10
- metadata +20 -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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
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.
|
|
3
28
|
|
|
4
29
|
## [0.6.0] - 2025-04-10
|
|
5
30
|
|
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
|
|
|
@@ -50,16 +50,11 @@ bundle add nero
|
|
|
50
50
|
## Configuration
|
|
51
51
|
|
|
52
52
|
```ruby
|
|
53
|
-
Nero.
|
|
54
|
-
|
|
55
|
-
nero.config_dir = "config"
|
|
56
|
-
|
|
57
|
-
# Add custom tags (also see section about custom tags)
|
|
58
|
-
nero.add_tag("upcase") do |tag|
|
|
59
|
-
# tag is an instance of [Nero::BaseTag](https://eval.github.io/nero/Nero/BaseTag.html).
|
|
60
|
-
tag.args.join.upcase
|
|
61
|
-
end
|
|
53
|
+
parser = Nero::Parser.new do |config|
|
|
54
|
+
config.add_tag("str/upcase", ->(args, **) { args.join.upcase })
|
|
62
55
|
end
|
|
56
|
+
parser.parse("hello: !str/upcase world")
|
|
57
|
+
# => #<Nero::Result:0x00000001239f8de0 @errors=[], @value={"hello" => "WORLD"}>
|
|
63
58
|
```
|
|
64
59
|
|
|
65
60
|
## Usage
|
|
@@ -71,7 +66,7 @@ end
|
|
|
71
66
|
|
|
72
67
|
Given the following config:
|
|
73
68
|
```yaml
|
|
74
|
-
# config/
|
|
69
|
+
# config/app.yml
|
|
75
70
|
development:
|
|
76
71
|
# env-var with a fallback
|
|
77
72
|
secret: !env [SECRET, "dummy"]
|
|
@@ -88,7 +83,7 @@ Loading this config:
|
|
|
88
83
|
|
|
89
84
|
```ruby
|
|
90
85
|
# Loading development
|
|
91
|
-
Nero.
|
|
86
|
+
Nero.parse_file("config/app.yml", root: :development)
|
|
92
87
|
# ...and no ENV-vars were provided
|
|
93
88
|
#=> {secret: "dummy", debug?: false}
|
|
94
89
|
|
|
@@ -96,7 +91,7 @@ Nero.load_file("config/settings", root: :development)
|
|
|
96
91
|
#=> {secret: "dummy", debug?: true}
|
|
97
92
|
|
|
98
93
|
# Loading production
|
|
99
|
-
Nero.
|
|
94
|
+
Nero.parse_file("config/app.yml", root: :production)
|
|
100
95
|
# ...and no ENV-vars were provided
|
|
101
96
|
# raises error: key not found: "SECRET" (KeyError)
|
|
102
97
|
|
|
@@ -104,13 +99,9 @@ Nero.load_file("config/settings", root: :production)
|
|
|
104
99
|
#=> {secret: "s3cr3t", max_threads: 3}
|
|
105
100
|
```
|
|
106
101
|
> [!TIP]
|
|
107
|
-
> You can also use `Nero.config_for` (similar to [Rails.application.config_for](https://api.rubyonrails.org/classes/Rails/Application.html#method-i-config_for)).
|
|
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)).
|
|
108
103
|
> In Rails applications this gets configured for you. For other application you might need to adjust the `config_dir`:
|
|
109
104
|
```ruby
|
|
110
|
-
Nero.configure do |config|
|
|
111
|
-
config.config_dir = "config"
|
|
112
|
-
end
|
|
113
|
-
|
|
114
105
|
Nero.config_for(:settings, env: Rails.env)
|
|
115
106
|
```
|
|
116
107
|
|
|
@@ -187,9 +178,9 @@ $ env NERO_ENV_ALL_OPTIONAL=1 SECRET_KEY_BASE_DUMMY=1 rails asset:precompile
|
|
|
187
178
|
|
|
188
179
|
# pass it a map (including a key 'fmt') to use references
|
|
189
180
|
smtp_url: !str/format
|
|
190
|
-
|
|
191
|
-
user: !env SMTP_USER
|
|
192
|
-
|
|
181
|
+
- smtps://%<user>s:%<pass>s@smtp.gmail.com
|
|
182
|
+
- user: !env SMTP_USER
|
|
183
|
+
pass: !env SMTP_PASS
|
|
193
184
|
```
|
|
194
185
|
- `!ref`
|
|
195
186
|
Include values from elsewhere:
|
|
@@ -207,7 +198,7 @@ $ env NERO_ENV_ALL_OPTIONAL=1 SECRET_KEY_BASE_DUMMY=1 rails asset:precompile
|
|
|
207
198
|
- !ref[base, url]
|
|
208
199
|
|
|
209
200
|
# refs are resolved within the tree of the selected root.
|
|
210
|
-
# 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)`
|
|
211
202
|
dev:
|
|
212
203
|
max_threads: 5
|
|
213
204
|
prod:
|
|
@@ -224,8 +215,7 @@ For all these methods it's helpful to see the API-docs for [Nero::BaseTag](https
|
|
|
224
215
|
1. **a proc**
|
|
225
216
|
```ruby
|
|
226
217
|
Nero.configure do |nero|
|
|
227
|
-
nero.add_tag("upcase") do |
|
|
228
|
-
# `tag` is a `Nero::BaseTag`.
|
|
218
|
+
nero.add_tag("upcase") do |args, context:|
|
|
229
219
|
# In YAML args are provided as scalar, seq or map:
|
|
230
220
|
# ---
|
|
231
221
|
# k: !upcase bar
|
|
@@ -237,12 +227,11 @@ For all these methods it's helpful to see the API-docs for [Nero::BaseTag](https
|
|
|
237
227
|
# k: !upcase
|
|
238
228
|
# bar: baz
|
|
239
229
|
#
|
|
240
|
-
|
|
241
|
-
case tag.args
|
|
230
|
+
case args
|
|
242
231
|
when Hash
|
|
243
|
-
|
|
232
|
+
args.each_with_object({}) {|(k,v), acc| acc[k] = v.upcase }
|
|
244
233
|
else
|
|
245
|
-
|
|
234
|
+
args.map(&:upcase)
|
|
246
235
|
end
|
|
247
236
|
|
|
248
237
|
# NOTE though a tag might just need one argument (ie scalar),
|
|
@@ -259,10 +248,8 @@ For all these methods it's helpful to see the API-docs for [Nero::BaseTag](https
|
|
|
259
248
|
Also: some tag-classes have options that allow for simple customizations (like `coerce` below):
|
|
260
249
|
```ruby
|
|
261
250
|
Nero.configure do |nero|
|
|
262
|
-
nero.add_tag("env/upcase", klass: Nero::EnvTag[coerce: :upcase])
|
|
263
|
-
|
|
264
251
|
# Alias for path/git_root:
|
|
265
|
-
nero.add_tag("path/project_root",
|
|
252
|
+
nero.add_tag("path/project_root", Nero::RootPathTag.new(".git"))
|
|
266
253
|
end
|
|
267
254
|
```
|
|
268
255
|
1. **custom class**
|
|
@@ -270,10 +257,8 @@ For all these methods it's helpful to see the API-docs for [Nero::BaseTag](https
|
|
|
270
257
|
class RotTag < Nero::BaseTag
|
|
271
258
|
# Configure:
|
|
272
259
|
# ```
|
|
273
|
-
# config.add_tag("rot/12",
|
|
274
|
-
# config.add_tag("rot/10",
|
|
275
|
-
# "#{secret} (try breaking this!)"
|
|
276
|
-
# end
|
|
260
|
+
# config.add_tag("rot/12", RotTag.new(12))
|
|
261
|
+
# config.add_tag("rot/10", RotTag.new(10))
|
|
277
262
|
# ```
|
|
278
263
|
#
|
|
279
264
|
# Usage in YAML:
|
|
@@ -281,35 +266,52 @@ For all these methods it's helpful to see the API-docs for [Nero::BaseTag](https
|
|
|
281
266
|
# secret: !rot/12 some message
|
|
282
267
|
# very_secret: !rot/10 [ !env [ MSG, some message ] ]
|
|
283
268
|
# ```
|
|
284
|
-
# => {secret: "EAyq yqEEmsq", very_secret: "Cywo woCCkqo
|
|
285
|
-
|
|
286
|
-
# By overriding `init_options` we can restrict/require options,
|
|
287
|
-
# provide default values and do any other setup.
|
|
288
|
-
# By default an option is available via `options[:foo]`.
|
|
289
|
-
def init_options(n: 10)
|
|
290
|
-
super # no specific assignments, so available via `options[:n]`.
|
|
291
|
-
end
|
|
269
|
+
# => {secret: "EAyq yqEEmsq", very_secret: "Cywo woCCkqo)"}
|
|
292
270
|
|
|
293
271
|
def chars
|
|
294
272
|
@chars ||= (('a'..'z').to_a + ('A'..'Z').to_a + ('0'..'9').to_a)
|
|
295
273
|
end
|
|
296
274
|
|
|
297
|
-
def resolve(
|
|
275
|
+
def resolve(args, context:)
|
|
298
276
|
# Here we actually do the work: get the args, rotate strings and delegate to the block.
|
|
299
277
|
# `args` are the resolved nested args (so e.g. `!env MSG` is already resolved).
|
|
300
|
-
# `config` is the tag's config, and contains e.g. the block.
|
|
301
|
-
block = config.fetch(:block, :itself.to_proc)
|
|
302
278
|
# String#tr replaces any character from the first collection with the same position in the other:
|
|
303
|
-
args.join.tr(chars.join, chars.rotate(
|
|
279
|
+
args.join.tr(chars.join, chars.rotate(@n).join)
|
|
304
280
|
end
|
|
305
281
|
end
|
|
306
282
|
```
|
|
307
283
|
|
|
308
284
|
## Development
|
|
309
285
|
|
|
310
|
-
|
|
286
|
+
```bash
|
|
287
|
+
# Setup
|
|
288
|
+
bin/setup # Make sure it exits with code 0
|
|
289
|
+
|
|
290
|
+
# Run tests
|
|
291
|
+
rake
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Using [mise](https://mise.jdx.dev/) for env-vars is recommended.
|
|
311
295
|
|
|
312
|
-
|
|
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
|
+
```
|
|
313
315
|
|
|
314
316
|
## Contributing
|
|
315
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
|
data/lib/nero/rails.rb
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "rails/credentials_tag"
|
|
4
|
+
require_relative "rails/duration_tag"
|
|
5
|
+
require_relative "rails/string_inquirer_tag"
|
|
6
|
+
|
|
7
|
+
module Nero
|
|
8
|
+
module Rails
|
|
9
|
+
module ParserExtension
|
|
10
|
+
def initialize(environ: ENV, root: nil, &block)
|
|
11
|
+
super(environ:, root:)
|
|
12
|
+
add_tag("credentials", Nero::Rails::CredentialsTag.new(::Rails.application.credentials))
|
|
13
|
+
add_tag("path/rails_root", RootPathTag.new(containing: "config.ru"))
|
|
14
|
+
add_tag("secret_key_base", ->(_, **) { ::Rails.application.secret_key_base })
|
|
15
|
+
add_tag("str/inquirer", Nero::Rails::StringInquirerTag.new)
|
|
16
|
+
add_tag("duration", Nero::Rails::DurationTag.new)
|
|
17
|
+
block&.call(self)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
Nero::Parser.prepend(Nero::Rails::ParserExtension)
|
|
24
|
+
|
|
25
|
+
module Nero
|
|
26
|
+
module Rails
|
|
27
|
+
module DefaultEnv
|
|
28
|
+
def config_for(file, env: ::Rails.env, **opts, &block)
|
|
29
|
+
super(file, env:, **opts, &block)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
Nero.singleton_class.prepend(Nero::Rails::DefaultEnv)
|
data/lib/nero/railtie.rb
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "rails"
|
|
4
|
+
|
|
1
5
|
module Nero
|
|
2
6
|
# @private
|
|
3
7
|
class Railtie < ::Rails::Railtie
|
|
4
|
-
config.before_configuration do
|
|
5
|
-
Nero.configure do |nero|
|
|
6
|
-
nero.config_dir = Rails.application.paths["config"].existent.first
|
|
7
|
-
end
|
|
8
|
-
end
|
|
9
8
|
end
|
|
10
9
|
end
|
data/lib/nero/ref.rb
ADDED