active_record_bitmask 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: []