stable 1.19.1 → 1.20.1
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 +4 -4
- data/lib/example/stateful_calculator.rb +25 -0
- data/lib/stable/fact.rb +11 -5
- data/lib/stable/version.rb +1 -1
- data/lib/stable.rb +39 -1
- data/lib/tasks/stable.rake +4 -3
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6e15bc20c1e3075bbcf961acf641350f5a86f28d344c4cdf10714d1f81e5cedb
|
4
|
+
data.tar.gz: 6c7e38b8cdb11d438057c612b7be9c7c0509ec627ff1702d5bb0a829cd4108f5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6f713c013b885c9297b227d46b8ee5195b4d38844f042a7c6df9706e605eaec397eceeac54cb40962df2f9cccef49ec617234f7abd42956026ecbd99e6d574bd
|
7
|
+
data.tar.gz: 59902723fb85cdde62aab42c994570d4f630f4373a046e90b6f955b2b6bbd296f091c63c06a0b0ce93671bfee8e1326addf6316858b48ac3a5ed850ba5fa1817
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# lib/example/stateful_calculator.rb
|
2
|
+
|
3
|
+
# this class demonstrates a stateful calculator where operations rely on the
|
4
|
+
# value stored in the `@memory` instance variable.
|
5
|
+
class StatefulCalculator
|
6
|
+
def initialize
|
7
|
+
@memory = 0
|
8
|
+
end
|
9
|
+
|
10
|
+
def add(number)
|
11
|
+
@memory += number
|
12
|
+
end
|
13
|
+
|
14
|
+
def subtract(number)
|
15
|
+
@memory -= number
|
16
|
+
end
|
17
|
+
|
18
|
+
def clear
|
19
|
+
@memory = 0
|
20
|
+
end
|
21
|
+
|
22
|
+
def memory
|
23
|
+
@memory
|
24
|
+
end
|
25
|
+
end
|
data/lib/stable/fact.rb
CHANGED
@@ -8,9 +8,9 @@ module Stable
|
|
8
8
|
# outputs. it's a self-contained, serializable representation of a method's
|
9
9
|
# behavior at a specific point in time.
|
10
10
|
class Fact
|
11
|
-
attr_reader :class_name, :method_name, :method_type, :args, :kwargs, :result, :error, :actual_result, :actual_error, :status, :uuid, :signature, :name, :source_file
|
11
|
+
attr_reader :class_name, :method_name, :method_type, :args, :kwargs, :result, :error, :actual_result, :actual_error, :status, :uuid, :signature, :name, :source_file, :prior
|
12
12
|
|
13
|
-
def initialize(class_name:, method_name:, args:, method_type: :instance, kwargs: {}, result: nil, error: nil, uuid: SecureRandom.uuid, name: nil, source_file: nil)
|
13
|
+
def initialize(class_name:, method_name:, args:, method_type: :instance, kwargs: {}, result: nil, error: nil, uuid: SecureRandom.uuid, name: nil, source_file: nil, prior: nil)
|
14
14
|
@class_name = class_name
|
15
15
|
@method_name = method_name
|
16
16
|
@method_type = method_type
|
@@ -23,6 +23,7 @@ module Stable
|
|
23
23
|
@signature = Digest::SHA256.hexdigest("#{class_name}##{method_name}:#{args.to_json}:#{kwargs.to_json}")
|
24
24
|
@name = name || uuid.split('-').last
|
25
25
|
@source_file = source_file
|
26
|
+
@prior = prior
|
26
27
|
end
|
27
28
|
|
28
29
|
def name=(new_name)
|
@@ -31,11 +32,16 @@ module Stable
|
|
31
32
|
end
|
32
33
|
|
33
34
|
|
34
|
-
def
|
35
|
+
def verify!
|
35
36
|
begin
|
36
37
|
klass = Object.const_get(class_name)
|
37
38
|
if method_type == :instance
|
38
39
|
instance = klass.new
|
40
|
+
if prior
|
41
|
+
prior.each do |var, value|
|
42
|
+
instance.instance_variable_set(var, value)
|
43
|
+
end
|
44
|
+
end
|
39
45
|
@actual_result = instance.public_send(method_name, *args, **kwargs)
|
40
46
|
else
|
41
47
|
@actual_result = klass.public_send(method_name, *args, **kwargs)
|
@@ -82,6 +88,7 @@ module Stable
|
|
82
88
|
method_type: method_type,
|
83
89
|
args: args,
|
84
90
|
kwargs: kwargs,
|
91
|
+
prior: prior,
|
85
92
|
result: result,
|
86
93
|
error: error,
|
87
94
|
uuid: uuid,
|
@@ -98,6 +105,7 @@ module Stable
|
|
98
105
|
method_type: (data['method_type'] || :instance).to_sym,
|
99
106
|
args: data['args'],
|
100
107
|
kwargs: data['kwargs'],
|
108
|
+
prior: data['prior'],
|
101
109
|
result: data['result'],
|
102
110
|
error: data['error'],
|
103
111
|
uuid: data['uuid'],
|
@@ -105,7 +113,5 @@ module Stable
|
|
105
113
|
source_file: source_file
|
106
114
|
)
|
107
115
|
end
|
108
|
-
|
109
|
-
|
110
116
|
end
|
111
117
|
end
|
data/lib/stable/version.rb
CHANGED
data/lib/stable.rb
CHANGED
@@ -58,6 +58,34 @@ module Stable
|
|
58
58
|
@storage = nil
|
59
59
|
end
|
60
60
|
|
61
|
+
# This is the core method for observing a method on a class or module. It
|
62
|
+
# uses a dynamic module and `prepend` to intercept method calls without
|
63
|
+
# altering the original method.
|
64
|
+
#
|
65
|
+
# The design handles several complexities:
|
66
|
+
#
|
67
|
+
# 1. **Instance vs. Class Methods:** It accepts a `type` parameter to
|
68
|
+
# differentiate between instance and class methods. For class methods,
|
69
|
+
# it targets the singleton class (`klass.singleton_class`) to inject
|
70
|
+
# the wrapper.
|
71
|
+
#
|
72
|
+
# 2. **State Capture:** For instance methods, it captures the object's state
|
73
|
+
# (instance variables) *before* the method is called. This `prior` state
|
74
|
+
# is crucial for rehydrating the object during verification. State is not
|
75
|
+
# captured for class methods to prevent infinite loops, as the recording
|
76
|
+
# process itself may call class methods (e.g., `.name`).
|
77
|
+
#
|
78
|
+
# 3. **Method Binding:** It correctly handles both bound (`Method`) and
|
79
|
+
# unbound (`UnboundMethod`) method objects, ensuring `self` is correctly
|
80
|
+
# bound when the original method is eventually called.
|
81
|
+
#
|
82
|
+
# 4. **Fact Creation:** It gathers all relevant data—class name, method name,
|
83
|
+
# arguments, prior state, and the result or error—into a `Fact` object.
|
84
|
+
#
|
85
|
+
# 5. **Duplicate Prevention:** It generates a signature for each potential
|
86
|
+
# fact and checks if a fact with the same signature has already been
|
87
|
+
# recorded to prevent creating duplicate entries.
|
88
|
+
#
|
61
89
|
def watch(klass, method_name, type: :instance)
|
62
90
|
original_method = type == :instance ? klass.instance_method(method_name) : klass.method(method_name)
|
63
91
|
target = type == :instance ? klass : klass.singleton_class
|
@@ -66,6 +94,7 @@ module Stable
|
|
66
94
|
define_method(method_name) do |*args, **kwargs, &block|
|
67
95
|
if Stable.enabled?
|
68
96
|
begin
|
97
|
+
prior = type == :instance ? Stable.send(:_capture_state, self) : nil
|
69
98
|
result = original_method.is_a?(UnboundMethod) ? original_method.bind(self).call(*args, **kwargs, &block) : original_method.call(*args, **kwargs, &block)
|
70
99
|
fact = Fact.new(
|
71
100
|
class_name: klass.name,
|
@@ -73,6 +102,7 @@ module Stable
|
|
73
102
|
method_type: type,
|
74
103
|
args: args,
|
75
104
|
kwargs: kwargs,
|
105
|
+
prior: prior,
|
76
106
|
result: result
|
77
107
|
)
|
78
108
|
unless Stable.send(:_fact_exists?, fact.signature)
|
@@ -82,12 +112,14 @@ module Stable
|
|
82
112
|
end
|
83
113
|
result
|
84
114
|
rescue => e
|
115
|
+
prior = type == :instance ? Stable.send(:_capture_state, self) : nil
|
85
116
|
fact = Fact.new(
|
86
117
|
class_name: klass.name,
|
87
118
|
method_name: method_name,
|
88
119
|
method_type: type,
|
89
120
|
args: args,
|
90
121
|
kwargs: kwargs,
|
122
|
+
prior: prior,
|
91
123
|
error: {
|
92
124
|
class: e.class.name,
|
93
125
|
message: e.message,
|
@@ -122,7 +154,7 @@ module Stable
|
|
122
154
|
end
|
123
155
|
|
124
156
|
def verify(record_hash)
|
125
|
-
Fact.from_jsonl(record_hash.to_json).
|
157
|
+
Fact.from_jsonl(record_hash.to_json).verify!
|
126
158
|
end
|
127
159
|
|
128
160
|
private
|
@@ -140,5 +172,11 @@ module Stable
|
|
140
172
|
def _fact_exists?(signature)
|
141
173
|
_recorded_facts.any? { |fact| fact.signature == signature }
|
142
174
|
end
|
175
|
+
|
176
|
+
def _capture_state(obj)
|
177
|
+
obj.instance_variables.each_with_object({}) do |var, hash|
|
178
|
+
hash[var] = obj.instance_variable_get(var)
|
179
|
+
end
|
180
|
+
end
|
143
181
|
end
|
144
182
|
end
|
data/lib/tasks/stable.rake
CHANGED
@@ -8,6 +8,7 @@ namespace :stable do
|
|
8
8
|
require_relative '../../lib/example/calculator'
|
9
9
|
require_relative '../../lib/example/kw_calculator'
|
10
10
|
require_relative '../../lib/example/class_methods'
|
11
|
+
require_relative '../../lib/example/stateful_calculator'
|
11
12
|
|
12
13
|
fact_path = File.expand_path('../../../facts/calculator.fact.example', __FILE__)
|
13
14
|
Stable.configure do |config|
|
@@ -23,7 +24,7 @@ namespace :stable do
|
|
23
24
|
puts formatter.header
|
24
25
|
|
25
26
|
_filter_facts(facts, args[:filter].to_s.strip.downcase).each do |fact|
|
26
|
-
fact.
|
27
|
+
fact.verify!
|
27
28
|
puts formatter.to_s(fact)
|
28
29
|
end
|
29
30
|
|
@@ -43,7 +44,7 @@ namespace :stable do
|
|
43
44
|
puts formatter.header
|
44
45
|
|
45
46
|
facts.each do |fact|
|
46
|
-
fact.
|
47
|
+
fact.verify!
|
47
48
|
puts formatter.to_s(fact)
|
48
49
|
end
|
49
50
|
|
@@ -96,7 +97,7 @@ namespace :stable do
|
|
96
97
|
|
97
98
|
updated_facts = []
|
98
99
|
_filter_facts(facts, _clean_filter(args[:filter])).each do |fact|
|
99
|
-
fact.
|
100
|
+
fact.verify!
|
100
101
|
if fact.status == :failed
|
101
102
|
puts formatter.to_s(fact)
|
102
103
|
print " update this fact? (y/n): "
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: stable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.20.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jeff Lunt
|
@@ -32,6 +32,7 @@ files:
|
|
32
32
|
- lib/example/calculator.rb
|
33
33
|
- lib/example/class_methods.rb
|
34
34
|
- lib/example/kw_calculator.rb
|
35
|
+
- lib/example/stateful_calculator.rb
|
35
36
|
- lib/stable.rb
|
36
37
|
- lib/stable/configuration.rb
|
37
38
|
- lib/stable/fact.rb
|