import: Config is a Data with a required host
Read refuses a config that names none — missing, empty, or blank —
naming the file, so ImportTasks.host has no nil to field.
Assisted-by: GLM-5.3 via pi
diff --git a/lib/pro_tacts/import/config.rb b/lib/pro_tacts/import/config.rb
index 9dbf24e..19c5012 100644
--- a/lib/pro_tacts/import/config.rb
+++ b/lib/pro_tacts/import/config.rb
@@ -2,28 +2,34 @@ require "yaml"
module ProTacts
module Import
- # The standing data the import tasks keep between plans, as a YAML
- # mapping: data/import/config.yml, at the root of the import
- # collateral under data/import. Not committed — /data is ignored —
- # because it holds this deployment's own host and choices. The keys
- # are the tasks' to grow as they take standing data off the command
- # line.
- class Config
- # A missing file is no configuration and an empty one is none
- # either; a file whose top level is not a mapping is refused
- # rather than half-read, naming the file.
- #: (Pathname path) -> Hash[untyped, untyped]
+ # The standing data the import tasks keep between plans, read from
+ # data/import/config.yml, at the root of the import collateral
+ # under data/import. Not committed — /data is ignored — because it
+ # holds this deployment's own host and choices. Each key of the
+ # file is a member, grown one at a time as the tasks take standing
+ # data off the command line.
+ # @rbs skip
+ class Config < Data.define(:host)
+ # Every problem is refused rather than skipped, naming the file —
+ # Card#read's rules for a hand-edited file. The host is required
+ # rather than nil-able: the tasks that read this are the ones
+ # with a host to land on, and a config that names none fails at
+ # the read, before any plan is picked.
def self.read(path)
- return {} unless path.file?
+ invalid = ->(why) { raise ArgumentError, "#{path}: #{why}" }
+ invalid.("does not exist") unless path.file?
document = YAML.safe_load_file(path)
- return {} if document.nil?
+ invalid.("has no host") if document.nil?
+ invalid.("is not a mapping") unless document.is_a?(Hash)
+ unknown = document.keys - members.map(&:to_s)
+ invalid.("has #{unknown.join(", ")}, which no member takes") unless unknown.empty?
- unless document.is_a?(Hash)
- raise ArgumentError, "#{path} is not a YAML mapping"
- end
-
- document
+ host = document.fetch("host", nil)
+ invalid.("has no host") if host.nil?
+ invalid.("host must be text") unless host.is_a?(String)
+ invalid.("host is blank") if host.strip.empty?
+ new(host:)
end
end
end
diff --git a/sig/pro_tacts/import.rbs b/sig/pro_tacts/import.rbs
index 90c8788..b74ff5c 100644
--- a/sig/pro_tacts/import.rbs
+++ b/sig/pro_tacts/import.rbs
@@ -43,6 +43,14 @@ module ProTacts
end
end
+ class Config
+ attr_reader host: String
+
+ def self.members: () -> Array[Symbol]
+ def self.new: (host: String) -> instance
+ def self.read: (Pathname path) -> instance
+ end
+
class Finalize
class Result
attr_reader done: Integer
diff --git a/tasks/import.rake b/tasks/import.rake
index a5ee47f..0d56d98 100644
--- a/tasks/import.rake
+++ b/tasks/import.rake
@@ -55,16 +55,13 @@ module ImportTasks
end
# The host to land a plan on: the standing `host` under CONFIG, the
- # one place a host is named. A bare hostname is the base URL of a
- # server that serves HTTPS, which every deployment does; the scheme
- # is spelled out here so the host a plan records is the one a second
- # run is compared against.
+ # one place a host is named, and required at the read. A bare
+ # hostname is the base URL of a server that serves HTTPS, which
+ # every deployment does; the scheme is spelled out here so the host
+ # a plan records is the one a second run is compared against.
#: () -> String
def self.host
- named = ProTacts::Import::Config.read(CONFIG).fetch("host", nil)
- abort "no host to land on: write host: under #{CONFIG}" if named.nil?
-
- uri = URI.parse(named)
+ uri = URI.parse(ProTacts::Import::Config.read(CONFIG).host)
uri = URI.parse("https://#{uri}") if uri.scheme.nil?
uri.to_s
end
diff --git a/test/pro_tacts/import/test_config.rb b/test/pro_tacts/import/test_config.rb
index 5681d08..0f24f7c 100644
--- a/test/pro_tacts/import/test_config.rb
+++ b/test/pro_tacts/import/test_config.rb
@@ -8,21 +8,25 @@ require "pro_tacts/import/config"
class ImportConfigTest < Minitest::Test
Config = ProTacts::Import::Config
- def test_a_missing_file_is_no_configuration
- Dir.mktmpdir do |dir|
- assert_equal({}, Config.read(Pathname.new(dir) / "config.yml"))
+ def test_the_mapping_is_read
+ with_config_file("host: https://contacts\n") do |path|
+ assert_equal Config.new(host: "https://contacts"), Config.read(path)
end
end
- def test_an_empty_file_is_no_configuration
- with_config_file("") do |path|
- assert_equal({}, Config.read(path))
+ def test_a_missing_file_is_refused
+ Dir.mktmpdir do |dir|
+ error = assert_raises(ArgumentError) { Config.read(Pathname.new(dir) / "config.yml") }
+
+ assert_equal "#{Pathname.new(dir) / "config.yml"}: does not exist", error.message
end
end
- def test_the_mapping_is_read
- with_config_file("host: https://contacts\n") do |path|
- assert_equal({"host" => "https://contacts"}, Config.read(path))
+ def test_an_empty_file_has_no_host
+ with_config_file("") do |path|
+ error = assert_raises(ArgumentError) { Config.read(path) }
+
+ assert_equal "#{path}: has no host", error.message
end
end
@@ -30,7 +34,31 @@ class ImportConfigTest < Minitest::Test
with_config_file("- one\n- two\n") do |path|
error = assert_raises(ArgumentError) { Config.read(path) }
- assert_equal "#{path} is not a YAML mapping", error.message
+ assert_equal "#{path}: is not a mapping", error.message
+ end
+ end
+
+ def test_a_key_no_member_takes_is_refused
+ with_config_file("hots: contacts\n") do |path|
+ error = assert_raises(ArgumentError) { Config.read(path) }
+
+ assert_equal "#{path}: has hots, which no member takes", error.message
+ end
+ end
+
+ def test_a_host_that_is_not_text_is_refused
+ with_config_file("host: 9292\n") do |path|
+ error = assert_raises(ArgumentError) { Config.read(path) }
+
+ assert_equal "#{path}: host must be text", error.message
+ end
+ end
+
+ def test_a_blank_host_is_refused
+ with_config_file("host: \"\"\n") do |path|
+ error = assert_raises(ArgumentError) { Config.read(path) }
+
+ assert_equal "#{path}: host is blank", error.message
end
end
diff --git a/test/tasks/test_import.rb b/test/tasks/test_import.rb
index c9d8996..839ef07 100644
--- a/test/tasks/test_import.rb
+++ b/test/tasks/test_import.rb
@@ -60,12 +60,12 @@ class ImportTasksTest < Minitest::Test
end
end
- def test_no_host_in_the_config_aborts_naming_it
+ def test_a_config_without_a_host_fails_the_read
Dir.mktmpdir do |root|
with_import_tasks(Pathname.new(root)) do
- _, err = capture_io { assert_raises(SystemExit) { ImportTasks.host } }
+ error = assert_raises(ArgumentError) { ImportTasks.host }
- assert_equal "no host to land on: write host: under data/import/config.yml\n", err
+ assert_equal "data/import/config.yml: does not exist", error.message
end
end
end