A library of utility services and concerns.
The Messages tool is used for defining and generating human-readable 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 (.).
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"
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)
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)
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.
Back to Documentation | Tools