hyper-store 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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