active_record_batteries 1.0.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: 627d133bd03a971733af4b6c13f785c7818fc091
4
+ data.tar.gz: da576d73fc56b2908ee439d57602f16190393d05
5
+ SHA512:
6
+ metadata.gz: dd346dedc4c333d84854dbf6ba7c207d64ee1c33cd27f3c14f7ed1decba4e66c64a86f0931616f03e05d811ec49caaf33e6b2e61dde007d20867f7f8e5859300
7
+ data.tar.gz: a9d073f004d367713145512ef781600970b3b39f177e29fa6c199836d0f37cf4132efc3c76e32edd6ae8a2dc665fd56a6a2ac39126a7d29c1cd578ac71e87fd9
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2015
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rspec/core'
8
+ require 'rspec/core/rake_task'
9
+
10
+ RSpec::Core::RakeTask.new(:spec)
11
+
12
+ load 'rails/tasks/statistics.rake'
13
+
14
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,7 @@
1
+ module ActiveRecordBatteries
2
+ end
3
+
4
+ require 'active_record_batteries/engine'
5
+ require 'active_record_batteries/version'
6
+ require 'active_record_batteries/concerns'
7
+ require 'active_record_batteries/loader'
@@ -0,0 +1,11 @@
1
+ module ActiveRecordBatteries
2
+ module Concerns
3
+ extend ActiveSupport::Autoload
4
+
5
+ autoload :Deletable, 'active_record_batteries/concerns/deletable'
6
+ autoload :Filterable, 'active_record_batteries/concerns/filterable'
7
+ autoload :Paginable, 'active_record_batteries/concerns/paginable'
8
+ autoload :Sluggable, 'active_record_batteries/concerns/sluggable'
9
+ autoload :RelationshipScopes, 'active_record_batteries/concerns/relationship_scopes'
10
+ end
11
+ end
@@ -0,0 +1,22 @@
1
+ module ActiveRecordBatteries
2
+ module Concerns
3
+ module Deletable
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ default_scope { where(deleted_at: nil) }
8
+
9
+ scope :including_deleted, -> { unscope(where: :deleted_at) }
10
+ end
11
+
12
+ def delete!
13
+ self.deleted_at = Time.current
14
+ self
15
+ end
16
+
17
+ def deleted?
18
+ !!self.deleted_at
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,30 @@
1
+ module ActiveRecordBatteries
2
+ module Concerns
3
+ module Filterable
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ scope :filtered, ->(values) {
8
+ current = all
9
+ filters.each do |key, method|
10
+ if values.try(:key?, key) && current.respond_to?(method)
11
+ current = current.send(method, values[key])
12
+ end
13
+ end
14
+ current
15
+ }
16
+ end
17
+
18
+ module ClassMethods
19
+ def filter_add(method_name, callable = nil)
20
+ scope(method_name, callable) if callable
21
+ filters[method_name] = method_name
22
+ end
23
+
24
+ def filters
25
+ @filters ||= {}
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,56 @@
1
+ module ActiveRecordBatteries
2
+ module Concerns
3
+ module Paginable
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ # Default items per page
8
+ page_items 25
9
+
10
+ scope :paginate, lambda { |page, page_items = @page_items|
11
+ limit(page_items)
12
+ .offset(([page.to_i, 1].max - 1) * page_items)
13
+ .extending do
14
+
15
+ def current_page
16
+ @current_page ||=
17
+ if total_pages.zero?
18
+ 0
19
+ else
20
+ (offset_value / limit_value.to_f).ceil + 1
21
+ end
22
+ end
23
+
24
+ # If pagination is used this
25
+ def total_items
26
+ @total_items ||=
27
+ begin
28
+ total_rows = except(:offset, :limit, :order)
29
+ total_rows = total_rows.except(:includes) if eager_loading?
30
+ total_rows.distinct(:id).count
31
+ end
32
+ end
33
+
34
+ def total_pages
35
+ @total_pages ||=
36
+ begin
37
+ (total_items / limit_value.to_f).ceil
38
+ end
39
+ end
40
+ end
41
+ }
42
+ end
43
+
44
+ module ClassMethods
45
+ def page_items(items)
46
+ @page_items = items
47
+ end
48
+
49
+ def pages(page_items = @page_items)
50
+ (all.except(:offset, :limit, :order, :includes)
51
+ .distinct(:id).count / page_items.to_f).ceil
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,24 @@
1
+ module ActiveRecordBatteries
2
+ module Concerns
3
+ module RelationshipScopes
4
+ extend ActiveSupport::Concern
5
+
6
+ module ClassMethods
7
+ def relationship_scopes(relationship, suffix = nil)
8
+ suffix ||= relationship
9
+
10
+ with_scope_name = "with_#{ suffix }".to_sym
11
+ include_scope_name = "include_#{ suffix }".to_sym
12
+
13
+ scope with_scope_name, -> { joins(relationship) }
14
+ scope include_scope_name, -> { includes(relationship) }
15
+ scope "and_#{ suffix }".to_sym, -> { send(with_scope_name).send(include_scope_name) }
16
+ scope "pload_#{ suffix }".to_sym, -> { preload(relationship) }
17
+ scope "eload_#{ suffix }".to_sym, -> { eager_load(relationship) }
18
+
19
+ nil
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,70 @@
1
+ module ActiveRecordBatteries
2
+ module Concerns
3
+ module Sluggable
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ # Backwards compatibility
8
+ slug_base_column :name
9
+
10
+ # Be sure to create a valid slug before validating and saving.
11
+ before_validation :set_slug, on: [:create, :update]
12
+ after_validation :delete_slug_errors
13
+
14
+ # Scope to query by slug
15
+ scope :by_slug, ->(slug) { where(slug: slug) }
16
+
17
+ # Slug must be present
18
+ validates :slug, presence: true, uniqueness:true, length: { maximum: 256 }
19
+ end
20
+
21
+ module ClassMethods
22
+ def slug_base_column(column_name)
23
+ @slug_base_column_name = column_name.to_sym
24
+ end
25
+
26
+ # Find by slug
27
+ def find_by_slug(slug)
28
+ by_slug(slug).take
29
+ end
30
+ end
31
+
32
+ def to_param
33
+ slug
34
+ end
35
+
36
+ private
37
+
38
+ def slug_base_column_value
39
+ send(self.class.instance_variable_get(:@slug_base_column_name))
40
+ end
41
+
42
+ def set_slug
43
+ self.slug = qualify_slug(slug_base_column_value.parameterize) if
44
+ slug_base_column_value.present? && slug.blank?
45
+ end
46
+
47
+ def delete_slug_errors
48
+ errors.delete(:slug)
49
+ end
50
+
51
+ def qualify_slug(base_slug)
52
+ tentative_slug = base_slug
53
+ while duplicate_slug?(tentative_slug)
54
+ tentative_slug = generate_slug_name(base_slug)
55
+ end
56
+ tentative_slug
57
+ end
58
+
59
+ def duplicate_slug?(slug)
60
+ self.class
61
+ .where.not(id: id)
62
+ .exists?(slug: slug)
63
+ end
64
+
65
+ def generate_slug_name(slug)
66
+ "#{slug}-#{SecureRandom.urlsafe_base64(4)}"
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,5 @@
1
+ module ActiveRecordBatteries
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace ActiveRecordBatteries
4
+ end
5
+ end
@@ -0,0 +1,19 @@
1
+ module ActiveRecordBatteries
2
+ module Loader
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ def batteries! (*modules)
7
+ modules = modules.is_a?(Array) ? modules : [ modules ]
8
+
9
+ modules.each.map{|i| i.to_s.camelize }.each do |mod|
10
+ include ActiveRecordBatteries::Concerns.const_get(mod)
11
+ end
12
+
13
+ nil
14
+ end
15
+ end
16
+
17
+ ActiveRecord::Base.send(:include, self)
18
+ end
19
+ end
@@ -0,0 +1,3 @@
1
+ module ActiveRecordBatteries
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :active_record_batteries do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,200 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ RSpec.describe ActiveRecordBatteries do
4
+ before :all do
5
+ # Have some authors
6
+ Author.create((1..5).map { |i| { name: SecureRandom.urlsafe_base64(5) } })
7
+
8
+ # And have eah author have some articles
9
+ Author.all.each do |author|
10
+ Article.create((1..10).map { |i|
11
+ {
12
+ title: SecureRandom.urlsafe_base64(10),
13
+ body: SecureRandom.urlsafe_base64(100),
14
+ author: author
15
+ }
16
+ })
17
+ end
18
+ end
19
+
20
+ context "Deletable" do
21
+ before :all do
22
+ Article.create(title: "article_deletable", author: Author.first)
23
+ end
24
+
25
+ after :all do
26
+ Article.including_deleted.find_by(title: "article_deletable").destroy
27
+ end
28
+
29
+ it "new instance should not deleted" do
30
+ expect(Article.new.deleted?).to be false
31
+ end
32
+
33
+ it "new instance should be deleted" do
34
+ expect(Article.new(deleted_at: Date.today).deleted?).to be true
35
+ end
36
+
37
+ it "should delete a new instance" do
38
+ obj = Article.new
39
+ obj.delete!
40
+
41
+ expect(obj.deleted?).to be true
42
+ end
43
+
44
+ it "should not be deleted" do
45
+ obj = Article.find_by(title: "article_deletable")
46
+
47
+ expect(obj.deleted?).to be false
48
+ end
49
+
50
+ it "should delete" do
51
+ obj = Article.find_by(title: "article_deletable")
52
+ obj.delete!
53
+ obj.save
54
+
55
+ expect(obj.deleted?).to be true
56
+ end
57
+
58
+ it "should not find" do
59
+ obj = Article.find_by(title: "article_deletable")
60
+ expect(obj).to eq(nil)
61
+ end
62
+
63
+ it "should find" do
64
+ obj = Article.including_deleted.find_by(title: "article_deletable")
65
+ expect(obj).not_to eq(nil)
66
+ end
67
+
68
+ it "should stay deleted" do
69
+ obj = Article.including_deleted.find_by(title: "article_deletable")
70
+ expect(obj.deleted?).to be true
71
+ end
72
+ end
73
+
74
+ context "Filterable" do
75
+ before :all do
76
+ Article.create(title: "article_filterable", author: Author.first)
77
+ Article.create(title: "article_filterable2", author: Author.first, slug: 'filtered_1')
78
+ end
79
+
80
+ after :all do
81
+ Article.by_title("article_filterable").destroy_all
82
+ Article.by_title("article_filterable2").destroy_all
83
+ end
84
+
85
+ it "should be hash" do
86
+ expect(Article.filters).to be_an_instance_of(Hash)
87
+ end
88
+
89
+ it "should have filters" do
90
+ expect(Article.filters.keys.size).to be > 0
91
+ end
92
+
93
+ it "should have filter" do
94
+ expect(Article.filters[:by_title]).to be_truthy
95
+ end
96
+
97
+ it "should find" do
98
+ results = Article.by_title("article_filterable")
99
+ expect(results).not_to be_empty
100
+ end
101
+
102
+ it "should filter" do
103
+ results = Article.filtered(by_title: "article_filterable2", by_slug: 'filtered_1')
104
+ expect(results).not_to be_empty
105
+ end
106
+ end
107
+
108
+ context "Paginable" do
109
+ it "should set items per page" do
110
+ expect(Author.page_items(2)).to eql(2)
111
+ end
112
+
113
+ it "should count pages" do
114
+ expect(Article.pages).to eql((Article.count / 5.0).ceil)
115
+ end
116
+
117
+ it "should count pages with parameter passed" do
118
+ expect(Article.pages(3)).to eql((Article.count / 3.0).ceil)
119
+ end
120
+
121
+ it "should paginate" do
122
+ expect(Article.paginate(2).current_page).to eq(2)
123
+ end
124
+
125
+ it "should paginate with params" do
126
+ expect(Article.paginate(3, 50).current_page).to eq(3)
127
+ end
128
+
129
+ it "should not lose items" do
130
+ expect(Article.paginate(1).total_items).to eq(Article.count)
131
+ end
132
+
133
+ it "should count pages correctly after pagination" do
134
+ expect(Article.paginate(2).total_pages).to eq(Article.pages)
135
+ end
136
+
137
+ it "should not be affected by includes" do
138
+ expect(Author.includes(:articles).
139
+ paginate(2).
140
+ total_pages).to eq(Author.pages)
141
+ end
142
+
143
+ it "should not be affected by joins" do
144
+ expect(Author.joins(:articles).
145
+ paginate(2).
146
+ total_pages).to eq(Author.pages)
147
+ end
148
+ end
149
+
150
+ context "RelationshipScopes" do
151
+ before :all do
152
+ Author.create(name: "articleless")
153
+ end
154
+
155
+ # Scopes should be added
156
+ [ :with_articles, :include_articles, :and_articles,
157
+ :pload_articles, :eload_articles ].each do |method|
158
+ it "should respond to #{method}" do
159
+ expect(Author.respond_to?(method)).to be true
160
+ end
161
+ end
162
+
163
+ it "should not have articles preloaded" do
164
+ author = Author.first
165
+ expect(author.articles.loaded?).to be false
166
+ end
167
+
168
+ it "should have articles included" do
169
+ author = Author.include_articles.first
170
+
171
+ expect(author.articles.loaded?).to be true
172
+ end
173
+
174
+ it "should have articles preloaded" do
175
+ author = Author.pload_articles.first
176
+
177
+ expect(author.articles.loaded?).to be true
178
+ end
179
+
180
+ it "should have articles eager loaded" do
181
+ author = Author.eload_articles.first
182
+
183
+ expect(author.articles.loaded?).to be true
184
+ end
185
+
186
+ it "should join but not preload" do
187
+ author = Author.with_articles.first
188
+
189
+ expect(author.articles.loaded?).to be false
190
+ end
191
+
192
+ it "should inner join" do
193
+ expect(Author.and_articles.where(name: "articleless")).to be_empty
194
+ end
195
+
196
+ it "should not inner join" do
197
+ expect(Author.pload_articles.where(name: "articleless")).not_to be_empty
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,55 @@
1
+ require 'rails/all'
2
+ require 'active_record_batteries'
3
+
4
+ ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ':memory:')
5
+ ActiveRecord::Schema.verbose = false
6
+
7
+ ActiveRecord::Schema.define do
8
+ self.verbose = false
9
+
10
+ create_table :authors do |t|
11
+ t.string :name
12
+
13
+ # Sluggable
14
+ t.string :slug
15
+
16
+ t.timestamps null: false
17
+ end
18
+
19
+ create_table :articles do |t|
20
+ t.string :title
21
+ t.text :body
22
+ t.references :author
23
+
24
+ # Sluggable
25
+ t.string :slug
26
+
27
+ # Deletable
28
+ t.datetime :deleted_at
29
+
30
+ t.timestamps null: false
31
+ end
32
+ end
33
+
34
+ class Author < ActiveRecord::Base
35
+ batteries! :sluggable, :paginable, :filterable, :relationship_scopes
36
+
37
+ has_many :articles
38
+
39
+ filter_add :by_slug
40
+
41
+ relationship_scopes :articles
42
+ end
43
+
44
+ class Article < ActiveRecord::Base
45
+ batteries! :sluggable, :paginable, :filterable, :relationship_scopes, :deletable
46
+
47
+ belongs_to :author
48
+
49
+ slug_base_column :title
50
+
51
+ filter_add :by_slug
52
+ filter_add :by_title, ->(title) { where(title: title) }
53
+
54
+ page_items 5
55
+ end
metadata ADDED
@@ -0,0 +1,103 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: active_record_batteries
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Francisco Soto
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-05-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 4.2.1
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.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: sqlite3
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: rspec-rails
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
+ description: Several small active record modules to empower models in a simple way.
56
+ email:
57
+ - ebobby@gmail.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - MIT-LICENSE
63
+ - Rakefile
64
+ - lib/active_record_batteries.rb
65
+ - lib/active_record_batteries/concerns.rb
66
+ - lib/active_record_batteries/concerns/deletable.rb
67
+ - lib/active_record_batteries/concerns/filterable.rb
68
+ - lib/active_record_batteries/concerns/paginable.rb
69
+ - lib/active_record_batteries/concerns/relationship_scopes.rb
70
+ - lib/active_record_batteries/concerns/sluggable.rb
71
+ - lib/active_record_batteries/engine.rb
72
+ - lib/active_record_batteries/loader.rb
73
+ - lib/active_record_batteries/version.rb
74
+ - lib/tasks/active_record_batteries_tasks.rake
75
+ - spec/active_record_batteries_spec.rb
76
+ - spec/spec_helper.rb
77
+ homepage: https://github.com/ebobby/active_record_batteries
78
+ licenses:
79
+ - MIT
80
+ metadata: {}
81
+ post_install_message:
82
+ rdoc_options: []
83
+ require_paths:
84
+ - lib
85
+ required_ruby_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ required_rubygems_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ requirements: []
96
+ rubyforge_project:
97
+ rubygems_version: 2.4.5
98
+ signing_key:
99
+ specification_version: 4
100
+ summary: ActiveRecord with Batteries. Extensions to make life easier.
101
+ test_files:
102
+ - spec/active_record_batteries_spec.rb
103
+ - spec/spec_helper.rb