Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/bkuhlmann/etcher

A monadic configuration loader, transformer, and validator.
https://github.com/bkuhlmann/etcher

cli configuration xdg

Last synced: about 2 months ago
JSON representation

A monadic configuration loader, transformer, and validator.

Awesome Lists containing this project

README

        

:toc: macro
:toclevels: 5
:figure-caption!:

:data_link: link:https://alchemists.io/articles/ruby_data[Data]
:demeter_link: link:https://en.wikipedia.org/wiki/Law_of_Demeter[Law of Demeter]
:dry_monads_link: link:https://dry-rb.org/gems/dry-monads[Dry Monads]
:dry_schema_link: link:https://dry-rb.org/gems/dry-schema[Dry Schema]
:dry_types_link: link:https://dry-rb.org/gems/dry-types[Dry Types]
:dry_validation_link: link:https://dry-rb.org/gems/dry-validation[Dry Validation]
:environment_link: link:https://rubyapi.org/3.2/o/env[Environment]
:gitt_link: link:https://alchemists.io/projects/gitt[Gitt]
:hash_link: link:https://rubyapi.org/o/hash[Hash]
:json_link: link:https://rubyapi.org/o/json[JSON]
:pipeable_link: link:https://alchemists.io/projects/pipeable[Pipeable]
:runcom_link: link:https://alchemists.io/projects/runcom[Runcom]
:sod_link: link:https://alchemists.io/projects/sod[Sod]
:string_formats_link: link:https://docs.ruby-lang.org/en/3.3/format_specifications_rdoc.html[String Formats]
:struct_link: link:https://alchemists.io/articles/ruby_structs[Struct]
:versionaire_link: link:https://alchemists.io/projects/versionaire[Versionaire]
:xdg_link: link:https://alchemists.io/projects/xdg[XDG]
:yaml_link: link:https://rubyapi.org/o/yaml[YAML]

= Etcher

Etcher allows you to take raw settings and/or user input and _etch_ them into a concrete and valid configuration for use within your application. As quoted from link:https://en.wikipedia.org/wiki/Etching[Wikipedia], to _etch_ is to:

____
[Use] strong acid or mordant to cut into the unprotected parts of a metal surface to create a design in intaglio (incised) in the metal.
____

By using Etcher, you have a reliable way to load default configurations (i.e. {hash_link}, {environment_link}, {json_link}, {yaml_link}) which can be validated and etched into _frozen_ records (i.e. {hash_link}, {data_link}, {struct_link}) for consumption within your application which doesn't violate the {demeter_link}. This comes complete with loaders, transformations, and validations all via a simple Object API that pairs well with the {xdg_link}, {runcom_link}, and {sod_link} gems.

toc::[]

== Features

* Supports contracts which respond to `#call` to validate a {hash_link} before building the final record. Pairs well with the {dry_schema_link} and {dry_validation_link} gems.
* Supports models which respond to `.[]` for consuming a splatted {hash_link} to instantiate new records. Pairs well with primitives such as: {hash_link}, {data_link}, and {struct_link}.
* Supports loading of default configurations from a {hash_link}, the {environment_link}, a {json_link} configuration, a {yaml_link} configuration, or anything that can answer a hash.
* Supports multiple transformations which can process loaded configuration hashes and answer a transformed hash.
* Supports {hash_link} overrides as a final customization which is handy for Command Line Interfaces (CLIs), as aided by {sod_link}, or anything that might require user input at runtime.

== Requirements

. link:https://www.ruby-lang.org[Ruby].

== Setup

To install _with_ security, run:

[source,bash]
----
# 💡 Skip this line if you already have the public certificate installed.
gem cert --add <(curl --compressed --location https://alchemists.io/gems.pem)
gem install etcher --trust-policy HighSecurity
----

To install _without_ security, run:

[source,bash]
----
gem install etcher
----

You can also add the gem directly to your project:

[source,bash]
----
bundle add etcher
----

Once the gem is installed, you only need to require it:

[source,ruby]
----
require "etcher"
----

== Usage

Basic usage is to new up an instance:

[source,ruby]
----
etcher = Etcher.new
etcher.call({one: 1, two: 2})

# Success({:one=>1, :two=>2})
----

Notice you get a monad -- either a `Success` or `Failure` -- as provided by the {dry_monads_link} gem. This allows you to create more sophisticated pipelines as found with the {pipeable_link} gem or any kind of failsafe workflow you might need.

By default, any attributes you message the instance with will only pass through what you gave it and always answer a `Success`. This is nice for initial experimentation but true power comes with full customization of the instance. Here's an advanced configuration showing all features:

[source,ruby]
----
require "dry/monads"
require "dry/schema"

Dry::Schema.load_extensions :monads

contract = Dry::Schema.Params do
required(:user).filled :string
required(:home).filled :string
end

model = Data.define :user, :home

transformer = lambda do |attributes, key = :user|
Dry::Monads::Success attributes.merge! key => attributes[key].upcase
end

Etcher::Registry.new(contract:, model:, transformers: [transformer])
.add_loader(:environment, %w[USER HOME])
.then { |registry| Etcher.new(registry).call }

# Success(#)
----

The above can be broken down into a series of steps:

. A {dry_schema_link} contract -- loaded with {dry_monads_link} extensions -- is created to verify untrusted attributes.
. A model is created with attributes: `user` and `home`.
. A registry is constructed with a custom contract, model, loader, and transformer.
. Finally, we see a _successfully_ built configuration for further use within your application.

While this is a more advanced use case, you'll usually only need to register a contract and model. The loaders and transformers provide additional firepower in situations where you need to do more with your data. We'll look at each of these components in greater detail next.

ℹī¸ All keys are converted to symbols before being processed. This is done to ensure consistency and improve debugablity when dealing with raw input that might be a mix of strings and/or symbols.

=== Steps

As hinted at above, the complete sequence of steps are performed in the order listed:

. *Load*: Each loader, if any, is called and merged with the previous loader to build initial attributes.
. *Transform*: Each transformer, if any, is called to transform and manipulate the attributes.
. *Override*: Overrides, if any, are merged with the result of the last transformer so you can fine tune the data as desired.
. *Validate*: The contract is called to validate the attributes as previously loaded, overwritten, and transformed.
. *Model*: The model consumes the attributes of the validated contract and creates a new record for you to use as needed.

Each step _mutates_ the attributes of the previous step in order to produce a record (success) or error (failure). You can use the above steps as a reference when using this gem. Each step is explained in greater below.

=== Registry

The registry provides a way to register any/all behavior for before creating a new Etcher instance. Here's what you get by default:

[source,ruby]
----
Etcher::Registry.new
# #, model=Hash, loaders=[], transformers=[]>
----

Since the registry is {data_link}, you can initialize with everything you need:

[source,ruby]
----
Etcher::Registry[
contract: MyContract,
model: MyModel,
loaders: [MyLoader.new],
transformers: [MyTransformer]
]
----

You can also add additional loaders and/or transformers after the fact:

[source,ruby]
----
registry = Etcher::Registry.new
.add_loader(MyLoader.new)
.add_transformer(MyTransformer)
----

💡 Order matters so ensure you list your loaders and transformers in the order you want them processed.

=== Contracts

Contracts are a critical piece of this workflow as they provide a way to validate incoming data, remove unwanted data, and create a sanitized record for use in your application. Any contract that has the following behavior will work:

* `#call`: Must be able to consume a {hash_link} and answer an object which can respond to `#to_monad`.

Both {dry_schema_link} and {dry_validation_link} respond to the `#to_monad` message. Ensure the {dry_monads_link} extensions are loaded too, as briefly shown earlier, so the result will respond to the `#to_monad` message. Here's how to enable monad support if using both gems:

[source,ruby]
----
Dry::Schema.load_extensions :monads
Dry::Validation.load_extensions :monads
----

Using {dry_schema_link} syntax, we could create a contract for verifying email addresses and use it to build a new Etcher instance. Example:

[source,ruby]
----
require "dry/schema"

Dry::Schema.load_extensions :monads

contract = Dry::Schema.Params do
required(:from).filled :string
required(:to).filled :string
end

etcher = Etcher::Registry[contract:].then { |registry| Etcher.new registry }
etcher.call

# Failure({:step=>:validate, :payload=>{:from=>["is missing"], :to=>["is missing"]}})

etcher.call from: "Mork", to: "Mindy"
# Success({:from=>"Mork", :to=>"Mindy"})
----

Here you can see the power of using a contract to validate your data both as a failure and a success. Unfortunately, with the success, we only get a {hash_link} as a record but it would be better to have a data structure which will be explained shortly.

=== Types

To support contracts further, there are a couple custom types which might be of interest. Each custom type, as described below, is made possible via {dry_types_link}.

==== Pathnames

[source,ruby]
----
Etcher::Types::Pathname
----

The above allows you to use pathname types in your contracts to validate and cast as pathnames:

[source,ruby]
----
contract = Dry::Schema.Params do
required(:path).filled Etcher::Types::Pathname
end

contract.call(path: "a/path").to_monad
# Success(##} errors={} path=[]>)
----

==== Versions

[source,ruby]
----
Etcher::Types::Version
----

The above allows you to validate and cast versions within your contracts -- via the {versionaire_link} gem -- as follows:

[source,ruby]
----
contract = Dry::Schema.Params do
required(:version).filled Etcher::Types::Version
end

contract.call(version: "1.2.3").to_monad
# Success(#"1.2.3"} errors={} path=[]>)
----

=== Models

A model is any object which responds to `.[]` and can accept a splatted hash. Example: `Model[**attributes]`. These primitives are excellent choices: {hash_link}, {data_link}, and {struct_link}.

ℹī¸ Keep in mind that using a `Hash` is the default model and will only result in a pass through situation. You'll want to reach for the more robust `Data` or `Struct` objects instead.

The model is used in the last step of the _etching_ process to create a _frozen_ record for further use by your application. Here's an example where a {data_link} model is used:

[source,ruby]
----
model = Data.define :from, :to
etcher = Etcher::Registry[model:].then { |registry| Etcher.new registry }

etcher.call
# Failure({:step=>:model, :payload=>"Missing keywords: :from, :to."})

etcher.call from: "Mork", to: "Mindy"
# Success(#)
----

Notice we get an failure if all attributes are not provided but if we supply the required attributes we get a success.

ℹī¸ Keep in mind the default contract is always a pass through so no validation is being done when only using a {hash_link}. Generally you want to supply both a custom contract and model at a minimum.

=== Loaders

Loaders are a great way to load a _default_ configuration for your application which can be in multiple formats. Loaders can either be defined when creating a new registry instance or added after the fact. Here are a couple examples:

[source,ruby]
----
# Initializer
registry = Etcher::Registry[loaders: [MyLoader.new]]

# Method
registry = Etcher::Registry.new.add_loader MyLoader.new
----

You can also remove a previously added loader by index:

[source,ruby]
----
registry = Etcher::Registry.new

# Application
registry.add_loader MyLoader.new

# RSpec
registry.remove_loader 0
----

The ability to remove a loader is especially handy in a testing environment where you might need to temporarily remove a loader or don't need a specific loader for testing purposes.

There are a few guidelines to using loaders:

* All loaders must respond to `#call` with no arguments.
* All loaders must answer either a success with attributes (i.e. `Success attributes`) or a failure with details about the failure (i.e. `Failure step: :load, constant: MyLoader, payload: "My error message.`)
* All keys are symbolized after the loader is called which helps streamline merging and overriding values from the same keys across multiple configurations.
* All nested keys will be flattened after being loaded. This means a key structure of `{demo: {one: "test"}}` will be flattened to `demo_one: "test"` which adheres to the {demeter_link} when a new recored is _etched_ for you.
* The order in which you define your loaders matters. This means the first loader defined will be processed first, then the second, and so forth. Loaders defined last take precedence over previously defined loaders when overriding the same keys.

For convenience, all loaders -- only packaged with this gem -- can be registered by symbol instead of constant/instance. Example:

[source,ruby]
----
registry = Etcher::Registry.new

# Environment
registry.add_loader :environment

# JSON
registry.add_loader :json, "path/to/configuration.json"

# YAML
registry.add_loader :yaml, "path/to/configuration.yml"
----

Any positional or keyword arguments will be passed to the loader's constructor. _This only works when using `Registry#add_loader`, though._

The next sections will help you learn about the supported loaders and how to build your own custom loader.

==== Environment

Use `:environment` or `Etcher::Loaders::Environment` to load configuration information from your {environment_link}. By default, this object wraps `ENV`, uses an empty array for included keys, and answers a filtered hash where all keys are downcased. _If you don't specify keys to include, then an empty hash is answered back_. Here's a few examples:

[source,ruby]
----
# Default behavior.
loader = Etcher::Loaders::Environment.new
loader.call
# Success({})

# With specific includes.
loader = Etcher::Loaders::Environment.new %w[RACK_ENV DATABASE_URL]
loader.call
# Success({"rack_env" => "test", "database_url" => "postgres://localhost/demo_test"})

# With a custom environment and specific include.
loader = Etcher::Loaders::Environment.new "USER", source: {"USER" => "Jack"}
loader.call
# Success({"user"=>"Jack"})
----

This loader is great for pulling from environment variables as a fallback configuration for your application.

==== Hash

Use `:hash` or `Etcher::Loaders::Hash` to load in-memory attributes. By default, this loader will answer an empty hash if not supplied with any attributes. Here's a few examples:

[source,ruby]
----
# Default behavior.
loader = Etcher::Loaders::Hash.new
loader.call
# Success({})

# With custom attributes
loader = Etcher::Loaders::Hash.new one: 1, two: 2
loader.call
# Success({:one=>1, :two=>2})
----

This loader is great for adding custom attributes, overriding/adjusting attributes from a previous loader, or customizing attributes for testing purposes within a test suite.

==== JSON

Use `Etcher::Loaders::JSON` to load configuration information from a {json_link} file. Here's how to use this loader (using a file that doesn't exist):

[source,ruby]
----
# Default behavior (a custom path is required).
loader = Etcher::Loaders::JSON.new "your/path/to/configuration.json"
loader.call # Success({})
----

You can also customize the fallback and logger used. Here are the defaults:

[source,ruby]
----
loader = Etcher::Loaders::JSON.new "your/path/to/configuration.json",
fallback: {},
logger: Logger.new(STDOUT)
loader.call # Success({})
----

If the file exists with _valid_ content, you'll get a `Hash` wrapped as a `Success`. In situations in which the file doesn't exist, you'll get a `Success` with an empty hash and debug information logged instead. Any failures will be provided with step, constant, and payload details. Example:

[source,ruby]
----
Failure step: :load, constant: Etcher::Loaders::JSON, payload: "Danger!"
----

==== YAML

Use `Etcher::Loaders::YAML` to load configuration information from a {yaml_link} file. Here's how to use this loader (using a file that doesn't exist):

[source,ruby]
----
# Default behavior (a custom path is required).
loader = Etcher::Loaders::YAML.new "your/path/to/configuration.yml"
loader.call # Success({})
----

You can also customize the fallback and logger used. Here are the defaults:

[source,ruby]
----
loader = Etcher::Loaders::YAML.new "your/path/to/configuration.yml",
fallback: {},
logger: Logger.new(STDOUT)
loader.call # Success({})
----

If the file exists with _valid_ content, you'll get a `Hash` wrapped as a `Success`. In situations in which the file doesn't exist, you'll get a `Success` with an empty hash and debug information logged instead. Any failures will be provided with step, constant, and payload details. Example:

[source,ruby]
----
Failure step: :load, constant: Etcher::Loaders::YAML, payload: "Danger!"
----

==== Custom

You can always create your own loader if you don't need or want any of the default loaders provided for you. The only requirement is your loader _must_ respond to `#call` and answer a monad with a `Hash` for content which means you can use a class, method, lambda, or proc. Here's an example of creating a custom loader, registering, and using it:

[source,ruby]
----
require "dry/monads"

class Demo
include Dry::Monads[:result]

def initialize processor: Processor.new
@processor = processor
end

def call
Success processor.call
rescue ProcessorError => error
Failure step: :load, constant: self.class, payload: error.message
end

private

attr_reader :processor
end

registry = Etcher::Registry[loaders: [Demo.new]]

Etcher.new(registry).call
----

While the above assumes you have some kind of `Processor` for loading attributes, you can see there is little effort required to implement and customize as desired.

=== Transformers

Transformers are great for _mutating_ specific keys and values. They give you fine grained customization over your configuration. Transformers can either be defined when creating a new registry instance or added after the fact. Here are a couple examples:

[source,ruby]
----
# Initializer
registry = Etcher::Registry[transformers: [MyTransformer]]

# Method
registry = Etcher::Registry.new.add_transformer MyTransformer
----

You can also remove a previously added transformer by index:

[source,ruby]
----
registry = Etcher::Registry.new

# Application
registry.add_transformer MyTransformer

# RSpec
registry.remove_transformer 0
----

The ability to remove a transformer is especially handy in a testing environment where you might need to temporarily remove a transformer or don't need a specific transformer for testing purposes.

The guidelines for using transformers are:

* They can be initialized with whatever requirements you need.
* They must respond to `#call` which takes a required `attributes` positional argument and answers a modified version of these attributes (`Hash`) wrapped as a monad.
* They must answer either a success with attributes (i.e. `Success attributes`) or a failure with details about the failure (i.e. `Failure step: :transform, constant: MyTransformer, payload: "My error message.`)
* When using a proc/lambda, the first, _required_, parameter should be the `attributes` parameter followed by a second positional `key` parameter.
* When using a class, the `key` should be your first positional parameter. Additional parameters can be supplied after if desired.
* The `attributes` passed to your transformer will have symbolized keys so you don't need to transform them further.

For example, the following capitalizes all values (which may or may not be good depending on your data structure):

[source,ruby]
----
require "dry/monads"

Capitalize = -> attributes { Dry::Monads::Success attributes.transform_values!(&:capitalize) }
Capitalize.call(name: "test")

# Success({:name=>"Test"})
----

The following obtains the current Git user's email address from the global Git configuration using the {gitt_link} gem:

[source,ruby]
----
require "dry/monads"
require "gitt"

class GitEmail
def initialize key = :author_email, git: Gitt::Repository.new
@key = key
@git = git
end

def call(attributes) = git.get("user.email").fmap { |value| attributes[key] = value }

private

attr_reader :key, :git
end

GitEmail.new.call({})

# Success("[email protected]")
----

To use all of the above, you'd only need to register and use them:

[source,ruby]
----
registry = Etcher::Registry[transformers: [Capitalize, GitEmail.new]]
etcher = Etcher.new(registry)
etcher.call
----

For convenience, all transformers -- only packaged with this gem -- can be registered by symbol instead of constant/instance. Example:

[source,ruby]
----
registry = Etcher::Registry.new

# Format
registry.add_transformer :format, :project_uri

# Time
registry.add_transformer :time
----

Any positional or keyword arguments will be passed to the transformers's constructor. _This only works when using `Registry#add_transformer`, though._ The following sections provide more details on each.

==== Basename

Use `Etcher::Transformers::Basename` to dynamically obtain the name of the current directory as a value for a key. This is handy for scripting or CLI purposes when needing to know the name of the current project you are working in. Example:

[source,ruby]
----
transformer = Etcher::Transformers::Basename.new :demo
transformer.call({})
# Success({:demo=>"scratch"})

transformer = Etcher::Transformers::Basename.new :demo, fallback: "undefined"
transformer.call({})
# Success({:demo=>"undefined"})

transformer = Etcher::Transformers::Basename.new :demo
transformer.call({demo: "defined"})
# Success({:demo=>"defined"})
----

==== Root

Use `Etcher::Transformers::Root` to dynamically obtain the current path as a value for a key. This is handy for obtaining the absolute path to a new or existing directory. Example:

[source,ruby]
----
transformer = Etcher::Transformers::Root.new :demo
transformer.call({})
# Success({:demo=>#})

transformer = Etcher::Transformers::Root.new :demo, fallback: "undefined"
transformer.call({})
# Success({:demo=>#})

transformer = Etcher::Transformers::Root.new :demo
transformer.call({demo: "defined"})
# Success({:demo=>#})
----

==== Format

Use `Etcher::Transformers::Format` to transform any key's value by using the configuration's existing attributes to format the value of a specific key using the {string_formats_link} Specification. To start, we'll use the same attributes for all examples:

[source,ruby]
----
attributes = {
organization_uri: "https://acme.io",
project_name: "test",
project_uri: "%s/projects/%s"
}
----

Using the above `attributes`, you'll get a `Success` when all required keys exist:

[source,ruby]
----
Etcher::Transformers::Format.new(:project_uri).call attributes
# Success(
{
organization_uri: "https://acme.io",
project_name: "test",
project_uri: "https://acme.io/projects/test"
}
)
----

When some required keys are missing, you'll get a `Failure`:

[source,ruby]
----
attributes.delete :project_name
Etcher::Transformers::Format.new(:project_uri).call attributes

# Failure(
# {
# step: :transform,
# constant: Etcher::Transformers::Format,
# payload: "Unable to transform :project_uri, missing specifier: \"\"."
# }
# )
----

You can partially transform a value using _retainers_ and/or _mappings_ for situations where you need to format a value while preserving and/or remapping string specifiers for delayed formatting. Here's an example using a _retainer_ which preserves the `:project_name`.

[source,ruby]
----
Etcher::Transformers::Format.new(:project_uri, :project_name).call attributes

# Success(
# {
# organization_uri: "https://acme.io",
# project_name: "test",
# project_uri: "https://acme.io/projects/%s"
# }
# )
----

Notice the `organization_uri` was formatted in the `project_uri` while the `project_name` was preserved. This allows you to format the `project_name` when you can supply the value later. Similarly, you can remap a string specifier. Example:

[source,ruby]
----
Etcher::Transformers::Format.new(:project_uri, project_name: "%s").call attributes

# Success(
# {
# organization_uri: "https://acme.io",
# project_name: "test",
# project_uri: "https://acme.io/projects/%s"
# }
# )
----

Notice the `organization_uri` was formatted in the `project_uri` (same as before) while the `project_name` was remapped as `%s`. As shown mentioned earlier, this allows you to _delay_ supplying the `id` when you might not have a value for it yet.

You can also, safely, transform a value which _doesn't_ have string specifiers:

[source,ruby]
----
Etcher::Transformers::Format.new(:version).call(version: "1.2.3")
# Success({:version=>"1.2.3"})
----

Normally, you'd get a "too many arguments for format string" warning but this transformer detects and immediately skips formatting when no string specifiers are detected. This is handy for situations where your configuration supports values which may or may not need formatting.

==== Time

Use `Etcher::Transformers::Time` to transform the any key in your configuration when you want to know the current time at which the configuration was loaded. Handy for situations where you need to calculate relative time or format time based on when your configuration was loaded.

You must supply a key and `Time.now.utc` is the default fallback. You can customize as desired. Example:

[source,ruby]
----
transformer = Etcher::Transformers::Time.new :now
transformer.call({})
# Success({:now=>2024-06-15 22:43:29.178488 UTC})

transformer = Etcher::Transformers::Time.new :now, fallback: Time.utc(2000, 1, 1)
transformer.call({})
# Success({:now=>2000-01-01 00:00:00 UTC})

transformer = Etcher::Transformers::Time.new :now
transformer.call({now: Time.utc(2000, 1, 1)})
# Success({:now=>2000-01-01 00:00:00 UTC})
----

=== Overrides

Overrides are what you pass to the Etcher instance when called. They allow you to override any values that were loaded and/or transformed. Example:

[source,ruby]
----
etcher = Etcher.new

# With symbol keys.
etcher.call name: "test", label: "Test"
# Success({:name=>"test", :label=>"Test"})

# With string keys.
etcher.call "name" => "test", "label" => "Test"
# Success({:name=>"test", :label=>"Test"})
----

Overrides are applied _after_ any transforms and _before_ validations. They are a nice way to deal with user input during runtime or provide additional attributes not supplied by the loading and/or transforming of your default configuration while ensuring they are validated properly. Any string keys will be transformed to symbol keys to ensure consistency and reduce issues when merged.

=== Resolver

In situations where you'd like Etcher to handle the complete load, transform, override, validate, and model steps for you, then you can use the resolver. This is provided for use cases where you'd like Etcher to handle everything for you and abort if otherwise. Example:

[source,ruby]
----
Etcher.call name: "demo"
# {:name=>"demo"}
----

When called -- and there are no issues -- you'll get the fully formed record as a result (in this case a Hash which is the default model). You'll never a get a monad when using `Etcher.call` because this is meant to resolve the monadic pipeline for you. If any failure is encountered, then Etcher will _abort_ with a fatal log message. Here's a variation of earlier examples which demonstrates fatals:

[source,ruby]
----
require "dry/monads"
require "dry/schema"

Dry::Schema.load_extensions :monads

contract = Dry::Schema.Params do
required(:to).filled :string
required(:from).filled :string
end

model = Data.define :to, :from
registry = Etcher::Registry.new(contract:, model:)

Etcher.call registry

# 🛑 Etcher validate failure (Etcher::Builder). Unable to load configuration:
# - to is missing
# - from is missing

Etcher.call registry, to: "Mindy"

# 🛑 Etcher validate failure (Etcher::Builder). Unable to load configuration:
# - from is missing

registry = Etcher::Registry.new(model: Data.define(:name, :label))
Etcher.call registry, to: "Mindy"

# 🛑 Etcher model failure (Etcher::Builder). Missing keywords: :name, :label.
----

💡 When using a custom registry, make sure it's the first argument. Additional arguments can be supplied afterwards and they can be any number of key/value overrides which is similar to how `Etcher.new` works.

== Development

To contribute, run:

[source,bash]
----
git clone https://github.com/bkuhlmann/etcher
cd etcher
bin/setup
----

You can also use the IRB console for direct access to all objects:

[source,bash]
----
bin/console
----

=== Architecture

The following illustrates the full sequences of events when _etching_ new records:

image::https://alchemists.io/images/projects/etcher/architecture.png[Architecture Diagram,1250,1071,role=focal_point]

== Tests

To test, run:

[source,bash]
----
bin/rake
----

== link:https://alchemists.io/policies/license[License]

== link:https://alchemists.io/policies/security[Security]

== link:https://alchemists.io/policies/code_of_conduct[Code of Conduct]

== link:https://alchemists.io/policies/contributions[Contributions]

== link:https://alchemists.io/projects/etcher/versions[Versions]

== link:https://alchemists.io/community[Community]

== Credits

* Built with link:https://alchemists.io/projects/gemsmith[Gemsmith].
* Engineered by link:https://alchemists.io/team/brooke_kuhlmann[Brooke Kuhlmann].