ovto 0.5.0 → 0.6.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/.gitmodules +3 -0
  3. data/CHANGELOG.md +14 -2
  4. data/Gemfile +2 -3
  5. data/Gemfile.lock +23 -27
  6. data/book/SUMMARY.md +2 -0
  7. data/book/api/actions.md +1 -1
  8. data/book/api/middleware.md +99 -0
  9. data/book/api/pure_component.md +3 -0
  10. data/docs/api/Array.html +190 -0
  11. data/docs/api/Hash.html +196 -0
  12. data/docs/api/Ovto/Actions.html +128 -40
  13. data/docs/api/Ovto/App.html +254 -40
  14. data/docs/api/Ovto/Component/MoreThanOneNode.html +6 -6
  15. data/docs/api/Ovto/Component.html +96 -29
  16. data/docs/api/Ovto/Middleware/Actions.html +428 -0
  17. data/docs/api/Ovto/Middleware/Base.html +605 -0
  18. data/docs/api/Ovto/Middleware/Component.html +354 -0
  19. data/docs/api/Ovto/Middleware.html +286 -0
  20. data/docs/api/Ovto/PureComponent/StateIsNotAvailable.html +6 -6
  21. data/docs/api/Ovto/PureComponent.html +11 -11
  22. data/docs/api/Ovto/Runtime.html +7 -7
  23. data/docs/api/Ovto/State/MissingValue.html +6 -6
  24. data/docs/api/Ovto/State/{UnknownKey.html → UnknownStateKey.html} +11 -11
  25. data/docs/api/Ovto/State.html +59 -43
  26. data/docs/api/Ovto/WiredActionSet.html +636 -0
  27. data/docs/api/Ovto/WiredActions.html +78 -35
  28. data/docs/api/Ovto.html +223 -30
  29. data/docs/api/_index.html +77 -11
  30. data/docs/api/actions.html +29 -3
  31. data/docs/api/app.html +28 -2
  32. data/docs/api/class_list.html +3 -3
  33. data/docs/api/component.html +30 -4
  34. data/docs/api/css/style.css +3 -2
  35. data/docs/api/fetch.html +30 -4
  36. data/docs/api/file.README.html +76 -78
  37. data/docs/api/file_list.html +2 -2
  38. data/docs/api/frames.html +2 -2
  39. data/docs/api/index.html +76 -78
  40. data/docs/api/js/app.js +14 -3
  41. data/docs/api/method_list.html +303 -31
  42. data/docs/api/middleware.html +498 -0
  43. data/docs/api/pure_component.html +428 -0
  44. data/docs/api/state.html +28 -2
  45. data/docs/api/top-level-namespace.html +11 -9
  46. data/docs/guides/debugging.html +28 -2
  47. data/docs/guides/development.html +28 -2
  48. data/docs/guides/install.html +28 -2
  49. data/docs/guides/tutorial.html +28 -2
  50. data/docs/index.html +28 -2
  51. data/docs/search_index.json +1 -1
  52. data/examples/sinatra/Gemfile +2 -0
  53. data/examples/sinatra/Gemfile.lock +32 -30
  54. data/examples/sinatra/app.rb +1 -4
  55. data/examples/sinatra/config.ru +6 -6
  56. data/examples/static/Gemfile.lock +9 -9
  57. data/lib/ovto/actions.rb +11 -0
  58. data/lib/ovto/app.rb +37 -13
  59. data/lib/ovto/component.rb +130 -67
  60. data/lib/ovto/middleware.rb +121 -0
  61. data/lib/ovto/pure_component.rb +1 -1
  62. data/lib/ovto/state.rb +13 -5
  63. data/lib/ovto/version.rb +1 -1
  64. data/lib/ovto/wired_action_set.rb +40 -0
  65. data/lib/ovto/wired_actions.rb +49 -11
  66. data/lib/ovto.rb +26 -0
  67. data/ovto.gemspec +1 -1
  68. metadata +21 -11
  69. data/docs/api/pure_component.md +0 -27
@@ -0,0 +1,121 @@
1
+ module Ovto
2
+ VALID_NAME_REXP = /\A[a-zA-Z0-9_]+\z/
3
+ # Create an ancestor of middleware class
4
+ # Example:
5
+ # class MiddlewareExample < Ovto::Middleware("middleware_example")
6
+ def self.Middleware(name)
7
+ unless VALID_NAME_REXP =~ name
8
+ raise "invalid middleware name: #{name}"
9
+ end
10
+ return Class.new(Ovto::Middleware::Base){
11
+ const_set(:OVTO_MIDDLEWARE_NAME, name)
12
+ const_set(:State, Ovto::State)
13
+ const_set(:Actions, Class.new(Ovto::Middleware::Actions))
14
+ const_get(:Actions).const_set(:OVTO_MIDDLEWARE_NAME, name)
15
+ const_set(:Component, Class.new(Ovto::Middleware::Component))
16
+ const_get(:Component).const_set(:OVTO_MIDDLEWARE_NAME, name)
17
+ }
18
+ end
19
+
20
+ module Middleware
21
+ # (internal) Create a subclass of Ovto::State that handles
22
+ # states of `middlewares`
23
+ # Called from App#initialize
24
+ def self.create_middleware_states_class(middlewares)
25
+ return Class.new(Ovto::State){
26
+ middlewares.each do |m|
27
+ item m.name, default_proc: ->{
28
+ mw_state_class = m.const_get('State')
29
+ mw_state_class.item :_middlewares, default_proc: -> {
30
+ Ovto::Middleware.create_middleware_states_class(m.middlewares).new
31
+ }
32
+ mw_state_class.new
33
+ }
34
+ end
35
+ }
36
+ end
37
+
38
+ # Get the state of the middleware specified with `middleware_path`
39
+ def self.dig_middleware_state(app_state, middleware_path)
40
+ return middleware_path.inject(app_state){|state, middleware_name|
41
+ state._middlewares.__send__(middleware_name)
42
+ }
43
+ end
44
+ end
45
+
46
+ # Base class of a middleware class
47
+ # Note: this is not the direct superclass of a middleware.
48
+ # `SomeMiddleware < (anonymous class) < Middleware::Base`
49
+ class Middleware::Base
50
+ # Nested middlewares
51
+ def self.middlewares
52
+ @middlewares ||= []
53
+ end
54
+
55
+ # Install a nested middleware
56
+ def self.use(middleware_class)
57
+ self.middlewares.push(middleware_class)
58
+ end
59
+
60
+ # Middleware name (set by Ovto.Middleware)
61
+ def self.name
62
+ const_get(:OVTO_MIDDLEWARE_NAME)
63
+ end
64
+
65
+ def self._run_setup(wired_action_set)
66
+ mw = new(wired_action_set[WiredActionSet::THE_MIDDLEWARE_ITSELF])
67
+ mw.setup
68
+ self.middlewares.each do |m|
69
+ m._run_setup(wired_action_set[m.name])
70
+ end
71
+ end
72
+
73
+ def initialize(wired_actions)
74
+ @wired_actions = wired_actions
75
+ end
76
+
77
+ # Override this if needed
78
+ def setup
79
+ end
80
+
81
+ def actions
82
+ @wired_actions
83
+ end
84
+ end
85
+
86
+ class Middleware::Actions < Ovto::Actions
87
+ def initialize(middleware_path)
88
+ @middleware_path = middleware_path
89
+ end
90
+ attr_reader :middleware_path
91
+
92
+ # The name of the middleware this Actions belongs to
93
+ def middleware_name
94
+ self.class::OVTO_MIDDLEWARE_NAME
95
+ end
96
+
97
+ def state
98
+ app_state = super
99
+ return Ovto::Middleware.dig_middleware_state(app_state, @middleware_path)
100
+ end
101
+ end
102
+
103
+ # Base class of middleware component
104
+ # Basically the same as Ovto::Component but `actions` is wired to
105
+ # middleware actions.
106
+ class Middleware::Component < Ovto::Component
107
+ def self.middleware_name
108
+ self::OVTO_MIDDLEWARE_NAME
109
+ end
110
+
111
+ # The name of the middleware this component belongs to
112
+ def middleware_name
113
+ self.class::OVTO_MIDDLEWARE_NAME
114
+ end
115
+
116
+ def state
117
+ app_state = super
118
+ return Ovto::Middleware.dig_middleware_state(app_state, @middleware_path)
119
+ end
120
+ end
121
+ end
@@ -2,7 +2,7 @@ module Ovto
2
2
  class PureComponent < Component
3
3
  class StateIsNotAvailable < StandardError; end
4
4
 
5
- def initialize(wired_actions)
5
+ def initialize(*args)
6
6
  super
7
7
  @prev_props = nil
8
8
  @cache = nil
data/lib/ovto/state.rb CHANGED
@@ -3,7 +3,7 @@ module Ovto
3
3
  # Mandatory value is omitted in the argument of State.new
4
4
  class MissingValue < StandardError; end
5
5
  # Unknown key is given
6
- class UnknownKey < StandardError; end
6
+ class UnknownStateKey < StandardError; end
7
7
 
8
8
  # (internal) initialize subclass
9
9
  def self.inherited(subclass)
@@ -28,15 +28,23 @@ module Ovto
28
28
  def initialize(hash = {})
29
29
  unknown_keys = hash.keys - self.class.item_specs.map(&:first)
30
30
  if unknown_keys.any?
31
- raise UnknownKey, "unknown key(s): #{unknown_keys.inspect}"
31
+ raise UnknownStateKey, "unknown key(s): #{unknown_keys.inspect}"
32
32
  end
33
33
 
34
34
  @values = self.class.item_specs.map{|name, options|
35
- if !hash.key?(name) && !options.key?(:default)
35
+ if !hash.key?(name) && !options.key?(:default) && !options.key?(:default_proc)
36
36
  raise MissingValue, ":#{name} is mandatory for #{self.class.name}.new"
37
37
  end
38
38
  # Note that `hash[key]` may be false or nil
39
- value = hash.key?(name) ? hash[name] : options[:default]
39
+ value = if hash.key?(name)
40
+ hash[name]
41
+ elsif options.key?(:default)
42
+ options[:default]
43
+ elsif options.key?(:default_proc)
44
+ options[:default_proc].call
45
+ else
46
+ raise "must not happen"
47
+ end
40
48
  [name, value]
41
49
  }.to_h
42
50
  end
@@ -46,7 +54,7 @@ module Ovto
46
54
  def merge(hash)
47
55
  unknown_keys = hash.keys - self.class.item_specs.map(&:first)
48
56
  if unknown_keys.any?
49
- raise UnknownKey, "unknown key(s): #{unknown_keys.inspect}"
57
+ raise UnknownStateKey, "unknown key(s): #{unknown_keys.inspect}"
50
58
  end
51
59
  self.class.new(@values.merge(hash))
52
60
  end
data/lib/ovto/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Ovto
2
- VERSION = '0.5.0'
2
+ VERSION = '0.6.2'
3
3
  end
@@ -0,0 +1,40 @@
1
+ require 'set'
2
+
3
+ module Ovto
4
+ # Set of WiredActions (One for the app, zero or more for middlewares)
5
+ class WiredActionSet
6
+ # Special key for @hash
7
+ I_AM_APP_NOT_A_MIDDLEWARE = ''
8
+ THE_MIDDLEWARE_ITSELF = ''
9
+
10
+ # For testing
11
+ def self.dummy()
12
+ new(nil, nil, [], [], nil)
13
+ end
14
+
15
+ def initialize(app, actions, middleware_path, middlewares, runtime)
16
+ @app = app
17
+ @hash = {}
18
+ @hash[THE_MIDDLEWARE_ITSELF] = WiredActions.new(actions, app, runtime, self)
19
+ middlewares.each do |m|
20
+ mw_path = middleware_path + [m.name]
21
+ mw_actions = m.const_get('Actions').new(mw_path)
22
+ mw_wired_action_set = WiredActionSet.new(app, mw_actions, mw_path, m.middlewares, runtime)
23
+ @hash[m.name] = mw_wired_action_set
24
+ mw_actions.wired_actions = mw_wired_action_set[THE_MIDDLEWARE_ITSELF]
25
+ end
26
+ @middleware_names = middlewares.map(&:name).to_set
27
+ end
28
+ attr_reader :app, :middleware_names
29
+
30
+ # Return the WiredActions of the app
31
+ def app_wired_actions
32
+ @hash[I_AM_APP_NOT_A_MIDDLEWARE]
33
+ end
34
+
35
+ # Return the WiredActions of a middleware
36
+ def [](name)
37
+ @hash.fetch(name)
38
+ end
39
+ end
40
+ end
@@ -3,18 +3,30 @@ require 'promise'
3
3
 
4
4
  module Ovto
5
5
  class WiredActions
6
- def initialize(actions, app, runtime)
7
- @actions, @app, @runtime = actions, app, runtime
6
+ # Create a WiredActions
7
+ #
8
+ # - actions: Ovto::Actions
9
+ # - app: Ovto::App
10
+ # - runtime: Ovto::Runtime (to call scheduleRender after state change)
11
+ # - parent: WiredActionSet
12
+ def initialize(actions, app, runtime, parent)
13
+ @actions, @app, @runtime, @parent = actions, app, runtime, parent
8
14
  end
9
15
 
10
16
  def method_missing(name, args_hash={})
11
- Ovto.log_error {
12
- invoke_action(name, args_hash)
13
- }
17
+ raise NoMethodError, "undefined method `#{name}' on #{self}" unless respond_to?(name)
18
+ if @actions.respond_to?(name)
19
+ Ovto.log_error {
20
+ Ovto.debug_trace_log("invoke action \"#{name}\" on #{@actions.class}")
21
+ invoke_action(name, args_hash)
22
+ }
23
+ else
24
+ @parent[name][WiredActionSet::THE_MIDDLEWARE_ITSELF] # WiredActions of a middleware named `name`
25
+ end
14
26
  end
15
27
 
16
28
  def respond_to?(name)
17
- @actions.respond_to?(name)
29
+ @actions.respond_to?(name) || @parent.middleware_names.include?(name)
18
30
  end
19
31
 
20
32
  # internal
@@ -26,8 +38,7 @@ module Ovto
26
38
 
27
39
  # Call action and schedule rendering
28
40
  def invoke_action(name, args_hash)
29
- kwargs = {state: @app.state}.merge(args_hash)
30
- state_diff = @actions.__send__(name, **kwargs)
41
+ state_diff = Ovto.send_args_with_state(@actions, name, args_hash, current_state)
31
42
  return if state_diff.nil? ||
32
43
  state_diff.is_a?(Promise) || `!!state_diff.then` ||
33
44
  # eg.
@@ -42,12 +53,39 @@ module Ovto
42
53
  unless state_diff.is_a?(Hash)
43
54
  raise "action `#{name}' must return hash but got #{state_diff.inspect}"
44
55
  end
45
- new_state = @app.state.merge(state_diff)
46
- if new_state != @app.state
56
+ new_state = current_state.merge(state_diff)
57
+ if new_state != current_state
47
58
  @runtime.scheduleRender
48
- @app._set_state(new_state)
59
+ update_state(new_state)
49
60
  end
50
61
  return new_state
51
62
  end
63
+
64
+ def middleware_name
65
+ @actions.middleware_name
66
+ end
67
+
68
+ def current_state
69
+ @actions.state
70
+ end
71
+
72
+ def update_state(new_item)
73
+ new_app_state = _new_app_state(@actions.middleware_path, @app.state, new_item)
74
+ @app._set_state(new_app_state)
75
+ end
76
+
77
+ def _new_app_state(middleware_path, old_state, new_item)
78
+ if middleware_path.empty?
79
+ return new_item
80
+ else
81
+ first, *rest = *middleware_path
82
+ orig_state = old_state._middlewares.__send__(first)
83
+ return old_state.merge(
84
+ _middlewares: old_state._middlewares.merge(
85
+ first => _new_app_state(rest, orig_state, new_item)
86
+ )
87
+ )
88
+ end
89
+ end
52
90
  end
53
91
  end
data/lib/ovto.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  if RUBY_ENGINE == 'opal'
2
+ require 'opal'
2
3
  require 'console'; def console; $console; end
3
4
  require_relative 'ovto/actions'
4
5
  require_relative 'ovto/app'
@@ -9,6 +10,8 @@ if RUBY_ENGINE == 'opal'
9
10
  require_relative 'ovto/state'
10
11
  require_relative 'ovto/version'
11
12
  require_relative 'ovto/wired_actions'
13
+ require_relative 'ovto/wired_action_set'
14
+ require_relative 'ovto/middleware'
12
15
  else
13
16
  require 'ovto/version'
14
17
  require 'opal'; Opal.append_path(__dir__)
@@ -56,4 +59,27 @@ module Ovto
56
59
  end
57
60
  raise ex
58
61
  end
62
+
63
+ # Something like `obj.meth(state: state, **args, &block)`
64
+ # Safe even if `obj.meth` does not have `state:`
65
+ def self.send_args_with_state(obj, meth, args, state, &block)
66
+ parameters = obj.method(meth).parameters
67
+ accepts_state = `!parameters` || parameters.nil? || parameters.any?{|item|
68
+ item == [:key, :state] ||
69
+ item == [:keyreq, :state] ||
70
+ item[0] == :keyrest
71
+ }
72
+ if accepts_state
73
+ # We can pass `state:` safely
74
+ args_with_state = {state: state}.merge(args)
75
+ return obj.__send__(meth, **args_with_state, &block)
76
+ else
77
+ # Check it is empty (see https://github.com/opal/opal/issues/1872)
78
+ if args.empty?
79
+ return obj.__send__(meth, &block)
80
+ else
81
+ return obj.__send__(meth, **args, &block)
82
+ end
83
+ end
84
+ end
59
85
  end
data/ovto.gemspec CHANGED
@@ -19,6 +19,6 @@ Gem::Specification.new do |spec|
19
19
  spec.require_paths = ["lib"]
20
20
 
21
21
  spec.add_dependency "opal", '>= 0.11', '< 2'
22
- spec.add_dependency "thor", '~> 0.20'
22
+ spec.add_dependency "thor", '>= 0.20'
23
23
  spec.add_dependency "rack", '~> 2.0'
24
24
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ovto
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yutaka HARA
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-12-22 00:00:00.000000000 Z
11
+ date: 2021-10-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: opal
@@ -34,14 +34,14 @@ dependencies:
34
34
  name: thor
35
35
  requirement: !ruby/object:Gem::Requirement
36
36
  requirements:
37
- - - "~>"
37
+ - - ">="
38
38
  - !ruby/object:Gem::Version
39
39
  version: '0.20'
40
40
  type: :runtime
41
41
  prerelease: false
42
42
  version_requirements: !ruby/object:Gem::Requirement
43
43
  requirements:
44
- - - "~>"
44
+ - - ">="
45
45
  - !ruby/object:Gem::Version
46
46
  version: '0.20'
47
47
  - !ruby/object:Gem::Dependency
@@ -80,6 +80,7 @@ files:
80
80
  - book/api/app.md
81
81
  - book/api/component.md
82
82
  - book/api/fetch.md
83
+ - book/api/middleware.md
83
84
  - book/api/pure_component.md
84
85
  - book/api/state.md
85
86
  - book/guides/debugging.md
@@ -88,17 +89,24 @@ files:
88
89
  - book/guides/tutorial.md
89
90
  - book/ovtologo.png
90
91
  - book/screenshot.png
92
+ - docs/api/Array.html
93
+ - docs/api/Hash.html
91
94
  - docs/api/Ovto.html
92
95
  - docs/api/Ovto/Actions.html
93
96
  - docs/api/Ovto/App.html
94
97
  - docs/api/Ovto/Component.html
95
98
  - docs/api/Ovto/Component/MoreThanOneNode.html
99
+ - docs/api/Ovto/Middleware.html
100
+ - docs/api/Ovto/Middleware/Actions.html
101
+ - docs/api/Ovto/Middleware/Base.html
102
+ - docs/api/Ovto/Middleware/Component.html
96
103
  - docs/api/Ovto/PureComponent.html
97
104
  - docs/api/Ovto/PureComponent/StateIsNotAvailable.html
98
105
  - docs/api/Ovto/Runtime.html
99
106
  - docs/api/Ovto/State.html
100
107
  - docs/api/Ovto/State/MissingValue.html
101
- - docs/api/Ovto/State/UnknownKey.html
108
+ - docs/api/Ovto/State/UnknownStateKey.html
109
+ - docs/api/Ovto/WiredActionSet.html
102
110
  - docs/api/Ovto/WiredActions.html
103
111
  - docs/api/_index.html
104
112
  - docs/api/actions.html
@@ -117,7 +125,8 @@ files:
117
125
  - docs/api/js/full_list.js
118
126
  - docs/api/js/jquery.js
119
127
  - docs/api/method_list.html
120
- - docs/api/pure_component.md
128
+ - docs/api/middleware.html
129
+ - docs/api/pure_component.html
121
130
  - docs/api/state.html
122
131
  - docs/api/top-level-namespace.html
123
132
  - docs/gitbook/fonts/fontawesome/FontAwesome.otf
@@ -173,10 +182,12 @@ files:
173
182
  - lib/ovto/app.rb
174
183
  - lib/ovto/component.rb
175
184
  - lib/ovto/fetch.rb
185
+ - lib/ovto/middleware.rb
176
186
  - lib/ovto/pure_component.rb
177
187
  - lib/ovto/runtime.rb
178
188
  - lib/ovto/state.rb
179
189
  - lib/ovto/version.rb
190
+ - lib/ovto/wired_action_set.rb
180
191
  - lib/ovto/wired_actions.rb
181
192
  - ovto.gemspec
182
193
  - ovtologo.odg
@@ -187,7 +198,7 @@ homepage: https://github.com/yhara/ovto
187
198
  licenses:
188
199
  - MIT
189
200
  metadata: {}
190
- post_install_message:
201
+ post_install_message:
191
202
  rdoc_options: []
192
203
  require_paths:
193
204
  - lib
@@ -202,9 +213,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
202
213
  - !ruby/object:Gem::Version
203
214
  version: '0'
204
215
  requirements: []
205
- rubyforge_project:
206
- rubygems_version: 2.7.6.2
207
- signing_key:
216
+ rubygems_version: 3.2.22
217
+ signing_key:
208
218
  specification_version: 4
209
219
  summary: Simple client-side framework for Opal
210
220
  test_files: []
@@ -1,27 +0,0 @@
1
- # Ovto::PureComponent
2
-
3
- It almost the same as `Ovto::Component`, but it caches the `render` method calling with arguments of the method.
4
-
5
-
6
- ## Cache strategy
7
-
8
- It compares `render` method arguments and the previous arguments.
9
-
10
- ```rb
11
- def render
12
- o 'div' do
13
- o Pure, foo: state.foo
14
- o NotPure bar: state.bar
15
- end
16
- end
17
- ```
18
-
19
- In this case, `NotPure` component's render method is called even if `state.foo` is changed.
20
- Whereas `Pure` component's render method is called only if `state.foo` is changed.
21
-
22
-
23
- ## State
24
-
25
- `state` method is not available in `PureComponent`, because `PureComponent` does not treat state as the cache key.
26
- If you'd like to use state in `PureComponent`, pass the state from the parent component.
27
-