diff --git a/README.md b/README.md index 924294a..84e6cff 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ methods onto `ENV` for your use. ENV.string("APP_NAME", default: "local") ENV.symbol("BUSINESS_DOMAIN", default: :engineering, required: true) ENV.boolean("ENABLE_FEATURE_FOO", default: false) +ENV.integer_range("ID_RANGE", default: (500..6000)) ENV.integer("MAX_THREAD_COUNT", default: 5) ENV.date("SCHEDULED_DATE", required: true, format: "%Y-%m-%d") ``` @@ -51,6 +52,8 @@ The available methods added to `ENV`: a fair variety of strings to map onto those boolean value, though you should probably just use "true" and "false" really. If you specify `required: true` and get a value like "maybe?", it'll raise an `EnvironmentHelpers::InvalidBooleanText` exception. +* `integer_range` - produces an integer Range object. It accepts `N-N`, `N..N`, or `N...N`, (the + latter means 'excluding the upper bound, as in ruby). * `integer` - produces an integer from the environment variable, by calling `to_i` on it (if it's present). Note that this means that providing a value like "hello" means you'll get `0`, since that's what ruby does when you call `"hello".to_i`. diff --git a/lib/environment_helpers.rb b/lib/environment_helpers.rb index 6876ab5..1a9c10d 100644 --- a/lib/environment_helpers.rb +++ b/lib/environment_helpers.rb @@ -1,6 +1,7 @@ require_relative "./environment_helpers/access_helpers" require_relative "./environment_helpers/string_helpers" require_relative "./environment_helpers/boolean_helpers" +require_relative "./environment_helpers/range_helpers" require_relative "./environment_helpers/numeric_helpers" require_relative "./environment_helpers/datetime_helpers" @@ -11,12 +12,14 @@ module EnvironmentHelpers InvalidValue = Class.new(Error) InvalidBooleanText = Class.new(InvalidValue) + InvalidRangeText = Class.new(InvalidValue) InvalidIntegerText = Class.new(InvalidValue) InvalidDateText = Class.new(InvalidValue) include AccessHelpers include StringHelpers include BooleanHelpers + include RangeHelpers include NumericHelpers include DatetimeHelpers end diff --git a/lib/environment_helpers/range_helpers.rb b/lib/environment_helpers/range_helpers.rb new file mode 100644 index 0000000..311c535 --- /dev/null +++ b/lib/environment_helpers/range_helpers.rb @@ -0,0 +1,42 @@ +module EnvironmentHelpers + module RangeHelpers + def integer_range(name, default: nil, required: false) + check_default_type(:integer_range, default, Range) + check_range_endpoint(:integer_range, default.begin) if default + check_range_endpoint(:integer_range, default.end) if default + + text = fetch_value(name, required: required) + range = parse_range_from(text) + return range if range + return default unless required + fail(InvalidRangeText, "Required Integer Range environment variable #{name} had inappropriate content '#{text}'") + end + + private + + def check_range_endpoint(context, value) + return if value.is_a?(Integer) + fail(BadDefault, "Invalid endpoint for default range of #{context} - must be Integer") + end + + def parse_range_bound_from(text) + return nil if text.nil? + return nil if text.empty? + text.to_i + end + + def parse_range_from(text) + text =~ /\A(\d*)(-|\.\.|\.\.\.)(\d*)\z/ + lower_bound = parse_range_bound_from($1) + separator = $2 + upper_bound = parse_range_bound_from($3) + + return nil if lower_bound.nil? || upper_bound.nil? + if separator == "..." + (lower_bound...upper_bound) + else + (lower_bound..upper_bound) + end + end + end +end diff --git a/lib/environment_helpers/version.rb b/lib/environment_helpers/version.rb index 647ff1c..5d887eb 100644 --- a/lib/environment_helpers/version.rb +++ b/lib/environment_helpers/version.rb @@ -1,3 +1,3 @@ module EnvironmentHelpers - VERSION = "1.0.1" + VERSION = "1.1.0" end diff --git a/spec/environment_helpers/range_helpers_spec.rb b/spec/environment_helpers/range_helpers_spec.rb new file mode 100644 index 0000000..2d96be5 --- /dev/null +++ b/spec/environment_helpers/range_helpers_spec.rb @@ -0,0 +1,118 @@ +RSpec.describe EnvironmentHelpers::RangeHelpers do + subject(:env) { ENV } + + describe "#integer_range" do + let(:name) { "FOO" } + let(:options) { {} } + subject(:integer_range) { ENV.integer_range(name, **options) } + + context "when required: true" do + let(:options) { {required: true} } + + context "and the key is supplied" do + with_env("FOO" => "52-63") + it { is_expected.to eq((52..63)) } + + context "but has invalid content" do + with_env("FOO" => "hello") + + it "raises a MissingVariableError" do + expect { integer_range }.to raise_error( + EnvironmentHelpers::InvalidRangeText, + /inappropriate content/ + ) + end + end + end + + context "and the environment variable is not supplied" do + before { expect(ENV["FOO"]).to be_nil } + + it "raises a MissingVariableError" do + expect { integer_range }.to raise_error( + EnvironmentHelpers::MissingVariableError, + /not supplied/ + ) + end + end + end + + context "without a default specified" do + let(:options) { {} } + + context "when the environment variable is present" do + with_env("FOO" => "58..61") + it { is_expected.to eq((58..61)) } + end + + context "when the environment variable is not present" do + before { expect(ENV["FOO"]).to be_nil } + it { is_expected.to be_nil } + end + end + + context "with a default specified" do + let(:options) { {default: (91..93)} } + + context "but of the wrong type" do + let(:options) { {default: "91"} } + + it "raises a BadDefault error" do + expect { integer_range }.to raise_error( + EnvironmentHelpers::BadDefault, + /inappropriate default/i + ) + end + end + + context "with the wrong type of endpoint" do + let(:options) { {default: ("a".."c")} } + + it "raises a BadDefault error" do + expect { integer_range }.to raise_error( + EnvironmentHelpers::BadDefault, + /invalid endpoint for default range/i + ) + end + end + + context "when the environment variable is present" do + with_env("FOO" => "58..62") + it { is_expected.to eq((58..62)) } + + context "but not actually an integer" do + with_env("FOO" => "hello") + it { is_expected.to eq((91..93)) } + end + end + + context "when the environment variable is not present" do + before { expect(ENV["FOO"]).to be_nil } + it { is_expected.to eq((91..93)) } + end + end + + context "for various formats" do + context "with a dash" do + with_env("FOO" => "3-5") + it { is_expected.to eq((3..5)) } + end + + context "with two dots" do + with_env("FOO" => "3..5") + it { is_expected.to eq((3..5)) } + end + + context "with three dots" do + with_env("FOO" => "3...5") + it { is_expected.to eq((3...5)) } + it { is_expected.not_to be_cover(5) } + end + + context "with missing bound" do + with_env("FOO" => "3..") + it { is_expected.to be_nil } + end + end + end +end