diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index bcd017e..f7aaca9 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -9,6 +9,8 @@ jobs: fail-fast: false matrix: ruby-version: ['2.7', '3.0', '3.1', '3.2', 'head'] + env: + NO_STEEP: "Because we need to test on versions that are older than steep supports" steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/type_check.yml b/.github/workflows/type_check.yml new file mode 100644 index 0000000..e15f94f --- /dev/null +++ b/.github/workflows/type_check.yml @@ -0,0 +1,18 @@ +name: Steep Type Checking + +on: push + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.3 + bundler-cache: true + + - name: Run Steep + run: bundle exec steep check diff --git a/Gemfile b/Gemfile index b4e2a20..026f34f 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,7 @@ source "https://rubygems.org" gemspec + +unless ENV["NO_STEEP"] + gem "steep", "~> 1.6.0" +end diff --git a/Steepfile b/Steepfile new file mode 100644 index 0000000..a9bffad --- /dev/null +++ b/Steepfile @@ -0,0 +1,17 @@ +D = Steep::Diagnostic + +target :lib do + signature "sig" + check "lib" + configure_code_diagnostics(D::Ruby.default) + library "pathname" + library "date" +end + +target :test do + signature "sig" + check "test" + configure_code_diagnostics(D::Ruby.default) + library "pathname" + library "date" +end diff --git a/lib/environment_helpers.rb b/lib/environment_helpers.rb index 35d6683..a0f47af 100644 --- a/lib/environment_helpers.rb +++ b/lib/environment_helpers.rb @@ -2,14 +2,14 @@ # And we're compatible back to 2.6 require "set" # rubocop:disable Lint/RedundantRequireStatement -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/file_helpers" -require_relative "./environment_helpers/datetime_helpers" -require_relative "./environment_helpers/enumerable_helpers" +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/file_helpers" +require_relative "environment_helpers/datetime_helpers" +require_relative "environment_helpers/enumerable_helpers" module EnvironmentHelpers Error = Class.new(::StandardError) diff --git a/lib/environment_helpers/enumerable_helpers.rb b/lib/environment_helpers/enumerable_helpers.rb index 4458fa4..1562802 100644 --- a/lib/environment_helpers/enumerable_helpers.rb +++ b/lib/environment_helpers/enumerable_helpers.rb @@ -2,6 +2,18 @@ module EnvironmentHelpers module EnumerableHelpers VALID_TYPES = %i[strings symbols integers] + TYPE_HANDLERS = { + integers: :to_i, + strings: :to_s, + symbols: :to_sym + } + + TYPE_MAP = { + integers: Integer, + strings: String, + symbols: Symbol + } + def array(key, of: :strings, delimiter: ",", default: nil, required: false) check_default_type(:array, default, Array) check_valid_data_type!(of) @@ -22,23 +34,12 @@ def check_valid_data_type!(type) end def check_default_data_types!(default, type) + return if default.nil? invalid = Array(default).reject { |val| val.is_a? TYPE_MAP[type] } unless invalid.empty? fail(BadDefault, "Default array contains values not of type `#{type}': #{invalid.join(", ")}") end end - - TYPE_HANDLERS = { - integers: :to_i, - strings: :to_s, - symbols: :to_sym - } - - TYPE_MAP = { - integers: Integer, - strings: String, - symbols: Symbol - } end end diff --git a/lib/environment_helpers/range_helpers.rb b/lib/environment_helpers/range_helpers.rb index 311c535..2019b3b 100644 --- a/lib/environment_helpers/range_helpers.rb +++ b/lib/environment_helpers/range_helpers.rb @@ -6,7 +6,7 @@ def integer_range(name, default: nil, required: false) check_range_endpoint(:integer_range, default.end) if default text = fetch_value(name, required: required) - range = parse_range_from(text) + range = text ? parse_range_from(text) : nil return range if range return default unless required fail(InvalidRangeText, "Required Integer Range environment variable #{name} had inappropriate content '#{text}'") diff --git a/lib/environment_helpers/string_helpers.rb b/lib/environment_helpers/string_helpers.rb index c4f9d8f..8f94a2b 100644 --- a/lib/environment_helpers/string_helpers.rb +++ b/lib/environment_helpers/string_helpers.rb @@ -6,7 +6,7 @@ def string(name, default: nil, required: false) def symbol(name, default: nil, required: false) check_default_type(:symbol, default, Symbol) - string(name, default: default, required: required)&.to_sym + string(name, default: default&.to_s, required: required)&.to_sym end end end diff --git a/sig/environment_helpers.rbs b/sig/environment_helpers.rbs new file mode 100644 index 0000000..e98fccb --- /dev/null +++ b/sig/environment_helpers.rbs @@ -0,0 +1,96 @@ +module EnvironmentHelpers + VERSION: String + + class Error < StandardError + end + + class MissingVariableError < Error + end + + class BadDefault < Error + end + + class BadFormat < Error + end + + class InvalidType < Error + end + + class InvalidValue < Error + end + + class InvalidBooleanText < InvalidValue + end + + class InvalidRangeText < InvalidValue + end + + class InvalidIntegerText < InvalidValue + end + + class InvalidDateText < InvalidValue + end + + class InvalidDateTimeText < InvalidValue + end + + module AccessHelpers + # (Actually provided by ENV, which these are all extended onto) + def fetch: (String name) -> String? + | (String name, String? default) -> String? + + private def fetch_value: (String name, required: bool) -> String? + private def check_default_type: (String | Symbol context, untyped value, *Class types) -> void + private def check_default_value: (String | Symbol context, untyped value, allow: Enumerable[untyped]) -> void + end + + module FileHelpers : AccessHelpers + def file_path: (String name, default: String?, required: bool) -> Pathname? + end + + module DatetimeHelpers : AccessHelpers + type date_time_result = DateTime? | Time? + def date: (String name, format: String, default: Date?, required: bool) -> Date? + def date_time: (String name, format: String | Symbol, default: date_time_result, required: bool) -> date_time_result + private def parse_date_from: (String? text, format: String) -> Date? + private def parse_date_time_from: (String? text, format: String | Symbol) -> DateTime? + private def iso8601_date_time: (String) -> DateTime? + private def unix_date_time: (String) -> DateTime? + private def strptime_date_time: (String, format: String) -> DateTime? + end + + module NumericHelpers : AccessHelpers + def integer: (String name, default: Integer?, required: bool) -> Integer? + end + + module RangeHelpers : AccessHelpers + def integer_range: (String name, default: Range[Integer]?, required: bool) -> Range[Integer]? + private def check_range_endpoint: (String | Symbol context, untyped value) -> void + private def parse_range_bound_from: (String?) -> Integer? + private def parse_range_from: (String) -> Range[Integer]? + end + + module BooleanHelpers : AccessHelpers + TRUTHY_STRINGS: Set[String] + FALSEY_STRINGS: Set[String] + BOOLEAN_VALUES: Set[bool] + def boolean: (String name, default: bool?, required: bool) -> boolish + private def truthy_text?: (String?) -> boolish + private def falsey_text?: (String?) -> boolish + end + + module EnumerableHelpers : AccessHelpers + VALID_TYPES: Array[Symbol] + TYPE_HANDLERS: Hash[Symbol, Symbol] + TYPE_MAP: Hash[Symbol, Class] + type arrayable = Integer | String | Symbol + def array: (String key, of: Symbol, delimiter: String, default: Array[arrayable]?, required: bool) -> Array[arrayable]? + private def check_valid_data_type!: (Symbol) -> void + private def check_default_data_types!: (Array[arrayable]?, Symbol) -> void + end + + module StringHelpers : AccessHelpers + def string: (String name, default: String?, required: bool) -> String? + def symbol: (String name, default: Symbol?, required: bool) -> Symbol? + end +end diff --git a/spec/environment_helpers/enumerable_helpers_spec.rb b/spec/environment_helpers/enumerable_helpers_spec.rb index 72ae080..2e6a620 100644 --- a/spec/environment_helpers/enumerable_helpers_spec.rb +++ b/spec/environment_helpers/enumerable_helpers_spec.rb @@ -7,6 +7,12 @@ subject(:array) { env.array(env_var, **params) } with_env "FOO" => "a,bc,d" + without_env "FOOBAR" + + context "when the value is not present" do + let(:env_var) { "FOOBAR" } + it { should be_nil } + end describe "parameters" do context "when passed an `of' param" do diff --git a/spec/environment_helpers/string_helpers_spec.rb b/spec/environment_helpers/string_helpers_spec.rb index b6122a5..b61b1c8 100644 --- a/spec/environment_helpers/string_helpers_spec.rb +++ b/spec/environment_helpers/string_helpers_spec.rb @@ -79,7 +79,7 @@ end context "when the env value is not set" do - before { expect(ENV["FOO"]).to be_nil } + without_env "FOO" it { is_expected.to be_nil } end end diff --git a/spec/support/with_environment.rb b/spec/support/with_environment.rb index c4058c8..e7b8487 100644 --- a/spec/support/with_environment.rb +++ b/spec/support/with_environment.rb @@ -10,6 +10,18 @@ def with_env(overrides) end end end + + def without_env(*keys) + around(:each) do |ex| + orig_env = ENV.to_h + keys.each { |key| ENV.delete(key) } + begin + ex.run + ensure + ENV.replace(orig_env) + end + end + end end RSpec.configure do |config|