datamappify 0.30.0 → 0.40.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.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -0
  3. data/ERD.png +0 -0
  4. data/README.md +78 -16
  5. data/datamappify.gemspec +4 -3
  6. data/lib/datamappify/data/criteria/active_record/destroy.rb +1 -1
  7. data/lib/datamappify/data/criteria/active_record/exists.rb +2 -2
  8. data/lib/datamappify/data/criteria/active_record/transaction.rb +1 -1
  9. data/lib/datamappify/data/criteria/common.rb +21 -1
  10. data/lib/datamappify/data/criteria/relational/count.rb +1 -1
  11. data/lib/datamappify/data/criteria/relational/find.rb +1 -1
  12. data/lib/datamappify/data/criteria/relational/save.rb +1 -1
  13. data/lib/datamappify/data/criteria/sequel/destroy.rb +1 -1
  14. data/lib/datamappify/data/criteria/sequel/exists.rb +1 -1
  15. data/lib/datamappify/data/criteria/sequel/transaction.rb +1 -1
  16. data/lib/datamappify/data/mapper/attribute.rb +9 -0
  17. data/lib/datamappify/data/mapper.rb +7 -2
  18. data/lib/datamappify/data/provider/common_provider.rb +1 -1
  19. data/lib/datamappify/entity/lazy_checking.rb +12 -0
  20. data/lib/datamappify/entity.rb +4 -0
  21. data/lib/datamappify/lazy/attributes_handler.rb +123 -0
  22. data/lib/datamappify/lazy/source_attributes_walker.rb +49 -0
  23. data/lib/datamappify/lazy.rb +24 -0
  24. data/lib/datamappify/logger.rb +13 -0
  25. data/lib/datamappify/repository/lazy_checking.rb +19 -0
  26. data/lib/datamappify/repository/mapping_dsl.rb +7 -1
  27. data/lib/datamappify/repository/query_method/callbacks.rb +83 -0
  28. data/lib/datamappify/repository/query_method/count.rb +6 -1
  29. data/lib/datamappify/repository/query_method/create.rb +10 -0
  30. data/lib/datamappify/repository/query_method/destroy.rb +6 -14
  31. data/lib/datamappify/repository/query_method/exists.rb +17 -0
  32. data/lib/datamappify/repository/query_method/find.rb +12 -19
  33. data/lib/datamappify/repository/query_method/method/source_attributes_walker.rb +81 -0
  34. data/lib/datamappify/repository/query_method/method.rb +74 -25
  35. data/lib/datamappify/repository/query_method/save.rb +15 -14
  36. data/lib/datamappify/repository/query_method/update.rb +10 -0
  37. data/lib/datamappify/repository/query_methods.rb +123 -0
  38. data/lib/datamappify/repository/unit_of_work/persistent_states/object.rb +122 -0
  39. data/lib/datamappify/repository/unit_of_work/persistent_states.rb +54 -0
  40. data/lib/datamappify/repository/unit_of_work/transaction.rb +18 -0
  41. data/lib/datamappify/repository/unit_of_work.rb +1 -0
  42. data/lib/datamappify/repository.rb +16 -51
  43. data/lib/datamappify/version.rb +1 -1
  44. data/lib/datamappify.rb +3 -1
  45. data/spec/lazy_spec.rb +73 -0
  46. data/spec/repository/callbacks_spec.rb +140 -0
  47. data/spec/repository/dirty_persistence_spec.rb +44 -0
  48. data/spec/repository/dirty_tracking_spec.rb +82 -0
  49. data/spec/repository/persistence_spec.rb +41 -119
  50. data/spec/repository/transactions_spec.rb +25 -0
  51. data/spec/repository/validation_spec.rb +42 -0
  52. data/spec/repository_spec.rb +8 -6
  53. data/spec/spec_helper.rb +2 -2
  54. data/spec/support/entities/hero_user.rb +5 -0
  55. data/spec/support/repositories/callbacks_chaining_repository.rb +92 -0
  56. data/spec/support/repositories/hero_user_repository.rb +30 -0
  57. data/spec/support/shared/contexts.rb +10 -0
  58. data/spec/support/tables/sequel.rb +1 -0
  59. data/spec/unit/repository/query_method_spec.rb +55 -0
  60. metadata +57 -10
  61. data/lib/datamappify/repository/query_method/transaction.rb +0 -18
  62. data/lib/datamappify/repository/query_method.rb +0 -3
@@ -1,12 +1,18 @@
1
1
  module Datamappify
2
2
  module Repository
3
3
  module MappingDSL
4
+ include LazyChecking
5
+
6
+ # If the entity is lazy loaded then it assigns
7
+ # the repository itself back to the entity
8
+ #
4
9
  # @param entity_class [Class]
5
- # entity class
6
10
  #
7
11
  # @return [void]
8
12
  def for_entity(entity_class)
9
13
  data_mapper.entity_class = entity_class
14
+
15
+ assign_to_entity if lazy_load?
10
16
  end
11
17
 
12
18
  # @param provider_name [String]
@@ -0,0 +1,83 @@
1
+ require 'hooks'
2
+
3
+ # Money-patches Hooks
4
+ module Hooks
5
+ module ClassMethods
6
+ # Added the ability to ignore callbacks if the previous callback returns `nil` or `false`
7
+ #
8
+ # @return [Boolean]
9
+ def run_hook_for(name, scope, *args)
10
+ callbacks = callbacks_for_hook(name)
11
+
12
+ callbacks.take_while do |callback|
13
+ if callback.kind_of? Symbol
14
+ scope.send(callback, *args)
15
+ else
16
+ scope.instance_exec(*args, &callback)
17
+ end
18
+ end.length == callbacks.length
19
+ end
20
+ end
21
+ end
22
+
23
+ module Datamappify
24
+ module Repository
25
+ module QueryMethod
26
+ module Callbacks
27
+ def self.included(klass)
28
+ klass.class_eval do
29
+ include Hooks
30
+
31
+ define_hooks :before_create, :after_create,
32
+ :before_update, :after_update,
33
+ :before_save, :after_save,
34
+ :before_destroy, :after_destroy
35
+ end
36
+ end
37
+
38
+ # @param entity [Entity]
39
+ #
40
+ # @param types [Symbol]
41
+ # e.g. :create, :update, :save or :destroy
42
+ #
43
+ # @yield callback
44
+ #
45
+ # @return [void]
46
+ def run_callbacks(entity, *types, &block)
47
+ run_hooks(types, :before, entity) &&
48
+ (yield_value = block.call) &&
49
+ run_hooks(types.reverse, :after, entity) &&
50
+ yield_value
51
+ end
52
+
53
+ private
54
+
55
+ # @param types [Array<Symbol]
56
+ # an array of types (e.g. :create, :update, :save or :destroy)
57
+ #
58
+ # @param filter [Symbol]
59
+ # e.g. :before or :after
60
+ #
61
+ # @param entity [Entity]
62
+ #
63
+ # @return [Boolean]
64
+ def run_hooks(types, filter, entity)
65
+ types.take_while do |type|
66
+ run_hook(hook_for(type, filter), entity)
67
+ end.length == types.length
68
+ end
69
+
70
+ # @param type [Symbol]
71
+ # e.g. :create, :update, :save or :destroy
72
+ #
73
+ # @param filter [Symbol]
74
+ # e.g. :before or :after
75
+ #
76
+ # @return [String]
77
+ def hook_for(type, filter)
78
+ "#{filter}_#{type}"
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -3,9 +3,14 @@ module Datamappify
3
3
  module QueryMethod
4
4
  class Count < Method
5
5
  # @return [Integer]
6
- def result
6
+ def perform
7
7
  dispatch_criteria_to_default_source(:Count)
8
8
  end
9
+
10
+ # @see Method#reader?
11
+ def reader?
12
+ true
13
+ end
9
14
  end
10
15
  end
11
16
  end
@@ -0,0 +1,10 @@
1
+ require 'datamappify/repository/query_method/save'
2
+
3
+ module Datamappify
4
+ module Repository
5
+ module QueryMethod
6
+ class Create < Save
7
+ end
8
+ end
9
+ end
10
+ end
@@ -2,22 +2,14 @@ module Datamappify
2
2
  module Repository
3
3
  module QueryMethod
4
4
  class Destroy < Method
5
- # @param mapper (see Method#initialize)
6
- #
7
- # @param id_or_ids_or_entity_or_entities [Entity, Array<Entity>]
8
- # an entity or a collection of ids or entities
9
- def initialize(mapper, id_or_ids_or_entity_or_entities)
10
- super
11
- @id_or_ids_or_entity_or_entities = id_or_ids_or_entity_or_entities
12
- end
13
-
14
5
  # @return [void, false]
15
- def result
16
- entities = Array.wrap(@id_or_ids_or_entity_or_entities).map do |id_or_entity|
17
- dispatch_criteria_to_default_source(:Destroy, extract_entity_id(id_or_entity))
18
- end
6
+ def perform
7
+ dispatch_criteria_to_default_source(:Destroy, @entity.id)
8
+ end
19
9
 
20
- @id_or_ids_or_entity_or_entities.is_a?(Array) ? entities : entities[0]
10
+ # @see Method#writer?
11
+ def writer?
12
+ true
21
13
  end
22
14
  end
23
15
  end
@@ -0,0 +1,17 @@
1
+ module Datamappify
2
+ module Repository
3
+ module QueryMethod
4
+ class Exists < Method
5
+ # @return [Boolean]
6
+ def perform
7
+ dispatch_criteria_to_default_source(:Exists, @entity)
8
+ end
9
+
10
+ # @see Method#reader?
11
+ def reader?
12
+ true
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -2,30 +2,18 @@ module Datamappify
2
2
  module Repository
3
3
  module QueryMethod
4
4
  class Find < Method
5
- # @param mapper (see Method#initialize)
5
+ # @param options (see Method#initialize)
6
6
  #
7
- # @param id_or_ids [Integer, Array<Integer>]
8
- # an entity id or a collection of entity ids
9
- def initialize(mapper, id_or_ids)
7
+ # @param id [Integer]
8
+ def initialize(options, id)
10
9
  super
11
- @id_or_ids = id_or_ids
10
+ @id = id
12
11
  end
13
12
 
14
- # @return [Entity, Array<Entity>, nil]
15
- def result
16
- entities = Array.wrap(@id_or_ids).map { |id| setup_new_entity(id) }.compact
17
-
18
- @id_or_ids.is_a?(Array) ? entities : entities[0]
19
- end
20
-
21
- private
22
-
23
- # @param id [Integer]
24
- #
25
13
  # @return [Entity, nil]
26
- def setup_new_entity(id)
27
- entity = @mapper.entity_class.new
28
- entity.id = id
14
+ def perform
15
+ entity = data_mapper.entity_class.new
16
+ entity.id = @id
29
17
 
30
18
  if dispatch_criteria_to_default_source(:Exists, entity)
31
19
  dispatch_criteria_to_providers(:FindByKey, entity)
@@ -35,6 +23,11 @@ module Datamappify
35
23
 
36
24
  entity
37
25
  end
26
+
27
+ # @see Method#reader?
28
+ def reader?
29
+ true
30
+ end
38
31
  end
39
32
  end
40
33
  end
@@ -0,0 +1,81 @@
1
+ module Datamappify
2
+ module Repository
3
+ module QueryMethod
4
+ class Method
5
+ # Walks through the attributes of the source classes under a provider (e.g. ActiveRecord),
6
+ # the walker is aware of the dirty state so that certain operations (i.e. #save) can be bypassed
7
+ class SourceAttributesWalker
8
+ def initialize(options = {})
9
+ @entity = options[:entity]
10
+ @provider_name = options[:provider_name]
11
+ @attributes = options[:attributes]
12
+ @dirty_aware = options[:dirty_aware?]
13
+ @dirty_attributes = options[:dirty_attributes]
14
+ @query_method = options[:query_method]
15
+ end
16
+
17
+ # @yield [provider_name, source_class, attributes]
18
+ # action to be performed on the attributes grouped by their source class
19
+ #
20
+ # @yieldparam provider_name [String]
21
+ #
22
+ # @yieldparam source_class [Class]
23
+ #
24
+ # @yieldparam attributes [Set<Data::Mapper::Attribute>]
25
+ #
26
+ # @yieldreturn [void]
27
+ #
28
+ # @return [void]
29
+ def execute(&block)
30
+ @attributes.classify(&:source_class).each do |source_class, attributes|
31
+ if do_walk?(source_class, attributes)
32
+ block.call(@provider_name, source_class, attributes)
33
+ walk_performed(attributes)
34
+ end
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ # Whether it is necessary to do the walk
41
+ #
42
+ # @param source_class [Class]
43
+ #
44
+ # @param attributes [Set<Data::Mapper::Attribute>]
45
+ #
46
+ # @return [Boolean]
47
+ def do_walk?(source_class, attributes)
48
+ check_dirty?(attributes)
49
+ end
50
+
51
+ # A hook method for when a walk is performed
52
+ #
53
+ # @param attributes [Set<Data::Mapper::Attribute>]
54
+ #
55
+ # @return [void]
56
+ def walk_performed(attributes)
57
+ Logger.performed(@query_method && @query_method.class)
58
+ end
59
+
60
+ # Only walk when it's not dirty aware, or it has dirty attributes
61
+ #
62
+ # @param attributes [Set<Data::Mapper::Attribute>]
63
+ #
64
+ # @return [Boolean]
65
+ def check_dirty?(attributes)
66
+ !@dirty_aware || dirty?(attributes)
67
+ end
68
+
69
+ # Whether the persistent state object is dirty
70
+ #
71
+ # @param (see #do_walk?)
72
+ #
73
+ # @return [Boolean]
74
+ def dirty?(attributes)
75
+ (attributes.map(&:key) & @dirty_attributes).any?
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -1,16 +1,58 @@
1
+ Dir[Datamappify.root.join('repository/query_method/method/*')].each { |file| require file }
2
+
1
3
  module Datamappify
2
4
  module Repository
3
5
  module QueryMethod
4
6
  # Provides a default set of methods to the varies {QueryMethod} classes
5
7
  class Method
6
- # @param mapper [Data::Mapper]
8
+ # @return [Data::Mapper]
9
+ attr_reader :data_mapper
10
+
11
+ # @return [UnitOfWork::PersistentStates]
12
+ attr_reader :states
13
+
14
+ # @param options [Hash]
15
+ # a hash containing required items like data_mapper and states
16
+ #
17
+ # @param entity [Entity]
7
18
  #
8
19
  # @param args [any]
9
- def initialize(mapper, *args)
10
- @mapper = mapper
20
+ def initialize(options, entity = nil, *args)
21
+ @data_mapper = options[:data_mapper]
22
+ @states = options[:states]
23
+ @lazy_load = options[:lazy_load?]
24
+ @entity = entity
25
+ end
26
+
27
+ # Should the method be aware of the dirty state?
28
+ # i.e. {Find} probably doesn't whereas {Save} does
29
+ #
30
+ # @note Override this method for individual query methods
31
+ #
32
+ # @return [Boolean]
33
+ def dirty_aware?
34
+ false
35
+ end
36
+
37
+ # The method is for reading data?
38
+ #
39
+ # @note Override this method for individual query methods
40
+ #
41
+ # @return [Boolean]
42
+ def reader?
43
+ false
44
+ end
45
+
46
+ # The method is for writing data?
47
+ #
48
+ # @note Override this method for individual query methods
49
+ #
50
+ # @return [Boolean]
51
+ def writer?
52
+ false
11
53
  end
12
54
 
13
- protected
55
+ private
14
56
 
15
57
  # Dispatches a {Criteria} according to
16
58
  # the {Data::Mapper data mapper}'s default provider and default source class
@@ -19,7 +61,9 @@ module Datamappify
19
61
  #
20
62
  # @param args [any]
21
63
  def dispatch_criteria_to_default_source(criteria_name, *args)
22
- @mapper.default_provider.build_criteria(criteria_name, @mapper.default_source_class, *args)
64
+ data_mapper.default_provider.build_criteria(
65
+ criteria_name, data_mapper.default_source_class, *args
66
+ )
23
67
  end
24
68
 
25
69
  # Dispatches a {Criteria} via {#attributes_walker}
@@ -30,8 +74,8 @@ module Datamappify
30
74
  #
31
75
  # @return [void]
32
76
  def dispatch_criteria_to_providers(criteria_name, entity)
33
- attributes_walker do |provider_name, source_class, attributes|
34
- @mapper.provider(provider_name).build_criteria(
77
+ attributes_walker(entity) do |provider_name, source_class, attributes|
78
+ data_mapper.provider(provider_name).build_criteria(
35
79
  criteria_name, source_class, entity, attributes
36
80
  )
37
81
  end
@@ -39,33 +83,38 @@ module Datamappify
39
83
 
40
84
  # Walks through the attributes and performs actions on them
41
85
  #
42
- # @yield [provider_name, source_class, attributes]
43
- # action to be performed on the attributes grouped by their source class
86
+ # @param entity [Entity]
44
87
  #
45
- # @yieldparam provider_name [String]
88
+ # @yield (see SourceAttributesWalker#execute)
46
89
  #
47
- # @yieldparam source_class [Class]
90
+ # @yieldparam (see SourceAttributesWalker#execute)
48
91
  #
49
- # @yieldparam attributes [Set]
92
+ # @yieldreturn (see SourceAttributesWalker#execute)
50
93
  #
51
94
  # @return [void]
52
- def attributes_walker(&block)
53
- Transaction.new(@mapper) do
54
- @mapper.classified_attributes.each do |provider_name, attributes|
55
- attributes.classify(&:source_class).each do |source_class, attrs|
56
- block.call(provider_name, source_class, attrs)
57
- end
95
+ #
96
+ # @see SourceAttributesWalker#execute
97
+ def attributes_walker(entity, &block)
98
+ UnitOfWork::Transaction.new(data_mapper) do
99
+ data_mapper.classified_attributes.each do |provider_name, attributes|
100
+ source_attributes_walker.new({
101
+ :entity => entity,
102
+ :provider_name => provider_name,
103
+ :attributes => attributes,
104
+ :dirty_aware? => dirty_aware?,
105
+ :dirty_attributes => states.find(entity).changed,
106
+ :query_method => self
107
+ }).execute(&block)
58
108
  end
59
109
  end
60
110
  end
61
111
 
62
- # Extract the id out of an entity, unless the argument is already an id
63
- #
64
- # @param id_or_entity [Entity, Integer]
65
- #
66
- # @return [Integer]
67
- def extract_entity_id(id_or_entity)
68
- id_or_entity.is_a?(Integer) ? id_or_entity : id_or_entity.id
112
+ def source_attributes_walker
113
+ if @lazy_load
114
+ Lazy::SourceAttributesWalker
115
+ else
116
+ SourceAttributesWalker
117
+ end
69
118
  end
70
119
  end
71
120
  end
@@ -2,26 +2,27 @@ module Datamappify
2
2
  module Repository
3
3
  module QueryMethod
4
4
  class Save < Method
5
- # @param mapper (see Method#initialize)
6
- #
7
- # @param entity_or_entities [Entity, Array<Entity>]
8
- # an entity or a collection of entities
9
- def initialize(mapper, entity_or_entities)
10
- super
11
- @entity_or_entities = entity_or_entities
12
- end
13
-
14
- # @return [Entity, Array<Entity>, false]
15
- def result
16
- Array.wrap(@entity_or_entities).each do |entity|
17
- create_or_update(entity)
5
+ # @return [Entity, false]
6
+ def perform
7
+ states.update(@entity) do
8
+ create_or_update(@entity)
18
9
  end
19
10
 
20
- @entity_or_entities
11
+ @entity
21
12
  rescue Data::EntityInvalid
22
13
  false
23
14
  end
24
15
 
16
+ # @see Method#dirty_aware?
17
+ def dirty_aware?
18
+ true
19
+ end
20
+
21
+ # @see Method#writer?
22
+ def writer?
23
+ true
24
+ end
25
+
25
26
  private
26
27
 
27
28
  # @param entity [Entity]
@@ -0,0 +1,10 @@
1
+ require 'datamappify/repository/query_method/save'
2
+
3
+ module Datamappify
4
+ module Repository
5
+ module QueryMethod
6
+ class Update < Save
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,123 @@
1
+ require 'datamappify/repository/query_method/method'
2
+
3
+ Dir[Datamappify.root.join('repository/query_method/*')].each { |file| require file }
4
+
5
+ module Datamappify
6
+ module Repository
7
+ module QueryMethods
8
+ def self.included(klass)
9
+ klass.class_eval do
10
+ include QueryMethod::Callbacks
11
+ end
12
+ end
13
+
14
+ # Does the entity exist already in the repository?
15
+ #
16
+ # @param entity [Entity]
17
+ #
18
+ # @return [Boolean]
19
+ def exists?(entity)
20
+ QueryMethod::Exists.new(query_options, entity).perform
21
+ end
22
+
23
+ # @param id [Integer]
24
+ # an entity id or a collection of entity ids
25
+ #
26
+ # @return [Entity, nil]
27
+ def find(id)
28
+ QueryMethod::Find.new(query_options, id).perform
29
+ end
30
+
31
+ # @param entity [Entity]
32
+ # an entity or a collection of entities
33
+ #
34
+ # @return [Entity, false]
35
+ def create(entity)
36
+ run_callbacks entity, :save, :create do
37
+ QueryMethod::Create.new(query_options, entity).perform
38
+ end
39
+ end
40
+
41
+ # @param (see #create)
42
+ #
43
+ # @raise [Data::EntityNotSaved]
44
+ #
45
+ # @return [Entity]
46
+ def create!(entity)
47
+ create(entity) || raise(Data::EntityNotSaved)
48
+ end
49
+
50
+ # @param entity [Entity]
51
+ # an entity or a collection of entities
52
+ #
53
+ # @return [Entity, false]
54
+ def update(entity)
55
+ run_callbacks entity, :save, :update do
56
+ QueryMethod::Update.new(query_options, entity).perform
57
+ end
58
+ end
59
+
60
+ # @param (see #update)
61
+ #
62
+ # @raise [Data::EntityNotSaved]
63
+ #
64
+ # @return [Entity]
65
+ def update!(entity)
66
+ update(entity) || raise(Data::EntityNotSaved)
67
+ end
68
+
69
+ # @param entity [Entity]
70
+ # an entity or a collection of entities
71
+ #
72
+ # @return [Entity, false]
73
+ def save(entity)
74
+ exists?(entity) ? update(entity) : create(entity)
75
+ end
76
+
77
+ # @param (see #save)
78
+ #
79
+ # @raise [Data::EntityNotSaved]
80
+ #
81
+ # @return [Entity]
82
+ def save!(entity)
83
+ exists?(entity) ? update!(entity) : create!(entity)
84
+ end
85
+
86
+ # @param entity [Entity]
87
+ #
88
+ # @return [void, false]
89
+ def destroy(entity)
90
+ run_callbacks entity, :destroy do
91
+ QueryMethod::Destroy.new(query_options, entity).perform
92
+ end
93
+ end
94
+
95
+ # @param (see #destroy)
96
+ #
97
+ # @raise [Data::EntityNotDestroyed]
98
+ #
99
+ # @return [void]
100
+ def destroy!(entity)
101
+ destroy(entity) || raise(Data::EntityNotDestroyed)
102
+ end
103
+
104
+ # @return [Integer]
105
+ def count
106
+ QueryMethod::Count.new(query_options).perform
107
+ end
108
+
109
+ private
110
+
111
+ # Some default, required objects passed into each query method
112
+ #
113
+ # @return [Hash]
114
+ def query_options
115
+ {
116
+ :data_mapper => data_mapper,
117
+ :states => states,
118
+ :lazy_load? => lazy_load?
119
+ }
120
+ end
121
+ end
122
+ end
123
+ end