cmdx 0.1.0
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/.DS_Store +0 -0
- data/.rspec +4 -0
- data/.rubocop.yml +64 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +76 -0
- data/Rakefile +12 -0
- data/docs/basics/call.md +31 -0
- data/docs/basics/context.md +67 -0
- data/docs/basics/run.md +34 -0
- data/docs/basics/setup.md +32 -0
- data/docs/batch.md +53 -0
- data/docs/configuration.md +57 -0
- data/docs/example.md +82 -0
- data/docs/getting_started.md +47 -0
- data/docs/hooks.md +59 -0
- data/docs/interruptions/exceptions.md +29 -0
- data/docs/interruptions/faults.md +89 -0
- data/docs/interruptions/halt.md +80 -0
- data/docs/logging.md +102 -0
- data/docs/outcomes/result.md +17 -0
- data/docs/outcomes/states.md +31 -0
- data/docs/outcomes/statuses.md +33 -0
- data/docs/parameters/coercions.md +52 -0
- data/docs/parameters/defaults.md +47 -0
- data/docs/parameters/definitions.md +123 -0
- data/docs/parameters/namespacing.md +57 -0
- data/docs/parameters/validations.md +312 -0
- data/docs/tips_and_tricks.md +79 -0
- data/lib/cmdx/.DS_Store +0 -0
- data/lib/cmdx/batch.rb +43 -0
- data/lib/cmdx/coercions/array.rb +15 -0
- data/lib/cmdx/coercions/big_decimal.rb +23 -0
- data/lib/cmdx/coercions/boolean.rb +27 -0
- data/lib/cmdx/coercions/complex.rb +21 -0
- data/lib/cmdx/coercions/date.rb +26 -0
- data/lib/cmdx/coercions/date_time.rb +26 -0
- data/lib/cmdx/coercions/float.rb +21 -0
- data/lib/cmdx/coercions/hash.rb +31 -0
- data/lib/cmdx/coercions/integer.rb +21 -0
- data/lib/cmdx/coercions/rational.rb +21 -0
- data/lib/cmdx/coercions/string.rb +15 -0
- data/lib/cmdx/coercions/time.rb +26 -0
- data/lib/cmdx/coercions/virtual.rb +15 -0
- data/lib/cmdx/configuration.rb +25 -0
- data/lib/cmdx/context.rb +15 -0
- data/lib/cmdx/core_ext/hash.rb +36 -0
- data/lib/cmdx/core_ext/module.rb +48 -0
- data/lib/cmdx/core_ext/object.rb +55 -0
- data/lib/cmdx/error.rb +23 -0
- data/lib/cmdx/errors.rb +92 -0
- data/lib/cmdx/fault.rb +47 -0
- data/lib/cmdx/faults.rb +11 -0
- data/lib/cmdx/immutator.rb +21 -0
- data/lib/cmdx/lazy_struct.rb +79 -0
- data/lib/cmdx/log_formatters/json.rb +13 -0
- data/lib/cmdx/log_formatters/key_value.rb +13 -0
- data/lib/cmdx/log_formatters/line.rb +14 -0
- data/lib/cmdx/log_formatters/logstash.rb +18 -0
- data/lib/cmdx/log_formatters/raw.rb +13 -0
- data/lib/cmdx/logger.rb +16 -0
- data/lib/cmdx/parameter.rb +101 -0
- data/lib/cmdx/parameter_inspector.rb +23 -0
- data/lib/cmdx/parameter_serializer.rb +20 -0
- data/lib/cmdx/parameter_validator.rb +19 -0
- data/lib/cmdx/parameter_value.rb +136 -0
- data/lib/cmdx/parameters.rb +34 -0
- data/lib/cmdx/parameters_inspector.rb +13 -0
- data/lib/cmdx/parameters_serializer.rb +13 -0
- data/lib/cmdx/railtie.rb +32 -0
- data/lib/cmdx/result.rb +170 -0
- data/lib/cmdx/result_inspector.rb +31 -0
- data/lib/cmdx/result_logger.rb +22 -0
- data/lib/cmdx/result_serializer.rb +38 -0
- data/lib/cmdx/run.rb +33 -0
- data/lib/cmdx/run_inspector.rb +21 -0
- data/lib/cmdx/run_serializer.rb +16 -0
- data/lib/cmdx/task.rb +151 -0
- data/lib/cmdx/task_hook.rb +18 -0
- data/lib/cmdx/utils/datetime_formatter.rb +17 -0
- data/lib/cmdx/utils/method_name.rb +24 -0
- data/lib/cmdx/utils/runtime.rb +19 -0
- data/lib/cmdx/validators/custom.rb +20 -0
- data/lib/cmdx/validators/exclusion.rb +51 -0
- data/lib/cmdx/validators/format.rb +27 -0
- data/lib/cmdx/validators/inclusion.rb +51 -0
- data/lib/cmdx/validators/length.rb +114 -0
- data/lib/cmdx/validators/numeric.rb +114 -0
- data/lib/cmdx/validators/presence.rb +27 -0
- data/lib/cmdx/version.rb +7 -0
- data/lib/cmdx.rb +80 -0
- data/lib/generators/cmdx/batch_generator.rb +30 -0
- data/lib/generators/cmdx/install_generator.rb +15 -0
- data/lib/generators/cmdx/task_generator.rb +30 -0
- data/lib/generators/cmdx/templates/batch.rb.tt +7 -0
- data/lib/generators/cmdx/templates/install.rb +23 -0
- data/lib/generators/cmdx/templates/task.rb.tt +9 -0
- data/lib/locales/en.yml +36 -0
- data/lib/locales/es.yml +36 -0
- metadata +288 -0
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CMDx
|
4
|
+
module LogFormatters
|
5
|
+
class Line
|
6
|
+
|
7
|
+
def call(severity, time, progname, message)
|
8
|
+
message = message.map { |k, v| "#{k}=#{v}" }.join(" ")
|
9
|
+
"#{severity[0]}, [#{Utils::DatetimeFormatter.call(time.utc)} ##{Process.pid}] #{severity} -- #{progname || 'CMDx'}: #{message}"
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CMDx
|
4
|
+
module LogFormatters
|
5
|
+
class Logstash
|
6
|
+
|
7
|
+
def call(_severity, time, _progname, message)
|
8
|
+
if message.is_a?(Hash)
|
9
|
+
message["@version"] ||= "1"
|
10
|
+
message["@timestamp"] ||= Utils::DatetimeFormatter.call(time.utc)
|
11
|
+
end
|
12
|
+
|
13
|
+
JSON.dump(message)
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/cmdx/logger.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CMDx
|
4
|
+
module Logger
|
5
|
+
|
6
|
+
module_function
|
7
|
+
|
8
|
+
def call(task)
|
9
|
+
logger = task.task_setting(:logger)
|
10
|
+
logger.formatter = task.task_setting(:log_formatter) if task.task_setting?(:log_formatter)
|
11
|
+
logger.level = task.task_setting(:log_level) if task.task_setting?(:log_level)
|
12
|
+
logger
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CMDx
|
4
|
+
class Parameter
|
5
|
+
|
6
|
+
__cmdx_attr_delegator :invalid?, :valid?, to: :errors
|
7
|
+
|
8
|
+
attr_accessor :task
|
9
|
+
attr_reader :klass, :parent, :name, :type, :options, :children, :errors
|
10
|
+
|
11
|
+
def initialize(name, **options, &)
|
12
|
+
@klass = options.delete(:klass) || raise(KeyError, "klass option required")
|
13
|
+
@parent = options.delete(:parent)
|
14
|
+
@type = options.delete(:type) || :virtual
|
15
|
+
@required = options.delete(:required) || false
|
16
|
+
|
17
|
+
@name = name
|
18
|
+
@options = options
|
19
|
+
@children = []
|
20
|
+
@errors = Errors.new
|
21
|
+
|
22
|
+
define_attribute(self)
|
23
|
+
instance_eval(&) if block_given?
|
24
|
+
end
|
25
|
+
|
26
|
+
class << self
|
27
|
+
|
28
|
+
def optional(*names, **options, &)
|
29
|
+
if names.none?
|
30
|
+
raise ArgumentError, "no parameters given"
|
31
|
+
elsif !names.one? && options.key?(:as)
|
32
|
+
raise ArgumentError, ":as option only supports one parameter per definition"
|
33
|
+
end
|
34
|
+
|
35
|
+
names.filter_map { |n| new(n, **options, &) }
|
36
|
+
end
|
37
|
+
|
38
|
+
def required(*names, **options, &)
|
39
|
+
optional(*names, **options.merge(required: true), &)
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
def optional(*names, **options, &)
|
45
|
+
parameters = Parameter.optional(*names, **options.merge(klass: @klass, parent: self), &)
|
46
|
+
children.concat(parameters)
|
47
|
+
end
|
48
|
+
|
49
|
+
def required(*names, **options, &)
|
50
|
+
parameters = Parameter.required(*names, **options.merge(klass: @klass, parent: self), &)
|
51
|
+
children.concat(parameters)
|
52
|
+
end
|
53
|
+
|
54
|
+
def required?
|
55
|
+
!!@required
|
56
|
+
end
|
57
|
+
|
58
|
+
def optional?
|
59
|
+
!required?
|
60
|
+
end
|
61
|
+
|
62
|
+
def method_name
|
63
|
+
@method_name ||= Utils::MethodName.call(name, method_source, options)
|
64
|
+
end
|
65
|
+
|
66
|
+
def method_source
|
67
|
+
@method_source ||= options[:source] || parent&.method_name || :context
|
68
|
+
end
|
69
|
+
|
70
|
+
def to_h
|
71
|
+
ParameterSerializer.call(self)
|
72
|
+
end
|
73
|
+
|
74
|
+
def to_s
|
75
|
+
ParameterInspector.call(to_h)
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def define_attribute(parameter)
|
81
|
+
klass.send(:define_method, parameter.method_name) do
|
82
|
+
@parameters_cache ||= {}
|
83
|
+
return @parameters_cache[parameter.method_name] if @parameters_cache.key?(parameter.method_name)
|
84
|
+
|
85
|
+
begin
|
86
|
+
parameter_value = ParameterValue.new(self, parameter).call
|
87
|
+
rescue CoercionError, ValidationError => e
|
88
|
+
parameter.errors.add(parameter.method_name, e.message)
|
89
|
+
errors.merge!(parameter.errors.to_hash)
|
90
|
+
ensure
|
91
|
+
@parameters_cache[parameter.method_name] = parameter_value
|
92
|
+
end
|
93
|
+
|
94
|
+
@parameters_cache[parameter.method_name]
|
95
|
+
end
|
96
|
+
|
97
|
+
klass.send(:private, parameter.method_name)
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CMDx
|
4
|
+
module ParameterInspector
|
5
|
+
|
6
|
+
ORDERED_KEYS = %i[
|
7
|
+
name type source required options children
|
8
|
+
].freeze
|
9
|
+
|
10
|
+
module_function
|
11
|
+
|
12
|
+
def call(parameter, depth = 1)
|
13
|
+
ORDERED_KEYS.filter_map do |key|
|
14
|
+
value = parameter[key]
|
15
|
+
next "#{key}=#{value}" unless key == :children
|
16
|
+
|
17
|
+
spaces = " " * (depth * 2)
|
18
|
+
value.map { |h| "\n#{spaces}↳ #{call(h, depth + 1)}" }.join
|
19
|
+
end.unshift("Parameter:").join(" ")
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CMDx
|
4
|
+
module ParameterSerializer
|
5
|
+
|
6
|
+
module_function
|
7
|
+
|
8
|
+
def call(parameter)
|
9
|
+
{
|
10
|
+
source: parameter.method_source,
|
11
|
+
name: parameter.method_name,
|
12
|
+
type: parameter.type,
|
13
|
+
required: parameter.required?,
|
14
|
+
options: parameter.options,
|
15
|
+
children: parameter.children.map(&:to_h)
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CMDx
|
4
|
+
module ParameterValidator
|
5
|
+
|
6
|
+
module_function
|
7
|
+
|
8
|
+
def call(task)
|
9
|
+
task.class.cmd_parameters.validate!(task)
|
10
|
+
return if task.errors.empty?
|
11
|
+
|
12
|
+
task.fail!(
|
13
|
+
reason: task.errors.full_messages.join(". "),
|
14
|
+
messages: task.errors.messages
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CMDx
|
4
|
+
class ParameterValue
|
5
|
+
|
6
|
+
__cmdx_attr_delegator :parent, :method_source, :name, :options, :required?, :optional?, :type, to: :parameter, private: true
|
7
|
+
|
8
|
+
attr_reader :task, :parameter
|
9
|
+
|
10
|
+
def initialize(task, parameter)
|
11
|
+
@task = task
|
12
|
+
@parameter = parameter
|
13
|
+
end
|
14
|
+
|
15
|
+
def call
|
16
|
+
coerce!.tap { validate! }
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def source_defined?
|
22
|
+
task.respond_to?(method_source, true) || task.__cmdx_try(method_source)
|
23
|
+
end
|
24
|
+
|
25
|
+
def source
|
26
|
+
return @source if defined?(@source)
|
27
|
+
|
28
|
+
unless source_defined?
|
29
|
+
raise ValidationError, I18n.t(
|
30
|
+
"cmdx.parameters.undefined",
|
31
|
+
default: "delegates to undefined method #{method_source}",
|
32
|
+
source: method_source
|
33
|
+
)
|
34
|
+
end
|
35
|
+
|
36
|
+
@source = task.__cmdx_try(method_source)
|
37
|
+
end
|
38
|
+
|
39
|
+
def source_value?
|
40
|
+
return false if source.nil?
|
41
|
+
|
42
|
+
source.__cmdx_respond_to?(name, true)
|
43
|
+
end
|
44
|
+
|
45
|
+
def source_value_required?
|
46
|
+
return false if parent&.optional? && source.nil?
|
47
|
+
|
48
|
+
required? && !source_value?
|
49
|
+
end
|
50
|
+
|
51
|
+
def value
|
52
|
+
return @value if defined?(@value)
|
53
|
+
|
54
|
+
if source_value_required?
|
55
|
+
raise ValidationError, I18n.t(
|
56
|
+
"cmdx.parameters.required",
|
57
|
+
default: "is a required parameter"
|
58
|
+
)
|
59
|
+
end
|
60
|
+
|
61
|
+
@value = source.__cmdx_try(name)
|
62
|
+
return @value unless @value.nil? && options.key?(:default)
|
63
|
+
|
64
|
+
@value = task.__cmdx_yield(options[:default])
|
65
|
+
end
|
66
|
+
|
67
|
+
def coerce!
|
68
|
+
types = Array(type)
|
69
|
+
tsize = types.size - 1
|
70
|
+
|
71
|
+
types.each_with_index do |t, i|
|
72
|
+
break case t.to_sym
|
73
|
+
when :array then Coercions::Array
|
74
|
+
when :big_decimal then Coercions::BigDecimal
|
75
|
+
when :boolean then Coercions::Boolean
|
76
|
+
when :complex then Coercions::Complex
|
77
|
+
when :date then Coercions::Date
|
78
|
+
when :datetime then Coercions::DateTime
|
79
|
+
when :float then Coercions::Float
|
80
|
+
when :hash then Coercions::Hash
|
81
|
+
when :integer then Coercions::Integer
|
82
|
+
when :rational then Coercions::Rational
|
83
|
+
when :string then Coercions::String
|
84
|
+
when :time then Coercion::Time
|
85
|
+
when :virtual then Coercions::Virtual
|
86
|
+
else raise UnknownCoercionError, "unknown coercion #{t}"
|
87
|
+
end.call(value, options)
|
88
|
+
rescue CoercionError => e
|
89
|
+
next if tsize != i
|
90
|
+
|
91
|
+
raise(e) if tsize.zero?
|
92
|
+
|
93
|
+
values = types.map(&:to_s).join(", ")
|
94
|
+
raise CoercionError, I18n.t(
|
95
|
+
"cmdx.coercions.into_any",
|
96
|
+
values:,
|
97
|
+
default: "could not coerce into one of: #{values}"
|
98
|
+
)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def skip_validations_due_to_optional_missing_argument?
|
103
|
+
optional? && value.nil? && !source.nil? && !source.__cmdx_respond_to?(name, true)
|
104
|
+
end
|
105
|
+
|
106
|
+
def skip_validator_due_to_conditional?(key)
|
107
|
+
opts = options[key]
|
108
|
+
opts.is_a?(Hash) && !task.__cmdx_eval(opts)
|
109
|
+
end
|
110
|
+
|
111
|
+
def skip_validator_due_to_allow_nil?(key)
|
112
|
+
opts = options[key]
|
113
|
+
opts.is_a?(Hash) && opts[:allow_nil] && value.nil?
|
114
|
+
end
|
115
|
+
|
116
|
+
def validate!
|
117
|
+
return if skip_validations_due_to_optional_missing_argument?
|
118
|
+
|
119
|
+
options.each_key do |key|
|
120
|
+
next if skip_validator_due_to_allow_nil?(key)
|
121
|
+
next if skip_validator_due_to_conditional?(key)
|
122
|
+
|
123
|
+
case key.to_sym
|
124
|
+
when :custom then Validators::Custom
|
125
|
+
when :exclusion then Validators::Exclusion
|
126
|
+
when :format then Validators::Format
|
127
|
+
when :inclusion then Validators::Inclusion
|
128
|
+
when :length then Validators::Length
|
129
|
+
when :numeric then Validators::Numeric
|
130
|
+
when :presence then Validators::Presence
|
131
|
+
end&.call(value, options)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CMDx
|
4
|
+
class Parameters < Array
|
5
|
+
|
6
|
+
def invalid?
|
7
|
+
!valid?
|
8
|
+
end
|
9
|
+
|
10
|
+
def valid?
|
11
|
+
all?(&:valid?)
|
12
|
+
end
|
13
|
+
|
14
|
+
def validate!(task)
|
15
|
+
each { |p| recursive_validate!(task, p) }
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_h
|
19
|
+
ParametersSerializer.call(self)
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_s
|
23
|
+
ParametersInspector.call(self)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def recursive_validate!(task, parameter)
|
29
|
+
task.send(parameter.method_name)
|
30
|
+
parameter.children.each { |cp| recursive_validate!(task, cp) }
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
data/lib/cmdx/railtie.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CMDx
|
4
|
+
class Railtie < Rails::Railtie
|
5
|
+
|
6
|
+
CONCEPTS = %w[batches tasks].freeze
|
7
|
+
|
8
|
+
railtie_name :cmdx
|
9
|
+
|
10
|
+
initializer("cmdx.configure_locales") do |app|
|
11
|
+
Array(app.config.i18n.available_locales).each do |locale|
|
12
|
+
path = File.expand_path("../../../lib/locales/#{locale}.yml", __FILE__)
|
13
|
+
next unless File.file?(path)
|
14
|
+
|
15
|
+
I18n.load_path << path
|
16
|
+
end
|
17
|
+
|
18
|
+
I18n.reload!
|
19
|
+
end
|
20
|
+
|
21
|
+
initializer("cmdx.configure_rails_auto_load_paths") do |app|
|
22
|
+
app.config.autoload_paths += %w[app/cmds]
|
23
|
+
app.autoloaders.each do |autoloader|
|
24
|
+
CONCEPTS.each do |concept|
|
25
|
+
dir = app.root.join("app/cmds/#{concept}")
|
26
|
+
autoloader.collapse(dir)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
data/lib/cmdx/result.rb
ADDED
@@ -0,0 +1,170 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CMDx
|
4
|
+
class Result
|
5
|
+
|
6
|
+
__cmdx_attr_delegator :context, :run, to: :task
|
7
|
+
|
8
|
+
attr_reader :task, :state, :status, :metadata
|
9
|
+
|
10
|
+
def initialize(task)
|
11
|
+
raise ArgumentError, "must be a Task or Batch" unless task.is_a?(Task)
|
12
|
+
|
13
|
+
@task = task
|
14
|
+
@state = INITIALIZED
|
15
|
+
@status = SUCCESS
|
16
|
+
@metadata = {}
|
17
|
+
end
|
18
|
+
|
19
|
+
STATES = [
|
20
|
+
INITIALIZED = "initialized",
|
21
|
+
EXECUTING = "executing",
|
22
|
+
COMPLETE = "complete",
|
23
|
+
INTERRUPTED = "interrupted"
|
24
|
+
].freeze
|
25
|
+
|
26
|
+
STATES.each do |s|
|
27
|
+
# eg: executing?
|
28
|
+
define_method(:"#{s}?") { state == s }
|
29
|
+
end
|
30
|
+
|
31
|
+
def executed?
|
32
|
+
complete? || interrupted?
|
33
|
+
end
|
34
|
+
|
35
|
+
def executing!
|
36
|
+
return if executing?
|
37
|
+
|
38
|
+
raise "can only transition to #{EXECUTING} from #{INITIALIZED}" unless initialized?
|
39
|
+
|
40
|
+
@state = EXECUTING
|
41
|
+
end
|
42
|
+
|
43
|
+
def complete!
|
44
|
+
return if complete?
|
45
|
+
|
46
|
+
raise "can only transition to #{COMPLETE} from #{EXECUTING}" unless executing?
|
47
|
+
|
48
|
+
@state = COMPLETE
|
49
|
+
end
|
50
|
+
|
51
|
+
def interrupt!
|
52
|
+
return if interrupted?
|
53
|
+
|
54
|
+
raise "cannot transition to #{INTERRUPTED} from #{COMPLETE}" if complete?
|
55
|
+
|
56
|
+
@state = INTERRUPTED
|
57
|
+
end
|
58
|
+
|
59
|
+
STATUSES = [
|
60
|
+
SUCCESS = "success",
|
61
|
+
SKIPPED = "skipped",
|
62
|
+
FAILED = "failed"
|
63
|
+
].freeze
|
64
|
+
|
65
|
+
STATUSES.each do |s|
|
66
|
+
# eg: skipped?
|
67
|
+
define_method(:"#{s}?") { status == s }
|
68
|
+
end
|
69
|
+
|
70
|
+
def good?
|
71
|
+
!failed?
|
72
|
+
end
|
73
|
+
|
74
|
+
def bad?
|
75
|
+
!success?
|
76
|
+
end
|
77
|
+
|
78
|
+
def skip!(**metadata)
|
79
|
+
return if skipped?
|
80
|
+
|
81
|
+
raise "can only transition to #{SKIPPED} from #{SUCCESS}" unless success?
|
82
|
+
|
83
|
+
@status = SKIPPED
|
84
|
+
@metadata = metadata
|
85
|
+
|
86
|
+
halt! unless metadata[:original_exception]
|
87
|
+
end
|
88
|
+
|
89
|
+
def fail!(**metadata)
|
90
|
+
return if failed?
|
91
|
+
|
92
|
+
raise "can only transition to #{FAILED} from #{SUCCESS}" unless success?
|
93
|
+
|
94
|
+
@status = FAILED
|
95
|
+
@metadata = metadata
|
96
|
+
|
97
|
+
halt! unless metadata[:original_exception]
|
98
|
+
end
|
99
|
+
|
100
|
+
def halt!
|
101
|
+
return if success?
|
102
|
+
|
103
|
+
raise Fault.build(self)
|
104
|
+
end
|
105
|
+
|
106
|
+
def throw!(result)
|
107
|
+
raise ArgumentError, "must be a Result" unless result.is_a?(Result)
|
108
|
+
|
109
|
+
skip!(**result.metadata) if result.skipped?
|
110
|
+
fail!(**result.metadata) if result.failed?
|
111
|
+
end
|
112
|
+
|
113
|
+
def caused_failure
|
114
|
+
return unless failed?
|
115
|
+
|
116
|
+
run.results.reverse.find(&:failed?)
|
117
|
+
end
|
118
|
+
|
119
|
+
def caused_failure?
|
120
|
+
return false unless failed?
|
121
|
+
|
122
|
+
caused_failure == self
|
123
|
+
end
|
124
|
+
|
125
|
+
def threw_failure
|
126
|
+
return unless failed?
|
127
|
+
|
128
|
+
results = run.results.select(&:failed?)
|
129
|
+
results.find { |r| r.index > index } || results.last
|
130
|
+
end
|
131
|
+
|
132
|
+
def threw_failure?
|
133
|
+
return false unless failed?
|
134
|
+
|
135
|
+
threw_failure == self
|
136
|
+
end
|
137
|
+
|
138
|
+
def thrown_failure?
|
139
|
+
failed? && !caused_failure?
|
140
|
+
end
|
141
|
+
|
142
|
+
def index
|
143
|
+
run.index(self)
|
144
|
+
end
|
145
|
+
|
146
|
+
def outcome
|
147
|
+
initialized? || thrown_failure? ? state : status
|
148
|
+
end
|
149
|
+
|
150
|
+
def runtime(&block)
|
151
|
+
return @runtime unless block_given?
|
152
|
+
|
153
|
+
timeout_type = is_a?(Batch) ? :batch_timeout : :task_timeout
|
154
|
+
timeout_secs = task.task_setting(timeout_type)
|
155
|
+
|
156
|
+
Timeout.timeout(timeout_secs, TimeoutError, "execution exceeded #{timeout_secs} seconds") do
|
157
|
+
@runtime = Utils::Runtime.call(&block)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def to_h
|
162
|
+
ResultSerializer.call(self)
|
163
|
+
end
|
164
|
+
|
165
|
+
def to_s
|
166
|
+
ResultInspector.call(to_h)
|
167
|
+
end
|
168
|
+
|
169
|
+
end
|
170
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CMDx
|
4
|
+
module ResultInspector
|
5
|
+
|
6
|
+
ORDERED_KEYS = %i[
|
7
|
+
class type index id state status outcome metadata
|
8
|
+
tags pid runtime caused_failure threw_failure
|
9
|
+
].freeze
|
10
|
+
|
11
|
+
module_function
|
12
|
+
|
13
|
+
def call(result)
|
14
|
+
ORDERED_KEYS.filter_map do |key|
|
15
|
+
next unless result.key?(key)
|
16
|
+
|
17
|
+
value = result[key]
|
18
|
+
|
19
|
+
case key
|
20
|
+
when :class
|
21
|
+
"#{value}:"
|
22
|
+
when :caused_failure, :threw_failure
|
23
|
+
"#{key}=<[#{value[:index]}] #{value[:class]}: #{value[:id]}>"
|
24
|
+
else
|
25
|
+
"#{key}=#{value}"
|
26
|
+
end
|
27
|
+
end.join(" ")
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CMDx
|
4
|
+
module ResultLogger
|
5
|
+
|
6
|
+
STATUS_TO_LEVEL = {
|
7
|
+
Result::SUCCESS => :info,
|
8
|
+
Result::SKIPPED => :warn,
|
9
|
+
Result::FAILED => :error
|
10
|
+
}.freeze
|
11
|
+
|
12
|
+
module_function
|
13
|
+
|
14
|
+
def call(result)
|
15
|
+
logger = result.task.send(:logger)
|
16
|
+
status = STATUS_TO_LEVEL[result.status]
|
17
|
+
|
18
|
+
logger.send(status) { result.to_h }
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|