lite-command 2.0.3 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,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,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
@@ -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,28 @@ 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
+ raise(e)
76
+ else
77
+ Utils.hook(self, :"on_#{state}")
78
+ end
79
+
73
80
  end
74
81
  end
75
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
@@ -3,7 +3,7 @@
3
3
  module Lite
4
4
  module Command
5
5
 
6
- VERSION = "2.0.3"
6
+ VERSION = "2.1.0"
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"