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 +7 -0
- data/lib/through_hierarchy/associations/association.rb +173 -0
- data/lib/through_hierarchy/associations/has_many.rb +6 -0
- data/lib/through_hierarchy/associations/has_one.rb +9 -0
- data/lib/through_hierarchy/base.rb +22 -0
- data/lib/through_hierarchy/builder.rb +29 -0
- data/lib/through_hierarchy/exceptions.rb +7 -0
- data/lib/through_hierarchy/hierarchy.rb +29 -0
- data/lib/through_hierarchy.rb +9 -0
- metadata +51 -0
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,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,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: []
|