diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..cd17261 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,54 @@ +env: + RUBY_VERSION: 3.2.2 + +name: RichEnums CI +on: + pull_request: + paths-ignore: + - .gitignore + - CHANGELOG.md + - CONTRIBUTING.md + - README.md + push: + paths-ignore: + - .gitignore + - CHANGELOG.md + - CONTRIBUTING.md + - README.md + +jobs: + rspec-test: + name: Run tests (ruby ${{ matrix.ruby }}, rails ${{ matrix.rails }}) + runs-on: ubuntu-22.04 + strategy: + matrix: + include: + - ruby: '3.2' + rails: '7.1' + - ruby: '3.2' + rails: '7.0' + - ruby: '3.2' + rails: '6.1' + - ruby: '3.1' + rails: '6.1' + env: + BUNDLE_GEMFILE: gemfiles/activerecord_${{ matrix.rails }}.gemfile + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + - name: Gem cache + uses: actions/cache@v4 + with: + path: vendor/bundle + key: ${{ runner.os }}-gem-use-ruby-${{ hashFiles('**/Gemfile.lock') }} + - name: Install dependencies + run: | + gem install bundler --version 2.3.25 --no-document + bundle config path vendor/bundle + bundle install --jobs 4 --retry 3 + - name: Run tests + run: bundle exec rake spec diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..94ff29c --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.1.1 diff --git a/Gemfile.lock b/Gemfile.lock index 245890e..eb05564 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,12 +1,29 @@ PATH remote: . specs: - rich_enums (0.1.3) + rich_enums (0.1.4) + activerecord (>= 6.1, < 8.0) GEM remote: https://rubygems.org/ specs: + activemodel (6.1.7.7) + activesupport (= 6.1.7.7) + activerecord (6.1.7.7) + activemodel (= 6.1.7.7) + activesupport (= 6.1.7.7) + activesupport (6.1.7.7) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + zeitwerk (~> 2.3) + concurrent-ruby (1.2.3) diff-lcs (1.5.0) + i18n (1.14.4) + concurrent-ruby (~> 1.0) + mini_portile2 (2.8.6) + minitest (5.22.3) rake (12.3.3) rspec (3.11.0) rspec-core (~> 3.11.0) @@ -21,6 +38,14 @@ GEM diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.11.0) rspec-support (3.11.0) + sqlite3 (1.7.3) + mini_portile2 (~> 2.8.0) + temping (4.1.1) + activerecord (>= 6.0, < 7.2) + activesupport (>= 6.0, < 7.2) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + zeitwerk (2.6.13) PLATFORMS ruby @@ -29,6 +54,8 @@ DEPENDENCIES rake (~> 12.0) rich_enums! rspec (~> 3.0) + sqlite3 (~> 1.4) + temping (~> 4.1) BUNDLED WITH 2.4.15 diff --git a/gemfiles/activerecord_6.1.gemfile b/gemfiles/activerecord_6.1.gemfile new file mode 100644 index 0000000..511e155 --- /dev/null +++ b/gemfiles/activerecord_6.1.gemfile @@ -0,0 +1,8 @@ +source "https://rubygems.org" + +gem "activerecord", "~> 6.1" +gem "rake", "~> 12.0" +gem "rspec", "~> 3.0" + +# Specify your gem's dependencies in rich_enums.gemspec +gemspec path: "../" diff --git a/gemfiles/activerecord_7.0.gemfile b/gemfiles/activerecord_7.0.gemfile new file mode 100644 index 0000000..d4bed58 --- /dev/null +++ b/gemfiles/activerecord_7.0.gemfile @@ -0,0 +1,8 @@ +source "https://rubygems.org" + +gem "activerecord", "~> 7.0" +gem "rake", "~> 12.0" +gem "rspec", "~> 3.0" + +# Specify your gem's dependencies in rich_enums.gemspec +gemspec path: "../" diff --git a/gemfiles/activerecord_7.1.gemfile b/gemfiles/activerecord_7.1.gemfile new file mode 100644 index 0000000..8cdffb0 --- /dev/null +++ b/gemfiles/activerecord_7.1.gemfile @@ -0,0 +1,8 @@ +source "https://rubygems.org" + +gem "activerecord", "~> 7.1" +gem "rake", "~> 12.0" +gem "rspec", "~> 3.0" + +# Specify your gem's dependencies in rich_enums.gemspec +gemspec path: "../" diff --git a/lib/rich_enums.rb b/lib/rich_enums.rb index e577e4d..917ac2d 100644 --- a/lib/rich_enums.rb +++ b/lib/rich_enums.rb @@ -38,22 +38,30 @@ def rich_enum(column_symbol_value_string_options) # e.learner_payment_path_name --> "P.O. / Check" -> our custom method that returns the string/description # TODO: explore if enum options in Array format instead of Hash format will need to be handled - raise 'rich_enum error' unless column_symbol_value_string_options.keys.count.positive? + unless column_symbol_value_string_options.is_a? Hash + raise RichEnums::Error + end + if column_symbol_value_string_options.keys.count.zero? + raise RichEnums::Error + end # extract out the column column = column_symbol_value_string_options.keys.first # extract the Enum options for the column which may be in standard enum hash format or our custom format symbol_value_string = column_symbol_value_string_options.delete(column) + + raise RichEnums::Error unless symbol_value_string.is_a? Hash + # at this point, only the enum options like _prefix etc. are present in the original argument options = column_symbol_value_string_options # we allow for an option called alt: to allow the users to tag the alternate mapping. Defaults to 'alt_name' - alt = options.delete(:alt) || 'alt_name' + alt = options.delete(:alt).to_s || 'alt_name' # create two hashes from the provided input - 1 to be used to define the enum and the other for the name map split_hash = symbol_value_string.each_with_object({ for_enum: {}, for_display: {} }) do |(symbol, value_string), obj| - obj[:for_enum][symbol] = value_string.is_a?(Array) ? value_string.first : value_string - obj[:for_display][symbol.to_s] = value_string.is_a?(Array) ? value_string.second : symbol.to_s + obj[:for_enum][symbol] = value_string.is_a?(Array) ? value_string[0] : value_string + obj[:for_display][symbol.to_s] = value_string.is_a?(Array) ? value_string[1] : symbol.to_s end # 1. Define the Enum diff --git a/lib/rich_enums/version.rb b/lib/rich_enums/version.rb index f79005b..1975db6 100644 --- a/lib/rich_enums/version.rb +++ b/lib/rich_enums/version.rb @@ -1,3 +1,3 @@ module RichEnums - VERSION = '0.1.3' + VERSION = '0.1.4' end diff --git a/rich_enums.gemspec b/rich_enums.gemspec index cd28b66..cc1b4dc 100644 --- a/rich_enums.gemspec +++ b/rich_enums.gemspec @@ -22,6 +22,9 @@ DESC spec.metadata['source_code_uri'] = 'https://github.com/betacraft/rich_enums' spec.metadata['changelog_uri'] = 'https://github.com/betacraft/rich_enums/README.md' + spec.add_development_dependency 'sqlite3', '~> 1.4' + spec.add_development_dependency 'temping', '~> 4.1' + spec.add_runtime_dependency 'activerecord', '>= 6.1', '< 8.0' # Specify which files should be added to the gem when it is released. # The `git ls-files -z` loads the files in the RubyGem that have been added into git. spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do diff --git a/spec/rich_enums_spec.rb b/spec/rich_enums_spec.rb index 8b7cbcd..37ba0ae 100644 --- a/spec/rich_enums_spec.rb +++ b/spec/rich_enums_spec.rb @@ -1,10 +1,149 @@ +require "spec_helper" + RSpec.describe RichEnums do - it "has a version number" do - expect(RichEnums::VERSION).not_to be nil + describe "error scenarios" do + it "raises an error if the argument to rich_enum is not a Hash" do + expect do + Temping.create(:course_class) do + include RichEnums + rich_enum([]) + end + end.to raise_error(RichEnums::Error) + end + + it "raises an error if an empty Hash is passed to rich_enum" do + expect do + Temping.create(:course_class) do + include RichEnums + rich_enum({}) + end + end.to raise_error(RichEnums::Error) + end + + it "raises an error if the enum definition uses the Array form" do + # This is not supported yet + expect do + Temping.create(:course_class) do + include RichEnums + rich_enum({status: [:active, :inactive]}) + end + end.to raise_error(RichEnums::Error) + end end - it "does something useful" do - # TODO write tests - expect(false).to eq(true) + describe "rich_enum usage" do + context "it calls the ActiveRecord::Enum.enum method" do + let(:course_class) do + Temping.create(:course_class) do + with_columns do |t| + t.integer :status + t.string :category + end + + include RichEnums + end + end + + it "invokes the enum method with the correct arguments" do + allow(course_class).to receive(:enum).and_call_original + + course_class.rich_enum status: { active: 0, inactive: 1 }, alt: :name + # it passes only the necessary arguments to the enum method, stripping out the alt: option + expect(course_class).to have_received(:enum).with(status: { active: 0, inactive: 1 }) + end + + it "invokes the enum method with the correct arguments" do + allow(course_class).to receive(:enum).and_call_original + + course_class.rich_enum status: { active: [0, 'LIVE'], inactive: [1, 'NOT_LIVE'] }, alt: :name + # it passes only the necessary arguments to the enum method, + # stripping out the alternate names LIVE and NOT_LIVE and the alt: option + expect(course_class).to have_received(:enum).with(status: { active: 0, inactive: 1 }) + end + + it "invokes the enum method with the correct arguments" do + allow(course_class).to receive(:enum).and_call_original + course_class.rich_enum status: { active: [0, 'LIVE'], inactive: [1, 'NOT_LIVE'] }, _prefix: true, alt: 'state' + # it passes only the necessary arguments to the enum method, + # stripping out the alternate names LIVE and NOT_LIVE and the alt: option + expect(course_class).to have_received(:enum).with(status: { active: 0, inactive: 1 }, _prefix: true) + end + end + + context "with only an alternate name specified without additional mapping" do + let(:course_class) do + Temping.create(:course_class) do + with_columns do |t| + t.integer :status + t.string :category + end + + include RichEnums + rich_enum status: { active: 0, inactive: 1 }, alt: :name + end + end + let(:test_instance) { course_class.new } + + it "defines a class method for each enum" do + expect(course_class).to respond_to(:status_names) + end + + it "returns a hash of enum values and names" do + expect(course_class.status_names).to eq({"active"=>"active", "inactive"=>"inactive"}) + end + + it "defines an instance method for each enum" do + expect(test_instance).to respond_to(:status_name) + end + + it "returns the value in response to alternate name" do + test_instance.status = 0 + expect(test_instance.status_name).to eq(test_instance.status) + test_instance.status = 1 + expect(test_instance.status_name).to eq(test_instance.status) + test_instance.status = :active + expect(test_instance.status_name).to eq(test_instance.status) + test_instance.status = 'inactive' + expect(test_instance.status_name).to eq(test_instance.status) + end + end + + context "with an alternate name and mapping specified" do + let(:course_class) do + Temping.create(:course_class) do + with_columns do |t| + t.string :status + end + + include RichEnums + rich_enum status: { active: [0, 'LIVE'], inactive: [1, 'NOT_LIVE'] }, alt: :name + end + end + + let(:test_instance) { course_class.new } + + it "defines a class method for each enum" do + expect(course_class).to respond_to(:status_names) + end + + it "returns a hash of enum values and names" do + expect(course_class.status_names).to eq({"active"=>"LIVE", "inactive"=>"NOT_LIVE"}) + end + + it "defines an instance method for each enum" do + expect(test_instance).to respond_to(:status_name) + end + + it "returns the alternate name of the enum value" do + test_instance.status = 0 + expect(test_instance.status_name).to eq("LIVE") + test_instance.status = 1 + expect(test_instance.status_name).to eq("NOT_LIVE") + test_instance.status = :active + expect(test_instance.status_name).to eq("LIVE") + test_instance.status = 'inactive' + expect(test_instance.status_name).to eq("NOT_LIVE") + end + end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ccf0686..2a61bd0 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,5 +1,6 @@ require "bundler/setup" require "rich_enums" +require "temping" RSpec.configure do |config| # Enable flags like --only-failures and --next-failure @@ -11,4 +12,15 @@ config.expect_with :rspec do |c| c.syntax = :expect end + + config.before(:all) do + ActiveRecord::Base.establish_connection( + adapter: "sqlite3", + database: ":memory:" + ) + end + + config.after do + Temping.teardown + end end