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 valuesecret: !env [SECRET, "dummy"]# optional env-var with coerciondebug?: !env/bool? DEBUGproduction:
# required env-var (not required during development)secret: !env SECRET# coercionmax_threads: !env/integer [MAX_THREADS, 5]# refer to other keysmin_threads: !env/integer [MIN_THREADS, !ref max_threads ]# descriptive namesasset_folder: !path/rails_root [ public/assets ]# easy to add custom tagscache_ttl: !duration [2, hours]- 💎 declarative YAML-tags for e.g. requiring and coercing env-vars
- 🛠️ add custom tags
- 🛤️
Rails.application.config_fordrop-in - ♻️ Zeitwerk-only dependency
Install the gem and add to the application's Gemfile by executing:
bundle add neroNero.configuredo |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.upcaseendendWarning
It's early days - the API and included tags will certainly change. Check the CHANGELOG when upgrading.
Given the following config:
# config/app.ymldevelopment:
# env-var with a fallbacksecret: !env [SECRET, "dummy"]# Though the default is false, explicitly providing "false"/"off"/"n"/"no" also works.debug?: !env/bool? DEBUGproduction:
# fail-fast on absence of SECRETsecret: !env SECRET# always an integermax_threads: !env/integer [MAX_THREADS, 5]Loading this config:
# Loading developmentNero.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 productionNero.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.configuredo |config|
config.config_dir="config"endNero.config_for(:settings,env: Rails.env)The following tags are provided:
!env KEY,!env? KEY
Resp. to fetch or get a value fromENV:--- # requiredsecret: !env SECRET# optional, with fallback:secret: !env [SECRET, "dummy-fallback"]# ...or nilsecret: !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 providedthreshold: !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 - !envPROJECT_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_rootconfig_folder: !path/rails_root [ config ]
!uri
Create a URI:smtp_url: !uri - smtps:// - !envSMTP_CREDS- @smtp.gmail.com
!str/format
Using Ruby's format specifications:smtp_url: !str/format - smtps://%s:%s@smtp.gmail.com - !envSMTP_USER - !envSMTP_PASS# pass it a map (including a key 'fmt') to use referencessmtp_url: !str/formatfmt: smtps://%<user>s:%<pass>s@smtp.gmail.comuser: !env SMTP_USERpass: !env SMTP_PASS
!ref
Include values from elsewhere:NOTE future version should raise properly over ref-ing a non-existing path.# simplemin_threads: !env/integer [MIN_THREADS, !ref [max_threads]]max_threads: 5# oauth_callback -refs-> base.url -refs-> base.hostbase: 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: 5prod: 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 procBlocks are passed instances of Nero::BaseTag.
Nero.configuredo |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):casetag.argswhenHashtag.args.each_with_object({}){|(k,v),acc| acc[k]=v.upcase}elsetag.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 ]endend
- 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 (likecoercebelow):Nero.configuredo |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
classRotTag < 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]`.definit_options(n: 10)super# no specific assignments, so available via `options[:n]`.enddefchars@chars ||= (('a'..'z').to_a + ('A'..'Z').to_a + ('0'..'9').to_a)enddefresolve(**)# 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)endend
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.