ovto 0.5.0 → 0.6.2

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.
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
-