through_hierarchy 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 2d022dd2e412e69e8daacf89cb32acdde7fa2aa8
4
+ data.tar.gz: d3b3db0afcabfbc671be794845a3b6fd52e9f191
5
+ SHA512:
6
+ metadata.gz: b6c87687c6180d6eb104be80238b9e9cd77f62458af378b152b31c9c1567666457f5112e183fd310eeed208be669cfc30f8c616d312c8047afbbd327fdaa7ffa
7
+ data.tar.gz: 2e39324c5fdee36ddfea5a839d58f37f93ce20410f041facfc21d38c8dd110925f8a0811bfbc89f9583fe23fe93f23a734a6aa1870d1824645082757d2c76540
@@ -0,0 +1,173 @@
1
+ module ThroughHierarchy
2
+ module Associations
3
+ class Association
4
+ def initialize(name, model, members, options = {})
5
+ @name = name
6
+ @model = model
7
+ @members = members
8
+ @as = options[:as].to_s
9
+ @scope = options[:scope]
10
+ @foreign_class_name = options[:class_name] || @name.to_s.classify
11
+ @uniq = options[:uniq]
12
+
13
+ validate_options
14
+ end
15
+
16
+ def find(instance)
17
+ return uniq_find(instance) if @uniq.present?
18
+
19
+ results = foreign_class.where(arel_instance_filters(instance))
20
+ results = results.instance_exec(&@scope) if @scope.present?
21
+ return results
22
+ end
23
+
24
+ def joins
25
+ # TODO: make this work
26
+ # joins = @model.arel_table.join(foreign_arel_table).on(arel_model_filters)
27
+
28
+ # joins = joins.join(arel_uniq_subquery).on(
29
+ # arel_hierarchy_rank.eq(uniq_subquery_arel_table[best_hierarchy_match_name]).
30
+ # and(foreign_arel_table[@uniq].eq(uniq_subquery_arel_table[@uniq]))
31
+ # ) if @uniq.present?
32
+
33
+ # results = @model.joins(joins.join_sources)
34
+ # results = results.merge(foreign_class.instance_exec(&@scope)) if @scope.present?
35
+ # return results
36
+ end
37
+
38
+ def create(member, attributes)
39
+ # NIY
40
+ end
41
+
42
+ private
43
+
44
+ def validate_options
45
+ @as.present? or raise ThroughHierarchyDefinitionError, "Must provide polymorphic `:as` options for through_hierarchy"
46
+ @model.is_a?(Class) or raise ThroughHierarchyDefinitionError, "Expected: class, got: #{@model.class}"
47
+ @model < ActiveRecord::Base or raise ThroughHierarchyDefinitionError, "Expected: ActiveRecord::Base descendant, got: #{@model}"
48
+ @scope.blank? || @scope.is_a?(Proc) or raise ThroughHierarchyDefinitionError, "Expected scope to be a Proc, got #{@scope.class}"
49
+ end
50
+
51
+ def foreign_class
52
+ @foreign_class_name.constantize
53
+ end
54
+
55
+ def foreign_arel_table
56
+ foreign_class.arel_table
57
+ end
58
+
59
+ def sql_hierarchy_rank
60
+ "CASE `#{foreign_class.table_name}`.`#{foreign_type_field}` " +
61
+ hierarchy_models.map.with_index{|m, ii| "WHEN #{@model.sanitize(model_type_constraint(m))} THEN #{ii} "}.join +
62
+ "END"
63
+ end
64
+
65
+ def arel_hierarchy_rank
66
+ Arel.sql(sql_hierarchy_rank)
67
+ end
68
+
69
+ # TODO: generate this dynamically based on existing columns and selects
70
+ def best_hierarchy_match_name
71
+ "through_hierarchy_match"
72
+ end
73
+
74
+ def arel_best_hierarchy_member
75
+ arel_hierarchy_rank.minimum.as(best_hierarchy_match_name)
76
+ end
77
+
78
+ # TODO: for model level joins, subquery needs to join to all hierarchy tables
79
+ def arel_uniq_subquery(instance = nil)
80
+ foreign_arel_table.
81
+ project(foreign_arel_table[Arel.star], arel_best_hierarchy_member).
82
+ where(instance.present? ? arel_instance_filters(instance) : arel_model_filters).
83
+ group(foreign_arel_table[@uniq]).
84
+ as(uniq_subquery_alias)
85
+ end
86
+
87
+ def uniq_subquery_alias
88
+ "through_hierarchy_subtable"
89
+ end
90
+
91
+ def uniq_subquery_arel_table
92
+ Arel::Table.new(uniq_subquery_alias)
93
+ end
94
+
95
+ # TODO: build join sources for model-level query
96
+ def uniq_subquery_join_sources(instance = nil)
97
+ foreign_arel_table.
98
+ join(arel_uniq_subquery(instance)).
99
+ on(
100
+ arel_hierarchy_rank.eq(uniq_subquery_arel_table[best_hierarchy_match_name]).
101
+ and(foreign_arel_table[@uniq].eq(uniq_subquery_arel_table[@uniq]))
102
+ ).join_sources
103
+ end
104
+
105
+ def uniq_find(instance)
106
+ join_sources = uniq_subquery_join_sources(instance)
107
+
108
+ return foreign_class.
109
+ joins(uniq_subquery_join_sources(instance)).
110
+ where(arel_instance_filters(instance)).
111
+ order(foreign_arel_table[@uniq])
112
+ end
113
+
114
+ def hierarchy_models
115
+ [@model] + @members.map{|m| @model.reflect_on_association(m).klass}
116
+ end
117
+
118
+ def hierarchy_instances(instance)
119
+ [instance] + @members.map{|m| instance.association(m).load_target}
120
+ end
121
+
122
+ def foreign_key_field
123
+ @as.foreign_key
124
+ end
125
+
126
+ def foreign_type_field
127
+ @as + "_type"
128
+ end
129
+
130
+ def arel_foreign_key_field
131
+ foreign_arel_table[foreign_key_field]
132
+ end
133
+
134
+ def arel_foreign_type_field
135
+ foreign_arel_table[foreign_type_field]
136
+ end
137
+
138
+ def arel_model_filters
139
+ hierarchy_models.map{|model| arel_model_filter(model)}.reduce{|q, cond| q.or(cond)}
140
+ end
141
+
142
+ def arel_instance_filters(instance)
143
+ hierarchy_instances(instance).map{|instance| arel_instance_filter(instance)}.reduce{|q, cond| q.or(cond)}
144
+ end
145
+
146
+ def arel_model_filter(model)
147
+ arel_foreign_type_field.eq(model_type_constraint(model)).
148
+ and(arel_foreign_key_field.eq(model_key_constraint(model)))
149
+ end
150
+
151
+ def arel_instance_filter(instance)
152
+ arel_foreign_type_field.eq(instance_type_constraint(instance)).
153
+ and(arel_foreign_key_field.eq(instance_key_constraint(instance)))
154
+ end
155
+
156
+ def instance_type_constraint(resource)
157
+ resource.class.base_class.to_s
158
+ end
159
+
160
+ def instance_key_constraint(resource)
161
+ resource.attributes[resource.class.primary_key]
162
+ end
163
+
164
+ def model_type_constraint(model_class)
165
+ model_class.base_class.to_s
166
+ end
167
+
168
+ def model_key_constraint(model_class)
169
+ model_class.arel_table[model_class.primary_key]
170
+ end
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,6 @@
1
+ module ThroughHierarchy
2
+ module Associations
3
+ class HasMany < Association
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,9 @@
1
+ module ThroughHierarchy
2
+ module Associations
3
+ class HasOne < Association
4
+ def find(instance)
5
+ super.first
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,22 @@
1
+ module ThroughHierarchy
2
+ module Base
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ class_attribute :hierarchical_associations
7
+ self.hierarchical_associations = {}
8
+ attr_reader :hierarchical_association_cache
9
+ after_initialize :reset_hierarchical_association_cache
10
+ end
11
+
12
+ def reset_hierarchical_association_cache
13
+ @hierarchical_association_cache = {}
14
+ end
15
+
16
+ module ClassMethods
17
+ def through_hierarchy(members, &blk)
18
+ Hierarchy.new(self, members).instance_eval(&blk)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,29 @@
1
+ module ThroughHierarchy
2
+ class Builder
3
+ def initialize(model)
4
+ @model = model
5
+ initialize_methods_class
6
+ end
7
+
8
+ def initialize_methods_class
9
+ @model.const_set("ThroughHierarchyAssociationMethods", Module.new) unless defined? @model::ThroughHierarchyAssociationMethods
10
+ @model.include @model::ThroughHierarchyAssociationMethods if !@model.ancestors.include?(@model::ThroughHierarchyAssociationMethods)
11
+ end
12
+
13
+ def add_association(name, assoc)
14
+ @model.hierarchical_associations[name] = assoc
15
+
16
+ @model::ThroughHierarchyAssociationMethods.class_eval do
17
+ define_method(name) do |reload = false|
18
+ return @hierarchical_association_cache[name] if !reload && @hierarchical_association_cache.key?(name)
19
+ @hierarchical_association_cache[name] = self.hierarchical_associations[name].find(self)
20
+ end
21
+
22
+ # TODO: join_through_hierarchy
23
+
24
+ # TODO: create_from_hierarchy(member, attributes)
25
+
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,7 @@
1
+ module ThroughHierarchy
2
+ class ThroughHierarchyError < StandardError
3
+ end
4
+
5
+ class ThroughHierarchyDefinitionError < ThroughHierarchyError
6
+ end
7
+ end
@@ -0,0 +1,29 @@
1
+ module ThroughHierarchy
2
+ class Hierarchy < BasicObject
3
+ def initialize(klass, members)
4
+ @klass = klass
5
+ @members = members
6
+
7
+ validate_members
8
+ end
9
+
10
+ def has_one(name, scope = nil, **options)
11
+ options.merge!(scope: scope) if scope.present?
12
+ assoc = ::ThroughHierarchy::Associations::HasOne.new(name, @klass, @members, options)
13
+ ::ThroughHierarchy::Builder.new(@klass).add_association(name, assoc)
14
+ end
15
+
16
+ def has_many(name, scope = nil, **options)
17
+ options.merge!(scope: scope) if scope.present?
18
+ assoc = ::ThroughHierarchy::Associations::HasMany.new(name, @klass, @members, options)
19
+ ::ThroughHierarchy::Builder.new(@klass).add_association(name, assoc)
20
+ end
21
+
22
+ private
23
+
24
+ def validate_members
25
+ @members.is_a?(::Array) or ::Kernel.raise ::ThroughHierarchy::ThroughHierarchyDefinitionError, "Hierarchy members: expected: Array, got: #{@members.class}"
26
+ @members.all?{|member| @klass.reflect_on_association(member).present?} or ::Kernel.raise ::ThroughHierarchy::ThroughHierarchyDefinitionError, "No association named #{member} was found. Perhaps you misspelled it?"
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,9 @@
1
+ require 'through_hierarchy/base.rb'
2
+ require 'through_hierarchy/hierarchy.rb'
3
+ require 'through_hierarchy/builder.rb'
4
+ require 'through_hierarchy/associations/association.rb'
5
+ require 'through_hierarchy/associations/has_one.rb'
6
+ require 'through_hierarchy/associations/has_many.rb'
7
+ require 'through_hierarchy/exceptions.rb'
8
+
9
+ ActiveRecord::Base.include(ThroughHierarchy::Base)
metadata ADDED
@@ -0,0 +1,51 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: through_hierarchy
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Andrew Schwartz
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-05-04 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Create hierarchical polymorphic associations
14
+ email: ozydingo@gmail.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - lib/through_hierarchy.rb
20
+ - lib/through_hierarchy/associations/association.rb
21
+ - lib/through_hierarchy/associations/has_many.rb
22
+ - lib/through_hierarchy/associations/has_one.rb
23
+ - lib/through_hierarchy/base.rb
24
+ - lib/through_hierarchy/builder.rb
25
+ - lib/through_hierarchy/exceptions.rb
26
+ - lib/through_hierarchy/hierarchy.rb
27
+ homepage: https://github.com/ozydingo/through_hierarchy
28
+ licenses:
29
+ - MIT
30
+ metadata: {}
31
+ post_install_message:
32
+ rdoc_options: []
33
+ require_paths:
34
+ - lib
35
+ required_ruby_version: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ required_rubygems_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: '0'
45
+ requirements: []
46
+ rubyforge_project:
47
+ rubygems_version: 2.4.5
48
+ signing_key:
49
+ specification_version: 4
50
+ summary: Has Many Through Hierarchy
51
+ test_files: []