Drop App wrapper for conventional Domus::DB access
The Domus.config writer and Web.opts[:app] injection made every entry
point replay an order-sensitive boot. A plain Domus::DB plus env-driven
config restores the conventional Roda/Sequel shape.
Assisted-by: Claude Opus 4.8 via Claude Code
diff --git a/Rakefile b/Rakefile
index 2a9ea7c..d8a6709 100644
--- a/Rakefile
+++ b/Rakefile
@@ -1,15 +1,15 @@
# Put lib/ on the load path so plain requires and Sequel's extension loader
# (db.extension(:sole) -> require "sequel/extensions/sole") resolve. The test
-# task adds lib/ for the spawned test run, but this Rakefile also builds an App
-# at load time for the db: tasks, which runs in the bare rake process.
+# task adds lib/ for the spawned test run, but this Rakefile also opens the
+# database connection at load time for the db: tasks, which runs in the bare
+# rake process.
$LOAD_PATH.unshift(File.expand_path("lib", __dir__))
require "minitest/test_task"
-require "domus/app"
+require "domus/db"
require "sequel/extensions/migration"
-DOMUS_APP = Domus::App.new
-DB = DOMUS_APP.db
+DB = Domus::DB
desc "Create bundler binstubs (runs when Gemfile.lock changes)"
file ".direnv/.bundled" => ["Gemfile.lock"] do
@@ -61,7 +61,7 @@ namespace :db do
desc "Seed the database with development data"
task :seed do
- require "seeds"
- puts Domus::Seeds.call(DOMUS_APP) ? "Seeded." : "Already seeded."
+ require "domus/seeds"
+ puts Domus::Seeds.call ? "Seeded." : "Already seeded."
end
end
diff --git a/Steepfile b/Steepfile
index bbed059..9f86720 100644
--- a/Steepfile
+++ b/Steepfile
@@ -1,7 +1,7 @@
target :lib do
signature "sig"
check "lib/domus/config.rb"
- check "lib/domus/app.rb"
+ check "lib/domus/db.rb"
check "lib/domus/models.rb"
check "lib/domus/relative_time.rb"
check "lib/domus/views/layout.rb"
diff --git a/config.ru b/config.ru
index 8f5de56..3ada7ed 100644
--- a/config.ru
+++ b/config.ru
@@ -2,17 +2,13 @@
# (Sequel::Model.plugin :sole -> require "sequel/plugins/sole") resolve.
$LOAD_PATH.unshift(File.expand_path("lib", __dir__))
-require "domus/app"
-require "domus/web"
+require "domus/db"
require "sequel/extensions/migration"
-app = Domus::App.new
-Sequel::Migrator.run(app.db, "db/migrate") unless Dir.empty?("db/migrate")
+Sequel::Migrator.run(Domus::DB, "db/migrate") unless Dir.empty?("db/migrate")
# Models reflect on their tables at class-load time, so they must be required
-# only after migrations have run.
-require "domus/models"
-
-Domus::Web.opts[:app] = app
+# only after migrations have run. Requiring the web app pulls them in.
+require "domus/web"
run Domus::Web
diff --git a/lib/domus/app.rb b/lib/domus/app.rb
deleted file mode 100644
index c5d2b35..0000000
--- a/lib/domus/app.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-# rbs_inline: enabled
-
-require "sequel"
-require_relative "config"
-
-module Domus
- class App
- attr_reader :config, :db
-
- # : (Config) -> void
- def initialize(config = Domus.config)
- @config = config
- @db = Sequel.sqlite(config.database_url)
- Sequel::Model.db = @db
- end
-
- # : (Hash[Symbol, untyped]) -> Pathname
- def file_path(record)
- uploads_root / "#{record[:id]}#{record[:extension]}"
- end
-
- # : () -> Pathname
- def uploads_root = config.storage_path / "uploads"
- end
-end
diff --git a/lib/domus/config.rb b/lib/domus/config.rb
index 3c68f29..c3a7d6e 100644
--- a/lib/domus/config.rb
+++ b/lib/domus/config.rb
@@ -14,15 +14,18 @@ module Domus
database_url: ENV.fetch("DATABASE_URL") { "db/domus.db" },
storage_path: Pathname(ENV.fetch("STORAGE_PATH") { "storage" }),
)
+
+ # Where stored upload blobs live on disk, derived from the storage root.
+ #: () -> Pathname
+ def uploads_root = storage_path / "uploads"
end
class << self
# The process-wide config, resolved from the environment the first time
# it's read. Loaded code reads this single instance: Web's :static plugin
- # needs the storage path at load, and App stores files there. Tests assign
- # a Config here before requiring the app to point it at a temp dir.
- attr_writer :config
-
+ # needs the storage path at load, and uploads are stored under it. Tests
+ # point it at a temp dir by setting DATABASE_URL / STORAGE_PATH before the
+ # config is first read.
#: () -> Config
def config = @config ||= Config.env
end
diff --git a/lib/domus/db.rb b/lib/domus/db.rb
new file mode 100644
index 0000000..f97ed75
--- /dev/null
+++ b/lib/domus/db.rb
@@ -0,0 +1,13 @@
+# rbs_inline: enabled
+
+require "sequel"
+require_relative "config"
+
+module Domus
+ # The process-wide database connection, opened once from the environment
+ # config. Requiring this file connects as a side effect, so models and the
+ # web app can require it and reach a live Sequel::Database via Domus::DB.
+ DB = Sequel.sqlite(config.database_url) #: Sequel::Database
+end
+
+Sequel::Model.db = Domus::DB
diff --git a/lib/domus/models.rb b/lib/domus/models.rb
index 1a7513a..91df0a0 100644
--- a/lib/domus/models.rb
+++ b/lib/domus/models.rb
@@ -1,6 +1,7 @@
# rbs_inline: enabled
require "sequel"
+require_relative "db"
Sequel::Model.plugin :sole
@@ -33,6 +34,10 @@ module Domus
validates_presence :extension
validates_includes IMAGE_EXTENSIONS, :extension
end
+
+ # Where this upload's blob lives on disk.
+ #: () -> Pathname
+ def file_path = Domus.config.uploads_root / "#{id}#{extension}"
end
class Document < Sequel::Model
diff --git a/lib/domus/seeds.rb b/lib/domus/seeds.rb
index 088e9f4..7d1252a 100644
--- a/lib/domus/seeds.rb
+++ b/lib/domus/seeds.rb
@@ -3,6 +3,7 @@
require "fileutils"
require "open-uri"
require "pathname"
+require_relative "models"
module Domus
# Development and test seed data. Populates a few sample assets so a fresh
@@ -80,21 +81,20 @@ module Domus
# Seeds the database when it's empty, returning true when it inserted data
# and false when assets already exist. The empty check keeps repeated dev
# runs and CI idempotent.
- # : (App) -> bool
- def self.call(app)
- db = app.db
- return false unless db[:assets].empty?
+ # : () -> bool
+ def self.call
+ return false unless Domus::DB[:assets].empty?
# Warm the cache outside the transaction so a slow download doesn't hold
# the write lock open.
ASSETS.flat_map(&:photos).uniq.each(&:fetch)
- db.transaction do
+ Domus::DB.transaction do
ASSETS.each do |seed_asset|
asset_record = Domus::Asset.create(name: seed_asset.name, description: seed_asset.description)
seed_asset.photos.each do |photo|
upload = Domus::Upload.create(extension: ".jpg")
- dest = app.file_path(id: upload.id, extension: ".jpg")
+ dest = upload.file_path
FileUtils.mkdir_p(dest.dirname)
FileUtils.cp(photo.cache_path, dest)
asset_record.add_upload(upload)
diff --git a/lib/domus/web.rb b/lib/domus/web.rb
index 5a23542..d9801f2 100644
--- a/lib/domus/web.rb
+++ b/lib/domus/web.rb
@@ -2,7 +2,7 @@
require "roda"
require "fileutils"
-require_relative "config"
+require_relative "models"
require_relative "views/layout"
require_relative "views/home"
require_relative "views/asset"
@@ -25,7 +25,7 @@ module Domus
# Serve stored uploads (storage/uploads/{id}{ext}) at /uploads/ straight
# off disk. The :static plugin needs a concrete root at load time, so it
- # reads the shared Domus.config (the same instance App uses). Uploads are
+ # reads the shared Domus.config resolved from the environment. Uploads are
# immutable once stored, so they cache indefinitely.
plugin :static, ["/uploads/"],
root: Domus.config.storage_path.to_s,
@@ -90,11 +90,6 @@ module Domus
private
- # : () -> App
- def app = opts.fetch(:app)
- # : () -> Sequel::Database
- def db = app.db
-
# Persists an uploaded image, raising ClientError when the upload is
# rejected so the error_handler plugin can render the right status.
# : (Hash[String, untyped]) -> void
@@ -111,9 +106,9 @@ module Domus
asset_names = Array(params["asset_names"]).flatten.map(&:strip).reject(&:empty?)
- db.transaction do
+ DB.transaction do
upload_record = Upload.create(extension: ext)
- dest = app.file_path(id: upload_record.id, extension: ext)
+ dest = upload_record.file_path
FileUtils.mkdir_p(::File.dirname(dest))
FileUtils.cp(upload[:tempfile].path, dest)
asset_names.each { |name| Asset.create(name:).add_upload(upload_record) }
diff --git a/sig/generated/domus/app.rbs b/sig/generated/domus/app.rbs
deleted file mode 100644
index d0bd611..0000000
--- a/sig/generated/domus/app.rbs
+++ /dev/null
@@ -1,18 +0,0 @@
-# Generated from lib/domus/app.rb with RBS::Inline
-
-module Domus
- class App
- attr_reader config: untyped
-
- attr_reader db: untyped
-
- # : (Config) -> void
- def initialize: (?untyped config) -> untyped
-
- # : (Hash[Symbol, untyped]) -> Pathname
- def file_path: (untyped record) -> untyped
-
- # : () -> Pathname
- def uploads_root: () -> untyped
- end
-end
diff --git a/sig/generated/domus/config.rbs b/sig/generated/domus/config.rbs
index 7a54d3c..24aa58f 100644
--- a/sig/generated/domus/config.rbs
+++ b/sig/generated/domus/config.rbs
@@ -17,14 +17,17 @@ module Domus
class Config
# : () -> Config
def self.env: () -> Config
+
+ # Where stored upload blobs live on disk, derived from the storage root.
+ # : () -> Pathname
+ def uploads_root: () -> Pathname
end
# The process-wide config, resolved from the environment the first time
# it's read. Loaded code reads this single instance: Web's :static plugin
- # needs the storage path at load, and App stores files there. Tests assign
- # a Config here before requiring the app to point it at a temp dir.
- attr_writer config: untyped
-
+ # needs the storage path at load, and uploads are stored under it. Tests
+ # point it at a temp dir by setting DATABASE_URL / STORAGE_PATH before the
+ # config is first read.
# : () -> Config
def self.config: () -> Config
end
diff --git a/sig/generated/domus/db.rbs b/sig/generated/domus/db.rbs
new file mode 100644
index 0000000..42dbe11
--- /dev/null
+++ b/sig/generated/domus/db.rbs
@@ -0,0 +1,8 @@
+# Generated from lib/domus/db.rb with RBS::Inline
+
+module Domus
+ # The process-wide database connection, opened once from the environment
+ # config. Requiring this file connects as a side effect, so models and the
+ # web app can require it and reach a live Sequel::Database via Domus::DB.
+ DB: Sequel::Database
+end
diff --git a/sig/generated/domus/models.rbs b/sig/generated/domus/models.rbs
index 74c2337..ab44f58 100644
--- a/sig/generated/domus/models.rbs
+++ b/sig/generated/domus/models.rbs
@@ -9,6 +9,10 @@ module Domus
IMAGE_EXTENSIONS: untyped
def validate: () -> untyped
+
+ # Where this upload's blob lives on disk.
+ # : () -> Pathname
+ def file_path: () -> Pathname
end
class Document < Sequel::Model
diff --git a/sig/generated/domus/seeds.rbs b/sig/generated/domus/seeds.rbs
index 7adbf59..7aa57e2 100644
--- a/sig/generated/domus/seeds.rbs
+++ b/sig/generated/domus/seeds.rbs
@@ -62,8 +62,8 @@ module Domus
# Seeds the database when it's empty, returning true when it inserted data
# and false when assets already exist. The empty check keeps repeated dev
# runs and CI idempotent.
- # : (App) -> bool
- def self.call: (untyped app) -> untyped
+ # : () -> bool
+ def self.call: () -> untyped
# The XDG cache directory for downloaded seed photos.
# : () -> Pathname
diff --git a/sig/generated/domus/web.rbs b/sig/generated/domus/web.rbs
index cc235a7..a8bc69a 100644
--- a/sig/generated/domus/web.rbs
+++ b/sig/generated/domus/web.rbs
@@ -15,12 +15,6 @@ module Domus
private
- # : () -> App
- def app: () -> untyped
-
- # : () -> Sequel::Database
- def db: () -> untyped
-
# Persists an uploaded image, raising ClientError when the upload is
# rejected so the error_handler plugin can render the right status.
# : (Hash[String, untyped]) -> void
diff --git a/test/test_app.rb b/test/test_app.rb
index 5695fe3..9a7e609 100644
--- a/test/test_app.rb
+++ b/test/test_app.rb
@@ -10,18 +10,16 @@ class TestApp < Minitest::Test
Domus::Web
end
- def domus = Domus::Web.opts.fetch(:app)
-
# Every test starts from the shared seed baseline, so dev and the suite
# exercise the same Domus::Seeds path. Tests that need a clean slate call
# #wipe; tests that assert on counts compare deltas around the action.
def setup
wipe
- Domus::Seeds.call(domus)
+ Domus::Seeds.call
end
def wipe
- domus.db[:asset_attachments].delete
+ Domus::DB[:asset_attachments].delete
Asset.dataset.delete
Upload.dataset.delete
end
@@ -153,7 +151,7 @@ class TestApp < Minitest::Test
assert_equal before + 1, Upload.count
upload_record = Upload.order(:id).last
assert_equal ".png", upload_record.extension
- assert_equal "fake-png-bytes", File.read(domus.file_path(id: upload_record.id, extension: upload_record.extension))
+ assert_equal "fake-png-bytes", File.read(upload_record.file_path)
end
def test_upload_without_file_is_rejected
diff --git a/test/test_helper.rb b/test/test_helper.rb
index a1e5d8b..85d0f23 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -2,27 +2,21 @@ require "minitest/autorun"
require "sequel"
require "sequel/extensions/migration"
require "fileutils"
-require "pathname"
-
-require "domus/app"
+require "tmpdir"
storage = Dir.mktmpdir("domus-test")
at_exit { FileUtils.rm_rf(storage) }
-# Web reads Domus.config at load to configure static upload serving, so inject
-# a temp-dir config before requiring it.
-Domus.config = Domus::Config.new(database_url: ":memory:", storage_path: Pathname(storage))
+# Drive config through the environment, the same way production does, before
+# anything reads Domus.config: an in-memory database and a temp storage dir.
+ENV["DATABASE_URL"] = ":memory:"
+ENV["STORAGE_PATH"] = storage
-app = Domus::App.new
+require "domus/db"
migrate_dir = File.expand_path("../db/migrate", __dir__)
-Sequel::Migrator.run(app.db, migrate_dir) unless Dir.empty?(migrate_dir)
+Sequel::Migrator.run(Domus::DB, migrate_dir) unless Dir.empty?(migrate_dir)
# Models reflect on their tables at class-load time, so they must be required
-# only after migrations have run. App#initialize sets Sequel::Model.db, so
-# models can be defined once the app exists.
-require "domus/models"
-
+# only after migrations have run. Requiring the web app pulls them in.
require "domus/web"
require "domus/seeds"
-
-Domus::Web.opts[:app] = app