opera 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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