flow_core 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|