lims-core 3.2.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (177) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +5 -0
  3. data/.rspec +1 -0
  4. data/.rvmrc +2 -0
  5. data/.travis.yml +2 -0
  6. data/.vimrc +27 -0
  7. data/.yard_templates/default/layout/html/footer.erb +0 -0
  8. data/.yardopts +1 -0
  9. data/Gemfile +54 -0
  10. data/Gemfile.lock +197 -0
  11. data/Guardfile +21 -0
  12. data/Guardfile.tmux +28 -0
  13. data/README.markdown +67 -0
  14. data/Rakefile +16 -0
  15. data/config/database.yml +16 -0
  16. data/doc/Array.html +116 -0
  17. data/doc/Array/ArrayLoggerPersistor.html +152 -0
  18. data/doc/Lims.html +114 -0
  19. data/doc/Lims/Core.html +178 -0
  20. data/doc/Lims/Core/Action.html +91 -0
  21. data/doc/Lims/Core/Actions.html +116 -0
  22. data/doc/Lims/Core/Actions/Action.html +216 -0
  23. data/doc/Lims/Core/Actions/Action/AfterEval.html +853 -0
  24. data/doc/Lims/Core/Actions/Action/InvalidParameters.html +268 -0
  25. data/doc/Lims/Core/Actions/ActionGroup.html +196 -0
  26. data/doc/Lims/Core/Actions/ActionGroup/AfterEval.html +315 -0
  27. data/doc/Lims/Core/Actions/BulkAction.html +224 -0
  28. data/doc/Lims/Core/Actions/BulkAction/AfterEval.html +253 -0
  29. data/doc/Lims/Core/Actions/TestActionGroup.html +101 -0
  30. data/doc/Lims/Core/Actions/TestActionGroup/Action.html +133 -0
  31. data/doc/Lims/Core/Actions/TestActionGroup/ActionGroup.html +127 -0
  32. data/doc/Lims/Core/Base.html +287 -0
  33. data/doc/Lims/Core/Base/AccessibleViaSuper.html +252 -0
  34. data/doc/Lims/Core/Base/ClassMethod.html +177 -0
  35. data/doc/Lims/Core/Base/HashString.html +177 -0
  36. data/doc/Lims/Core/Base/IsArrayOf.html +606 -0
  37. data/doc/Lims/Core/Base/State.html +130 -0
  38. data/doc/Lims/Core/Organization.html +113 -0
  39. data/doc/Lims/Core/Organization/Batch.html +106 -0
  40. data/doc/Lims/Core/Persistence.html +267 -0
  41. data/doc/Lims/Core/Persistence/ComparisonFilter.html +318 -0
  42. data/doc/Lims/Core/Persistence/Filter.html +252 -0
  43. data/doc/Lims/Core/Persistence/IdentityMap.html +409 -0
  44. data/doc/Lims/Core/Persistence/IdentityMap/Class.html +144 -0
  45. data/doc/Lims/Core/Persistence/IdentityMap/DuplicateError.html +126 -0
  46. data/doc/Lims/Core/Persistence/IdentityMap/DuplicateIdError.html +136 -0
  47. data/doc/Lims/Core/Persistence/IdentityMap/DuplicateObjectError.html +136 -0
  48. data/doc/Lims/Core/Persistence/IdentityMapClass.html +133 -0
  49. data/doc/Lims/Core/Persistence/Logger.html +105 -0
  50. data/doc/Lims/Core/Persistence/Logger/Persistor.html +334 -0
  51. data/doc/Lims/Core/Persistence/Logger/Session.html +452 -0
  52. data/doc/Lims/Core/Persistence/Logger/Store.html +470 -0
  53. data/doc/Lims/Core/Persistence/MessageBus.html +871 -0
  54. data/doc/Lims/Core/Persistence/MessageBus/ConnectionError.html +123 -0
  55. data/doc/Lims/Core/Persistence/MessageBus/InvalidSettingsError.html +122 -0
  56. data/doc/Lims/Core/Persistence/MultiCriteriaFilter.html +293 -0
  57. data/doc/Lims/Core/Persistence/PersistAssociationTrait.html +91 -0
  58. data/doc/Lims/Core/Persistence/PersistableTrait.html +91 -0
  59. data/doc/Lims/Core/Persistence/Persistor.html +3072 -0
  60. data/doc/Lims/Core/Persistence/Persistor/DuplicateError.html +205 -0
  61. data/doc/Lims/Core/Persistence/Persistor/DuplicateIdError.html +147 -0
  62. data/doc/Lims/Core/Persistence/Persistor/DuplicateObjectError.html +147 -0
  63. data/doc/Lims/Core/Persistence/PersistorTrait.html +91 -0
  64. data/doc/Lims/Core/Persistence/ResourceState.html +1738 -0
  65. data/doc/Lims/Core/Persistence/Search.html +269 -0
  66. data/doc/Lims/Core/Persistence/Search/CreateSearch.html +251 -0
  67. data/doc/Lims/Core/Persistence/Search/SearchPersistor.html +240 -0
  68. data/doc/Lims/Core/Persistence/Search/SearchSequelPersistor.html +396 -0
  69. data/doc/Lims/Core/Persistence/Sequel.html +117 -0
  70. data/doc/Lims/Core/Persistence/Sequel/Filters.html +462 -0
  71. data/doc/Lims/Core/Persistence/Sequel/ForTest.html +101 -0
  72. data/doc/Lims/Core/Persistence/Sequel/ForTest/Name.html +137 -0
  73. data/doc/Lims/Core/Persistence/Sequel/ForTest/Name/NamePersitor.html +143 -0
  74. data/doc/Lims/Core/Persistence/Sequel/Migrations.html +266 -0
  75. data/doc/Lims/Core/Persistence/Sequel/Persistor.html +665 -0
  76. data/doc/Lims/Core/Persistence/Sequel/Session.html +501 -0
  77. data/doc/Lims/Core/Persistence/Sequel/Store.html +417 -0
  78. data/doc/Lims/Core/Persistence/Session.html +2751 -0
  79. data/doc/Lims/Core/Persistence/Session/UnmanagedObjectError.html +111 -0
  80. data/doc/Lims/Core/Persistence/StateGroup.html +696 -0
  81. data/doc/Lims/Core/Persistence/StateList.html +498 -0
  82. data/doc/Lims/Core/Persistence/Store.html +695 -0
  83. data/doc/Lims/Core/Persistence/UuidResource.html +1044 -0
  84. data/doc/Lims/Core/Persistence/UuidResource/InvalidUuidError.html +111 -0
  85. data/doc/Lims/Core/Persistence/UuidResource/UuidResourcePersistor.html +337 -0
  86. data/doc/Lims/Core/Persistence/Uuidable.html +799 -0
  87. data/doc/Lims/Core/Persistor.html +320 -0
  88. data/doc/Lims/Core/Resource.html +165 -0
  89. data/doc/Object.html +228 -0
  90. data/doc/SessionSpec.html +101 -0
  91. data/doc/SessionSpec/Model.html +279 -0
  92. data/doc/SessionSpec/Model/ModelPersistor.html +327 -0
  93. data/doc/_index.html +732 -0
  94. data/doc/class_list.html +47 -0
  95. data/doc/css/common.css +1 -0
  96. data/doc/css/full_list.css +55 -0
  97. data/doc/css/style.css +322 -0
  98. data/doc/file.README.html +127 -0
  99. data/doc/file_list.html +49 -0
  100. data/doc/frames.html +13 -0
  101. data/doc/index.html +127 -0
  102. data/doc/js/app.js +205 -0
  103. data/doc/js/full_list.js +167 -0
  104. data/doc/js/jquery.js +16 -0
  105. data/doc/method_list.html +1894 -0
  106. data/doc/top-level-namespace.html +100 -0
  107. data/lib/common.rb +18 -0
  108. data/lib/lims-core.rb +29 -0
  109. data/lib/lims-core/actions.rb +10 -0
  110. data/lib/lims-core/actions/action.rb +185 -0
  111. data/lib/lims-core/actions/action_group.rb +54 -0
  112. data/lib/lims-core/actions/bulk_action.rb +65 -0
  113. data/lib/lims-core/base.rb +132 -0
  114. data/lib/lims-core/helpers.rb +41 -0
  115. data/lib/lims-core/persistence.rb +15 -0
  116. data/lib/lims-core/persistence/comparison_filter.rb +54 -0
  117. data/lib/lims-core/persistence/filter.rb +23 -0
  118. data/lib/lims-core/persistence/identity_map.rb +55 -0
  119. data/lib/lims-core/persistence/logger/all.rb +5 -0
  120. data/lib/lims-core/persistence/logger/persistor.rb +35 -0
  121. data/lib/lims-core/persistence/logger/session.rb +30 -0
  122. data/lib/lims-core/persistence/logger/store.rb +37 -0
  123. data/lib/lims-core/persistence/message_bus.rb +131 -0
  124. data/lib/lims-core/persistence/multi_criteria_filter.rb +50 -0
  125. data/lib/lims-core/persistence/persist_association_trait.rb +96 -0
  126. data/lib/lims-core/persistence/persistable_trait.rb +150 -0
  127. data/lib/lims-core/persistence/persistor.rb +495 -0
  128. data/lib/lims-core/persistence/resource_state.rb +157 -0
  129. data/lib/lims-core/persistence/search.rb +3 -0
  130. data/lib/lims-core/persistence/search/all.rb +3 -0
  131. data/lib/lims-core/persistence/search/create_search.rb +55 -0
  132. data/lib/lims-core/persistence/search/search_persistor.rb +45 -0
  133. data/lib/lims-core/persistence/search/search_sequel_persistor.rb +40 -0
  134. data/lib/lims-core/persistence/sequel.rb +14 -0
  135. data/lib/lims-core/persistence/sequel/filters.rb +106 -0
  136. data/lib/lims-core/persistence/sequel/migrations.rb +14 -0
  137. data/lib/lims-core/persistence/sequel/migrations/add_audit_tables.rb +147 -0
  138. data/lib/lims-core/persistence/sequel/migrations/initial.rb +156 -0
  139. data/lib/lims-core/persistence/sequel/persistor.rb +200 -0
  140. data/lib/lims-core/persistence/sequel/session.rb +136 -0
  141. data/lib/lims-core/persistence/sequel/store.rb +37 -0
  142. data/lib/lims-core/persistence/session.rb +409 -0
  143. data/lib/lims-core/persistence/state_group.rb +97 -0
  144. data/lib/lims-core/persistence/state_list.rb +56 -0
  145. data/lib/lims-core/persistence/store.rb +73 -0
  146. data/lib/lims-core/persistence/uuid_resource.rb +115 -0
  147. data/lib/lims-core/persistence/uuid_resource_persistor.rb +43 -0
  148. data/lib/lims-core/persistence/uuidable.rb +107 -0
  149. data/lib/lims-core/resource.rb +21 -0
  150. data/lib/lims-core/subclass_tracker.rb +30 -0
  151. data/lib/lims-core/version.rb +5 -0
  152. data/lims-core.gemspec +40 -0
  153. data/makefile +52 -0
  154. data/showoff/core-2012-06-11/core/01_slide.md +237 -0
  155. data/showoff/core-2012-06-11/core/02_slide.md +110 -0
  156. data/showoff/core-2012-06-11/custom.css +44 -0
  157. data/showoff/core-2012-06-11/main/01_slide.md +53 -0
  158. data/showoff/core-2012-06-11/showoff.json +10 -0
  159. data/showoff/core-2012-06-11/tp1.tpl +1 -0
  160. data/spec/actions/action_group_spec.rb +39 -0
  161. data/spec/actions/spec_helper.rb +1 -0
  162. data/spec/persistence/identity_map_spec.rb +55 -0
  163. data/spec/persistence/logger/spec_helper.rb +7 -0
  164. data/spec/persistence/logger/store_spec.rb +48 -0
  165. data/spec/persistence/message_bus_spec.rb +76 -0
  166. data/spec/persistence/sequel/session_spec.rb +125 -0
  167. data/spec/persistence/sequel/spec_helper.rb +39 -0
  168. data/spec/persistence/sequel/store_shared.rb +25 -0
  169. data/spec/persistence/sequel/store_spec.rb +22 -0
  170. data/spec/persistence/session_spec.rb +199 -0
  171. data/spec/persistence/spec_helper.rb +2 -0
  172. data/spec/persistence/uuid_resource_spec.rb +80 -0
  173. data/spec/spec_helper.rb +10 -0
  174. data/spec/subclass_tracker_sperc.rb +62 -0
  175. data/utils/constant_tree.rb +29 -0
  176. data/utils/stack.rb +48 -0
  177. metadata +402 -0
@@ -0,0 +1,50 @@
1
+ # vi: ts=2:sts=2:et:sw=2 spell:spelllang=en
2
+
3
+ require 'lims-core/persistence/filter'
4
+ require 'lims-core/resource'
5
+
6
+
7
+ module Lims::Core
8
+ module Persistence
9
+ # Filter performing a && between all the pairs of a map.
10
+ # Key being the field
11
+ # Value can be either a String, an Array or a Hash.
12
+ # Strings and Arrays are normal filters, whereas Hashes
13
+ # correspond to a joined search. The criteria will apply to the
14
+ # joined object corresponding to the key.
15
+ # @example
16
+ # {
17
+ # :status => [:pending, :in_progress],
18
+ # :item => {
19
+ # :status => [:pending],
20
+ # :uuid => <plate_uuid>
21
+ # }
22
+ # }
23
+ # Will look for all the orders in pending or in progress status
24
+ # *holding* a plate with a pending status.
25
+ #
26
+ class MultiCriteriaFilter < Filter
27
+ include Resource
28
+ attribute :criteria, Hash, :required => true
29
+ # For Sequel, keys needs to be a Symbol to be seen as column.
30
+ # String are seen as 'value'
31
+ def initialize(criteria)
32
+ criteria = { :criteria => criteria } unless criteria.include?(:criteria)
33
+ criteria[:criteria].rekey!{ |k| k.to_sym }
34
+ super(criteria)
35
+ end
36
+
37
+ def call(persistor)
38
+ persistor.multi_criteria_filter(criteria)
39
+ end
40
+ end
41
+ end
42
+
43
+ class Persistor
44
+ # @param [Hash] criteria a
45
+ # @return [Persistor]
46
+ def multi_criteria_filter(criteria)
47
+ raise NotImplementedError "multi_criteria_filter methods needs to be implemented for subclass of Persistor"
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,96 @@
1
+ require 'lims-core/persistence/persistor'
2
+ require 'lims-core/persistence/persistable_trait'
3
+ require 'modularity'
4
+
5
+ module Lims::Core
6
+ module Persistence
7
+ module PersistAssociationTrait
8
+ as_trait do |parent_class=nil, args={}|
9
+ model_name = name.split('::').last.snakecase
10
+ session_name = "#{model_name}_persistor"
11
+ parents = attributes.select { |a| a.options[:relation] == :parent }
12
+ children = attributes.select { |a| a.options[:relation] == :child}
13
+ parent_class.class_eval <<-EOC
14
+ def #{model_name}
15
+ @session.#{session_name}
16
+ end
17
+ EOC
18
+ class_eval <<-EOC
19
+ NOT_IN_ROOT = true
20
+ SESSION_NAME = '#{session_name}'
21
+ def initialize(*args)
22
+ #{
23
+ attributes.map do |att|
24
+ "@#{att.name}=args.shift"
25
+ end.join(';')
26
+ }
27
+ end
28
+
29
+ # inline attributes method
30
+ def attributes
31
+ {
32
+ #{
33
+ attributes.map { |a| "#{a.name}: @#{a.name}" }.join(',')
34
+ }
35
+ }
36
+ end
37
+
38
+ def keys
39
+ [#{
40
+ attributes.reject { |a| a.options[:exclude_from_key] }.map do |a|
41
+ a.options[:primitive].ancestors.include?(Resource) ? "@#{a.name}.object_id" : "@#{a.name}"
42
+ end.join(', ')
43
+ }]
44
+ end
45
+
46
+ def hash
47
+ keys.hash
48
+ end
49
+
50
+ def eql?(other)
51
+ keys == other.keys
52
+ end
53
+
54
+ does 'lims/core/persistence/persistable', :parents => [
55
+ #{
56
+ parents.map do |a|
57
+ a.options.merge(:name => a.name).inspect
58
+ end.join(', ')
59
+ }
60
+ ], :children => [
61
+ #{
62
+ children.map { |a| ":#{a.name}" }.join(', ')
63
+ }
64
+ ]
65
+
66
+ class #{name.split('::').last}Persistor
67
+ def new_from_attributes(attributes)
68
+ #{
69
+ attributes.map do |a|
70
+ if parents.include?(a)
71
+ "@session_#{a.name} ||= @session.#{a.name}"
72
+ end
73
+ end.join('; ')
74
+ }
75
+
76
+ super(attributes) do
77
+ model.new(
78
+ #{
79
+ attributes.map do |a|
80
+ if parents.include?(a)
81
+ "@session_#{a.name}[attributes.delete(:#{a.name}_id)]"
82
+ else
83
+ "attributes.delete(:#{a.name})"
84
+ end
85
+ end.join(', ')
86
+ }
87
+ ).tap { |m| m.on_load}
88
+ end
89
+ end
90
+ end
91
+ EOC
92
+
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,150 @@
1
+ require 'lims-core/persistence/persistor'
2
+ require 'modularity'
3
+
4
+ module Lims::Core
5
+ module Persistence
6
+ module PersistableTrait
7
+ as_trait do |args={}|
8
+ # Define basic persistor
9
+ model_name = self.name.split('::').last
10
+ persistor_name = "#{model_name}Persistor"
11
+ class_eval <<-EOC
12
+ # define Persistor class
13
+ class #{persistor_name} < Persistor
14
+ does 'lims/core/persistence/persistor', #{name}, #{args.inspect}
15
+ end
16
+ EOC
17
+ end
18
+ end
19
+ module PersistorTrait
20
+ as_trait do |model, args|
21
+ self::Model=model
22
+ model_name = model.name.split('::').last
23
+ parents = []
24
+ deletable_parents = []
25
+ session_names = {}
26
+ skip_parents_for_attributes = {}
27
+
28
+ args[:parents].andtap do |_parents|
29
+ # preprocess parents to get a list
30
+
31
+ _parents.each do |parent|
32
+ if parent.is_a? Hash
33
+ name = parent[:name].to_s
34
+ session_names[name] = parent[:session_name] || name
35
+ skip_parents_for_attributes[name] = parent[:skip_parents_for_attributes]
36
+ deletable_parents << name if parent[:deletable]
37
+ else
38
+ name = parent.to_s
39
+ session_names[name] = name
40
+ end
41
+ parents << name
42
+ end
43
+ end
44
+
45
+ children = []
46
+ deletable_children = []
47
+ args[:children].andtap do |_children|
48
+ _children.each do |child|
49
+ if child.is_a? Hash
50
+ name = child[:name].to_s
51
+ deletable_children << name if child[:deletable]
52
+ else
53
+ name = child.to_s
54
+ session_names[name] = name
55
+ end
56
+ children << name
57
+ end
58
+ end
59
+
60
+ if parents.size >= 1
61
+ class_eval <<-EOC
62
+ def filter_attributes_on_load(attributes)
63
+ attributes.mash do |k, v|
64
+ case k
65
+ #{ parents.map do |p|
66
+ "when :#{p}_id then [:#{p}, (@session_#{p} ||= @session.#{session_names[p]})[v]]"
67
+ end.join(';')
68
+ }
69
+ else [k,v]
70
+ end
71
+ end
72
+ end
73
+
74
+ def attribute_for(key)
75
+ {
76
+ #{ parents.map {|p| "#{p}: '#{p}_id'" }.join(',')}
77
+ }[key]
78
+ end
79
+
80
+ def parents(resource)
81
+ [
82
+ #{ parents.map do |p|
83
+ "resource.#{p}"
84
+ end.join(',') }
85
+ ].compact
86
+ end
87
+
88
+ def parents_for_attributes(attributes)
89
+ [
90
+ #{ parents.reject{|p| skip_parents_for_attributes[p] }.map do |p|
91
+ "(@session_#{p} ||= @session.#{session_names[p]}).state_for_id(attributes[:#{p}_id])"
92
+ end.join(',') }
93
+ ]
94
+ end
95
+ EOC
96
+ end
97
+ unless children.empty?
98
+ class_eval <<-EOC
99
+ def children(resource)
100
+ [].tap do |list|
101
+ #{
102
+ children.map do |child|
103
+ "children_#{child}(resource, list)"
104
+ end.join(';')
105
+ }
106
+ end
107
+ end
108
+
109
+ def load_children(states)
110
+ #{
111
+ children.map do |child|
112
+ "#{child}.find_by(:#{model_name.snakecase}_id => states.map(&:id))"
113
+ end.join(';')
114
+ }
115
+ 1
116
+ end
117
+ EOC
118
+ end
119
+
120
+ unless deletable_children.empty?
121
+ class_eval <<-EOC
122
+ def deletable_children(resource)
123
+ [].tap do |list|
124
+ #{
125
+ deletable_children.map do |child|
126
+ "children_#{child}(resource, list)"
127
+ end.join(';')
128
+ }
129
+ end
130
+ end
131
+ EOC
132
+ end
133
+ unless deletable_parents.empty?
134
+ class_eval <<-EOC
135
+ def deletable_parents(resource)
136
+ [].tap do |list|
137
+ #{
138
+ deletable_parents.map do |parent|
139
+ "resource.#{parent}.andtap { |p| list << p }"
140
+ end.join(';')
141
+ }
142
+ end
143
+ end
144
+ EOC
145
+ end
146
+ self
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,495 @@
1
+ # vi: ts=2:sts=2:et:sw=2 spell:spelllang=en
2
+
3
+ require 'lims-core/persistence/identity_map'
4
+
5
+ module Lims::Core
6
+ module Persistence
7
+ # @abstract Base class for all the persistors, needs to implements a `self.model`
8
+ # returning the class to persist.
9
+ # A persistor , is used to save and load it's cousin class.
10
+ # The specific code of a persistor should be extended by writting
11
+ # a persistor class within the class to persist and module corresponding to the store.
12
+ # The common Persistor architecture would be like this (let's consider we have a Plate class and a Sequel Persistor).
13
+ # @code
14
+ # module SequelPersistor
15
+ # end
16
+ # Class Plate
17
+ # Common to all store
18
+ # class PlatePersistor < Persistence::Persistor
19
+ # end
20
+ #
21
+ # class PlateSequelPersistor < PlatePersistor
22
+ # include SequelPersistor
23
+ # end
24
+ # end
25
+ # if a base persistor exists for a class but not the store specific one (PlatePersistor exists
26
+ # but PlateSequelPersistor not). If there is a store pecific Persistor module (like SequelPersistor).
27
+ # The equivalent of PlateSequelPersistor will be generated on the fly by deriving the base one and including the mixin.
28
+ # Persistor needs to be registered to be accessible form the session.
29
+ # However, if NO_AUTO_REGISTRATION is not enabled persistors will register themselves. In that case,
30
+ # they will need to be defined in class to persist see {register_model}.
31
+ # If a base peristor for exists for a class but there is no
32
+ # Each instance can get an identity map, and or parameter
33
+ # specific to a session/thread.
34
+ # * Methods relative to store are
35
+ # - insert : a new object to the store
36
+ # - delete : remove an object fromt the store
37
+ # - update : modify an existing object from the store.
38
+ # - retrieve : get an object from the store.
39
+ # - bulk_<method> vs <method> refers to method acting on a list of states
40
+ # instead of an individual object. Althoug only one version needs to be implemted
41
+ # , the bulk version is prefered for performance reason.
42
+ # - raw_<method_ refers when exists to the physical action done to the store
43
+ # without any side effect on the Session or Persistor. They should not normally be called.
44
+ # * Methods relative to parents/children
45
+ # - parents : resources needed to be saved BEFORE the resource itself.
46
+ # - children : resources needed to be save AFTER the resource itself.
47
+ # - deletable_children : resources which needs to be deleted BEFORE the resource itself.
48
+ # - deletable_parent : resources which needs to be deleted AFTER the resource itself.
49
+ class Persistor
50
+
51
+ # Raised if there is any duplicate in the identity maps
52
+ class DuplicateError < RuntimeError
53
+ def initialize(persistor, value)
54
+ super("#{value.inspect} already exists for persistor #{persistor.model}")
55
+ end
56
+ end
57
+
58
+ #Raised if the `id` is already associated to a different `object`
59
+ class DuplicateIdError <DuplicateError
60
+ end
61
+
62
+ #Raised if the `object` is already associated to a different `id`
63
+ class DuplicateObjectError < DuplicateError
64
+ end
65
+ # Performs an autoregistration if needed.
66
+ # Autoregistration can be skipped by defined NO_AUTO_REGISTRATION
67
+ # on the model class.
68
+ # See {Persistor::register_model}.
69
+ def self.inherited(subclass)
70
+ register_model(subclass)
71
+ end
72
+
73
+ # Register a sub-persistor to the {Session}.
74
+ # The name used to register the persistor would be
75
+ # either the name of the model (parent) class
76
+ # or if SESSION_NAME is specified on the model : SESSION_NAME
77
+ # @param [Class] subclass
78
+ def self.register_model(subclass)
79
+ model = subclass.parent_scope
80
+ return if model::const_defined? :NO_AUTO_REGISTRATION
81
+
82
+ name =\
83
+ if model::const_defined? :SESSION_NAME
84
+ model::SESSION_NAME
85
+ else
86
+ name = model.name.split('::').pop
87
+ end
88
+
89
+ Session::register_model(name, model)
90
+ end
91
+
92
+ def initialize (session, *args, &block)
93
+ @session = session
94
+ @id_to_state = Hash.new { |h,k| h[k] = ResourceState.new(nil, self, k) }
95
+ @object_to_state = Hash.new { |h,k| h[k] = ResourceState.new(k, self) }
96
+ super(*args, &block)
97
+ end
98
+
99
+ # Associate class (without persistence).
100
+ # @return [Class]
101
+ def model
102
+ self.class::Model
103
+ end
104
+
105
+ # Load a model by different criteria. Could be either :
106
+ # - an Id
107
+ # - a Hash
108
+ # - a list of Ids
109
+ # This method will return either a single object or a list of object,
110
+ # depending of the parameter.
111
+ # Note that loaded object are automatically _added_ to the session.
112
+ # @param [Fixnum, Hash] id the id in the database
113
+ # @param [Boolean] single or list of object to return
114
+ # @return [Object,nil] nil if object not found.
115
+ def [](id, single=true)
116
+ case id
117
+ when Fixnum then retrieve(id)
118
+ when Hash then find_by(filter_attributes_on_save(id), single)
119
+ when Array, Enumerable then bulk_retrieve(id)
120
+ end
121
+ end
122
+
123
+ # Get the id from an object from the cache.
124
+ # @param [Resource] object object to find the id for.
125
+ # @return [Id, Nil]
126
+ def id_for(object)
127
+ state_for(object).andtap { |state| state.id }
128
+ end
129
+
130
+ # Get the object from a given id.
131
+ # @param [Fixnum] id
132
+ # @return [Resourec, Nil]
133
+ def object_for(id)
134
+ @id_to_state[id].andtap(&:resource)
135
+ end
136
+
137
+
138
+ # Returns the state proxy of an object.
139
+ # Creates it if needed.
140
+ # @param [Resource] object
141
+ # @return [ResourceState]
142
+ def state_for(object)
143
+ @object_to_state[object]
144
+ end
145
+
146
+
147
+ def state_for?(object)
148
+ @object_to_state.include?(object)
149
+ end
150
+
151
+ # Returns the state proxy of an object fromt its id (in cache).
152
+ # Creates the state if needed.
153
+ # @param [Id] object
154
+ # @return [ResourceState]
155
+ def state_for_id(id)
156
+ @id_to_state[id]
157
+ end
158
+
159
+ # Updates the cache so id_to_state
160
+ # reflects state.id
161
+ # @param [ResourceState]
162
+ def bind_state_to_id(state)
163
+ raise RuntimeError, 'Invalid state' if state.persistor != self
164
+ raise DuplicateIdError.new(self, state.id)if @id_to_state.include?(state.id)
165
+ on_object_load(state)
166
+ @id_to_state[state.id] = state
167
+ end
168
+
169
+ # Called by Persistor to inform the session
170
+ # about the loading of an object.
171
+ # MUST be called by persistors creating Resources.
172
+ # @param [ResourceState]
173
+ def on_object_load(state)
174
+ @session.manage_state(state)
175
+ end
176
+
177
+ # Update the cache
178
+ def bind_state_to_resource(state)
179
+ raise RuntimeError, 'Invalobject state' if state.persistor != self
180
+ raise DuplicateIdError.new(self, state.resource) if @object_to_state.include?(state.resource)
181
+ @object_to_state[state.resource] = state
182
+ end
183
+
184
+ # Creates a new object from a Hash and associate it to its id
185
+ # @param [Id] id id of the new object
186
+ # @param [Hash] attributes of the new object.
187
+ # @return [Resource]
188
+ def new_object(id, attributes)
189
+ id = attributes.delete(primary_key)
190
+ model.new(filter_attributes_on_load(attributes)).tap do |resource|
191
+ state = state_for_id(id)
192
+ state.resource = resource
193
+ end
194
+ end
195
+
196
+ # Computes "dirty_key" of an object.
197
+ # The dirty key is used to decide if an object
198
+ # has been modified or not.
199
+ # @param [ Resource]
200
+ # @return [Object]
201
+ def dirty_key_for(resource)
202
+ if resource && @session.dirty_attribute_strategy
203
+ @session.dirty_key_for(filter_attributes_on_save(resource.attributes_for_dirty))
204
+ end
205
+ end
206
+
207
+ # Delete all invalid object loaded by a persistor.
208
+ # Typically invalid object are association which doesn't exist anymore
209
+ def purge_invalid_object
210
+ to_delete = StateGroup.new(self, [])
211
+ @object_to_state.each do |object, state|
212
+ to_delete << state if invalid_resource?(object)
213
+ end
214
+
215
+ to_delete.destroy
216
+ end
217
+
218
+ # Create or get one or object matching the criteria
219
+ # @param [Hash] criteria, map of (attributes, value) to match
220
+ # @param [Boolean] single wether to check for uniquess or not
221
+ # @return [Object,nil,Array<Object>] an Object or and Array depending of single.
222
+ #
223
+ def find_by(criteria, single=false)
224
+ ids = ids_for(criteria)
225
+
226
+ if single
227
+ raise RuntimeError, "More than one object match the criteria" if ids.size > 1
228
+ return nil if ids.size < 1
229
+ self[ids].first
230
+ else
231
+ self[ids]
232
+ end
233
+ end
234
+ protected :find_by
235
+
236
+ # compute a list of ids matching the criteria
237
+ # @param [Hash] criteria list of attribute/value pais
238
+ # @return [Array<Id>]
239
+ def ids_for(criteria)
240
+ raise NotImplementedError
241
+ end
242
+
243
+ # @abstract
244
+ # Returns the number of object in the store
245
+ # @return [Fixnum]
246
+ def count
247
+ raise NotImplementedError
248
+ end
249
+
250
+ # @abstract
251
+ # Load a slice. Doesn't return an object but a hash
252
+ # allowing to build it.
253
+ # @param [Fixnum] start (0 based)
254
+ # @param [Fixnum] length
255
+ # @yieldparam [Fixnum] key
256
+ # @yieldparam [Hash] attributes of the object
257
+ def for_each_in_slice(start, length)
258
+ raise NotImplementedError
259
+ end
260
+
261
+ # Get a slice of object by offset, length.
262
+ # +start+ here is an offset (starting at 0) not an Id.
263
+ # @param [Fixnum] start (0 based)
264
+ # @param [Fixnum] length
265
+ # @return [Enumerable<Hash>]
266
+ def slice(start, length)
267
+ to_load = StateGroup.new(self, [])
268
+ for_each_in_slice(start, length) do |att|
269
+ to_load << new_from_attributes(att)
270
+ end
271
+ to_load.load.map(&:resource)
272
+ end
273
+
274
+ # Inserts objects in the underlying store AND manages them.
275
+ # This method only care about the objects themselves not about
276
+ # theirs parents or children.
277
+ # The physical insert in the store must be specified for each store.
278
+ def bulk_insert(states, *params)
279
+ states.map { |state| insert(state, *params) }
280
+ end
281
+
282
+ # Remove object form the underlying store and Manages them.
283
+ # This method only care about the objects themselves not about
284
+ # theirs parents or children.
285
+ def bulk_delete(states, *params)
286
+ # delete theme but leave them in cache
287
+ # in case they need to be displayed.
288
+ states.each do |state|
289
+ state.id.andtap { |id| @id_to_state.delete(id) }
290
+ state.resource #.andtap { |object| @object_to_state.delete(object) }
291
+ end
292
+ bulk_delete_raw(states.map(&:id).compact, *params)
293
+ end
294
+
295
+ # @abstract
296
+ # Physically remove objects from a store.
297
+ def bulk_delete_raw(states, *params)
298
+ raise NotImplementedError
299
+ end
300
+
301
+ %w(insert update delete_raw).each do |method|
302
+ class_eval %Q{
303
+ #bulk_#{method} and #{method} can be both implemented from each other.
304
+ #raise a NotImplementedError is none of them have been implemented
305
+ def #{method}(param, *params)
306
+ raise NotImplementedError if @__simple_#{method}
307
+ @__simple_#{method} = true
308
+ bulk_#{method}([param], *params).andtap do |results|
309
+ @__simple_#{method} = false
310
+ results.first
311
+ end
312
+ end
313
+ }
314
+ end
315
+
316
+ # Retrieves an object from it's id.
317
+ # Doesn't load it if it's been alreday loaded.
318
+ # @param [Id] id
319
+ # @return [Object, nil]
320
+ def retrieve(id, *params)
321
+ object_for(id).andtap { |o| return o }
322
+ objects = bulk_retrieve([id], *params)
323
+ return objects.first if objects && objects.size == 1
324
+
325
+ end
326
+
327
+ # Retreives a list of objects .
328
+ # @param[Array<Id>] ids
329
+ # @return [Array<Object]
330
+ def bulk_retrieve(ids, *params)
331
+ # create a list of states and load them
332
+ states = StateGroup.new(self, ids.map do |id|
333
+ @id_to_state[id]
334
+ end)
335
+
336
+ states.load
337
+ return StateList.new(states.map { |state| state.resource })
338
+
339
+ # we need to separate object which need to be loaded
340
+ # from the one which are already in cache
341
+ to_load = ids.reject { |id| id == nil || @id_to_state.include?(id) }
342
+ loaded_states = bulk_load_raw_attributes(to_load, *params) do |att|
343
+ id = att.delete(primary_key)
344
+ new_state_for_attribute(id, att).resource
345
+ end
346
+
347
+ bulk_retrieve_children(new_states, *params)
348
+ #bulk_retrieve_parent(new_states, *params)
349
+
350
+
351
+ ids.map { |id| object_for(id) }
352
+ end
353
+
354
+ # Updates the store and manages object.
355
+ # Doesn't care of children or parents.
356
+ # @param [Array<ResourceState] states
357
+ def bulk_update(states, *params)
358
+ attributes = states.map do |state|
359
+ filter_attributes_on_save(state.resource.attributes).merge(primary_key => state.id)
360
+ end
361
+ bulk_update_raw_attributes(attributes, *params)
362
+ states.each do |state|
363
+ state.updated
364
+ end
365
+ end
366
+
367
+ %w(parents children deletable_children deletable_parents).each do |m|
368
+ # @method #{m}_for
369
+ # @param [Resource]
370
+ # @return [Array<ResourceState>]
371
+ define_method "#{m}_for" do |resource|
372
+ @session.states_for(public_send(m, resource))
373
+ end
374
+ end
375
+
376
+ # List of parents of object, i.e. object which need to be saved BEFORE it.
377
+ # Default implementation get all Resource attributes.
378
+ # @param [Resource] resource
379
+ # @return [Array<Resource>]
380
+ def parents(resource)
381
+ resource.attributes.values.select { |v| v.is_a? Resource }
382
+ end
383
+
384
+ # List of children , i.e, object which need to be saved AFTER it.
385
+ # @param [Resource] resource
386
+ # @return [Array<Resource>]
387
+ def children(resource)
388
+ []
389
+ end
390
+
391
+ # @todo
392
+ def deletable_children(resource)
393
+ []
394
+ end
395
+
396
+ def deletable_parents(resource)
397
+ []
398
+ end
399
+
400
+ # if a resource is invalid and need to be deleted.
401
+ # For example an association proxy corresponding
402
+ # to an old relation.
403
+ def invalid_resource?(resource)
404
+ resource.respond_to?(:invalid?) && resource.invalid?
405
+ end
406
+
407
+
408
+
409
+ protected
410
+ # The primary key
411
+ # @return [Symbol]
412
+ def primary_key()
413
+ :id
414
+ end
415
+
416
+ # Transform store fields to object attributes
417
+ # This can be used to change the name of an attribute (its key)
418
+ # or its value or both (example resource to resource_id)
419
+ # This is the reverse of {#filter_attributes_on_save}
420
+ # @param [Hash] attributes
421
+ # @return [Hash]
422
+ def filter_attributes_on_load(attributes)
423
+ if block_given?
424
+ attributes.mash do |k,v|
425
+ yield(k,v) || [k,v]
426
+ end
427
+ else attributes
428
+ end
429
+ end
430
+
431
+ def parents_for_attributes(attributes)
432
+ []
433
+ end
434
+
435
+ public :parents_for_attributes
436
+ def load_children(states, *params)
437
+ []
438
+ end
439
+ public :load_children
440
+
441
+ def new_from_attributes(attributes)
442
+ id = attributes.delete(primary_key)
443
+ resource = block_given? ? yield(attributes) : model.new(filter_attributes_on_load(attributes))
444
+ state_for_id(id).tap { |state| state.resource = resource }
445
+ end
446
+ public :new_from_attributes
447
+
448
+ # Transform object attributes to store fields
449
+ # This can be used to change the name of an attribute (its key)
450
+ # or its value or both (example resource to resource_id)
451
+ # @param [Hash] attributes
452
+ # @return [Hash]
453
+ def filter_attributes_on_save(attributes)
454
+ attributes.mash do |k, v|
455
+ if block_given?
456
+ result = yield(k,v)
457
+ next result if result
458
+ end
459
+ key = attribute_for(k)
460
+ if key && key != k
461
+ [key, @session.id_for(v) ]
462
+ else
463
+ [k, v]
464
+ end
465
+ end
466
+ end
467
+
468
+
469
+ def attribute_for(key)
470
+ key
471
+ end
472
+
473
+ def self.association_class(association, &block)
474
+ snake = association.snakecase
475
+ association_class = class_eval <<-EOC
476
+ class #{association}
477
+ include Lims::Core::Resource
478
+ end
479
+
480
+ def #{snake}
481
+ @session.#{snake}_persistor
482
+ end
483
+ #{association}
484
+ EOC
485
+ association_class.class_eval(&block)
486
+ association_class.class_eval do
487
+ does "lims/core/persistence/persist_association", self
488
+ end
489
+ association_class
490
+
491
+ end
492
+ end
493
+ end
494
+ end
495
+ require 'lims-core/persistence/session'