hyper-store 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +46 -0
- data/.rubocop.yml +107 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +302 -0
- data/LICENSE.txt +21 -0
- data/README.md +38 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/hyper-store.gemspec +40 -0
- data/lib/hyper-store.rb +26 -0
- data/lib/hyper-store/class_methods.rb +32 -0
- data/lib/hyper-store/dispatch_receiver.rb +38 -0
- data/lib/hyper-store/instance_methods.rb +40 -0
- data/lib/hyper-store/mutator_wrapper.rb +71 -0
- data/lib/hyper-store/state_wrapper.rb +108 -0
- data/lib/hyper-store/state_wrapper/argument_validator.rb +91 -0
- data/lib/hyper-store/version.rb +3 -0
- data/lib/hyperloop/application/boot.rb +34 -0
- data/lib/hyperloop/store.rb +12 -0
- data/lib/hyperloop/store/mixin.rb +21 -0
- data/lib/react/observable.rb +29 -0
- data/lib/react/state.rb +158 -0
- metadata +293 -0
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2017 Adam Creekroad, Mitch VanDuyn
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
[ ](https://app.codeship.com/projects/202301)
|
2
|
+
|
3
|
+
## Hyper-Store 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
|
+
**Stores are Ruby classes that keep the dynamic parts of the state in special state variables**
|
8
|
+
|
9
|
+
+ `Hyperloop::Store::Mixin` can be mixed in to any class to turn it into a Flux Store.
|
10
|
+
+ You can also create Stores by subclassing `Hyperloop::Store`.
|
11
|
+
+ Stores are built out of *reactive state variables*.
|
12
|
+
+ Components that *read* a Store's state will **automatically** update when the state changes.
|
13
|
+
+ All of your **shared** reactive state should be Stores - *The Store is the Truth*!
|
14
|
+
+ Stores can *receive* **dispatches** from *Operations*
|
15
|
+
|
16
|
+
## Documentation and Help
|
17
|
+
|
18
|
+
+ Please see the [ruby-hyperloop.io](http://ruby-hyperloop.io/) website for documentation.
|
19
|
+
+ Join the Hyperloop [gitter.io](https://gitter.im/ruby-hyperloop/chat) chat for help and support.
|
20
|
+
|
21
|
+
## Basic Installation and Setup
|
22
|
+
|
23
|
+
The easiest way to install is to use the `hyper-rails` gem.
|
24
|
+
|
25
|
+
1. Add `gem 'hyper-rails'` to your Rails `Gemfile` development section.
|
26
|
+
2. Install the Gem: `bundle install`
|
27
|
+
3. Run the generator: `bundle exec rails g hyperloop:install --all`
|
28
|
+
4. Update the bundle: `bundle update`
|
29
|
+
|
30
|
+
Your Isomorphic Operations live in a `hyperloop/stores` folder.
|
31
|
+
|
32
|
+
## Contributing
|
33
|
+
|
34
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/ruby-hyperloop/hyper-store. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Code of Conduct](https://github.com/ruby-hyperloop/hyper-store/blob/master/CODE_OF_CONDUCT.md) code of conduct.
|
35
|
+
|
36
|
+
## License
|
37
|
+
|
38
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "hyper/store"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
data/hyper-store.gemspec
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'hyper-store/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'hyper-store'
|
8
|
+
spec.version = HyperStore::VERSION
|
9
|
+
spec.authors = ['catmando', 'adamcreekroad']
|
10
|
+
spec.email = ['mitch@catprint.com']
|
11
|
+
|
12
|
+
spec.summary = 'Flux Stores and more for Hyperloop'
|
13
|
+
spec.homepage = 'https://ruby-hyperloop.io'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
18
|
+
spec.bindir = 'exe'
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ['lib']
|
21
|
+
|
22
|
+
spec.add_dependency 'hyperloop-config'
|
23
|
+
spec.add_development_dependency 'bundler', '~> 1.12'
|
24
|
+
spec.add_development_dependency 'hyper-react', '>= 0.12.0'
|
25
|
+
spec.add_development_dependency 'hyper-spec'
|
26
|
+
spec.add_development_dependency 'listen'
|
27
|
+
spec.add_development_dependency 'opal'
|
28
|
+
spec.add_development_dependency 'opal-browser'
|
29
|
+
spec.add_development_dependency 'opal-rails'
|
30
|
+
spec.add_development_dependency 'pry-byebug'
|
31
|
+
spec.add_development_dependency 'rails'
|
32
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
33
|
+
spec.add_development_dependency 'react-rails', '< 1.10.0'
|
34
|
+
spec.add_development_dependency 'rspec'
|
35
|
+
spec.add_development_dependency 'rspec-steps'
|
36
|
+
spec.add_development_dependency 'sqlite3'
|
37
|
+
|
38
|
+
# Keep linter-rubocop happy
|
39
|
+
spec.add_development_dependency 'rubocop'
|
40
|
+
end
|
data/lib/hyper-store.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'hyperloop-config'
|
3
|
+
Hyperloop.require 'hyper-store', gem: true
|
4
|
+
|
5
|
+
|
6
|
+
module HyperStore # allows us to easily turn off BasicObject for debug
|
7
|
+
class BaseStoreClass < BasicObject
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
require 'hyper-store/class_methods'
|
12
|
+
require 'hyper-store/dispatch_receiver'
|
13
|
+
require 'hyper-store/instance_methods'
|
14
|
+
require 'hyper-store/mutator_wrapper'
|
15
|
+
require 'hyper-store/state_wrapper/argument_validator'
|
16
|
+
require 'hyper-store/state_wrapper'
|
17
|
+
require 'hyper-store/version'
|
18
|
+
require 'hyperloop/store'
|
19
|
+
require 'hyperloop/application/boot'
|
20
|
+
require 'hyperloop/store/mixin'
|
21
|
+
require 'react/state'
|
22
|
+
|
23
|
+
if RUBY_ENGINE != 'opal'
|
24
|
+
require 'opal'
|
25
|
+
Opal.append_path(File.expand_path('../', __FILE__).untaint)
|
26
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module HyperStore
|
2
|
+
module ClassMethods
|
3
|
+
attr_accessor :__shared_states, :__class_states, :__instance_states
|
4
|
+
|
5
|
+
def state(*args, &block)
|
6
|
+
# If we're passing in any arguments then we are calling the macro to define a state
|
7
|
+
if args.count > 0
|
8
|
+
singleton_class.__state_wrapper.class_state_wrapper
|
9
|
+
.define_state_methods(self, *args, &block)
|
10
|
+
# Otherwise we are just accessing it
|
11
|
+
else
|
12
|
+
@state ||= singleton_class.__state_wrapper.class_state_wrapper.new(self)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def mutate
|
17
|
+
@mutate ||= singleton_class.__state_wrapper.class_mutator_wrapper.new(self)
|
18
|
+
end
|
19
|
+
|
20
|
+
def __shared_states
|
21
|
+
@__shared_states ||= []
|
22
|
+
end
|
23
|
+
|
24
|
+
def __class_states
|
25
|
+
@__class_states ||= []
|
26
|
+
end
|
27
|
+
|
28
|
+
def __instance_states
|
29
|
+
@__instance_states ||= []
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module HyperStore
|
2
|
+
module DispatchReceiver
|
3
|
+
class InvalidOperationError < StandardError; end
|
4
|
+
|
5
|
+
attr_accessor :params
|
6
|
+
|
7
|
+
def receives(*args, &block)
|
8
|
+
# Format the callback to be Proc or Nil
|
9
|
+
callback = format_callback(args)
|
10
|
+
|
11
|
+
if args.empty?
|
12
|
+
message = 'At least one operation must be passed in to the \'receives\' macro'
|
13
|
+
raise InvalidOperationError, message
|
14
|
+
end
|
15
|
+
|
16
|
+
# Loop through receivers and call callback and block on dispatch
|
17
|
+
args.each do |operation|
|
18
|
+
operation.on_dispatch do |params|
|
19
|
+
@params = params
|
20
|
+
|
21
|
+
callback.call if callback
|
22
|
+
yield params if block
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def format_callback(args)
|
30
|
+
if args.last.is_a?(Symbol)
|
31
|
+
method_name = args.pop
|
32
|
+
-> { send(:"#{method_name}") }
|
33
|
+
elsif args.last.is_a?(Proc)
|
34
|
+
args.pop
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module HyperStore
|
2
|
+
module InstanceMethods
|
3
|
+
def init_store
|
4
|
+
self.class.__instance_states.each do |instance_state|
|
5
|
+
# If the scope is shared then we initialize at the class level
|
6
|
+
next if instance_state[1][:scope] == :shared
|
7
|
+
|
8
|
+
# TODO: Figure out exactly how we're going to handle passing in procs and blocks together
|
9
|
+
# But for now...just do the proc first then the block
|
10
|
+
|
11
|
+
# First initialize value from initializer Proc
|
12
|
+
proc_value = initializer_value(instance_state[1][:initializer])
|
13
|
+
mutate.__send__(:"#{instance_state[0]}", proc_value)
|
14
|
+
|
15
|
+
# Then call the block if a block is passed
|
16
|
+
next unless instance_state[1][:block]
|
17
|
+
|
18
|
+
block_value = instance_eval(&instance_state[1][:block])
|
19
|
+
mutate.__send__(:"#{instance_state[0]}", block_value)
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
def state
|
25
|
+
@state ||= self.class.singleton_class.__state_wrapper.instance_state_wrapper.new(self)
|
26
|
+
end
|
27
|
+
|
28
|
+
def mutate
|
29
|
+
@mutate ||= self.class.singleton_class.__state_wrapper.instance_mutator_wrapper.new(self)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def initializer_value(initializer)
|
35
|
+
# We gotta check the arity because a Proc passed in directly from initializer has no args,
|
36
|
+
# but if we created one then we might have wanted self
|
37
|
+
initializer.arity > 0 ? initializer.call(self) : initializer.call
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module HyperStore
|
2
|
+
class MutatorWrapper < BaseStoreClass # < BasicObject
|
3
|
+
|
4
|
+
class << self
|
5
|
+
def add_method(klass, method_name, opts = {})
|
6
|
+
define_method(:"#{method_name}") do |*args|
|
7
|
+
from = opts[:scope] == :shared ? klass.state.__from__ : __from__
|
8
|
+
current_value = React::State.get_state(from, method_name.to_s)
|
9
|
+
|
10
|
+
if args.count > 0
|
11
|
+
React::State.set_state(from, method_name.to_s, args[0])
|
12
|
+
current_value
|
13
|
+
else
|
14
|
+
React::State.set_state(from, method_name.to_s, current_value)
|
15
|
+
React::Observable.new(current_value) do |update|
|
16
|
+
React::State.set_state(from, method_name.to_s, update)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
initialize_values(klass, method_name, opts) if initialize_values?(opts)
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize_values?(opts)
|
25
|
+
[:class, :shared].include?(opts[:scope]) && (opts[:initializer] || opts[:block])
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize_values(klass, name, opts)
|
29
|
+
initializer = initializer_proc(opts[:initializer], klass, name) if opts[:initializer]
|
30
|
+
|
31
|
+
if initializer && opts[:block]
|
32
|
+
klass.receives(Hyperloop::Application::Boot, initializer) do
|
33
|
+
klass.mutate.__send__(:"#{name}", opts[:block].call)
|
34
|
+
end
|
35
|
+
elsif initializer
|
36
|
+
klass.receives(Hyperloop::Application::Boot, initializer)
|
37
|
+
elsif opts[:block]
|
38
|
+
klass.receives(Hyperloop::Application::Boot) do
|
39
|
+
klass.mutate.__send__(:"#{name}", opts[:block].call)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def initializer_proc(initializer, klass, name)
|
47
|
+
# We gotta check the arity because a Proc passed in directly from initializer has no args,
|
48
|
+
# but if we created one then we might have wanted the class
|
49
|
+
if initializer.arity > 0
|
50
|
+
-> { klass.mutate.__send__(:"#{name}", initializer.call(klass)) }
|
51
|
+
else
|
52
|
+
-> { klass.mutate.__send__(:"#{name}", initializer.call) }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
attr_accessor :__from__
|
58
|
+
|
59
|
+
def self.new(from)
|
60
|
+
instance = allocate
|
61
|
+
instance.__from__ = from
|
62
|
+
instance
|
63
|
+
end
|
64
|
+
|
65
|
+
# Any method_missing call will create a state and accessor with that name
|
66
|
+
def method_missing(name, *args, &block) # rubocop:disable Style/MethodMissing
|
67
|
+
(class << self; self end).add_method(nil, name)
|
68
|
+
__send__(name, *args, &block)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
module HyperStore
|
2
|
+
class StateWrapper < BaseStoreClass #< BasicObject # TODO StateWrapper should from basic object to avoid name space conflicts
|
3
|
+
extend ArgumentValidator
|
4
|
+
|
5
|
+
class << self
|
6
|
+
attr_reader :instance_state_wrapper, :class_state_wrapper,
|
7
|
+
:instance_mutator_wrapper, :class_mutator_wrapper,
|
8
|
+
:wrappers
|
9
|
+
|
10
|
+
def inherited(subclass)
|
11
|
+
subclass.add_class_instance_vars(subclass) if self == StateWrapper
|
12
|
+
end
|
13
|
+
|
14
|
+
def add_class_instance_vars(subclass)
|
15
|
+
@shared_state_wrapper = subclass
|
16
|
+
@instance_state_wrapper = Class.new(@shared_state_wrapper)
|
17
|
+
@class_state_wrapper = Class.new(@shared_state_wrapper)
|
18
|
+
|
19
|
+
@shared_mutator_wrapper = Class.new(MutatorWrapper)
|
20
|
+
@instance_mutator_wrapper = Class.new(@shared_mutator_wrapper)
|
21
|
+
@class_mutator_wrapper = Class.new(@shared_mutator_wrapper)
|
22
|
+
|
23
|
+
@wrappers = [@instance_state_wrapper, @instance_mutator_wrapper,
|
24
|
+
@class_state_wrapper, @class_mutator_wrapper]
|
25
|
+
end
|
26
|
+
|
27
|
+
def define_state_methods(klass, *args, &block)
|
28
|
+
return self if args.empty?
|
29
|
+
|
30
|
+
name, opts = validate_args!(klass, *args, &block)
|
31
|
+
|
32
|
+
add_readers(klass, name, opts)
|
33
|
+
klass.singleton_class.state.add_error_methods(name, opts)
|
34
|
+
klass.singleton_class.state.add_methods(klass, name, opts)
|
35
|
+
klass.singleton_class.state.remove_methods(name, opts)
|
36
|
+
klass.send(:"__#{opts[:scope]}_states") << [name, opts]
|
37
|
+
end
|
38
|
+
|
39
|
+
def add_readers(klass, name, opts)
|
40
|
+
return unless opts[:reader]
|
41
|
+
|
42
|
+
if [:instance, :shared].include?(opts[:scope])
|
43
|
+
klass.class_eval do
|
44
|
+
define_method(:"#{opts[:reader]}") { state.__send__(:"#{name}") }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
if [:class, :shared].include?(opts[:scope])
|
49
|
+
klass.define_singleton_method(:"#{opts[:reader]}") { state.__send__(:"#{name}") }
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def add_error_methods(name, opts)
|
54
|
+
return if opts[:scope] == :shared
|
55
|
+
|
56
|
+
[@shared_state_wrapper, @shared_mutator_wrapper].each do |klass|
|
57
|
+
klass.define_singleton_method(:"#{name}") do
|
58
|
+
'nope!'
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def add_methods(klass, name, opts)
|
64
|
+
instance_variable_get("@#{opts[:scope]}_state_wrapper").add_method(klass, name, opts)
|
65
|
+
instance_variable_get("@#{opts[:scope]}_mutator_wrapper").add_method(klass, name, opts)
|
66
|
+
end
|
67
|
+
|
68
|
+
def add_method(klass, method_name, opts = {})
|
69
|
+
define_method(:"#{method_name}") do
|
70
|
+
#puts "**************** args = #{args}"
|
71
|
+
from = opts[:scope] == :shared ? klass.state.__from__ : @__from__
|
72
|
+
React::State.get_state(from, method_name.to_s)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def remove_methods(name, opts)
|
77
|
+
return unless opts[:scope] == :shared
|
78
|
+
|
79
|
+
wrappers.each do |wrapper|
|
80
|
+
wrapper.send(:remove_method, :"#{name}") if wrapper.respond_to?(:"#{name}")
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def default_scope(klass)
|
85
|
+
if self == klass.singleton_class.__state_wrapper.class_state_wrapper
|
86
|
+
:instance
|
87
|
+
else
|
88
|
+
:class
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
attr_accessor :__from__
|
94
|
+
|
95
|
+
def self.new(from)
|
96
|
+
instance = allocate
|
97
|
+
instance.__from__ = from
|
98
|
+
instance
|
99
|
+
end
|
100
|
+
|
101
|
+
# Any method_missing call will create a state and accessor with that name
|
102
|
+
def method_missing(name, *args, &block) # rubocop:disable Style/MethodMissing
|
103
|
+
$method_missing = [name, *args]
|
104
|
+
(class << self; self end).add_method(nil, name) #(class << self; self end).superclass.add_method(nil, name)
|
105
|
+
__send__(name, *args, &block)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|