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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 27c26aa51df5427a9e49e1cab0579b2221dff05d
4
- data.tar.gz: c097635034e3eacb38684804688c2299ae5eab7f
3
+ metadata.gz: 3bd7fff1c41265161aea2372a9fb041ce4ff77e0
4
+ data.tar.gz: 7af54ffb489231247f270348800a6e144418d847
5
5
  SHA512:
6
- metadata.gz: e859a4392f780a056d70379f3fc5bc0a1e965422f852421354240cf3dabe36a65bf3da912f1b5dbb1d1c85c24187a06ce00059cdc4251bff8093b68709b52306
7
- data.tar.gz: b2042d49ad28fc35e24e6752f5823cf03bb4c0e6d6a09f0aa5487ecce69090fbfd3803540168f9b2e330db02d25536302221228bbda6161b9c579a535724b8a3
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(@reader.conditions_for_find)
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 create(instance)
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{|c1, c2| c1.and(c2)}
56
+ end.reduce(&:and)
62
57
  end
63
58
 
64
59
  def instance_find_conditions(instance)
65
- instance_conditions(instance, @reader.conditions_for_find)
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 instance_create_attributes(instance)
69
- instance_find_conditions(instance).merge(instance_conditions(instance, @reader.attribuets_for_create))
67
+ def relation_reader
68
+ @relation_reader ||= apply_scopes(SelfishAssociations::ScopeReaders::Relation.new(@model))
70
69
  end
71
70
 
72
- def joins_for_find
73
- @reader.joins_for_find
71
+ def instance_reader(instance)
72
+ apply_scopes(ScopeReaders::Instance.new(instance))
74
73
  end
75
74
 
76
- def joins_for_create
77
- @reader.joins_for_create
75
+ def read_instance_find_conditions(instance)
76
+ instance_reader(instance).attributes_for_find
78
77
  end
79
78
 
80
- # TODO: does this even work for create??
81
- def instance_conditions(instance, conditions)
82
- # TODO: if no joins, we can just read fields off of instance
83
- # TODO: if cached associations exist, we can read fields off those too
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
- record = @model.joins(joins_for_find).select(selector).find_by(id: instance.id)
87
- arel_conditions.keys.each{|k| static_conditions[k] = record[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
- # self.class.const_defined?(foreign_class) or raise SelfishAssociations::SelfishException, "No class named #{foreign_class} found."
93
- @scope.nil? || @scope.is_a?(Proc) or raise SelfishAssociations::SelfishException, "Scope must be a Proc"
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,7 +2,7 @@ module SelfishAssociations
2
2
  module Associations
3
3
  class HasMany < SelfishAssociations::Association
4
4
  def find(instance)
5
- foreign_class.instance_exec(instance, @scope)
5
+ matches_for(instance)
6
6
  end
7
7
  end
8
8
  end
@@ -2,9 +2,7 @@ module SelfishAssociations
2
2
  module Associations
3
3
  class HasOne < SelfishAssociations::Association
4
4
  def find(instance)
5
- # TODO: just this? In fact, can we just combined this entirely with AR?
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::Association::HasOne.new(name, self, scope, options))
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::Association::HasMany.new(name, self, scope, options))
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 !ancestors.include?(@model::SelfishAssociationMethods)
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 assocaitions, it collects every model that
13
- # it has seen. You can then call :associations! to fetch these assocaitions.
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
@@ -1,7 +1,13 @@
1
- ActiveRecord::Base.include(SelfishAssociations::Base)
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
- require 'selfish_associations/association_traverser'
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.0.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 ActiveRecord-like associations with self-awareness
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/path_merger.rb
27
- - lib/selfish_associations/scope_reader.rb
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