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
change
commit a66bea6ef6c8661a9fb6131e6596ae1d5767d553
author Claude <noreply@anthropic.com>
date
parent ff57223f
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