Skip to content

Commit

Permalink
Merge pull request #22 from GabrielNagy/values-with-indexes
Browse files Browse the repository at this point in the history
Allow initializing enummer with hash
  • Loading branch information
shkm authored Sep 12, 2024
2 parents 6fd34bf + b051e9e commit 6a5155f
Show file tree
Hide file tree
Showing 13 changed files with 69 additions and 38 deletions.
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ group :test do
gem "sqlite3", "~> 1.4"
gem "mysql2"
gem "simplecov", require: false
gem 'simplecov-cobertura', require: false
gem "simplecov-cobertura", require: false
end
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@ Now set up enummer with the available values in your model:
enummer permissions: %i[read write execute]
```

Similar to `enum`, enummer can also be initialized with a hash, where the numeric index represents the position of the bit that maps to the flag:

``` ruby
enummer permissions: {
read: 0,
write: 1,
execute: 2
}
```

This makes it easier to add/remove entries without worrying about migrating historical data.

### Scopes

Scopes will now be provided for `<option>` and `not_<option>`.
Expand Down
2 changes: 1 addition & 1 deletion enummer.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ Gem::Specification.new do |spec|
Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"]
end

spec.required_ruby_version = '>= 2.7'
spec.required_ruby_version = ">= 2.7"
spec.add_dependency "rails", ">= 7.0.0"
end
23 changes: 10 additions & 13 deletions lib/enummer/enummer_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,24 @@

module Enummer
class EnummerType < ::ActiveRecord::Type::Value
# @param [Array<Symbol>] value_names list of all possible values for this type
def initialize(value_names:)
@value_names = value_names
@bit_pairs = determine_bit_pairs(value_names)
# @param [Array<Symbol>] values hash with bit-value pairs for all possible values for this type
def initialize(values:)
@values = values
end

# @return Symbol Representation of this type
# @example
# :enummer[read|write|execute]
def type
"enummer[#{@value_names.join("|")}]".to_sym
:"enummer[#{@values.keys.join("|")}]"
end

# @param [Symbol|Array<Symbol>] value Current value represented as one or more symbols
# @return Numeric Numeric representation of values
def serialize(value)
return unless value

Array.wrap(value).sum { |value_name| @bit_pairs.fetch(value_name.to_sym, 0) }
Array.wrap(value).sum { |value_name| @values.fetch(value_name, 0) }
end

# @param [Numeric] value Numeric representation of values
Expand All @@ -31,19 +30,17 @@ def deserialize(value)
return [] unless value
return [] if value.to_i.zero?

@bit_pairs.each_with_object([]) do |(pair_name, pair_value), value_names|
@values.each_with_object([]) do |(pair_name, pair_value), value_names|
next if (value & pair_value).zero?

value_names << pair_name
end
end

private

def determine_bit_pairs(value_names)
value_names.map.with_index do |name, shift|
[name, 1 << shift]
end.to_h
# @param [Array<Symbol>] value Current value represented as one or more symbols or strings
# @return [Array<Symbol>] Current value represented as symbols
def cast(value)
Array.wrap(value).map(&:to_sym)
end
end
end
28 changes: 17 additions & 11 deletions lib/enummer/extension.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,44 +13,43 @@ def enummer(values)
options[:_suffix] = values.delete(:_suffix)

name, values = values.first
values = _enummer_determine_bit_pairs(values)

attribute(name, :enummer, value_names: values)
attribute(name, :enummer, values: values)

singleton_class.__send__(:define_method, name) { values }
singleton_class.__send__(:define_method, name) { values.keys }

_enummer_build_with_scope(name, values)
_enummer_build_values(name, values, options)
end

private

def _enummer_build_with_scope(attribute_name, value_names)
def _enummer_build_with_scope(attribute_name, values)
scope "with_#{attribute_name}", lambda { |desired|
expected = Array.wrap(desired).sum(0) { |value| 1 << value_names.index(value.to_sym) }
expected = Array.wrap(desired).sum(0) { |value| values[value.to_sym] }

where("#{attribute_name} & :expected = :expected", expected: expected)
}
end

def _enummer_build_values(attribute_name, value_names, options)
value_names.each_with_index do |name, i|
def _enummer_build_values(attribute_name, values, options)
values.each do |name, bit|
method_name = _enummer_method_name(attribute_name, name, options)

define_method("#{method_name}?") { self[attribute_name].include?(name) }
define_method("#{method_name}=") do |new_value|
define_method(:"#{method_name}?") { self[attribute_name].include?(name) }
define_method(:"#{method_name}=") do |new_value|
if ActiveModel::Type::Boolean.new.cast(new_value)
self[attribute_name] += [name]
else
self[attribute_name] -= [name]
end
self[attribute_name].uniq!
end
define_method("#{method_name}!") do
define_method(:"#{method_name}!") do
update(attribute_name => self[attribute_name] + [name])
end

bit = 1 << i

scope method_name, -> { where("#{attribute_name} & :bit = :bit", bit: bit) }
scope "not_#{method_name}", -> { where("#{attribute_name} & :bit != :bit", bit: bit) }
end
Expand All @@ -69,5 +68,12 @@ def _enummer_affix(attribute_name, value)

value
end

def _enummer_determine_bit_pairs(values)
values = values.map.with_index { |value, i| [value, i] }.to_h if values.is_a?(Array)
values.transform_values do |shift|
1 << shift
end
end
end
end
1 change: 1 addition & 0 deletions lib/tasks/enummer_tasks.rake
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# frozen_string_literal: true

# desc "Explaining what the task does"
# task :enummer do
# # Task goes here
Expand Down
6 changes: 5 additions & 1 deletion test/dummy/app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ class User < ApplicationRecord
enummer permissions: %i[read write execute]

enummer facial_features: %i[nose mouth eyes], _prefix: true
enummer diets: %i[cigarettes alcohol greens], _prefix: "consumes"
enummer diets: {
alcohol: 1,
cigarettes: 0,
greens: 2
}, _prefix: "consumes"

enummer transport: %i[car truck submarine], _suffix: true
enummer home: %i[box apartment house], _suffix: "home"
Expand Down
1 change: 1 addition & 0 deletions test/dummy/config/initializers/content_security_policy.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# frozen_string_literal: true

# Be sure to restart your server when you modify this file.

# Define an application-wide content security policy
Expand Down
1 change: 1 addition & 0 deletions test/dummy/config/initializers/inflections.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# frozen_string_literal: true

# Be sure to restart your server when you modify this file.

# Add new inflection rules using the following format. Inflections
Expand Down
1 change: 1 addition & 0 deletions test/dummy/config/initializers/permissions_policy.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# frozen_string_literal: true

# Define an application-wide HTTP permissions policy. For further
# information see https://developers.google.com/web/updates/2018/06/feature-policy
#
Expand Down
2 changes: 0 additions & 2 deletions test/dummy/db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 2022_01_30_163927) do

# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"

Expand All @@ -22,5 +21,4 @@
t.integer "transport"
t.integer "home"
end

end
18 changes: 14 additions & 4 deletions test/enummer_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ def setup
diets: %i[cigarettes alcohol],
transport: %i[submarine],
home: %i[box])
@user2 = User.new(permissions: %i[read write])
@user2 = User.new(permissions: %i[read write], diets: %i[cigarettes])
@user3 = User.new(permissions: %i[execute])

[@user1, @user2, @user3].map { |user| user.save && user.reload }
Expand All @@ -32,7 +32,8 @@ def setup
test "with_ scope returns users with all of those bits set" do
assert_equal [@user1], User.with_permissions(%i[execute read write])
assert_equal [@user1], User.with_permissions(%w[execute read write])
assert_equal [@user1, @user2], User.with_permissions(['read', :write])
assert_equal [@user1, @user2], User.with_permissions(["read", :write])
assert_equal [@user1, @user2], User.with_diets(%i[cigarettes])
end

test "not scopes return users without those bits set" do
Expand Down Expand Up @@ -76,9 +77,18 @@ def setup
end

test "setting the attribute with strings adds the values" do
@user3.update(permissions: ["read", "write"])
@user3.update(permissions: %w[read write])

assert_equal %i[read write], @user3.permissions

updated = false
callback = lambda { |_name, _start, _finish, _id, payload| updated = true if payload[:sql].starts_with?("UPDATE") }

ActiveSupport::Notifications.subscribed(callback, "sql.active_record") do
@user3.update(permissions: %w[read write])
end

refute updated, "subsequent updates with the same values should be idempotent"
end

test "using a bang method properly updates the persisted field" do
Expand Down Expand Up @@ -106,7 +116,7 @@ def setup
end

test "recognizes boolean params" do
@user1.update!(ActionController::Parameters.new({"consumes_cigarettes"=>"false"}).permit(:consumes_cigarettes))
@user1.update!(ActionController::Parameters.new({"consumes_cigarettes" => "false"}).permit(:consumes_cigarettes))
refute @user1.consumes_cigarettes?
end

Expand Down
10 changes: 5 additions & 5 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
# frozen_string_literal: true

require 'simplecov'
require "simplecov"
SimpleCov.start do
enable_coverage :branch
add_filter '/test/dummy/'
add_filter "/test/dummy/"

if ENV['CI']
require 'simplecov-cobertura'
if ENV["CI"]
require "simplecov-cobertura"
SimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter
end
end

# Configure Rails Environment
# Configure Rails Environment
ENV["RAILS_ENV"] = "test"

require_relative "../test/dummy/config/environment"
Expand Down

0 comments on commit 6a5155f

Please sign in to comment.