quickery 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
+ SHA256:
3
+ metadata.gz: 3bb77d2c32c2ce80c651fd671701fd561c4ba37d8920827804bf5d092cd5085e
4
+ data.tar.gz: 84388c8df61f76837adfd630b9c8a0421d818d5098b4d3966a547ee8408efd04
5
+ SHA512:
6
+ metadata.gz: 3f8f3efade73d92df662b8f43aaaab39af3b22390c83f3f2ff82ce292cdae737e451dbd83c1b5981d7272c5effbbb294288d823a6ea07002be3deb12277ff738
7
+ data.tar.gz: 1229a87bf58a64bcc2f2fe1e014bc4c96b28f07ebc537b0e73164a30c4775e515db4730c927957c133263a58097471a429bbc48d5399faed2bf546c689a4e2fa
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ /spec/internal/log/*
11
+ /spec/internal/tmp/*
12
+ *.sqlite
13
+ *.sqlite-journal
14
+
15
+ # rspec failure tracking
16
+ .rspec_status
17
+
18
+ *.gem
19
+ .byebug_history
20
+
21
+ Gemfile.lock
22
+ gemfiles/*.lock
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,9 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.5.1
5
+ before_install:
6
+ - gem install bundler -v 1.16.1
7
+ - bundle exec appraisal install
8
+ script:
9
+ - bundle exec appraisal rake
data/Appraisals ADDED
@@ -0,0 +1,7 @@
1
+ appraise 'rails-4' do
2
+ gem 'rails', '4.2.10'
3
+ end
4
+
5
+ appraise 'rails-5' do
6
+ gem 'rails', '5.2.1'
7
+ end
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ group :development, :test do
6
+ gem 'factory_bot_rails', require: false
7
+ end
8
+
9
+ # Specify your gem's dependencies in quickery.gemspec
10
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,163 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ quickery (0.1.0)
5
+ activerecord
6
+ activesupport
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ actionpack (5.2.0)
12
+ actionview (= 5.2.0)
13
+ activesupport (= 5.2.0)
14
+ rack (~> 2.0)
15
+ rack-test (>= 0.6.3)
16
+ rails-dom-testing (~> 2.0)
17
+ rails-html-sanitizer (~> 1.0, >= 1.0.2)
18
+ actionview (5.2.0)
19
+ activesupport (= 5.2.0)
20
+ builder (~> 3.1)
21
+ erubi (~> 1.4)
22
+ rails-dom-testing (~> 2.0)
23
+ rails-html-sanitizer (~> 1.0, >= 1.0.3)
24
+ activemodel (5.2.0)
25
+ activesupport (= 5.2.0)
26
+ activerecord (5.2.0)
27
+ activemodel (= 5.2.0)
28
+ activesupport (= 5.2.0)
29
+ arel (>= 9.0)
30
+ activesupport (5.2.0)
31
+ concurrent-ruby (~> 1.0, >= 1.0.2)
32
+ i18n (>= 0.7, < 2)
33
+ minitest (~> 5.1)
34
+ tzinfo (~> 1.1)
35
+ appraisal (2.2.0)
36
+ bundler
37
+ rake
38
+ thor (>= 0.14.0)
39
+ arel (9.0.0)
40
+ builder (3.2.3)
41
+ byebug (9.1.0)
42
+ coderay (1.1.2)
43
+ combustion (0.9.1)
44
+ activesupport (>= 3.0.0)
45
+ railties (>= 3.0.0)
46
+ thor (>= 0.14.6)
47
+ concurrent-ruby (1.0.5)
48
+ crass (1.0.4)
49
+ database_cleaner (1.7.0)
50
+ diff-lcs (1.3)
51
+ erubi (1.7.1)
52
+ factory_bot (4.10.0)
53
+ activesupport (>= 3.0.0)
54
+ factory_bot_rails (4.10.0)
55
+ factory_bot (~> 4.10.0)
56
+ railties (>= 3.0.0)
57
+ faker (1.9.1)
58
+ i18n (>= 0.7)
59
+ ffi (1.9.25)
60
+ formatador (0.2.5)
61
+ guard (2.14.2)
62
+ formatador (>= 0.2.4)
63
+ listen (>= 2.7, < 4.0)
64
+ lumberjack (>= 1.0.12, < 2.0)
65
+ nenv (~> 0.1)
66
+ notiffany (~> 0.0)
67
+ pry (>= 0.9.12)
68
+ shellany (~> 0.0)
69
+ thor (>= 0.18.1)
70
+ guard-compat (1.2.1)
71
+ guard-rspec (4.7.3)
72
+ guard (~> 2.1)
73
+ guard-compat (~> 1.1)
74
+ rspec (>= 2.99.0, < 4.0)
75
+ i18n (1.0.1)
76
+ concurrent-ruby (~> 1.0)
77
+ listen (3.1.5)
78
+ rb-fsevent (~> 0.9, >= 0.9.4)
79
+ rb-inotify (~> 0.9, >= 0.9.7)
80
+ ruby_dep (~> 1.2)
81
+ loofah (2.2.2)
82
+ crass (~> 1.0.2)
83
+ nokogiri (>= 1.5.9)
84
+ lumberjack (1.0.13)
85
+ method_source (0.9.0)
86
+ mini_portile2 (2.3.0)
87
+ minitest (5.11.3)
88
+ nenv (0.3.0)
89
+ nokogiri (1.8.4)
90
+ mini_portile2 (~> 2.3.0)
91
+ notiffany (0.1.1)
92
+ nenv (~> 0.1)
93
+ shellany (~> 0.0)
94
+ pry (0.11.3)
95
+ coderay (~> 1.1.0)
96
+ method_source (~> 0.9.0)
97
+ rack (2.0.5)
98
+ rack-test (1.1.0)
99
+ rack (>= 1.0, < 3)
100
+ rails-dom-testing (2.0.3)
101
+ activesupport (>= 4.2.0)
102
+ nokogiri (>= 1.6)
103
+ rails-html-sanitizer (1.0.4)
104
+ loofah (~> 2.2, >= 2.2.2)
105
+ railties (5.2.0)
106
+ actionpack (= 5.2.0)
107
+ activesupport (= 5.2.0)
108
+ method_source
109
+ rake (>= 0.8.7)
110
+ thor (>= 0.18.1, < 2.0)
111
+ rake (10.5.0)
112
+ rb-fsevent (0.10.3)
113
+ rb-inotify (0.9.10)
114
+ ffi (>= 0.5.0, < 2)
115
+ rspec (3.7.0)
116
+ rspec-core (~> 3.7.0)
117
+ rspec-expectations (~> 3.7.0)
118
+ rspec-mocks (~> 3.7.0)
119
+ rspec-core (3.7.1)
120
+ rspec-support (~> 3.7.0)
121
+ rspec-expectations (3.7.0)
122
+ diff-lcs (>= 1.2.0, < 2.0)
123
+ rspec-support (~> 3.7.0)
124
+ rspec-mocks (3.7.0)
125
+ diff-lcs (>= 1.2.0, < 2.0)
126
+ rspec-support (~> 3.7.0)
127
+ rspec-rails (3.7.2)
128
+ actionpack (>= 3.0)
129
+ activesupport (>= 3.0)
130
+ railties (>= 3.0)
131
+ rspec-core (~> 3.7.0)
132
+ rspec-expectations (~> 3.7.0)
133
+ rspec-mocks (~> 3.7.0)
134
+ rspec-support (~> 3.7.0)
135
+ rspec-support (3.7.1)
136
+ ruby_dep (1.5.0)
137
+ shellany (0.0.1)
138
+ sqlite3 (1.3.13)
139
+ thor (0.20.0)
140
+ thread_safe (0.3.6)
141
+ tzinfo (1.2.5)
142
+ thread_safe (~> 0.1)
143
+
144
+ PLATFORMS
145
+ ruby
146
+
147
+ DEPENDENCIES
148
+ appraisal (~> 2.2.0)
149
+ bundler (~> 1.16)
150
+ byebug (~> 9.0)
151
+ combustion (~> 0.9.1)
152
+ database_cleaner (~> 1.7.0)
153
+ factory_bot_rails
154
+ faker (~> 1.9.1)
155
+ guard-rspec (~> 4.7.3)
156
+ quickery!
157
+ rake (~> 10.0)
158
+ rspec (~> 3.7.0)
159
+ rspec-rails (~> 3.7.2)
160
+ sqlite3 (~> 1.3.13)
161
+
162
+ BUNDLED WITH
163
+ 1.16.1
data/Guardfile ADDED
@@ -0,0 +1,5 @@
1
+ guard :rspec, cmd: 'bundle exec appraisal rails-5 rspec' do
2
+ watch(%r{^spec/.+_spec\.rb$})
3
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
4
+ watch('spec/spec_helper.rb') { "spec" }
5
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Jules Roman Polidario
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,209 @@
1
+ # Quickery
2
+
3
+ ## About
4
+
5
+ * Implements Law of Demeter by mapping associated record attributes as own attributes (one-way read-only)
6
+ * Consequently, speeds up SQL queries by removing joins queries between intermediary models, at the cost of slower writes.
7
+ * This is an anti-normalization pattern in favour of actual data-redundancy and faster queries. Use this only as necessary.
8
+
9
+ ## Dependencies
10
+
11
+ * **Rails 4 or 5**
12
+ * **(Rails 3 still untested)**
13
+
14
+ ## Setup
15
+ 1. Add the following to your `Gemfile`:
16
+
17
+ ```ruby
18
+ gem 'quickery', '~> 0.1'
19
+ ```
20
+
21
+ 2. Run:
22
+
23
+ ```bash
24
+ bundle install
25
+ ```
26
+
27
+ ## Usage Example 1
28
+
29
+ ```ruby
30
+ # app/models/employee.rb
31
+ class Employee < ApplicationRecord
32
+ # say we have the following attributes:
33
+ # branch_id:integer
34
+ # branch_company_name:string
35
+ belongs_to :branch
36
+
37
+ quickery do
38
+ # TL;RD: the following line means:
39
+ # make sure that this record's `branch_company_name` attribute will always have
40
+ # the same value as branch.company.name and updates the value accordingly if it changes
41
+ branch.company.name == :branch_company_name
42
+
43
+ # feel free to rename :branch_company_name as you wish; it's just like any other attribute anyway
44
+ # the == is a custom overloaded operator; it does not mean "is equal" but means "should equal to"
45
+ # branch.company.name is a fluid expression that defines the attribute dependency
46
+ # `branch` and `company` does not mean `branch` and `company` record
47
+
48
+ # you may add more inside this quickery-block: i.e:
49
+ # branch.name == :branch_name
50
+ # branch.id == :branch_id
51
+ # branch.company.country.name == :branch_company_country_name
52
+ end
53
+ end
54
+
55
+ # app/models/branch.rb
56
+ class Branch < ApplicationRecord
57
+ # say we have the following attributes:
58
+ # company_id:integer
59
+ belongs_to :company
60
+ end
61
+
62
+ # app/models/company.rb
63
+ class Company < ApplicationRecord
64
+ # say we have the following attributes:
65
+ # name:string
66
+ end
67
+ ```
68
+
69
+ ```bash
70
+ # bash
71
+ rails generate migration add_branch_company_name_to_employees branch_company_name:string
72
+ bundle exec rake db:migrate
73
+ ```
74
+
75
+ ```ruby
76
+ # rails console
77
+ company = Company.create!(name: 'Jollibee')
78
+ branch = Branch.create!(company: company)
79
+ employee = Employee.create!(branch: branch)
80
+
81
+ puts employee.branch_company_name
82
+ # => 'Jollibee'
83
+
84
+ # As you can see the `branch_company_name` attribute above has the same value as the associated record's attribute
85
+ # Now, let's try updating company, and you will see below that `branch_company_name` automatically gets updated as well
86
+
87
+ company.update!(name: 'Mang Inasal')
88
+
89
+ puts employee.branch_company_name
90
+ # => 'Jollibee'
91
+
92
+ # You need to reload the object, if you expect that it's been changed:
93
+ employee.reload
94
+
95
+ puts employee.branch_company_name
96
+ # => 'Mang Inasal'
97
+
98
+ # Now, let's try updating the intermediary association, and you will see below that `branch_company_name` would be updated accordingly
99
+ other_company = Company.create!(name: 'McDonalds')
100
+ branch.update!(company: other_company)
101
+
102
+ employee.reload
103
+
104
+ puts employee.branch_company_name
105
+ # => 'McDonalds'
106
+ ```
107
+
108
+ ## Usage Example 2
109
+
110
+ * let `Branch` and `Company` model be the same as the Usage Example 1 above
111
+
112
+ ```ruby
113
+ # app/models/employee.rb
114
+ class Employee < ApplicationRecord
115
+ belongs_to :branch
116
+ belongs_to :country, foreign_key: :branch_company_id
117
+
118
+ quickery do
119
+ branch.company.id == :branch_company_id
120
+ end
121
+ end
122
+ ```
123
+
124
+ ```bash
125
+ # bash
126
+ rails generate migration add_branch_company_id_to_employees branch_company_id:bigint
127
+ bundle exec rake db:migrate
128
+ ```
129
+
130
+ ```ruby
131
+ # rails console
132
+ company = Company.create!(name: 'Jollibee')
133
+ branch = Branch.create!(company: company)
134
+ employee = Employee.create!(branch: branch)
135
+
136
+ puts employee.branch_company_id
137
+ # => 1
138
+
139
+ puts Employee.where(company: company)
140
+ # => [#<Employee id: 1>]
141
+
142
+ # as you may notice, the query above is a lot simpler and faster instead of doing it normally like below (if not using Quickery)
143
+ # you may however still using belongs_to `:through` to achieve the simplified query like above, but it's still a lot slower because of JOINS
144
+ puts Employee.joins(branch: :company).where(companies: { id: company.id })
145
+ # => [#<Employee id: 1>]
146
+ ```
147
+
148
+ ## DSL
149
+
150
+ ### For any subclass of `ActiveRecord::Base`:
151
+
152
+ #### Class Methods:
153
+
154
+ ##### `quickery(&block)`
155
+ * returns a `Quickery::AssociationBuilder` object
156
+ * block is executed in the context of the `Quickery::AssociationBuilder` object,
157
+ which means that you cannot access the model instance inside the block, as you are not supposed to.
158
+ * inside the block you may define "quickery-defined attribute mappings";
159
+ each mapping will create a `Quickery::QuickeryBuilder` object. i.e:
160
+ * `branch.company.country.category.name == :branch_company_country_category_name`
161
+ * You are required to specify `belongs_to :branch` association in this model.
162
+ * Similarly, you are required to specify `belongs_to :company` inside `Branch` model, `belongs_to :country` inside `Company` model; etc...
163
+ * each `Quickery::AssociationBuilder` defines a set of "hidden" `before_save`, `before_update`, `before_destroy`, and `before_create` callbacks across all models specified in the quickery-defined attribute association chain.
164
+ * quickery-defined attributes such as say `:branch_company_country_category_name` are updated by Quickery automatically whenever any of it's dependent records across models have been changed. Note that updates in this way do not trigger model callbacks, as I wanted to isolate logic and scope of Quickery by not triggering model callbacks that you already have.
165
+ * quickery-defined attributes such as say `:branch_company_country_category_name` are READ-only! Do not update these attributes manually. You can, but it will not automatically update the other end, and thus will break data integrity. If you want to re-update these attributes to match the other end, see `recreate_quickery_cache!` below.
166
+
167
+ ##### `quickery_builders`
168
+ * returns an `Array` of `Quickery::QuickeryBuilder` objects that have already been defined
169
+ * for more info, see `quickery(&block)` above
170
+ * you normally do not need to use this method
171
+
172
+ #### Instance Methods:
173
+
174
+ ##### `recreate_quickery_cache!`
175
+ * force-updates the quickery-defined attributes
176
+ * useful if you already have records, and you want these old records to be updated immediately
177
+ * i.e. you can do so something like the following:
178
+ ```ruby
179
+ Employee.each do |employee|
180
+ employee.recreate_quickery_cache!
181
+ end
182
+ ```
183
+
184
+ ##### `determine_quickery_value(depender_column_name)`
185
+ * returns the current "actual" supposed value of the "original" dependee column
186
+ * useful for debugging to check if the quickery-defined attributes do not have correct mapped values
187
+ * i.e. you can do something like the following:
188
+
189
+ ```ruby
190
+ employee = Employee.first
191
+ puts employee.determine_quickery_value(:branch_company_country_name)
192
+ # => 'Ireland'
193
+ ```
194
+
195
+ ## TODOs
196
+ * Possibly support two-way mapping of attributes? So that you can do, say... `employee.update!(branch_company_name: 'somenewcompanyname')`
197
+
198
+ ## Contributing
199
+ * pull requests and forks are very much welcomed! :) Let me know if you find any bug! Thanks
200
+
201
+ ## License
202
+ * MIT
203
+
204
+ ## Developer Guide
205
+ * see [developer_guide.md](developer_guide.md)
206
+
207
+ ## Changelog
208
+ * 0.1.0
209
+ * initial beta release
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "quickery"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
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
data/config.ru ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubygems"
4
+ require "bundler"
5
+
6
+ Bundler.require :default, :development
7
+
8
+ Combustion.initialize! :all
9
+ run Combustion::Application
@@ -0,0 +1,26 @@
1
+ ## Development
2
+
3
+ ### Publishing as Ruby Gem
4
+
5
+ ```bash
6
+ # [increment gem VERSION]
7
+ gem build quickery
8
+ gem push quickery-X.X.X.gem
9
+ ```
10
+
11
+ ## Test
12
+
13
+ * Test suite makes use of the following gems worth mentioning (in addition to some others):
14
+ * [rspec-rails](https://github.com/rspec/rspec-rails)
15
+ * [combustion](https://github.com/pat/combustion)
16
+ * [appraisal](https://github.com/thoughtbot/appraisal)
17
+
18
+ ```bash
19
+ # to auto-test specs whenever a spec file has been modified:
20
+ bundle exec guard
21
+
22
+ # to manually run specs for a particular rails version (for more info: see Appraisals file):
23
+ bundle exec appraisal rails-5 rspec
24
+ # or
25
+ bundle exec appraisal rails-4 rspec
26
+ ```
@@ -0,0 +1,2 @@
1
+ ---
2
+ BUNDLE_RETRY: "1"
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "3.2.22.5"
6
+
7
+ gemspec path: "../"
@@ -0,0 +1,11 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "4.2.10"
6
+
7
+ group :development, :test do
8
+ gem "factory_bot_rails", require: false
9
+ end
10
+
11
+ gemspec path: "../"
@@ -0,0 +1,11 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "5.2.1"
6
+
7
+ group :development, :test do
8
+ gem "factory_bot_rails", require: false
9
+ end
10
+
11
+ gemspec path: "../"
@@ -0,0 +1,44 @@
1
+ require 'byebug'
2
+ require 'active_support'
3
+
4
+ module Quickery
5
+ module ActiveRecordExtensions
6
+ module DSL
7
+ def self.included(base)
8
+ base.extend ClassMethods
9
+ base.include InstanceMethods
10
+ end
11
+
12
+ module ClassMethods
13
+ def quickery(&block)
14
+ association_builder = AssociationBuilder.new(model: self)
15
+ association_builder.instance_exec(&block)
16
+ end
17
+ end
18
+
19
+ module InstanceMethods
20
+ def recreate_quickery_cache!
21
+ self.class.quickery_builders.each do |depender_column_name, quickery_builder|
22
+ new_value = determine_quickery_value(depender_column_name)
23
+ update_columns(depender_column_name => new_value)
24
+ end
25
+
26
+ true
27
+ end
28
+
29
+ def determine_quickery_value(depender_column_name)
30
+ quickery_builder = self.class.quickery_builders[depender_column_name]
31
+
32
+ raise ArgumentError, "No defined quickery builder for #{depender_column_name}. Defined values are #{self.class.quickery_builders.keys}" unless quickery_builder
33
+
34
+ dependee_record = quickery_builder.first_association_builder._quickery_dependee_record(self)
35
+ dependee_record.send(quickery_builder.dependee_column_name)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ ActiveSupport.on_load(:active_record) do
43
+ include Quickery::ActiveRecordExtensions::DSL
44
+ end
@@ -0,0 +1,108 @@
1
+ module Quickery
2
+ class AssociationBuilder
3
+ attr_reader :model
4
+ attr_reader :parent_builder
5
+ attr_reader :child_builder
6
+ attr_reader :inverse_association_name
7
+ attr_reader :belongs_to
8
+
9
+ def initialize(model:, parent_builder: nil, inverse_association_name: nil)
10
+ @model = model
11
+ @parent_builder = parent_builder
12
+ @inverse_association_name = inverse_association_name
13
+ @reflections = model.reflections
14
+ @belongs_to_association_names = @reflections.map{ |key, value| value.macro == :belongs_to ? key : nil }.compact
15
+ @column_names = model.column_names
16
+ end
17
+
18
+ # we need to prepend _quickery to all methods, to make sure no conflicts with association names that are dynamically invoked through `method_missing` below
19
+
20
+ def _quickery_get_child_builders(include_self: false, builders: [])
21
+ builders << self if include_self
22
+
23
+ if @child_builder.nil?
24
+ builders
25
+ else
26
+ builders << @child_builder
27
+ return @child_builder._quickery_get_child_builders(builders: builders)
28
+ end
29
+ end
30
+
31
+ def _quickery_get_parent_builders(include_self: false, builders: [])
32
+ builders << self if include_self
33
+
34
+ if @parent_builder.nil?
35
+ builders
36
+ else
37
+ builders << @parent_builder
38
+ @parent_builder._quickery_get_parent_builders(builders: builders)
39
+ end
40
+ end
41
+
42
+ def _quickery_get_joins_arg(current_joins_arg = nil)
43
+ if @parent_builder.nil?
44
+ current_joins_arg
45
+ else
46
+ if current_joins_arg.nil?
47
+ @parent_builder._quickery_get_joins_arg(@inverse_association_name.to_sym)
48
+ else
49
+ @parent_builder._quickery_get_joins_arg({ @inverse_association_name.to_sym => current_joins_arg })
50
+ end
51
+ end
52
+ end
53
+
54
+ def _quickery_dependent_records(record_to_be_saved)
55
+ primary_key_value = record_to_be_saved.send(record_to_be_saved.class.primary_key)
56
+ most_parent_model = _quickery_get_parent_builders.last.model
57
+
58
+ records = most_parent_model.all
59
+
60
+ unless (joins_arg = _quickery_get_joins_arg).nil?
61
+ records = records.joins(joins_arg)
62
+ end
63
+
64
+ records = records.where(
65
+ model.table_name => {
66
+ model.primary_key => primary_key_value
67
+ }
68
+ )
69
+ end
70
+
71
+ def _quickery_dependee_record(record_to_be_saved)
72
+ raise ArgumentError, 'argument should be an instance of @model' unless record_to_be_saved.is_a? model
73
+
74
+ _quickery_get_child_builders(include_self: true).inject(record_to_be_saved) do |record, association_builder|
75
+ if association_builder.belongs_to
76
+ record.send(association_builder.belongs_to.name)
77
+ else
78
+ record
79
+ end
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ def method_missing(method_name, *args, &block)
86
+ method_name_str = method_name.to_s
87
+ if @belongs_to_association_names.include? method_name_str
88
+ @belongs_to = @reflections[method_name_str]
89
+ @child_builder = AssociationBuilder.new(model: belongs_to.class_name.constantize, parent_builder: self, inverse_association_name: method_name_str)
90
+ elsif @column_names.include? method_name_str
91
+ QuickeryBuilder.new(dependee_column_name: method_name_str, last_association_builder: self)
92
+ else
93
+ super
94
+ end
95
+ end
96
+
97
+ def respond_to_missing(method_name, include_private = false)
98
+ method_name_str = method_name.to_s
99
+ if @belongs_to_association_names.include? method_name_str
100
+ true
101
+ elsif @column_names.include? method_name_str
102
+ true
103
+ else
104
+ super
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,107 @@
1
+ module Quickery
2
+ class CallbacksBuilder
3
+ attr_reader :quickery_builder
4
+
5
+ def initialize(quickery_builder:, should_add_callbacks: true)
6
+ @quickery_builder = quickery_builder
7
+ add_callbacks if should_add_callbacks
8
+ end
9
+
10
+ private
11
+
12
+ def add_callbacks
13
+ add_callback_to_depender_model
14
+ add_callback_to_dependee_model
15
+ add_callback_to_each_intermediate_model
16
+ end
17
+
18
+ # add callback to immediately sync value after a record has been created / updated
19
+ def add_callback_to_depender_model
20
+ first_association_builder = @quickery_builder.first_association_builder
21
+ depender_column_name = @quickery_builder.depender_column_name
22
+ dependee_column_name = @quickery_builder.dependee_column_name
23
+
24
+ first_association_builder.model.class_exec do
25
+
26
+ # before create or update
27
+ before_save do
28
+ if changes.keys.include? first_association_builder.belongs_to.foreign_key
29
+ if send(first_association_builder.belongs_to.foreign_key).nil?
30
+ new_value = nil
31
+ else
32
+ dependee_record = first_association_builder._quickery_dependee_record(self)
33
+ new_value = dependee_record.send(dependee_column_name)
34
+ end
35
+
36
+ assign_attributes(depender_column_name => new_value)
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ # add callback to sync changes when dependee_column has been updated
43
+ def add_callback_to_dependee_model
44
+ last_association_builder = @quickery_builder.last_association_builder
45
+ depender_column_name = @quickery_builder.depender_column_name
46
+ dependee_column_name = @quickery_builder.dependee_column_name
47
+
48
+ last_association_builder.model.class_exec do
49
+
50
+ before_update do
51
+ if changes.keys.include? dependee_column_name
52
+ new_value = send(dependee_column_name)
53
+
54
+ dependent_records = last_association_builder._quickery_dependent_records(self)
55
+ dependent_records.update_all(depender_column_name => new_value)
56
+ end
57
+ end
58
+
59
+ before_destroy do
60
+ if attributes.keys.include? dependee_column_name
61
+ new_value = nil
62
+
63
+ dependent_records = last_association_builder._quickery_dependent_records(self)
64
+ dependent_records.update_all(depender_column_name => new_value)
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ # also add callbacks to sync changes when intermediary associations have been changed (this does not include first and last builder)
71
+ def add_callback_to_each_intermediate_model
72
+ last_association_builder = @quickery_builder.last_association_builder
73
+ depender_column_name = @quickery_builder.depender_column_name
74
+ dependee_column_name = @quickery_builder.dependee_column_name
75
+
76
+ last_association_builder._quickery_get_parent_builders(include_self: true)[1..-2].each do |association_builder|
77
+ intermediate_model = association_builder.model
78
+
79
+ intermediate_model.class_exec do
80
+
81
+ before_update do
82
+ if changes.keys.include? association_builder.belongs_to.foreign_key
83
+ if send(association_builder.belongs_to.foreign_key).nil?
84
+ new_value = nil
85
+ else
86
+ dependee_record = association_builder._quickery_dependee_record(self)
87
+ new_value = dependee_record.send(dependee_column_name)
88
+ end
89
+
90
+ dependent_records = association_builder._quickery_dependent_records(self)
91
+ dependent_records.update_all(depender_column_name => new_value)
92
+ end
93
+ end
94
+
95
+ before_destroy do
96
+ if attributes.keys.include? association_builder.belongs_to.foreign_key
97
+ new_value = nil
98
+
99
+ dependent_records = last_association_builder._quickery_dependent_records(self)
100
+ dependent_records.update_all(depender_column_name => new_value)
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,41 @@
1
+ module Quickery
2
+ class QuickeryBuilder
3
+ attr_reader :model
4
+ attr_reader :depender_column_name
5
+ attr_reader :dependee_column_name
6
+ attr_reader :first_association_builder
7
+ attr_reader :last_association_builder
8
+ attr_reader :callbacks_builder
9
+
10
+ def initialize(dependee_column_name:, last_association_builder:)
11
+ @dependee_column_name = dependee_column_name
12
+ @last_association_builder = last_association_builder
13
+ @first_association_builder = last_association_builder._quickery_get_parent_builders.last
14
+ @model = @first_association_builder.model
15
+ end
16
+
17
+ def ==(depender_column_name)
18
+ @depender_column_name = depender_column_name
19
+
20
+ @callbacks_builder = CallbacksBuilder.new(quickery_builder: self)
21
+
22
+ define_quickery_builders_in_model_class unless @model.respond_to? :quickery_builders
23
+
24
+ # include this to the list of quickery builders defined for this model
25
+ @model.quickery_builders[depender_column_name] = self
26
+ end
27
+
28
+ private
29
+
30
+ def define_quickery_builders_in_model_class
31
+ # set default empty Hash if first time setting quickery_builders
32
+ @model.class_eval do
33
+ @quickery_builders = {}
34
+
35
+ class << self
36
+ attr_reader :quickery_builders
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,3 @@
1
+ module Quickery
2
+ VERSION = '0.1.0'
3
+ end
data/lib/quickery.rb ADDED
@@ -0,0 +1,6 @@
1
+ Dir[__dir__ + '/quickery/*.rb'].each {|file| require file }
2
+ Dir[__dir__ + '/quickery/active_record_extensions/*.rb'].each {|file| require file }
3
+
4
+ module Quickery
5
+ # Your code goes here...
6
+ end
data/quickery.gemspec ADDED
@@ -0,0 +1,38 @@
1
+
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'quickery/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'quickery'
8
+ spec.version = Quickery::VERSION
9
+ spec.authors = ['Jules Roman Polidario']
10
+ spec.email = ['jrpolidario@gmail.com']
11
+
12
+ spec.summary = 'Database Anti-normalization pattern implementing Law of Demeter by mapping associated record attributes as own attributes (one-way read-only), and therefore improves query speeds at the cost of slower writes.'
13
+ spec.description = 'Implements Law of Demeter by mapping associated record attributes as own attributes (one-way read-only). Consequently, speeds up SQL queries by removing joins queries between intermediary models, at the cost of slower writes. This is an anti-normalization pattern in favour of actual data-redundancy and faster queries. Use this only as necessary.'
14
+ spec.homepage = 'https://github.com/jrpolidario/quickery'
15
+ spec.license = 'MIT'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ spec.bindir = 'exe'
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ['lib']
23
+
24
+ spec.add_dependency 'activerecord', '~> 4.0'
25
+ spec.add_dependency 'activesupport', '~> 4.0'
26
+
27
+ spec.add_development_dependency 'byebug', '~> 9.0'
28
+ spec.add_development_dependency 'bundler', '~> 1.16'
29
+ spec.add_development_dependency 'rake', '~> 10.0'
30
+ spec.add_development_dependency 'rspec', '~> 3.7.0'
31
+ spec.add_development_dependency 'rspec-rails', '~> 3.7.2'
32
+ spec.add_development_dependency 'sqlite3', '~> 1.3.13'
33
+ spec.add_development_dependency 'database_cleaner', '~> 1.7.0'
34
+ spec.add_development_dependency 'faker', '~> 1.9.1'
35
+ spec.add_development_dependency 'combustion', '~> 0.9.1'
36
+ spec.add_development_dependency 'guard-rspec', '~> 4.7.3'
37
+ spec.add_development_dependency 'appraisal', '~> 2.2.0'
38
+ end
metadata ADDED
@@ -0,0 +1,257 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: quickery
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jules Roman Polidario
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-09-05 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.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.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '4.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '4.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: byebug
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '9.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '9.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.16'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.16'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '10.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '10.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 3.7.0
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 3.7.0
97
+ - !ruby/object:Gem::Dependency
98
+ name: rspec-rails
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 3.7.2
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 3.7.2
111
+ - !ruby/object:Gem::Dependency
112
+ name: sqlite3
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: 1.3.13
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: 1.3.13
125
+ - !ruby/object:Gem::Dependency
126
+ name: database_cleaner
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: 1.7.0
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: 1.7.0
139
+ - !ruby/object:Gem::Dependency
140
+ name: faker
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: 1.9.1
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: 1.9.1
153
+ - !ruby/object:Gem::Dependency
154
+ name: combustion
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: 0.9.1
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: 0.9.1
167
+ - !ruby/object:Gem::Dependency
168
+ name: guard-rspec
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: 4.7.3
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: 4.7.3
181
+ - !ruby/object:Gem::Dependency
182
+ name: appraisal
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - "~>"
186
+ - !ruby/object:Gem::Version
187
+ version: 2.2.0
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - "~>"
193
+ - !ruby/object:Gem::Version
194
+ version: 2.2.0
195
+ description: Implements Law of Demeter by mapping associated record attributes as
196
+ own attributes (one-way read-only). Consequently, speeds up SQL queries by removing
197
+ joins queries between intermediary models, at the cost of slower writes. This is
198
+ an anti-normalization pattern in favour of actual data-redundancy and faster queries.
199
+ Use this only as necessary.
200
+ email:
201
+ - jrpolidario@gmail.com
202
+ executables: []
203
+ extensions: []
204
+ extra_rdoc_files: []
205
+ files:
206
+ - ".gitignore"
207
+ - ".rspec"
208
+ - ".travis.yml"
209
+ - Appraisals
210
+ - Gemfile
211
+ - Gemfile.lock
212
+ - Guardfile
213
+ - LICENSE.txt
214
+ - README.md
215
+ - Rakefile
216
+ - bin/console
217
+ - bin/setup
218
+ - config.ru
219
+ - developer_guide.md
220
+ - gemfiles/.bundle/config
221
+ - gemfiles/rails_3.gemfile
222
+ - gemfiles/rails_4.gemfile
223
+ - gemfiles/rails_5.gemfile
224
+ - lib/quickery.rb
225
+ - lib/quickery/active_record_extensions/dsl.rb
226
+ - lib/quickery/association_builder.rb
227
+ - lib/quickery/callbacks_builder.rb
228
+ - lib/quickery/quickery_builder.rb
229
+ - lib/quickery/version.rb
230
+ - quickery.gemspec
231
+ homepage: https://github.com/jrpolidario/quickery
232
+ licenses:
233
+ - MIT
234
+ metadata: {}
235
+ post_install_message:
236
+ rdoc_options: []
237
+ require_paths:
238
+ - lib
239
+ required_ruby_version: !ruby/object:Gem::Requirement
240
+ requirements:
241
+ - - ">="
242
+ - !ruby/object:Gem::Version
243
+ version: '0'
244
+ required_rubygems_version: !ruby/object:Gem::Requirement
245
+ requirements:
246
+ - - ">="
247
+ - !ruby/object:Gem::Version
248
+ version: '0'
249
+ requirements: []
250
+ rubyforge_project:
251
+ rubygems_version: 2.7.6
252
+ signing_key:
253
+ specification_version: 4
254
+ summary: Database Anti-normalization pattern implementing Law of Demeter by mapping
255
+ associated record attributes as own attributes (one-way read-only), and therefore
256
+ improves query speeds at the cost of slower writes.
257
+ test_files: []