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 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
@@ -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/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.3
5
+ before_install: gem install bundler -v 1.13.6
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in active_flag.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,118 @@
1
+ # ActiveFlag - Bit array for ActiveRecord
2
+
3
+ [![Build Status](https://travis-ci.org/kenn/active_flag.svg)](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
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ end
9
+
10
+ task :default => :test
@@ -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
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'active_flag'
5
+
6
+ require_relative '../test/load_fixtures'
7
+
8
+ require 'irb'
9
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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,11 @@
1
+ module ActiveFlag
2
+ if defined? Rails::Railtie
3
+ class Railtie < Rails::Railtie
4
+ initializer 'active_flag.insert_into_active_record' do |app|
5
+ ActiveSupport.on_load :active_record do
6
+ include ActiveFlag
7
+ end
8
+ end
9
+ end
10
+ end
11
+ 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
@@ -0,0 +1,3 @@
1
+ module ActiveFlag
2
+ VERSION = '0.1.0'
3
+ 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: []