A library of utility services and concerns.
The Messages tool is used for defining and generating human-readable messages.
For a full list of available methods, see the Reference documentation.
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 (.).
String or a Symbol in underscore_format. If the scope has multiple parts, each part is separated by periods (.). In the above example, the scope is 'sleeping_king_studios.tools.assertions'.String or a Symbol in underscore_format. In the above example, the key is 'name'.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.
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.
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"
You can force an exception to be raised for missing parameters by passing the :reraise_exceptions flag:
tools = SleepingKingStudios::Tools::Toolbelt.instance
tools.messages.message('sleeping_king_studios.tools.assertions.instance_of', reraise_exceptions: true)
#=> raises a KeyError with message "key<expected> not found"
You can also provide a default value or Proc in case the requested message is not defined.
If you pass an object as the default: keyword, that default value will be returned if the requested message is not defined.
tools = SleepingKingStudios::Tools::Toolbelt.instance
tools.messages.message(
'example.messages.undefined_message',
default: 'message not found'
)
#=> "message not found"
If you pass a Proc as the default: keyword, the proc will be called with the scoped message key and any additional parameters passed to #message.
tools = SleepingKingStudios::Tools::Toolbelt.instance
tools.messages.message(
'example.messages.undefined_message',
default: lambda do |key, locale: 'en', **|
"message not found with key #{key} and locale #{locale}"
end,
locale: 'es'
)
#=> "message not found with key example.messages.undefined_message and locale es"
Registering messages for use in the #messages tool takes two steps.
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:
I18n.Hash.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).
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.
.json (for a JSON file), or either .yaml or .yml (for a YAML file). To support other file types, subclass FileStrategy.Hash, and each key must either be a message String or a nested Hash.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)
By default, a FileStrategy will flatten the parsed templates for efficient lookup - a nested Hash of { foo: { bar: { baz: 'template' } } } is stored as a flat Hash with contents { 'foo.bar.baz' => 'template' }. This makes lookup of scoped keys more efficient but prevents retrieving non-leaf nodes (such as "foo" or "foo.bar" in the above Hash). If you need to retrieve Hash values, initialize the strategy with flatten_templates: false.
file_name = 'config/messages.yml'
strategy =
SleepingKingStudios::Tools::Messages::Strategies::FileStrategy
.new(file_name, flatten_templates: false)
strategy.get('space.messages.errors')
#=> { 'failure' => 'not going to space' }
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:
String, that string will be formatted using Kernel#format and the given parameters.Proc, the proc will be called with the given parameters and any additional keywords passed to #message. This provides more fine-grained control over generated messages. In the above example, the 'space.messages.rockets.launch_status' message is defined as a Proc, which takes an optional ready: flag when generating the message.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)
By default, a HashStrategy will flatten the parsed templates for efficient lookup - a nested Hash of { foo: { bar: { baz: 'template' } } } is stored as a flat Hash with contents { 'foo.bar.baz' => 'template' }. This makes lookup of scoped keys more efficient but prevents retrieving non-leaf nodes (such as "foo" or "foo.bar" in the above Hash). If you need to retrieve Hash values, initialize the strategy with flatten_templates: false.
strategy =
SleepingKingStudios::Tools::Messages::Strategies::FileStrategy
.new(definitions, flatten_templates: false)
strategy.get('space.messages.errors')
#=> { 'failure' => 'not going to space' }
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.
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.
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?"
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.
In addition to retrieving messages from tools.messages, you can also generate messages directly from a registry. This allows defining multiple registries for different scopes or purposes, or using dependency injection to define different registries for development, test, and production environments.
# Access the registry instance.
registry = SleepingKingStudios::Tools::Messages::Registry.global
strategy = registry.get('example.message_key')
message = strategy&.call('example.message_key', default: nil)
First, we retrieve the strategy that defines messages for the requested key. If there is no matching strategy, registry.get will return nil.
Second, if we have a matching strategy, we then ask the strategy to generate the message using strategy.call. We also pass default: nil to ensure that message will be nil when either the strategy or the message template is not defined. (When called via tools.messages, this is automatically handled to generate an error message).
For some use cases, you may need to access the raw message templates rather than the formatted values. For example, you may have custom message formatting logic, or you may store messages in a custom format, such as a Hash with optional properties. The Strategy#get and Strategy#fetch methods allow you to do just that.
# Returns nil if the strategy does not define the message.
strategy.get('example.message_key')
# Returns 'Missing template "example.message_key"' if the strategy does not
# define the message.
strategy.fetch('example.message_key', 'Missing template "example.message_key"')
# Returns 'Missing template "example.message_key"' if the strategy does not
# define the message.
strategy.fetch('example.message_key') do |scoped_key, **options|
message = 'Missing template "example.message_key"'
message += " with options #{options.inspect}" unless options.empty?
message
end
Back to Documentation | Tools