lite-command 2.0.2 → 2.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.
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lite
4
+ module Command
5
+ class Attribute
6
+
7
+ # TODO: allow procs
8
+
9
+ attr_reader :command, :method_name, :options, :errors
10
+
11
+ def initialize(command, method_name, options)
12
+ @command = command
13
+ @method_name = method_name
14
+ @options = options
15
+ @errors = []
16
+ end
17
+
18
+ def from
19
+ options[:from] || :context
20
+ end
21
+
22
+ def filled?
23
+ Utils.call(command, options[:filled]) || false
24
+ end
25
+
26
+ def required?
27
+ Utils.call(command, options[:required]) || false
28
+ end
29
+
30
+ def typed?
31
+ options.key?(:types) && types.any?
32
+ end
33
+
34
+ def types
35
+ @types ||= begin
36
+ t = Array(Utils.call(command, options[:types]))
37
+
38
+ if filled?
39
+ t - [NilClass]
40
+ else
41
+ t | [NilClass]
42
+ end
43
+ end
44
+ end
45
+
46
+ def validate!
47
+ validate_respond_attribute!
48
+ return unless errors.empty?
49
+
50
+ validate_required_attribute!
51
+ validate_attribute_type!
52
+ validate_attribute_filled!
53
+ end
54
+
55
+ def valid?
56
+ errors.empty?
57
+ end
58
+
59
+ def value
60
+ return @value if defined?(@value)
61
+
62
+ @value = command.send(from).public_send(method_name)
63
+ end
64
+
65
+ private
66
+
67
+ def validate_respond_attribute!
68
+ return if command.respond_to?(from, true)
69
+
70
+ @errors << "is not defined or an attribute"
71
+ end
72
+
73
+ def validate_required_attribute!
74
+ return unless required?
75
+ return if command.send(from).respond_to?(method_name)
76
+
77
+ @errors << "#{method_name} is required"
78
+ end
79
+
80
+ def validate_attribute_type!
81
+ return unless typed?
82
+ return if types.include?(value.class)
83
+
84
+ @errors << "#{method_name} type invalid"
85
+ end
86
+
87
+ def validate_attribute_filled!
88
+ return unless filled?
89
+ return unless value.nil?
90
+
91
+ @errors << "#{method_name} must be filled"
92
+ end
93
+
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lite
4
+ module Command
5
+ class AttributeValidator
6
+
7
+ attr_reader :command
8
+
9
+ def initialize(command)
10
+ @command = command
11
+ end
12
+
13
+ def attributes
14
+ @attributes ||=
15
+ command.class.attributes.map do |method_name, options|
16
+ Lite::Command::Attribute.new(command, method_name, options)
17
+ end
18
+ end
19
+
20
+ def errors
21
+ @errors ||= attributes.each_with_object({}) do |attribute, h|
22
+ attribute.validate!
23
+ next if attribute.valid?
24
+
25
+ h[attribute.from] ||= []
26
+ h[attribute.from] = h[attribute.from] | attribute.errors
27
+ end
28
+ end
29
+
30
+ def valid?
31
+ attributes.empty? || errors.empty?
32
+ end
33
+
34
+ end
35
+ end
36
+ end
@@ -5,24 +5,24 @@ module Lite
5
5
  class Base
6
6
 
7
7
  def self.inherited(base)
8
- super
9
-
10
- base.include Lite::Command::Internals::Callable
11
- base.include Lite::Command::Internals::Executable
12
- base.include Lite::Command::Internals::Faultable
13
- base.include Lite::Command::Internals::Resultable
8
+ base.include Lite::Command::Internals::Context
9
+ base.include Lite::Command::Internals::Call
10
+ base.include Lite::Command::Internals::Execute
11
+ base.include Lite::Command::Internals::Fault
12
+ base.include Lite::Command::Internals::Result
14
13
 
15
14
  base.class_eval <<-RUBY, __FILE__, __LINE__ + 1
16
- # eg: Users::ResetPassword::Fault
17
- class #{base}::Fault < Lite::Command::Fault; end
15
+ # eg: Users::ResetPassword::Fault < Lite::Command::Fault
16
+ #{base}::Fault = Class.new(Lite::Command::Fault)
17
+
18
+ # eg: Users::ResetPassword::Noop < Users::ResetPassword::Fault
19
+ #{base}::Noop = Class.new(#{base}::Fault)
20
+ #{base}::Invalid = Class.new(#{base}::Fault)
21
+ #{base}::Failure = Class.new(#{base}::Fault)
22
+ #{base}::Error = Class.new(#{base}::Fault)
18
23
  RUBY
19
24
 
20
- FAULTS.each do |f|
21
- base.class_eval <<-RUBY, __FILE__, __LINE__ + 1
22
- # eg: Users::ResetPassword::Noop < Users::ResetPassword::Fault
23
- class #{base}::#{f.capitalize} < #{base}::Fault; end
24
- RUBY
25
- end
25
+ super
26
26
  end
27
27
 
28
28
  attr_reader :context
@@ -30,16 +30,7 @@ module Lite
30
30
 
31
31
  def initialize(context = {})
32
32
  @context = Lite::Command::Context.build(context)
33
- end
34
-
35
- private
36
-
37
- def on_before_execution
38
- # Define in your class to run code before execution
39
- end
40
-
41
- def on_after_execution
42
- # Define in your class to run code after execution
33
+ Utils.hook(self, :on_pending)
43
34
  end
44
35
 
45
36
  end
@@ -5,22 +5,32 @@ module Lite
5
5
 
6
6
  class Fault < StandardError
7
7
 
8
- attr_reader :caused_by, :thrown_by, :reason
8
+ attr_reader :caused_by, :thrown_by, :reason, :metadata
9
9
 
10
- def initialize(caused_by, thrown_by, reason)
11
- super(reason)
10
+ def initialize(**params)
11
+ @reason = params.fetch(:reason)
12
+ @metadata = params.fetch(:metadata)
13
+ @caused_by = params.fetch(:caused_by)
14
+ @thrown_by = params.fetch(:thrown_by)
12
15
 
13
- @caused_by = caused_by
14
- @thrown_by = thrown_by
15
- @reason = reason
16
+ super(reason)
16
17
  end
17
18
 
18
- def fault_klass
19
- @fault_klass ||= self.class.name.split("::").last
19
+ def self.build(type, command, thrown_exception, dynamic: false)
20
+ klass = dynamic ? command.class : Lite::Command
21
+ fault = klass.const_get(type.to_s)
22
+ fault = fault.new(
23
+ reason: command.reason,
24
+ metadata: command.metadata,
25
+ caused_by: command.caused_by || command,
26
+ thrown_by: command
27
+ )
28
+ fault.set_backtrace(thrown_exception.backtrace) if thrown_exception.respond_to?(:backtrace)
29
+ fault
20
30
  end
21
31
 
22
- def fault_name
23
- @fault_name ||= fault_klass.downcase
32
+ def type
33
+ @type ||= self.class.name.split("::").last.downcase
24
34
  end
25
35
 
26
36
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lite
4
+ module Command
5
+ class FaultStreamer
6
+
7
+ attr_reader :command, :object
8
+
9
+ def initialize(command, object)
10
+ @command = command
11
+ @object = object
12
+ end
13
+
14
+ def caused_by
15
+ Utils.try(object, :caused_by) || command
16
+ end
17
+
18
+ def thrown_by
19
+ return object if Utils.try(object, :executed?)
20
+
21
+ Utils.try(object, :thrown_by) || command.caused_by
22
+ end
23
+
24
+ def metadata
25
+ Utils.try(object, :metadata) || command.metadata
26
+ end
27
+
28
+ def reason
29
+ if object.respond_to?(:reason)
30
+ object.reason
31
+ elsif object.is_a?(StandardError)
32
+ "[#{object.class.name}] #{object.message}".chomp(".")
33
+ else
34
+ object
35
+ end
36
+ end
37
+
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lite
4
+ module Command
5
+
6
+ STATUSES = [
7
+ SUCCESS = "success",
8
+ NOOP = "noop",
9
+ INVALID = "invalid",
10
+ FAILURE = "failure",
11
+ ERROR = "error"
12
+ ].freeze
13
+ FAULTS = (STATUSES - [SUCCESS]).freeze
14
+
15
+ module Internals
16
+ module Call
17
+
18
+ def self.included(base)
19
+ base.extend ClassMethods
20
+ base.class_eval do
21
+ attr_reader :reason, :metadata
22
+ end
23
+ end
24
+
25
+ module ClassMethods
26
+
27
+ def call(context = {})
28
+ instance = send(:new, context)
29
+ instance.send(:execute)
30
+ instance
31
+ end
32
+
33
+ def call!(context = {})
34
+ instance = send(:new, context)
35
+ instance.send(:execute!)
36
+ instance
37
+ end
38
+
39
+ end
40
+
41
+ def call
42
+ raise NotImplementedError, "call method not defined in #{self.class}"
43
+ end
44
+
45
+ def status
46
+ @status || SUCCESS
47
+ end
48
+
49
+ def success?
50
+ status == SUCCESS
51
+ end
52
+
53
+ def ok?(reason = nil)
54
+ success? || noop?(reason)
55
+ end
56
+
57
+ def fault?(reason = nil)
58
+ !success? && reason?(reason)
59
+ end
60
+
61
+ FAULTS.each do |f|
62
+ # eg: noop? or failure?("idk")
63
+ define_method(:"#{f}?") do |reason = nil|
64
+ status == f && reason?(reason)
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def reason?(str)
71
+ str.nil? || str == reason
72
+ end
73
+
74
+ def fault(object, status, metadata)
75
+ @status = status
76
+ @metadata = metadata
77
+
78
+ down_stream = Lite::Command::FaultStreamer.new(self, object)
79
+ @reason ||= down_stream.reason
80
+ @metadata ||= down_stream.metadata
81
+ @caused_by ||= down_stream.caused_by
82
+ @thrown_by ||= down_stream.thrown_by
83
+ end
84
+
85
+ FAULTS.each do |f|
86
+ # eg: invalid!("idk") or failure!(fault) or error!("idk", { error_key: "some.error" })
87
+ define_method(:"#{f}!") do |object, metadata = nil|
88
+ fault(object, f, metadata)
89
+
90
+ raise Lite::Command::Fault.build(f.capitalize, self, object, dynamic: raise_dynamic_faults?)
91
+ end
92
+ end
93
+
94
+ alias fail! failure!
95
+
96
+ end
97
+ end
98
+
99
+ end
100
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lite
4
+ module Command
5
+ module Internals
6
+ module Context
7
+
8
+ def self.included(base)
9
+ base.extend ClassMethods
10
+ end
11
+
12
+ module ClassMethods
13
+
14
+ def attribute(*args, **options)
15
+ args.each do |method_name|
16
+ attributes[method_name] = options
17
+
18
+ define_method(method_name) do
19
+ ivar = :"@#{method_name}"
20
+ return instance_variable_get(ivar) if instance_variable_defined?(ivar)
21
+
22
+ attribute = Lite::Command::Attribute.new(self, method_name, options)
23
+ instance_variable_set(ivar, attribute.value)
24
+ end
25
+ end
26
+ end
27
+
28
+ def attributes
29
+ @attributes ||= {}
30
+ end
31
+
32
+ end
33
+
34
+ private
35
+
36
+ def validate_context_attributes
37
+ validator = Lite::Command::AttributeValidator.new(self)
38
+ return if validator.valid?
39
+
40
+ invalid!("Invalid context attributes", validator.errors)
41
+ end
42
+
43
+ end
44
+ end
45
+ end
46
+ end
@@ -4,48 +4,28 @@ module Lite
4
4
  module Command
5
5
 
6
6
  STATES = [
7
- PENDING = "pending",
8
- EXECUTING = "executing",
9
- COMPLETE = "complete",
10
- DNF = "dnf"
7
+ PENDING = "pending",
8
+ EXECUTING = "executing",
9
+ COMPLETE = "complete",
10
+ INTERRUPTED = "interrupted"
11
11
  ].freeze
12
12
 
13
13
  module Internals
14
- module Executable
15
-
16
- def execute
17
- around_execution { call }
18
- rescue StandardError => e
19
- fn = e.respond_to?(:fault_name) ? e.fault_name : ERROR
20
-
21
- send(:"#{fn}", e)
22
- after_execution
23
- send(:"on_#{fn}", e)
24
- end
25
-
26
- def execute!
27
- around_execution { call }
28
- rescue StandardError => e
29
- after_execution
30
-
31
- raise(e) unless raise_dynamic_faults? && e.is_a?(Lite::Command::Fault)
32
-
33
- raise_dynamic_fault(e)
34
- end
14
+ module Execute
35
15
 
36
16
  def state
37
17
  @state || PENDING
38
18
  end
39
19
 
40
20
  def executed?
41
- dnf? || complete?
21
+ complete? || interrupted?
42
22
  end
43
23
 
44
24
  STATES.each do |s|
45
25
  # eg: executing?
46
26
  define_method(:"#{s}?") { state == s }
47
27
 
48
- # eg: dnf!
28
+ # eg: interrupted!
49
29
  define_method(:"#{s}!") { @state = s }
50
30
  end
51
31
 
@@ -53,15 +33,17 @@ module Lite
53
33
 
54
34
  def before_execution
55
35
  increment_execution_index
56
- assign_execution_cid
36
+ assign_execution_cmd_id
57
37
  start_monotonic_time
38
+ Utils.hook(self, :on_before_execution)
39
+ validate_context_attributes
58
40
  executing!
59
- on_before_execution
41
+ Utils.hook(self, :on_executing)
60
42
  end
61
43
 
62
44
  def after_execution
63
- fault? ? dnf! : complete!
64
- on_after_execution
45
+ send(:"#{success? ? COMPLETE : INTERRUPTED}!")
46
+ Utils.hook(self, :on_after_execution)
65
47
  stop_monotonic_time
66
48
  append_execution_result
67
49
  freeze_execution_objects
@@ -73,6 +55,28 @@ module Lite
73
55
  after_execution
74
56
  end
75
57
 
58
+ def execute
59
+ around_execution { call }
60
+ Utils.hook(self, :on_success)
61
+ rescue StandardError => e
62
+ fault(e, ERROR, metadata) unless e.is_a?(Lite::Command::Fault)
63
+ after_execution
64
+ Utils.hook(self, :"on_#{status}", e)
65
+ ensure
66
+ Utils.hook(self, :"on_#{state}")
67
+ end
68
+
69
+ def execute!
70
+ around_execution { call }
71
+ Utils.hook(self, :on_success)
72
+ rescue StandardError => e
73
+ fault(e, ERROR, metadata) unless e.is_a?(Lite::Command::Fault)
74
+ after_execution
75
+ raise(e)
76
+ else
77
+ Utils.hook(self, :"on_#{state}")
78
+ end
79
+
76
80
  end
77
81
  end
78
82
 
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lite
4
+ module Command
5
+ module Internals
6
+ module Fault
7
+
8
+ def self.included(base)
9
+ base.class_eval do
10
+ attr_reader :caused_by, :thrown_by
11
+ end
12
+ end
13
+
14
+ def caused_fault?
15
+ caused_by == self
16
+ end
17
+
18
+ def threw_fault?
19
+ thrown_by == self
20
+ end
21
+
22
+ def thrown?
23
+ fault? && !caused_fault?
24
+ end
25
+
26
+ private
27
+
28
+ def throw!(command)
29
+ return if command.success?
30
+
31
+ send(:"#{command.status}!", command)
32
+ end
33
+
34
+ def raise_dynamic_faults?
35
+ false
36
+ end
37
+
38
+ end
39
+ end
40
+ end
41
+ end
@@ -5,14 +5,14 @@ require "securerandom" unless defined?(SecureRandom)
5
5
  module Lite
6
6
  module Command
7
7
  module Internals
8
- module Resultable
8
+ module Result
9
9
 
10
10
  def index
11
11
  @index ||= context.index ||= 0
12
12
  end
13
13
 
14
- def cid
15
- @cid ||= context.cid
14
+ def cmd_id
15
+ @cmd_id ||= context.cmd_id ||= SecureRandom.uuid
16
16
  end
17
17
 
18
18
  def outcome
@@ -28,12 +28,13 @@ module Lite
28
28
  def to_hash
29
29
  {
30
30
  index:,
31
- cid:,
31
+ cmd_id:,
32
32
  command: self.class.name,
33
33
  outcome:,
34
34
  state:,
35
35
  status:,
36
36
  reason:,
37
+ metadata:,
37
38
  caused_by: caused_by&.index,
38
39
  thrown_by: thrown_by&.index,
39
40
  runtime:
@@ -43,8 +44,8 @@ module Lite
43
44
 
44
45
  private
45
46
 
46
- def assign_execution_cid
47
- context.cid ||= SecureRandom.uuid
47
+ def assign_execution_cmd_id
48
+ @cmd_id = context.cmd_id ||= cmd_id
48
49
  end
49
50
 
50
51
  def increment_execution_index
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lite
4
+ module Command
5
+ class Sequence < Base
6
+
7
+ def self.step(*commands, **options)
8
+ commands.flatten.each do |command|
9
+ steps << Step.new(command, options)
10
+ end
11
+ end
12
+
13
+ def self.steps
14
+ @steps ||= []
15
+ end
16
+
17
+ def call
18
+ self.class.steps.each do |step|
19
+ next unless step.run?(self)
20
+
21
+ cmd = step.command.call(context)
22
+ throw!(cmd) unless cmd.ok?
23
+ end
24
+ end
25
+
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lite
4
+ module Command
5
+ class Step
6
+
7
+ attr_reader :command, :options
8
+
9
+ def initialize(command, options)
10
+ @command = command
11
+ @options = options
12
+ end
13
+
14
+ def run?(cmd)
15
+ if options[:if]
16
+ Utils.call(cmd, options[:if])
17
+ elsif options[:unless]
18
+ !Utils.call(cmd, options[:unless])
19
+ else
20
+ true
21
+ end
22
+ end
23
+
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lite
4
+ module Command
5
+ module Utils
6
+
7
+ module_function
8
+
9
+ def try(object, method_name, *args, include_private: false)
10
+ return unless object.respond_to?(method_name, include_private)
11
+
12
+ object.send(method_name, *args)
13
+ end
14
+
15
+ def hook(object, method_name, *args)
16
+ try(object, method_name, *args, include_private: true)
17
+ end
18
+
19
+ def call(object, method_name_or_proc)
20
+ if method_name_or_proc.is_a?(Symbol) || method_name_or_proc.is_a?(String)
21
+ object.send(method_name_or_proc)
22
+ elsif method_name_or_proc.is_a?(Proc)
23
+ object.instance_eval(&method_name_or_proc)
24
+ else
25
+ method_name_or_proc
26
+ end
27
+ end
28
+
29
+ end
30
+ end
31
+ end