active_record_bitmask 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f0836338456ad6155feff23e641e6f6b991ba9a670763b2c09c9ab2a3d243797
4
+ data.tar.gz: 0210e6445e4ac868fe4e41d3473b4f8fdb17ff0202e2058ad89845023e939c11
5
+ SHA512:
6
+ metadata.gz: 040e8da0c1ef2cdc42687c5ae66eb7d379d25a29d7b13c29877a27dd8c834075542b4757a2950e57b8cb945ebe37fa2a5ad2ddc36653eb2434ada7e088837487
7
+ data.tar.gz: 8373e9a4903dfab97a8ec21052192e99f4813ff99257e362c025427f1de3b97228565f2b35df65edb35ef39f0c3ab28fe917ec43c49ceb877c4601ec1b3fc34d
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --require spec_helper
3
+ --color
@@ -0,0 +1,88 @@
1
+ AllCops:
2
+ Exclude:
3
+ - '.bundle/**/*'
4
+ - Gemfile
5
+ TargetRubyVersion: 2.6
6
+ DisplayCopNames: true
7
+
8
+ Bundler:
9
+ Enabled: false
10
+
11
+ Naming:
12
+ Enabled: false
13
+
14
+ Metrics/BlockNesting:
15
+ Enabled: false
16
+
17
+ Metrics/ClassLength:
18
+ Enabled: false
19
+
20
+ Layout/LineLength:
21
+ Enabled: false
22
+
23
+ Metrics/MethodLength:
24
+ Enabled: false
25
+
26
+ Metrics/BlockLength:
27
+ Enabled: false
28
+
29
+ Metrics/ModuleLength:
30
+ Enabled: false
31
+
32
+ Style/AsciiComments:
33
+ Enabled: false
34
+
35
+ Style/IfUnlessModifier:
36
+ Enabled: false
37
+
38
+ Style/Documentation:
39
+ Enabled: false
40
+
41
+ Style/DocumentationMethod:
42
+ Enabled: true
43
+
44
+ Style/BlockDelimiters:
45
+ Enabled: false
46
+
47
+ Style/DoubleNegation:
48
+ Enabled: false
49
+
50
+ Style/GuardClause:
51
+ Enabled: false
52
+
53
+ Style/SpecialGlobalVars:
54
+ Enabled: false
55
+
56
+ Style/NumericPredicate:
57
+ Enabled: false
58
+
59
+ Style/Lambda:
60
+ Enabled: false
61
+
62
+ Layout/ParameterAlignment:
63
+ EnforcedStyle: with_fixed_indentation
64
+
65
+ Layout/ArgumentAlignment:
66
+ EnforcedStyle: with_fixed_indentation
67
+
68
+ Layout/MultilineMethodCallIndentation:
69
+ EnforcedStyle: indented
70
+
71
+ Lint/AmbiguousRegexpLiteral:
72
+ Enabled: false
73
+
74
+ Lint/DisjunctiveAssignmentInConstructor:
75
+ Enabled: false
76
+
77
+ Metrics/AbcSize:
78
+ Enabled: false
79
+
80
+ Metrics/CyclomaticComplexity:
81
+ Max: 10
82
+
83
+ Metrics/PerceivedComplexity:
84
+ Max: 10
85
+
86
+ Lint/UselessAccessModifier:
87
+ ContextCreatingMethods:
88
+ - concerning
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.4.0
5
+ before_install: gem install bundler -v 1.13.7
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ gem 'pry'
6
+ gem 'rubocop'
7
+
8
+ group :test do
9
+ gem 'sqlite3'
10
+ end
@@ -0,0 +1,76 @@
1
+ # ActiveRecordBitmask
2
+
3
+ Transparent manipulation of bitmask attributes for ActiveRecord
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'active_record_bitmask'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install active_record_bitmask
20
+
21
+ ## Usage
22
+
23
+ Simply declare an existing integer column as a bitmask.
24
+
25
+ ```ruby
26
+ class Post < ApplicationRecord
27
+ bitmask(roles: [:administrator, :provider, :guest])
28
+ end
29
+ ```
30
+
31
+ You can then modify the column using the declared values.
32
+
33
+ ```ruby
34
+ post = Post.create(roles: [:provider, :guest])
35
+ post.roles #=> [:provider, :guest]
36
+ post.roles += [:administrator]
37
+ post.roles #=> [:administrator, :provider, :guest]
38
+ ```
39
+
40
+ You can check bitmask
41
+
42
+ ```ruby
43
+ post = Post.create(roles: [:provider, :guest])
44
+ post.roles?(:provider) #=> false
45
+ post.roles?(:guest, :provider) #=> true
46
+ ```
47
+
48
+ You can get the definition of bitmask
49
+
50
+ ```ruby
51
+ map = Post.bitmask_for(:rules)
52
+ map.keys #=> [:administrator, :provider, :guest]
53
+ map.values #=> [1, 2, 4]
54
+ ```
55
+
56
+ ### Scopes
57
+
58
+ #### `with_rules`
59
+
60
+ #### `with_any_rules`
61
+
62
+ #### `without_rules`
63
+
64
+ #### `with_exact_rules`
65
+
66
+ #### `no_rules`
67
+
68
+ ## Development
69
+
70
+ After checking out the repo, run `bundle install` to install dependencies. Then, run `bundle exec rake` to run the tests.
71
+
72
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
73
+
74
+ ## Contributing
75
+
76
+ Bug reports and pull requests are welcome on GitHub at https://github.com/alpaca-tc/active_record_bitmask.
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'active_record_bitmask/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'active_record_bitmask'
9
+ spec.version = ActiveRecordBitmask::VERSION
10
+ spec.authors = ['alpaca-tc']
11
+ spec.email = ['alpaca-tc@alpaca.tc']
12
+
13
+ spec.summary = 'Simple bitmask attribute support for ActiveRecord'
14
+ spec.homepage = 'https://github.com/pixiv/active_record_bitmask'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
+ f.match(%r{^(test|spec|features)/})
18
+ end
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_dependency 'activerecord', '>= 5.0'
22
+ spec.add_development_dependency 'bundler'
23
+ spec.add_development_dependency 'rake'
24
+ spec.add_development_dependency 'rspec'
25
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/lazy_load_hooks'
4
+
5
+ module ActiveRecordBitmask
6
+ require 'active_record_bitmask/version'
7
+ require 'active_record_bitmask/attribute_methods'
8
+ require 'active_record_bitmask/bitmask_type'
9
+ require 'active_record_bitmask/map'
10
+ end
11
+
12
+ ActiveSupport.on_load :active_record do
13
+ require 'active_record_bitmask/model'
14
+ include(ActiveRecordBitmask::Model)
15
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordBitmask
4
+ module AttributeMethods
5
+ require 'active_record_bitmask/attribute_methods/query'
6
+ end
7
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordBitmask
4
+ module AttributeMethods
5
+ module Query
6
+ private
7
+
8
+ def bitmask_attribute?(attribute_name)
9
+ self.class.bitmasks.key?(attribute_name.to_sym)
10
+ end
11
+
12
+ def attribute?(attribute_name, *values)
13
+ if bitmask_attribute?(attribute_name) && values.present?
14
+ # assert bitmask values
15
+ map = self.class.bitmask_for(attribute_name)
16
+ map.attributes_to_bitmask(values)
17
+
18
+ current_value = attribute(attribute_name)
19
+ values.all? { |value| current_value.include?(value) }
20
+ else
21
+ super
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_model'
4
+
5
+ module ActiveRecordBitmask
6
+ class BitmaskType < ActiveModel::Type::Value
7
+ # @param name [String]
8
+ # @param map [ActiveRecordBitmask::Map]
9
+ # @param sub_type [ActiveModel::Type::Value]
10
+ def initialize(_name, map, sub_type)
11
+ @map = map
12
+ @sub_type = sub_type
13
+ end
14
+
15
+ # @return [Symbol]
16
+ def type
17
+ @sub_type.type
18
+ end
19
+
20
+ # @return [Array<Symbol>]
21
+ def cast(value)
22
+ return [] if value.blank?
23
+
24
+ bitmask = @map.bitmask_or_attributes_to_bitmask(value)
25
+ @map.bitmask_to_attributes(bitmask)
26
+ end
27
+
28
+ # @return [Integer]
29
+ def serialize(value)
30
+ @map.bitmask_or_attributes_to_bitmask(value) || 0
31
+ end
32
+
33
+ # @param raw_value [Integer, nil]
34
+ #
35
+ # @return [Array<Symbol>]
36
+ def deserialize(raw_value)
37
+ value = @sub_type.deserialize(raw_value)
38
+ return [] if value.nil?
39
+
40
+ @map.bitmask_to_attributes(value)
41
+ end
42
+
43
+ # @param raw_old_value [Integer]
44
+ # @param new_value [Array<Symbol>]
45
+ #
46
+ # @return [boolean]
47
+ def changed_in_place?(raw_old_value, new_value)
48
+ raw_old_value.nil? != new_value.nil? ||
49
+ cast(raw_old_value) != cast(new_value)
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordBitmask
4
+ class Map
5
+ attr_reader :mapping
6
+
7
+ # @param keys [Array<#to_sym>]
8
+ def initialize(keys)
9
+ @mapping = attributes_to_mapping(keys).freeze
10
+ end
11
+
12
+ # @param value [Integer, Symbol, Array<#to_sym>]
13
+ #
14
+ # @return [Integer]
15
+ def bitmask_or_attributes_to_bitmask(value)
16
+ value = bitmask_to_attributes(value) if value.is_a?(Integer)
17
+ attributes_to_bitmask(value)
18
+ end
19
+
20
+ # @param bitmask [Integer]
21
+ #
22
+ # @return [Array<Integer>]
23
+ def bitmask_combination(bitmask)
24
+ return [0] if bitmask.to_i.zero?
25
+
26
+ max_value = values.max
27
+ combination_pattern_size = (max_value << 1) - 1
28
+ 0.upto(combination_pattern_size).select { |i| i & bitmask == bitmask }
29
+ end
30
+
31
+ # @return [Range<Integer>]
32
+ def all_combination
33
+ max_bit = values.size
34
+ max_value = (2 << (max_bit - 1)) - 1
35
+
36
+ 1..max_value
37
+ end
38
+
39
+ # @param bitmask [Integer]
40
+ #
41
+ # @return [Array<Symbol>]
42
+ def bitmask_to_attributes(bitmask)
43
+ return [] if bitmask.to_i.zero?
44
+
45
+ mapping.each_with_object([]) do |(key, value), values|
46
+ values << key.to_sym if (value & bitmask).nonzero?
47
+ end
48
+ end
49
+
50
+ # @param attributes [Array<#to_sym>]
51
+ #
52
+ # @return [Integer]
53
+ def attributes_to_bitmask(attributes)
54
+ attributes = [attributes].compact unless attributes.respond_to?(:inject)
55
+
56
+ attributes.inject(0) do |bitmask, key|
57
+ key = key.to_sym if key.respond_to?(:to_sym)
58
+ bit = mapping.fetch(key) { raise(ArgumentError, "#{key.inspect} is not a valid value") }
59
+ bitmask | bit
60
+ end
61
+ end
62
+
63
+ # @return [Array<Symbol>]
64
+ def keys
65
+ mapping.keys
66
+ end
67
+
68
+ # @return [Array<Integer>]
69
+ def values
70
+ mapping.values
71
+ end
72
+
73
+ private
74
+
75
+ def attributes_to_mapping(keys)
76
+ keys.each_with_index.each_with_object({}) do |(value, index), hash|
77
+ hash[value.to_sym] = 0b1 << index
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/concern'
4
+ require 'active_support/core_ext/class/attribute'
5
+
6
+ module ActiveRecordBitmask
7
+ module Model
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ include ActiveRecordBitmask::AttributeMethods::Query
12
+ class_attribute(:bitmasks, instance_accessor: false, default: {})
13
+ end
14
+
15
+ class_methods do
16
+ # @param base [ActiveRecord::Base]
17
+ def inherited(base)
18
+ base.bitmasks = bitmasks.deep_dup
19
+ super
20
+ end
21
+
22
+ # @param definitions [Hash]
23
+ #
24
+ # @return [void]
25
+ def bitmask(definitions)
26
+ definitions.each do |attribute, values|
27
+ raise ArgumentError, 'must provide an Array option' if values.empty?
28
+
29
+ attribute = attribute.to_sym
30
+ map = ActiveRecordBitmask::Map.new(values)
31
+
32
+ bitmasks[attribute] = map
33
+
34
+ define_bitmask_attribute(attribute, map)
35
+ define_bitmask_scopes(attribute)
36
+ end
37
+ end
38
+
39
+ # @param attribute [#to_sym]
40
+ #
41
+ # @raise [KeyError]
42
+ #
43
+ # @return [ActiveRecordBitmask::Map]
44
+ def bitmask_for(attribute)
45
+ bitmasks.fetch(attribute.to_sym)
46
+ end
47
+
48
+ private
49
+
50
+ def define_bitmask_attribute(attribute, map)
51
+ decorate_attribute_type(attribute, :bitmask) do |subtype|
52
+ ActiveRecordBitmask::BitmaskType.new(attribute, map, subtype)
53
+ end
54
+ end
55
+
56
+ def define_bitmask_scopes(attribute)
57
+ blank = [0, nil].freeze
58
+
59
+ scope :"with_#{attribute}", ->(*values) {
60
+ map = bitmask_for(attribute)
61
+ bitmask = map.bitmask_or_attributes_to_bitmask(values)
62
+ combination = map.bitmask_combination(bitmask)
63
+
64
+ if bitmask.zero?
65
+ where(attribute => map.all_combination)
66
+ else
67
+ where(attribute => combination)
68
+ end
69
+ }
70
+
71
+ scope :"with_any_#{attribute}", ->(*values) {
72
+ map = bitmask_for(attribute)
73
+ bitmasks = values.map { |value| map.bitmask_or_attributes_to_bitmask(value) }
74
+ combination = bitmasks.flat_map { |bitmask| map.bitmask_combination(bitmask) }
75
+
76
+ if combination.empty?
77
+ where(attribute => map.all_combination)
78
+ else
79
+ where(attribute => combination)
80
+ end
81
+ }
82
+
83
+ scope :"without_#{attribute}", ->(*values) {
84
+ map = bitmask_for(attribute)
85
+ bitmasks = values.map { |value| map.bitmask_or_attributes_to_bitmask(value) }
86
+ combination = bitmasks.flat_map { |bitmask| map.bitmask_combination(bitmask) }
87
+
88
+ if values.empty?
89
+ public_send(:"no_#{attribute}")
90
+ else
91
+ excepted = (map.all_combination.to_a + blank) - combination
92
+ where(attribute => excepted)
93
+ end
94
+ }
95
+
96
+ scope :"with_exact_#{attribute}", ->(*values) {
97
+ map = bitmask_for(attribute)
98
+ bitmasks = values.map { |value| map.bitmask_or_attributes_to_bitmask(value) }
99
+ bitmask = bitmasks.inject(0, &:|)
100
+
101
+ if bitmask.zero?
102
+ public_send(:"no_#{attribute}")
103
+ else
104
+ where(attribute => bitmask)
105
+ end
106
+ }
107
+
108
+ scope :"no_#{attribute}", -> {
109
+ where(attribute => blank)
110
+ }
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordBitmask
4
+ VERSION = '0.0.1'
5
+ end
metadata ADDED
@@ -0,0 +1,113 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: active_record_bitmask
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - alpaca-tc
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-01-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '5.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '5.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description:
70
+ email:
71
+ - alpaca-tc@alpaca.tc
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".gitignore"
77
+ - ".rspec"
78
+ - ".rubocop.yml"
79
+ - ".travis.yml"
80
+ - Gemfile
81
+ - README.md
82
+ - Rakefile
83
+ - active_record_bitmask.gemspec
84
+ - lib/active_record_bitmask.rb
85
+ - lib/active_record_bitmask/attribute_methods.rb
86
+ - lib/active_record_bitmask/attribute_methods/query.rb
87
+ - lib/active_record_bitmask/bitmask_type.rb
88
+ - lib/active_record_bitmask/map.rb
89
+ - lib/active_record_bitmask/model.rb
90
+ - lib/active_record_bitmask/version.rb
91
+ homepage: https://github.com/pixiv/active_record_bitmask
92
+ licenses: []
93
+ metadata: {}
94
+ post_install_message:
95
+ rdoc_options: []
96
+ require_paths:
97
+ - lib
98
+ required_ruby_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ required_rubygems_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ requirements: []
109
+ rubygems_version: 3.0.3
110
+ signing_key:
111
+ specification_version: 4
112
+ summary: Simple bitmask attribute support for ActiveRecord
113
+ test_files: []