active_record-framing 0.1.0.pre.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: db9fa84eb9a0af089cf82457843e6b46c05178f0dd3679253df766ba17755ec1
4
+ data.tar.gz: 37c02526ee91a9fbe029b9f4f740cac732738ddf9c18a189f7b6ba56a2614c10
5
+ SHA512:
6
+ metadata.gz: 734a4dff29f64d57563c622088256f8e3f12d992d4f4943c8eec959e47bab3e0ce91cf4058691e7b7a391cc98a4d35b689ec95c5ac6d15d13bdd69c5cb99fa04
7
+ data.tar.gz: 3abe8e57cf9d98312ab3e651fe48964854a03bb68e1577755249ad0dd33ba4dee615246229acf5c299d4593e8ea44ede1115a309b1de721ea259b5e7a02145f4
data/CHANGELOG.md ADDED
@@ -0,0 +1,53 @@
1
+ # DeletedAt
2
+
3
+ ## 0.5.0 _(June 25, 2018)_
4
+ - Removed use of invasive views in preference of sub-selects
5
+ - Dropped support for Ruby 2.0, 2.1, 2.2
6
+ - Dropped support for Rails 4.1
7
+ - Default `active_record-framing` options using `Proc`
8
+
9
+ ## 0.4.0 _(Never Released)_
10
+ - Specs for Rails 4.0-5.1
11
+ - Uses `combustion` gem for cleaner and more comprehensive testing
12
+ - Added badges to ReadMe
13
+ - Using `:prepend` to leverage ancestry chain
14
+ - Add logger for internal use
15
+ - DRYd up init code
16
+ - Removed partially supported features
17
+ - Added DSL in migrations/schema for adding `active_record-framing` timestamps to tables
18
+
19
+ ## 0.3.0 _(May 10, 2017)_
20
+ - Add specs
21
+ - Clean up dependencies
22
+ - Auto-init models after installing views
23
+ - Remove chained `create` methods
24
+
25
+ ## 0.2.6 _(April 06, 2017)_
26
+ - Add warning when no DB connection present
27
+
28
+ ## 0.2.5 _(March 28, 2017)_
29
+ - Extract injections to `.load` method
30
+
31
+ ## 0.2.4 _(February 03, 2017)_
32
+ - Use `becomes` to mask `::All` etc classes
33
+
34
+ ## 0.2.3 _(February 03, 2017)_
35
+ - Chain `create!` method to work properly
36
+
37
+ ## 0.2.2 _(February 03, 2017)_
38
+ - Chain `create` method to work properly
39
+
40
+ ## 0.2.1 _(February 03, 2017)_
41
+ - More reliable table name handling
42
+ - Changed API for installing views (e.g. `destroy_deleted_view`, `uninstall_deleted_view`)
43
+
44
+ ## 0.1.1 _(January 31, 2017)_
45
+ - Added instructions to readme
46
+ - Fixes stack-too-deep edge-case (by moving to `:include` over `:prepend`)
47
+
48
+ ## 0.1.0 _(January 30, 2017)_
49
+ - Renames primary table to `model_name/all`
50
+ - Creates views for each model using `active_record-framing`
51
+ - `model_name/deleted`
52
+ - `model_name/present`
53
+ - Classes created to read from views (`::All`, `::Present`, `::Deleted`)
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2019 Dale Stevens
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,91 @@
1
+ [![License ](https://img.shields.io/github/license/TwilightCoders/active_record-framing.svg)]()
2
+ [![Version ](https://img.shields.io/gem/v/active_record-framing.svg)](https://rubygems.org/gems/active_record-framing)
3
+ [![Build Status](https://travis-ci.org/TwilightCoders/active_record-framing.svg)](https://travis-ci.org/TwilightCoders/active_record-framing)
4
+ [![Maintenence ](https://api.codeclimate.com/v1/badges/762cdcd63990efa768b0/maintainability)](https://codeclimate.com/github/TwilightCoders/active_record-framing/maintainability)
5
+ [![Coverage ](https://codeclimate.com/github/TwilightCoders/active_record-framing/badges/coverage.svg)](https://codeclimate.com/github/TwilightCoders/active_record-framing/coverage)
6
+ [![Dependencies](https://img.shields.io/librariesio/github/twilightcoders/active_record-framing.svg)](https://depfu.com/github/TwilightCoders/active_record-framing)
7
+
8
+ # ActiveRecord::Framing
9
+
10
+ Works similar to `scopes`. Rather than modifying the where clause of the `ActiveRecord::Relation`, it creates a common table expression (CTE) to be applied upon execution.
11
+
12
+ Unlike scopes, they do not affect the values of attributes upon creation.
13
+
14
+ ## Requirements
15
+
16
+ - Ruby 2.3+
17
+ - ActiveRecord 4.2+
18
+
19
+ ## Installation
20
+
21
+ Add this line to your application's Gemfile:
22
+
23
+ ```ruby
24
+ gem 'active_record-framing'
25
+ ```
26
+
27
+ And then execute:
28
+
29
+ $ bundle
30
+
31
+ Or install it yourself as:
32
+
33
+ $ gem install active_record-framing
34
+
35
+ ## Usage
36
+
37
+ Any `ActiveRecord::Base` descendant has access to two additional methods: `frame` and `default_frame`.
38
+
39
+ ```ruby
40
+ class User < ActiveRecord::Base
41
+ default_frame { where(active: true) }
42
+ # ...
43
+ end
44
+ ```
45
+ Afterwards, `User.all.to_sql` yields
46
+ ```sql
47
+ WITH "users" AS
48
+ (SELECT "users".* FROM "users" WHERE "users"."active" = true)
49
+ SELECT "users".* FROM "users"
50
+ ```
51
+
52
+ ```ruby
53
+ class Admin < User
54
+ default_frame('admins') { where(kind: 1) }
55
+ # ...
56
+ end
57
+ ```
58
+
59
+ Afterwards, `Admin.all.to_sql` yields
60
+ ```sql
61
+ WITH "admins" AS
62
+ (SELECT "users".* )
63
+ ```
64
+
65
+ If you're starting with a brand-new table, the existing `timestamps` DSL has been extended to accept `deleted_at: true` as an option, for convenience. Or you can do it seperately as shown above.
66
+
67
+ ```ruby
68
+ class CreatCommentsTable < ActiveRecord::Migration
69
+
70
+ def change
71
+ create_table :comments do |t|
72
+ # ...
73
+ # to the `timestamps` DSL
74
+ t.timestamps null: false, deleted_at: true
75
+ end
76
+ end
77
+
78
+ end
79
+ ```
80
+
81
+ ## Development
82
+
83
+ After checking out the repo, run `bundle` to install dependencies. Then, run `bundle exec rspec` to run the tests.
84
+
85
+ ## Contributing
86
+
87
+ Bug reports and pull requests are welcome on GitHub at https://github.com/TwilightCoders/active_record-framing. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
88
+
89
+ ## License
90
+
91
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
@@ -0,0 +1,21 @@
1
+ module ActiveRecord
2
+ module Framing
3
+ module AttributeMethods
4
+ extend ActiveSupport::Concern
5
+
6
+ BLACKLISTED_CLASS_CONSTS = %w(unframed)
7
+
8
+ module ClassMethods
9
+ # A class const is 'dangerous' if it is already defined by Active Record, but
10
+ # not by any ancestors. (So 'All' is not dangerous but 'Frameless' is.)
11
+ def dangerous_class_const?(const_name)
12
+ BLACKLISTED_CLASS_CONSTS.include?(const_name.to_s) || class_const_defined_within?(const_name, Base)
13
+ end
14
+
15
+ def class_const_defined_within?(name, klass, superklass = klass.superclass) # :nodoc:
16
+ klass.const_defined?(name) || superklass.const_defined?(name)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/per_thread_registry"
4
+
5
+ module ActiveRecord
6
+ module Framing
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ include Default
11
+ include Named
12
+ include AttributeMethods
13
+ end
14
+
15
+ module ClassMethods # :nodoc:
16
+ def current_frame
17
+ FrameRegistry.value_for(:current_frame, self)
18
+ end
19
+
20
+ def current_frame=(frame)
21
+ FrameRegistry.set_value_for(:current_frame, self, frame)
22
+ end
23
+ end
24
+
25
+ # This class stores the +:current_frame+ and +:ignore_default_frame+ values
26
+ # for different classes. The registry is stored as a thread local, which is
27
+ # accessed through +FrameRegistry.current+.
28
+ #
29
+ # This class allows you to store and get the frame values on different
30
+ # classes and different types of frames. For example, if you are attempting
31
+ # to get the current_frame for the +Board+ model, then you would use the
32
+ # following code:
33
+ #
34
+ # registry = ActiveRecord::Framing::FrameRegistry
35
+ # registry.set_value_for(:current_frame, Board, some_new_frame)
36
+ #
37
+ # Now when you run:
38
+ #
39
+ # registry.value_for(:current_frame, Board)
40
+ #
41
+ # You will obtain whatever was defined in +some_new_frame+. The #value_for
42
+ # and #set_value_for methods are delegated to the current FrameRegistry
43
+ # object, so the above example code can also be called as:
44
+ #
45
+ # ActiveRecord::Framing::FrameRegistry.set_value_for(:current_frame,
46
+ # Board, some_new_frame)
47
+ class FrameRegistry # :nodoc:
48
+ extend ActiveSupport::PerThreadRegistry
49
+
50
+ VALID_SCOPE_TYPES = [:current_frame, :ignore_default_frame]
51
+
52
+ def initialize
53
+ @registry = Hash.new { |hash, key| hash[key] = {} }
54
+ end
55
+
56
+ # Obtains the value for a given +frame_type+ and +model+.
57
+ def value_for(frame_type, model)
58
+ raise_invalid_frame_type!(frame_type)
59
+ return @registry[frame_type][model.name]
60
+ end
61
+
62
+ # def value_for(frame_type, model, skip_inherited_frame = false)
63
+ # raise_invalid_frame_type!(frame_type)
64
+ # return @registry[frame_type][model.name] if skip_inherited_frame
65
+ # klass = model
66
+ # base = model.base_class
67
+ # while klass <= base
68
+ # value = @registry[frame_type][klass.name]
69
+ # return value if value
70
+ # klass = klass.superclass
71
+ # end
72
+ # end
73
+
74
+ # Sets the +value+ for a given +frame_type+ and +model+.
75
+ def set_value_for(frame_type, model, value)
76
+ raise_invalid_frame_type!(frame_type)
77
+ @registry[frame_type][model.name] = value
78
+ end
79
+
80
+ private
81
+
82
+ def raise_invalid_frame_type!(frame_type)
83
+ if !VALID_SCOPE_TYPES.include?(frame_type)
84
+ raise ArgumentError, "Invalid frame type '#{frame_type}' sent to the registry. Frame types must be included in VALID_SCOPE_TYPES"
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Framing
5
+ module Default
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ # Stores the default frame for the class.
10
+ class_attribute :default_frames, instance_writer: false, instance_predicate: false
11
+ class_attribute :default_frame_override, instance_writer: false, instance_predicate: false
12
+
13
+ self.default_frames = []
14
+ self.default_frame_override = nil
15
+ end
16
+
17
+ module ClassMethods
18
+ # Returns a frame for the model without the previously set frames.
19
+ #
20
+ # class Post < ActiveRecord::Base
21
+ # def self.default_frame
22
+ # where(published: true)
23
+ # end
24
+ # end
25
+ #
26
+ # Post.all # Fires "WITH posts AS (SELECT * FROM posts WHERE published = true) SELECT * FROM posts"
27
+ # Post.unframed.all # Fires "SELECT * FROM posts"
28
+ # Post.where(published: false).unframed.all # Fires "SELECT * FROM posts"
29
+ #
30
+ # This method also accepts a block. All queries inside the block will
31
+ # not use the previously set frames.
32
+ #
33
+ # Post.unframed {
34
+ # Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10"
35
+ # }
36
+ def unframed
37
+ block_given? ? relation.framing { yield } : relation
38
+ end
39
+
40
+ # Are there attributes associated with this frame?
41
+ def frame_attributes? # :nodoc:
42
+ super || default_frames.any? || respond_to?(:default_frame)
43
+ end
44
+
45
+ def before_remove_const #:nodoc:
46
+ self.current_frame = nil
47
+ end
48
+
49
+ def ignore_default_frame?
50
+ FrameRegistry.value_for(:ignore_default_frame, base_class)
51
+ end
52
+
53
+ private
54
+
55
+ # Use this macro in your model to set a default frame for all operations on
56
+ # the model.
57
+ #
58
+ # class Article < ActiveRecord::Base
59
+ # default_frame { where(published: true) }
60
+ # end
61
+ #
62
+ # Article.all # => # Fires "WITH articles AS (SELECT * FROM articles WHERE published = true) SELECT * FROM articles"
63
+ #
64
+ # The #default_frame is not applied while updating/creating/building a record.
65
+ #
66
+ # Article.new.published # => nil
67
+ # Article.create.published # => nil
68
+ # Article.first.update(name: 'A Tale of Two Cities').published # => nil
69
+ #
70
+ # (You can also pass any object which responds to +call+ to the
71
+ # +default_frame+ macro, and it will be called when building the
72
+ # default frame.)
73
+ #
74
+ # If you use multiple #default_frame declarations in your model then
75
+ # they will be merged together:
76
+ #
77
+ # class Article < ActiveRecord::Base
78
+ # default_frame { where(published: true) }
79
+ # default_frame { where(rating: 'G') }
80
+ # end
81
+ #
82
+ # Article.all # => WITH articles AS (SELECT * FROM articles WHERE published = true AND rating = 'G') SELECT * FROM articles
83
+ #
84
+ # This is also the case with inheritance and module includes where the
85
+ # parent or module defines a #default_frame and the child or including
86
+ # class defines a second one.
87
+ #
88
+ # If you need to do more complex things with a default frame, you can
89
+ # alternatively define it as a class method:
90
+ #
91
+ # class Article < ActiveRecord::Base
92
+ # def self.default_frame
93
+ # # Should return a frame, you can call 'super' here etc.
94
+ # end
95
+ # end
96
+ def default_frame(frame = nil) # :doc:
97
+ frame = Proc.new if block_given?
98
+
99
+ if frame.is_a?(Relation) || !frame.respond_to?(:call)
100
+ raise ArgumentError,
101
+ "Support for calling #default_frame without a block is removed. For example instead " \
102
+ "of `default_frame where(color: 'red')`, please use " \
103
+ "`default_frame { where(color: 'red') }`. (Alternatively you can just redefine " \
104
+ "self.default_frame.)"
105
+ end
106
+
107
+ self.default_frames += [frame]
108
+ end
109
+
110
+ def build_default_frame(base_rel = nil)
111
+ return if abstract_class?
112
+
113
+ if default_frame_override.nil?
114
+ self.default_frame_override = !Base.is_a?(method(:default_frame).owner)
115
+ end
116
+
117
+ if default_frame_override
118
+ # The user has defined their own default frame method, so call that
119
+ # evaluate_default_frame do
120
+ # if frame = default_frame
121
+ # (base_rel ||= relation).merge!(frame)
122
+ # end
123
+ # end
124
+ warn "come back to me!"
125
+ elsif default_frames.any?
126
+ # cte_table = arel_table
127
+ cte_table = Arel::Table.new(table_name)
128
+
129
+ evaluate_default_frame do
130
+ # Create CTE here
131
+
132
+ cte_relation = default_frames.inject(relation) do |default_frame, frame|
133
+ frame = frame.respond_to?(:to_proc) ? frame : frame.method(:call)
134
+ default_frame.merge!(relation.instance_exec(&frame))
135
+ end
136
+
137
+ base_rel ||= relation
138
+ base_rel.frame!(Arel::Nodes::As.new(Arel::Table.new(table_name), cte_relation.arel))# if cte_relation
139
+ end
140
+ end
141
+ end
142
+
143
+ def ignore_default_frame=(ignore)
144
+ FrameRegistry.set_value_for(:ignore_default_frame, base_class, ignore)
145
+ end
146
+
147
+ # The ignore_default_frame flag is used to prevent an infinite recursion
148
+ # situation where a default frame references a frame which has a default
149
+ # frame which references a frame...
150
+ def evaluate_default_frame
151
+ return if ignore_default_frame?
152
+ self.ignore_default_frame = true
153
+ yield
154
+ ensure
155
+ self.ignore_default_frame = false
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,239 @@
1
+ require 'delegate'
2
+
3
+ module ActiveRecord
4
+ # = Active Record \Named \Frames
5
+ module Framing
6
+ module Named
7
+ extend ActiveSupport::Concern
8
+
9
+ module ClassMethods
10
+ # Returns an ActiveRecord::Relation frame object.
11
+ #
12
+ # posts = Post.all
13
+ # posts.size # Fires "select count(*) from posts" and returns the count
14
+ # posts.each {|p| puts p.name } # Fires "select * from posts" and loads post objects
15
+ #
16
+ # fruits = Fruit.all
17
+ # fruits = fruits.where(color: 'red') if options[:red_only]
18
+ # fruits = fruits.limit(10) if limited?
19
+ #
20
+ # You can define a frame that applies to all finders using
21
+ # {default_frame}[rdoc-ref:Framing::Default::ClassMethods#default_frame].
22
+ # def all
23
+ # super.tap do |rel|
24
+ # if frame = framed_all
25
+ # rel.with(frame)
26
+ # end
27
+ # end
28
+ # end
29
+
30
+ # alias this method?, framed_all
31
+ # def all
32
+ # # if current_scope
33
+ # # current_scope.clone
34
+ # # else
35
+ # # default_scoped
36
+ # # end
37
+ # if current_frame
38
+ # puts "#{name} has a current_frame: #{current_frame.to_sql}"
39
+ # super.frame(current_frame.clone)
40
+ # else
41
+ # puts "#{name} is using a default_frame: #{default_framed.to_sql}" if default_framed
42
+ # super.frame(default_framed)
43
+ # end
44
+ # end
45
+
46
+ def all
47
+ framed_all(super)
48
+ end
49
+
50
+ def framed_all(rel)
51
+ if current_frame = self.current_frame
52
+ if self == current_frame.klass
53
+ current_frame.clone
54
+ else
55
+ rel.merge!(current_frame)
56
+ end
57
+ else
58
+ default_framed(rel)
59
+ end
60
+ end
61
+
62
+ def const_missing(const_name)
63
+ registered_frames[const_name]&.call() || super
64
+ end
65
+
66
+ def registered_frames
67
+ @registered_frames ||= {}
68
+ end
69
+
70
+ def frame_for_association(frame = relation) # :nodoc:
71
+ current_frame = self.current_frame
72
+
73
+ if current_frame && current_frame.empty_frame?
74
+ frame
75
+ else
76
+ default_framed(frame)
77
+ end
78
+ end
79
+
80
+ # def default_framed(frame = relation) # :nodoc:
81
+ def default_framed(frame = nil) # :nodoc:
82
+ !ignore_default_frame? && build_default_frame(frame) || frame
83
+ end
84
+
85
+ # Adds a class method for retrieving and querying objects.
86
+ # The method is intended to return an ActiveRecord::Relation
87
+ # object, which is composable with other frames.
88
+ # If it returns +nil+ or +false+, an
89
+ # {all}[rdoc-ref:Framing::Named::ClassMethods#all] frame is returned instead.
90
+ #
91
+ # A \frame represents a narrowing of a database query, such as
92
+ # <tt>where(color: :red).select('shirts.*').includes(:washing_instructions)</tt>.
93
+ #
94
+ # class Shirt < ActiveRecord::Base
95
+ # frame :red, -> { where(color: 'red') }
96
+ # frame :dry_clean_only, -> { joins(:washing_instructions).where('washing_instructions.dry_clean_only = ?', true) }
97
+ # end
98
+ #
99
+ # The above calls to #frame define class methods <tt>Shirt.red</tt> and
100
+ # <tt>Shirt::DryCleanOnly</tt>. <tt>Shirt::Red</tt>, in effect,
101
+ # represents the query <tt>Shirt.where(color: 'red')</tt>.
102
+ #
103
+ # You should always pass a callable object to the frames defined
104
+ # with #frame. This ensures that the frame is re-evaluated each
105
+ # time it is called.
106
+ #
107
+ # Note that this is simply 'syntactic sugar' for defining an actual
108
+ # class method:
109
+ #
110
+ # class Shirt < ActiveRecord::Base
111
+ # def self.red
112
+ # where(color: 'red')
113
+ # end
114
+ # end
115
+ #
116
+ # Unlike <tt>Shirt.find(...)</tt>, however, the object returned by
117
+ # <tt>Shirt.red</tt> is not an Array but an ActiveRecord::Relation,
118
+ # which is composable with other frames; it resembles the association object
119
+ # constructed by a {has_many}[rdoc-ref:Associations::ClassMethods#has_many]
120
+ # declaration. For instance, you can invoke <tt>Shirt.red.first</tt>, <tt>Shirt.red.count</tt>,
121
+ # <tt>Shirt.red.where(size: 'small')</tt>. Also, just as with the
122
+ # association objects, named \frames act like an Array, implementing
123
+ # Enumerable; <tt>Shirt.red.each(&block)</tt>, <tt>Shirt.red.first</tt>,
124
+ # and <tt>Shirt.red.inject(memo, &block)</tt> all behave as if
125
+ # <tt>Shirt.red</tt> really was an array.
126
+ #
127
+ # These named \frames are composable. For instance,
128
+ # <tt>Shirt.red.dry_clean_only</tt> will produce all shirts that are
129
+ # both red and dry clean only. Nested finds and calculations also work
130
+ # with these compositions: <tt>Shirt.red.dry_clean_only.count</tt>
131
+ # returns the number of garments for which these criteria obtain.
132
+ # Similarly with <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>.
133
+ #
134
+ # All frames are available as class methods on the ActiveRecord::Base
135
+ # descendant upon which the \frames were defined. But they are also
136
+ # available to {has_many}[rdoc-ref:Associations::ClassMethods#has_many]
137
+ # associations. If,
138
+ #
139
+ # class Person < ActiveRecord::Base
140
+ # has_many :shirts
141
+ # end
142
+ #
143
+ # then <tt>elton.shirts.red.dry_clean_only</tt> will return all of
144
+ # Elton's red, dry clean only shirts.
145
+ #
146
+ # \Named frames can also have extensions, just as with
147
+ # {has_many}[rdoc-ref:Associations::ClassMethods#has_many] declarations:
148
+ #
149
+ # class Shirt < ActiveRecord::Base
150
+ # frame :red, -> { where(color: 'red') } do
151
+ # def dom_id
152
+ # 'red_shirts'
153
+ # end
154
+ # end
155
+ # end
156
+ #
157
+ # Frames cannot be used while creating/building a record.
158
+ #
159
+ # class Article < ActiveRecord::Base
160
+ # frame :published, -> { where(published: true) }
161
+ # end
162
+ #
163
+ # Article.published.new.published # => nil
164
+ # Article.published.create.published # => nil
165
+ #
166
+ # \Class methods on your model are automatically available
167
+ # on frames. Assuming the following setup:
168
+ #
169
+ # class Article < ActiveRecord::Base
170
+ # frame :published, -> { where(published: true) }
171
+ # frame :featured, -> { where(featured: true) }
172
+ #
173
+ # def self.latest_article
174
+ # order('published_at desc').first
175
+ # end
176
+ #
177
+ # def self.titles
178
+ # pluck(:title)
179
+ # end
180
+ # end
181
+ #
182
+ # We are able to call the methods like this:
183
+ #
184
+ # Article.published.featured.latest_article
185
+ # Article.featured.titles
186
+ def frame(frame_name, body, &block)
187
+ unless body.respond_to?(:call)
188
+ raise ArgumentError, "The frame body needs to be callable."
189
+ end
190
+
191
+ constant = frame_name.to_s.classify.to_sym
192
+ arel_tn = "#{frame_name}/#{self.table_name}"
193
+
194
+ the_frame = body.respond_to?(:to_proc) ? body : body.method(:call)
195
+ cte_relation = relation.merge!(relation.instance_exec(&the_frame) || relation)
196
+
197
+ # self.const_set constant, Class.new(DelegateClass(self)) do |klass|
198
+ delegator = self.name.to_sym
199
+ self.const_set(constant, self.dup).class_eval do |klass|
200
+ extend SingleForwardable
201
+ def_delegator delegator, :type_caster
202
+ def_delegator delegator, :table_name
203
+
204
+ klass.default_frames = []
205
+
206
+ @arel_table = klass.arel_table.dup.tap do |at|
207
+ at.name = arel_tn
208
+ end
209
+
210
+ klass.current_frame = build_frame(cte_relation, &block)
211
+ end
212
+
213
+ if dangerous_class_const?(constant)
214
+ raise ArgumentError, "You tried to define a frame named \"#{constant}\" " \
215
+ "on the model \"#{self.constant}\", but Active Record already defined " \
216
+ "a class method with the same name."
217
+ end
218
+
219
+ end
220
+
221
+ def build_frame(frame, &block)
222
+ extension = Module.new(&block) if block
223
+ relation.frame!(Arel::Nodes::As.new(arel_table, frame.arel)).tap do |rel|
224
+ rel.extending!(extension) if extension
225
+ end
226
+ end
227
+
228
+ private
229
+
230
+ def valid_frame_name?(name)
231
+ if respond_to?(name, true) && logger
232
+ logger.warn "Creating frame :#{name}. " \
233
+ "Overwriting existing method #{self.name}.#{name}."
234
+ end
235
+ end
236
+ end
237
+ end
238
+ end
239
+ end
@@ -0,0 +1,119 @@
1
+ module ActiveRecord
2
+ module Framing
3
+ module QueryMethods
4
+
5
+ if ::ActiveRecord.version >= Gem::Version.new("5.1") # 5.1+
6
+ def frames_values
7
+ get_value(:frames)
8
+ end
9
+ def frames_values=(value)
10
+ set_value(:frames, value)
11
+ end
12
+ ::ActiveRecord::Relation::DEFAULT_VALUES[:frames] = ::ActiveRecord::Relation::FROZEN_EMPTY_HASH
13
+ elsif ::ActiveRecord.version >= Gem::Version.new("5.0") # 5.0+
14
+ def frames_values
15
+ @values[:frames] || ::ActiveRecord::Relation::FROZEN_EMPTY_HASH
16
+ end
17
+ def frames_values=(values)
18
+ assert_mutability!
19
+ @values[:frames] = values
20
+ end
21
+ elsif ::ActiveRecord.version >= Gem::Version.new("4.2") # 4.2+
22
+ def frames_values
23
+ @values[:frames] || {}
24
+ end
25
+ def frames_values=(values)
26
+ raise ImmutableRelation if @loaded
27
+ check_cached_relation
28
+ @values[:frames] = values
29
+ end
30
+ else
31
+ raise NotImplementedError, "ActiveRecord::Framing does not support Rails #{::ActiveRecord.version}"
32
+ end
33
+
34
+ def from!(value, subquery_name = nil) # :nodoc:
35
+ super.tap do |rel|
36
+ frames_values = frames_values.merge(value.frames_values) if value.is_a?(::ActiveRecord::Relation)
37
+ end
38
+ end
39
+
40
+ def frame(value)
41
+ spawn.frame!(value)
42
+ end
43
+
44
+ def frame!(value)
45
+ if key = frame_key(value)
46
+ self.frames_values = self.frames_values.merge(key => value)
47
+ end
48
+ self
49
+ end
50
+
51
+ # Removes an unwanted relation that is already defined on a chain of relations.
52
+ # This is useful when passing around chains of relations and would like to
53
+ # modify the relations without reconstructing the entire chain.
54
+ #
55
+ # User.order('email DESC').unframe(:order) == User.all
56
+ #
57
+ # The method arguments are symbols which correspond to the names of the methods
58
+ # which should be unframed. The valid arguments are given in VALID_UNSCOPING_VALUES.
59
+ # The method can also be called with multiple arguments. For example:
60
+ #
61
+ # User.order('email DESC').select('id').where(name: "John")
62
+ # .unframe(:order, :select, :where) == User.all
63
+ #
64
+ # One can additionally pass a hash as an argument to unframe specific +:where+ values.
65
+ # This is done by passing a hash with a single key-value pair. The key should be
66
+ # +:where+ and the value should be the where value to unframe. For example:
67
+ #
68
+ # User.where(name: "John", active: true).unframe(where: :name)
69
+ # == User.where(active: true)
70
+ #
71
+ # This method is similar to #except, but unlike
72
+ # #except, it persists across merges:
73
+ #
74
+ # User.order('email').merge(User.except(:order))
75
+ # == User.order('email')
76
+ #
77
+ # User.order('email').merge(User.unframe(:order))
78
+ # == User.all
79
+ #
80
+ # This means it can be used in association definitions:
81
+ #
82
+ # has_many :comments, -> { unframe(where: :trashed) }
83
+ #
84
+ # def unframe(*args)
85
+ # check_if_method_has_arguments!(:unframe, args)
86
+ # spawn.unframe!(*args)
87
+ # end
88
+
89
+ # def unframe!(*args) # :nodoc:
90
+ # args.flatten!
91
+ # self.unframe_values += args
92
+
93
+ # args.each do |frame|
94
+ # case frame
95
+ # when Symbol
96
+ # frame = :left_outer_joins if frame == :left_joins
97
+ # if !VALID_UNSCOPING_VALUES.include?(frame)
98
+ # raise ArgumentError, "Called unframe() with invalid unframing argument ':#{frame}'. Valid arguments are :#{VALID_UNSCOPING_VALUES.to_a.join(", :")}."
99
+ # end
100
+ # set_value(frame, DEFAULT_VALUES[frame])
101
+ # when Hash
102
+ # frame.each do |key, target_value|
103
+ # if key != :where
104
+ # raise ArgumentError, "Hash arguments in .unframe(*args) must have :where as the key."
105
+ # end
106
+
107
+ # target_values = Array(target_value).map(&:to_s)
108
+ # self.where_clause = where_clause.except(*target_values)
109
+ # end
110
+ # else
111
+ # raise ArgumentError, "Unrecognized framing: #{args.inspect}. Use .unframe(where: :attribute_name) or .unframe(:order), for example."
112
+ # end
113
+ # end
114
+
115
+ # self
116
+ # end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,19 @@
1
+ require 'rails/railtie'
2
+ require 'active_record/framing/core_extension'
3
+ require 'active_record/framing/query_methods'
4
+ require 'active_record/framing/spawn_methods'
5
+ require 'active_record/framing/attribute_methods'
6
+ require 'active_record/framing/relation'
7
+
8
+ module ActiveRecord::Framing
9
+ class Railtie < Rails::Railtie
10
+ initializer 'active_record-framing.load' do |_app|
11
+ ActiveSupport.on_load(:active_record) do
12
+ ::ActiveRecord::Base.include(ActiveRecord::Framing)
13
+ ::ActiveRecord::Relation.prepend(ActiveRecord::Framing::Relation)
14
+ ::ActiveRecord::Relation.include(ActiveRecord::Framing::QueryMethods)
15
+ ::ActiveRecord::Relation.prepend(ActiveRecord::Framing::SpawnMethods)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,162 @@
1
+ module ActiveRecord
2
+ # = Active Record \Relation
3
+ module Framing
4
+ module Relation
5
+
6
+ # def arel_without_frames
7
+ # klass.ignore_default_frame
8
+ # Thread.currently(:without_frames, true) do
9
+ # @arel_without_frames ||= build_arel_without_frames
10
+ # end
11
+ # end
12
+
13
+ # def build_arel_without_frames
14
+ # @arel, old = nil, @arel
15
+ # arel
16
+ # ensure
17
+ # @arel = old
18
+ # end
19
+
20
+ def build_arel(*)
21
+ super.tap do |ar|
22
+ unless ignore_default_frame?
23
+ # alias_tracker.aliased_table_for(
24
+ # reflection.table_name,
25
+ # table_alias_for(reflection, parent, reflection != node.reflection),
26
+ # reflection.klass.type_caster
27
+ # )
28
+ build_frames(ar)
29
+
30
+ frames_values.each do |k,v|
31
+ puts "#{k} => #{v.to_sql}"
32
+ end
33
+
34
+ ar.with(*frames_values.values) if frames_values.any?
35
+ end
36
+ end
37
+ end
38
+
39
+ # This is all very unfortunate (rails 4.2):
40
+ # ActiveRecord, in it's infinite wisdom, has decided
41
+ # to create JoinDependency objects with arel_tables using a
42
+ # generic engine (ActiveRecord::Base) as opposed to that of
43
+ # the driving class. For example:
44
+ #
45
+ # #<Arel::Nodes::InnerJoin:0x00007f974e6eb008
46
+ # @left=
47
+ # #<Arel::Table:0x00007f97508f42f8
48
+ # @aliases=[],
49
+ # @columns=nil,
50
+ # @engine=ActiveRecord::Base, <=== Problem, should be `User`
51
+ # @name="users",
52
+ # @primary_key=nil,
53
+ # @table_alias=nil>,
54
+ # NOTE: In Rails 5.2 (at least) we could use the InnerJoin.left.type_caster
55
+ def build_frames(manager)
56
+ join_names = manager.join_sources.collect do |source|
57
+ source.left.name.to_s # TODO: Need to_s?
58
+ end
59
+
60
+ # scopes = klass.reflections.slice(*join_names).values.inject(Hash.new) do |collector, assoc|
61
+ # NOTE: We cannot early exclude associations because some associations are different from their table names
62
+ klass.reflect_on_all_associations.each do |assoc|
63
+ # collector.merge(assoc.klass.default_scopes) if join_names.include?(assoc.table_name)
64
+ # (collector[assoc.klass] ||= Set.new).merge(assoc.klass.default_scopes) if join_names.include?(assoc.table_name) && assoc.klass.default_scopes.any?
65
+ if join_names.include?(assoc.table_name) && assoc.klass.default_frames.any? && assoc_default_frame = assoc.klass.send(:build_default_frame)
66
+ merge!(assoc_default_frame)
67
+ # collector[assoc_default_frame.table_name] ||= assoc_default_frame
68
+ end
69
+
70
+ # collector
71
+ end
72
+ end
73
+
74
+ def frame_key(cte)
75
+ case cte
76
+ when ActiveRecord::Relation
77
+ cte.table.name
78
+ when Arel::Nodes::As
79
+ cte.left.name
80
+ when String
81
+ cte
82
+ else
83
+ nil
84
+ end
85
+ end
86
+
87
+ # def reframe(*args) # :nodoc:
88
+ # args.compact!
89
+ # args.flatten!
90
+ # # binding.pry
91
+ # self
92
+ # end
93
+
94
+ # def reframe!(*args) # :nodoc:
95
+ # args.flatten!
96
+ # self.unframe_values += args
97
+
98
+ # args.each do |frame|
99
+ # case frame
100
+ # when Symbol
101
+ # frame = :left_outer_joins if frame == :left_joins
102
+ # if !VALID_UNSCOPING_VALUES.include?(frame)
103
+ # raise ArgumentError, "Called unframe() with invalid unframing argument ':#{frame}'. Valid arguments are :#{VALID_UNSCOPING_VALUES.to_a.join(", :")}."
104
+ # end
105
+ # set_value(frame, DEFAULT_VALUES[frame])
106
+ # when Hash
107
+ # frame.each do |key, target_value|
108
+ # if key != :where
109
+ # raise ArgumentError, "Hash arguments in .unframe(*args) must have :where as the key."
110
+ # end
111
+
112
+ # target_values = Array(target_value).map(&:to_s)
113
+ # self.where_clause = where_clause.except(*target_values)
114
+ # end
115
+ # else
116
+ # raise ArgumentError, "Unrecognized framing: #{args.inspect}. Use .unframe(where: :attribute_name) or .unframe(:order), for example."
117
+ # end
118
+ # end
119
+
120
+ # self
121
+ # end
122
+
123
+ # Frame all queries to the current frame.
124
+ #
125
+ # Comment.where(post_id: 1).framing do
126
+ # Comment.first
127
+ # end
128
+ # # => WITH "comments" AS (
129
+ # # => SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 1
130
+ # # => ) SELECT "comments".* FROM "comments" ORDER BY "comments"."id" ASC LIMIT 1
131
+ #
132
+ # Please check unframed if you want to remove all previous frames (including
133
+ # the default_frame) during the execution of a block.
134
+ def framing
135
+ previous, klass.current_frame = klass.current_frame, self unless @delegate_to_klass
136
+ yield
137
+ ensure
138
+ klass.current_frame = previous unless @delegate_to_klass
139
+ end
140
+
141
+ def scoping
142
+ framing { super }
143
+ end
144
+
145
+ # def _exec_frame(*args, &block) # :nodoc:
146
+ # @delegate_to_klass = true
147
+ # instance_exec(*args, &block) || self
148
+ # ensure
149
+ # @delegate_to_klass = false
150
+ # end
151
+
152
+ # def frame_for_create
153
+ # where_values_hash.merge!(create_with_value.stringify_keys)
154
+ # end
155
+
156
+ # def empty_frame? # :nodoc:
157
+ # @values == klass.unframed.values
158
+ # end
159
+
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,13 @@
1
+ module ActiveRecord
2
+ module Framing
3
+ module SpawnMethods
4
+
5
+ def merge!(other) # :nodoc:
6
+ super.tap do |rel|
7
+ rel.frames_values = rel.frames_values.merge(other.frames_values)
8
+ end
9
+ end
10
+
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ module ActiveRecord
2
+ module Framing
3
+ VERSION = "0.1.0-1"
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ require 'active_record/framing/default'
2
+ require 'active_record/framing/named'
3
+ require 'active_record/framing/railtie' if defined?(Rails::Railtie)
metadata ADDED
@@ -0,0 +1,148 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: active_record-framing
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0.pre.1
5
+ platform: ruby
6
+ authors:
7
+ - Dale Stevens
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-05-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: '4.2'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '6'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '4.2'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '6'
33
+ - !ruby/object:Gem::Dependency
34
+ name: pg
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ type: :development
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: pry-byebug
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '3'
61
+ - !ruby/object:Gem::Dependency
62
+ name: bundler
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ - !ruby/object:Gem::Dependency
76
+ name: rake
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '12.0'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '12.0'
89
+ - !ruby/object:Gem::Dependency
90
+ name: combustion
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '0.7'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '0.7'
103
+ description: Allows for larger level scoping (framing) that affect complicated queries
104
+ more holistically
105
+ email:
106
+ - dale@twilightcoders.net
107
+ executables: []
108
+ extensions: []
109
+ extra_rdoc_files: []
110
+ files:
111
+ - CHANGELOG.md
112
+ - LICENSE
113
+ - README.md
114
+ - lib/active_record/framing.rb
115
+ - lib/active_record/framing/attribute_methods.rb
116
+ - lib/active_record/framing/core_extension.rb
117
+ - lib/active_record/framing/default.rb
118
+ - lib/active_record/framing/named.rb
119
+ - lib/active_record/framing/query_methods.rb
120
+ - lib/active_record/framing/railtie.rb
121
+ - lib/active_record/framing/relation.rb
122
+ - lib/active_record/framing/spawn_methods.rb
123
+ - lib/active_record/framing/version.rb
124
+ homepage: https://github.com/TwilightCoders/active_record-framing
125
+ licenses:
126
+ - MIT
127
+ metadata:
128
+ allowed_push_host: https://rubygems.org
129
+ post_install_message:
130
+ rdoc_options: []
131
+ require_paths:
132
+ - lib
133
+ required_ruby_version: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '2.3'
138
+ required_rubygems_version: !ruby/object:Gem::Requirement
139
+ requirements:
140
+ - - ">"
141
+ - !ruby/object:Gem::Version
142
+ version: 1.3.1
143
+ requirements: []
144
+ rubygems_version: 3.0.3
145
+ signing_key:
146
+ specification_version: 4
147
+ summary: Provides larger level scopes (frames) through the use of common table expressions.
148
+ test_files: []