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.
Files changed (103) hide show
  1. checksums.yaml +7 -0
  2. data/.DS_Store +0 -0
  3. data/.rspec +4 -0
  4. data/.rubocop.yml +64 -0
  5. data/.ruby-version +1 -0
  6. data/CHANGELOG.md +5 -0
  7. data/CODE_OF_CONDUCT.md +132 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +76 -0
  10. data/Rakefile +12 -0
  11. data/docs/basics/call.md +31 -0
  12. data/docs/basics/context.md +67 -0
  13. data/docs/basics/run.md +34 -0
  14. data/docs/basics/setup.md +32 -0
  15. data/docs/batch.md +53 -0
  16. data/docs/configuration.md +57 -0
  17. data/docs/example.md +82 -0
  18. data/docs/getting_started.md +47 -0
  19. data/docs/hooks.md +59 -0
  20. data/docs/interruptions/exceptions.md +29 -0
  21. data/docs/interruptions/faults.md +89 -0
  22. data/docs/interruptions/halt.md +80 -0
  23. data/docs/logging.md +102 -0
  24. data/docs/outcomes/result.md +17 -0
  25. data/docs/outcomes/states.md +31 -0
  26. data/docs/outcomes/statuses.md +33 -0
  27. data/docs/parameters/coercions.md +52 -0
  28. data/docs/parameters/defaults.md +47 -0
  29. data/docs/parameters/definitions.md +123 -0
  30. data/docs/parameters/namespacing.md +57 -0
  31. data/docs/parameters/validations.md +312 -0
  32. data/docs/tips_and_tricks.md +79 -0
  33. data/lib/cmdx/.DS_Store +0 -0
  34. data/lib/cmdx/batch.rb +43 -0
  35. data/lib/cmdx/coercions/array.rb +15 -0
  36. data/lib/cmdx/coercions/big_decimal.rb +23 -0
  37. data/lib/cmdx/coercions/boolean.rb +27 -0
  38. data/lib/cmdx/coercions/complex.rb +21 -0
  39. data/lib/cmdx/coercions/date.rb +26 -0
  40. data/lib/cmdx/coercions/date_time.rb +26 -0
  41. data/lib/cmdx/coercions/float.rb +21 -0
  42. data/lib/cmdx/coercions/hash.rb +31 -0
  43. data/lib/cmdx/coercions/integer.rb +21 -0
  44. data/lib/cmdx/coercions/rational.rb +21 -0
  45. data/lib/cmdx/coercions/string.rb +15 -0
  46. data/lib/cmdx/coercions/time.rb +26 -0
  47. data/lib/cmdx/coercions/virtual.rb +15 -0
  48. data/lib/cmdx/configuration.rb +25 -0
  49. data/lib/cmdx/context.rb +15 -0
  50. data/lib/cmdx/core_ext/hash.rb +36 -0
  51. data/lib/cmdx/core_ext/module.rb +48 -0
  52. data/lib/cmdx/core_ext/object.rb +55 -0
  53. data/lib/cmdx/error.rb +23 -0
  54. data/lib/cmdx/errors.rb +92 -0
  55. data/lib/cmdx/fault.rb +47 -0
  56. data/lib/cmdx/faults.rb +11 -0
  57. data/lib/cmdx/immutator.rb +21 -0
  58. data/lib/cmdx/lazy_struct.rb +79 -0
  59. data/lib/cmdx/log_formatters/json.rb +13 -0
  60. data/lib/cmdx/log_formatters/key_value.rb +13 -0
  61. data/lib/cmdx/log_formatters/line.rb +14 -0
  62. data/lib/cmdx/log_formatters/logstash.rb +18 -0
  63. data/lib/cmdx/log_formatters/raw.rb +13 -0
  64. data/lib/cmdx/logger.rb +16 -0
  65. data/lib/cmdx/parameter.rb +101 -0
  66. data/lib/cmdx/parameter_inspector.rb +23 -0
  67. data/lib/cmdx/parameter_serializer.rb +20 -0
  68. data/lib/cmdx/parameter_validator.rb +19 -0
  69. data/lib/cmdx/parameter_value.rb +136 -0
  70. data/lib/cmdx/parameters.rb +34 -0
  71. data/lib/cmdx/parameters_inspector.rb +13 -0
  72. data/lib/cmdx/parameters_serializer.rb +13 -0
  73. data/lib/cmdx/railtie.rb +32 -0
  74. data/lib/cmdx/result.rb +170 -0
  75. data/lib/cmdx/result_inspector.rb +31 -0
  76. data/lib/cmdx/result_logger.rb +22 -0
  77. data/lib/cmdx/result_serializer.rb +38 -0
  78. data/lib/cmdx/run.rb +33 -0
  79. data/lib/cmdx/run_inspector.rb +21 -0
  80. data/lib/cmdx/run_serializer.rb +16 -0
  81. data/lib/cmdx/task.rb +151 -0
  82. data/lib/cmdx/task_hook.rb +18 -0
  83. data/lib/cmdx/utils/datetime_formatter.rb +17 -0
  84. data/lib/cmdx/utils/method_name.rb +24 -0
  85. data/lib/cmdx/utils/runtime.rb +19 -0
  86. data/lib/cmdx/validators/custom.rb +20 -0
  87. data/lib/cmdx/validators/exclusion.rb +51 -0
  88. data/lib/cmdx/validators/format.rb +27 -0
  89. data/lib/cmdx/validators/inclusion.rb +51 -0
  90. data/lib/cmdx/validators/length.rb +114 -0
  91. data/lib/cmdx/validators/numeric.rb +114 -0
  92. data/lib/cmdx/validators/presence.rb +27 -0
  93. data/lib/cmdx/version.rb +7 -0
  94. data/lib/cmdx.rb +80 -0
  95. data/lib/generators/cmdx/batch_generator.rb +30 -0
  96. data/lib/generators/cmdx/install_generator.rb +15 -0
  97. data/lib/generators/cmdx/task_generator.rb +30 -0
  98. data/lib/generators/cmdx/templates/batch.rb.tt +7 -0
  99. data/lib/generators/cmdx/templates/install.rb +23 -0
  100. data/lib/generators/cmdx/templates/task.rb.tt +9 -0
  101. data/lib/locales/en.yml +36 -0
  102. data/lib/locales/es.yml +36 -0
  103. 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
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMDx
4
+ module LogFormatters
5
+ class Raw
6
+
7
+ def call(_severity, _time, _progname, message)
8
+ message
9
+ end
10
+
11
+ end
12
+ end
13
+ end
@@ -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
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMDx
4
+ module ParametersInspector
5
+
6
+ module_function
7
+
8
+ def call(parameters)
9
+ parameters.map(&:to_s).join("\n")
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMDx
4
+ module ParametersSerializer
5
+
6
+ module_function
7
+
8
+ def call(parameters)
9
+ parameters.map(&:to_h)
10
+ end
11
+
12
+ end
13
+ end
@@ -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
@@ -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