Tools

A library of utility services and concerns.

Messages

The Messages tool is used for defining and generating human-readable messages.

Contents

Generating Messages

To generate a human-readable message, use the #message method:

tools = SleepingKingStudios::Tools::Toolbelt.instance

tools.messages.message('sleeping_king_studios.tools.assertions.name')
#=> "is not a String or a Symbol"

Each defined message has a scope and a key, separated by a period (.).

Generally speaking, a message’s scope should match the structure of the relevant code. The scope should always start with the name of the gem, application, or project.

Alternatively, you can pass the scope separately when calling #message:

tools = SleepingKingStudios::Tools::Toolbelt.instance
scope = 'sleeping_king_studios.tools.assertions'
key   = :name

tools.messages.message(key, scope:)
#=> "is not a String or a Symbol"

This can be useful when generating multiple human-readable messages with the same base scope.

Missing Messages

If a requested message is not defined, #message returns an error string with the scoped key:

tools = SleepingKingStudios::Tools::Toolbelt.instance

tools.messages.message('sleeping_king_studios.tools.assertions.undefined_message')
#=> "Message missing: sleeping_king_studios.tools.assertions.undefined_message"

To define your own message definitions, see Registering Messages, below.

Parameterized Messages

Messages can also have named parameters.

tools = SleepingKingStudios::Tools::Toolbelt.instance

tools.messages.message(
  'sleeping_king_studios.tools.assertions.instance_of',
  parameters: { expected: 'String' }
)
#=> "is not an instance of String"

If a message requires parameters but a parameter is missing, #message returns an error string:

tools = SleepingKingStudios::Tools::Toolbelt.instance

tools.messages.message('sleeping_king_studios.tools.assertions.instance_of')
#=> "Message missing parameters: sleeping_king_studios.tools.assertions.instance_of key<expected> not found"

Registering Messages

Registering messages for use in the #messages tool takes two steps.

Message Strategies

Each message strategy must define a #call method that takes a key argument and optional parameters: and scope: keywords and returns the matching generated message. The strategy must ignore unexpected keyword parameters.

SleepingKingStudios::Tools provides two pre-defined strategy implementations:

You can also define a custom strategy class. This allows you to retrieve messages from an external tool, or specify additional filters for matching the message definition (such as a locale: keyword).

File Strategies

A FileStrategy loads message definitions from a file.

# In config/messages.yml
---
space:
  messages:
    errors:
      failure: 'not going to space'
    rockets:
      launch_status: 'rocket %<name>s is ready to launch'
  module_name: 'Console Space Program'
file_name = 'config/messages.yml'
strategy  =
  SleepingKingStudios::Tools::Messages::Strategies::FileStrategy
  .new(file_name)

strategy.call('space.module_name')
#=> "Console Space Program"
strategy.call(:launch_status, parameters: { name: 'Imp VI' }, scope: 'space.messages.rockets')
#=> "rocket Imp VI is ready to launch"

The FileStrategy is initialized with the path to a definitions file.

Nested scopes will be automatically flattened when the file is read. For example, the above file defines the scoped key 'space.messages.errors.failure' to be 'not going to space'.

Each message defined in a FileStrategy must be a String. Parameterized messages are supported using Kernel#format. For example, the 'space.messages.rockets.launch_status definition takes a name parameter.

You can also register a file strategy using the following shorthand:

registry.register(scope: 'space', file: file_name)

Hash Strategies

A HashStrategy takes message definitions as a pre-defined Hash.

launch_status = lambda do |parameters: {}, ready: false, **|
  str = +'rocket'
  str << ' ' << parameters[:name] if parameters.key?(:name)
  str << (ready ? ' is' : ' is not')
  str << ' ready to launch'
  str.freeze
end
definitions = {
  'space' => {
    'messages'    => {
      'errors'  => {
        'failure' => 'not going to space'
      },
      'rockets' => {
        'launch_status' => launch_status
      }
    }
    'module_name' => 'Console Space Program'
  }
}
strategy =
  SleepingKingStudios::Tools::Messages::Strategies::HashStrategy
  .new(definitions)

strategy.call('space.module_name')
#=> "Console Space Program"

strategy.call(:launch_status, ready: true, scope: 'space.messages.rockets')
#=> "rocket is ready to launch"

The HashStrategy is initialized with a Hash of message definitions. Hash strategies support two types of message definition:

Nested scopes will be automatically flattened when the file is read. For example, the above definitions define the scoped key 'space.messages.errors.failure' to be 'not going to space'.

You can also register a hash strategy using the following shorthand:

registry.register(scope: 'space', hash: definitions)

Message Registries

Once your messages have been defined using a Messages strategy, the strategy must be added to a registry. By default, the Messages tool uses a shared global registry.

registry = SleepingKingStudios::Tools::Messages::Registry.global
strategy = SleepingKingStudios::Tools::Messages::Strategies::HashStrategy.new

registry.register(scope: 'space', strategy:)

registry.get('space')
#=> strategy

When #message is called with a scope: or a scoped key starting with 'space.', the registry will resolve the message definition to the registed strategy with matching scope. That strategy is then called with the key, scope, and parameters to generate the message.

Registed strategies can be nested, in which case the registry will use the most specific strategy matching the message scope. For example, one strategy might be registered with scope 'space', and another strategy with scope 'space.planets'. A message with scope 'space.stars' would be resolved using the 'space' strategy, but a message with scope 'space.planets.names' would be resolved using the 'space.planets' strategy.

Global Registry

The global registry provides a single entry point for defining and accessing message definitions across an application and its dependencies. Using the global registry has several advantages:

The default Toolbelt.instance is already configured to use the global registry.

Custom Registries

A custom registry can be defined by instantiating a new copy of Messages::Registry or a subclass thereof. The registry can then be used by passing it a custom toolbelt.

definitions = {
  'spec.test_cases.going_to_space' = 'Is the rocket going to space?'
}

registry = SleepingKingStudios::Tools::Messages::Registry.new
registry.register(scope: 'spec', hash: definitions)

toolbelt = SleepingKingStudios::Tools::Toolbelt.new(messages_registry: registry)
toolbelt.messages.message(:going_to_space, scope: 'spec.test_cases')
#=> "Is the rocket going to space?"

Initializing Messages

The recommended way to register messages for your application or project is using a project initializer.

module Space
  @initializer = SleepingKingStudios::Tools::Toolbox::Initializer.new do
    # Load human-readable message definitions for the project namespace.
    SleepingKingStudios::Tools.initializer.call
    SleepingKingStudios::Tools::Messages::Registry
      .global
      .register(file: 'config/messages.yml' scope: 'space')
  end

  def self.initializer = @initializer
end

Once the initializer is defined, update the entry point for your project to call the initializer.


Back to Documentation | Tools