Prefer Pathname over File for paths
Build paths with Pathname and convert to a String only where a caller
needs one. Record the convention in AGENTS.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RTqTY7HvPy5e5RvUQ2TQdH
diff --git a/AGENTS.md b/AGENTS.md
index 8de48bb..fbc079e 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -87,6 +87,10 @@ Tests set `DATABASE_URL=:memory:` in `test/test_helper.rb`.
- 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); lists from before users existed have no owner and are visible to everyone
- Config is a frozen `Data.define` value object
+- Paths are `Pathname`s, not strings built with `File`/`Dir`
+ (`Pathname(__dir__).join(...)`, `#exist?`, `#read`). Convert with
+ `to_s` only at a boundary that wants a String, such as `$LOAD_PATH`,
+ `Rack::Builder.parse_file`, or `Sequel::Migrator.run`.
- Types are inline RBS comments (`#:` before a method, `# @rbs` for the
rest), checked by `rake steep`. `sig/` holds only what that syntax
cannot say, each file saying why: gems with no signatures (Sequel,
diff --git a/Rakefile b/Rakefile
index f07e090..7cb2df0 100644
--- a/Rakefile
+++ b/Rakefile
@@ -1,4 +1,6 @@
-$LOAD_PATH.unshift File.expand_path("lib", __dir__)
+require "pathname"
+
+$LOAD_PATH.unshift Pathname(__dir__).join("lib").to_s
require "minitest/test_task"
diff --git a/config.ru b/config.ru
index e7331d6..1f8d7bd 100644
--- a/config.ru
+++ b/config.ru
@@ -1,6 +1,8 @@
# frozen_string_literal: true
-$LOAD_PATH.unshift(File.expand_path("lib", __dir__))
+require "pathname"
+
+$LOAD_PATH.unshift(Pathname(__dir__).join("lib").to_s)
require_relative "lib/rank_king/web"
diff --git a/demo.ru b/demo.ru
index e9958a2..db3bf51 100644
--- a/demo.ru
+++ b/demo.ru
@@ -4,9 +4,11 @@
# write it. The dev server and the Fly apps serve from here; a deployment
# behind the proxy runs config.ru alone.
-$LOAD_PATH.unshift(File.expand_path("lib", __dir__))
+require "pathname"
+
+$LOAD_PATH.unshift(Pathname(__dir__).join("lib").to_s)
require "rank_king/stub_login"
use RankKing::StubLogin
-run Rack::Builder.parse_file(File.expand_path("config.ru", __dir__))
+run Rack::Builder.parse_file(Pathname(__dir__).join("config.ru").to_s)
diff --git a/lib/rank_king/db.rb b/lib/rank_king/db.rb
index d9ffb7a..8a1d60d 100644
--- a/lib/rank_king/db.rb
+++ b/lib/rank_king/db.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
+require "pathname"
require "sequel"
require_relative "config"
@@ -9,9 +10,8 @@ module RankKing
Sequel.extension :migration
# `__dir__` is nil only for code with no file behind it, which a
# required library is not, and the assertion says so to Steep.
- MIGRATIONS = File.expand_path(
- "../../db/migrate",
+ MIGRATIONS = Pathname(
__dir__, #: String
- )
- Sequel::Migrator.run(DB, MIGRATIONS)
+ ).join("../../db/migrate").expand_path
+ Sequel::Migrator.run(DB, MIGRATIONS.to_s)
end
diff --git a/lib/rank_king/views/layout.rb b/lib/rank_king/views/layout.rb
index b4b6cf8..f1321c3 100644
--- a/lib/rank_king/views/layout.rb
+++ b/lib/rank_king/views/layout.rb
@@ -1,20 +1,22 @@
# frozen_string_literal: true
require "digest"
+require "pathname"
require "phlex"
module RankKing
module Views
class Layout < Phlex::HTML
ASSET_VERSIONS = begin
- root = File.expand_path("../../../public", __dir__)
+ root = Pathname(__dir__).join("../../../public").expand_path
%w[
/css/reset.css
/css/utopia.css
/css/app.css
].to_h {|path|
- file = File.join(root, path)
- [path, File.exist?(file) ? Digest::MD5.file(file).hexdigest[0, 10] : "0"]
+ # Pathname#join would treat the leading slash as a new root.
+ file = root.join(path.delete_prefix("/"))
+ [path, file.exist? ? Digest::MD5.file(file).hexdigest[0, 10] : "0"]
}.freeze
end