Nero is a RubyGem that offers declarative YAML-tags to simplify config files, e.g. for requiring and coercion of env-vars.
Additionally, it allows you to create your own.
Sample:
development:
# env-var with default value
secret: !env [SECRET, "dummy"]
# optional env-var with coercion
debug?: !env/bool? DEBUG
production:
# required env-var (not required during development)
secret: !env SECRET
# coercion
max_threads: !env/integer [MAX_THREADS, 5]
# refer to other keys
min_threads: !env/integer [MIN_THREADS, !ref max_threads ]
# descriptive names
asset_folder: !path/rails_root [ public/assets ]
# easy to add custom tags
cache_ttl: !duration [2, hours]
- π declarative YAML-tags for e.g. requiring and coercing env-vars
- π οΈ add custom tags
- π€οΈ
Rails.application.config_for
drop-in - β»οΈ Zeitwerk-only dependency
Install the gem and add to the application's Gemfile by executing:
bundle add nero
Nero.configure do |nero|
# Path that `Nero.config_for` uses to resolve Symbol or String files, e.g. `Nero.config_for(:app)`
nero.config_dir = "config"
# Add custom tags (also see section about custom tags)
nero.add_tag("upcase") do |tag|
# tag is an instance of [Nero::BaseTag](https://eval.github.io/nero/Nero/BaseTag.html).
tag.args.join.upcase
end
end
Warning
It's early days - the API and included tags will certainly change. Check the CHANGELOG when upgrading.
Given the following config:
# config/app.yml
development:
# env-var with a fallback
secret: !env [SECRET, "dummy"]
# Though the default is false, explicitly providing "false"/"off"/"n"/"no" also works.
debug?: !env/bool? DEBUG
production:
# fail-fast on absence of SECRET
secret: !env SECRET
# always an integer
max_threads: !env/integer [MAX_THREADS, 5]
Loading this config:
# Loading development
Nero.load_file("config/app.yml", root: :development)
# ...and no ENV-vars were provided
#=> {secret: "dummy", debug?: false}
# ...with ENV {"debug" => "true"}
#=> {secret: "dummy", debug?: true}
# Loading production
Nero.load_file("config/app.yml", root: :production)
# ...and no ENV-vars were provided
# raises error: key not found: "SECRET" (KeyError)
# ...with ENV {"SECRET" => "s3cr3t", "MAX_THREADS" => "3"}
#=> {secret: "s3cr3t", max_threads: 3}
Tip
You can also use Nero.config_for(:app)
(similar to Rails.application.config_for).
In Rails applications this gets configured for you. For other application you might need to adjust the config_dir
:
Nero.configure do |config|
config.config_dir = "config"
end
Nero.config_for(:settings, env: Rails.env)
The following tags are provided:
!env KEY
,!env? KEY
Resp. to fetch or get a value fromENV
:--- # required secret: !env SECRET # optional, with fallback: secret: !env [SECRET, "dummy-fallback"] # ...or nil secret: !env? SECRET
- to coerce env-values:
env/integer
,env/integer?
,env/float
,env/float?
:port: !env/integer [PORT, 3000] threads: !env/integer? THREADS # nil when not provided threshold: !env/float CUTOFF
env/bool
,env/bool?
:# required (valid values 'y(es)'/'n(o)', 'true'/'false', 'on'/'off') over18: !env/bool OVER18 # optional, with fallback: secure: !env/bool [SECURE, true] # ...or false: debug?: !env/bool? DEBUG
Tip
Make all env-var's optional by providing ENV["NERO_ENV_ALL_OPTIONAL"]
, e.g.
$ env NERO_ENV_ALL_OPTIONAL=1 SECRET_KEY_BASE_DUMMY=1 rails asset:precompile
!path
Create a Pathname:config: !path config # combining tags: asset_folder: !path - !env PROJECT_ROOT - /public/assets
!path/git_root
,!path/rails_root
Create a Pathname relative to some root-path.
The root-path is expected to be an existing ancestor folder of the yaml-config being parsed.
It's found by traversing up and checking for the presence of specific files/folders, e.g. '.git' (!path/git_root
) or 'config.ru' (!path/rails_root
).
While the root-path needs to exist, the resulting Pathname doesn't need to.project_root: !path/git_root config_folder: !path/rails_root [ config ]
!uri
Create a URI:smtp_url: !uri - smtps:// - !env SMTP_CREDS - @smtp.gmail.com
!str/format
Using Ruby's format specifications:smtp_url: !str/format - smtps://%s:%s@smtp.gmail.com - !env SMTP_USER - !env SMTP_PASS # pass it a map (including a key 'fmt') to use references smtp_url: !str/format fmt: smtps://%<user>s:%<pass>s@smtp.gmail.com user: !env SMTP_USER pass: !env SMTP_PASS
!ref
Include values from elsewhere:NOTE future version should raise properly over ref-ing a non-existing path.# simple min_threads: !env/integer [MIN_THREADS, !ref [max_threads]] max_threads: 5 # oauth_callback -refs-> base.url -refs-> base.host base: host: !env [HOST] url: !str/format ['https://%s', !ref[base, host]] oauth_callback: !str/format - '%s/oauth/callback' - !ref[base, url] # refs are resolved within the tree of the selected root. # The following config won't work when doing `Nero.load_file("config/app.yml", root: :prod)` dev: max_threads: 5 prod: max_threads: !env[MAX_THREADS, !ref[dev, max_threads]]
There's three ways to create your own tags.
For all these methods it's helpful to see the API-docs for Nero::BaseTag.
- a proc
Blocks are passed instances of Nero::BaseTag.
Nero.configure do |nero| nero.add_tag("upcase") do |tag| # `tag` is a `Nero::BaseTag`. # In YAML args are provided as scalar, seq or map: # --- # k: !upcase bar # --- # k: !upcase [bar] # equivalent to: # k: !upcase # - bar # --- # k: !upcase # bar: baz # # Find these args via `tag.args` (Array or Hash): case tag.args when Hash tag.args.each_with_object({}) {|(k,v), acc| acc[k] = v.upcase } else tag.args.map(&:upcase) end # NOTE though a tag might just need one argument (ie scalar), # it's helpful to accept a seq as it allows for chaining: # a: !my/inc 4 # scalar suffices # ...but when chaining, it needs to be a seq: # a: !my/inc [ !my/square 2 ] end end
- re-use existing tag-class
You can add an existing tag under a better fitting name this way.
Also: some tag-classes have options that allow for simple customizations (likecoerce
below):Nero.configure do |nero| nero.add_tag("env/upcase", klass: Nero::EnvTag[coerce: :upcase]) # Alias for path/git_root: nero.add_tag("path/project_root", klass: Nero::PathRootTag[containing: '.git']) end
- custom class
class RotTag < Nero::BaseTag # Configure: # ``` # config.add_tag("rot/12", klass: RotTag[n: 12]) # config.add_tag("rot/10", klass: RotTag[n: 10]) do |secret| # "#{secret} (try breaking this!)" # end # ``` # # Usage in YAML: # ``` # secret: !rot/12 some message # very_secret: !rot/10 [ !env [ MSG, some message ] ] # ``` # => {secret: "EAyq yqEEmsq", very_secret: "Cywo woCCkqo (try breaking this!)"} # By overriding `init_options` we can restrict/require options, # provide default values and do any other setup. # By default an option is available via `options[:foo]`. def init_options(n: 10) super # no specific assignments, so available via `options[:n]`. end def chars @chars ||= (('a'..'z').to_a + ('A'..'Z').to_a + ('0'..'9').to_a) end def resolve(**) # currently no keywords are passed, but `**` allows for future ones. # Here we actually do the work: get the args, rotate strings and delegate to the block. # `args` are the resolved nested args (so e.g. `!env MSG` is already resolved). # `config` is the tag's config, and contains e.g. the block. block = config.fetch(:block, :itself.to_proc) # String#tr replaces any character from the first collection with the same position in the other: args.join.tr(chars.join, chars.rotate(options[:n]).join).then(&block) end end
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.
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.
Bug reports and pull requests are welcome on GitHub at https://github.com/eval/nero.
The gem is available as open source under the terms of the MIT License.