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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 136899b1a5b7fd355608891703ad0e8b0c628dfcb3ae74d9a5344fb0a78b41b8
4
- data.tar.gz: 47fce2793b077af0a637d7cac6a5e31b471c16bbbff71561f97bdf4aa904a3d5
3
+ metadata.gz: 69db9ab0dfd43a40d12a05bd3eb311d12a292a4ba7f1ce0c9f965e908344d659
4
+ data.tar.gz: 6c29320c5e9f8deafc4c3e207554f59df3e2a97240b73bcc3debefca0a6e3d04
5
5
  SHA512:
6
- metadata.gz: 979f099766b6c33577346e608fa8e82f890d4d3ef5b49913a142cdbcb7385ecff78776882e5ee78420f0b285388972aef8e6c4c980d4aff120b8c142efec2212
7
- data.tar.gz: e43d9f3916bfc43b3abe49939b3953a72679fec64e8de4de5b682600d6d5ef2f66f97557e262a5558046c9e332532bc908a4317d4a114bc46754d9ff60d3242c
6
+ metadata.gz: 5351b2b97b1d49b3a405b5f829d0f716e190a9d9439dd5d54eb8ac673b11efaed1665e352a8973de4a80cde786fda5e0061646d63beafe2626f4fd95d25bf7df
7
+ data.tar.gz: 3a296f997e83ffd96a508553a638b74abbb0adef2e6487904a4c1be64809f2d5e62cfd7bc98ab0b1ee6e0092cf6888a01359209d540632325542e970102e11f3
data/.envrc CHANGED
@@ -1 +1,3 @@
1
- PATH_add bin
1
+ PATH_add bin
2
+
3
+ source_env_if_exists .envrc.local
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
- * ♻️ Zeitwerk-only dependency
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.configure do |nero|
54
- # Path that `Nero.config_for` uses to resolve Symbol or String files, e.g. `Nero.config_for(:app)`
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/settings.yml
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.load_file("config/settings", root: :development)
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.load_file("config/settings", root: :production)
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
- fmt: smtps://%<user>s:%<pass>s@smtp.gmail.com
191
- user: !env SMTP_USER
192
- pass: !env SMTP_PASS
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.load_config(:app, root: :prod)`
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 |tag|
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
- # Find these args via `tag.args` (Array or Hash):
241
- case tag.args
230
+ case args
242
231
  when Hash
243
- tag.args.each_with_object({}) {|(k,v), acc| acc[k] = v.upcase }
232
+ args.each_with_object({}) {|(k,v), acc| acc[k] = v.upcase }
244
233
  else
245
- tag.args.map(&:upcase)
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", klass: Nero::PathRootTag[containing: '.git'])
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", klass: RotTag[n: 12])
274
- # config.add_tag("rot/10", klass: RotTag[n: 10]) do |secret|
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 (try breaking this!)"}
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(**) # currently no keywords are passed, but `**` allows for future ones.
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(options[:n]).join).then(&block)
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
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
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
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
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
 
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nero
4
+ class BaseTag
5
+ def resolve(args, context:)
6
+ raise NotImplementedError
7
+ end
8
+ end
9
+ end
@@ -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
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nero
4
+ class Deferred
5
+ attr_reader :tag, :args
6
+
7
+ def initialize(tag, args) = (@tag, @args = tag, args)
8
+ end
9
+ end
@@ -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
@@ -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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nero
4
+ class ProcTag < BaseTag
5
+ def initialize(callable) = @callable = callable
6
+
7
+ def resolve(args, context:) = @callable.call(args, context:)
8
+ end
9
+ 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
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nero
4
+ class Ref
5
+ attr_reader :path
6
+
7
+ def initialize(path) = @path = path.split(".")
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nero
4
+ class RefTag < BaseTag
5
+ def resolve(args, context:)
6
+ Ref.new(args[0].to_s)
7
+ end
8
+ end
9
+ end