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.
@@ -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.
@@ -0,0 +1,38 @@
1
+ [ ![Codeship Status for ruby-hyperloop/hyper-store](https://app.codeship.com/projects/4454c560-d4ea-0134-7c96-362b4886dd22/status?branch=master)](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).
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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
@@ -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