sources: let each kind mount its own routes
Assisted-by: Claude Opus 5.5 via Claude Code
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.