lims-core 3.2.3

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 (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'