selfish_associations 0.0.0 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/selfish_associations/associations/association.rb +57 -33
- data/lib/selfish_associations/associations/has_many.rb +1 -1
- data/lib/selfish_associations/associations/has_one.rb +1 -3
- data/lib/selfish_associations/base.rb +2 -2
- data/lib/selfish_associations/builder.rb +3 -2
- data/lib/selfish_associations/scope_readers/instance.rb +38 -0
- data/lib/selfish_associations/scope_readers/relation.rb +61 -0
- data/lib/selfish_associations/{association_traverser.rb → utils/association_traverser.rb} +6 -2
- data/lib/selfish_associations/utils/nilifier.rb +24 -0
- data/lib/selfish_associations/{path_merger.rb → utils/path_merger.rb} +0 -0
- data/lib/selfish_associations.rb +12 -6
- metadata +7 -5
- data/lib/selfish_associations/scope_reader.rb +0 -55
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3bd7fff1c41265161aea2372a9fb041ce4ff77e0
|
4
|
+
data.tar.gz: 7af54ffb489231247f270348800a6e144418d847
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 93cdb13cfba94868478e470979fb9cbc4696771ae45344ead80bb9cb3f578039d61622dc833e96914fa86b5869bc2a0a3dfa8a79f012fca3623d8a8b6fcc8e35
|
7
|
+
data.tar.gz: 3a50a8ef46c954d305ddfce23a041b53b059b33103e7fc88f53c9fa80db0b0a24ca9f3633a9fe953c965678f3e1662f2f614211b0c5a0abf2b8963a7daab39aa
|
@@ -16,14 +16,8 @@ module SelfishAssociations
|
|
16
16
|
@scope = scope
|
17
17
|
@foreign_class_name = (options[:class_name] || name.to_s.classify).to_s
|
18
18
|
@foreign_key = options[:foreign_key] == false ? false : (options[:foreign_key] || @model.name.foreign_key).to_sym
|
19
|
-
|
19
|
+
@foreign_key_scope = @foreign_key.present? ? foreign_key_scope : nil
|
20
20
|
validate
|
21
|
-
|
22
|
-
@reader = SelfishAssociations::ScopeReader.new(@model)
|
23
|
-
# Can't use ivar inside Lambda
|
24
|
-
foreign_key = @foreign_key
|
25
|
-
add_scope(->(selph){ where foreign_key => selph.id }) if @foreign_key.present?
|
26
|
-
add_scope(@scope) if @scope.present?
|
27
21
|
end
|
28
22
|
|
29
23
|
def inspect
|
@@ -34,63 +28,93 @@ module SelfishAssociations
|
|
34
28
|
@foreign_class ||= self.class.const_get(@foreign_class_name)
|
35
29
|
end
|
36
30
|
|
37
|
-
def add_scope(scope)
|
38
|
-
@reader.read(scope)
|
39
|
-
end
|
40
|
-
|
41
31
|
def join
|
42
|
-
conditions = arelize_conditions(
|
32
|
+
conditions = arelize_conditions(relation_reader.conditions_for_find)
|
43
33
|
arel_join = @model.arel_table.join(foreign_class.arel_table).on(conditions).join_sources
|
44
34
|
@model.joins(joins_for_find).joins(arel_join).merge(foreign_class.all)
|
45
35
|
end
|
46
36
|
|
47
|
-
def
|
48
|
-
foreign_class.create(instance_create_attributes(instance))
|
49
|
-
end
|
50
|
-
|
51
|
-
def matches(instance)
|
37
|
+
def matches_for(instance)
|
52
38
|
foreign_class.where(instance_find_conditions(instance))
|
53
39
|
end
|
54
40
|
|
41
|
+
def create_for(instance)
|
42
|
+
foreign_class.create(instance_create_attributes(instance))
|
43
|
+
end
|
55
44
|
|
56
45
|
private
|
57
46
|
|
47
|
+
def apply_scopes(reader)
|
48
|
+
reader.read(@scope) if @scope.present?
|
49
|
+
reader.read(@foreign_key_scope) if @foreign_key.present?
|
50
|
+
return reader
|
51
|
+
end
|
52
|
+
|
58
53
|
def arelize_conditions(conditions)
|
59
54
|
conditions.map do |foreign_field, node|
|
60
55
|
foreign_class.arel_table[foreign_field].eq(node)
|
61
|
-
end.reduce
|
56
|
+
end.reduce(&:and)
|
62
57
|
end
|
63
58
|
|
64
59
|
def instance_find_conditions(instance)
|
65
|
-
|
60
|
+
read_instance_find_conditions(instance)
|
61
|
+
# TODO: dynamically determine if we should use lookup_instance_find_conditions instead
|
62
|
+
# cannot use if the scope constains non-associations
|
63
|
+
# should not use if all associations are preloaded on instance
|
64
|
+
# cannot use if instance contains unpersisted changes
|
66
65
|
end
|
67
66
|
|
68
|
-
def
|
69
|
-
|
67
|
+
def relation_reader
|
68
|
+
@relation_reader ||= apply_scopes(SelfishAssociations::ScopeReaders::Relation.new(@model))
|
70
69
|
end
|
71
70
|
|
72
|
-
def
|
73
|
-
|
71
|
+
def instance_reader(instance)
|
72
|
+
apply_scopes(ScopeReaders::Instance.new(instance))
|
74
73
|
end
|
75
74
|
|
76
|
-
def
|
77
|
-
|
75
|
+
def read_instance_find_conditions(instance)
|
76
|
+
instance_reader(instance).attributes_for_find
|
78
77
|
end
|
79
78
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
79
|
+
def read_instance_create_attributes(instance)
|
80
|
+
instance_reader(instance).attributes_for_create
|
81
|
+
end
|
82
|
+
|
83
|
+
def lookup_instance_find_conditions(instance)
|
84
|
+
instantiate_conditions(instance, relation_reader.conditions_for_find)
|
85
|
+
end
|
86
|
+
|
87
|
+
def lookup_instance_create_attributes(instance)
|
88
|
+
instantiate_conditions(instance, relation_reader.attributes_for_create)
|
89
|
+
end
|
90
|
+
|
91
|
+
def instantiate_conditions(instance, conditions)
|
84
92
|
arel_conditions, static_conditions = conditions.partition{|field, value| value.is_a?(::Arel::Attribute)}.map(&:to_h)
|
85
93
|
selector = arel_conditions.map{|field, arel| arel.as(field.to_s)}
|
86
|
-
|
87
|
-
arel_conditions.keys.each{|k| static_conditions[k] =
|
94
|
+
instance = @model.joins(joins_for_find).select(selector).find_by(id: instance.id)
|
95
|
+
arel_conditions.keys.each{|k| static_conditions[k] = instance[k]}
|
88
96
|
return static_conditions
|
89
97
|
end
|
90
98
|
|
99
|
+
def joins_for_find
|
100
|
+
relation_reader.joins_for_find
|
101
|
+
end
|
102
|
+
|
103
|
+
def joins_for_create
|
104
|
+
relation_reader.joins_for_create
|
105
|
+
end
|
106
|
+
|
107
|
+
def foreign_key_scope
|
108
|
+
# TODO: pass foreign_key in rather than using the closure
|
109
|
+
foreign_key = @foreign_key
|
110
|
+
return ->(obj){ where foreign_key => obj.id }
|
111
|
+
end
|
112
|
+
|
91
113
|
def validate
|
92
|
-
|
93
|
-
|
114
|
+
if @scope.present?
|
115
|
+
@scope.is_a?(Proc) or raise SelfishAssociations::SelfishException, "Scope must be a Proc"
|
116
|
+
@scope.arity == 0 || @scope.arity == 1 or raise SelfishAssociations::SelfishException, "Scope must have arity of 0 or 1"
|
117
|
+
end
|
94
118
|
@model.is_a?(Class) && @model < ActiveRecord::Base or raise SelfishAssociations::SelfishException, "Tried to define a SelfishAssociation for an invalid object (#{@model})"
|
95
119
|
end
|
96
120
|
end
|
@@ -2,9 +2,7 @@ module SelfishAssociations
|
|
2
2
|
module Associations
|
3
3
|
class HasOne < SelfishAssociations::Association
|
4
4
|
def find(instance)
|
5
|
-
|
6
|
-
# foreign_class.instance_exec(instance, @scope).first
|
7
|
-
foreign_class.find_by(instance_find_conditions(instance))
|
5
|
+
matches_for(instance).first
|
8
6
|
end
|
9
7
|
end
|
10
8
|
end
|
@@ -15,11 +15,11 @@ module SelfishAssociations
|
|
15
15
|
|
16
16
|
module ClassMethods
|
17
17
|
def has_one_selfish(name, scope = nil, **options)
|
18
|
-
SelfishAssociations::Builder.new(self).add_association(SelfishAssociations::
|
18
|
+
SelfishAssociations::Builder.new(self).add_association(name, SelfishAssociations::Associations::HasOne.new(name, self, scope, options))
|
19
19
|
end
|
20
20
|
|
21
21
|
def has_many_selfish(name, scope = nil, **options)
|
22
|
-
SelfishAssociations::Builder.new(self).add_association(SelfishAssociations::
|
22
|
+
SelfishAssociations::Builder.new(self).add_association(name, SelfishAssociations::Associations::HasMany.new(name, self, scope, options))
|
23
23
|
end
|
24
24
|
|
25
25
|
def selfish_joins(name)
|
@@ -6,10 +6,11 @@ module SelfishAssociations
|
|
6
6
|
|
7
7
|
def initialize_methods_class
|
8
8
|
@model.const_set("SelfishAssociationMethods", Module.new) unless defined? @model::SelfishAssociationMethods
|
9
|
-
@model.include @model::SelfishAssociationMethods if
|
9
|
+
@model.include @model::SelfishAssociationMethods if !@model.ancestors.include?(@model::SelfishAssociationMethods)
|
10
10
|
end
|
11
11
|
|
12
|
-
def add_association(assoc)
|
12
|
+
def add_association(name, assoc)
|
13
|
+
initialize_methods_class
|
13
14
|
@model.selfish_associations[name] = assoc
|
14
15
|
|
15
16
|
@model::SelfishAssociationMethods.class_eval do
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module SelfishAssociations
|
2
|
+
module ScopeReaders
|
3
|
+
class Instance < BasicObject
|
4
|
+
attr_reader :attributes_for_find, :attributes_for_create
|
5
|
+
|
6
|
+
def initialize(instance)
|
7
|
+
@instance = Nilifier.new(instance)
|
8
|
+
@attributes_for_find = {}
|
9
|
+
@attributes_for_create = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def read(scope)
|
13
|
+
args = scope.arity == 0 ? [] : [@instance]
|
14
|
+
instance_exec(*args, &scope)
|
15
|
+
return self
|
16
|
+
end
|
17
|
+
|
18
|
+
def where(conditions)
|
19
|
+
unnilify(conditions)
|
20
|
+
create_with(conditions)
|
21
|
+
@attributes_for_find.merge!(conditions)
|
22
|
+
return self
|
23
|
+
end
|
24
|
+
|
25
|
+
def create_with(conditions)
|
26
|
+
unnilify(conditions)
|
27
|
+
@attributes_for_create.merge!(conditions)
|
28
|
+
return self
|
29
|
+
end
|
30
|
+
|
31
|
+
def unnilify(conditions)
|
32
|
+
conditions.keys.each do |key|
|
33
|
+
conditions[key] = conditions[key].unnilify if conditions[key].respond_to?(:unnilify?)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# A scope reader is an object that response to scope methods such as `where` and
|
2
|
+
# `create_with`. It must return a ScopeReader for each of these methods so that
|
3
|
+
# these methods can be chained. For it to be useful, these methods must also have
|
4
|
+
# side effects that can be inspected by a third party.
|
5
|
+
#
|
6
|
+
# For SelfishAssociations, we generally want to support passing a single argument
|
7
|
+
# to the scope lambda. E.g. `->(record){ where field: record.matching_field }`. In
|
8
|
+
# order to collect useful information, we may want to pass a special object in place
|
9
|
+
# of `record`: one that can respond to the same methods that `record` could respond
|
10
|
+
# to, but in doing so collects information about each call that is being made. This
|
11
|
+
# is what we are using the `AssociationTraverser` for in `ScopeReaders::Relation`.
|
12
|
+
# This traverser can respond to relations and field names of the record model.
|
13
|
+
# Similarly, it returns a modified traverser in respons to this so that those calls
|
14
|
+
# may also be chained. For more, see association_traverser.rb.
|
15
|
+
|
16
|
+
module SelfishAssociations
|
17
|
+
module ScopeReaders
|
18
|
+
class Relation < BasicObject
|
19
|
+
attr_reader :conditions_for_find, :attributes_for_create
|
20
|
+
attr_reader :traverser
|
21
|
+
|
22
|
+
def initialize(klass)
|
23
|
+
@traverser = ::SelfishAssociations::AssociationTraverser.new(klass)
|
24
|
+
@conditions_for_find = {}
|
25
|
+
@joins_for_find = []
|
26
|
+
@attributes_for_create = {}
|
27
|
+
@joins_for_create = []
|
28
|
+
end
|
29
|
+
|
30
|
+
def read(scope)
|
31
|
+
args = scope.arity == 0 ? [] : [@traverser]
|
32
|
+
instance_exec(*args, &scope)
|
33
|
+
return self
|
34
|
+
end
|
35
|
+
|
36
|
+
# TODO: implement argless where() and not()
|
37
|
+
def where(conditions)
|
38
|
+
create_with(conditions)
|
39
|
+
@conditions_for_find.merge!(conditions)
|
40
|
+
@joins_for_find += @traverser.associations!(merge: false)
|
41
|
+
@traverser.reset!
|
42
|
+
return self
|
43
|
+
end
|
44
|
+
|
45
|
+
def create_with(conditions)
|
46
|
+
@attributes_for_create.merge!(conditions)
|
47
|
+
@joins_for_create += @traverser.associations!(merge: false)
|
48
|
+
@traverser.reset!
|
49
|
+
return self
|
50
|
+
end
|
51
|
+
|
52
|
+
def joins_for_find
|
53
|
+
::SelfishAssociations::PathMerger.new(@joins_for_find).merge
|
54
|
+
end
|
55
|
+
|
56
|
+
def joins_for_create
|
57
|
+
::SelfishAssociations::PathMerger.new(@joins_for_create).merge
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -9,8 +9,8 @@
|
|
9
9
|
# t.bar.baz.id
|
10
10
|
# # => Arel::Attribute(table: "bazs", field: "id")
|
11
11
|
#
|
12
|
-
# As a Traverser gets iterated accross
|
13
|
-
# it has seen. You can then call :associations! to fetch these
|
12
|
+
# As a Traverser gets iterated accross associations, it collects every model that
|
13
|
+
# it has seen. You can then call :associations! to fetch these associations.
|
14
14
|
#
|
15
15
|
# e.g., continuing from above
|
16
16
|
# # t.associations!
|
@@ -71,6 +71,10 @@ module SelfishAssociations
|
|
71
71
|
@path << method
|
72
72
|
@klass = @klass.reflect_on_association(method).klass
|
73
73
|
return self
|
74
|
+
elsif @klass.selfish_associations[method].present?
|
75
|
+
@path << method
|
76
|
+
@klass = @klass.selfish_associations[method].foreign_class
|
77
|
+
return self
|
74
78
|
else
|
75
79
|
message = "No association or field named #{method} found for class #{@klass}"
|
76
80
|
reset!
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module SelfishAssociations
|
2
|
+
class Nilifier < BasicObject
|
3
|
+
def initialize(object)
|
4
|
+
@object = object
|
5
|
+
end
|
6
|
+
|
7
|
+
def inspect
|
8
|
+
@object.inspect + " (nil-safe)"
|
9
|
+
end
|
10
|
+
|
11
|
+
def unnilify
|
12
|
+
@object
|
13
|
+
end
|
14
|
+
|
15
|
+
def respond_to?(method)
|
16
|
+
true
|
17
|
+
end
|
18
|
+
|
19
|
+
def method_missing(method, *args)
|
20
|
+
result = @object.respond_to?(method) ? @object.public_send(method, *args) : nil
|
21
|
+
::SelfishAssociations::Nilifier.new(result)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
File without changes
|
data/lib/selfish_associations.rb
CHANGED
@@ -1,7 +1,13 @@
|
|
1
|
-
|
1
|
+
require 'selfish_associations/base'
|
2
|
+
require 'selfish_associations/builder'
|
3
|
+
require 'selfish_associations/selfish_exception'
|
4
|
+
require 'selfish_associations/utils/association_traverser'
|
5
|
+
require 'selfish_associations/utils/nilifier'
|
6
|
+
require 'selfish_associations/utils/path_merger'
|
7
|
+
require 'selfish_associations/scope_readers/instance'
|
8
|
+
require 'selfish_associations/scope_readers/relation'
|
9
|
+
require 'selfish_associations/associations/association'
|
10
|
+
require 'selfish_associations/associations/has_one'
|
11
|
+
require 'selfish_associations/associations/has_many'
|
2
12
|
|
3
|
-
|
4
|
-
require 'selfish_associations/association'
|
5
|
-
require 'selfish_associations/path_merger'
|
6
|
-
require 'selfish_associations/scope_reader'
|
7
|
-
require 'selfish_associations/selfish_exception'
|
13
|
+
ActiveRecord::Base.include(SelfishAssociations::Base)
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: selfish_associations
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Schwartz
|
@@ -10,22 +10,24 @@ bindir: bin
|
|
10
10
|
cert_chain: []
|
11
11
|
date: 2016-05-04 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
|
-
description: Create
|
13
|
+
description: Create joinable associations with instance/record-level constraints
|
14
14
|
email: ozydingo@gmail.com
|
15
15
|
executables: []
|
16
16
|
extensions: []
|
17
17
|
extra_rdoc_files: []
|
18
18
|
files:
|
19
19
|
- lib/selfish_associations.rb
|
20
|
-
- lib/selfish_associations/association_traverser.rb
|
21
20
|
- lib/selfish_associations/associations/association.rb
|
22
21
|
- lib/selfish_associations/associations/has_many.rb
|
23
22
|
- lib/selfish_associations/associations/has_one.rb
|
24
23
|
- lib/selfish_associations/base.rb
|
25
24
|
- lib/selfish_associations/builder.rb
|
26
|
-
- lib/selfish_associations/
|
27
|
-
- lib/selfish_associations/
|
25
|
+
- lib/selfish_associations/scope_readers/instance.rb
|
26
|
+
- lib/selfish_associations/scope_readers/relation.rb
|
28
27
|
- lib/selfish_associations/selfish_exception.rb
|
28
|
+
- lib/selfish_associations/utils/association_traverser.rb
|
29
|
+
- lib/selfish_associations/utils/nilifier.rb
|
30
|
+
- lib/selfish_associations/utils/path_merger.rb
|
29
31
|
homepage: https://github.com/ozydingo/selfish_associations
|
30
32
|
licenses:
|
31
33
|
- MIT
|
@@ -1,55 +0,0 @@
|
|
1
|
-
# A SelfishAssociations::ScopeReader is responsible for reading a scope lambda, such
|
2
|
-
# as `->(foo){ where language_id: foo.language_id}`. The reader collects information
|
3
|
-
# about the scope in order to apply it either to an instnace-level find, e.g.,
|
4
|
-
# `where(language_id: 1)`, or to a joins, e.g., `where("language_id = foos.language_id")
|
5
|
-
# It does this by responding to `where` and `create_with` methods that you might
|
6
|
-
# use in a scope and keeping track of the resulitng Arel Nodes in order to build
|
7
|
-
# the find or joins query, using an SelfishAssociations::AssociationTraverser to interpret
|
8
|
-
# the conditions themselves.
|
9
|
-
|
10
|
-
module SelfishAssociations
|
11
|
-
class ScopeReader < BasicObject
|
12
|
-
attr_reader :traverser
|
13
|
-
|
14
|
-
def initialize(klass)
|
15
|
-
@traverser = ::SelfishAssociations::AssociationTraverser.new(klass)
|
16
|
-
@conditions_for_find = {}
|
17
|
-
@joins_for_find = []
|
18
|
-
@attribuets_for_create = {}
|
19
|
-
@joins_for_create = []
|
20
|
-
end
|
21
|
-
|
22
|
-
def read(scope)
|
23
|
-
args = scope.arity == 0 ? [] : [@traverser]
|
24
|
-
instance_exec(*args, &scope)
|
25
|
-
end
|
26
|
-
|
27
|
-
def where(conditions)
|
28
|
-
@conditions_for_find.merge!(conditions)
|
29
|
-
@joins_for_find += @traverser.associations!(merge: false)
|
30
|
-
@traverser.reset!
|
31
|
-
return self
|
32
|
-
end
|
33
|
-
|
34
|
-
def create_with(conditions)
|
35
|
-
@attribuets_for_create.merge!(conditions)
|
36
|
-
@joins_for_create += @traverser.associations!
|
37
|
-
@traverser.reset!
|
38
|
-
return self
|
39
|
-
end
|
40
|
-
|
41
|
-
attr_reader :conditions_for_find, :attribuets_for_create
|
42
|
-
|
43
|
-
def joins_for_find
|
44
|
-
::SelfishAssociations::PathMerger.new(@joins_for_find).merge
|
45
|
-
end
|
46
|
-
|
47
|
-
def joins_for_create
|
48
|
-
::SelfishAssociations::PathMerger.new(@joins_for_create).merge
|
49
|
-
end
|
50
|
-
|
51
|
-
# TODO: implement `.where.not`
|
52
|
-
# TODO: join to other SelfishAssociations associations
|
53
|
-
|
54
|
-
end
|
55
|
-
end
|