standard-procedure-signal 0.1.1 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +41 -27
- data/checksums/standard-procedure-signal-0.1.2.gem.sha512 +1 -0
- data/lib/standard_procedure/signal/attribute/array.rb +53 -0
- data/lib/standard_procedure/signal/attribute/boolean.rb +14 -0
- data/lib/standard_procedure/signal/attribute/date.rb +22 -0
- data/lib/standard_procedure/signal/attribute/float.rb +11 -0
- data/lib/standard_procedure/signal/attribute/format_error.rb +8 -0
- data/lib/standard_procedure/signal/attribute/hash.rb +89 -0
- data/lib/standard_procedure/signal/attribute/integer.rb +13 -0
- data/lib/standard_procedure/signal/attribute/text.rb +11 -0
- data/lib/standard_procedure/signal/attribute/time.rb +22 -0
- data/lib/standard_procedure/signal/attribute.rb +31 -0
- data/lib/standard_procedure/signal/manager.rb +32 -0
- data/lib/standard_procedure/signal/observable.rb +83 -0
- data/lib/standard_procedure/signal/observer.rb +39 -0
- data/lib/standard_procedure/signal/version.rb +7 -0
- data/lib/standard_procedure/signal.rb +74 -0
- metadata +17 -16
- data/lib/signal/attribute/array.rb +0 -51
- data/lib/signal/attribute/boolean.rb +0 -12
- data/lib/signal/attribute/date.rb +0 -20
- data/lib/signal/attribute/float.rb +0 -9
- data/lib/signal/attribute/format_error.rb +0 -6
- data/lib/signal/attribute/hash.rb +0 -87
- data/lib/signal/attribute/integer.rb +0 -11
- data/lib/signal/attribute/text.rb +0 -9
- data/lib/signal/attribute/time.rb +0 -20
- data/lib/signal/attribute.rb +0 -30
- data/lib/signal/manager.rb +0 -30
- data/lib/signal/observable.rb +0 -87
- data/lib/signal/observer.rb +0 -37
- data/lib/signal/version.rb +0 -5
- data/lib/signal.rb +0 -55
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz: '
|
3
|
+
metadata.gz: 44295f07c45ae5d41938b3cb51bd961b20e4773d5fc2608d22883e0ce260f9d0
|
4
|
+
data.tar.gz: '04385160692a3133f85e1424d6daa059df47c0c8e520cea90bd674e199f80369'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 91a28199dd1c3abe0956b9916c3c0e1131363a640ea202e825159efee813257046ef44933f1ccbea409caaead57229bae5cf06c3476c76762bbc9ca4d25baad3
|
7
|
+
data.tar.gz: a937ae13eb14feeb04f341efa0770c0288a7900ae68964d5499a154e83cb30acc659e68fd423eb0b4d2c17249c9f578b6d150215e36ef7453482d495f764ea7b
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -15,18 +15,20 @@ Finally, we define an observer (our "name badge") that simply writes the display
|
|
15
15
|
When we update the values stored in those various attributes, our "name badge" redraws itself when it has to but does nothing if it does not need to change.
|
16
16
|
|
17
17
|
```ruby
|
18
|
+
include StandardProcedure::Signal
|
19
|
+
|
18
20
|
# Define the basic attributes
|
19
|
-
first_name =
|
20
|
-
last_name =
|
21
|
-
show_full_name =
|
21
|
+
first_name = attribute.text "Alice"
|
22
|
+
last_name = attribute.text "Aardvark"
|
23
|
+
show_full_name = attribute.boolean true
|
22
24
|
|
23
25
|
# Define the composite attribute
|
24
|
-
display_name =
|
26
|
+
display_name = compute do
|
25
27
|
show_full_name.get ? "#{first_name.get} #{last_name.get}" : first_name.get
|
26
28
|
end
|
27
29
|
|
28
30
|
# Define the output that the end-user will see
|
29
|
-
|
31
|
+
observe do
|
30
32
|
puts "My name is #{display_name.get}"
|
31
33
|
end
|
32
34
|
# => My name is Alice Aardvark
|
@@ -39,7 +41,7 @@ show_full_name.set true
|
|
39
41
|
# => My name is Alice Anteater
|
40
42
|
|
41
43
|
# Perform a batch update, with no notifications until the batch is completed
|
42
|
-
|
44
|
+
update do
|
43
45
|
first_name.set "Anthony"
|
44
46
|
# no output
|
45
47
|
show_full_name.set false
|
@@ -80,7 +82,7 @@ This method (which, admittedly, is much harder to describe and to understand whe
|
|
80
82
|
- As the dependencies are discovered at the time that the observer is being built or at the time the observer is being updated, if those dependencies change, they are automatically removed or added as required
|
81
83
|
- As dependencies get removed automatically, we no longer maintain those hanging references, meaning memory will be cleaned up and garbage collected
|
82
84
|
|
83
|
-
Looking back at the example code above, you can see that our display_name attribute has either two or three dependencies. It depends on `show_full_name` and `first_name` and it may also depend on `last_name` (if `show_full_name` is true). We then add in our "name badge", using the `Signal.observe` call. This depends on `display_name` and prints to the console every time `display_name` changes.
|
85
|
+
Looking back at the example code above, you can see that our display_name attribute has either two or three dependencies. It depends on `show_full_name` and `first_name` and it may also depend on `last_name` (if `show_full_name` is true). We then add in our "name badge", using the `StandardProcedure::Signal.observe` call. This depends on `display_name` and prints to the console every time `display_name` changes.
|
84
86
|
|
85
87
|
So, when the observer is built, it prints "My name is Alice Aardvark".
|
86
88
|
|
@@ -98,26 +100,32 @@ Finally, note that all this effectively comes for free, with no additional compl
|
|
98
100
|
|
99
101
|
## Usage
|
100
102
|
|
101
|
-
All this is handled for you by the interaction between the [Signal](lib/signal.rb) and [Signal::Observable](lib/signal/observable.rb) modules and the [Signal::Observer](lib/signal/observer.rb) class. You never deal with Signal::Observers directly, as the Signal module will build one when you call `Signal.observe`.
|
103
|
+
All this is handled for you by the interaction between the [StandardProcedure::Signal](lib/standard_procedure/signal.rb) and [StandardProcedure::Signal::Observable](lib/standard_procedure/signal/observable.rb) modules and the [StandardProcedure::Signal::Observer](lib/standard_procedure/signal/observer.rb) class. You never deal with StandardProcedure::Signal::Observers directly, as the StandardProcedure::Signal module will build one when you call `StandardProcedure::Signal.observe`.
|
102
104
|
|
103
|
-
In addition, there is a concrete implementation of the Signal::Observable module that you can use directly. A [Signal::Attribute](lib/signal/attribute.rb) is an observable that stores any arbitrary object and notifies its observers when it is updated. There are also subclasses of
|
105
|
+
In addition, there is a concrete implementation of the StandardProcedure::Signal::Observable module that you can use directly. A [StandardProcedure::Signal::Attribute](lib/standard_procedure/signal/attribute.rb) is an observable that stores any arbitrary object and notifies its observers when it is updated. There are also subclasses of attribute that automatically perform type-conversions for you (`text, integer, float, date, time, boolean`).
|
104
106
|
|
105
|
-
And `Signal.compute` allows you to build composite observables which depend on multiple other observables.
|
107
|
+
And `StandardProcedure::Signal.compute` allows you to build composite observables which depend on multiple other observables.
|
106
108
|
|
107
109
|
```ruby
|
108
|
-
|
109
|
-
|
110
|
-
@
|
111
|
-
@
|
112
|
-
@
|
113
|
-
|
110
|
+
include StandardProcedure::Signal
|
111
|
+
|
112
|
+
@my_object = attribute.new MyObject.new
|
113
|
+
@my_text = attribute.text "The total is: "
|
114
|
+
@a = attribute.integer 1
|
115
|
+
@b = attribute.integer 2
|
116
|
+
@sum = compute { @a.get + @b.get }
|
117
|
+
observe do
|
114
118
|
puts "#{@my_text.get} #{@sum.get}"
|
115
119
|
end
|
116
120
|
```
|
117
121
|
|
118
|
-
To access the values stored in a `
|
122
|
+
To access the values stored in a `attribute`, you can call `attribute#get`. This is aliased as both `attribute#read` and `attribute#call` (which means you can use the short-hand `@my_attribute.()` as well).
|
123
|
+
|
124
|
+
To place a value into an attribute you call `attribute#set`, aliased as `attribute#write`.
|
125
|
+
|
126
|
+
### Extensions
|
119
127
|
|
120
|
-
|
128
|
+
Because `StandardProcedure::Signal.observe` and `StandardProcedure::Signal::Attribute` are quite long names, you can just include the [StandardProcedure::Signal module](lib/standard_procedure/signal.rb) that you can include into your classes, to give you shortcuts.
|
121
129
|
|
122
130
|
### Triggering updates
|
123
131
|
|
@@ -126,50 +134,56 @@ It's important to note that most observables only trigger updates when the `set`
|
|
126
134
|
For example:
|
127
135
|
|
128
136
|
```ruby
|
137
|
+
include StandardProcedure::Signal
|
138
|
+
|
129
139
|
# This will not trigger any updates
|
130
|
-
@attribute =
|
140
|
+
@attribute = attribute.string "hello"
|
131
141
|
@attribute.get.upcase!
|
132
142
|
|
133
143
|
# This will trigger updates
|
134
|
-
@attribute =
|
144
|
+
@attribute = attribute.string "hello"
|
135
145
|
@attribute.set @attribute.get.upcase
|
136
146
|
```
|
137
147
|
|
138
148
|
If necessary, you can manually trigger updates on an observable.
|
139
149
|
```ruby
|
150
|
+
include StandardProcedure::Signal
|
151
|
+
|
140
152
|
# Manually trigger updates
|
141
|
-
@attribute =
|
153
|
+
@attribute = attribute.string "hello"
|
142
154
|
@attribute.get.upcase!
|
143
155
|
@attribute.update_observers
|
144
156
|
```
|
145
157
|
|
146
|
-
However, there are two mutable attributes that you can use - [
|
158
|
+
However, there are two mutable attributes that you can use - [attribute::Array](/lib/standard_procedure/signal/attribute/array.rb) and [attribute::Hash](/lib/standard_procedure/signal/attribute/hash.rb).
|
147
159
|
|
148
160
|
These are partial implementations of the ruby Array and Hash classes that are convenience wrappers when it comes to updates. They implement Enumerable, so you can use `each`, `map` and your other favourites, plus they include a subset of the mutation methods to make it easier to manipulate the contents without repeatedly copying, changing and then setting your attributes contents.
|
149
161
|
|
150
162
|
```ruby
|
163
|
+
include StandardProcedure::Signal
|
164
|
+
|
151
165
|
# Non-mutable array attribute
|
152
166
|
@array = [1, 2, 3]
|
153
|
-
@attribute =
|
167
|
+
@attribute = attribute.new @array
|
154
168
|
@new_array = @array.dup
|
155
169
|
@new_array.push 4
|
156
170
|
@attribute.set @new_array
|
157
171
|
|
158
172
|
# Mutable array attribute
|
159
173
|
@array = [1, 2, 3]
|
160
|
-
@attribute =
|
174
|
+
@attribute = attribute.array @array
|
161
175
|
@attribute << 4
|
162
176
|
|
163
177
|
# Non-mutable hash attribute
|
164
178
|
@hash = { key1: "value1", key2: "value2" }
|
165
|
-
@attribute =
|
179
|
+
@attribute = attribute.new @hash
|
166
180
|
@new_hash = @hash.dup
|
167
181
|
@new_hash[:key3] = "value3"
|
168
182
|
@attribute.set @new_hash
|
169
183
|
|
170
184
|
# Mutable hash attribute
|
171
185
|
@hash = { key1: "value1", key2: "value2" }
|
172
|
-
@attribute =
|
186
|
+
@attribute = attribute.array @hash
|
173
187
|
@attribute[:key3] = "value3"
|
174
188
|
```
|
175
189
|
|
@@ -185,7 +199,7 @@ If bundler is not being used to manage dependencies, install the gem by executin
|
|
185
199
|
|
186
200
|
Then
|
187
201
|
|
188
|
-
require "signal"
|
202
|
+
require "standard_procedure/signal"
|
189
203
|
|
190
204
|
## Contributing
|
191
205
|
|
@@ -0,0 +1 @@
|
|
1
|
+
bb5307c388afc2992144df30c31e2c0b59bd555ab33f3be35557a37720deab3bf9b3a60d3ef2840f9344de3f7fd29712452ce90a0b87df6dde34b61962342d65
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module StandardProcedure
|
2
|
+
module Signal
|
3
|
+
class Attribute
|
4
|
+
class Array < Attribute
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
def set(new_value)
|
8
|
+
new_value = if new_value.nil?
|
9
|
+
nil
|
10
|
+
elsif new_value.respond_to? :map
|
11
|
+
new_value.map { |i| Attribute.for i }
|
12
|
+
else
|
13
|
+
[Attribute.new(new_value)]
|
14
|
+
end
|
15
|
+
super new_value
|
16
|
+
end
|
17
|
+
|
18
|
+
def push item
|
19
|
+
@value.push item
|
20
|
+
update_observers
|
21
|
+
self
|
22
|
+
end
|
23
|
+
alias_method :<<, :push
|
24
|
+
|
25
|
+
def pop
|
26
|
+
item = @value.pop
|
27
|
+
update_observers
|
28
|
+
item
|
29
|
+
end
|
30
|
+
|
31
|
+
def shift
|
32
|
+
item = @value.shift
|
33
|
+
update_observers
|
34
|
+
item
|
35
|
+
end
|
36
|
+
|
37
|
+
def unshift item
|
38
|
+
@value.unshift Attribute.for(item)
|
39
|
+
update_observers
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
def last
|
44
|
+
@value.last
|
45
|
+
end
|
46
|
+
|
47
|
+
def each &block
|
48
|
+
@value.each(&block)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module StandardProcedure
|
2
|
+
module Signal
|
3
|
+
class Attribute
|
4
|
+
class Boolean < Attribute
|
5
|
+
def set(new_value)
|
6
|
+
new_value = !FALSEY.include?(new_value) unless new_value.nil?
|
7
|
+
super new_value
|
8
|
+
end
|
9
|
+
|
10
|
+
FALSEY = [false, 0, "0", :"0", "f", :f, "F", :F, "false", :false, "FALSE", :FALSE, "off", :off, "OFF", :OFF].freeze # standard:disable Lint/BooleanSymbol
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require "date"
|
2
|
+
require "time"
|
3
|
+
module StandardProcedure
|
4
|
+
module Signal
|
5
|
+
class Attribute
|
6
|
+
class Date < Attribute
|
7
|
+
def set(new_value)
|
8
|
+
new_value = case new_value
|
9
|
+
when nil then nil
|
10
|
+
when ::Date then new_value
|
11
|
+
when ::Time then ::Date.new(new_value.year, new_value.month, new_value.day)
|
12
|
+
when String then ::Date.parse(new_value)
|
13
|
+
else raise "#{new_value} not recognised"
|
14
|
+
end
|
15
|
+
super new_value
|
16
|
+
rescue => e
|
17
|
+
raise FormatError, "Cannot convert #{new_value} into a date: #{e.message}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module StandardProcedure
|
2
|
+
module Signal
|
3
|
+
class Attribute
|
4
|
+
class Hash < Attribute
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
def set(new_value)
|
8
|
+
new_value = if new_value.nil?
|
9
|
+
nil
|
10
|
+
elsif new_value.respond_to? :transform_values
|
11
|
+
new_value.transform_values { |value| Attribute.for value }
|
12
|
+
else
|
13
|
+
raise ArgumentError.new "#{new_value.inspect} is not recognised as a Hash"
|
14
|
+
end
|
15
|
+
super new_value
|
16
|
+
end
|
17
|
+
|
18
|
+
def each &block
|
19
|
+
@value.each(&block)
|
20
|
+
end
|
21
|
+
|
22
|
+
def keys
|
23
|
+
@value.keys
|
24
|
+
end
|
25
|
+
|
26
|
+
def include? key
|
27
|
+
@value.include? key
|
28
|
+
end
|
29
|
+
|
30
|
+
def has_key? key
|
31
|
+
@value.has_key? key
|
32
|
+
end
|
33
|
+
|
34
|
+
def has_value? value, attribute: false
|
35
|
+
if attribute
|
36
|
+
@value.has_value? value
|
37
|
+
else
|
38
|
+
@value.values.map(&:get).include? value
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def values
|
43
|
+
@value.values
|
44
|
+
end
|
45
|
+
|
46
|
+
def size
|
47
|
+
@value.size
|
48
|
+
end
|
49
|
+
alias_method :length, :size
|
50
|
+
|
51
|
+
def any?
|
52
|
+
@value.any?
|
53
|
+
end
|
54
|
+
|
55
|
+
def empty?
|
56
|
+
@value.empty?
|
57
|
+
end
|
58
|
+
|
59
|
+
def [] key
|
60
|
+
@value[key]
|
61
|
+
end
|
62
|
+
|
63
|
+
def fetch key
|
64
|
+
@value.fetch key
|
65
|
+
end
|
66
|
+
|
67
|
+
def []= key, value
|
68
|
+
@value[key] = value
|
69
|
+
update_observers
|
70
|
+
end
|
71
|
+
|
72
|
+
def store key, value
|
73
|
+
@value.store key, value
|
74
|
+
update_observers
|
75
|
+
end
|
76
|
+
|
77
|
+
def delete key
|
78
|
+
@value.delete key
|
79
|
+
update_observers
|
80
|
+
end
|
81
|
+
|
82
|
+
def clear
|
83
|
+
@value.clear
|
84
|
+
update_observers
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require "date"
|
2
|
+
require "time"
|
3
|
+
module StandardProcedure
|
4
|
+
module Signal
|
5
|
+
class Attribute
|
6
|
+
class Time < Attribute
|
7
|
+
def set(new_value)
|
8
|
+
new_value = case new_value
|
9
|
+
when nil then nil
|
10
|
+
when ::Time then new_value
|
11
|
+
when ::Date then new_value.to_time
|
12
|
+
when String then ::Time.new(new_value)
|
13
|
+
else raise "#{new_value} not recognised"
|
14
|
+
end
|
15
|
+
super new_value
|
16
|
+
rescue => e
|
17
|
+
raise FormatError, "Cannot convert #{new_value} into a time: #{e.message}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "observable"
|
4
|
+
module StandardProcedure
|
5
|
+
module Signal
|
6
|
+
class Attribute
|
7
|
+
include Observable
|
8
|
+
Dir["#{__dir__}/attribute/*.rb"].sort.each { |file| require file }
|
9
|
+
|
10
|
+
# Create a new, untyped, Attribute, with the given value
|
11
|
+
# Alternatively, you can use the class factory methods to build an attribute that automatically performs type conversions.
|
12
|
+
# These are Attribute.text, integer, float, boolean, date, time, array, hash
|
13
|
+
def initialize(value)
|
14
|
+
set value
|
15
|
+
end
|
16
|
+
|
17
|
+
class << self
|
18
|
+
%i[text integer float date time boolean array hash].each do |type|
|
19
|
+
class_name = "StandardProcedure::Signal::Attribute::#{type.to_s.capitalize}"
|
20
|
+
define_method type do |value|
|
21
|
+
const_get(class_name).new value
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def for item
|
26
|
+
item.is_a?(StandardProcedure::Signal::Observable) ? item : Attribute.new(item)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module StandardProcedure
|
2
|
+
module Signal
|
3
|
+
module Manager
|
4
|
+
def self.update_in_progress?
|
5
|
+
@@update_in_progress ||= false
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.call_stack
|
9
|
+
@@call_stack ||= []
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.updated_observables
|
13
|
+
@@updated_observables ||= Set.new
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.start_update
|
17
|
+
@@update_in_progress = true
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.finish_update
|
21
|
+
@@update_in_progress = false
|
22
|
+
begin
|
23
|
+
updated_observables.each do |observable|
|
24
|
+
observable.update_observers
|
25
|
+
end
|
26
|
+
ensure
|
27
|
+
updated_observables.clear
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module StandardProcedure
|
2
|
+
module Signal
|
3
|
+
module Observable
|
4
|
+
include Signal
|
5
|
+
|
6
|
+
# Get this value converted to a string
|
7
|
+
# @return [String]
|
8
|
+
def to_s
|
9
|
+
@value.to_s
|
10
|
+
end
|
11
|
+
|
12
|
+
# Get the value of this signal
|
13
|
+
#
|
14
|
+
# This method is aliased as `get` and `read` so it can be accessed in whichever way makes most sense to you
|
15
|
+
# The following are all equivalent:
|
16
|
+
# @signal.call
|
17
|
+
# @signal.get
|
18
|
+
# @signal.read
|
19
|
+
# @signal.()
|
20
|
+
#
|
21
|
+
# @return [Object]
|
22
|
+
def call
|
23
|
+
if (current = call_stack.last)
|
24
|
+
observers << current
|
25
|
+
current.add observers
|
26
|
+
end
|
27
|
+
@value
|
28
|
+
end
|
29
|
+
alias_method :get, :call
|
30
|
+
alias_method :read, :call
|
31
|
+
|
32
|
+
# Set the value of this signal and notify any observers
|
33
|
+
#
|
34
|
+
# This method is aliased as `write` it can be used in whichever way makes most sense to you
|
35
|
+
# The following are all equivalent:
|
36
|
+
# @signal.set @new_value
|
37
|
+
# @signal.write @new_value
|
38
|
+
#
|
39
|
+
# @param new_value [Object]
|
40
|
+
# @return [Object]
|
41
|
+
def set(new_value)
|
42
|
+
if new_value != @value
|
43
|
+
@value = new_value
|
44
|
+
update_observers
|
45
|
+
end
|
46
|
+
@value
|
47
|
+
end
|
48
|
+
alias_method :write, :set
|
49
|
+
|
50
|
+
# Notify all observers that this signal has changed
|
51
|
+
#
|
52
|
+
# If a batch update is in progress, the observers will not be notified immediately,
|
53
|
+
# but rather when the batch is completed. Otherwise this triggers all observers
|
54
|
+
def update_observers
|
55
|
+
if update_in_progress?
|
56
|
+
updated_observables.add self
|
57
|
+
else
|
58
|
+
observers.each do |observer|
|
59
|
+
observer.call
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def observers
|
67
|
+
@observers ||= [] # contains observers
|
68
|
+
end
|
69
|
+
|
70
|
+
def call_stack
|
71
|
+
Signal::Manager.call_stack
|
72
|
+
end
|
73
|
+
|
74
|
+
def update_in_progress?
|
75
|
+
Signal::Manager.update_in_progress?
|
76
|
+
end
|
77
|
+
|
78
|
+
def updated_observables
|
79
|
+
Signal::Manager.updated_observables
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module StandardProcedure
|
2
|
+
module Signal
|
3
|
+
class Observer
|
4
|
+
def initialize &block
|
5
|
+
@block = block
|
6
|
+
@observers = Set.new # contains Sets of Observers
|
7
|
+
end
|
8
|
+
|
9
|
+
def add set_of_observers
|
10
|
+
@observers.add set_of_observers
|
11
|
+
end
|
12
|
+
|
13
|
+
def call
|
14
|
+
start
|
15
|
+
begin
|
16
|
+
@block.call
|
17
|
+
ensure
|
18
|
+
finish
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def start
|
23
|
+
@observers.each do |observer|
|
24
|
+
observer.delete self
|
25
|
+
end
|
26
|
+
@observers.clear
|
27
|
+
call_stack.push self
|
28
|
+
end
|
29
|
+
|
30
|
+
def finish
|
31
|
+
call_stack.pop
|
32
|
+
end
|
33
|
+
|
34
|
+
def call_stack
|
35
|
+
Signal::Manager.call_stack
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module StandardProcedure
|
4
|
+
module Signal
|
5
|
+
require_relative "signal/version"
|
6
|
+
require_relative "signal/manager"
|
7
|
+
require_relative "signal/observer"
|
8
|
+
require_relative "signal/observable"
|
9
|
+
require_relative "signal/attribute"
|
10
|
+
|
11
|
+
# Build a "computed" attribute that automatically updates based upon its dependents
|
12
|
+
#
|
13
|
+
# This compound address will be updated any time any of its constituent fields is updated
|
14
|
+
#
|
15
|
+
# include StandardProcedure::Signal
|
16
|
+
# @first_line = attribute.text "123 Fake Street"
|
17
|
+
# @second_line = attribute.text "Some place"
|
18
|
+
# @city = attribute.text "Springfield"
|
19
|
+
# @region = attribute.text "Who Knows"
|
20
|
+
# address = compute do
|
21
|
+
# "#{first_line.get}\n#{second_line.get}\n#{city.get}\n#{region.get}"
|
22
|
+
# end
|
23
|
+
def compute(&block)
|
24
|
+
StandardProcedure::Signal::Attribute.new(nil).tap do |attribute|
|
25
|
+
observe { attribute.set block.call }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Build an observer that gets updated whenever any of the attributes that are accessed within the block are updated
|
30
|
+
#
|
31
|
+
# This API will be posted to every time the headline or contents change
|
32
|
+
#
|
33
|
+
# include StandardProcedure::Signal
|
34
|
+
# @headline = attribute.text "Breaking news"
|
35
|
+
# @contents = attribute.text "Things have happened around the world today"
|
36
|
+
# api = Some::NewsBroadcaster.new(access_token)
|
37
|
+
# observe do
|
38
|
+
# api.post headline: @headline.get, contents: @contents.get
|
39
|
+
# end
|
40
|
+
def observe(&block)
|
41
|
+
StandardProcedure::Signal::Observer.new(&block).call
|
42
|
+
end
|
43
|
+
|
44
|
+
# Batch changes to various attributes without triggering updates till the very end
|
45
|
+
#
|
46
|
+
# include StandardProcedure::Signal
|
47
|
+
# @expensive = attribute.text "Prada"
|
48
|
+
# @operation = attribute.text "Hip Replacement"
|
49
|
+
# update do
|
50
|
+
# @expensive.set "Gucci"
|
51
|
+
# @operation.set "Brain Surgery"
|
52
|
+
# end
|
53
|
+
def update(&block)
|
54
|
+
StandardProcedure::Signal::Manager.start_update
|
55
|
+
begin
|
56
|
+
block.call
|
57
|
+
ensure
|
58
|
+
StandardProcedure::Signal::Manager.finish_update
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Build an attribute that signals its changes to dependents
|
63
|
+
# See StandardProcedure::Signal::Attribute for the different types available
|
64
|
+
#
|
65
|
+
# include StandardProcedure::Signal
|
66
|
+
# @name = attribute.text "Alice"
|
67
|
+
# @age = attribute.integer 23
|
68
|
+
# @colours = attribute.array %w[red green blue]
|
69
|
+
# @object = attribute.new @some_object
|
70
|
+
def attribute
|
71
|
+
StandardProcedure::Signal::Attribute
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: standard-procedure-signal
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rahoul Baruah
|
@@ -30,21 +30,22 @@ files:
|
|
30
30
|
- Rakefile
|
31
31
|
- checksums/standard-procedure-signal-0.1.0.gem.sha512
|
32
32
|
- checksums/standard-procedure-signal-0.1.1.gem.sha512
|
33
|
-
-
|
34
|
-
- lib/signal
|
35
|
-
- lib/signal/attribute
|
36
|
-
- lib/signal/attribute/
|
37
|
-
- lib/signal/attribute/
|
38
|
-
- lib/signal/attribute/
|
39
|
-
- lib/signal/attribute/
|
40
|
-
- lib/signal/attribute/
|
41
|
-
- lib/signal/attribute/
|
42
|
-
- lib/signal/attribute/
|
43
|
-
- lib/signal/attribute/
|
44
|
-
- lib/signal/
|
45
|
-
- lib/signal/
|
46
|
-
- lib/signal/
|
47
|
-
- lib/signal/
|
33
|
+
- checksums/standard-procedure-signal-0.1.2.gem.sha512
|
34
|
+
- lib/standard_procedure/signal.rb
|
35
|
+
- lib/standard_procedure/signal/attribute.rb
|
36
|
+
- lib/standard_procedure/signal/attribute/array.rb
|
37
|
+
- lib/standard_procedure/signal/attribute/boolean.rb
|
38
|
+
- lib/standard_procedure/signal/attribute/date.rb
|
39
|
+
- lib/standard_procedure/signal/attribute/float.rb
|
40
|
+
- lib/standard_procedure/signal/attribute/format_error.rb
|
41
|
+
- lib/standard_procedure/signal/attribute/hash.rb
|
42
|
+
- lib/standard_procedure/signal/attribute/integer.rb
|
43
|
+
- lib/standard_procedure/signal/attribute/text.rb
|
44
|
+
- lib/standard_procedure/signal/attribute/time.rb
|
45
|
+
- lib/standard_procedure/signal/manager.rb
|
46
|
+
- lib/standard_procedure/signal/observable.rb
|
47
|
+
- lib/standard_procedure/signal/observer.rb
|
48
|
+
- lib/standard_procedure/signal/version.rb
|
48
49
|
- sig/standard/procedure/attribute.rbs
|
49
50
|
homepage: https://theartandscienceofruby.com
|
50
51
|
licenses: []
|
@@ -1,51 +0,0 @@
|
|
1
|
-
module Signal
|
2
|
-
class Attribute
|
3
|
-
class Array < Attribute
|
4
|
-
include Enumerable
|
5
|
-
|
6
|
-
def set(new_value)
|
7
|
-
new_value = if new_value.nil?
|
8
|
-
nil
|
9
|
-
elsif new_value.respond_to? :map
|
10
|
-
new_value.map { |i| Attribute.for i }
|
11
|
-
else
|
12
|
-
[Attribute.new(new_value)]
|
13
|
-
end
|
14
|
-
super new_value
|
15
|
-
end
|
16
|
-
|
17
|
-
def push item
|
18
|
-
@value.push item
|
19
|
-
update_observers
|
20
|
-
self
|
21
|
-
end
|
22
|
-
alias_method :<<, :push
|
23
|
-
|
24
|
-
def pop
|
25
|
-
item = @value.pop
|
26
|
-
update_observers
|
27
|
-
item
|
28
|
-
end
|
29
|
-
|
30
|
-
def shift
|
31
|
-
item = @value.shift
|
32
|
-
update_observers
|
33
|
-
item
|
34
|
-
end
|
35
|
-
|
36
|
-
def unshift item
|
37
|
-
@value.unshift Attribute.for(item)
|
38
|
-
update_observers
|
39
|
-
self
|
40
|
-
end
|
41
|
-
|
42
|
-
def last
|
43
|
-
@value.last
|
44
|
-
end
|
45
|
-
|
46
|
-
def each &block
|
47
|
-
@value.each(&block)
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
@@ -1,12 +0,0 @@
|
|
1
|
-
module Signal
|
2
|
-
class Attribute
|
3
|
-
class Boolean < Attribute
|
4
|
-
def set(new_value)
|
5
|
-
new_value = !FALSEY.include?(new_value) unless new_value.nil?
|
6
|
-
super new_value
|
7
|
-
end
|
8
|
-
|
9
|
-
FALSEY = [false, 0, "0", :"0", "f", :f, "F", :F, "false", :false, "FALSE", :FALSE, "off", :off, "OFF", :OFF].freeze # standard:disable Lint/BooleanSymbol
|
10
|
-
end
|
11
|
-
end
|
12
|
-
end
|
@@ -1,20 +0,0 @@
|
|
1
|
-
require "date"
|
2
|
-
require "time"
|
3
|
-
module Signal
|
4
|
-
class Attribute
|
5
|
-
class Date < Attribute
|
6
|
-
def set(new_value)
|
7
|
-
new_value = case new_value
|
8
|
-
when nil then nil
|
9
|
-
when ::Date then new_value
|
10
|
-
when ::Time then ::Date.new(new_value.year, new_value.month, new_value.day)
|
11
|
-
when String then ::Date.parse(new_value)
|
12
|
-
else raise "#{new_value} not recognised"
|
13
|
-
end
|
14
|
-
super new_value
|
15
|
-
rescue => e
|
16
|
-
raise FormatError, "Cannot convert #{new_value} into a date: #{e.message}"
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
@@ -1,87 +0,0 @@
|
|
1
|
-
module Signal
|
2
|
-
class Attribute
|
3
|
-
class Hash < Attribute
|
4
|
-
include Enumerable
|
5
|
-
|
6
|
-
def set(new_value)
|
7
|
-
new_value = if new_value.nil?
|
8
|
-
nil
|
9
|
-
elsif new_value.respond_to? :transform_values
|
10
|
-
new_value.transform_values { |value| Attribute.for value }
|
11
|
-
else
|
12
|
-
raise ArgumentError.new "#{new_value.inspect} is not recognised as a Hash"
|
13
|
-
end
|
14
|
-
super new_value
|
15
|
-
end
|
16
|
-
|
17
|
-
def each &block
|
18
|
-
@value.each(&block)
|
19
|
-
end
|
20
|
-
|
21
|
-
def keys
|
22
|
-
@value.keys
|
23
|
-
end
|
24
|
-
|
25
|
-
def include? key
|
26
|
-
@value.include? key
|
27
|
-
end
|
28
|
-
|
29
|
-
def has_key? key
|
30
|
-
@value.has_key? key
|
31
|
-
end
|
32
|
-
|
33
|
-
def has_value? value, attribute: false
|
34
|
-
if attribute
|
35
|
-
@value.has_value? value
|
36
|
-
else
|
37
|
-
@value.values.map(&:get).include? value
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
def values
|
42
|
-
@value.values
|
43
|
-
end
|
44
|
-
|
45
|
-
def size
|
46
|
-
@value.size
|
47
|
-
end
|
48
|
-
alias_method :length, :size
|
49
|
-
|
50
|
-
def any?
|
51
|
-
@value.any?
|
52
|
-
end
|
53
|
-
|
54
|
-
def empty?
|
55
|
-
@value.empty?
|
56
|
-
end
|
57
|
-
|
58
|
-
def [] key
|
59
|
-
@value[key]
|
60
|
-
end
|
61
|
-
|
62
|
-
def fetch key
|
63
|
-
@value.fetch key
|
64
|
-
end
|
65
|
-
|
66
|
-
def []= key, value
|
67
|
-
@value[key] = value
|
68
|
-
update_observers
|
69
|
-
end
|
70
|
-
|
71
|
-
def store key, value
|
72
|
-
@value.store key, value
|
73
|
-
update_observers
|
74
|
-
end
|
75
|
-
|
76
|
-
def delete key
|
77
|
-
@value.delete key
|
78
|
-
update_observers
|
79
|
-
end
|
80
|
-
|
81
|
-
def clear
|
82
|
-
@value.clear
|
83
|
-
update_observers
|
84
|
-
end
|
85
|
-
end
|
86
|
-
end
|
87
|
-
end
|
@@ -1,20 +0,0 @@
|
|
1
|
-
require "date"
|
2
|
-
require "time"
|
3
|
-
module Signal
|
4
|
-
class Attribute
|
5
|
-
class Time < Attribute
|
6
|
-
def set(new_value)
|
7
|
-
new_value = case new_value
|
8
|
-
when nil then nil
|
9
|
-
when ::Time then new_value
|
10
|
-
when ::Date then new_value.to_time
|
11
|
-
when String then ::Time.new(new_value)
|
12
|
-
else raise "#{new_value} not recognised"
|
13
|
-
end
|
14
|
-
super new_value
|
15
|
-
rescue => e
|
16
|
-
raise FormatError, "Cannot convert #{new_value} into a time: #{e.message}"
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
data/lib/signal/attribute.rb
DELETED
@@ -1,30 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative "observable"
|
4
|
-
|
5
|
-
module Signal
|
6
|
-
class Attribute
|
7
|
-
include Observable
|
8
|
-
Dir["#{__dir__}/attribute/*.rb"].sort.each { |file| require file }
|
9
|
-
|
10
|
-
# Create a new, untyped, Attribute, with the given value
|
11
|
-
# Alternatively, you can use the class factory methods to build an attribute that automatically performs type conversions.
|
12
|
-
# These are Attribute.text, integer, float, boolean, date, time, array, hash
|
13
|
-
def initialize(value)
|
14
|
-
set value
|
15
|
-
end
|
16
|
-
|
17
|
-
class << self
|
18
|
-
%i[text integer float date time boolean array hash].each do |type|
|
19
|
-
class_name = "Signal::Attribute::#{type.to_s.capitalize}"
|
20
|
-
define_method type do |value|
|
21
|
-
const_get(class_name).new value
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
def for item
|
26
|
-
item.is_a?(Signal::Observable) ? item : Attribute.new(item)
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
data/lib/signal/manager.rb
DELETED
@@ -1,30 +0,0 @@
|
|
1
|
-
module Signal
|
2
|
-
module Manager
|
3
|
-
def self.update_in_progress?
|
4
|
-
@@update_in_progress ||= false
|
5
|
-
end
|
6
|
-
|
7
|
-
def self.call_stack
|
8
|
-
@@call_stack ||= []
|
9
|
-
end
|
10
|
-
|
11
|
-
def self.updated_observables
|
12
|
-
@@updated_observables ||= Set.new
|
13
|
-
end
|
14
|
-
|
15
|
-
def self.start_update
|
16
|
-
@@update_in_progress = true
|
17
|
-
end
|
18
|
-
|
19
|
-
def self.finish_update
|
20
|
-
@@update_in_progress = false
|
21
|
-
begin
|
22
|
-
updated_observables.each do |observable|
|
23
|
-
observable.update_observers
|
24
|
-
end
|
25
|
-
ensure
|
26
|
-
updated_observables.clear
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
data/lib/signal/observable.rb
DELETED
@@ -1,87 +0,0 @@
|
|
1
|
-
module Signal
|
2
|
-
module Observable
|
3
|
-
# Get this value converted to a string
|
4
|
-
# @return [String]
|
5
|
-
def to_s
|
6
|
-
@value.to_s
|
7
|
-
end
|
8
|
-
|
9
|
-
# Get the value of this signal
|
10
|
-
#
|
11
|
-
# This method is aliased as `get` and `read` so it can be accessed in whichever way makes most sense to you
|
12
|
-
# The following are all equivalent:
|
13
|
-
# @signal.call
|
14
|
-
# @signal.get
|
15
|
-
# @signal.read
|
16
|
-
# @signal.()
|
17
|
-
#
|
18
|
-
# @return [Object]
|
19
|
-
def call
|
20
|
-
if (current = call_stack.last)
|
21
|
-
observers << current
|
22
|
-
current.add observers
|
23
|
-
end
|
24
|
-
@value
|
25
|
-
end
|
26
|
-
alias_method :get, :call
|
27
|
-
alias_method :read, :call
|
28
|
-
|
29
|
-
# Set the value of this signal and notify any observers
|
30
|
-
#
|
31
|
-
# This method is aliased as `write` it can be used in whichever way makes most sense to you
|
32
|
-
# The following are all equivalent:
|
33
|
-
# @signal.set @new_value
|
34
|
-
# @signal.write @new_value
|
35
|
-
#
|
36
|
-
# @param new_value [Object]
|
37
|
-
# @return [Object]
|
38
|
-
def set(new_value)
|
39
|
-
if new_value != @value
|
40
|
-
@value = new_value
|
41
|
-
update_observers
|
42
|
-
end
|
43
|
-
@value
|
44
|
-
end
|
45
|
-
alias_method :write, :set
|
46
|
-
|
47
|
-
# Observe this signal
|
48
|
-
#
|
49
|
-
# The block will be called whenever this signal or its dependents is updated.
|
50
|
-
# The block handler does not require any parameters, simply access the signal, or any other signals and act accordingly. If you access any dependents outside of this signal, they will be tracked and you will be notified again when they update.
|
51
|
-
def observe(&block)
|
52
|
-
Signal.observe(&block)
|
53
|
-
end
|
54
|
-
|
55
|
-
# Notify all observers that this signal has changed
|
56
|
-
#
|
57
|
-
# If a batch update is in progress, the observers will not be notified immediately,
|
58
|
-
# but rather when the batch is completed. Otherwise this triggers all observers
|
59
|
-
def update_observers
|
60
|
-
if update_in_progress?
|
61
|
-
updated_observables.add self
|
62
|
-
else
|
63
|
-
observers.each do |observer|
|
64
|
-
observer.call
|
65
|
-
end
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
private
|
70
|
-
|
71
|
-
def observers
|
72
|
-
@observers ||= [] # contains observers
|
73
|
-
end
|
74
|
-
|
75
|
-
def call_stack
|
76
|
-
Signal::Manager.call_stack
|
77
|
-
end
|
78
|
-
|
79
|
-
def update_in_progress?
|
80
|
-
Signal::Manager.update_in_progress?
|
81
|
-
end
|
82
|
-
|
83
|
-
def updated_observables
|
84
|
-
Signal::Manager.updated_observables
|
85
|
-
end
|
86
|
-
end
|
87
|
-
end
|
data/lib/signal/observer.rb
DELETED
@@ -1,37 +0,0 @@
|
|
1
|
-
module Signal
|
2
|
-
class Observer
|
3
|
-
def initialize &block
|
4
|
-
@block = block
|
5
|
-
@observers = Set.new # contains Sets of Observers
|
6
|
-
end
|
7
|
-
|
8
|
-
def add set_of_observers
|
9
|
-
@observers.add set_of_observers
|
10
|
-
end
|
11
|
-
|
12
|
-
def call
|
13
|
-
start
|
14
|
-
begin
|
15
|
-
@block.call
|
16
|
-
ensure
|
17
|
-
finish
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
def start
|
22
|
-
@observers.each do |observer|
|
23
|
-
observer.delete self
|
24
|
-
end
|
25
|
-
@observers.clear
|
26
|
-
call_stack.push self
|
27
|
-
end
|
28
|
-
|
29
|
-
def finish
|
30
|
-
call_stack.pop
|
31
|
-
end
|
32
|
-
|
33
|
-
def call_stack
|
34
|
-
Signal::Manager.call_stack
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
data/lib/signal/version.rb
DELETED
data/lib/signal.rb
DELETED
@@ -1,55 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Signal
|
4
|
-
require_relative "signal/version"
|
5
|
-
require_relative "signal/manager"
|
6
|
-
require_relative "signal/observer"
|
7
|
-
require_relative "signal/observable"
|
8
|
-
require_relative "signal/attribute"
|
9
|
-
|
10
|
-
# Build a `computed` attribute that automatically updates based upon its dependents
|
11
|
-
#
|
12
|
-
# This compound address will be updated any time any of its constituent fields is updated
|
13
|
-
# @first_line = Attribute.text "123 Fake Street"
|
14
|
-
# @second_line = Attribute.text "Some place"
|
15
|
-
# @city = Attribute.text "Springfield"
|
16
|
-
# @region = Attribute.text "Who Knows"
|
17
|
-
# address = Attribute.computed do
|
18
|
-
# "#{first_line.get}\n#{second_line.get}\n#{city.get}\n#{region.get}"
|
19
|
-
# end
|
20
|
-
def self.compute(&block)
|
21
|
-
Attribute.new(nil).tap do |attribute|
|
22
|
-
observe { attribute.set block.call }
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
# Build an observer that gets updated whenever any of the attributes that are accessed within the block are updated
|
27
|
-
#
|
28
|
-
# This API will be posted to every time the headline or contents change
|
29
|
-
# @headline = Attribute.text "Breaking news"
|
30
|
-
# @contents = Attribute.text "Things have happened around the world today"
|
31
|
-
# api = Some::NewsBroadcaster.new(access_token)
|
32
|
-
# Attribute.observe do
|
33
|
-
# api.post headline: @headline.get, contents: @contents.get
|
34
|
-
# end
|
35
|
-
def self.observe(&block)
|
36
|
-
Observer.new(&block).call
|
37
|
-
end
|
38
|
-
|
39
|
-
# Batch changes to various attributes without triggering updates till the very end
|
40
|
-
#
|
41
|
-
# @expensive = Attribute.text "Prada"
|
42
|
-
# @operation = Attribute.text "Hip Replacement"
|
43
|
-
# Attribute.update do
|
44
|
-
# @expensive.set "Gucci"
|
45
|
-
# @operation.set "Brain Surgery"
|
46
|
-
# end
|
47
|
-
def self.update(&block)
|
48
|
-
Signal::Manager.start_update
|
49
|
-
begin
|
50
|
-
block.call
|
51
|
-
ensure
|
52
|
-
Signal::Manager.finish_update
|
53
|
-
end
|
54
|
-
end
|
55
|
-
end
|