NxtRegistry
NxtRegistry
is a simple container that allows you to register and resolve values in nested structures.
Installation
Add this line to your application's Gemfile:
gem 'nxt_registry'
And then execute:
$ bundle
Or install it yourself as:
$ gem install nxt_registry
Usage
Simple use case
Instance Level
If you simply need a single global instance of a registry include NxtRegistry::Singleton
:
class Example
include NxtRegistry::Singleton
registry do
register(:ruby, 'Stone')
register(:python, 'Snake')
register(:javascript, 'undefined')
end
end
Example.resolve(:ruby) # => 'Stone'
Alternatively you can simply create instances of NxtRegistry::Registry
:
registry = NxtRegistry::Registry.new do
register(:andy, 'Andy')
register(:anthony, 'Anthony')
register(:aki, 'Aki')
end
registry.resolve(:aki) # => 'Aki'
Class Level
You can also add registries on the class level simply by extending your class with NxtRegistry
class OtherExample
extend NxtRegistry
registry(:errors) do
register(KeyError, ->(error) { puts 'KeyError handler' } )
register(ArgumentError, ->(error) { puts 'ArgumentError handler' } )
end
registry(:country_codes) do
register(:germany, :de)
register(:england, :uk)
register(:france, :fr)
end
end
OtherExample.registry(:errors).resolve(KeyError)
# KeyError handler
# => nil
OtherExample.registry(:country_codes).resolve(:germany)
# => :de
Register Patterns
You can also register values with patterns as keys. Non pattern keys are always evaluated first and then patterns will be tried to match by definition sequence.
class Example
extend NxtRegistry
registry :status_codes do
register(/\A4\d{2}\z/, 'Client errors')
register(/\A5.*\z/, 'Server errors')
register('422', 'Unprocessable Entity')
register(:'503', 'Internal Server Error')
end
end
Example.registry(:status_codes).resolve('503') # => "Internal Server Error"
Example.registry(:status_codes).resolve(503) # => "Internal Server Error"
Example.registry(:status_codes).resolve(422) # => "Unprocessable Entity"
Example.registry(:status_codes).resolve(404) # => "Client Errors"
Readers
Access your defined registries with the registry(:country_code)
method.
Nesting registries
You can also simply nest registries like so:
class Nested
extend NxtRegistry
registry :developers do
register(:frontend) do
register(:igor, 'Igor')
register(:ben, 'Ben')
end
register(:backend) do
register(:rapha, 'Rapha')
register(:aki, 'Aki')
end
end
end
Nested.registry(:developers).resolve(:frontend, :igor)
# => 'Igor'
Inherit options in nested registries
class Nested
extend NxtRegistry
registry :developers, default: 'options can be inherited' do
register(:frontend, inherit_options: true) do
register(:igor, 'Igor')
register(:ben, 'Ben')
end
end
end
Nested.registry(:developers).resolve(:frontend, :blank)
# => 'options can be inherited'
Defining specific nesting levels of a registry
Another feature of NxtRegistry
is that you can define the nesting levels for a registry. Levels allow you to dynamically
register values within the defined levels. This means that on any level the registry will resolve to another registry and
you can register values into a deeply nested structure.
class Layer
extend NxtRegistry
registry :from do
level :to do
level :via
end
end
end
# On every upper level every resolve returns a registry
Layer.registry(:from) # => Registry[from]
Layer.registry(:from).resolve(:munich) # => Registry[to] -> {}
Layer.registry(:from).resolve(:amsterdam) # => Registry[to] -> {}
Layer.registry(:from).resolve(:any_key) # => Registry[to] -> {}
Layer.registry(:from).resolve(:munich, :amsterdam) # => Registry[via] -> {}
# Register a value on the bottom level
Layer.registry(:from).resolve(:munich, :amsterdam).register(:train, -> { 'train' })
# Resolve the complete path
Layer.registry(:from).resolve(:munich, :amsterdam, :train) # => 'train'
For registries with multiple levels the normal syntax for registering and resolving becomes quite weird and unreadable. This is why every registry can be accessed through it's name or a custom accessor. The above example then can be simplified as follows.
class Layer
extend NxtRegistry
registry :path, accessor: :from do # registry named path, can be accessed with .from(...)
level :to do
level :via
end
end
end
# Register a value
Layer.registry(:path).from(:munich).to(:amsterdam).via(:train, -> { 'train' })
# Resolve the complete path
Layer.registry(:path).from(:munich).to(:amsterdam).via(:train) # => 'train'
Note that this feature is also available for registries with a single level only.
Restrict keys to a certain set
Use allowed_keys
to restrict which keys can be registered on a specific level.
registry :example, allowed_keys: %w[one two three]
Require a certain set of keys to be registered
Use required_keys
to enforce a certain set of keys to be registered on a specific level. This is especially helpful
if you use registries in multiple places and you want to ensure they all register the same set of keys.
registry :example, required_keys: %w[one two three]
Default values
Use default
to register a default value that will be resolved in case an attribute was not registered.
registry :example, default: ->(value) { 'default' }
Blocks
When you register a block value that can be called, it will automatically be called when you resolve the value.
If that's not what you want, you can configure your registry (on each level) not to call blocks directly by defining call false
registry :example, call: false do
register(:one, ->(value) { 'Not called when resolved' } )
end
Memoize
Values are memoized per default. Switch it off with memoize: false
registry :example, memoize: false do
register(:one, -> { Time.current } )
end
registry.resolve(:one)
# => 2020-01-02 23:56:15 +0100
registry.resolve(:one)
# => 2020-01-02 23:56:17 +0100
registry.resolve(:one)
# => 2020-01-02 23:56:18 +0100
IMPORTANT: whenever you want your value to be evaluated anew every time it is resolved, you should always wrap it in a lambda.
For example, if you're resolving an ENV variable you should do it this way:
registry :example do
register(:env_variable, -> { ENV['FEATURE_FLAG'] })
end
In this case config can be reloaded on the fly, and tests can also overwrite feature flags, for example.
Resolve callbacks
You can hook into the before and after resolver callbacks in case you need to lay hands on your values
before and / or after resolving. A callback can be anything that implements :call
to which the value is passed.
registry :example do
key_resolver ->(key) { key.strip }
resolver ->(value) { value.upcase }
register(:input, 'output')
end
registry.resolve(' input ')
# => 'OUTPUT'
Transform keys
NxtRegistry
uses a plain ruby hash to store values internally. Per default all keys used are transformed with &:to_s
.
Thus you can use symbols or strings to register and resolve values. If it's not what you want, switch it off with
transform_keys false
or define your own key transformer by assigning a block to transform_keys:
transform_keys ->(key) { key.upcase }
registry :example do
transform_keys ->(key) { key.to_s.downcase }
register(:bombshell, 'hanna')
end
registry.resolve('BOMBSHELL')
# => 'hanna'
Customize registry errors
You can also customize what kind of errors are being raised in case a of a key was not registered or was already registered.
by providing blocks or a handler responding to :call for on_key_already_registered
and on_key_already_registered
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 tags, and push the .gem
file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/nxt_registry.
License
The gem is available as open source under the terms of the MIT License.