Skip to content

eval/nero

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

61 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ”₯ Nero

Gem Version

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]

Highlights

  • πŸ’Ž declarative YAML-tags for e.g. requiring and coercing env-vars
  • πŸ› οΈ add custom tags
  • πŸ›€οΈ Rails.application.config_for drop-in
  • ♻️ Zeitwerk-only dependency

Installation

Install the gem and add to the application's Gemfile by executing:

bundle add nero

Configuration

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

Usage

Warning

It's early days - the API and included tags will certainly change. Check the CHANGELOG when upgrading.

loading a config

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)

API Documentation.

built-in tags

The following tags are provided:

  • !env KEY, !env? KEY
    Resp. to fetch or get a value from ENV:
    ---
    # 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:
    # 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]]
    NOTE future version should raise properly over ref-ing a non-existing path.

custom tags

There's three ways to create your own tags.

For all these methods it's helpful to see the API-docs for Nero::BaseTag.

  1. a proc
    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
    Blocks are passed instances of Nero::BaseTag.
  2. 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 (like coerce 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
  3. 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

Development

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.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/eval/nero.

License

The gem is available as open source under the terms of the MIT License.

About

πŸ”₯ Declarative YAML-tags

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •