From f810ed33b588d9bf8ae6f9bf3676ed88c8501443 Mon Sep 17 00:00:00 2001 From: Eric Mueller Date: Tue, 25 Apr 2023 12:49:36 -0400 Subject: [PATCH 1/8] implement ENV.integer_range --- lib/environment_helpers.rb | 3 + lib/environment_helpers/range_helpers.rb | 46 ++++++++ .../environment_helpers/range_helpers_spec.rb | 110 ++++++++++++++++++ 3 files changed, 159 insertions(+) create mode 100644 lib/environment_helpers/range_helpers.rb create mode 100644 spec/environment_helpers/range_helpers_spec.rb 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..fe592e8 --- /dev/null +++ b/lib/environment_helpers/range_helpers.rb @@ -0,0 +1,46 @@ +module EnvironmentHelpers + module RangeHelpers + def integer_range(name, default: nil, required: false) + check_default_type(:integer, default, Range) + check_range_endpoint(default.begin) if default + check_range_endpoint(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 contenxt '#{text}'") + end + + private + + def check_range_endpoint(value) + return if value.nil? || value.is_a?(Integer) + fail(BadDefault, "Invalid endpoint for default range of #{context} - must be Integer or nil") + end + + def convert_range_endpoint(value) + return nil if value.empty + 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/spec/environment_helpers/range_helpers_spec.rb b/spec/environment_helpers/range_helpers_spec.rb new file mode 100644 index 0000000..7d8c8ac --- /dev/null +++ b/spec/environment_helpers/range_helpers_spec.rb @@ -0,0 +1,110 @@ +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)) } + 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 "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 the first value missing" do + with_env("FOO" => "..8") + it { is_expected.to eq((..8)) } + it { is_expected.to be_cover(-3) } + it { is_expected.not_to be_cover(9) } + end + + context "with the second value missing" do + with_env("FOO" => "8..") + it { is_expected.to eq((8..)) } + it { is_expected.to be_cover(8) } + it { is_expected.not_to be_cover(7) } + end + + context "with _both_ values missing" do + with_env("FOO" => "...") + it { is_expected.to be_nil } + end + end + end +end From a35f34ae89ae3c05153c24e4a7d31aba12fc6c4f Mon Sep 17 00:00:00 2001 From: Eric Mueller Date: Tue, 25 Apr 2023 12:51:48 -0400 Subject: [PATCH 2/8] explain integer_range in the README --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 924294a..74232d5 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..)) ENV.integer("MAX_THREAD_COUNT", default: 5) ENV.date("SCHEDULED_DATE", required: true, format: "%Y-%m-%d") ``` @@ -51,6 +52,9 @@ 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). It supports `N..` and `..N`, in rubies that + allow such Ranges. * `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`. From 2318c3d4774404b7dc419bf9f469b67006149ffa Mon Sep 17 00:00:00 2001 From: Eric Mueller Date: Tue, 25 Apr 2023 12:54:42 -0400 Subject: [PATCH 3/8] bump to 1.1.0 --- lib/environment_helpers/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 54189eac4ca6ebce52628535b9f3760480aa358f Mon Sep 17 00:00:00 2001 From: Eric Mueller Date: Tue, 25 Apr 2023 13:06:20 -0400 Subject: [PATCH 4/8] don't use endless range literals in the specs --- spec/environment_helpers/range_helpers_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/environment_helpers/range_helpers_spec.rb b/spec/environment_helpers/range_helpers_spec.rb index 7d8c8ac..83afd4b 100644 --- a/spec/environment_helpers/range_helpers_spec.rb +++ b/spec/environment_helpers/range_helpers_spec.rb @@ -89,14 +89,14 @@ context "with the first value missing" do with_env("FOO" => "..8") - it { is_expected.to eq((..8)) } + it { is_expected.to eq((nil..8)) } it { is_expected.to be_cover(-3) } it { is_expected.not_to be_cover(9) } end context "with the second value missing" do with_env("FOO" => "8..") - it { is_expected.to eq((8..)) } + it { is_expected.to eq((8..nil)) } it { is_expected.to be_cover(8) } it { is_expected.not_to be_cover(7) } end From 27c6d6baf79be8a0186d16ab90b2408077137f71 Mon Sep 17 00:00:00 2001 From: Eric Mueller Date: Tue, 25 Apr 2023 13:14:59 -0400 Subject: [PATCH 5/8] get full coverage (and pass it) --- lib/environment_helpers/range_helpers.rb | 14 +++++------- .../environment_helpers/range_helpers_spec.rb | 22 +++++++++++++++++++ 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/lib/environment_helpers/range_helpers.rb b/lib/environment_helpers/range_helpers.rb index fe592e8..3706186 100644 --- a/lib/environment_helpers/range_helpers.rb +++ b/lib/environment_helpers/range_helpers.rb @@ -1,28 +1,24 @@ module EnvironmentHelpers module RangeHelpers def integer_range(name, default: nil, required: false) - check_default_type(:integer, default, Range) - check_range_endpoint(default.begin) if default - check_range_endpoint(default.end) if default + 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 contenxt '#{text}'") + fail(InvalidRangeText, "Required Integer Range environment variable #{name} had inappropriate content '#{text}'") end private - def check_range_endpoint(value) + def check_range_endpoint(context, value) return if value.nil? || value.is_a?(Integer) fail(BadDefault, "Invalid endpoint for default range of #{context} - must be Integer or nil") end - def convert_range_endpoint(value) - return nil if value.empty - end - def parse_range_bound_from(text) return nil if text.nil? return nil if text.empty? diff --git a/spec/environment_helpers/range_helpers_spec.rb b/spec/environment_helpers/range_helpers_spec.rb index 83afd4b..0813f28 100644 --- a/spec/environment_helpers/range_helpers_spec.rb +++ b/spec/environment_helpers/range_helpers_spec.rb @@ -12,6 +12,17 @@ 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 @@ -54,6 +65,17 @@ 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)) } From d9297d0a24f7df27285ed08069b6fcb00abacb14 Mon Sep 17 00:00:00 2001 From: Eric Mueller Date: Tue, 25 Apr 2023 13:20:17 -0400 Subject: [PATCH 6/8] skip the first-value-missing specs for ruby 2.6 and lower --- spec/environment_helpers/range_helpers_spec.rb | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/spec/environment_helpers/range_helpers_spec.rb b/spec/environment_helpers/range_helpers_spec.rb index 0813f28..d8b93fb 100644 --- a/spec/environment_helpers/range_helpers_spec.rb +++ b/spec/environment_helpers/range_helpers_spec.rb @@ -109,11 +109,14 @@ it { is_expected.not_to be_cover(5) } end - context "with the first value missing" do - with_env("FOO" => "..8") - it { is_expected.to eq((nil..8)) } - it { is_expected.to be_cover(-3) } - it { is_expected.not_to be_cover(9) } + # ruby 2.6 doesn't support ranges with no lower bound + if RUBY_VERSION >= "2.7" + context "with the first value missing" do + with_env("FOO" => "..8") + it { is_expected.to eq((nil..8)) } + it { is_expected.to be_cover(-3) } + it { is_expected.not_to be_cover(9) } + end end context "with the second value missing" do From 38a6ed10bd28deaf567a1aa5ddab1356d5b7fbab Mon Sep 17 00:00:00 2001 From: Eric Mueller Date: Tue, 25 Apr 2023 13:24:08 -0400 Subject: [PATCH 7/8] drop support for ranges with missing/nil bounds - we haven't seen a need and since 2.6 doesn't support some of these, it made things oddly complex --- README.md | 5 ++--- lib/environment_helpers/range_helpers.rb | 6 ++--- .../environment_helpers/range_helpers_spec.rb | 22 ------------------- 3 files changed, 5 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 74232d5..84e6cff 100644 --- a/README.md +++ b/README.md @@ -34,7 +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..)) +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") ``` @@ -53,8 +53,7 @@ The available methods added to `ENV`: "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). It supports `N..` and `..N`, in rubies that - allow such Ranges. + 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/range_helpers.rb b/lib/environment_helpers/range_helpers.rb index 3706186..311c535 100644 --- a/lib/environment_helpers/range_helpers.rb +++ b/lib/environment_helpers/range_helpers.rb @@ -15,8 +15,8 @@ def integer_range(name, default: nil, required: false) private def check_range_endpoint(context, value) - return if value.nil? || value.is_a?(Integer) - fail(BadDefault, "Invalid endpoint for default range of #{context} - must be Integer or nil") + 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) @@ -31,7 +31,7 @@ def parse_range_from(text) separator = $2 upper_bound = parse_range_bound_from($3) - return nil if lower_bound.nil? && upper_bound.nil? + return nil if lower_bound.nil? || upper_bound.nil? if separator == "..." (lower_bound...upper_bound) else diff --git a/spec/environment_helpers/range_helpers_spec.rb b/spec/environment_helpers/range_helpers_spec.rb index d8b93fb..b514eac 100644 --- a/spec/environment_helpers/range_helpers_spec.rb +++ b/spec/environment_helpers/range_helpers_spec.rb @@ -108,28 +108,6 @@ it { is_expected.to eq((3...5)) } it { is_expected.not_to be_cover(5) } end - - # ruby 2.6 doesn't support ranges with no lower bound - if RUBY_VERSION >= "2.7" - context "with the first value missing" do - with_env("FOO" => "..8") - it { is_expected.to eq((nil..8)) } - it { is_expected.to be_cover(-3) } - it { is_expected.not_to be_cover(9) } - end - end - - context "with the second value missing" do - with_env("FOO" => "8..") - it { is_expected.to eq((8..nil)) } - it { is_expected.to be_cover(8) } - it { is_expected.not_to be_cover(7) } - end - - context "with _both_ values missing" do - with_env("FOO" => "...") - it { is_expected.to be_nil } - end end end end From a574156b58386f2f73d5b27f4cf131145d0d4dd3 Mon Sep 17 00:00:00 2001 From: Eric Mueller Date: Tue, 25 Apr 2023 13:26:00 -0400 Subject: [PATCH 8/8] get full coverage again --- spec/environment_helpers/range_helpers_spec.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/spec/environment_helpers/range_helpers_spec.rb b/spec/environment_helpers/range_helpers_spec.rb index b514eac..2d96be5 100644 --- a/spec/environment_helpers/range_helpers_spec.rb +++ b/spec/environment_helpers/range_helpers_spec.rb @@ -108,6 +108,11 @@ 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