standard-procedure-signal 0.1.1 → 0.1.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +4 -0
  3. data/README.md +41 -27
  4. data/checksums/standard-procedure-signal-0.1.2.1.gem.sha512 +1 -0
  5. data/checksums/standard-procedure-signal-0.1.2.gem.sha512 +1 -0
  6. data/lib/standard_procedure/signal/attribute/array.rb +53 -0
  7. data/lib/standard_procedure/signal/attribute/boolean.rb +14 -0
  8. data/lib/standard_procedure/signal/attribute/date.rb +22 -0
  9. data/lib/standard_procedure/signal/attribute/float.rb +11 -0
  10. data/lib/standard_procedure/signal/attribute/format_error.rb +8 -0
  11. data/lib/standard_procedure/signal/attribute/hash.rb +89 -0
  12. data/lib/standard_procedure/signal/attribute/integer.rb +13 -0
  13. data/lib/standard_procedure/signal/attribute/text.rb +11 -0
  14. data/lib/standard_procedure/signal/attribute/time.rb +22 -0
  15. data/lib/standard_procedure/signal/attribute.rb +39 -0
  16. data/lib/standard_procedure/signal/manager.rb +34 -0
  17. data/lib/standard_procedure/signal/observable.rb +85 -0
  18. data/lib/standard_procedure/signal/observer.rb +41 -0
  19. data/lib/standard_procedure/signal/version.rb +7 -0
  20. data/lib/standard_procedure/signal.rb +74 -0
  21. metadata +18 -16
  22. data/lib/signal/attribute/array.rb +0 -51
  23. data/lib/signal/attribute/boolean.rb +0 -12
  24. data/lib/signal/attribute/date.rb +0 -20
  25. data/lib/signal/attribute/float.rb +0 -9
  26. data/lib/signal/attribute/format_error.rb +0 -6
  27. data/lib/signal/attribute/hash.rb +0 -87
  28. data/lib/signal/attribute/integer.rb +0 -11
  29. data/lib/signal/attribute/text.rb +0 -9
  30. data/lib/signal/attribute/time.rb +0 -20
  31. data/lib/signal/attribute.rb +0 -30
  32. data/lib/signal/manager.rb +0 -30
  33. data/lib/signal/observable.rb +0 -87
  34. data/lib/signal/observer.rb +0 -37
  35. data/lib/signal/version.rb +0 -5
  36. data/lib/signal.rb +0 -55
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c347178dc2149032ef830dc559022331c72955415a2bf989e12460a0f3114460
4
- data.tar.gz: '09db2d1f954710ce4f4eb277dd950f231d93db17ba660d3d95861c7c68bb9c74'
3
+ metadata.gz: 62ddde59890dc0a13e2704932107e8b25bd33fabf5a48a2100a97cf7c3cbff10
4
+ data.tar.gz: 31233250f1bc23143b6ef40b40081cd0b31da06ced7b4a1cbc39ef5cf12d7fad
5
5
  SHA512:
6
- metadata.gz: a0ad515217b9f1c5db1c179fb3b0ae9ff7320299e56d1eec1cb038ddd44f2fd31b96d60318af1c39a6a1155764633ebc03d0e2ae6471b0da86ef386fc4e78d0c
7
- data.tar.gz: 604b0ed2905965347e560071cd171b817f3a75882da9ad81f518f8e86038cb676da1f2263fa07bfda8f7958567ed1a62080d7a2406949cca716392f6446beb40
6
+ metadata.gz: 0a7786dad91fcc094e1b98362c9644502e82a35aae9b09cc93abea6b82993e2487c6ba6be30519ae9f8581119bc359ae7b01e3231aaf56cd23d46c44ad6c5c7b
7
+ data.tar.gz: c14bc7e66a1f1ca678e2295c88bb9aba9503c8ebe7910606d5f85223b4edf2eca19008396cafde904f2010357c2d10a9a69f802a501ce0f1ec57753fcf3293ae
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## [0.1.2] - 2023-09-13
2
+
3
+ - Added the StandardProcedure namespace, as ::Signal is used by Ruby itself
4
+
1
5
  ## [0.1.1] - 2023-09-13
2
6
 
3
7
  - Added in [Signal::Attribute::Array](/lib/signal/attribute/array.rb) and [Signal::Attribute::Hash](/lib/signal/attribute/hash.rb)
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 = Signal::Attribute.text "Alice"
20
- last_name = Signal::Attribute.text "Aardvark"
21
- show_full_name = Signal::Attribute.boolean true
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 = Signal.compute do
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
- display_name.observe do
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
- Signal.update do
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 Signal::Attribute that automatically perform type-conversions for you (`text, integer, float, date, time, boolean`).
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
- @my_object = Signal::Attribute.new MyObject.new
109
- @my_text = Signal::Attribute.text "The total is: "
110
- @a = Signal::Attribute.integer 1
111
- @b = Signal::Attribute.integer 2
112
- @sum = Signal.compute { @a.get + @b.get }
113
- Signal.observe do
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 `Signal::Attribute`, you can call `Signal::Attribute#get`. This is aliased as both `Signal::Attribute#read` and `Signal::Attribute#call` (which means you can use the short-hand `@my_attribute.()` as well).
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
- To place a value into an attribute you call `Signal::Attribute#set`, aliased as `Signal::Attribute#write`.
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 = Signal::Attribute.string "hello"
140
+ @attribute = attribute.string "hello"
131
141
  @attribute.get.upcase!
132
142
 
133
143
  # This will trigger updates
134
- @attribute = Signal::Attribute.string "hello"
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 = Signal::Attribute.string "hello"
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 - [Signal::Attribute::Array](/lib/signal/attribute/array.rb) and [Signal::Attribute::Hash](/lib/signal/attribute/hash.rb).
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 = Signal::Attribute.new @array
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 = Signal::Attribute.array @array
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 = Signal::Attribute.new @hash
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 = Signal::Attribute.array @hash
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
+ c5a5dd921fe67d5faa376542bd566d2802d5e0389306f0e149d82a9b55b957f0270d051243e02a5fab2cca43463586408f9d1bfe707659f8135e6b20da609860
@@ -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,11 @@
1
+ module StandardProcedure
2
+ module Signal
3
+ class Attribute
4
+ class Float < Attribute
5
+ def set(new_value)
6
+ super new_value&.to_f
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,8 @@
1
+ module StandardProcedure
2
+ module Signal
3
+ class Attribute
4
+ class FormatError < RuntimeError
5
+ end
6
+ end
7
+ end
8
+ 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,13 @@
1
+ module StandardProcedure
2
+ module Signal
3
+ class Attribute
4
+ class Integer < Attribute
5
+ def set(new_value)
6
+ super new_value&.to_i
7
+ rescue
8
+ raise FormatError, "Cannot convert #{new_value} to integer"
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,11 @@
1
+ module StandardProcedure
2
+ module Signal
3
+ class Attribute
4
+ class Text < Attribute
5
+ def set(new_value)
6
+ super new_value&.to_s
7
+ end
8
+ end
9
+ end
10
+ end
11
+ 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,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "observable"
4
+ module StandardProcedure
5
+ module Signal
6
+ class Attribute
7
+ include Observable
8
+ require_relative "attribute/format_error"
9
+ require_relative "attribute/array"
10
+ require_relative "attribute/boolean"
11
+ require_relative "attribute/date"
12
+ require_relative "attribute/float"
13
+ require_relative "attribute/hash"
14
+ require_relative "attribute/integer"
15
+ require_relative "attribute/text"
16
+ require_relative "attribute/time"
17
+
18
+ # Create a new, untyped, Attribute, with the given value
19
+ # Alternatively, you can use the class factory methods to build an attribute that automatically performs type conversions.
20
+ # These are Attribute.text, integer, float, boolean, date, time, array, hash
21
+ def initialize(value)
22
+ set value
23
+ end
24
+
25
+ class << self
26
+ %i[text integer float date time boolean array hash].each do |type|
27
+ class_name = "StandardProcedure::Signal::Attribute::#{type.to_s.capitalize}"
28
+ define_method type do |value|
29
+ const_get(class_name).new value
30
+ end
31
+ end
32
+
33
+ def for item
34
+ item.is_a?(StandardProcedure::Signal::Observable) ? item : Attribute.new(item)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StandardProcedure
4
+ module Signal
5
+ module Manager
6
+ def self.update_in_progress?
7
+ @@update_in_progress ||= false
8
+ end
9
+
10
+ def self.call_stack
11
+ @@call_stack ||= []
12
+ end
13
+
14
+ def self.updated_observables
15
+ @@updated_observables ||= Set.new
16
+ end
17
+
18
+ def self.start_update
19
+ @@update_in_progress = true
20
+ end
21
+
22
+ def self.finish_update
23
+ @@update_in_progress = false
24
+ begin
25
+ updated_observables.each do |observable|
26
+ observable.update_observers
27
+ end
28
+ ensure
29
+ updated_observables.clear
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StandardProcedure
4
+ module Signal
5
+ module Observable
6
+ include Signal
7
+
8
+ # Get this value converted to a string
9
+ # @return [String]
10
+ def to_s
11
+ @value.to_s
12
+ end
13
+
14
+ # Get the value of this signal
15
+ #
16
+ # This method is aliased as `get` and `read` so it can be accessed in whichever way makes most sense to you
17
+ # The following are all equivalent:
18
+ # @signal.call
19
+ # @signal.get
20
+ # @signal.read
21
+ # @signal.()
22
+ #
23
+ # @return [Object]
24
+ def call
25
+ if (current = call_stack.last)
26
+ observers << current
27
+ current.add observers
28
+ end
29
+ @value
30
+ end
31
+ alias_method :get, :call
32
+ alias_method :read, :call
33
+
34
+ # Set the value of this signal and notify any observers
35
+ #
36
+ # This method is aliased as `write` it can be used in whichever way makes most sense to you
37
+ # The following are all equivalent:
38
+ # @signal.set @new_value
39
+ # @signal.write @new_value
40
+ #
41
+ # @param new_value [Object]
42
+ # @return [Object]
43
+ def set(new_value)
44
+ if new_value != @value
45
+ @value = new_value
46
+ update_observers
47
+ end
48
+ @value
49
+ end
50
+ alias_method :write, :set
51
+
52
+ # Notify all observers that this signal has changed
53
+ #
54
+ # If a batch update is in progress, the observers will not be notified immediately,
55
+ # but rather when the batch is completed. Otherwise this triggers all observers
56
+ def update_observers
57
+ if update_in_progress?
58
+ updated_observables.add self
59
+ else
60
+ observers.each do |observer|
61
+ observer.call
62
+ end
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ def observers
69
+ @observers ||= [] # contains observers
70
+ end
71
+
72
+ def call_stack
73
+ Signal::Manager.call_stack
74
+ end
75
+
76
+ def update_in_progress?
77
+ Signal::Manager.update_in_progress?
78
+ end
79
+
80
+ def updated_observables
81
+ Signal::Manager.updated_observables
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StandardProcedure
4
+ module Signal
5
+ class Observer
6
+ def initialize &block
7
+ @block = block
8
+ @observers = Set.new # contains Sets of Observers
9
+ end
10
+
11
+ def add set_of_observers
12
+ @observers.add set_of_observers
13
+ end
14
+
15
+ def call
16
+ start
17
+ begin
18
+ @block.call
19
+ ensure
20
+ finish
21
+ end
22
+ end
23
+
24
+ def start
25
+ @observers.each do |observer|
26
+ observer.delete self
27
+ end
28
+ @observers.clear
29
+ call_stack.push self
30
+ end
31
+
32
+ def finish
33
+ call_stack.pop
34
+ end
35
+
36
+ def call_stack
37
+ Signal::Manager.call_stack
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StandardProcedure
4
+ module Signal
5
+ VERSION = "0.1.2.1"
6
+ end
7
+ 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.1
4
+ version: 0.1.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rahoul Baruah
@@ -30,21 +30,23 @@ 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
- - lib/signal.rb
34
- - lib/signal/attribute.rb
35
- - lib/signal/attribute/array.rb
36
- - lib/signal/attribute/boolean.rb
37
- - lib/signal/attribute/date.rb
38
- - lib/signal/attribute/float.rb
39
- - lib/signal/attribute/format_error.rb
40
- - lib/signal/attribute/hash.rb
41
- - lib/signal/attribute/integer.rb
42
- - lib/signal/attribute/text.rb
43
- - lib/signal/attribute/time.rb
44
- - lib/signal/manager.rb
45
- - lib/signal/observable.rb
46
- - lib/signal/observer.rb
47
- - lib/signal/version.rb
33
+ - checksums/standard-procedure-signal-0.1.2.1.gem.sha512
34
+ - checksums/standard-procedure-signal-0.1.2.gem.sha512
35
+ - lib/standard_procedure/signal.rb
36
+ - lib/standard_procedure/signal/attribute.rb
37
+ - lib/standard_procedure/signal/attribute/array.rb
38
+ - lib/standard_procedure/signal/attribute/boolean.rb
39
+ - lib/standard_procedure/signal/attribute/date.rb
40
+ - lib/standard_procedure/signal/attribute/float.rb
41
+ - lib/standard_procedure/signal/attribute/format_error.rb
42
+ - lib/standard_procedure/signal/attribute/hash.rb
43
+ - lib/standard_procedure/signal/attribute/integer.rb
44
+ - lib/standard_procedure/signal/attribute/text.rb
45
+ - lib/standard_procedure/signal/attribute/time.rb
46
+ - lib/standard_procedure/signal/manager.rb
47
+ - lib/standard_procedure/signal/observable.rb
48
+ - lib/standard_procedure/signal/observer.rb
49
+ - lib/standard_procedure/signal/version.rb
48
50
  - sig/standard/procedure/attribute.rbs
49
51
  homepage: https://theartandscienceofruby.com
50
52
  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,9 +0,0 @@
1
- module Signal
2
- class Attribute
3
- class Float < Attribute
4
- def set(new_value)
5
- super new_value&.to_f
6
- end
7
- end
8
- end
9
- end
@@ -1,6 +0,0 @@
1
- module Signal
2
- class Attribute
3
- class FormatError < RuntimeError
4
- end
5
- end
6
- 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,11 +0,0 @@
1
- module Signal
2
- class Attribute
3
- class Integer < Attribute
4
- def set(new_value)
5
- super new_value&.to_i
6
- rescue
7
- raise FormatError, "Cannot convert #{new_value} to integer"
8
- end
9
- end
10
- end
11
- end
@@ -1,9 +0,0 @@
1
- module Signal
2
- class Attribute
3
- class Text < Attribute
4
- def set(new_value)
5
- super new_value&.to_s
6
- end
7
- end
8
- end
9
- 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -1,5 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Signal
4
- VERSION = "0.1.1"
5
- end
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