DeclarativeInitialization
Declare a class’s keyword inputs once and get a keyword-only initialize with assignments, readers, and helpful argument errors.
- Keyword-only initializer: rejects positional args and unknown keywords
- Assignments: sets
@keywordinstance variables from declared inputs - Readers: defines
attr_readerfor each input (and ablockreader) - Defaults: supports optional keywords with default values
- No dependencies: plain Ruby ((>= 3.0))
When to use it
Use this when you have small POROs that take keyword inputs and you’re tired of repeating the same initializer boilerplate:
- Service / command objects that take dependencies and parameters
- Value objects with a fixed set of attributes
- Configuration objects with a handful of optional flags
- Components / presenters that accept a stable set of inputs
If you need complex inheritance initialization, multiple initializer “shapes”, or highly dynamic defaults, a handwritten initialize may be clearer.
Installation
Add to your Gemfile:
gem "declarative_initialization"
Then install:
bundle install
In non-Bundler contexts, require it directly:
require "declarative_initialization"
Quick start
class UserGreeter
include DeclarativeInitialization
initialize_with :user
def call
"Hello, #{user.name}!"
end
end
UserGreeter.new(user: current_user).call
# => "Hello, Alice!"
UserGreeter.new
# ArgumentError: [UserGreeter] Missing keyword argument(s): user
UserGreeter.new(user: current_user, extra: true)
# ArgumentError: [UserGreeter] Unknown keyword argument(s): extra
Usage
Required vs optional keywords (defaults)
Declare required keywords as symbols, and optional keywords as keyword arguments:
class Search
include DeclarativeInitialization
initialize_with :query, limit: 10, order: :desc
def call
results = perform_search(query).take(limit)
order == :desc ? results.reverse : results
end
end
Search.new(query: "ruby").call
Search.new(query: "ruby", limit: 50).call
Post-initialize hook
Pass a block to initialize_with to run code after assignments. The block runs in the instance context.
class Rectangle
include DeclarativeInitialization
initialize_with :width, :height do
raise ArgumentError, "Dimensions must be positive" if width <= 0 || height <= 0
@area = width * height
end
attr_reader :area
end
Capturing a block passed to .new
If the caller passes a block to .new, it’s stored in @block and available via the block reader.
class Wrapper
include DeclarativeInitialization
initialize_with :tag
def render
"<#{tag}>#{block&.call}</#{tag}>"
end
end
Wrapper.new(tag: "div") { "Content" }.render
# => "<div>Content</div>"
Behavior notes / gotchas
Only keyword arguments are accepted
The generated initializer is keyword-only. Passing positional arguments raises an ArgumentError.
Readers are public by default
Inputs are exposed with attr_reader. If you prefer private readers, make them private after the declaration:
class Example
include DeclarativeInitialization
initialize_with :user, admin: false
private :user, :admin
end
Defaults are literal values
Defaults are applied when the caller omits that keyword. For common mutable defaults (Array, Hash, Set, String), the value is duplicated per instance (shallow). If you need deeper setup (or derived values), use the post-initialize block.
Method name conflicts
initialize_with defines readers for each declared input (and block). If a method with the same name already exists, it will be overridden.
In Rails development/test (or when your logger level allows it), the gem logs a warning when it overrides an existing method.
Referencing other inputs in defaults
You can’t reference one declared input from another input’s default at declaration time:
initialize_with :user, account: user.account # user is not available here
Use the post-initialize block instead:
initialize_with :user, account: nil do
@account ||= user.account
end
Inheritance and super
initialize_with generates an initialize method. If a subclass calls initialize_with, it replaces the parent initializer and does not call super. Prefer a single initializer per hierarchy, or avoid this gem for complex inheritance chains.
Contributing
- Source: teamshares/declarative_initialization
- Code of conduct:
CODE_OF_CONDUCT.md