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))
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.
Add to your Gemfile:
gem "declarative_initialization"Then install:
bundle installIn non-Bundler contexts, require it directly:
require "declarative_initialization"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): extraDeclare 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).callPass a block to initialize_with to run code after assignments. The block runs in the instance context. You can use either the reader (e.g. foo) or the instance variable (@foo) to read values; to assign back to an attribute, assign to the instance variable (e.g. @foo = foo.to_s).
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
endIf 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>"The generated initializer is keyword-only. Passing positional arguments raises an ArgumentError.
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
endDefaults 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.
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.
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 hereUse the post-initialize block instead:
initialize_with :user, account: nil do
@account ||= user.account
endinitialize_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.
- Source: teamshares/declarative_initialization
- Code of conduct:
CODE_OF_CONDUCT.md