diff --git a/README.md b/README.md index 5c8bb03..c5f2060 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ ENV.integer_range("ID_RANGE", default: (500..6000)) ENV.integer("MAX_THREAD_COUNT", default: 5) ENV.file_path("FILE_PATH", default: "/some/path", required: true) ENV.date("SCHEDULED_DATE", required: true, format: "%Y-%m-%d") +ENV.date_time("RUN_AT", required: true, default: DateTime.now) ``` Each of the supplied methods takes a positional parameter for the name of the environment variable, @@ -64,3 +65,8 @@ The available methods added to `ENV`: which would parse a date like `2023-12-25`. It will handle invalid values (or format strings) like the variable not being present, though if it's specified as `required`, you will see a different exception in each case. +* `date_time` - produces a `DateTime` object, using either `DateTime.strptime` or `DateTime.iso8601`. + The default format is `:iso8601`, and `:unix` is also an allowed 'format'. But if it is supplied + as a _string_, it will be handled as a strptime format string (the `:unix` format is equivalent to + the format string `"%s"`). It handles invalid or unparseable values like `ENV.date` does, in that + they are treated as if not supplied. diff --git a/lib/environment_helpers.rb b/lib/environment_helpers.rb index 2760f6d..f72f134 100644 --- a/lib/environment_helpers.rb +++ b/lib/environment_helpers.rb @@ -14,12 +14,14 @@ module EnvironmentHelpers Error = Class.new(::StandardError) MissingVariableError = Class.new(Error) BadDefault = Class.new(Error) + BadFormat = Class.new(Error) InvalidValue = Class.new(Error) InvalidBooleanText = Class.new(InvalidValue) InvalidRangeText = Class.new(InvalidValue) InvalidIntegerText = Class.new(InvalidValue) InvalidDateText = Class.new(InvalidValue) + InvalidDateTimeText = Class.new(InvalidValue) include AccessHelpers include StringHelpers diff --git a/lib/environment_helpers/datetime_helpers.rb b/lib/environment_helpers/datetime_helpers.rb index fa15c21..0c5a86c 100644 --- a/lib/environment_helpers/datetime_helpers.rb +++ b/lib/environment_helpers/datetime_helpers.rb @@ -12,6 +12,16 @@ def date(name, format: "%Y-%m-%d", default: nil, required: false) fail(InvalidDateText, "Required date environment variable #{name} had inappropriate content '#{text}'") end + def date_time(name, format: :iso8601, default: nil, required: false) + check_default_type(:date_time, default, DateTime) + text = fetch_value(name, required: required) + dt = parse_date_time_from(text, format: format) + + return dt if dt + return default unless required + fail(InvalidDateTimeText, "Require date_time environment variable #{name} had inappropriate content '#{text}'") + end + private def parse_date_from(text, format:) @@ -20,5 +30,37 @@ def parse_date_from(text, format:) rescue ArgumentError nil end + + def parse_date_time_from(text, format:) + if text.nil? + nil + elsif format == :iso8601 + iso8601_date_time(text) + elsif format == :unix + unix_date_time(text) + elsif format.is_a?(String) + strptime_date_time(text, format: format) + else + fail(BadFormat, "ENV.date_time requires either a strptime format string, :unix, or :iso8601") + end + end + + def iso8601_date_time(text) + DateTime.iso8601(text) + rescue + nil + end + + def unix_date_time(text) + DateTime.strptime(text, "%s") + rescue + nil + end + + def strptime_date_time(text, format:) + DateTime.strptime(text, format) + rescue + nil + end end end diff --git a/lib/environment_helpers/version.rb b/lib/environment_helpers/version.rb index 1fa9e85..b17a57c 100644 --- a/lib/environment_helpers/version.rb +++ b/lib/environment_helpers/version.rb @@ -1,3 +1,3 @@ module EnvironmentHelpers - VERSION = "1.2.1" + VERSION = "1.3.0" end diff --git a/spec/environment_helpers/datetime_helpers_spec.rb b/spec/environment_helpers/datetime_helpers_spec.rb index 42e30d3..7754cb7 100644 --- a/spec/environment_helpers/datetime_helpers_spec.rb +++ b/spec/environment_helpers/datetime_helpers_spec.rb @@ -92,4 +92,115 @@ def self.it_parses_date_as(text:, format:, result:) it_parses_date_as text: "2023-4-12", format: "hello", result: nil end end + + describe "#date_time" do + let(:name) { "FOO" } + let(:options) { {} } + subject(:date_time) { env.date_time(name, **options) } + + context "with required: true" do + let(:options) { {required: true} } + + context "when the environment value is not set" do + before { expect(ENV["FOO"]).to be_nil } + + it "raises a MissingVariableError" do + expect { date_time }.to raise_error( + EnvironmentHelpers::MissingVariableError, + /not supplied/ + ) + end + end + + context "when the environment value is set" do + with_env("FOO" => "2023-04-25T13:44:59.75+07:30") + it { is_expected.to eq(DateTime.new(2023, 4, 25, 13, 44, 59.75, "+7:30")) } + + context "to an invalid value" do + with_env("FOO" => "hello") + + it "raises a MissingVariableError" do + expect { date_time }.to raise_error( + EnvironmentHelpers::InvalidDateTimeText, + /inappropriate content/ + ) + end + end + end + end + + context "with default set" do + let(:options) { {default: DateTime.new(2000, 1, 1, 4, 5, 6, "+7")} } + + context "to a value of the wrong type" do + let(:options) { {default: Date.new(2023, 1, 2)} } + + it "raises a BadDefault error" do + expect { date_time }.to raise_error( + EnvironmentHelpers::BadDefault, + /inappropriate default/i + ) + end + end + + context "when the environment value is not set" do + before { expect(ENV["FOO"]).to be_nil } + it { is_expected.to eq(options[:default]) } + end + + context "when the environment value is set" do + with_env("FOO" => "2023-04-25T14:22:33+02") + it { is_expected.to eq(DateTime.new(2023, 4, 25, 14, 22, 33, "+2")) } + end + end + + context "with default not set" do + before { expect(options).not_to include(:default) } + + context "when the environment value is not set" do + before { expect(ENV["FOO"]).to be_nil } + it { is_expected.to be_nil } + end + + context "when the environment value is set" do + with_env("FOO" => "2023-04-25T03:13:56Z") + it { is_expected.to eq(DateTime.new(2023, 4, 25, 3, 13, 56, "UTC")) } + end + end + + context "with other formats supplied" do + def self.it_parses_datetime_as(text:, format:, result:) + context "for supplied text '#{text}' and format '#{format}'" do + let(:options) { {format: format} } + with_env("FOO" => text) + it { is_expected.to eq(result) } + end + end + + context "for :unix format" do + it_parses_datetime_as text: "1684200709", format: :unix, result: DateTime.new(2023, 5, 16, 1, 31, 49, "UTC") + it_parses_datetime_as text: "hello", format: :unix, result: nil + end + + context "for :iso8601 format" do + it_parses_datetime_as text: "2023-05-15T23:25:24-04:00", format: :iso8601, result: DateTime.new(2023, 5, 15, 23, 25, 24, "-4") + it_parses_datetime_as text: "2023-05-15T23:25:24.75-04:00", format: :iso8601, result: DateTime.new(2023, 5, 15, 23, 25, 24.75, "-4") + it_parses_datetime_as text: "hello", format: :iso8601, result: nil + end + + context "for string formats" do + it_parses_datetime_as text: "2023.05.15.23.25.25", format: "%Y.%m.%d.%H.%M.%S", result: DateTime.new(2023, 5, 15, 23, 25, 25, "UTC") + it_parses_datetime_as text: "hello", format: "%Y.%m.%d.%H.%M.%S", result: nil + end + + context "with a bad format" do + let(:options) { {format: :notreal} } + with_env("FOO" => "2023-05-15T23:25:24-04:00") + + it "raises a BadFormat exception" do + expect { date_time }.to raise_error(EnvironmentHelpers::BadFormat, /date_time requires either/) + end + end + end + end end