Replace case statement in Claude Message.from_hash with dynamic class resolution
Open
@juniper-shopify is already working on this.
Since Jun 2, 2026.
enhancement
- Dominant language
- Ruby
- Stars
- 1.2k
- Forks
- 76
- PR merge metrics
- No merged PRs in 30d
Description
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 creatingMessages::ImageMessage— no need to touch the factory method. - Precedent: This mirrors how
Workflow#load_cog_or_provideralready usescamelize/constantizefor dynamic class resolution (seelib/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
typeisnil→"_message".camelize→"Message"—Messages.const_defined?("Message", false)returnsfalse, so we correctly fall through toUnknownMessage.- Unknown type like
:foo→"FooMessage"— not defined inMessages, falls through toUnknownMessage. ✓ - Existing types all resolve correctly (confirmed: the naming convention holds for all 8 current types). ✓
Acceptance criteria
- Replace the case statement with dynamic resolution
- All existing tests in
test/roast/cogs/agent/providers/claude/messages/continue to pass - Add a test confirming that an unknown type falls back to
UnknownMessage
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.