solid-result 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rubocop.yml +98 -0
- data/.rubocop_todo.yml +12 -0
- data/CHANGELOG.md +600 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +2691 -0
- data/Rakefile +28 -0
- data/Steepfile +31 -0
- data/examples/multiple_listeners/Rakefile +55 -0
- data/examples/multiple_listeners/app/models/account/member.rb +10 -0
- data/examples/multiple_listeners/app/models/account/owner_creation.rb +62 -0
- data/examples/multiple_listeners/app/models/account.rb +11 -0
- data/examples/multiple_listeners/app/models/user/creation.rb +67 -0
- data/examples/multiple_listeners/app/models/user/token/creation.rb +51 -0
- data/examples/multiple_listeners/app/models/user/token.rb +7 -0
- data/examples/multiple_listeners/app/models/user.rb +15 -0
- data/examples/multiple_listeners/config/boot.rb +16 -0
- data/examples/multiple_listeners/config/initializers/solid_result.rb +9 -0
- data/examples/multiple_listeners/config.rb +27 -0
- data/examples/multiple_listeners/db/setup.rb +60 -0
- data/examples/multiple_listeners/lib/event_logs_listener/stdout.rb +60 -0
- data/examples/multiple_listeners/lib/runtime_breaker.rb +11 -0
- data/examples/multiple_listeners/lib/solid/result/event_logs_record.rb +27 -0
- data/examples/multiple_listeners/lib/solid/result/rollback_on_failure.rb +15 -0
- data/examples/service_objects/Rakefile +36 -0
- data/examples/service_objects/app/models/account/member.rb +10 -0
- data/examples/service_objects/app/models/account.rb +11 -0
- data/examples/service_objects/app/models/user/token.rb +7 -0
- data/examples/service_objects/app/models/user.rb +15 -0
- data/examples/service_objects/app/services/account/owner_creation.rb +47 -0
- data/examples/service_objects/app/services/application_service.rb +79 -0
- data/examples/service_objects/app/services/user/creation.rb +56 -0
- data/examples/service_objects/app/services/user/token/creation.rb +37 -0
- data/examples/service_objects/config/boot.rb +17 -0
- data/examples/service_objects/config/initializers/solid_result.rb +9 -0
- data/examples/service_objects/config.rb +20 -0
- data/examples/service_objects/db/setup.rb +49 -0
- data/examples/single_listener/Rakefile +92 -0
- data/examples/single_listener/app/models/account/member.rb +10 -0
- data/examples/single_listener/app/models/account/owner_creation.rb +62 -0
- data/examples/single_listener/app/models/account.rb +11 -0
- data/examples/single_listener/app/models/user/creation.rb +67 -0
- data/examples/single_listener/app/models/user/token/creation.rb +51 -0
- data/examples/single_listener/app/models/user/token.rb +7 -0
- data/examples/single_listener/app/models/user.rb +15 -0
- data/examples/single_listener/config/boot.rb +16 -0
- data/examples/single_listener/config/initializers/solid_result.rb +9 -0
- data/examples/single_listener/config.rb +23 -0
- data/examples/single_listener/db/setup.rb +49 -0
- data/examples/single_listener/lib/runtime_breaker.rb +11 -0
- data/examples/single_listener/lib/single_event_logs_listener.rb +117 -0
- data/examples/single_listener/lib/solid/result/rollback_on_failure.rb +15 -0
- data/lib/solid/failure.rb +23 -0
- data/lib/solid/output/callable_and_then.rb +40 -0
- data/lib/solid/output/expectations/mixin.rb +31 -0
- data/lib/solid/output/expectations.rb +25 -0
- data/lib/solid/output/failure.rb +9 -0
- data/lib/solid/output/mixin.rb +57 -0
- data/lib/solid/output/success.rb +37 -0
- data/lib/solid/output.rb +115 -0
- data/lib/solid/result/_self.rb +198 -0
- data/lib/solid/result/callable_and_then/caller.rb +49 -0
- data/lib/solid/result/callable_and_then/config.rb +15 -0
- data/lib/solid/result/callable_and_then/error.rb +11 -0
- data/lib/solid/result/callable_and_then.rb +9 -0
- data/lib/solid/result/config/options.rb +27 -0
- data/lib/solid/result/config/switcher.rb +82 -0
- data/lib/solid/result/config/switchers/addons.rb +25 -0
- data/lib/solid/result/config/switchers/constant_aliases.rb +33 -0
- data/lib/solid/result/config/switchers/features.rb +32 -0
- data/lib/solid/result/config/switchers/pattern_matching.rb +20 -0
- data/lib/solid/result/config.rb +64 -0
- data/lib/solid/result/contract/disabled.rb +25 -0
- data/lib/solid/result/contract/error.rb +17 -0
- data/lib/solid/result/contract/evaluator.rb +45 -0
- data/lib/solid/result/contract/for_types.rb +29 -0
- data/lib/solid/result/contract/for_types_and_values.rb +46 -0
- data/lib/solid/result/contract/interface.rb +21 -0
- data/lib/solid/result/contract/type_checker.rb +37 -0
- data/lib/solid/result/contract.rb +33 -0
- data/lib/solid/result/data.rb +33 -0
- data/lib/solid/result/error.rb +59 -0
- data/lib/solid/result/event_logs/config.rb +28 -0
- data/lib/solid/result/event_logs/listener.rb +51 -0
- data/lib/solid/result/event_logs/listeners.rb +87 -0
- data/lib/solid/result/event_logs/tracking/disabled.rb +15 -0
- data/lib/solid/result/event_logs/tracking/enabled.rb +161 -0
- data/lib/solid/result/event_logs/tracking.rb +26 -0
- data/lib/solid/result/event_logs/tree.rb +141 -0
- data/lib/solid/result/event_logs.rb +27 -0
- data/lib/solid/result/expectations/mixin.rb +58 -0
- data/lib/solid/result/expectations.rb +75 -0
- data/lib/solid/result/failure.rb +11 -0
- data/lib/solid/result/handler/allowed_types.rb +45 -0
- data/lib/solid/result/handler.rb +57 -0
- data/lib/solid/result/ignored_types.rb +14 -0
- data/lib/solid/result/mixin.rb +72 -0
- data/lib/solid/result/success.rb +11 -0
- data/lib/solid/result/version.rb +7 -0
- data/lib/solid/result.rb +27 -0
- data/lib/solid/success.rb +23 -0
- data/lib/solid-result.rb +3 -0
- data/sig/solid/failure.rbs +13 -0
- data/sig/solid/output.rbs +175 -0
- data/sig/solid/result/callable_and_then.rbs +60 -0
- data/sig/solid/result/config.rbs +102 -0
- data/sig/solid/result/contract.rbs +120 -0
- data/sig/solid/result/data.rbs +16 -0
- data/sig/solid/result/error.rbs +34 -0
- data/sig/solid/result/event_logs.rbs +189 -0
- data/sig/solid/result/expectations.rbs +71 -0
- data/sig/solid/result/handler.rbs +47 -0
- data/sig/solid/result/ignored_types.rbs +9 -0
- data/sig/solid/result/mixin.rbs +45 -0
- data/sig/solid/result/version.rbs +5 -0
- data/sig/solid/result.rbs +85 -0
- data/sig/solid/success.rbs +13 -0
- metadata +167 -0
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Solid::Result
|
4
|
+
class Contract::ForTypesAndValues
|
5
|
+
include Contract::Interface
|
6
|
+
|
7
|
+
def initialize(types_and_values, config)
|
8
|
+
@nil_as_valid_value_checking =
|
9
|
+
Config::Options
|
10
|
+
.with_defaults(config, :pattern_matching)
|
11
|
+
.fetch(:nil_as_valid_value_checking)
|
12
|
+
|
13
|
+
@types_and_values = types_and_values.transform_keys(&:to_sym)
|
14
|
+
|
15
|
+
@types_contract = Contract::ForTypes.new(@types_and_values.keys)
|
16
|
+
end
|
17
|
+
|
18
|
+
def allowed_types
|
19
|
+
@types_contract.allowed_types
|
20
|
+
end
|
21
|
+
|
22
|
+
def type?(type)
|
23
|
+
@types_contract.type?(type)
|
24
|
+
end
|
25
|
+
|
26
|
+
def type!(type)
|
27
|
+
@types_contract.type!(type)
|
28
|
+
end
|
29
|
+
|
30
|
+
def type_and_value!(data)
|
31
|
+
type, value = data.type, data.value
|
32
|
+
|
33
|
+
return value if IgnoredTypes.include?(type)
|
34
|
+
|
35
|
+
value_checking = @types_and_values[type!(type)]
|
36
|
+
|
37
|
+
checking_result = value_checking === value
|
38
|
+
|
39
|
+
return value if checking_result || (checking_result.nil? && @nil_as_valid_value_checking)
|
40
|
+
|
41
|
+
raise Contract::Error::UnexpectedValue.build(type: type, value: value)
|
42
|
+
rescue ::NoMatchingPatternError => e
|
43
|
+
raise Contract::Error::UnexpectedValue.build(type: data.type, value: data.value, cause: e)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Solid::Result
|
4
|
+
module Contract::Interface
|
5
|
+
def allowed_types
|
6
|
+
raise Error::NotImplemented
|
7
|
+
end
|
8
|
+
|
9
|
+
def type?(_type)
|
10
|
+
raise Error::NotImplemented
|
11
|
+
end
|
12
|
+
|
13
|
+
def type!(_type)
|
14
|
+
raise Error::NotImplemented
|
15
|
+
end
|
16
|
+
|
17
|
+
def type_and_value!(_data)
|
18
|
+
raise Error::NotImplemented
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Solid::Result::Contract
|
4
|
+
class TypeChecker
|
5
|
+
attr_reader :result_type, :expectations
|
6
|
+
|
7
|
+
def initialize(result_type, expectations:)
|
8
|
+
@result_type = result_type
|
9
|
+
|
10
|
+
@expectations = expectations
|
11
|
+
end
|
12
|
+
|
13
|
+
def allow!(type)
|
14
|
+
expectations.type!(type)
|
15
|
+
end
|
16
|
+
|
17
|
+
def allow?(types)
|
18
|
+
validate(types, expected: expectations, allow_empty: false)
|
19
|
+
end
|
20
|
+
|
21
|
+
def allow_success?(types)
|
22
|
+
validate(types, expected: expectations.success, allow_empty: true)
|
23
|
+
end
|
24
|
+
|
25
|
+
def allow_failure?(types)
|
26
|
+
validate(types, expected: expectations.failure, allow_empty: true)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def validate(types, expected:, allow_empty:)
|
32
|
+
(allow_empty && types.empty?) || types.any? { |type| expected.type!(type) == result_type }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private_constant :TypeChecker
|
37
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Solid::Result::Contract
|
4
|
+
require_relative 'contract/error'
|
5
|
+
require_relative 'contract/type_checker'
|
6
|
+
require_relative 'contract/interface'
|
7
|
+
require_relative 'contract/evaluator'
|
8
|
+
require_relative 'contract/disabled'
|
9
|
+
require_relative 'contract/for_types'
|
10
|
+
require_relative 'contract/for_types_and_values'
|
11
|
+
|
12
|
+
NONE = Evaluator.new(Disabled, Disabled).freeze
|
13
|
+
|
14
|
+
def self.evaluate(data, contract)
|
15
|
+
contract ||= NONE
|
16
|
+
|
17
|
+
contract.type_and_value!(data)
|
18
|
+
|
19
|
+
TypeChecker.new(data.type, expectations: contract)
|
20
|
+
end
|
21
|
+
|
22
|
+
ToEnsure = ->(spec, config) do
|
23
|
+
return Disabled if spec.nil?
|
24
|
+
|
25
|
+
spec.is_a?(::Hash) ? ForTypesAndValues.new(spec, config) : ForTypes.new(Array(spec))
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.new(success:, failure:, config:)
|
29
|
+
Evaluator.new(ToEnsure[success, config], ToEnsure[failure, config])
|
30
|
+
end
|
31
|
+
|
32
|
+
private_constant :ToEnsure
|
33
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Solid::Result
|
4
|
+
class Data
|
5
|
+
attr_reader :kind, :type, :value
|
6
|
+
|
7
|
+
def initialize(kind, type, value)
|
8
|
+
@kind = kind
|
9
|
+
@type = type.to_sym
|
10
|
+
@value = value
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_h
|
14
|
+
{ kind: kind, type: type, value: value }
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_a
|
18
|
+
[kind, type, value]
|
19
|
+
end
|
20
|
+
|
21
|
+
def inspect
|
22
|
+
format(
|
23
|
+
'#<%<class_name>s kind=%<kind>p type=%<type>p value=%<value>p>',
|
24
|
+
class_name: self.class.name, kind: kind, type: type, value: value
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
alias to_ary to_a
|
29
|
+
alias to_hash to_h
|
30
|
+
end
|
31
|
+
|
32
|
+
private_constant :Data
|
33
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Solid::Result::Error < StandardError
|
4
|
+
def self.build(**_kargs)
|
5
|
+
new
|
6
|
+
end
|
7
|
+
|
8
|
+
class NotImplemented < self
|
9
|
+
end
|
10
|
+
|
11
|
+
class MissingTypeArgument < self
|
12
|
+
def initialize(_message = nil)
|
13
|
+
super('A type (argument) is required to invoke the #on/#on_type method')
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class UnexpectedOutcome < self
|
18
|
+
def self.build(outcome:, origin:, expected: nil)
|
19
|
+
expected ||= 'Solid::Result::Success or Solid::Result::Failure'
|
20
|
+
|
21
|
+
new("Unexpected outcome: #{outcome.inspect}. The #{origin} must return this object wrapped by #{expected}")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class InvalidResultSource < self
|
26
|
+
def self.build(given_result:, expected_source:)
|
27
|
+
message =
|
28
|
+
"You cannot call #and_then and return a result that does not belong to the same source!\n" \
|
29
|
+
"Expected source: #{expected_source.inspect}\n" \
|
30
|
+
"Given source: #{given_result.send(:source).inspect}\n" \
|
31
|
+
"Given result: #{given_result.inspect}"
|
32
|
+
|
33
|
+
new(message)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class InvalidSourceMethodArity < self
|
38
|
+
def self.build(source:, method:, max_arity:)
|
39
|
+
new("#{source.class}##{method.name} has unsupported arity (#{method.arity}). Expected 0..#{max_arity}")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class UnhandledTypes < self
|
44
|
+
def self.build(types:)
|
45
|
+
source = types.size == 1 ? 'This was' : 'These were'
|
46
|
+
|
47
|
+
new("You must handle all cases. #{source} not handled: #{types.map(&:inspect).join(', ')}")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class CallableAndThenDisabled < self
|
52
|
+
def initialize(_message = nil)
|
53
|
+
super(
|
54
|
+
'You cannot use #and_then! as the feature is disabled. ' \
|
55
|
+
'Please use Solid::Result.config.feature.enable!(:and_then!) to enable it.'
|
56
|
+
)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Solid::Result::EventLogs
|
4
|
+
class Config
|
5
|
+
attr_reader :listener, :trace_id
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@trace_id = -> {}
|
9
|
+
@listener = Listener::Null.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def listener=(arg)
|
13
|
+
Listener.kind?(arg) or raise ::ArgumentError, "#{arg.inspect} must be a #{Listener}"
|
14
|
+
|
15
|
+
@listener = arg
|
16
|
+
end
|
17
|
+
|
18
|
+
def trace_id=(arg)
|
19
|
+
raise ::ArgumentError, 'must be a lambda with arity 0' unless arg.is_a?(::Proc) && arg.lambda? && arg.arity.zero?
|
20
|
+
|
21
|
+
@trace_id = arg
|
22
|
+
end
|
23
|
+
|
24
|
+
@instance = new
|
25
|
+
|
26
|
+
singleton_class.send(:attr_reader, :instance)
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Solid::Result::EventLogs
|
4
|
+
module Listener
|
5
|
+
module ClassMethods
|
6
|
+
def around_event_logs?
|
7
|
+
false
|
8
|
+
end
|
9
|
+
|
10
|
+
def around_and_then?
|
11
|
+
false
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.included(base)
|
16
|
+
base.extend(ClassMethods)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.extended(base)
|
20
|
+
base.extend(ClassMethods)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.kind?(arg)
|
24
|
+
(arg.is_a?(::Class) && arg < self) || (arg.is_a?(::Module) && arg.is_a?(self)) || arg.is_a?(Listeners::Chain)
|
25
|
+
end
|
26
|
+
|
27
|
+
def on_start(scope:); end
|
28
|
+
|
29
|
+
def around_event_logs(scope:)
|
30
|
+
yield
|
31
|
+
end
|
32
|
+
|
33
|
+
def around_and_then(scope:, and_then:)
|
34
|
+
yield
|
35
|
+
end
|
36
|
+
|
37
|
+
def on_record(record:); end
|
38
|
+
|
39
|
+
def on_finish(event_logs:); end
|
40
|
+
|
41
|
+
def before_interruption(exception:, event_logs:); end
|
42
|
+
end
|
43
|
+
|
44
|
+
module Listener::Null
|
45
|
+
extend Listener
|
46
|
+
|
47
|
+
def self.new
|
48
|
+
self
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Solid::Result::EventLogs
|
4
|
+
class Listeners
|
5
|
+
class Chain
|
6
|
+
include Listener
|
7
|
+
|
8
|
+
attr_reader :listeners
|
9
|
+
|
10
|
+
def initialize(list)
|
11
|
+
if list.empty? || list.any? { !Listener.kind?(_1) }
|
12
|
+
raise ArgumentError, "listeners must be a list of #{Listener}"
|
13
|
+
end
|
14
|
+
|
15
|
+
around_and_then = list.select(&:around_and_then?)
|
16
|
+
around_event_logs = list.select(&:around_event_logs?)
|
17
|
+
|
18
|
+
raise ArgumentError, 'only one listener can have around_and_then? == true' if around_and_then.size > 1
|
19
|
+
raise ArgumentError, 'only one listener can have around_event_logs? == true' if around_event_logs.size > 1
|
20
|
+
|
21
|
+
@listeners = { list: list, around_and_then: around_and_then[0], around_event_logs: around_event_logs[0] }
|
22
|
+
end
|
23
|
+
|
24
|
+
def new
|
25
|
+
list, around_and_then, around_event_logs = listeners[:list], nil, nil
|
26
|
+
|
27
|
+
instances = list.map do |item|
|
28
|
+
instance = item.new
|
29
|
+
around_and_then = instance if listener?(:around_and_then, instance)
|
30
|
+
around_event_logs = instance if listener?(:around_event_logs, instance)
|
31
|
+
|
32
|
+
instance
|
33
|
+
end
|
34
|
+
|
35
|
+
list.one? ? list[0].new : Listeners.send(:new, instances, around_and_then, around_event_logs)
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def listener?(name, obj)
|
41
|
+
listener = listeners[name]
|
42
|
+
|
43
|
+
!listener.nil? && (obj.is_a?(listener) || obj == listener)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private_class_method :new
|
48
|
+
|
49
|
+
def self.[](*listeners)
|
50
|
+
Chain.new(listeners)
|
51
|
+
end
|
52
|
+
|
53
|
+
attr_reader :listeners, :around_and_then_listener, :around_event_logs_listener
|
54
|
+
|
55
|
+
private :listeners, :around_and_then_listener, :around_event_logs_listener
|
56
|
+
|
57
|
+
def initialize(listeners, around_and_then_listener, around_event_logs_listener)
|
58
|
+
@listeners = listeners
|
59
|
+
@around_and_then_listener = around_and_then_listener || Listener::Null
|
60
|
+
@around_event_logs_listener = around_event_logs_listener || Listener::Null
|
61
|
+
end
|
62
|
+
|
63
|
+
def on_start(scope:)
|
64
|
+
listeners.each { _1.on_start(scope: scope) }
|
65
|
+
end
|
66
|
+
|
67
|
+
def around_event_logs(scope:, &block)
|
68
|
+
around_event_logs_listener.around_event_logs(scope: scope, &block)
|
69
|
+
end
|
70
|
+
|
71
|
+
def around_and_then(scope:, and_then:, &block)
|
72
|
+
around_and_then_listener.around_and_then(scope: scope, and_then: and_then, &block)
|
73
|
+
end
|
74
|
+
|
75
|
+
def on_record(record:)
|
76
|
+
listeners.each { _1.on_record(record: record) }
|
77
|
+
end
|
78
|
+
|
79
|
+
def on_finish(event_logs:)
|
80
|
+
listeners.each { _1.on_finish(event_logs: event_logs) }
|
81
|
+
end
|
82
|
+
|
83
|
+
def before_interruption(exception:, event_logs:)
|
84
|
+
listeners.each { _1.before_interruption(exception: exception, event_logs: event_logs) }
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Solid::Result::EventLogs
|
4
|
+
module Tracking::Disabled
|
5
|
+
def self.exec(_name, _desc)
|
6
|
+
EnsureResult[yield]
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.record(result); end
|
10
|
+
|
11
|
+
def self.record_and_then(_type, _data)
|
12
|
+
yield
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Solid::Result::EventLogs
|
4
|
+
class Tracking::Enabled
|
5
|
+
attr_accessor :tree, :records, :root_started_at, :listener
|
6
|
+
|
7
|
+
private :tree, :tree=, :records, :records=, :root_started_at, :root_started_at=, :listener, :listener=
|
8
|
+
|
9
|
+
def exec(name, desc)
|
10
|
+
event_log_node, scope = start(name, desc)
|
11
|
+
|
12
|
+
result = nil
|
13
|
+
|
14
|
+
listener.around_event_logs(scope: scope) do
|
15
|
+
result = EnsureResult[yield]
|
16
|
+
end
|
17
|
+
|
18
|
+
tree.move_to_root! if event_log_node.root?
|
19
|
+
|
20
|
+
finish(result)
|
21
|
+
|
22
|
+
result
|
23
|
+
rescue ::Exception => e
|
24
|
+
err!(e, event_log_node)
|
25
|
+
end
|
26
|
+
|
27
|
+
def err!(exception, event_log_node)
|
28
|
+
if event_log_node.root?
|
29
|
+
listener.before_interruption(exception: exception, event_logs: map_event_logs)
|
30
|
+
|
31
|
+
reset!
|
32
|
+
end
|
33
|
+
|
34
|
+
raise exception
|
35
|
+
end
|
36
|
+
|
37
|
+
def reset!
|
38
|
+
self.tree = Tracking::EMPTY_TREE
|
39
|
+
end
|
40
|
+
|
41
|
+
def record(result)
|
42
|
+
return if tree.frozen?
|
43
|
+
|
44
|
+
track(result, time: ::Time.now.getutc)
|
45
|
+
end
|
46
|
+
|
47
|
+
def record_and_then(type_arg, arg)
|
48
|
+
return yield if tree.frozen?
|
49
|
+
|
50
|
+
type = type_arg.instance_of?(::Method) ? :method : type_arg
|
51
|
+
|
52
|
+
current_and_then = { type: type, arg: arg }
|
53
|
+
current_and_then[:method_name] = type_arg.name if type == :method
|
54
|
+
|
55
|
+
tree.current.value[1] = current_and_then
|
56
|
+
|
57
|
+
scope, and_then = tree.current_value
|
58
|
+
|
59
|
+
result = nil
|
60
|
+
|
61
|
+
listener.around_and_then(scope: scope, and_then: and_then) { result = yield }
|
62
|
+
|
63
|
+
result
|
64
|
+
end
|
65
|
+
|
66
|
+
def reset_and_then!
|
67
|
+
return if tree.frozen?
|
68
|
+
|
69
|
+
tree.current.value[1] = Tracking::EMPTY_HASH
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def start(name, desc)
|
75
|
+
name_and_desc = [name, desc]
|
76
|
+
|
77
|
+
tree.frozen? ? root_start(name_and_desc) : tree.insert!(name_and_desc)
|
78
|
+
|
79
|
+
scope = tree.current.value[0]
|
80
|
+
|
81
|
+
listener.on_start(scope: scope)
|
82
|
+
|
83
|
+
[tree.current, scope]
|
84
|
+
end
|
85
|
+
|
86
|
+
def finish(result)
|
87
|
+
node = tree.current
|
88
|
+
|
89
|
+
tree.move_up!
|
90
|
+
|
91
|
+
return unless node.root?
|
92
|
+
|
93
|
+
event_logs = map_event_logs
|
94
|
+
|
95
|
+
result.send(:event_logs=, event_logs)
|
96
|
+
|
97
|
+
listener.on_finish(event_logs: event_logs)
|
98
|
+
|
99
|
+
reset!
|
100
|
+
end
|
101
|
+
|
102
|
+
TreeNodeValueNormalizer = ->(id, (nam, des)) { [{ id: id, name: nam, desc: des }, Tracking::EMPTY_HASH] }
|
103
|
+
|
104
|
+
def root_start(name_and_desc)
|
105
|
+
self.root_started_at = now_in_milliseconds
|
106
|
+
|
107
|
+
self.listener = build_listener
|
108
|
+
|
109
|
+
self.records = []
|
110
|
+
|
111
|
+
self.tree = Tree.new(name_and_desc, normalizer: TreeNodeValueNormalizer)
|
112
|
+
end
|
113
|
+
|
114
|
+
def track(result, time:)
|
115
|
+
record = track_record(result, time)
|
116
|
+
|
117
|
+
records << record
|
118
|
+
|
119
|
+
listener.on_record(record: record)
|
120
|
+
|
121
|
+
record
|
122
|
+
end
|
123
|
+
|
124
|
+
def track_record(result, time)
|
125
|
+
result_data = result.data.to_h
|
126
|
+
result_data[:source] = result.send(:source)
|
127
|
+
|
128
|
+
root, = tree.root_value
|
129
|
+
parent, = tree.parent_value
|
130
|
+
current, and_then = tree.current_value
|
131
|
+
|
132
|
+
{ root: root, parent: parent, current: current, result: result_data, and_then: and_then, time: time }
|
133
|
+
end
|
134
|
+
|
135
|
+
def now_in_milliseconds
|
136
|
+
::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :millisecond)
|
137
|
+
end
|
138
|
+
|
139
|
+
def map_event_logs
|
140
|
+
duration = (now_in_milliseconds - root_started_at)
|
141
|
+
|
142
|
+
trace_id = Config.instance.trace_id.call
|
143
|
+
|
144
|
+
ids = { tree: tree.ids, matrix: tree.ids_matrix, level_parent: tree.ids_level_parent }
|
145
|
+
|
146
|
+
metadata = { duration: duration, trace_id: trace_id, ids: ids }
|
147
|
+
|
148
|
+
{ version: Tracking::VERSION, records: records, metadata: metadata }
|
149
|
+
end
|
150
|
+
|
151
|
+
def build_listener
|
152
|
+
Config.instance.listener.new
|
153
|
+
rescue ::StandardError => e
|
154
|
+
err = "#{e.message} (#{e.class}); Backtrace: #{e.backtrace&.join(', ')}"
|
155
|
+
|
156
|
+
warn("Fallback to #{Listener::Null} because registered listener raised an exception: #{err}")
|
157
|
+
|
158
|
+
Listener::Null.new
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Solid::Result
|
4
|
+
module EventLogs
|
5
|
+
module Tracking
|
6
|
+
require_relative 'tracking/enabled'
|
7
|
+
require_relative 'tracking/disabled'
|
8
|
+
|
9
|
+
VERSION = 1
|
10
|
+
|
11
|
+
EMPTY_ARRAY = [].freeze
|
12
|
+
EMPTY_HASH = {}.freeze
|
13
|
+
EMPTY_TREE = Tree.new(nil).freeze
|
14
|
+
EMPTY_IDS = { tree: EMPTY_ARRAY, matrix: EMPTY_HASH, level_parent: EMPTY_HASH }.freeze
|
15
|
+
EMPTY = {
|
16
|
+
version: VERSION,
|
17
|
+
records: EMPTY_ARRAY,
|
18
|
+
metadata: { duration: 0, ids: EMPTY_IDS, trace_id: nil }.freeze
|
19
|
+
}.freeze
|
20
|
+
|
21
|
+
def self.instance
|
22
|
+
::Solid::Result::Config.instance.feature.enabled?(:event_logs) ? Tracking::Enabled.new : Tracking::Disabled
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|