opera 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.
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opera
4
+ module Operation
5
+ class Base
6
+ extend Gem::Deprecate
7
+ include Opera::Operation::Builder
8
+
9
+ attr_accessor :context
10
+ attr_reader :params, :dependencies, :result
11
+
12
+ def initialize(params: {}, dependencies: {})
13
+ @context = {}
14
+ @finished = false
15
+ @result = Result.new
16
+ @params = params.freeze
17
+ @dependencies = dependencies.freeze
18
+ end
19
+
20
+ def config
21
+ self.class.config
22
+ end
23
+
24
+ def finish
25
+ finish!
26
+ end
27
+
28
+ deprecate :finish, :finish!, 2019, 6
29
+
30
+ def finish!
31
+ @finished = true
32
+ end
33
+
34
+ def finished?
35
+ @finished
36
+ end
37
+
38
+ class << self
39
+ def call(args = {})
40
+ operation = new(params: args.fetch(:params, {}), dependencies: args.fetch(:dependencies, {}))
41
+ executor = Executor.new(operation)
42
+ executor.evaluate_instructions(instructions)
43
+ executor.result
44
+ end
45
+
46
+ def config
47
+ @config ||= Config.new
48
+ end
49
+
50
+ def configure
51
+ yield config
52
+ end
53
+
54
+ def reporter
55
+ config.reporter
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opera
4
+ module Operation
5
+ module Builder
6
+ INSTRUCTIONS = %I[validate transaction benchmark step success operation operations].freeze
7
+
8
+ def self.included(base)
9
+ base.extend(ClassMethods)
10
+ end
11
+
12
+ module ClassMethods
13
+ def instructions
14
+ @instructions ||= []
15
+ end
16
+
17
+ INSTRUCTIONS.each do |instruction|
18
+ define_method instruction do |method = nil, &blk|
19
+ instructions.concat(InnerBuilder.new.send(instruction, method, &blk))
20
+ end
21
+ end
22
+ end
23
+
24
+ class InnerBuilder
25
+ attr_reader :instructions
26
+
27
+ def initialize(&block)
28
+ @instructions = []
29
+ instance_eval(&block) if block_given?
30
+ end
31
+
32
+ INSTRUCTIONS.each do |instruction|
33
+ define_method instruction do |method = nil, &blk|
34
+ instructions << if !blk.nil?
35
+ {
36
+ kind: instruction,
37
+ instructions: InnerBuilder.new(&blk).instructions
38
+ }
39
+ else
40
+ {
41
+ kind: instruction,
42
+ method: method
43
+ }
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opera
4
+ module Operation
5
+ class Config
6
+ attr_accessor :transaction_class, :transaction_method, :reporter
7
+
8
+ def initialize
9
+ @transaction_class = self.class.transaction_class
10
+ @transaction_method = self.class.transaction_method || :transaction
11
+ @reporter = custom_reporter || self.class.reporter
12
+ end
13
+
14
+ def configure
15
+ yield self
16
+ end
17
+
18
+ def custom_reporter
19
+ Rails.application.config.x.reporter.presence if defined?(Rails)
20
+ end
21
+
22
+ class << self
23
+ attr_accessor :transaction_class, :transaction_method, :reporter
24
+
25
+ def configure
26
+ yield self
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opera
4
+ module Operation
5
+ class Executor
6
+ attr_reader :operation
7
+
8
+ def initialize(operation)
9
+ @operation = operation
10
+ end
11
+
12
+ def call(instruction)
13
+ instructions = instruction[:instructions]
14
+
15
+ if instructions
16
+ evaluate_instructions(instructions)
17
+ else
18
+ evaluate_instruction(instruction)
19
+ end
20
+ end
21
+
22
+ def evaluate_instructions(instructions = [])
23
+ instruction_copy = Marshal.load(Marshal.dump(instructions))
24
+
25
+ while instruction_copy.any?
26
+ instruction = instruction_copy.shift
27
+ evaluate_instruction(instruction)
28
+ break if break_condition
29
+ end
30
+ end
31
+
32
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity
33
+ def evaluate_instruction(instruction)
34
+ case instruction[:kind]
35
+ when :step
36
+ Instructions::Executors::Step.new(operation).call(instruction)
37
+ when :operation
38
+ Instructions::Executors::Operation.new(operation).call(instruction)
39
+ when :operations
40
+ Instructions::Executors::Operations.new(operation).call(instruction)
41
+ when :success
42
+ Instructions::Executors::Success.new(operation).call(instruction)
43
+ when :validate
44
+ Instructions::Executors::Validate.new(operation).call(instruction)
45
+ when :transaction
46
+ Instructions::Executors::Transaction.new(operation).call(instruction)
47
+ when :benchmark
48
+ Instructions::Executors::Benchmark.new(operation).call(instruction)
49
+ else
50
+ raise(UnknownInstructionError, "Unknown instruction #{instruction[:kind]}")
51
+ end
52
+ end
53
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity
54
+
55
+ def result
56
+ operation.result
57
+ end
58
+
59
+ def config
60
+ operation.config
61
+ end
62
+
63
+ def context
64
+ operation.context
65
+ end
66
+
67
+ def reporter
68
+ config.reporter
69
+ end
70
+
71
+ def break_condition
72
+ operation.finished? || result.failure?
73
+ end
74
+
75
+ def add_instruction_output(instruction, output = {})
76
+ context["#{instruction[:method]}_output".to_sym] = output
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opera
4
+ module Operation
5
+ module Instructions
6
+ module Executors
7
+ class Benchmark < Executor
8
+ def call(instruction)
9
+ benchmark = ::Benchmark.measure do
10
+ super
11
+ end
12
+
13
+ result.add_information(real: benchmark.real, total: benchmark.total)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opera
4
+ module Operation
5
+ module Instructions
6
+ module Executors
7
+ class Operation < Executor
8
+ def call(instruction)
9
+ instruction[:kind] = :step
10
+ operation_result = super
11
+
12
+ if operation_result.success?
13
+ add_instruction_output(instruction, operation_result.output)
14
+ execution = result.executions.pop
15
+ result.executions << { execution => operation_result.executions }
16
+ else
17
+ result.add_errors(operation_result.errors)
18
+ result.add_exceptions(operation_result.exceptions)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opera
4
+ module Operation
5
+ module Instructions
6
+ module Executors
7
+ class Operations < Executor
8
+ class WrongOperationsResultError < Opera::Error; end
9
+
10
+ # rubocop:disable Metrics/MethodLength
11
+ def call(instruction)
12
+ instruction[:kind] = :step
13
+ operations_results = super
14
+
15
+ return if result.exceptions.any?
16
+
17
+ case operations_results
18
+ when Array
19
+ operations_results.each do |operation_result|
20
+ raise_error unless operation_result.is_a?(Opera::Operation::Result)
21
+ end
22
+
23
+ failures = operations_results.select(&:failure?)
24
+
25
+ if failures.any?
26
+ add_failures(failures)
27
+ else
28
+ add_results(instruction, operations_results)
29
+ end
30
+ else
31
+ raise_error
32
+ end
33
+ end
34
+ # rubocop:enable Metrics/MethodLength
35
+
36
+ private
37
+
38
+ def add_failures(failures)
39
+ failures.each do |failure|
40
+ result.add_errors(failure.errors)
41
+ result.add_exceptions(failure.exceptions)
42
+ end
43
+ end
44
+
45
+ def add_results(instruction, results)
46
+ add_instruction_output(instruction, results.map(&:output))
47
+ execution = result.executions.pop
48
+ result.executions << { execution => results.map(&:executions) }
49
+ end
50
+
51
+ def raise_error
52
+ raise WrongOperationsResultError, 'Have to return array of Opera::Operation::Result'
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opera
4
+ module Operation
5
+ module Instructions
6
+ module Executors
7
+ class Step < Executor
8
+ def call(instruction)
9
+ method = instruction[:method]
10
+
11
+ operation.result.add_execution(method)
12
+ operation.send(method)
13
+ rescue StandardError => exception
14
+ reporter&.error(exception)
15
+ operation.result.add_exception(method, exception.message, classname: operation.class.name)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opera
4
+ module Operation
5
+ module Instructions
6
+ module Executors
7
+ class Success < Executor
8
+ def call(instruction)
9
+ instruction[:kind] = :step
10
+ super
11
+ end
12
+
13
+ def break_condition
14
+ operation.finished?
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opera
4
+ module Operation
5
+ module Instructions
6
+ module Executors
7
+ class Transaction < Executor
8
+ class RollbackTransactionError < Opera::Error; end
9
+
10
+ def call(instruction)
11
+ transaction_class.send(transaction_method) do
12
+ super
13
+
14
+ return if !operation.finished? && result.success?
15
+
16
+ raise(RollbackTransactionError)
17
+ end
18
+ rescue RollbackTransactionError
19
+ nil
20
+ end
21
+
22
+ def transaction_class
23
+ config.transaction_class
24
+ end
25
+
26
+ def transaction_method
27
+ config.transaction_method
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opera
4
+ module Operation
5
+ module Instructions
6
+ module Executors
7
+ class Validate < Executor
8
+ def break_condition
9
+ operation.finished?
10
+ end
11
+
12
+ private
13
+
14
+ def evaluate_instruction(instruction)
15
+ instruction[:kind] = :step
16
+ dry_result = super
17
+ add_instruction_output(instruction, dry_result.output)
18
+ result.add_errors(dry_result.errors) unless dry_result.success?
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Opera
4
+ module Operation
5
+ class Result
6
+ attr_reader :errors, # Acumulator of errors in validation + steps
7
+ :exceptions, # Acumulator of exceptions in steps
8
+ :information, # Temporal object to store related information
9
+ :executions # Stacktrace or Pipe of the methods evaludated
10
+
11
+ attr_accessor :output # Final object returned if success?
12
+
13
+ def initialize(output: nil, errors: {})
14
+ @errors = errors
15
+ @exceptions = {}
16
+ @information = {}
17
+ @executions = []
18
+ @output = output
19
+ end
20
+
21
+ def failure?
22
+ errors.any? || exceptions.any?
23
+ end
24
+
25
+ def success?
26
+ !failure?
27
+ end
28
+
29
+ # rubocop:disable Metrics/MethodLength
30
+ def add_error(field, message)
31
+ @errors[field] ||= []
32
+ if message.is_a?(Hash)
33
+ if @errors[field].first&.is_a?(Hash)
34
+ @errors[field].first.merge!(message)
35
+ else
36
+ @errors[field].push(message)
37
+ end
38
+ else
39
+ @errors[field].concat(Array(message))
40
+ end
41
+ @errors[field].uniq!
42
+ end
43
+ # rubocop:enable Metrics/MethodLength
44
+
45
+ def add_errors(errors)
46
+ errors.each_pair do |key, value|
47
+ add_error(key, value)
48
+ end
49
+ end
50
+
51
+ def add_exception(method, message, classname: nil)
52
+ key = [classname, Array(method).first].compact.join('#')
53
+ @exceptions[key] ||= []
54
+ @exceptions[key].push(message)
55
+ end
56
+
57
+ def add_exceptions(exceptions)
58
+ exceptions.each_pair do |key, value|
59
+ add_exception(key, value)
60
+ end
61
+ end
62
+
63
+ def add_information(hash)
64
+ @information.merge!(hash)
65
+ end
66
+
67
+ def add_execution(step)
68
+ @executions << step
69
+ end
70
+ end
71
+ end
72
+ end