opera 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +6 -0
- data/CHANGELOG.md +6 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +65 -0
- data/LICENSE.txt +21 -0
- data/README.md +828 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/opera.rb +7 -0
- data/lib/opera/errors.rb +6 -0
- data/lib/opera/operation.rb +20 -0
- data/lib/opera/operation/README.md +786 -0
- data/lib/opera/operation/base.rb +60 -0
- data/lib/opera/operation/builder.rb +50 -0
- data/lib/opera/operation/config.rb +31 -0
- data/lib/opera/operation/executor.rb +80 -0
- data/lib/opera/operation/instructions/executors/benchmark.rb +19 -0
- data/lib/opera/operation/instructions/executors/operation.rb +25 -0
- data/lib/opera/operation/instructions/executors/operations.rb +58 -0
- data/lib/opera/operation/instructions/executors/step.rb +21 -0
- data/lib/opera/operation/instructions/executors/success.rb +20 -0
- data/lib/opera/operation/instructions/executors/transaction.rb +33 -0
- data/lib/opera/operation/instructions/executors/validate.rb +24 -0
- data/lib/opera/operation/result.rb +72 -0
- data/lib/opera/version.rb +3 -0
- data/opera.gemspec +28 -0
- metadata +90 -0
@@ -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
|