sources: let each kind mount its own routes
Assisted-by: Claude Opus 5.5 via Claude Code
change kypqnmxyrznqnwkzlzkurxkrksotvxkn
commit 19584a54b45f130894f723b2f0fbee1acac09645
author Alpha Chen <alpha@kejadlen.dev>
date
parent lskopnso
diff --git a/AGENTS.md b/AGENTS.md
index 00becad..4b280b7 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -130,8 +130,9 @@ rank-king never stores it.
 - A list's source is fixed when it is made. `Sources::KINDS` lists the
   kinds the new-list form offers, and each kind's `title` names it there
   and in settings. Its sync dialog opens from the list's ⋯ menu. Each
-  kind also renders its own fields on the new-list form (`fields`) and
-  sets the new list up from them (`create`).
+  kind also renders its own fields on the new-list form (`fields`),
+  sets the new list up from them (`create`), and mounts any routes those
+  fields post to (`routes`).
 - Every request needs a `Remote-User` header or it gets a 401. That is safe only because a proxy (Tailscale identity) writes the header. `StubLogin` fills it in for `demo.ru` (dev server, Fly apps) and nowhere else: never in `config.ru`, and never in front of real data.
 - Lists are private to their owner (404 for anyone else)
 - Config is a frozen `Data.define` value object
diff --git a/lib/rank_king/sources/text.rb b/lib/rank_king/sources/text.rb
index e6c48fa..d057702 100644
--- a/lib/rank_king/sources/text.rb
+++ b/lib/rank_king/sources/text.rb
@@ -17,6 +17,9 @@ module RankKing
       #: (csrf: Method) -> _Component
       def self.fields(csrf:) = Views::Sources::TextFields.new
 
+      #: (Roda::RodaRequest r, Config config) -> void
+      def self.routes(r, config) = nil
+
       # Adds the items typed into the new-list form, of which there must
       # be at least two.
       #: (List list, Hash[String, untyped] params, Config config) -> bool
diff --git a/lib/rank_king/sources/youtube.rb b/lib/rank_king/sources/youtube.rb
index a64a79a..2f3d232 100644
--- a/lib/rank_king/sources/youtube.rb
+++ b/lib/rank_king/sources/youtube.rb
@@ -28,6 +28,15 @@ module RankKing
       #: (csrf: Method) -> _Component
       def self.fields(csrf:) = Views::Sources::YouTubeFields.new(csrf:)
 
+      # The new-list form's playlist picker, drawn from a pasted cookie. It
+      # is a POST so the cookie stays out of URLs and logs.
+      #: (Roda::RodaRequest r, _InnerTubeConfig config) -> void
+      def self.routes(r, config)
+        r.post PLAYLISTS_PATH.delete_prefix("/") do
+          playlists(r.params["cookie"], config).call
+        end
+      end
+
       # The choice of playlists a pasted cookie opens. Whatever stops it
       # is said in the choice's place.
       #: (untyped text, _InnerTubeConfig config) -> _Component
diff --git a/lib/rank_king/web.rb b/lib/rank_king/web.rb
index 813d343..d4a862d 100644
--- a/lib/rank_king/web.rb
+++ b/lib/rank_king/web.rb
@@ -37,11 +37,7 @@ module RankKing
         Views::Home.new(lists: lists).call
       end
 
-      # A fragment for the new-list form: the playlists a pasted cookie
-      # opens. It is a POST so the cookie stays out of URLs and logs.
-      r.post "youtube", "playlists" do
-        Sources::YouTube.playlists(r.params["cookie"], config).call
-      end
+      Sources::KINDS.each { |kind| Sources.fetch(kind).routes(r, config) }
 
       r.on "lists" do
         r.is do
diff --git a/sig/rank_king/sources.rbs b/sig/rank_king/sources.rbs
index 93150bb..39c51a4 100644
--- a/sig/rank_king/sources.rbs
+++ b/sig/rank_king/sources.rbs
@@ -49,6 +49,10 @@ module RankKing
       # Sets a new list up from what those fields posted: false, and the
       # list is not made, when they are not enough to go on.
       def create: (List list, Hash[String, untyped] params, Config config) -> bool
+
+      # Mounts the routes the kind's own fields and dialogs post to, ahead
+      # of the app's.
+      def routes: (Roda::RodaRequest r, Config config) -> void
     end
 
     # All a YouTube source reads off the app's config.