libis-workflow 2.0.24 → 2.0.25

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.coveralls.yml +1 -1
  3. data/.gitignore +36 -36
  4. data/.travis.yml +32 -32
  5. data/Gemfile +4 -4
  6. data/LICENSE +20 -20
  7. data/README.md +380 -380
  8. data/Rakefile +6 -6
  9. data/lib/libis/exceptions.rb +6 -6
  10. data/lib/libis/workflow.rb +41 -41
  11. data/lib/libis/workflow/action.rb +24 -24
  12. data/lib/libis/workflow/base/dir_item.rb +13 -13
  13. data/lib/libis/workflow/base/file_item.rb +80 -80
  14. data/lib/libis/workflow/base/job.rb +83 -83
  15. data/lib/libis/workflow/base/logging.rb +66 -66
  16. data/lib/libis/workflow/base/run.rb +95 -95
  17. data/lib/libis/workflow/base/work_item.rb +173 -173
  18. data/lib/libis/workflow/base/workflow.rb +149 -149
  19. data/lib/libis/workflow/config.rb +22 -22
  20. data/lib/libis/workflow/dir_item.rb +10 -10
  21. data/lib/libis/workflow/file_item.rb +15 -15
  22. data/lib/libis/workflow/job.rb +28 -28
  23. data/lib/libis/workflow/message_registry.rb +30 -30
  24. data/lib/libis/workflow/run.rb +34 -34
  25. data/lib/libis/workflow/status.rb +133 -133
  26. data/lib/libis/workflow/task.rb +316 -316
  27. data/lib/libis/workflow/task_group.rb +71 -71
  28. data/lib/libis/workflow/task_runner.rb +34 -34
  29. data/lib/libis/workflow/version.rb +5 -5
  30. data/lib/libis/workflow/work_item.rb +37 -37
  31. data/lib/libis/workflow/worker.rb +42 -42
  32. data/lib/libis/workflow/workflow.rb +20 -20
  33. data/libis-workflow.gemspec +38 -38
  34. data/spec/items.rb +2 -2
  35. data/spec/items/test_dir_item.rb +13 -13
  36. data/spec/items/test_file_item.rb +16 -16
  37. data/spec/items/test_run.rb +8 -8
  38. data/spec/spec_helper.rb +8 -8
  39. data/spec/task_spec.rb +15 -15
  40. data/spec/tasks/camelize_name.rb +12 -12
  41. data/spec/tasks/checksum_tester.rb +32 -32
  42. data/spec/tasks/collect_files.rb +47 -47
  43. data/spec/workflow_spec.rb +154 -154
  44. metadata +3 -3
@@ -1,67 +1,67 @@
1
- module Libis
2
- module Workflow
3
- module Base
4
- module Logging
5
-
6
- # Add a structured message to the log history. The message text can be submitted as an integer or text. If an
7
- # integer is submitted, it will be used to look up the text in the MessageRegistry. The message text will be
8
- # passed to the % operator with the args parameter. If that failes (e.g. because the format string is not correct)
9
- # the args value is appended to the message.
10
- #
11
- # @param [Symbol] severity
12
- # @param [Hash] msg should contain message text as :id or :text and the hierarchical name of the task as :task
13
- # @param [Array] args string format values
14
- def log_message(severity, msg, *args)
15
- # Prepare info from msg struct for use with string substitution
16
- message_id, message_text = if msg[:id]
17
- [msg[:id], MessageRegistry.instance.get_message(msg[:id])]
18
- elsif msg[:text]
19
- [0, msg[:text]]
20
- else
21
- [0, '']
22
- end
23
- task = msg[:task] || ''
24
- message_text = (message_text % args rescue "#{message_text} - #{args}")
25
-
26
- run_id = self.get_run.id rescue nil
27
-
28
- self.add_log severity: severity, id: message_id.to_i, text: message_text, task: task, run_id: run_id
29
- end
30
-
31
- # Helper function for the WorkItems to add a log entry to the log_history.
32
- #
33
- # The supplied message structure is expected to contain the following fields:
34
- # - :severity : ::Logger::Severity value
35
- # - :id : optional message id
36
- # - :text : message text
37
- # - :task : list of tasks names (task hierarchy) that submits the message
38
- #
39
- # @param [Hash] message
40
- def add_log(message = {})
41
- msg = message_struct(message)
42
- add_log_entry(msg)
43
- self.save!
44
- end
45
-
46
- def <=(message = {})
47
- self.add_log(message)
48
- end
49
-
50
- protected
51
-
52
- # create and return a proper message structure
53
- # @param [Hash] opts
54
- def message_struct(opts = {})
55
- opts.reverse_merge!(severity: :info, code: nil, text: '')
56
- {
57
- severity: ::Logging::levelify(opts[:severity]).upcase,
58
- task: opts[:task],
59
- code: opts[:code],
60
- message: opts[:text]
61
- }.cleanup
62
- end
63
-
64
- end
65
- end
66
- end
1
+ module Libis
2
+ module Workflow
3
+ module Base
4
+ module Logging
5
+
6
+ # Add a structured message to the log history. The message text can be submitted as an integer or text. If an
7
+ # integer is submitted, it will be used to look up the text in the MessageRegistry. The message text will be
8
+ # passed to the % operator with the args parameter. If that failes (e.g. because the format string is not correct)
9
+ # the args value is appended to the message.
10
+ #
11
+ # @param [Symbol] severity
12
+ # @param [Hash] msg should contain message text as :id or :text and the hierarchical name of the task as :task
13
+ # @param [Array] args string format values
14
+ def log_message(severity, msg, *args)
15
+ # Prepare info from msg struct for use with string substitution
16
+ message_id, message_text = if msg[:id]
17
+ [msg[:id], MessageRegistry.instance.get_message(msg[:id])]
18
+ elsif msg[:text]
19
+ [0, msg[:text]]
20
+ else
21
+ [0, '']
22
+ end
23
+ task = msg[:task] || ''
24
+ message_text = (message_text % args rescue "#{message_text} - #{args}")
25
+
26
+ run_id = self.get_run.id rescue nil
27
+
28
+ self.add_log severity: severity, id: message_id.to_i, text: message_text, task: task, run_id: run_id
29
+ end
30
+
31
+ # Helper function for the WorkItems to add a log entry to the log_history.
32
+ #
33
+ # The supplied message structure is expected to contain the following fields:
34
+ # - :severity : ::Logger::Severity value
35
+ # - :id : optional message id
36
+ # - :text : message text
37
+ # - :task : list of tasks names (task hierarchy) that submits the message
38
+ #
39
+ # @param [Hash] message
40
+ def add_log(message = {})
41
+ msg = message_struct(message)
42
+ add_log_entry(msg)
43
+ self.save!
44
+ end
45
+
46
+ def <=(message = {})
47
+ self.add_log(message)
48
+ end
49
+
50
+ protected
51
+
52
+ # create and return a proper message structure
53
+ # @param [Hash] opts
54
+ def message_struct(opts = {})
55
+ opts.reverse_merge!(severity: :info, code: nil, text: '')
56
+ {
57
+ severity: ::Logging::levelify(opts[:severity]).upcase,
58
+ task: opts[:task],
59
+ code: opts[:code],
60
+ message: opts[:text]
61
+ }.cleanup
62
+ end
63
+
64
+ end
65
+ end
66
+ end
67
67
  end
@@ -1,95 +1,95 @@
1
- require 'fileutils'
2
-
3
- require 'libis/workflow/base/work_item'
4
- require 'libis/workflow/task_runner'
5
-
6
- module Libis
7
- module Workflow
8
- module Base
9
-
10
- # Base module for all workflow runs. It is created by an associated workflow when the workflow is executed.
11
- #
12
- # This module lacks the implementation for the data attributes. It functions as an interface that describes the
13
- # common functionality regardless of the storage implementation. These attributes require some implementation:
14
- #
15
- # - start_date: [Time] the timestamp of the execution of the run
16
- # - job: [Object] a reference to the Job this Run belongs to
17
- # - id: [String] (Optional) a unique run number
18
- #
19
- # Note that ::Libis::Workflow::Base::WorkItem is a parent module and therefore requires implementation of the
20
- # attributes of that module too.
21
- #
22
- # A simple in-memory implementation can be found in ::Libis::Workflow::Run
23
- module Run
24
- include ::Libis::Workflow::Base::WorkItem
25
-
26
- attr_accessor :tasks, :action
27
-
28
- def work_dir
29
- # noinspection RubyResolve
30
- dir = File.join(Libis::Workflow::Config.workdir, self.name)
31
- FileUtils.mkpath dir unless Dir.exist?(dir)
32
- dir
33
- end
34
-
35
- def name
36
- self.job.run_name(self.start_date)
37
- end
38
-
39
- def names
40
- Array.new
41
- end
42
-
43
- def namepath
44
- self.name
45
- end
46
-
47
- def workflow
48
- self.job.workflow
49
- end
50
-
51
- def logger
52
- self.properties['logger'] || self.job.logger rescue ::Libis::Workflow::Config.logger
53
- end
54
-
55
- # Execute the workflow.
56
- #
57
- # The action parameter defines how the execution of the tasks will behave:
58
- # - With the default :run action each task will be executed regardsless how the task performed on the item
59
- # previously.
60
- # - When using the :retry action a task will not perform on an item if it was successful the last time. This
61
- # allows you to retry a run when an temporary error (e.g. asynchronous wait or halt) occured.
62
- #
63
- # @param [Symbol] action the type of action to take during this run. :run or :retry
64
- def run(action = :run)
65
- self.action = action
66
-
67
- self.start_date = Time.now unless action == :retry
68
-
69
- self.options = workflow.prepare_input(self.options)
70
-
71
- self.tasks = workflow.tasks
72
- configure_tasks self.options
73
-
74
- self.save!
75
-
76
- runner = Libis::Workflow::TaskRunner.new nil
77
-
78
- self.tasks.each do |task|
79
- runner << task
80
- end
81
-
82
- runner.run self
83
-
84
- end
85
-
86
- protected
87
-
88
- def configure_tasks(opts)
89
- self.tasks.each { |task| task.apply_options opts }
90
- end
91
-
92
- end
93
- end
94
- end
95
- end
1
+ require 'fileutils'
2
+
3
+ require 'libis/workflow/base/work_item'
4
+ require 'libis/workflow/task_runner'
5
+
6
+ module Libis
7
+ module Workflow
8
+ module Base
9
+
10
+ # Base module for all workflow runs. It is created by an associated workflow when the workflow is executed.
11
+ #
12
+ # This module lacks the implementation for the data attributes. It functions as an interface that describes the
13
+ # common functionality regardless of the storage implementation. These attributes require some implementation:
14
+ #
15
+ # - start_date: [Time] the timestamp of the execution of the run
16
+ # - job: [Object] a reference to the Job this Run belongs to
17
+ # - id: [String] (Optional) a unique run number
18
+ #
19
+ # Note that ::Libis::Workflow::Base::WorkItem is a parent module and therefore requires implementation of the
20
+ # attributes of that module too.
21
+ #
22
+ # A simple in-memory implementation can be found in ::Libis::Workflow::Run
23
+ module Run
24
+ include ::Libis::Workflow::Base::WorkItem
25
+
26
+ attr_accessor :tasks, :action
27
+
28
+ def work_dir
29
+ # noinspection RubyResolve
30
+ dir = File.join(Libis::Workflow::Config.workdir, self.name)
31
+ FileUtils.mkpath dir unless Dir.exist?(dir)
32
+ dir
33
+ end
34
+
35
+ def name
36
+ self.job.run_name(self.start_date)
37
+ end
38
+
39
+ def names
40
+ Array.new
41
+ end
42
+
43
+ def namepath
44
+ self.name
45
+ end
46
+
47
+ def workflow
48
+ self.job.workflow
49
+ end
50
+
51
+ def logger
52
+ self.properties['logger'] || self.job.logger rescue ::Libis::Workflow::Config.logger
53
+ end
54
+
55
+ # Execute the workflow.
56
+ #
57
+ # The action parameter defines how the execution of the tasks will behave:
58
+ # - With the default :run action each task will be executed regardsless how the task performed on the item
59
+ # previously.
60
+ # - When using the :retry action a task will not perform on an item if it was successful the last time. This
61
+ # allows you to retry a run when an temporary error (e.g. asynchronous wait or halt) occured.
62
+ #
63
+ # @param [Symbol] action the type of action to take during this run. :run or :retry
64
+ def run(action = :run)
65
+ self.action = action
66
+
67
+ self.start_date = Time.now unless action == :retry
68
+
69
+ self.options = workflow.prepare_input(self.options)
70
+
71
+ self.tasks = workflow.tasks
72
+ configure_tasks self.options
73
+
74
+ self.save!
75
+
76
+ runner = Libis::Workflow::TaskRunner.new nil
77
+
78
+ self.tasks.each do |task|
79
+ runner << task
80
+ end
81
+
82
+ runner.run self
83
+
84
+ end
85
+
86
+ protected
87
+
88
+ def configure_tasks(opts)
89
+ self.tasks.each { |task| task.apply_options opts }
90
+ end
91
+
92
+ end
93
+ end
94
+ end
95
+ end
@@ -1,173 +1,173 @@
1
- require 'backports/rails/hash'
2
- require 'libis/tools/extend/hash'
3
-
4
- require 'libis/workflow/config'
5
- require 'libis/workflow/status'
6
- require_relative 'logging'
7
-
8
- module Libis
9
- module Workflow
10
- module Base
11
-
12
- # Base module for all work items.
13
- #
14
- # This module lacks the implementation for the data attributes. It functions as an interface that describes the
15
- # common functionality regardless of the storage implementation. These attributes require some implementation:
16
- #
17
- # - parent: [Object|nil] a link to a parent work item. Work items can be organized in any hierarchy you think is
18
- # relevant for your workflow (e.g. directory[/directory...]/file/line or library/section/book/page). Of course
19
- # hierarchies are not mandatory.
20
- # - items: [Array] a list of child work items. see above.
21
- # - options: [Hash] a set of options for the task chain on how to deal with this work item. This attribute can be
22
- # used to fine-tune the behaviour of tasks for a particular work item.
23
- # - properties: [Hash] a set of properties, typically collected during the workflow processing and used to store
24
- # final or intermediate resulst of tasks. The ::Lias::Ingester::FileItem module uses this attribute to store the
25
- # properties (e.g. size, checksum, ...) of the file it represents.
26
- # - status_log: [Array] a list of all status changes the work item went through.
27
- # - summary: [Hash] collected statistics about the ingest for the work item and its children. This structure will
28
- # be filled in by the included task ::Lias::Ingester::Tasks::Analyzer wich is appended to the workflow by default.
29
- #
30
- # The module is created so that it is possible to implement an ActiveRecord/Datamapper/... implementation easily.
31
- # A simple in-memory implementation would require:
32
- #
33
- # attr_accessor :parent
34
- # attr_accessor :items
35
- # attr_accessor :options, :properties
36
- # attr_accessor :status_log
37
- # attr_accessor :summary
38
- #
39
- # def initialize
40
- # self.parent = nil
41
- # self.items = []
42
- # self.options = {}
43
- # self.properties = {}
44
- # self.status_log = []
45
- # self.summary = {}
46
- # end
47
- #
48
- # protected
49
- #
50
- # ## Method below should be adapted to match the implementation of the status array
51
- #
52
- # def add_status_log(info)
53
- # self.status_log << info
54
- # end
55
- #
56
- #
57
- module WorkItem
58
- include Enumerable
59
- include Libis::Workflow::Status
60
- include Libis::Workflow::Base::Logging
61
-
62
- # String representation of the identity of the work item.
63
- #
64
- # You may want to overwrite this method as it tries the :name property or whatever #inspect returns if that
65
- # failes. Typically this should return the key value, file name or id number. If that's what your :name property
66
- # contains, you're fine.
67
- #
68
- # @return [String] string identification for this work item.
69
- def name
70
- # noinspection RubyResolve
71
- self.properties['name'] || self.inspect
72
- end
73
-
74
- def to_s;
75
- self.name;
76
- end
77
-
78
- def names
79
- (self.parent.names rescue Array.new).push(name).compact
80
- end
81
-
82
- def namepath;
83
- self.names.join('/');
84
- end
85
-
86
- # File name safe version of the to_s output.
87
- #
88
- # The output should be safe to use as a file name to store work item
89
- # data. Typical use is when extra file items are created by a task and need to be stored on disk. The default
90
- # implementation URL-encodes (%xx) all characters except alphanumeric, '.' and '-'.
91
- #
92
- # @return [String] file name
93
- def to_filename
94
- self.to_s.gsub(/[^\w.-]/) { |s| '%%%02x' % s.ord }
95
- end
96
-
97
- # Iterates over the work item clients and invokes code on each of them.
98
- def each(&block)
99
- self.items.each(&block)
100
- end
101
-
102
- def size
103
- self.items.size
104
- end
105
-
106
- alias_method :count, :size
107
-
108
- # Add a child work item
109
- #
110
- # @param [WorkItem] item to be added to the child list :items
111
- def add_item(item)
112
- return self unless item and item.is_a?(Libis::Workflow::Base::WorkItem)
113
- self.items << item
114
- item.parent = self
115
- self.save!
116
- item.save!
117
- self
118
- end
119
-
120
- alias_method :<<, :add_item
121
-
122
- # Get list of items.
123
- #
124
- # This method should return a list of items that can be accessed during long processing times.
125
- def get_items
126
- self.items
127
- end
128
-
129
- # Get list of items.
130
- #
131
- # This method should return a list of items that is safe to iterate over while it is being altered.
132
- def get_item_list
133
- self.items.dup
134
- end
135
-
136
- # Return item's parent
137
- # @return [Libis::Workflow::Base::WorkItem]
138
- def get_parent
139
- self.parent
140
- end
141
-
142
- # go up the hierarchy and return the topmost work item
143
- #
144
- # @return [Libis::Workflow::Base::WorkItem]
145
- def get_root
146
- self.get_parent && self.get_parent.is_a?(Libis::Workflow::Base::WorkItem) && self.get_parent.get_root || self
147
- end
148
-
149
- # Get the top
150
- #
151
- # @return [Libis::Workflow::Base::Run]
152
- def get_run
153
- return self if self.is_a?(Libis::Workflow::Base::Run)
154
- self.get_parent && self.get_parent.get_run || nil
155
- end
156
-
157
- # Dummy method. It is a placeholder for DB backed implementations. Wherever appropriate WorkItem#save will be
158
- # called to save the current item's state. If state needs to persisted, you should override this method or make
159
- # sure your persistence layer implements it in your class.
160
- def save
161
- end
162
-
163
- # Dummy method. It is a placeholder for DB backed implementations. Wherever appropriate WorkItem#save will be
164
- # called to save the current item's state. If state needs to persisted, you should override this method or make
165
- # sure your persistence layer implements it in your class.
166
- def save!
167
- end
168
-
169
- end
170
-
171
- end
172
- end
173
- end
1
+ require 'backports/rails/hash'
2
+ require 'libis/tools/extend/hash'
3
+
4
+ require 'libis/workflow/config'
5
+ require 'libis/workflow/status'
6
+ require_relative 'logging'
7
+
8
+ module Libis
9
+ module Workflow
10
+ module Base
11
+
12
+ # Base module for all work items.
13
+ #
14
+ # This module lacks the implementation for the data attributes. It functions as an interface that describes the
15
+ # common functionality regardless of the storage implementation. These attributes require some implementation:
16
+ #
17
+ # - parent: [Object|nil] a link to a parent work item. Work items can be organized in any hierarchy you think is
18
+ # relevant for your workflow (e.g. directory[/directory...]/file/line or library/section/book/page). Of course
19
+ # hierarchies are not mandatory.
20
+ # - items: [Array] a list of child work items. see above.
21
+ # - options: [Hash] a set of options for the task chain on how to deal with this work item. This attribute can be
22
+ # used to fine-tune the behaviour of tasks for a particular work item.
23
+ # - properties: [Hash] a set of properties, typically collected during the workflow processing and used to store
24
+ # final or intermediate resulst of tasks. The ::Lias::Ingester::FileItem module uses this attribute to store the
25
+ # properties (e.g. size, checksum, ...) of the file it represents.
26
+ # - status_log: [Array] a list of all status changes the work item went through.
27
+ # - summary: [Hash] collected statistics about the ingest for the work item and its children. This structure will
28
+ # be filled in by the included task ::Lias::Ingester::Tasks::Analyzer wich is appended to the workflow by default.
29
+ #
30
+ # The module is created so that it is possible to implement an ActiveRecord/Datamapper/... implementation easily.
31
+ # A simple in-memory implementation would require:
32
+ #
33
+ # attr_accessor :parent
34
+ # attr_accessor :items
35
+ # attr_accessor :options, :properties
36
+ # attr_accessor :status_log
37
+ # attr_accessor :summary
38
+ #
39
+ # def initialize
40
+ # self.parent = nil
41
+ # self.items = []
42
+ # self.options = {}
43
+ # self.properties = {}
44
+ # self.status_log = []
45
+ # self.summary = {}
46
+ # end
47
+ #
48
+ # protected
49
+ #
50
+ # ## Method below should be adapted to match the implementation of the status array
51
+ #
52
+ # def add_status_log(info)
53
+ # self.status_log << info
54
+ # end
55
+ #
56
+ #
57
+ module WorkItem
58
+ include Enumerable
59
+ include Libis::Workflow::Status
60
+ include Libis::Workflow::Base::Logging
61
+
62
+ # String representation of the identity of the work item.
63
+ #
64
+ # You may want to overwrite this method as it tries the :name property or whatever #inspect returns if that
65
+ # failes. Typically this should return the key value, file name or id number. If that's what your :name property
66
+ # contains, you're fine.
67
+ #
68
+ # @return [String] string identification for this work item.
69
+ def name
70
+ # noinspection RubyResolve
71
+ self.properties['name'] || self.inspect
72
+ end
73
+
74
+ def to_s;
75
+ self.name;
76
+ end
77
+
78
+ def names
79
+ (self.parent.names rescue Array.new).push(name).compact
80
+ end
81
+
82
+ def namepath;
83
+ self.names.join('/');
84
+ end
85
+
86
+ # File name safe version of the to_s output.
87
+ #
88
+ # The output should be safe to use as a file name to store work item
89
+ # data. Typical use is when extra file items are created by a task and need to be stored on disk. The default
90
+ # implementation URL-encodes (%xx) all characters except alphanumeric, '.' and '-'.
91
+ #
92
+ # @return [String] file name
93
+ def to_filename
94
+ self.to_s.gsub(/[^\w.-]/) { |s| '%%%02x' % s.ord }
95
+ end
96
+
97
+ # Iterates over the work item clients and invokes code on each of them.
98
+ def each(&block)
99
+ self.items.each(&block)
100
+ end
101
+
102
+ def size
103
+ self.items.size
104
+ end
105
+
106
+ alias_method :count, :size
107
+
108
+ # Add a child work item
109
+ #
110
+ # @param [WorkItem] item to be added to the child list :items
111
+ def add_item(item)
112
+ return self unless item and item.is_a?(Libis::Workflow::Base::WorkItem)
113
+ self.items << item
114
+ item.parent = self
115
+ self.save!
116
+ item.save!
117
+ self
118
+ end
119
+
120
+ alias_method :<<, :add_item
121
+
122
+ # Get list of items.
123
+ #
124
+ # This method should return a list of items that can be accessed during long processing times.
125
+ def get_items
126
+ self.items
127
+ end
128
+
129
+ # Get list of items.
130
+ #
131
+ # This method should return a list of items that is safe to iterate over while it is being altered.
132
+ def get_item_list
133
+ self.items.dup
134
+ end
135
+
136
+ # Return item's parent
137
+ # @return [Libis::Workflow::Base::WorkItem]
138
+ def get_parent
139
+ self.parent
140
+ end
141
+
142
+ # go up the hierarchy and return the topmost work item
143
+ #
144
+ # @return [Libis::Workflow::Base::WorkItem]
145
+ def get_root
146
+ self.get_parent && self.get_parent.is_a?(Libis::Workflow::Base::WorkItem) && self.get_parent.get_root || self
147
+ end
148
+
149
+ # Get the top
150
+ #
151
+ # @return [Libis::Workflow::Base::Run]
152
+ def get_run
153
+ return self if self.is_a?(Libis::Workflow::Base::Run)
154
+ self.get_parent && self.get_parent.get_run || nil
155
+ end
156
+
157
+ # Dummy method. It is a placeholder for DB backed implementations. Wherever appropriate WorkItem#save will be
158
+ # called to save the current item's state. If state needs to persisted, you should override this method or make
159
+ # sure your persistence layer implements it in your class.
160
+ def save
161
+ end
162
+
163
+ # Dummy method. It is a placeholder for DB backed implementations. Wherever appropriate WorkItem#save will be
164
+ # called to save the current item's state. If state needs to persisted, you should override this method or make
165
+ # sure your persistence layer implements it in your class.
166
+ def save!
167
+ end
168
+
169
+ end
170
+
171
+ end
172
+ end
173
+ end