lite-command 2.0.3 → 2.1.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.
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lite
4
+ module Command
5
+ class Attribute
6
+
7
+ attr_accessor :command
8
+ attr_reader :method_name, :options, :errors
9
+
10
+ def initialize(method_name, options)
11
+ @method_name = method_name
12
+ @options = options
13
+ @errors = []
14
+ end
15
+
16
+ def from
17
+ options[:from] || :context
18
+ end
19
+
20
+ def filled?
21
+ Utils.call(command, options[:filled]) || false
22
+ end
23
+
24
+ def required?
25
+ Utils.call(command, options[:required]) || false
26
+ end
27
+
28
+ def typed?
29
+ options.key?(:types) && types.any?
30
+ end
31
+
32
+ def types
33
+ @types ||= begin
34
+ t = Array(Utils.call(command, options[:types]))
35
+
36
+ if filled?
37
+ t.uniq - [NilClass]
38
+ else
39
+ t | [NilClass]
40
+ end
41
+ end
42
+ end
43
+
44
+ def validate!
45
+ validate_respond_attribute!
46
+ return unless errors.empty?
47
+
48
+ validate_required_attribute!
49
+ validate_attribute_type!
50
+ validate_attribute_filled!
51
+ end
52
+
53
+ def valid?
54
+ errors.empty?
55
+ end
56
+
57
+ def value
58
+ return @value if defined?(@value)
59
+
60
+ @value = command.send(from).public_send(method_name)
61
+ end
62
+
63
+ private
64
+
65
+ def validate_respond_attribute!
66
+ return if command.respond_to?(from, true)
67
+
68
+ @errors << "is not defined or an attribute"
69
+ end
70
+
71
+ def validate_required_attribute!
72
+ return unless required?
73
+ return if command.send(from).respond_to?(method_name)
74
+
75
+ @errors << "#{method_name} is required"
76
+ end
77
+
78
+ def validate_attribute_type!
79
+ return unless typed?
80
+ return if types.include?(value.class)
81
+
82
+ @errors << "#{method_name} type invalid"
83
+ end
84
+
85
+ def validate_attribute_filled!
86
+ return unless filled? && value.nil?
87
+
88
+ @errors << "#{method_name} must be filled"
89
+ end
90
+
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,35 @@
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, attribute|
16
+ attribute.tap { |a| a.command = command }
17
+ end
18
+ end
19
+
20
+ def errors
21
+ @errors ||= attributes.each_with_object({}) do |attribute, h|
22
+ next if attribute.tap(&:validate!).valid?
23
+
24
+ h[attribute.from] ||= []
25
+ h[attribute.from] = h[attribute.from] | attribute.errors
26
+ end
27
+ end
28
+
29
+ def valid?
30
+ attributes.empty? || errors.empty?
31
+ end
32
+
33
+ end
34
+ end
35
+ end
@@ -5,12 +5,11 @@ 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
15
  # eg: Users::ResetPassword::Fault < Lite::Command::Fault
@@ -22,6 +21,8 @@ module Lite
22
21
  #{base}::Failure = Class.new(#{base}::Fault)
23
22
  #{base}::Error = Class.new(#{base}::Fault)
24
23
  RUBY
24
+
25
+ super
25
26
  end
26
27
 
27
28
  attr_reader :context
@@ -29,16 +30,7 @@ module Lite
29
30
 
30
31
  def initialize(context = {})
31
32
  @context = Lite::Command::Context.build(context)
32
- end
33
-
34
- private
35
-
36
- def on_before_execution
37
- # Define in your class to run code before execution
38
- end
39
-
40
- def on_after_execution
41
- # Define in your class to run code after execution
33
+ Utils.hook(self, :on_pending)
42
34
  end
43
35
 
44
36
  end
@@ -5,14 +5,28 @@ module Lite
5
5
 
6
6
  class Fault < StandardError
7
7
 
8
- attr_reader :reason, :caused_by, :thrown_by
8
+ attr_reader :caused_by, :thrown_by, :reason, :metadata
9
+
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)
9
15
 
10
- def initialize(reason, caused_by, thrown_by)
11
16
  super(reason)
17
+ end
12
18
 
13
- @reason = reason
14
- @caused_by = caused_by
15
- @thrown_by = thrown_by
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
16
30
  end
17
31
 
18
32
  def type
@@ -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,99 @@
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
+ raise Lite::Command::Fault.build(f.capitalize, self, object, dynamic: raise_dynamic_faults?)
90
+ end
91
+ end
92
+
93
+ alias fail! failure!
94
+
95
+ end
96
+ end
97
+
98
+ end
99
+ 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
+ attribute = Lite::Command::Attribute.new(method_name, options)
17
+ attributes[method_name] = attribute
18
+
19
+ define_method(method_name) do
20
+ ivar = :"@#{method_name}"
21
+ return instance_variable_get(ivar) if instance_variable_defined?(ivar)
22
+
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
@@ -11,24 +11,7 @@ module Lite
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
- f = e.respond_to?(:type) ? e.type : ERROR
20
-
21
- send(:"#{f}", e)
22
- after_execution
23
- send(:"on_#{f}", e)
24
- end
25
-
26
- def execute!
27
- around_execution { call }
28
- rescue StandardError => e
29
- after_execution
30
- raise(e)
31
- end
14
+ module Execute
32
15
 
33
16
  def state
34
17
  @state || PENDING
@@ -50,15 +33,17 @@ module Lite
50
33
 
51
34
  def before_execution
52
35
  increment_execution_index
53
- assign_execution_cid
36
+ assign_execution_cmd_id
54
37
  start_monotonic_time
38
+ Utils.hook(self, :on_before_execution)
39
+ validate_context_attributes
55
40
  executing!
56
- on_before_execution
41
+ Utils.hook(self, :on_executing)
57
42
  end
58
43
 
59
44
  def after_execution
60
- fault? ? interrupted! : complete!
61
- on_after_execution
45
+ send(:"#{success? ? COMPLETE : INTERRUPTED}!")
46
+ Utils.hook(self, :on_after_execution)
62
47
  stop_monotonic_time
63
48
  append_execution_result
64
49
  freeze_execution_objects
@@ -70,6 +55,29 @@ module Lite
70
55
  after_execution
71
56
  end
72
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
+ Utils.hook(self, :"on_#{status}", e)
76
+ raise(e)
77
+ else
78
+ Utils.hook(self, :"on_#{state}")
79
+ end
80
+
73
81
  end
74
82
  end
75
83
 
@@ -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, argument)
20
+ if argument.is_a?(Symbol) || argument.is_a?(String)
21
+ object.send(argument)
22
+ elsif argument.is_a?(Proc)
23
+ object.instance_eval(&argument)
24
+ else
25
+ argument
26
+ end
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -3,7 +3,7 @@
3
3
  module Lite
4
4
  module Command
5
5
 
6
- VERSION = "2.0.3"
6
+ VERSION = "2.1.1"
7
7
 
8
8
  end
9
9
  end
data/lib/lite/command.rb CHANGED
@@ -3,10 +3,17 @@
3
3
  require "generators/rails/command_generator" if defined?(Rails::Generators)
4
4
 
5
5
  require "lite/command/version"
6
- require "lite/command/internals/callable"
7
- require "lite/command/internals/executable"
8
- require "lite/command/internals/faultable"
9
- require "lite/command/internals/resultable"
10
- require "lite/command/fault"
6
+ require "lite/command/utils"
11
7
  require "lite/command/context"
8
+ require "lite/command/attribute"
9
+ require "lite/command/attribute_validator"
10
+ require "lite/command/fault"
11
+ require "lite/command/fault_streamer"
12
+ require "lite/command/internals/context"
13
+ require "lite/command/internals/call"
14
+ require "lite/command/internals/execute"
15
+ require "lite/command/internals/fault"
16
+ require "lite/command/internals/result"
12
17
  require "lite/command/base"
18
+ require "lite/command/step"
19
+ require "lite/command/sequence"