through_hierarchy 0.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: 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: []