active_flag 0.1.0
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.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/README.md +118 -0
- data/Rakefile +10 -0
- data/active_flag.gemspec +27 -0
- data/bin/console +9 -0
- data/bin/setup +8 -0
- data/lib/active_flag.rb +46 -0
- data/lib/active_flag/definition.rb +70 -0
- data/lib/active_flag/railtie.rb +11 -0
- data/lib/active_flag/value.rb +45 -0
- data/lib/active_flag/version.rb +3 -0
- metadata +128 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 1eab2c1d74e4f219e5fa9c3518fdc65efb97cb76
|
4
|
+
data.tar.gz: 9e694fdbf84d951e190d5986738f49d3adfb2c8e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2cf6ca0d48b366dc48de2278225827a8ada2fa797cacaa62514d14d180f4c4948f11741eee3e7fc5fac3944090ff0fe6532f565b60539e6ae47576b8f9474d40
|
7
|
+
data.tar.gz: 3b802d4cc801ad8ad3b95dc55bca376712f06cf2c1776a51643b0f5092555901f8344f5be17e16da9b101d4b4296de838108ad52ec34d37819a51e668565aa5a
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
# ActiveFlag - Bit array for ActiveRecord
|
2
|
+
|
3
|
+
[](https://travis-ci.org/kenn/active_flag)
|
4
|
+
|
5
|
+
Store up to 64 multiple flags ([bit array](https://en.wikipedia.org/wiki/Bit_array)) in a single integer column with ActiveRecord.
|
6
|
+
|
7
|
+
## Usage
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
class Profile < ActiveRecord::Base
|
11
|
+
flag :languages, [:english, :spanish, :chinese, :french, :japanese]
|
12
|
+
end
|
13
|
+
|
14
|
+
# Instance methods
|
15
|
+
profile.languages #=> #<ActiveFlag::Value: {:english, :japanese}>
|
16
|
+
profile.languages.english? #=> true
|
17
|
+
profile.languages.set(:spanish)
|
18
|
+
profile.languages.unset(:japanese)
|
19
|
+
profile.languages.raw #=> 3
|
20
|
+
profile.languages.to_a #=> [:english, :spanish]
|
21
|
+
|
22
|
+
profile.languages = [:spanish, :japanese] # Direct assignment that works with forms
|
23
|
+
|
24
|
+
# Class methods
|
25
|
+
Profile.languages.maps #=> {:english=>1, :spanish=>2, :chinese=>4, :french=>8, :japanese=>16 }
|
26
|
+
Profile.where_languages(:french) #=> SELECT * FROM profiles WHERE languages & 8 > 0
|
27
|
+
```
|
28
|
+
|
29
|
+
## Install
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
gem 'active_flag'
|
33
|
+
```
|
34
|
+
|
35
|
+
### Migration
|
36
|
+
|
37
|
+
Always set `0` as a default.
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
t.integer :languages, null: false, default: 0, limit: 8
|
41
|
+
# OR
|
42
|
+
add_column :users, :languages, :integer, null: false, default: 0, limit: 8
|
43
|
+
```
|
44
|
+
|
45
|
+
`limit: 8` is only required if you need more than 32 flags.
|
46
|
+
|
47
|
+
## Query
|
48
|
+
|
49
|
+
For a querying purpose, use `where_[column]` scope.
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
Profile.where_languages(:french) #=> SELECT * FROM profiles WHERE languages & 8 > 0
|
53
|
+
```
|
54
|
+
|
55
|
+
Also takes multiple values.
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
Profile.where_languages(:french, :spanish)
|
59
|
+
```
|
60
|
+
|
61
|
+
By default, it searches with `or` operation, so the query above returns profiles that have either French or Spanish.
|
62
|
+
|
63
|
+
If you want to change it to `and` operation, you can specify:
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
Profile.where_languages(:french, :spanish, op: :and)
|
67
|
+
```
|
68
|
+
|
69
|
+
## Translation
|
70
|
+
|
71
|
+
`ActiveFlag` supports [i18n](http://guides.rubyonrails.org/i18n.html) just as ActiveModel does.
|
72
|
+
|
73
|
+
For instance, create a Japanese translation in `config/locales/ja.yml`
|
74
|
+
|
75
|
+
```yaml
|
76
|
+
ja:
|
77
|
+
active_flag:
|
78
|
+
profile:
|
79
|
+
languages:
|
80
|
+
english: 英語
|
81
|
+
spanish: スペイン語
|
82
|
+
chinese: 中国語
|
83
|
+
french: フランス語
|
84
|
+
japanese: 日本語
|
85
|
+
```
|
86
|
+
|
87
|
+
and now `profile.languages.to_human` returns a translated string.
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
I18n.locale = :ja
|
91
|
+
profile.languages.to_human #=> ['英語', 'スペイン語']
|
92
|
+
|
93
|
+
I18n.locale = :en
|
94
|
+
profile.languages.to_human #=> ['English', 'Spanish']
|
95
|
+
```
|
96
|
+
|
97
|
+
## Forms
|
98
|
+
|
99
|
+
Thanks to the translation support, forms just work as you would expect with the `pairs` convenience method.
|
100
|
+
|
101
|
+
```ruby
|
102
|
+
# With FormBuilder
|
103
|
+
= form_for(@profile) do |f|
|
104
|
+
= f.collection_check_boxes :languages, Profile.languages.pairs
|
105
|
+
|
106
|
+
# With SimpleForm
|
107
|
+
= simple_form_for(@profile) do |f|
|
108
|
+
= f.input :languages, as: :check_boxes, collection: Profile.languages.pairs
|
109
|
+
```
|
110
|
+
|
111
|
+
## Other solutions
|
112
|
+
|
113
|
+
There are plenty of gems that share the same goal. However they have messy syntax than necessary in my opinion, and I wanted a better API to achieve that goal.
|
114
|
+
|
115
|
+
- [bitfields](https://github.com/grosser/bitfields)
|
116
|
+
- [flag_shih_tzu](https://github.com/pboling/flag_shih_tzu)
|
117
|
+
|
118
|
+
Also, `ActiveFlag` has one of the simplest code base that you can easily reason about or hack on.
|
data/Rakefile
ADDED
data/active_flag.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'active_flag/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'active_flag'
|
8
|
+
spec.version = ActiveFlag::VERSION
|
9
|
+
spec.authors = ['Kenn Ejima']
|
10
|
+
spec.email = ['kenn.ejima@gmail.com']
|
11
|
+
|
12
|
+
spec.summary = %q{Bit array for ActiveRecord}
|
13
|
+
spec.description = %q{Bit array for ActiveRecord}
|
14
|
+
spec.homepage = 'https://github.com/kenn/active_flag'
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
17
|
+
f.match(%r{^(test|spec|features)/})
|
18
|
+
end
|
19
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ['lib']
|
21
|
+
|
22
|
+
spec.add_runtime_dependency 'activerecord', '~> 4.2.0'
|
23
|
+
spec.add_development_dependency 'bundler', '~> 1.13'
|
24
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
25
|
+
spec.add_development_dependency 'minitest', '~> 5.0'
|
26
|
+
spec.add_development_dependency 'sqlite3'
|
27
|
+
end
|
data/bin/console
ADDED
data/bin/setup
ADDED
data/lib/active_flag.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'active_flag/definition'
|
2
|
+
require 'active_flag/railtie'
|
3
|
+
require 'active_flag/value'
|
4
|
+
require 'active_flag/version'
|
5
|
+
require 'active_record'
|
6
|
+
|
7
|
+
module ActiveFlag
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
def flag(column, keys, options = {})
|
12
|
+
class << self
|
13
|
+
attr_reader :active_flags
|
14
|
+
end
|
15
|
+
@active_flags ||= {}
|
16
|
+
@active_flags[column] = Definition.new(column, keys, options, self)
|
17
|
+
|
18
|
+
# Getter
|
19
|
+
define_method column do
|
20
|
+
self.class.active_flags[column].get(self, read_attribute(column))
|
21
|
+
end
|
22
|
+
|
23
|
+
# Setter
|
24
|
+
define_method "#{column}=" do |arg|
|
25
|
+
self.class.active_flags[column].set(self, arg)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Reference to definition
|
29
|
+
define_singleton_method column.to_s.pluralize do
|
30
|
+
active_flags[column]
|
31
|
+
end
|
32
|
+
|
33
|
+
# Scopes
|
34
|
+
define_singleton_method "where_#{column}" do |*args|
|
35
|
+
options = args.extract_options!
|
36
|
+
integer = active_flags[column].to_i(args)
|
37
|
+
column_name = connection.quote_table_name_for_assignment(table_name, column)
|
38
|
+
if options[:op] == :and
|
39
|
+
where("#{column_name} & #{integer} = #{integer}")
|
40
|
+
else
|
41
|
+
where("#{column_name} & #{integer} > 0")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module ActiveFlag
|
2
|
+
class Definition
|
3
|
+
attr_accessor :keys, :maps, :options, :column
|
4
|
+
|
5
|
+
def initialize(column, keys, options, klass)
|
6
|
+
@column = column
|
7
|
+
@keys = keys.freeze
|
8
|
+
@maps = Hash[keys.map.with_index{|key, i| [key, 2**i] }].freeze
|
9
|
+
@options = options
|
10
|
+
@klass = klass
|
11
|
+
end
|
12
|
+
|
13
|
+
def get(instance, integer)
|
14
|
+
to_value(instance, integer)
|
15
|
+
end
|
16
|
+
|
17
|
+
def set(instance, arg)
|
18
|
+
instance.send :write_attribute, @column, to_i(arg)
|
19
|
+
end
|
20
|
+
|
21
|
+
def humans
|
22
|
+
@humans ||= {}
|
23
|
+
@humans[I18n.locale] ||= begin
|
24
|
+
@keys.map{|key| [key, human(key)] }.to_h
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def pairs
|
29
|
+
@pairs ||= {}
|
30
|
+
@pairs[I18n.locale] ||= humans.invert
|
31
|
+
end
|
32
|
+
|
33
|
+
# Set / unset a bit on all records for migration
|
34
|
+
# http://stackoverflow.com/a/12928899/157384
|
35
|
+
|
36
|
+
def set_all!(key)
|
37
|
+
@klass.update_all("#{@column} = COALESCE(#{@column},0) | #{@maps[key]}")
|
38
|
+
end
|
39
|
+
|
40
|
+
def unset_all!(key)
|
41
|
+
@klass.update_all("#{@column} = COALESCE(#{@column},0) & ~#{@maps[key]}")
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_i(arg)
|
45
|
+
return 0 if arg.blank?
|
46
|
+
arg = [arg] unless arg.is_a?(Enumerable)
|
47
|
+
arg.map{|i| i && @maps[i.to_sym] || 0 }.sum
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def to_value(instance, integer)
|
53
|
+
Value.new(@maps.map{|key, mask| (integer & mask > 0) ? key : nil }.compact).with(instance, self)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Human-friendly print on class level
|
57
|
+
def human(key, options={})
|
58
|
+
return if key.nil? # otherwise, key.to_s.humanize will return ""
|
59
|
+
# Mimics ActiveModel::Translation.human_attribute_name
|
60
|
+
defaults = @klass.lookup_ancestors.map do |klass|
|
61
|
+
:"#{@klass.i18n_scope}.active_flag.#{klass.model_name.i18n_key}.#{@column}.#{key}"
|
62
|
+
end
|
63
|
+
defaults << :"active_flag.#{@klass.model_name.i18n_key}.#{@column}.#{key}"
|
64
|
+
defaults << :"active_flag.#{@column}.#{key}"
|
65
|
+
defaults << options.delete(:default) if options[:default]
|
66
|
+
defaults << key.to_s.humanize
|
67
|
+
I18n.translate defaults.shift, options.reverse_merge(count: 1, default: defaults)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module ActiveFlag
|
2
|
+
class Value < Set
|
3
|
+
def with(instance, definition)
|
4
|
+
@instance = instance
|
5
|
+
@definition = definition
|
6
|
+
@column = definition.column
|
7
|
+
|
8
|
+
@definition.keys.each do |key|
|
9
|
+
self.class.class_eval do
|
10
|
+
define_method "#{key}?" do
|
11
|
+
@instance.send(@column).include?(key)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
return self
|
17
|
+
end
|
18
|
+
|
19
|
+
def raw
|
20
|
+
@instance.read_attribute(@column)
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_human
|
24
|
+
@instance.send(@column).to_a.map{|key| @definition.humans[key] }
|
25
|
+
end
|
26
|
+
|
27
|
+
def set(key)
|
28
|
+
@instance.send "#{@column}=", add(key)
|
29
|
+
end
|
30
|
+
|
31
|
+
def unset(key)
|
32
|
+
@instance.send "#{@column}=", delete(key)
|
33
|
+
end
|
34
|
+
|
35
|
+
def set!(key, options={})
|
36
|
+
set(key)
|
37
|
+
@instance.save!(options)
|
38
|
+
end
|
39
|
+
|
40
|
+
def unset!(key, options={})
|
41
|
+
unset(key)
|
42
|
+
@instance.save!(options)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
metadata
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: active_flag
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Kenn Ejima
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-12-22 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: 4.2.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 4.2.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: '1.13'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.13'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: minitest
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '5.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '5.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: sqlite3
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description: Bit array for ActiveRecord
|
84
|
+
email:
|
85
|
+
- kenn.ejima@gmail.com
|
86
|
+
executables:
|
87
|
+
- console
|
88
|
+
- setup
|
89
|
+
extensions: []
|
90
|
+
extra_rdoc_files: []
|
91
|
+
files:
|
92
|
+
- ".gitignore"
|
93
|
+
- ".travis.yml"
|
94
|
+
- Gemfile
|
95
|
+
- README.md
|
96
|
+
- Rakefile
|
97
|
+
- active_flag.gemspec
|
98
|
+
- bin/console
|
99
|
+
- bin/setup
|
100
|
+
- lib/active_flag.rb
|
101
|
+
- lib/active_flag/definition.rb
|
102
|
+
- lib/active_flag/railtie.rb
|
103
|
+
- lib/active_flag/value.rb
|
104
|
+
- lib/active_flag/version.rb
|
105
|
+
homepage: https://github.com/kenn/active_flag
|
106
|
+
licenses: []
|
107
|
+
metadata: {}
|
108
|
+
post_install_message:
|
109
|
+
rdoc_options: []
|
110
|
+
require_paths:
|
111
|
+
- lib
|
112
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - ">="
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0'
|
117
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
118
|
+
requirements:
|
119
|
+
- - ">="
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
version: '0'
|
122
|
+
requirements: []
|
123
|
+
rubyforge_project:
|
124
|
+
rubygems_version: 2.5.2
|
125
|
+
signing_key:
|
126
|
+
specification_version: 4
|
127
|
+
summary: Bit array for ActiveRecord
|
128
|
+
test_files: []
|