flow_core 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +255 -0
- data/Rakefile +29 -0
- data/app/models/flow_core/application_record.rb +7 -0
- data/app/models/flow_core/arc.rb +43 -0
- data/app/models/flow_core/arc_guard.rb +18 -0
- data/app/models/flow_core/end_place.rb +17 -0
- data/app/models/flow_core/instance.rb +115 -0
- data/app/models/flow_core/place.rb +63 -0
- data/app/models/flow_core/start_place.rb +17 -0
- data/app/models/flow_core/task.rb +197 -0
- data/app/models/flow_core/token.rb +105 -0
- data/app/models/flow_core/transition.rb +143 -0
- data/app/models/flow_core/transition_callback.rb +24 -0
- data/app/models/flow_core/transition_trigger.rb +24 -0
- data/app/models/flow_core/workflow.rb +131 -0
- data/config/routes.rb +4 -0
- data/db/migrate/20200130200532_create_flow_core_tables.rb +151 -0
- data/lib/flow_core.rb +25 -0
- data/lib/flow_core/arc_guardable.rb +15 -0
- data/lib/flow_core/definition.rb +17 -0
- data/lib/flow_core/definition/callback.rb +33 -0
- data/lib/flow_core/definition/guard.rb +33 -0
- data/lib/flow_core/definition/net.rb +111 -0
- data/lib/flow_core/definition/place.rb +33 -0
- data/lib/flow_core/definition/transition.rb +107 -0
- data/lib/flow_core/definition/trigger.rb +33 -0
- data/lib/flow_core/engine.rb +6 -0
- data/lib/flow_core/errors.rb +14 -0
- data/lib/flow_core/locale/en.yml +26 -0
- data/lib/flow_core/task_executable.rb +34 -0
- data/lib/flow_core/transition_callbackable.rb +33 -0
- data/lib/flow_core/transition_triggerable.rb +27 -0
- data/lib/flow_core/version.rb +5 -0
- data/lib/flow_core/violations.rb +253 -0
- data/lib/tasks/flow_core_tasks.rake +6 -0
- metadata +123 -0
@@ -0,0 +1,111 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FlowCore::Definition
|
4
|
+
class Net
|
5
|
+
attr_reader :attributes, :places, :transitions, :start_tag, :end_tag
|
6
|
+
|
7
|
+
def initialize(attributes = {})
|
8
|
+
@attributes = attributes.with_indifferent_access.except(FlowCore::Workflow::FORBIDDEN_ATTRIBUTES)
|
9
|
+
@places = []
|
10
|
+
@transitions = []
|
11
|
+
@start_tag = nil
|
12
|
+
@end_tag = nil
|
13
|
+
|
14
|
+
yield(self)
|
15
|
+
end
|
16
|
+
|
17
|
+
def add_place(tag_or_place, attributes = {})
|
18
|
+
entity =
|
19
|
+
if tag_or_place.is_a? FlowCore::Definition::Place
|
20
|
+
tag_or_place
|
21
|
+
else
|
22
|
+
attributes[:name] ||= tag_or_place.to_s
|
23
|
+
FlowCore::Definition::Place.new(tag_or_place, attributes)
|
24
|
+
end
|
25
|
+
|
26
|
+
@places << entity unless @places.include?(entity)
|
27
|
+
entity
|
28
|
+
end
|
29
|
+
|
30
|
+
def start_place(tag, attributes = {})
|
31
|
+
raise "`start_place` can only call once" if @start_tag
|
32
|
+
|
33
|
+
place = FlowCore::Definition::Place.new(tag, attributes.merge(type: FlowCore::StartPlace.to_s))
|
34
|
+
@places << place
|
35
|
+
@start_tag = place.tag
|
36
|
+
place
|
37
|
+
end
|
38
|
+
|
39
|
+
def end_place(tag, attributes = {})
|
40
|
+
raise "`end_place` can only call once" if @end_tag
|
41
|
+
|
42
|
+
place = FlowCore::Definition::Place.new(tag, attributes.merge(type: FlowCore::EndPlace.to_s))
|
43
|
+
@places << place
|
44
|
+
@end_tag = place.tag
|
45
|
+
place
|
46
|
+
end
|
47
|
+
|
48
|
+
def transition(tag, options = {}, &block)
|
49
|
+
raise TypeError unless tag.is_a? Symbol
|
50
|
+
raise ArgumentError if @transitions.include? tag
|
51
|
+
|
52
|
+
@transitions << FlowCore::Definition::Transition.new(self, tag, options, &block)
|
53
|
+
end
|
54
|
+
|
55
|
+
def compile
|
56
|
+
{
|
57
|
+
attributes: @attributes,
|
58
|
+
start_tag: @start_tag,
|
59
|
+
end_tag: @end_tag,
|
60
|
+
places: @places.map(&:compile),
|
61
|
+
transitions: @transitions.map(&:compile)
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
def deploy!
|
66
|
+
# TODO: Simple validation
|
67
|
+
|
68
|
+
workflow = nil
|
69
|
+
FlowCore::ApplicationRecord.transaction do
|
70
|
+
workflow = FlowCore::Workflow.create! attributes
|
71
|
+
|
72
|
+
# Places
|
73
|
+
place_records = {}
|
74
|
+
places.each do |pd|
|
75
|
+
place_records[pd.tag] =
|
76
|
+
workflow.places.create! pd.attributes
|
77
|
+
end
|
78
|
+
|
79
|
+
# Transitions
|
80
|
+
transition_records = {}
|
81
|
+
transitions.each do |td|
|
82
|
+
transition_records[td.tag] =
|
83
|
+
workflow.transitions.create! td.attributes
|
84
|
+
|
85
|
+
if td.trigger
|
86
|
+
transition_records[td.tag].create_trigger! td.trigger.compile
|
87
|
+
end
|
88
|
+
|
89
|
+
td.callbacks.each do |cb|
|
90
|
+
transition_records[td.tag].callbacks.create! cb.compile
|
91
|
+
end
|
92
|
+
|
93
|
+
td.input_tags.each do |place_tag|
|
94
|
+
workflow.arcs.in.create! transition: transition_records[td.tag],
|
95
|
+
place: place_records[place_tag]
|
96
|
+
end
|
97
|
+
|
98
|
+
td.output_tags.each do |output|
|
99
|
+
arc = workflow.arcs.out.create! transition: transition_records[td.tag],
|
100
|
+
place: place_records[output[:tag]]
|
101
|
+
if output[:guard]
|
102
|
+
arc.guards.create! output[:guard].compile
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
workflow&.verify!
|
108
|
+
workflow
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FlowCore::Definition
|
4
|
+
class Place
|
5
|
+
attr_reader :tag, :attributes
|
6
|
+
|
7
|
+
def initialize(tag, attributes = {})
|
8
|
+
raise TypeError unless tag.is_a? Symbol
|
9
|
+
|
10
|
+
@tag = tag
|
11
|
+
@attributes = attributes.with_indifferent_access.except(FlowCore::Place::FORBIDDEN_ATTRIBUTES)
|
12
|
+
@attributes[:name] ||= tag.to_s
|
13
|
+
@attributes[:tag] ||= tag.to_s
|
14
|
+
end
|
15
|
+
|
16
|
+
def compile
|
17
|
+
{
|
18
|
+
tag: @tag,
|
19
|
+
attributes: @attributes
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
def eql?(other)
|
24
|
+
if other.is_a? FlowCore::Definition::Place
|
25
|
+
@tag == other.tag
|
26
|
+
else
|
27
|
+
false
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
alias == eql?
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FlowCore::Definition
|
4
|
+
class Transition
|
5
|
+
attr_reader :net, :tag, :attributes, :trigger, :callbacks, :input_tags, :output_tags
|
6
|
+
private :net
|
7
|
+
|
8
|
+
def initialize(net, tag, attributes = {}, &block)
|
9
|
+
raise TypeError unless net.is_a? FlowCore::Definition::Net
|
10
|
+
raise TypeError unless tag.is_a? Symbol
|
11
|
+
|
12
|
+
@net = net
|
13
|
+
@tag = tag
|
14
|
+
@input_tags = []
|
15
|
+
@output_tags = []
|
16
|
+
@callbacks = []
|
17
|
+
|
18
|
+
input = attributes.delete(:input)
|
19
|
+
output = attributes.delete(:output)
|
20
|
+
raise ArgumentError, "Require `input`" unless input
|
21
|
+
|
22
|
+
self.input input
|
23
|
+
self.output output if output
|
24
|
+
|
25
|
+
trigger = attributes.delete :with_trigger
|
26
|
+
@trigger =
|
27
|
+
if trigger
|
28
|
+
FlowCore::Definition::Trigger.new trigger
|
29
|
+
end
|
30
|
+
|
31
|
+
callbacks = []
|
32
|
+
callbacks.concat Array.wrap(attributes.delete(:with_callbacks))
|
33
|
+
callbacks.concat Array.wrap(attributes.delete(:with_callback))
|
34
|
+
@callbacks = callbacks.map { |cb| FlowCore::Definition::Callback.new cb }
|
35
|
+
|
36
|
+
@attributes = attributes.with_indifferent_access.except(FlowCore::Transition::FORBIDDEN_ATTRIBUTES)
|
37
|
+
@attributes[:name] ||= tag.to_s
|
38
|
+
@attributes[:tag] ||= tag.to_s
|
39
|
+
|
40
|
+
block&.call(self)
|
41
|
+
end
|
42
|
+
|
43
|
+
def with_trigger(type, attributes = {})
|
44
|
+
@trigger = FlowCore::Definition::Trigger.new attributes.merge(type: type)
|
45
|
+
end
|
46
|
+
|
47
|
+
def with_callback(type, attributes = {})
|
48
|
+
@callbacks << FlowCore::Definition::Callback.new(attributes.merge(type: type))
|
49
|
+
end
|
50
|
+
|
51
|
+
def with_callbacks(*callbacks)
|
52
|
+
callbacks.each do |cb|
|
53
|
+
with_callback(*cb)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def input(tag)
|
58
|
+
places = Array.wrap(tag)
|
59
|
+
places.each do |place|
|
60
|
+
case place
|
61
|
+
when Symbol
|
62
|
+
net.add_place(place)
|
63
|
+
tag = place
|
64
|
+
when Array # Expect `[:p1, {name: "Place 1"}]`
|
65
|
+
net.add_place(*place)
|
66
|
+
tag = place.first
|
67
|
+
else
|
68
|
+
raise TypeError, "Unknown pattern - #{place}"
|
69
|
+
end
|
70
|
+
|
71
|
+
@input_tags << tag
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def output(tag, attributes = {})
|
76
|
+
guard = attributes.delete :with_guard
|
77
|
+
guard = FlowCore::Definition::Guard.new(guard) if guard&.is_a?(Hash)
|
78
|
+
|
79
|
+
places = Array.wrap(tag)
|
80
|
+
places.each do |place|
|
81
|
+
case place
|
82
|
+
when Symbol
|
83
|
+
net.add_place(place)
|
84
|
+
tag = place
|
85
|
+
when Array # Expect `[:p1, {name: "Place 1"}]`
|
86
|
+
net.add_place(*place)
|
87
|
+
tag = place.first
|
88
|
+
else
|
89
|
+
raise TypeError, "Unknown pattern - #{place}"
|
90
|
+
end
|
91
|
+
|
92
|
+
@output_tags << { tag: tag, guard: guard }
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def compile
|
97
|
+
{
|
98
|
+
tag: @tag,
|
99
|
+
attributes: @attributes,
|
100
|
+
trigger: @trigger&.compile,
|
101
|
+
callbacks: @callbacks.map(&:compile),
|
102
|
+
input_tags: @input_tags,
|
103
|
+
output_tags: @output_tags.map { |output| { tag: output[:tag], guard: output[:guard]&.compile } }
|
104
|
+
}
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FlowCore::Definition
|
4
|
+
class Trigger
|
5
|
+
def initialize(attributes)
|
6
|
+
constant_or_klass = attributes.is_a?(Hash) ? attributes.delete(:type) : attributes
|
7
|
+
@klass =
|
8
|
+
if constant_or_klass.is_a? String
|
9
|
+
constant_or_klass.safe_constantize
|
10
|
+
else
|
11
|
+
constant_or_klass
|
12
|
+
end
|
13
|
+
unless @klass && @klass < FlowCore::TransitionTrigger
|
14
|
+
raise TypeError, "First argument expect `FlowCore::TransitionTrigger` subclass or its constant name - #{constant_or_klass}"
|
15
|
+
end
|
16
|
+
|
17
|
+
@configuration = attributes.is_a?(Hash) ? attributes : nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def compile
|
21
|
+
if @configuration&.any?
|
22
|
+
{
|
23
|
+
type: @klass.to_s,
|
24
|
+
configuration: @configuration
|
25
|
+
}
|
26
|
+
else
|
27
|
+
{
|
28
|
+
type: @klass.to_s
|
29
|
+
}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FlowCore
|
4
|
+
# Generic base class for all Workflow Core exceptions.
|
5
|
+
class Error < StandardError; end
|
6
|
+
|
7
|
+
class UnverifiedWorkflow < Error; end
|
8
|
+
|
9
|
+
class InvalidTransition < Error; end
|
10
|
+
|
11
|
+
class NoNewTokenCreated < Error; end
|
12
|
+
|
13
|
+
class ForbiddenOperation < Error; end
|
14
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
en:
|
2
|
+
activerecord:
|
3
|
+
models:
|
4
|
+
flow_core/workflow: Workflow
|
5
|
+
flow_core/place: Place
|
6
|
+
flow_core/start_place: Start Place
|
7
|
+
flow_core/end_place: End Place
|
8
|
+
flow_core/transition: Workflow
|
9
|
+
flow_core/transition_trigger: Transition Trigger
|
10
|
+
flow_core/transition_callback: Trasition Callback
|
11
|
+
flow_core/arc: Arc
|
12
|
+
flow_core/arc_guard: Arc Guard
|
13
|
+
flow_core/instance: Instance
|
14
|
+
flow_core/token: Token
|
15
|
+
flow_core/task: Task
|
16
|
+
flow_core:
|
17
|
+
verify_status:
|
18
|
+
unverify: Unverify
|
19
|
+
verified: Verified
|
20
|
+
invalid: Invalid
|
21
|
+
violations:
|
22
|
+
format: "%{model} %{name} %{message}"
|
23
|
+
unreachable: "Unreachable from start place"
|
24
|
+
impassable: "Impassable to end place"
|
25
|
+
trigger_unconfigure: "Trigger not configured yet"
|
26
|
+
callback_unconfigure: "Callback not configured yet"
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FlowCore
|
4
|
+
module TaskExecutable
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
has_one :task, as: :executable, required: true, class_name: "FlowCore::Task"
|
9
|
+
has_one :instance, through: :task, class_name: "FlowCore::Instance"
|
10
|
+
has_one :transition, -> { readonly }, through: :task, class_name: "FlowCore::Transition"
|
11
|
+
|
12
|
+
after_save :notify_workflow_task_finished!, if: :implicit_notify_workflow_task_finished
|
13
|
+
end
|
14
|
+
|
15
|
+
# For automatic task
|
16
|
+
def run!
|
17
|
+
raise NotImplementedError
|
18
|
+
end
|
19
|
+
|
20
|
+
def finished?
|
21
|
+
raise NotImplementedError
|
22
|
+
end
|
23
|
+
|
24
|
+
def implicit_notify_workflow_task_finished
|
25
|
+
true
|
26
|
+
end
|
27
|
+
|
28
|
+
protected
|
29
|
+
|
30
|
+
def notify_workflow_task_finished!
|
31
|
+
task.finish! if finished?
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FlowCore
|
4
|
+
module TransitionCallbackable
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
private :_call
|
9
|
+
end
|
10
|
+
|
11
|
+
# Should in
|
12
|
+
# :created, :enabled, :finished, :terminated,
|
13
|
+
# :errored, :rescued :suspended, :resumed
|
14
|
+
def on
|
15
|
+
[]
|
16
|
+
end
|
17
|
+
|
18
|
+
def callable?(_task)
|
19
|
+
true
|
20
|
+
end
|
21
|
+
|
22
|
+
def _call(_task)
|
23
|
+
raise NotImplementedError
|
24
|
+
end
|
25
|
+
|
26
|
+
def call(task)
|
27
|
+
return unless on.include? task.stage.to_sym
|
28
|
+
return unless callable? task
|
29
|
+
|
30
|
+
_call task
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FlowCore
|
4
|
+
module TransitionTriggerable
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
def on_verify(_transition, _violations); end
|
8
|
+
|
9
|
+
def on_task_created(_task); end
|
10
|
+
|
11
|
+
def on_task_enabled(_task); end
|
12
|
+
|
13
|
+
def on_task_finished(_task); end
|
14
|
+
|
15
|
+
def on_task_terminated(_task); end
|
16
|
+
|
17
|
+
def on_task_suspended(_task); end
|
18
|
+
|
19
|
+
def on_task_resumed(_task); end
|
20
|
+
|
21
|
+
def on_task_errored(_task, error)
|
22
|
+
raise error
|
23
|
+
end
|
24
|
+
|
25
|
+
def on_task_rescued(_task); end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,253 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/array/conversions"
|
4
|
+
require "active_support/core_ext/string/inflections"
|
5
|
+
require "active_support/core_ext/object/deep_dup"
|
6
|
+
require "active_support/core_ext/string/filters"
|
7
|
+
|
8
|
+
module FlowCore
|
9
|
+
class Violations
|
10
|
+
include Enumerable
|
11
|
+
|
12
|
+
MESSAGE_OPTIONS = %i[message].freeze
|
13
|
+
|
14
|
+
attr_reader :messages, :details, :records
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
@messages = apply_default_array({})
|
18
|
+
@details = apply_default_array({})
|
19
|
+
@records = apply_default_array({})
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize_dup(other) # :nodoc:
|
23
|
+
@messages = other.messages.dup
|
24
|
+
@details = other.details.deep_dup
|
25
|
+
@records = other.records.deep_dup
|
26
|
+
super
|
27
|
+
end
|
28
|
+
|
29
|
+
def copy!(other) # :nodoc:
|
30
|
+
@messages = other.messages.dup
|
31
|
+
@details = other.details.dup
|
32
|
+
@records = other.records.dup
|
33
|
+
end
|
34
|
+
|
35
|
+
def merge!(other)
|
36
|
+
@messages.merge!(other.messages) { |_, ary1, ary2| ary1 + ary2 }
|
37
|
+
@details.merge!(other.details) { |_, ary1, ary2| ary1 + ary2 }
|
38
|
+
@records.merge!(other.records) { |_, ary1, ary2| ary1 + ary2 }
|
39
|
+
end
|
40
|
+
|
41
|
+
def slice!(*keys)
|
42
|
+
keys = keys.map(&:to_sym)
|
43
|
+
@details.slice!(*keys)
|
44
|
+
@messages.slice!(*keys)
|
45
|
+
@records.slice!(*keys)
|
46
|
+
end
|
47
|
+
|
48
|
+
def clear
|
49
|
+
messages.clear
|
50
|
+
details.clear
|
51
|
+
records.clear
|
52
|
+
end
|
53
|
+
|
54
|
+
def include?(record)
|
55
|
+
record = "#{record.class}/#{record.id}"
|
56
|
+
messages.key?(record) && messages[record].present?
|
57
|
+
end
|
58
|
+
alias has_key? include?
|
59
|
+
alias key? include?
|
60
|
+
|
61
|
+
def delete(record)
|
62
|
+
record = "#{record.class}/#{record.id}"
|
63
|
+
details.delete(record)
|
64
|
+
messages.delete(record)
|
65
|
+
records.delete(record)
|
66
|
+
end
|
67
|
+
|
68
|
+
def [](record)
|
69
|
+
record = "#{record.class}/#{record.id}"
|
70
|
+
messages[record]
|
71
|
+
end
|
72
|
+
|
73
|
+
def each
|
74
|
+
messages.each_key do |record_key|
|
75
|
+
model = records[record_key][:model]
|
76
|
+
id = records[record_key][:id]
|
77
|
+
name = records[record_key][:name]
|
78
|
+
messages[record_key].each { |error| yield record_key, model, id, name, error }
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def size
|
83
|
+
values.flatten.size
|
84
|
+
end
|
85
|
+
alias count size
|
86
|
+
|
87
|
+
def values
|
88
|
+
messages.reject do |_key, value|
|
89
|
+
value.empty?
|
90
|
+
end.values
|
91
|
+
end
|
92
|
+
|
93
|
+
def keys
|
94
|
+
messages.reject do |_key, value|
|
95
|
+
value.empty?
|
96
|
+
end.keys
|
97
|
+
end
|
98
|
+
|
99
|
+
def empty?
|
100
|
+
size.zero?
|
101
|
+
end
|
102
|
+
alias blank? empty?
|
103
|
+
|
104
|
+
def to_xml(options = {})
|
105
|
+
to_a.to_xml({ root: "errors", skip_types: true }.merge!(options))
|
106
|
+
end
|
107
|
+
|
108
|
+
def as_json(options = nil)
|
109
|
+
to_hash(options && options[:full_messages])
|
110
|
+
end
|
111
|
+
|
112
|
+
def to_hash(full_messages = false)
|
113
|
+
if full_messages
|
114
|
+
messages.each_with_object({}) do |(record_key, array), messages|
|
115
|
+
messages[record_key] = array.map { |message| full_message(record_key, message) }
|
116
|
+
end
|
117
|
+
else
|
118
|
+
without_default_proc(messages)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def add(record, message = :invalid, options = {})
|
123
|
+
message = message.call if message.respond_to?(:call)
|
124
|
+
detail = normalize_detail(message, options)
|
125
|
+
message = normalize_message(record, message, options)
|
126
|
+
n_record = normalize_record(record)
|
127
|
+
|
128
|
+
record_key = "#{record.class}/#{record.id}"
|
129
|
+
details[record_key] << detail
|
130
|
+
messages[record_key] << message
|
131
|
+
records[record_key] = n_record
|
132
|
+
|
133
|
+
if exception = options[:strict]
|
134
|
+
exception = FlowCore::StrictViolationFailed if exception == true
|
135
|
+
raise exception, full_message(record_key, message)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def added?(record, message = :invalid, options = {})
|
140
|
+
message = message.call if message.respond_to?(:call)
|
141
|
+
|
142
|
+
record_key = "#{record.class}/#{record.id}"
|
143
|
+
if message.is_a? Symbol
|
144
|
+
details[record_key].include? normalize_detail(message, options)
|
145
|
+
else
|
146
|
+
self[record_key].include? message
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def of_kind?(record, message = :invalid)
|
151
|
+
message = message.call if message.respond_to?(:call)
|
152
|
+
|
153
|
+
record_key = "#{record.class}/#{record.id}"
|
154
|
+
if message.is_a? Symbol
|
155
|
+
details[record_key].map { |e| e[:error] }.include? message
|
156
|
+
else
|
157
|
+
self[record_key].include? message
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def full_messages
|
162
|
+
map do |record_key, _, _, _, message|
|
163
|
+
full_message(record_key, message)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
alias to_a full_messages
|
167
|
+
|
168
|
+
def full_messages_for(record)
|
169
|
+
record_key = "#{record.class}/#{record.id}"
|
170
|
+
messages[record].map { |message| full_message(record_key, message) }
|
171
|
+
end
|
172
|
+
|
173
|
+
def full_message(record_key, message)
|
174
|
+
model = records[record_key][:model]
|
175
|
+
model_name = records[record_key][:model].model_name.human
|
176
|
+
id = records[record_key][:id]
|
177
|
+
name = records[record_key][:name]
|
178
|
+
|
179
|
+
defaults = [:"flow_core.violations.format"]
|
180
|
+
defaults << "%{name} %{message}"
|
181
|
+
|
182
|
+
I18n.t(defaults.shift,
|
183
|
+
default: defaults, model: model, model_name: model_name, id: id, name: name, message: message)
|
184
|
+
end
|
185
|
+
|
186
|
+
def generate_message(record, type = :invalid, options = {})
|
187
|
+
type = options.delete(:message) if options[:message].is_a?(Symbol)
|
188
|
+
|
189
|
+
options = {
|
190
|
+
model: record.class,
|
191
|
+
model_name: record.model_name.human,
|
192
|
+
id: record.id,
|
193
|
+
name: record.name
|
194
|
+
}.merge!(options)
|
195
|
+
|
196
|
+
options[:default] = options.delete(:message) if options[:message]
|
197
|
+
|
198
|
+
I18n.translate("flow_core.violations.#{type}", options)
|
199
|
+
end
|
200
|
+
|
201
|
+
def marshal_dump # :nodoc:
|
202
|
+
[without_default_proc(@messages), without_default_proc(@details), without_default_proc(@records)]
|
203
|
+
end
|
204
|
+
|
205
|
+
def marshal_load(array) # :nodoc:
|
206
|
+
@messages, @details, @records = array
|
207
|
+
apply_default_array(@messages)
|
208
|
+
apply_default_array(@details)
|
209
|
+
apply_default_array(@records)
|
210
|
+
end
|
211
|
+
|
212
|
+
def init_with(coder) # :nodoc:
|
213
|
+
coder.map.each { |k, v| instance_variable_set(:"@#{k}", v) }
|
214
|
+
@details ||= {}
|
215
|
+
apply_default_array(@messages)
|
216
|
+
apply_default_array(@details)
|
217
|
+
apply_default_array(@records)
|
218
|
+
end
|
219
|
+
|
220
|
+
private
|
221
|
+
|
222
|
+
def normalize_message(record, message, options)
|
223
|
+
case message
|
224
|
+
when Symbol
|
225
|
+
generate_message(record, message, options)
|
226
|
+
else
|
227
|
+
message
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
def normalize_detail(message, options)
|
232
|
+
{ error: message }.merge(options.except(*MESSAGE_OPTIONS))
|
233
|
+
end
|
234
|
+
|
235
|
+
def normalize_record(record)
|
236
|
+
{ model: record.class, id: record.id, name: record.name }
|
237
|
+
end
|
238
|
+
|
239
|
+
def without_default_proc(hash)
|
240
|
+
hash.dup.tap do |new_h|
|
241
|
+
new_h.default_proc = nil
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
def apply_default_array(hash)
|
246
|
+
hash.default_proc = proc { |h, key| h[key] = [] }
|
247
|
+
hash
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
class StrictViolationFailed < StandardError
|
252
|
+
end
|
253
|
+
end
|