Introduce a data directory in place of the contacts directory
PRO_TACTS_DATA_DIR (default data) now roots everything under it, with
contacts at data/contacts, so the coming database has a home. Paths
compose with Pathname throughout.
Assisted-by: GLM-5.3 via pi
diff --git a/Rakefile b/Rakefile
index 8a4365a..9e6f52f 100644
--- a/Rakefile
+++ b/Rakefile
@@ -2,7 +2,6 @@
require "pathname"
$LOAD_PATH.unshift(Pathname.new(__dir__) / "lib")
-
require "minitest/test_task"
Minitest::TestTask.create
@@ -21,7 +20,7 @@ task :fixtures do
ENV["RACK_ENV"] = "test"
# Mirrors test/test_helper.rb, which cannot be required here without
# minitest/autorun running its at_exit hook inside rake.
- ENV["PRO_TACTS_CONTACTS_DIR"] = File.expand_path("test/fixtures/contacts", __dir__)
+ ENV["PRO_TACTS_DATA_DIR"] = (Pathname.new(__dir__) / "test/fixtures").to_s
require "pro_tacts/web"
require_relative "test/pro_tacts/exchange_fixtures"
ExchangeFixtures.record_responses(ProTacts::Web)
diff --git a/lib/pro_tacts/config.rb b/lib/pro_tacts/config.rb
index 3d9804b..667a78d 100644
--- a/lib/pro_tacts/config.rb
+++ b/lib/pro_tacts/config.rb
@@ -1,4 +1,6 @@
+require "pathname"
+
module ProTacts
# Single source of truth for configuration read from the environment.
# Nothing else in the app should read ENV directly; add a method here and
@@ -32,10 +34,17 @@ module ProTacts
!value.nil? && value.match?(TRUTHY)
end
- # Directory of contact KDL files, one contact per file; the filename
- # is the contact ID. See docs/plans/2026-01-12-carddav-architecture.md.
+ # Root data directory: holds the contacts directory and, later, the
+ # database. Overridable with PRO_TACTS_DATA_DIR.
+ def data_dir
+ Pathname.new(@env.fetch("PRO_TACTS_DATA_DIR", "data"))
+ end
+
+ # Contacts live at data/contacts, one KDL file per contact; the
+ # filename is the contact ID. See
+ # docs/plans/2026-01-12-carddav-architecture.md.
def contacts_dir
- @env.fetch("PRO_TACTS_CONTACTS_DIR", "data/contacts")
+ data_dir / "contacts"
end
# Where the debug logger writes. A path, overridable with
diff --git a/lib/pro_tacts/contact.rb b/lib/pro_tacts/contact.rb
index 1b6cfe7..0c65823 100644
--- a/lib/pro_tacts/contact.rb
+++ b/lib/pro_tacts/contact.rb
@@ -18,7 +18,7 @@ module ProTacts
# Every contact in the directory, one file per contact. An empty
# directory is a valid empty address book, but any non-hidden
# non-.kdl file raises — a misplaced file or a wrong
- # PRO_TACTS_CONTACTS_DIR should not quietly serve a partial address
+ # PRO_TACTS_DATA_DIR should not quietly serve a partial address
# book. Files that fail to parse or render are reported and skipped;
# Sentry is a no-op while uninitialized, so tests need no DSN.
def self.all(directory = ProTacts.config.contacts_dir)
diff --git a/test/pro_tacts/test_config.rb b/test/pro_tacts/test_config.rb
index aaa0d52..bcfb1cc 100644
--- a/test/pro_tacts/test_config.rb
+++ b/test/pro_tacts/test_config.rb
@@ -25,12 +25,17 @@ class ConfigTest < Minitest::Test
assert_raises(KeyError) { ProTacts::Config.new({}).sentry_dsn }
end
- def test_contacts_dir_defaults_to_data_contacts
- assert_equal "data/contacts", ProTacts::Config.new({}).contacts_dir
+ def test_data_dir_defaults_to_data
+ assert_equal Pathname.new("data"), ProTacts::Config.new({}).data_dir
end
- def test_contacts_dir_is_overridable
- assert_equal "/tmp/kdl", ProTacts::Config.new("PRO_TACTS_CONTACTS_DIR" => "/tmp/kdl").contacts_dir
+ def test_data_dir_is_overridable
+ assert_equal Pathname.new("/tmp/state"), ProTacts::Config.new("PRO_TACTS_DATA_DIR" => "/tmp/state").data_dir
+ end
+
+ def test_contacts_dir_lives_under_the_data_dir
+ assert_equal Pathname.new("data/contacts"), ProTacts::Config.new({}).contacts_dir
+ assert_equal Pathname.new("/tmp/state/contacts"), ProTacts::Config.new("PRO_TACTS_DATA_DIR" => "/tmp/state").contacts_dir
end
def test_debug_defaults_off
diff --git a/test/pro_tacts/test_contact.rb b/test/pro_tacts/test_contact.rb
index b30b72e..f4af361 100644
--- a/test/pro_tacts/test_contact.rb
+++ b/test/pro_tacts/test_contact.rb
@@ -1,5 +1,6 @@
require_relative "../test_helper"
+require "pathname"
require "tmpdir"
require "pro_tacts/contact"
@@ -7,8 +8,9 @@ require "pro_tacts/contact"
class ContactTest < Minitest::Test
def with_contacts(files)
Dir.mktmpdir do |dir|
- files.each { |name, content| File.write(File.join(dir, name), content) }
- yield ProTacts::Contact.all(dir)
+ directory = Pathname.new(dir)
+ files.each { |name, content| File.write(directory / name, content) }
+ yield ProTacts::Contact.all(directory)
end
end
@@ -46,7 +48,7 @@ class ContactTest < Minitest::Test
def test_non_kdl_files_raise
Dir.mktmpdir do |dir|
- File.write(File.join(dir, "notes.txt"), "hello")
+ File.write(Pathname.new(dir) / "notes.txt", "hello")
error = assert_raises(ArgumentError) { ProTacts::Contact.all(dir) }
diff --git a/test/pro_tacts/test_web.rb b/test/pro_tacts/test_web.rb
index ec4126a..7fc855c 100644
--- a/test/pro_tacts/test_web.rb
+++ b/test/pro_tacts/test_web.rb
@@ -1,5 +1,6 @@
require_relative "../test_helper"
+require "pathname"
require "rack/test"
require "tmpdir"
@@ -106,15 +107,17 @@ class WebTest < Minitest::Test
assert_equal "Not Found", last_response.body
end
- # Swaps in a throwaway contacts directory so the multi-contact routes
- # can be exercised without touching the exchange fixture data.
+ # Swaps in a throwaway data directory so the multi-contact routes can
+ # be exercised without touching the exchange fixture data.
def with_contacts(files)
Dir.mktmpdir do |dir|
- files.each { |name, content| File.write(File.join(dir, name), content) }
+ contacts_dir = Pathname.new(dir) / "contacts"
+ Dir.mkdir(contacts_dir)
+ files.each { |name, content| File.write(contacts_dir / name, content) }
original = ProTacts.config
ProTacts.config = ProTacts::Config.new({
"RACK_ENV" => "test",
- "PRO_TACTS_CONTACTS_DIR" => dir,
+ "PRO_TACTS_DATA_DIR" => dir,
})
begin
yield
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 3606673..6d59f00 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -1,9 +1,13 @@
# Marks the process as running tests before anything requires the app, so
-# web.rb skips Sentry.init and no SENTRY_DSN is needed. The contacts
-# directory holds the card the recorded macOS exchange asked for, so the
-# fixture replay resolves the same hrefs the client did.
+# web.rb skips Sentry.init and no SENTRY_DSN is needed. The fixtures
+# directory serves as the data directory: its contacts/ holds the card the
+# recorded macOS exchange asked for, so the replay resolves the same hrefs
+# the client did.
ENV["RACK_ENV"] = "test"
-ENV["PRO_TACTS_CONTACTS_DIR"] = File.expand_path("fixtures/contacts", __dir__)
+
+require "pathname"
+
+ENV["PRO_TACTS_DATA_DIR"] = (Pathname.new(__dir__) / "fixtures").to_s
require "minitest/autorun"