Summary
Message.from_hash in lib/roast/cogs/agent/providers/claude/message.rb uses an 8-branch case statement to map a :type symbol to a Messages::*Message class. Since every branch follows the exact same naming convention (:"#{type}_message".to_s.camelize), we can replace the static dispatch with dynamic constant resolution.
Current code (lines 23–44)
def from_hash(hash)
type = hash.delete(:type)&.to_sym
case type
when :assistant
Messages::AssistantMessage.new(type:, hash:)
when :result
Messages::ResultMessage.new(type:, hash:)
when :system
Messages::SystemMessage.new(type:, hash:)
when :text
Messages::TextMessage.new(type:, hash:)
when :thinking
Messages::ThinkingMessage.new(type:, hash:)
when :tool_result
Messages::ToolResultMessage.new(type:, hash:)
when :tool_use
Messages::ToolUseMessage.new(type:, hash:)
when :user
Messages::UserMessage.new(type:, hash:)
else
Messages::UnknownMessage.new(type:, hash:)
end
end
Proposed replacement
def from_hash(hash)
type = hash.delete(:type)&.to_sym
class_name = :"#{type}_message".to_s.camelize # e.g. :tool_use → "ToolUseMessage"
klass = if Messages.const_defined?(class_name, false)
Messages.const_get(class_name, false)
else
Messages::UnknownMessage
end
klass.new(type:, hash:)
end
The false argument to const_defined?/const_get restricts the lookup to Messages itself (no ancestor walk), which is the safest approach.
Why
- Open/Closed Principle: Adding a new message type (e.g.
:image) only requires creating Messages::ImageMessage — no need to touch the factory method.
- Precedent: This mirrors how
Workflow#load_cog_or_provider already uses camelize/constantize for dynamic class resolution (see lib/roast/workflow.rb:116-119).
- Less surface area: Eliminates 16 lines of mechanical case branches that are easy to forget when adding new types.
Edge cases to verify
type is nil → "_message".camelize → "Message" — Messages.const_defined?("Message", false) returns false, so we correctly fall through to UnknownMessage.
- Unknown type like
:foo → "FooMessage" — not defined in Messages, falls through to UnknownMessage. ✓
- Existing types all resolve correctly (confirmed: the naming convention holds for all 8 current types). ✓
Acceptance criteria
Summary
Message.from_hashinlib/roast/cogs/agent/providers/claude/message.rbuses an 8-branchcasestatement to map a:typesymbol to aMessages::*Messageclass. Since every branch follows the exact same naming convention (:"#{type}_message".to_s.camelize), we can replace the static dispatch with dynamic constant resolution.Current code (lines 23–44)
Proposed replacement
The
falseargument toconst_defined?/const_getrestricts the lookup toMessagesitself (no ancestor walk), which is the safest approach.Why
:image) only requires creatingMessages::ImageMessage— no need to touch the factory method.Workflow#load_cog_or_provideralready usescamelize/constantizefor dynamic class resolution (seelib/roast/workflow.rb:116-119).Edge cases to verify
typeisnil→"_message".camelize→"Message"—Messages.const_defined?("Message", false)returnsfalse, so we correctly fall through toUnknownMessage.:foo→"FooMessage"— not defined inMessages, falls through toUnknownMessage. ✓Acceptance criteria
test/roast/cogs/agent/providers/claude/messages/continue to passUnknownMessage