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.
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)
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rails"
4
+
5
+ module Nero
6
+ # @private
7
+ class Railtie < ::Rails::Railtie
8
+ end
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
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nero
4
+ class Result
5
+ attr_reader :value, :errors
6
+
7
+ def initialize(value, errors = [])
8
+ @value = value
9
+ @errors = errors
10
+ end
11
+
12
+ def ok? = errors.empty?
13
+
14
+ def value!
15
+ raise ParseError, errors unless ok?
16
+ value
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nero
4
+ class RootPathTag < BaseTag
5
+ def initialize(containing:)
6
+ @containing = containing
7
+ end
8
+
9
+ def resolve(args, context:)
10
+ relative_path = args[0]&.then { |p| p.empty? ? nil : p }
11
+ dir = context.dir
12
+
13
+ loop do
14
+ if File.exist?(File.join(dir, @containing))
15
+ return relative_path ? Pathname.new(File.join(dir, relative_path)) : Pathname.new(dir)
16
+ end
17
+ parent = File.dirname(dir)
18
+ if parent == dir
19
+ context.add_error("could not find #{@containing} in any ancestor of #{context.dir}")
20
+ return nil
21
+ end
22
+ dir = parent
23
+ end
24
+ end
25
+ end
26
+ end
data/lib/nero/version.rb CHANGED
@@ -3,5 +3,5 @@
3
3
  module Nero
4
4
  # NOTE this is written upon release via:
5
5
  # $ rake gem:build[version=0.3.0]
6
- VERSION = "0.5.0"
6
+ VERSION = "0.7.0.rc2"
7
7
  end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nero
4
+ class Visitor < ::Psych::Visitors::ToRuby
5
+ def self.build(tags, ctx)
6
+ visitor = create
7
+ visitor.instance_variable_set(:@nero_tags, tags)
8
+ visitor.instance_variable_set(:@nero_ctx, ctx)
9
+ visitor
10
+ end
11
+
12
+ def visit_Psych_Nodes_Scalar(o)
13
+ handler = find_nero_tag(o.tag)
14
+ return super unless handler
15
+
16
+ o.tag = nil
17
+ handler.resolve([super], context: @nero_ctx)
18
+ end
19
+
20
+ def visit_Psych_Nodes_Sequence(o)
21
+ handler = find_nero_tag(o.tag)
22
+ return super unless handler
23
+
24
+ o.tag = nil
25
+ args = super
26
+ contains_ref?(args) ? Deferred.new(handler, args) : handler.resolve(args, context: @nero_ctx)
27
+ end
28
+
29
+ def visit_Psych_Nodes_Mapping(o)
30
+ handler = find_nero_tag(o.tag)
31
+ return super unless handler
32
+
33
+ o.tag = nil
34
+ args = super
35
+ contains_ref?(args) ? Deferred.new(handler, args) : handler.resolve(args, context: @nero_ctx)
36
+ end
37
+
38
+ private
39
+
40
+ def find_nero_tag(tag)
41
+ return nil unless tag&.start_with?("!")
42
+ @nero_tags[tag[1..]]
43
+ end
44
+
45
+ def contains_ref?(value)
46
+ case value
47
+ when Ref, Deferred then true
48
+ when Hash then value.values.any? { |v| contains_ref?(v) }
49
+ when Array then value.any? { |v| contains_ref?(v) }
50
+ else false
51
+ end
52
+ end
53
+ end
54
+ end
data/lib/nero.rb CHANGED
@@ -1,416 +1,42 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "zeitwerk"
4
- loader = Zeitwerk::Loader.for_gem
5
- loader.setup
6
-
7
- require "uri"
8
- require "yaml"
9
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"
10
21
 
11
- # TODO fail on unknown tag
12
- # TODO show missing env's at once
13
- # TODO raise when missing arg(s) for tag
14
22
  module Nero
15
- class Error < StandardError; end
16
-
17
- module Resolvable
18
- def try_resolve(ctx, object)
19
- if object.respond_to?(:resolve)
20
- object.resolve(**ctx)
21
- else
22
- object
23
- end
24
- end
25
-
26
- def gen_resolve_tryer(ctx)
27
- method(:try_resolve).curry.call(ctx)
28
- end
29
-
30
- def deep_resolve(object, **ctx)
31
- Util.deep_transform_values(object, &gen_resolve_tryer(ctx))
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
42
- end
43
- extend Resolvable
44
- private_class_method \
45
- :deep_resolve,
46
- :gen_resolve_tryer,
47
- :try_resolve
48
-
49
- class Configuration
50
- attr_reader :tags, :config_dir
51
-
52
- def config_dir=(dir)
53
- @config_dir = Pathname(dir).expand_path
54
- end
55
-
56
- def add_tag(name, klass: BaseTag, &block)
57
- klass, klass_options = klass
58
-
59
- (@tags ||= {})[name] = {klass:}.tap do |h|
60
- h[:block] = block if block
61
- h[:options] = klass_options if klass_options
62
- end
63
- end
64
- end
65
-
66
- def self.configuration
67
- @configuration ||= Configuration.new
68
- end
69
-
70
- def self.configure
71
- yield configuration if block_given?
72
- end
73
-
74
- class BaseTag
75
- include Resolvable
76
-
77
- attr_reader :coder, :options, :ctx
78
-
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
100
-
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
113
- end
114
- end
115
-
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
130
- end
131
- end
132
- end
133
-
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
164
- end
165
- end
166
-
167
- def coercer
168
- return unless @coerce
169
-
170
- @coercer ||= case @coerce
171
- when Symbol then @coerce.to_proc
172
- else
173
- @coerce
174
- end
175
- end
176
-
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
185
-
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)
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/
203
-
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})"
210
- end
211
- end
23
+ def self.parse(yaml, **opts, &block)
24
+ Parser.new(**opts, &block).parse(yaml).value!
212
25
  end
213
26
 
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
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!
246
30
  end
247
31
 
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), **{})
255
- end
256
-
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)
270
- end
271
-
272
- config.add_tag("path") do |tag|
273
- Pathname.new(tag.args.join("/"))
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"])
277
-
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)
287
- else
288
- sprintf(*tag.args)
289
- end
290
- end
291
- end
292
- end
293
- private_class_method :add_default_tags!
294
-
295
- def self.reset_configuration!
296
- @configuration = nil
297
-
298
- configure do |config|
299
- config.config_dir = Pathname.pwd
300
- end
301
-
302
- add_default_tags!
303
- end
304
- reset_configuration!
305
-
306
- def self.yaml_options
307
- {
308
- permitted_classes: [Symbol] + configuration.tags.values.map { _1[:klass] },
309
- aliases: true
310
- }
311
- end
312
- private_class_method :yaml_options
313
-
314
- def self.load_config(file, root: nil, env: nil, resolve: true)
315
- root ||= env
316
- add_tags!
317
-
318
- config_file = resolve_file(file)
319
-
320
- if config_file.exist?
321
- process_yaml(yaml_load_file(config_file, yaml_options), root:, config_file:, resolve:)
322
- else
323
- raise "Can't find file #{config_file}"
324
- end
325
- end
326
-
327
- def self.resolve_file(file)
328
- case file
32
+ def self.config_for(file, env: nil, root: nil, &block)
33
+ root ||= env&.to_s
34
+ path = case file
329
35
  when Pathname then file
330
- # TODO expand full path
331
- else
332
- configuration.config_dir / "#{file}.yml"
333
- end
334
- end
335
- private_class_method :resolve_file
336
-
337
- def self.load(raw, root: nil, env: nil, resolve: true)
338
- root ||= env
339
- add_tags!
340
-
341
- process_yaml(yaml_load(raw, yaml_options), root:, resolve:)
342
- end
343
-
344
- def self.process_yaml(yaml, root: nil, resolve: true, config_file: nil)
345
- config_file ||= (Pathname.pwd / __FILE__)
346
-
347
- unresolved = Util.deep_symbolize_keys(yaml).then do
348
- root ? _1[root.to_sym] : _1
349
- end
350
- ctx = {tags: configuration.tags, yaml: unresolved, yaml_file: config_file}
351
- init_tags!(collect_tags(unresolved), ctx:)
352
-
353
- return unresolved unless resolve
354
-
355
- # NOTE originally ctx was passed at this point. Maybe delete this.
356
- deep_resolve(unresolved, **{})
357
- end
358
- private_class_method :process_yaml
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
-
368
- def self.yaml_load_file(file, opts = {})
369
- if Psych::VERSION < "4"
370
- YAML.load_file(file)
371
- else
372
- YAML.load_file(file, **opts)
373
- end
374
- end
375
- private_class_method :yaml_load_file
376
-
377
- def self.yaml_load(file, opts = {})
378
- if Psych::VERSION < "4"
379
- YAML.load(file)
380
- else
381
- YAML.load(file, **opts)
382
- end
383
- end
384
- private_class_method :yaml_load
385
-
386
- def self.add_tags!
387
- configuration.tags.each do |tag_name, tag|
388
- YAML.add_tag("!#{tag_name}", tag[:klass])
389
- end
390
- end
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
- []
36
+ else Pathname.new("config") / "#{file}.yml"
411
37
  end
38
+ parse_file(path.expand_path, root: root, &block)
412
39
  end
413
- private_class_method :collect_tags
414
40
  end
415
41
 
416
- 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
data/rakelib/yard.rake ADDED
@@ -0,0 +1,12 @@
1
+ require "yard"
2
+
3
+ YARD::Rake::YardocTask.new(:docs) do |t|
4
+ # Options defined in `.yardopts` are read first, then merged with
5
+ # options defined here.
6
+ #
7
+ # It's recommended to define options in `.yardopts` instead of here,
8
+ # as `.yardopts` can be read by external YARD tools, like the
9
+ # hot-reload YARD server `yard server --reload`.
10
+
11
+ # t.options += ['--title', "Something custom"]
12
+ end