hyper-store 1.0.alpha1.8 → 1.0.0.lap28

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 04e678fd3317d566b6d5fc37af2e9f47a114248160c490e510439c52da2c6b7a
4
- data.tar.gz: e8a5ed58f90b3b7bf97aaa755a701b512da9177b53dc195f22a3885b78b54627
3
+ metadata.gz: 61561ae2ef17af35227e0a5c5227512567e0f3006be750477727e71fd3008e2d
4
+ data.tar.gz: ac9c2a5d2f4f7193ae73b78fd9f30a00ca5b73ac11c85f765b78792473983872
5
5
  SHA512:
6
- metadata.gz: 6da5e72a5da18eb0042e8f806cb2dcd8b5c799c8cba8e6fbed39bb80ac96a9d8bc311daa1b65de7fa185d74e983f9d47aec2d6b0bdb8e5fcc2e9a560c31bcf87
7
- data.tar.gz: 66b4f2c22194ff8888bfcb72df94523828fce8fbf1a804e64d60f6f3ecea3a0fa1180dfe7d402e6e046cf01645b204754fd2fbd523874dd36eb8618d0f1ddd76
6
+ metadata.gz: 7003f127e4551440f30587ac10207f54fd5d06a39162f327e5963cc9bcd79c9aeafa200e0040c531a875040fd4fc5ca42fc9968e18c6ee331cbdc871046c466f
7
+ data.tar.gz: 6aa28c8d48442ecc0f24264a6ea966c09e8b81b5e2b5ff57e03c724b30691b765c51f9df1a73fe84268a252df811e2dffe8ce25adb69169fe687b486e57465e0
data/.gitignore CHANGED
@@ -11,6 +11,7 @@ capybara-*.html
11
11
  **.orig
12
12
  rerun.txt
13
13
  pickle-email-*.html
14
+ Gemfile.lock
14
15
 
15
16
  # TODO Comment out these rules if you are OK with secrets being uploaded to the repo
16
17
  config/initializers/secret_token.rb
@@ -50,7 +51,3 @@ bower.json
50
51
 
51
52
  # ignore IDE files
52
53
  .idea
53
-
54
- # ignore Gemfile.locks https://yehudakatz.com/2010/12/16/clarifying-the-roles-of-the-gemspec-and-gemfile/
55
- /spec/test_app/Gemfile.lock
56
- /Gemfile.lock
data/.rubocop.yml ADDED
@@ -0,0 +1,107 @@
1
+ Style/BlockDelimiters:
2
+ EnforcedStyle: line_count_based
3
+ SupportedStyles:
4
+ # The `line_count_based` style enforces braces around single line blocks and
5
+ # do..end around multi-line blocks.
6
+ - line_count_based
7
+ # The `semantic` style enforces braces around functional blocks, where the
8
+ # primary purpose of the block is to return a value and do..end for
9
+ # procedural blocks, where the primary purpose of the block is its
10
+ # side-effects.
11
+ #
12
+ # This looks at the usage of a block's method to determine its type (e.g. is
13
+ # the result of a `map` assigned to a variable or passed to another
14
+ # method) but exceptions are permitted in the `ProceduralMethods`,
15
+ # `FunctionalMethods` and `IgnoredMethods` sections below.
16
+ - semantic
17
+ # The `braces_for_chaining` style enforces braces around single line blocks
18
+ # and do..end around multi-line blocks, except for multi-line blocks whose
19
+ # return value is being chained with another method (in which case braces
20
+ # are enforced).
21
+ - braces_for_chaining
22
+ ProceduralMethods:
23
+ # Methods that are known to be procedural in nature but look functional from
24
+ # their usage, e.g.
25
+ #
26
+ # time = Benchmark.realtime do
27
+ # foo.bar
28
+ # end
29
+ #
30
+ # Here, the return value of the block is discarded but the return value of
31
+ # `Benchmark.realtime` is used.
32
+ - benchmark
33
+ - bm
34
+ - bmbm
35
+ - create
36
+ - each_with_object
37
+ - measure
38
+ - new
39
+ - realtime
40
+ - tap
41
+ - with_object
42
+ FunctionalMethods:
43
+ # Methods that are known to be functional in nature but look procedural from
44
+ # their usage, e.g.
45
+ #
46
+ # let(:foo) { Foo.new }
47
+ #
48
+ # Here, the return value of `Foo.new` is used to define a `foo` helper but
49
+ # doesn't appear to be used from the return value of `let`.
50
+ - let
51
+ - let!
52
+ - subject
53
+ - watch
54
+ IgnoredMethods:
55
+ # Methods that can be either procedural or functional and cannot be
56
+ # categorised from their usage alone, e.g.
57
+ #
58
+ # foo = lambda do |x|
59
+ # puts "Hello, #{x}"
60
+ # end
61
+ #
62
+ # foo = lambda do |x|
63
+ # x * 100
64
+ # end
65
+ #
66
+ # Here, it is impossible to tell from the return value of `lambda` whether
67
+ # the inner block's return value is significant.
68
+ - lambda
69
+ - proc
70
+ - it
71
+
72
+ Style/Documentation:
73
+ Description: 'Document classes and non-namespace modules.'
74
+ Enabled: false
75
+ Exclude:
76
+ - 'spec/**/*'
77
+ - 'test/**/*'
78
+
79
+ # Multi-line method chaining should be done with trailing dots.
80
+ Style/DotPosition:
81
+ EnforcedStyle: leading
82
+ SupportedStyles:
83
+ - leading
84
+ - trailing
85
+
86
+ Style/FrozenStringLiteralComment:
87
+ Description: >-
88
+ Add the frozen_string_literal comment to the top of files
89
+ to help transition from Ruby 2.3.0 to Ruby 3.0.
90
+ Enabled: false
91
+
92
+ Style/MultilineBlockChain:
93
+ Description: 'Avoid multi-line chains of blocks.'
94
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#single-line-blocks'
95
+ Enabled: false
96
+
97
+ Style/MutableConstant:
98
+ Description: 'Do not assign mutable objects to constants.'
99
+ Enabled: false
100
+
101
+ Metrics/AbcSize:
102
+ # The ABC size is a calculated magnitude, so this number can be a Fixnum or
103
+ # a Float.
104
+ Max: 20
105
+
106
+ Metrics/LineLength:
107
+ Max: 100
@@ -0,0 +1,49 @@
1
+ # Contributor Code of Conduct
2
+
3
+ As contributors and maintainers of this project, and in the interest of
4
+ fostering an open and welcoming community, we pledge to respect all people who
5
+ contribute through reporting issues, posting feature requests, updating
6
+ documentation, submitting pull requests or patches, and other activities.
7
+
8
+ We are committed to making participation in this project a harassment-free
9
+ experience for everyone, regardless of level of experience, gender, gender
10
+ identity and expression, sexual orientation, disability, personal appearance,
11
+ body size, race, ethnicity, age, religion, or nationality.
12
+
13
+ Examples of unacceptable behavior by participants include:
14
+
15
+ * The use of sexualized language or imagery
16
+ * Personal attacks
17
+ * Trolling or insulting/derogatory comments
18
+ * Public or private harassment
19
+ * Publishing other's private information, such as physical or electronic
20
+ addresses, without explicit permission
21
+ * Other unethical or unprofessional conduct
22
+
23
+ Project maintainers have the right and responsibility to remove, edit, or
24
+ reject comments, commits, code, wiki edits, issues, and other contributions
25
+ that are not aligned to this Code of Conduct, or to ban temporarily or
26
+ permanently any contributor for other behaviors that they deem inappropriate,
27
+ threatening, offensive, or harmful.
28
+
29
+ By adopting this Code of Conduct, project maintainers commit themselves to
30
+ fairly and consistently applying these principles to every aspect of managing
31
+ this project. Project maintainers who do not follow or enforce the Code of
32
+ Conduct may be permanently removed from the project team.
33
+
34
+ This code of conduct applies both within project spaces and in public spaces
35
+ when an individual is representing the project or its community.
36
+
37
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
38
+ reported by contacting a project maintainer at mitch@catprint.com. All
39
+ complaints will be reviewed and investigated and will result in a response that
40
+ is deemed necessary and appropriate to the circumstances. Maintainers are
41
+ obligated to maintain confidentiality with regard to the reporter of an
42
+ incident.
43
+
44
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
45
+ version 1.3.0, available at
46
+ [http://contributor-covenant.org/version/1/3/0/][version]
47
+
48
+ [homepage]: http://contributor-covenant.org
49
+ [version]: http://contributor-covenant.org/version/1/3/0/
data/DOCS.md ADDED
@@ -0,0 +1,312 @@
1
+ # Hyperloop Stores
2
+
3
+ Hyperloop **Stores** are implemented in the **HyperStore Gem**.
4
+
5
+ Stores are where the state of your Application lives. Anything but a completely static web page will have dynamic states that change because of user inputs, the passage of time, or other external events.
6
+
7
+ ```ruby
8
+ class UserStore < Hyperloop::Store
9
+ state :current, scope: :class, reader: true
10
+
11
+ def self.set_current! user
12
+ mutate.current user
13
+ end
14
+ end
15
+
16
+ # to access the store
17
+ UserStore.set_current! user
18
+ UserStore.current_user
19
+ ```
20
+
21
+ **Stores are Ruby classes that keep the dynamic parts of the state in special state variables**
22
+
23
+ + `Hyperloop::Store::Mixin` can be mixed in to any class to turn it into a Flux Store.
24
+ + You can also create Stores by subclassing `Hyperloop::Store`.
25
+ + Stores are built out of *reactive state variables*.
26
+ + Components that *read* a Store's state will **automatically** update when the state changes.
27
+ + All of your **shared** reactive state should be Stores - *The Store is the Truth*!
28
+ + Stores can *receive* **dispatches** from *Operations*
29
+
30
+ ## Reading and Mutating States
31
+
32
+ A Store will have one or more *Reactive State Variables* or *State* for short. States are read using the `state` method, and are changed using the `mutate` method.
33
+
34
+ `state.items` reads the current value of the state named `items`. Hyperloop tracks all reads of state, and mutating those states will trigger a re-render of any Components depending on the current value.
35
+
36
+ `mutate.items` returns the current value of the state named `items`, but also tells Hyperloop that the value is changing, and that any Components depending on the current value will have to be re-rendered.
37
+
38
+ The one thing you must remember to do is use `mutate` if you intend to update the internal value of a state. For example if the state contains a hash, and you are updating the Hash's internal value you would use `mutate` otherwise the change will go unrecorded.
39
+
40
+ #### Initializing States
41
+
42
+ To assign a new value to a state use the `mutate` method and pass a parameter to the state:
43
+
44
+ ```ruby
45
+ mutate.items(Hash.new { |h, k| h[k] = 0 })
46
+ ```
47
+
48
+ #### Reading States
49
+
50
+ To read the current value of a state use the `state` method:
51
+
52
+ ```ruby
53
+ state.items # returns current value of items
54
+ ```
55
+
56
+ Typically a store will have quite a few reader (aka getter) methods that hide the details of the state, allowing the Store's implementation to change, without effecting the interface.
57
+
58
+ #### Mutating States
59
+
60
+ Often states hold data structures like arrays, hashes, sets, or other Ruby classes, which may be *mutated*. For example when you push a new value onto an array you will mutate it. The *value* of the array does not change, but its *contents* does. If you are accessing a state with the intent to change its content then use the `mutate` method:
61
+
62
+ ```ruby
63
+ mutate.items[item] = value
64
+ ```
65
+
66
+ ### Instances and Classes
67
+
68
+ Stores are often singleton classes. In an application there is one 'cart' for example.
69
+
70
+ However sometimes you will want to create a class where each instance is a Store. This is straight forward because if a state is read or mutated in an instance method, then you will be referring to that instance's copy of the state.
71
+
72
+ ```ruby
73
+ # Each UserStream provides a stream of unique user profiles.
74
+ # Each instance has a single HyperStore state variable called user
75
+ # user will contain a single hash representing the user profile.
76
+ class UserStream < Hyperloop::Store
77
+
78
+ # get another user
79
+
80
+ def get_another!
81
+ mutate.user UserStream._select_random_user
82
+ end
83
+
84
+ # extract various attributes from the user hash
85
+
86
+ def user_name
87
+ state.user[:login]
88
+ end
89
+
90
+ def user_url
91
+ state.user[:html_url]
92
+ end
93
+
94
+ def avatar
95
+ state.user[:avatar_url]
96
+ end
97
+
98
+ def initialize
99
+ get_another!
100
+ end
101
+
102
+ def self._select_random_user
103
+ # _select_random_user provides a stream of unique user profiles.
104
+ # It will either return a user profile hash, or a promise of one
105
+ # to come.
106
+
107
+ # The cache of users to choose from does not have to be an state
108
+ # variable, so we use plain instance variables.
109
+ return @users.delete_at(rand(@users.length)) unless @users.blank?
110
+ # execute the GetMoreUsers Operation to grab another batch of users
111
+ # if we are not already waiting on a promise
112
+ @promise = GetMoreUsers.then do |response|
113
+ @users = response.json
114
+ end if @promise.nil? || @promise.resolved?
115
+ # wait for the promise to resolve then try again
116
+ @promise.then { _select_random_user }
117
+ end
118
+ end
119
+
120
+ class GetMoreUsers < HyperOperation
121
+ def execute
122
+ HTTP.get("https://api.github.com/users?since=#{rand(500)}")
123
+ end
124
+ end
125
+ ```
126
+ Stores that have multiple instances will typically have instance methods that directly mutate the store. We recommend you end these methods with an exclamation (!) to make it clear you are exposing a mutator.
127
+
128
+ ### States and Promises
129
+
130
+ The above example is greatly simplified because if a promise is assigned to a state it will not mutate *until the promise resolves*. Combining this with instance Stores gives a powerful way to encapsulate system behavior.
131
+
132
+ ### Explicitly Declaring States
133
+
134
+ States like instance variables are created when they are first referenced.
135
+
136
+ As a convenience you may also explicitly declare states. This reduces code noise, and improves readability.
137
+
138
+ ```ruby
139
+ class Cart < Hyperloop::Store
140
+ state items: Hash.new { |h, k| h[k] = 0 }, scope: :class, reader: true
141
+ end
142
+ ```
143
+
144
+ This *declares* the `items` state as a class state variable, will initialize it with the hash on `Hyperloop::Boot`, and provides a reader method.
145
+ That is 6 lines of code for the price of 1, plus now the intention of `items` is clearly defined.
146
+
147
+ The `state` declaration has the following flavors, depending on how the state is to be initialized:
148
+
149
+ ```ruby
150
+ state :items, ... other options ... # items will be initialized to nil
151
+ state items: [1, 2, 3], ... other options ... # items will be initialized to the array [1, 2, 3]
152
+ state :items, ... other options ... do
153
+ ... compute initial value ...
154
+ ... context will be either the class an ...
155
+ ... instance depending on the scope ...
156
+ end
157
+ ```
158
+
159
+ Other options to the `state` declaration are:
160
+
161
+ + `scope:` either `:class`, `:instance`, `:shared`. Details below!
162
+ + `reader:` either `true`, or a symbol used to declare a reader (getter) method.
163
+ + `initializer:` either a Proc or a Symbol (indicating a method), to be used to initialize the state.
164
+
165
+ The value of the `scope` option determines where the state resides.
166
+
167
+ + A class state has one instance per class and is directly accessible in class methods, and indirectly in instances using `self.class.state`.
168
+ + An instance state has a different copy in each instance of the class, and is not accessible by class methods.
169
+ + A shared state is like a class state, but is also directly accessible in instances.
170
+
171
+ The default value for `scope:` depends on where the state is declared:
172
+
173
+ ```ruby
174
+ state :items # declares an instance state variable, each instance gets its own state
175
+ class << self
176
+ state :items # declares a class instance state variable
177
+ end
178
+ ```
179
+
180
+ In the above example there is one class instance state named `items` and an additional state variable also called
181
+ items for each instance.
182
+
183
+ The `shared` option just makes it easier to access a class state from instances.
184
+
185
+ ```ruby
186
+ class MyStore < Hyperloop::Store
187
+ state :shared_state, scope: :shared
188
+ state :class_state, scope: :class
189
+ state :instance_state # scope: :instance is default here
190
+
191
+ def instance_method
192
+ # shared state makes class states easy to access
193
+ state.shared_state
194
+ # without shared state class_state is still accessible
195
+ # with more typing
196
+ self.class.class_state
197
+ # each instance gets its own copy of instance states
198
+ state.instance_state
199
+ # attempt to access a declared state variable out of context
200
+ # results in an error!
201
+ state.class_state # exception!
202
+ end
203
+
204
+ def self.class_method
205
+ # this is the same state as was referenced in instance_method
206
+ state.shared_state
207
+ # and so is this
208
+ state.class_state
209
+ # and this will raise an exception
210
+ state.instance_state
211
+ end
212
+ ```
213
+
214
+ Class state variables are initialized by an implicit `Hyperloop::Application::Boot` receiver. If an initial value is directly provided (not via a proc, method or block) then the value will be `dup`ed when the second and following Boot dispatches are received. The proc, method or block initializers will run in the context of the class, and the state variable will be available. For example:
215
+
216
+ ```ruby
217
+ state :boot_counter, scope: :shared do
218
+ (state.boot_counter || 0)+1
219
+ end
220
+
221
+ # more practically perhaps:
222
+
223
+ state :my_state, scope: :shared do
224
+ state.my_state || [] # don't re-initialize me on reboots
225
+ end
226
+ ```
227
+
228
+ Instance variables are initialized when instances of the Store are created. Each initialization will `dup` the initial value unless supplied by a proc, method or block.
229
+
230
+ This initialization behavior will work in most cases but for more control simply leave off any initializer, and write your own.
231
+
232
+ **Note for class states there is a subtle difference between saying:**
233
+
234
+ ```ruby
235
+ state my_state: nil, scope: :shared # or :class
236
+ # and
237
+ state :my_state, scope: :shared # or :class
238
+
239
+ ```
240
+
241
+ In the first case `my_state` will be re-initialized to nil on every boot, in the second case it will not.
242
+
243
+ ## Receiving Operation Dispatches
244
+
245
+ Stores can receive Operation dispatches using the receive method.
246
+
247
+ Here is a simple shopping cart Store that receives Add, Remove and Empty Operations:
248
+
249
+ ```ruby
250
+ class Cart < Hyperloop::Store
251
+ # First we will define the two Operations.
252
+ # Because these are closely associated with the Cart
253
+ # we will name space them inside the cart.
254
+ class Add < HyperOperation
255
+ param :item
256
+ param :qty, type: Integer, min: 1
257
+ end
258
+ class Remove < HyperOperation
259
+ param :item
260
+ param :qty, type: Integer, nils: true, min: 1
261
+ end
262
+ class Empty < HyperOperation
263
+ end
264
+
265
+ # The cart's state is represented as a hash, items are the keys, qty is the value
266
+ # initialize the hash by receiving the system Hyperloop::Application::Boot or Empty dispatches
267
+
268
+ receives Hyperloop::Application::Boot, Empty do
269
+ mutate.items(Hash.new { |h, k| h[k] = 0 })
270
+ end
271
+
272
+ # The stores getter (or reader) method
273
+
274
+ def self.items
275
+ state.items
276
+ end
277
+
278
+ def self.empty?
279
+ state.items.empty?
280
+ end
281
+
282
+ receives Add do
283
+ # notice we use mutate.items since we are modifying the hash
284
+ mutate.items[params.item] += params.qty
285
+ end
286
+
287
+ receives Remove do
288
+ mutate.items[params.item] -= params.qty
289
+ # remove any items with zero qty from the cart
290
+ mutate.items.delete(params.item) if state.items[params.item] < 1
291
+ end
292
+ end
293
+ ```
294
+
295
+ This example demonstrates the two ingredients of a Store:
296
+
297
+ + Receiving Operation Dispatches and
298
+ + Reading, and Mutating *states*.
299
+
300
+ These are explained in detail below.
301
+
302
+ The `receive` method takes an list of Operations, and either a symbol (indicating a class method to call), a proc, or a block.
303
+
304
+ When the dispatch is received the method, proc, or block will be run within the context of the Store's class (not an instance.) In addition the `params` method from the Operation will be available to access the Operations parameters.
305
+
306
+ The *Flux* paradigm promotes only mutating state inside of receivers.
307
+
308
+ Hyperloop is less opinionated. You may also add mutator methods to your class. Our recommendation is that you append an exclamation (!) to methods that mutate state.
309
+
310
+ Note that it is reasonable to have several receivers for the same Operation. This allows subclassing, mixins, and separation of concerns.
311
+
312
+ Note also that the Ruby scoping rules make it very reasonable to define the Operations to be received by a Store inside the Store's scope. This does not change the semantics of either the Store or the Operation, but simply keeps the name space organized.