sources: raise Invalid when the new-list form falls short
Assisted-by: Claude Opus 5.5 via Claude Code
change ukpkkxnmmtstypuvwzruszzzryknmspx
commit 324bb7e51d67c50f2476717399984b0eedbc58b1
author Alpha Chen <alpha@kejadlen.dev>
date
parent kypqnmxy
diff --git a/lib/rank_king/sources.rb b/lib/rank_king/sources.rb
index c3bf080..b26d969 100644
--- a/lib/rank_king/sources.rb
+++ b/lib/rank_king/sources.rb
@@ -8,6 +8,10 @@ module RankKing
     # Signed in sig/rank_king/sources.rbs: a Data class.
     # @rbs skip
     Entry = Data.define(:key, :name)
+
+    # Raised by a kind's create when the new-list form did not post enough
+    # to set the list up; the message says what is missing.
+    class Invalid < StandardError; end
   end
 end
 
diff --git a/lib/rank_king/sources/text.rb b/lib/rank_king/sources/text.rb
index d057702..1282e25 100644
--- a/lib/rank_king/sources/text.rb
+++ b/lib/rank_king/sources/text.rb
@@ -22,15 +22,14 @@ module RankKing
 
       # 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
+      #: (List list, Hash[String, untyped] params, Config config) -> void
       def self.create(list, params, config)
         names = params["items"].to_s.lines.map(&:strip).reject(&:empty?)
-        return false if names.size < 2
+        raise Invalid, "A list needs at least two items." if names.size < 2
 
         names.each do |name|
           Item.create(list_id: list.id, name:)
         end
-        true
       end
 
       #: (List list) -> void
diff --git a/lib/rank_king/sources/youtube.rb b/lib/rank_king/sources/youtube.rb
index 2f3d232..5cb25a3 100644
--- a/lib/rank_king/sources/youtube.rb
+++ b/lib/rank_king/sources/youtube.rb
@@ -53,15 +53,13 @@ module RankKing
 
       # Follows the playlist chosen on the new-list form and fetches it
       # with the cookie pasted there.
-      #: (List list, Hash[String, untyped] params, _InnerTubeConfig config) -> bool
+      #: (List list, Hash[String, untyped] params, _InnerTubeConfig config) -> void
       def self.create(list, params, config)
-        playlist_id = params["playlist"].to_s[/\A[\w-]+\z/]
-        cookie = InnerTube.cookie(params["cookie"].to_s)
-        return false unless playlist_id && cookie
+        playlist_id = params["playlist"].to_s[/\A[\w-]+\z/] or raise Invalid, "Choose a playlist to follow."
+        cookie = InnerTube.cookie(params["cookie"].to_s) or raise Invalid, "That cookie has no SAPISID to sign requests with."
 
         DB[TABLE].insert(list_id: list.id, playlist_id:)
         new(list, config).fetch(cookie)
-        true
       end
 
       #: (List list, _InnerTubeConfig config) -> void
diff --git a/lib/rank_king/web.rb b/lib/rank_king/web.rb
index d4a862d..1101777 100644
--- a/lib/rank_king/web.rb
+++ b/lib/rank_king/web.rb
@@ -17,9 +17,12 @@ module RankKing
     plugin :static, %w[ /css /js /vendor ]
     plugin :sessions, secret: CONFIG.session_secret
     plugin :route_csrf, csrf_failure: :empty_403, check_request_methods: %w[POST]
-    plugin :error_handler, classes: [ProxyAuth::MissingLogin, InnerTube::Error] do |e|
+    plugin :error_handler, classes: [ProxyAuth::MissingLogin, InnerTube::Error, Sources::Invalid] do |e|
       response["Content-Type"] = "text/plain"
-      if e.is_a?(InnerTube::Error)
+      if e.is_a?(Sources::Invalid)
+        response.status = 422
+        "#{e.message}\n"
+      elsif e.is_a?(InnerTube::Error)
         response.status = 502
         "YouTube: #{e.message}\n"
       else
@@ -50,18 +53,17 @@ module RankKing
 
             axis_names = names_param(r.params["axes"])
 
-            # The source fills the list from its own fields, or turns it
-            # down and nothing is made.
+            # The source fills the list from its own fields, or raises and
+            # nothing is made.
             list = DB.transaction {
               l = List.create(name: name, user_id: user.id, source_kind: source_kind)
-              raise Sequel::Rollback unless Sources.fetch(source_kind).create(l, r.params, config)
+              Sources.fetch(source_kind).create(l, r.params, config)
 
               axis_names.each do |n|
                 Axis.create(list_id: l.id, name: n)
               end
               l
             }
-            r.halt 422 unless list
 
             r.redirect "/lists/#{list.id}"
           end
diff --git a/sig/rank_king/sources.rbs b/sig/rank_king/sources.rbs
index 39c51a4..66e12e3 100644
--- a/sig/rank_king/sources.rbs
+++ b/sig/rank_king/sources.rbs
@@ -46,9 +46,9 @@ module RankKing
       # routes of their own as they are filled in.
       def fields: (csrf: Method) -> _Component
 
-      # 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
+      # Sets a new list up from what those fields posted, raising Invalid,
+      # and the list is not made, when they are not enough to go on.
+      def create: (List list, Hash[String, untyped] params, Config config) -> void
 
       # Mounts the routes the kind's own fields and dialogs post to, ahead
       # of the app's.
diff --git a/test/test_web.rb b/test/test_web.rb
index cbbf488..d2d0a3e 100644
--- a/test/test_web.rb
+++ b/test/test_web.rb
@@ -51,6 +51,7 @@ class TestWeb < Minitest::Test
   def test_create_list_rejects_fewer_than_two_items
     create_list(name: "Solo", items: "OnlyOne")
     assert_equal 422, last_response.status
+    assert_equal "A list needs at least two items.\n", last_response.body
   end
 
   def test_create_list_without_axes
diff --git a/test/test_youtube.rb b/test/test_youtube.rb
index bc8eff8..71a6817 100644
--- a/test/test_youtube.rb
+++ b/test/test_youtube.rb
@@ -190,6 +190,7 @@ class TestYouTube < Minitest::Test
   def test_create_needs_a_cookie
     create_list(playlist: PLAYLIST, cookie: "SID=sid")
     assert_equal 422, last_response.status
+    assert_equal "That cookie has no SAPISID to sign requests with.\n", last_response.body
     assert_equal 0, RankKing::List.count
     assert_empty @inner_tube.calls
   end
@@ -197,6 +198,7 @@ class TestYouTube < Minitest::Test
   def test_create_needs_a_playlist
     create_list(playlist: "not a playlist")
     assert_equal 422, last_response.status
+    assert_equal "Choose a playlist to follow.\n", last_response.body
     assert_equal 0, RankKing::List.count
   end