Name source kinds by class and look them up with const_get
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LG4iASZnHb1yEM1JAASiWn
diff --git a/AGENTS.md b/AGENTS.md
index 8236972..411ded5 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -24,7 +24,7 @@ lib/rank_king/ — app code
config.rb — Config data class, reads from ENV
db.rb — Sequel connection + migration runner
models.rb — Sequel models (User, List, Item, Axis, Result)
- sources.rb — hardcoded registry of source kinds (Sources::ALL)
+ sources.rb — Sources.fetch: a list's source_kind names a class in Sources
sources/ — one class per source kind
sync.rb — reconciles a list's items with a fresh snapshot from its source
proxy_auth.rb — reads the login from Remote-User
@@ -45,7 +45,7 @@ fly.toml — Fly config for the main demo and PR previews
## Domain model
- **User** has many Lists; identified by the login on the `Remote-User` header
-- **List** belongs to a User and has many Items and Axes, and one source (`source_kind` picks a class from `Sources::ALL`; a kind that needs configuring keeps it in its own table keyed by `list_id`) that produces its Items
+- **List** belongs to a User and has many Items and Axes, and one source (`source_kind` names a class in `Sources`, e.g. `Text`; a kind that needs configuring keeps it in its own table keyed by `list_id`) that produces its Items
- **Item** belongs to a List; items missing from the latest sync are archived (`archived_at`) and hidden everywhere
- **Axis** belongs to a List, has many Results
- **Result** belongs to an Axis, a winner (Item), and a loser (Item)
diff --git a/db/migrate/004_add_list_sources.rb b/db/migrate/004_add_list_sources.rb
index 58c147b..3857643 100644
--- a/db/migrate/004_add_list_sources.rb
+++ b/db/migrate/004_add_list_sources.rb
@@ -4,10 +4,10 @@ Sequel.migration do
up do
# Every list gets its items from exactly one source. Lists before this
# were made from the textarea, so they default to the text source. A
- # kind that needs configuring gets its own table keyed by list_id; text
+ # kind that needs configuring gets its own table keyed by list_id; Text
# needs none.
alter_table(:lists) do
- add_column :source_kind, String, null: false, default: "text"
+ add_column :source_kind, String, null: false, default: "Text"
add_column :synced_at, DateTime
end
diff --git a/lib/rank_king/sources.rb b/lib/rank_king/sources.rb
index f728529..179827b 100644
--- a/lib/rank_king/sources.rb
+++ b/lib/rank_king/sources.rb
@@ -14,11 +14,8 @@ require_relative "sources/text"
module RankKing
module Sources
- ALL = {
- Text.kind => Text,
- }.freeze #: Hash[String, _SourceClass]
-
+ # A list's source_kind names a class in this module.
#: (String kind) -> _SourceClass
- def self.fetch(kind) = ALL.fetch(kind)
+ def self.fetch(kind) = const_get(kind, false)
end
end
diff --git a/lib/rank_king/sources/text.rb b/lib/rank_king/sources/text.rb
index aa1fdc8..a784c5f 100644
--- a/lib/rank_king/sources/text.rb
+++ b/lib/rank_king/sources/text.rb
@@ -4,9 +4,6 @@ module RankKing
module Sources
# A push source: the snapshot is pasted text, one item per line.
class Text
- #: () -> String
- def self.kind = "text"
-
#: () -> bool
def self.push? = true
diff --git a/lib/rank_king/web.rb b/lib/rank_king/web.rb
index ea08681..d282ff9 100644
--- a/lib/rank_king/web.rb
+++ b/lib/rank_king/web.rb
@@ -42,7 +42,7 @@ module RankKing
axis_names = axes_text.lines.map(&:strip).reject(&:empty?)
list = DB.transaction do
- l = List.create(name: name, user_id: user.id, source_kind: Sources::Text.kind)
+ l = List.create(name: name, user_id: user.id, source_kind: "Text")
Sync.call(l, entries)
axis_names.each { |n| Axis.create(list_id: l.id, name: n) }
l
diff --git a/sig/rank_king/sources.rbs b/sig/rank_king/sources.rbs
index a57e9d2..de9485d 100644
--- a/sig/rank_king/sources.rbs
+++ b/sig/rank_king/sources.rbs
@@ -1,5 +1,5 @@
# Entry is a Data class, whose members the inline parser cannot read, and
-# the interfaces every source kind meets are named here so Sources::ALL
+# the interfaces every source kind meets are named here so Sources.fetch
# and List#source can be typed by them.
module RankKing
module Sources
@@ -18,9 +18,8 @@ module RankKing
def entries: (untyped snapshot) -> Array[Entry]
end
- # A source kind, the class registered in Sources::ALL.
+ # A source kind: a class in Sources, named by a list's source_kind.
interface _SourceClass
- def kind: () -> String
def push?: () -> bool
# The source for a list, configured from the kind's own table.
def for: (List list) -> _Source
diff --git a/test/test_web.rb b/test/test_web.rb
index 90cf79b..e5253f9 100644
--- a/test/test_web.rb
+++ b/test/test_web.rb
@@ -129,7 +129,7 @@ class TestWeb < Minitest::Test
create_list(name: "Foods", items: "Pizza\nSushi")
list = RankKing::List.first
- assert_equal "text", list.source_kind
+ assert_equal "Text", list.source_kind
assert_equal %w[Pizza Sushi], list.items.map(&:external_id)
end