selfish_associations 0.0.0 → 0.1.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 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