Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow initializing enummer with hash #22

Merged
merged 3 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading