solid-result 2.0.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.
- 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
|