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.
data/lib/nero.rb CHANGED
@@ -1,588 +1,42 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "zeitwerk"
4
- loader = Zeitwerk::Loader.for_gem
5
- loader.do_not_eager_load("#{__dir__}/nero/railtie.rb")
6
- loader.setup
7
-
8
- require "uri"
9
- require "yaml"
10
3
  require "pathname"
4
+ require "psych"
5
+ require "set"
6
+
7
+ require_relative "nero/version"
8
+ require_relative "nero/error"
9
+ require_relative "nero/result"
10
+ require_relative "nero/context"
11
+ require_relative "nero/ref"
12
+ require_relative "nero/deferred"
13
+ require_relative "nero/base_tag"
14
+ require_relative "nero/proc_tag"
15
+ require_relative "nero/ref_tag"
16
+ require_relative "nero/env_tag"
17
+ require_relative "nero/root_path_tag"
18
+ require_relative "nero/format_tag"
19
+ require_relative "nero/visitor"
20
+ require_relative "nero/parser"
11
21
 
12
- # TODO fail on unknown tag
13
- # TODO show missing env's at once
14
- # TODO raise when missing arg(s) for tag
15
22
  module Nero
16
- class Error < StandardError; end
17
-
18
- module DigExt
19
- # ⛏️💥 Like `dig`, but raises `ArgumentError` when `path` does not exist.
20
- # @example like dig
21
- # {a: {b: 2}}.dig!(:a, :b) #=> 2
22
- # {a: {b: 2}}.dig!(:a, :c) #=> ArgumentError, path not found [:a, :c] (ArgumentError)
23
- # @raise [ArgumentError] when `path` does not exist.
24
- # @overload dig!(*path)
25
- # @param path nested keys into config
26
- def dig!(k0, *k)
27
- k.unshift(k0)
28
-
29
- unless paths.include?(k)
30
- raise ArgumentError, "path not found #{k}"
31
- end
32
- dig(*k)
33
- end
34
-
35
- private
36
-
37
- def paths
38
- @paths ||= gather_paths(self).to_set
39
- end
40
-
41
- def gather_paths(item, acc: [], path: [])
42
- acc += [path]
43
-
44
- case item
45
- when NilClass
46
- []
47
- when Hash
48
- item.flat_map { |(k, v)| gather_paths(v, acc: acc, path: path + [k]) }
49
- when Array
50
- item.each_with_index.flat_map do |item, ix|
51
- gather_paths(item, acc: acc, path: path + [ix])
52
- end
53
- else
54
- acc
55
- end
56
- end
57
- end
58
-
59
- class Config < Hash
60
- include DigExt
61
-
62
- def self.for(v)
63
- case v
64
- when self then v
65
- when Hash then self.[](v)
66
- else
67
- v
68
- end
69
- end
70
- end
71
-
72
- module Resolvable
73
- def try_resolve(object)
74
- if object.respond_to?(:resolve)
75
- object.resolve
76
- else
77
- object
78
- end
79
- end
80
-
81
- def deep_resolve(object)
82
- Util.deep_transform_values(object, &method(:try_resolve))
83
- end
84
-
85
- def resolve_nested!(coder)
86
- case coder.type
87
- when :seq
88
- coder.seq.map!(&method(:try_resolve))
89
- when :map
90
- coder.map = deep_resolve(coder.map)
91
- end
92
- end
93
- end
94
- extend Resolvable
95
- private_class_method \
96
- :deep_resolve,
97
- :try_resolve
98
-
99
- class Configuration
100
- attr_reader :config_dir
101
-
102
- def tags
103
- @tags ||= {}
104
- end
105
-
106
- def config_dir=(dir)
107
- @config_dir = Pathname(dir).expand_path
108
- end
109
-
110
- def add_tag(name, klass: BaseTag, &block)
111
- klass, klass_options = klass
112
-
113
- tags[name] = {klass:}.tap do |h|
114
- h[:block] = block if block
115
- h[:options] = klass_options if klass_options
116
- end
117
- end
118
- end
119
-
120
- def self.configuration
121
- @configuration ||= Configuration.new
122
- end
123
-
124
- def self.add_tags!
125
- configuration.tags.each do |tag_name, tag|
126
- YAML.add_tag("!#{tag_name}", tag[:klass])
127
- end
128
- end
129
- private_class_method :add_tags!
130
-
131
- def self.configure
132
- yield configuration if block_given?
133
- ensure
134
- add_tags!
135
- end
136
-
137
- # Superclass for all tags.
138
- #
139
- # Writing your own tag-class would look something like this:
140
- #
141
- # Wanted usage in YAML:
142
- # ```ruby
143
- # Nero.load(<<~YAML)
144
- # secret: !rot/12 "some message"
145
- # other_secret: !rot/13 [ !env [SECRET, some message] ]
146
- # YAML
147
- # ```
148
- #
149
- # Required config:
150
- # ```ruby
151
- # config.add_tag("rot/12", klass: RotTag[n: 12])
152
- # config.add_tag("rot/13", klass: RotTag[n: 13]) do |secret|
153
- # "#{secret} (try breaking this!)"
154
- # end
155
- # ```
156
- # The class then would look like this:
157
- # ```ruby
158
- # class RotTag < Nero::BaseTag
159
- # attr_reader :n
160
- #
161
- # # Overriding this method...:
162
- # # - restricts options
163
- # # ie `RotTag[x: 1]` would raise.
164
- # # - sets default values
165
- # # - makes options available via getters
166
- # # (otherwise available via `options[:n]`).
167
- # def init_options(n: 10)
168
- # super
169
- # @n = n
170
- # end
171
- #
172
- # # This is where the magic happens.
173
- # # (Accepting any keyword arguments keeps the method fw-compatible).
174
- # def resolve(**)
175
- # # `args` are the resolved arguments (Array or Hash).
176
- # # `config` the config of the tag (containing e.g. the proc).
177
- # block = config.fetch(:block, :itself.to_proc)
178
- # args.join.tr(chars.join, chars.rotate(n).join).then(&block)
179
- # end
180
- #
181
- # # Just some helper method with all characters that can be rotated.
182
- # def chars
183
- # %w(a b c) # etc
184
- # end
185
- # end
186
- # ```
187
- #
188
- class BaseTag
189
- include Resolvable
190
-
191
- attr_reader :coder, :options, :ctx
192
-
193
- # Convenience method simplifying {Nero::Configuration#add_tag}:
194
- #
195
- # ```ruby
196
- # config.add_tag("foo", klass: SomeTag[some_option: 1])
197
- # ```
198
- def self.[](**options)
199
- [self, options]
200
- end
201
-
202
- # @private used by YAML
203
- def init_with(coder)
204
- @coder = coder
205
- end
206
-
207
- def init(ctx:, options:)
208
- init_ctx(ctx)
209
- init_options(**options)
210
- end
211
-
212
- def init_ctx(ctx)
213
- @ctx = ctx
214
- end
215
-
216
- def init_options(**options)
217
- @options = options
218
- end
219
-
220
- def tag_name
221
- coder.tag[1..]
222
- end
223
-
224
- def args
225
- @args ||= begin
226
- resolve_nested!(coder)
227
- case coder.type
228
- when :map then Util.deep_symbolize_keys(coder.map)
229
- else
230
- Array(coder.public_send(coder.type))
231
- end
232
- end
233
- end
234
-
235
- def config
236
- ctx.dig(:tags, tag_name)
237
- end
238
-
239
- def resolve(**)
240
- if (block = config[:block])
241
- if block.parameters.map(&:last).include?(:coder)
242
- # legacy
243
- block.call(coder, ctx)
244
- else
245
- block.call(self)
246
- end
247
- else
248
- args
249
- end
250
- end
251
- end
252
-
253
- # Requires an env-var to be available and coerces the value.
254
- # When tag-name ends with "?", the env-var is optional.
255
- #
256
- # Given config:
257
- # ```ruby
258
- # config.add_tag("env/upcase", klass: Nero::EnvTag[coerce: :upcase])
259
- # config.add_tag("env/upcase?", klass: Nero::EnvTag[coerce: :upcase])
260
- # ```
261
- #
262
- # Then YAML => result:
263
- # ```ruby
264
- # "--- env/upcase [MSG, Hello World]" #=> "HELLO WORLD"
265
- # "--- env/upcase MSG" #=> raises when not ENV.has_key? "MSG"
266
- # "--- env/upcase? MSG" #=> nil
267
- # ```
268
- #
269
- # YAML-args supported:
270
- # - scalar —
271
- # name of env-var, e.g. `!env HOME`
272
- # - seq —
273
- # name of env-var and fallback, e.g. `!env [HOME, /root]`
274
- #
275
- # Options:
276
- # - `coerce` —
277
- # symbol or proc to be applied to value of env-var.
278
- # when using coerce, the block is ignored.
279
- #
280
- class EnvTag < BaseTag
281
- def resolve(**)
282
- if coercer
283
- coercer.call(env_value) unless env_value.nil?
284
- elsif ctx.dig(:tags, tag_name, :block)
285
- super
286
- else
287
- env_value
288
- end
289
- end
290
-
291
- def coercer
292
- return unless @coerce
293
-
294
- @coercer ||= case @coerce
295
- when Symbol then @coerce.to_proc
296
- else
297
- @coerce
298
- end
299
- end
300
-
301
- def init_options(coerce: nil)
302
- @coerce = coerce
303
- end
304
-
305
- def optional
306
- tag_name.end_with?("?") || !!ENV["NERO_ENV_ALL_OPTIONAL"]
307
- end
308
- alias_method :optional?, :optional
309
-
310
- def env_value
311
- self.class.env_value(*args, optional:)
312
- end
313
-
314
- def self.env_value(k, fallback = nil, optional: false)
315
- if fallback.nil? && !optional
316
- ENV.fetch(k)
317
- else
318
- ENV.fetch(k, fallback)
319
- end
320
- end
321
-
322
- def self.coerce_bool(v)
323
- return false unless v
324
-
325
- re_true = /y|Y|yes|Yes|YES|true|True|TRUE|on|On|ON/
326
- re_false = /n|N|no|No|NO|false|False|FALSE|off|Off|OFF/
327
-
328
- case v
329
- when TrueClass, FalseClass then v
330
- when re_true then true
331
- when re_false then false
332
- else
333
- raise "bool value should be one of y(es)/n(o), on/off, true/false (got #{v.inspect})"
334
- end
335
- end
336
- end
337
-
338
- # Construct path relative to some root-path.
339
- # Root-paths are expected to be ancestors of the yaml-file being parsed.
340
- # They are found by traversing up and checking for specific files/folders, e.g. '.git' or 'Gemfile'.
341
- # Any argument is appended to the root-path, constructing a path-instance that may exist.
342
- class PathRootTag < BaseTag
343
- # Config:
344
- # config.add_tag("path/git_root", klass: PathRootTag[containing: ".git"])
345
- # config.add_tag("path/rails_root", klass: PathRootTag[containing: "Gemfile"])
346
- #
347
- # YAML:
348
- # project_root: !path/git_root
349
- # config_path: !path/git_root [ config ]
350
- def init_options(containing:)
351
- super
352
- end
353
-
354
- def resolve(**)
355
- # TODO validate upfront
356
- raise <<~ERR unless root_path
357
- #{tag_name}: failed to find root-path (ie an ancestor of #{ctx[:yaml_file]} containing #{options[:containing].inspect}).
358
- ERR
359
- root_path.join(*args).then(&config.fetch(:block, :itself.to_proc))
360
- end
361
-
362
- def root_path
363
- find_up(ctx[:yaml_file], options[:containing])
364
- end
365
-
366
- def find_up(path, containing)
367
- (path = path.parent) until path.root? || (path / containing).exist?
368
- path unless path.root?
369
- end
23
+ def self.parse(yaml, **opts, &block)
24
+ Parser.new(**opts, &block).parse(yaml).value!
370
25
  end
371
26
 
372
- def self.add_default_tags!
373
- configure do |config|
374
- config.add_tag("ref") do |tag|
375
- # validate: non-empty coder.seq, only strs, path must exists in ctx[:config]
376
-
377
- path = tag.args.map(&:to_sym)
378
- deep_resolve(tag.ctx[:yaml].dig(*path))
379
- end
380
-
381
- config.add_tag("env", klass: EnvTag)
382
- config.add_tag("env?", klass: EnvTag)
383
- config.add_tag("env/float", klass: EnvTag[coerce: :to_f])
384
- config.add_tag("env/float?", klass: EnvTag[coerce: :to_f])
385
-
386
- config.add_tag("env/integer", klass: EnvTag[coerce: :to_i])
387
- config.add_tag("env/integer?", klass: EnvTag[coerce: :to_i])
388
-
389
- config.add_tag("env/bool", klass: EnvTag) do |tag|
390
- EnvTag.coerce_bool(tag.env_value)
391
- end
392
- config.add_tag("env/bool?", klass: EnvTag) do |tag|
393
- EnvTag.coerce_bool(tag.env_value)
394
- end
395
-
396
- config.add_tag("path") do |tag|
397
- Pathname.new(tag.args.join("/"))
398
- end
399
- config.add_tag("path/git_root", klass: PathRootTag[containing: ".git"])
400
- config.add_tag("path/rails_root", klass: PathRootTag[containing: "config.ru"])
401
-
402
- config.add_tag("uri") do |tag|
403
- URI.join(*tag.args.join)
404
- end
405
-
406
- config.add_tag("str/format") do |tag|
407
- case tag.args
408
- when Hash
409
- fmt = tag.args.delete(:fmt)
410
- sprintf(fmt, tag.args)
411
- else
412
- sprintf(*tag.args)
413
- end
414
- end
415
- end
27
+ def self.parse_file(path, env: nil, root: nil, &block)
28
+ root ||= env&.to_s
29
+ Parser.new(root: root, &block).parse_file(path).value!
416
30
  end
417
- private_class_method :add_default_tags!
418
31
 
419
- def self.reset_configuration!
420
- @configuration = nil
421
-
422
- configure do |config|
423
- config.config_dir = Pathname.new("config").expand_path
424
- end
425
-
426
- add_default_tags!
427
- add_tags!
428
- end
429
- reset_configuration!
430
-
431
- def self.default_yaml_options
432
- {
433
- permitted_classes: [Symbol] + configuration.tags.values.map { _1[:klass] },
434
- aliases: true
435
- }
436
- end
437
- private_class_method :default_yaml_options
438
-
439
- def self.yaml_options(yaml_options)
440
- epc = yaml_options.delete(:extra_permitted_classes)
441
- default_yaml_options.merge(yaml_options).tap do
442
- _1[:permitted_classes].push(*epc)
443
- end
444
- end
445
- private_class_method :yaml_options
446
-
447
- # Like `YAML.load` with extra options.
448
- #
449
- # @param [Symbol, String] root return the value of this root key.
450
- # @param [Boolean] resolve (for debug purposes) not resolving would leave the Nero-tags as-is.
451
- # @param [Array<ClassName>] extra_permitted_classes classes that are added
452
- # to the default permitted_classes and passed to `YAML.load`.
453
- # @param [Hash] yaml_options options passed to `YAML.load`.
454
- # @return [Nero::Config (when the data is a Hash)]
455
- # @example
456
- # Nero.load(<<~YAML, extra_permitted_classes: [Time])
457
- # home: !env HOME,
458
- # created_at: 2010-02-11 11:02:57
459
- # project_root: !path/git_root
460
- # YAML
461
- # #=> {
462
- # # home: "/Users/gert",
463
- # # created_at: 2010-02-11 12:02:57 +0100,
464
- # # project_root: #<Pathname:/Users/gert/projects/nero>
465
- # # }
466
- def self.load(yaml, root: nil, resolve: true, **yaml_options)
467
- process_yaml(yaml_load(yaml, yaml_options(yaml_options)), root:, resolve:)
468
- end
469
-
470
- # Like `YAML.load_file`. See {load} for options.
471
- # @return [Nero::Config (when the YAML-data is a Hash)]
472
- def self.load_file(file, root: nil, resolve: true, **yaml_options)
473
- config_file = (file.is_a?(Pathname) ? file : Pathname.new(file)).expand_path
474
- process_yaml(yaml_load_file(config_file, yaml_options(yaml_options)), root:, config_file:, resolve:)
475
- end
476
-
477
- # Convenience wrapper for {load_file} that works like `Rails.application.config_for`.
478
- # @see https://api.rubyonrails.org/classes/Rails/Application.html#method-i-config_for Rails' config_for documentation
479
- #
480
- # The file-argument is expanded like so `(configuration.config_dir / "#{file}.yml").expand_path`.
481
- #
482
- # @param [Symbol, String, Pathname] file `Symbol` or `String` are expanded as shown above. A `Pathname` is used as-is.
483
- # @param [Symbol, String] env return the value of this root key.
484
- # @param [Symbol, String] root return the value of this root key.
485
- # @param [Boolean] resolve (for debug purposes) not resolving would leave the Nero-tags as-is.
486
- # @param [Array<ClassName>] extra_permitted_classes classes that are added
487
- # to the default permitted_classes and passed to `YAML.load`.
488
- # @param [Hash] yaml_options options passed to `YAML.load_file`.
489
- # @return [Nero::Config (when the data is a Hash)]
490
- # @example
491
- # Nero.config_for(:app, env: Rails.env) #=> {...}
492
- def self.config_for(file, root: nil, env: nil, **yaml_options)
493
- root ||= env
494
-
495
- load_file(resolve_file(file), root:, **yaml_options)
496
- end
497
-
498
- # @deprecated Use `load_file` or `config_for` instead.
499
- def self.load_config(file, root: nil, env: nil, resolve: true)
500
- warn "[DEPRECATION] `load_config` is deprecated. Use `load_file` or `config_for` instead."
501
- root ||= env
502
- add_tags!
503
-
504
- config_file = resolve_file(file)
505
-
506
- if config_file.exist?
507
- process_yaml(yaml_load_file(config_file, yaml_options), root:, config_file:, resolve:)
508
- else
509
- raise "Can't find file #{config_file}"
510
- end
511
- end
512
-
513
- def self.resolve_file(file)
514
- case file
32
+ def self.config_for(file, env: nil, root: nil, &block)
33
+ root ||= env&.to_s
34
+ path = case file
515
35
  when Pathname then file
516
- else
517
- (configuration.config_dir / "#{file}.yml").expand_path
36
+ else Pathname.new("config") / "#{file}.yml"
518
37
  end
38
+ parse_file(path.expand_path, root: root, &block)
519
39
  end
520
- private_class_method :resolve_file
521
-
522
- def self.process_yaml(yaml, root: nil, resolve: true, config_file: nil)
523
- config_file ||= (Pathname.pwd / __FILE__)
524
-
525
- unresolved = Util.deep_symbolize_keys(yaml).then do
526
- root ? _1[root.to_sym] : _1
527
- end
528
- ctx = {tags: configuration.tags, yaml: unresolved, yaml_file: config_file}
529
- init_tags!(collect_tags(unresolved), ctx:)
530
-
531
- return unresolved unless resolve
532
-
533
- Config.for(deep_resolve(unresolved))
534
- end
535
- private_class_method :process_yaml
536
-
537
- def self.init_tags!(tags, ctx:)
538
- tags.each do |tag|
539
- options = ctx.dig(:tags, tag.tag_name, :options) || {}
540
- tag.init(ctx:, options:)
541
- end
542
- end
543
- private_class_method :init_tags!
544
-
545
- def self.yaml_load_file(file, opts = {})
546
- if Psych::VERSION < "4"
547
- YAML.load_file(file)
548
- else
549
- YAML.load_file(file, **opts)
550
- end
551
- end
552
- private_class_method :yaml_load_file
553
-
554
- def self.yaml_load(file, opts = {})
555
- if Psych::VERSION < "4"
556
- YAML.load(file)
557
- else
558
- YAML.load(file, **opts)
559
- end
560
- end
561
- private_class_method :yaml_load
562
-
563
- def self.collect_tags(obj)
564
- case obj
565
- when Hash
566
- obj.each_value.flat_map { collect_tags(_1) }.compact
567
- when Nero::BaseTag
568
- [obj] +
569
- case obj.coder.type
570
- when :seq
571
- collect_tags(obj.coder.seq)
572
- when :map
573
- collect_tags(obj.coder.map)
574
- else
575
- []
576
- end
577
- when Array
578
- obj.flat_map { collect_tags(_1) }.compact
579
- else
580
- []
581
- end
582
- end
583
- private_class_method :collect_tags
584
40
  end
585
41
 
586
- require "nero/railtie" if defined?(Rails::Railtie)
587
-
588
- loader.eager_load if ENV.key?("CI")
42
+ require_relative "nero/railtie" if defined?(Rails::Railtie)
data/rakelib/gem.rake CHANGED
@@ -1,4 +1,5 @@
1
1
  namespace :gem do
2
+ desc "Write new version to version.rb"
2
3
  task "write_version", [:version] do |_task, args|
3
4
  if args[:version]
4
5
  version = args[:version].split("=").last
@@ -8,18 +9,11 @@ namespace :gem do
8
9
  ruby -pi -e 'gsub(/VERSION = ".*"/, %{VERSION = "#{version}"})' #{version_file}
9
10
  CMD
10
11
  Bundler.ui.confirm "Version #{version} written to #{version_file}."
12
+
13
+ system("bundle install", exception: true)
14
+ Bundler.ui.confirm "Gemfile.lock updated."
11
15
  else
12
16
  Bundler.ui.warn "No version provided, keeping version.rb as is."
13
17
  end
14
18
  end
15
-
16
- desc "Build [version]"
17
- task "build", [:version] => %w[write_version] do
18
- Rake::Task["build"].invoke
19
- end
20
-
21
- desc "Build and push [version] to rubygems"
22
- task "release", [:version] => %w[build] do
23
- Rake::Task["release:rubygem_push"].invoke
24
- end
25
19
  end
metadata CHANGED
@@ -1,28 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nero
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0.rc2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gert Goet
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-04-10 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
- - !ruby/object:Gem::Dependency
13
- name: zeitwerk
14
- requirement: !ruby/object:Gem::Requirement
15
- requirements:
16
- - - ">="
17
- - !ruby/object:Gem::Version
18
- version: '0'
19
- type: :runtime
20
- prerelease: false
21
- version_requirements: !ruby/object:Gem::Requirement
22
- requirements:
23
- - - ">="
24
- - !ruby/object:Gem::Version
25
- version: '0'
26
12
  - !ruby/object:Gem::Dependency
27
13
  name: appraisal
28
14
  requirement: !ruby/object:Gem::Requirement
@@ -62,9 +48,25 @@ files:
62
48
  - gemfiles/psych_3.gemfile
63
49
  - gemfiles/psych_4.gemfile
64
50
  - lib/nero.rb
51
+ - lib/nero/base_tag.rb
52
+ - lib/nero/context.rb
53
+ - lib/nero/deferred.rb
54
+ - lib/nero/env_tag.rb
55
+ - lib/nero/error.rb
56
+ - lib/nero/format_tag.rb
57
+ - lib/nero/parser.rb
58
+ - lib/nero/proc_tag.rb
59
+ - lib/nero/rails.rb
60
+ - lib/nero/rails/credentials_tag.rb
61
+ - lib/nero/rails/duration_tag.rb
62
+ - lib/nero/rails/string_inquirer_tag.rb
65
63
  - lib/nero/railtie.rb
66
- - lib/nero/util.rb
64
+ - lib/nero/ref.rb
65
+ - lib/nero/ref_tag.rb
66
+ - lib/nero/result.rb
67
+ - lib/nero/root_path_tag.rb
67
68
  - lib/nero/version.rb
69
+ - lib/nero/visitor.rb
68
70
  - rakelib/gem.rake
69
71
  - rakelib/yard.rake
70
72
  - sig/nero.rbs
@@ -89,7 +91,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
89
91
  - !ruby/object:Gem::Version
90
92
  version: '0'
91
93
  requirements: []
92
- rubygems_version: 3.6.2
94
+ rubygems_version: 3.6.7
93
95
  specification_version: 4
94
96
  summary: Convenience YAML-tags
95
97
  test_files: []