volt 0.8.18 → 0.8.19

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/Readme.md +2 -1
  4. data/VERSION +1 -1
  5. data/app/volt/controllers/notices_controller.rb +12 -2
  6. data/app/volt/models/user.rb +34 -1
  7. data/app/volt/tasks/store_tasks.rb +26 -23
  8. data/app/volt/tasks/user_tasks.rb +26 -2
  9. data/app/volt/views/notices/index.html +8 -6
  10. data/lib/volt.rb +47 -1
  11. data/lib/volt/cli/asset_compile.rb +24 -42
  12. data/lib/volt/config.rb +24 -14
  13. data/lib/volt/controllers/model_controller.rb +6 -1
  14. data/lib/volt/data_stores/mongo_driver.rb +7 -3
  15. data/lib/volt/models.rb +3 -0
  16. data/lib/volt/models/array_model.rb +21 -3
  17. data/lib/volt/models/model.rb +109 -48
  18. data/lib/volt/models/model_hash_behaviour.rb +25 -8
  19. data/lib/volt/models/persistors/array_store.rb +5 -2
  20. data/lib/volt/models/persistors/cookies.rb +91 -0
  21. data/lib/volt/models/persistors/model_store.rb +52 -14
  22. data/lib/volt/models/persistors/query/query_listener.rb +4 -1
  23. data/lib/volt/models/persistors/query/query_listener_pool.rb +1 -1
  24. data/lib/volt/models/persistors/store_state.rb +5 -5
  25. data/lib/volt/models/validations.rb +87 -18
  26. data/lib/volt/models/validators/length_validator.rb +1 -1
  27. data/lib/volt/models/validators/presence_validator.rb +1 -1
  28. data/lib/volt/models/validators/unique_validator.rb +28 -0
  29. data/lib/volt/page/bindings/template_binding.rb +2 -2
  30. data/lib/volt/page/page.rb +4 -0
  31. data/lib/volt/page/tasks.rb +2 -2
  32. data/lib/volt/reactive/computation.rb +2 -0
  33. data/lib/volt/reactive/hash_dependency.rb +1 -3
  34. data/lib/volt/reactive/reactive_array.rb +7 -0
  35. data/lib/volt/reactive/reactive_hash.rb +17 -0
  36. data/lib/volt/server.rb +1 -1
  37. data/lib/volt/server/component_templates.rb +3 -6
  38. data/lib/volt/server/html_parser/view_scope.rb +1 -1
  39. data/lib/volt/server/rack/component_code.rb +3 -2
  40. data/lib/volt/server/rack/component_paths.rb +15 -0
  41. data/lib/volt/server/rack/index_files.rb +1 -1
  42. data/lib/volt/tasks/dispatcher.rb +26 -12
  43. data/lib/volt/tasks/task_handler.rb +17 -15
  44. data/spec/apps/kitchen_sink/app/main/config/routes.rb +3 -0
  45. data/spec/apps/kitchen_sink/app/main/controllers/main_controller.rb +26 -0
  46. data/spec/apps/kitchen_sink/app/main/controllers/users_test_controller.rb +27 -0
  47. data/spec/apps/kitchen_sink/app/main/views/main/cookie_test.html +25 -0
  48. data/spec/apps/kitchen_sink/app/main/views/main/flash.html +13 -0
  49. data/spec/apps/kitchen_sink/app/main/views/main/main.html +3 -0
  50. data/spec/apps/kitchen_sink/app/main/views/users_test/index.html +22 -0
  51. data/spec/apps/kitchen_sink/config/app.rb +3 -0
  52. data/spec/integration/bindings_spec.rb +1 -1
  53. data/spec/integration/cookies_spec.rb +48 -0
  54. data/spec/integration/flash_spec.rb +32 -0
  55. data/spec/models/model_spec.rb +3 -4
  56. data/spec/models/validations_spec.rb +13 -13
  57. data/spec/tasks/dispatcher_spec.rb +1 -1
  58. data/templates/project/config/app.rb.tt +6 -1
  59. data/templates/project/{public → config/base_page}/index.html +0 -0
  60. data/volt.gemspec +4 -0
  61. metadata +47 -3
@@ -25,6 +25,11 @@ module Volt
25
25
  @array.each(&block)
26
26
  end
27
27
 
28
+ def empty?
29
+ @size_dep.depend
30
+ @array.empty?
31
+ end
32
+
28
33
  def count(&block)
29
34
  if block
30
35
  count = 0
@@ -220,6 +225,8 @@ module Volt
220
225
  end
221
226
 
222
227
  def inspect
228
+ # TODO: should also depend on items in array
229
+ @size_dep.depend
223
230
  "#<#{self.class}:#{object_id} #{@array.inspect}>"
224
231
  end
225
232
 
@@ -35,6 +35,7 @@ module Volt
35
35
 
36
36
  def delete(key)
37
37
  @deps.delete(key)
38
+ @all_deps.changed!
38
39
  @hash.delete(key)
39
40
  end
40
41
 
@@ -42,9 +43,25 @@ module Volt
42
43
  @hash.each_pair do |key, _|
43
44
  delete(key)
44
45
  end
46
+
47
+ @all_deps.changed!
48
+ end
49
+
50
+ def replace(hash)
51
+ clear
52
+
53
+ hash.each_pair do |key, value|
54
+ self[key] = value
55
+ end
56
+ end
57
+
58
+ def to_h
59
+ @all_deps.depend
60
+ @hash
45
61
  end
46
62
 
47
63
  def inspect
64
+ @all_deps.depend
48
65
  "#<ReactiveHash #{@hash.inspect}>"
49
66
  end
50
67
  end
data/lib/volt/server.rb CHANGED
@@ -122,7 +122,7 @@ module Volt
122
122
 
123
123
  @app.use Rack::Static,
124
124
  urls: ['/'],
125
- root: 'public',
125
+ root: 'config/base',
126
126
  index: '',
127
127
  header_rules: [
128
128
  [:all, { 'Cache-Control' => 'public, max-age=86400' }]
@@ -4,10 +4,11 @@ require 'volt/server/html_parser/view_parser'
4
4
  # setup code (for controllers, models, views, and routes)
5
5
  module Volt
6
6
  class ComponentTemplates
7
+ # client is if we are generating for the client or backend
7
8
  def initialize(component_path, component_name, client = true)
8
9
  @component_path = component_path
9
10
  @component_name = component_name
10
- @client = true
11
+ @client = client
11
12
  end
12
13
 
13
14
  def code
@@ -21,11 +22,7 @@ module Volt
21
22
  end
22
23
 
23
24
  def page_reference
24
- if @client
25
- '$page'
26
- else
27
- 'page'
28
- end
25
+ '$page'
29
26
  end
30
27
 
31
28
  def generate_view_code
@@ -107,7 +107,7 @@ module Volt
107
107
  end
108
108
 
109
109
  def add_component(tag_name, attributes, unary)
110
- component_name = tag_name[1..-1].gsub(':', '/')
110
+ component_name = tag_name[1..-1].tr(':', '/')
111
111
 
112
112
  @handler.html << "<!-- $#{@binding_number} --><!-- $/#{@binding_number} -->"
113
113
 
@@ -12,12 +12,13 @@ module Volt
12
12
  @client = client
13
13
  end
14
14
 
15
- def code
15
+ # The client argument is for if this code is being generated for the client
16
+ def code(client=true)
16
17
  code = ''
17
18
 
18
19
  asset_files = AssetFiles.new(@component_name, @component_paths)
19
20
  asset_files.component_paths.each do |component_path, component_name|
20
- code << ComponentTemplates.new(component_path, component_name, @client).code
21
+ code << ComponentTemplates.new(component_path, component_name, client).code
21
22
  code << "\n\n"
22
23
  end
23
24
 
@@ -1,4 +1,6 @@
1
1
  # ComponentPaths gives an array of every folder where you find a component.
2
+ require 'volt/server/rack/component_code'
3
+
2
4
  module Volt
3
5
  class ComponentPaths
4
6
  def initialize(root = nil)
@@ -73,6 +75,19 @@ module Volt
73
75
  end
74
76
  end
75
77
  end
78
+
79
+ # component_names = []
80
+ # app_folders do |app_folder|
81
+ # Dir["#{app_folder}/*"].map {|cp| cp[/[^\/]+$/] }.each do |component_name|
82
+ # component_names << component_name
83
+ # end
84
+ # end
85
+ #
86
+ # # Load in all views
87
+ # component_names.uniq.each do |component_name|
88
+ # code = Volt::ComponentCode.new(component_name, self).code
89
+ # eval(code)
90
+ # end
76
91
  end
77
92
  end
78
93
 
@@ -34,7 +34,7 @@ module Volt
34
34
  end
35
35
 
36
36
  def html
37
- index_path = File.expand_path(File.join(Volt.root, 'public/index.html'))
37
+ index_path = File.expand_path(File.join(Volt.root, 'config/base/index.html'))
38
38
  html = File.read(index_path)
39
39
 
40
40
  ERB.new(html).result(binding)
@@ -2,34 +2,48 @@ module Volt
2
2
  # The task dispatcher is responsible for taking incoming messages
3
3
  # from the socket channel and dispatching them to the proper handler.
4
4
  class Dispatcher
5
+
6
+ # Dispatch takes an incoming Task from the client and runs it on the
7
+ # server, returning the result to the client.
8
+ # Tasks returning a promise will wait to return.
5
9
  def dispatch(channel, message)
6
- callback_id, class_name, method_name, *args = message
10
+ callback_id, class_name, method_name, meta_data, *args = message
7
11
  method_name = method_name.to_sym
8
12
 
9
13
  # Get the class
10
14
  klass = Object.send(:const_get, class_name)
11
15
 
12
- result = nil
13
- error = nil
16
+ promise = Promise.new
14
17
 
18
+ # Check that we are calling on a TaskHandler class and a method provide at
19
+ # TaskHandler or above in the ancestor chain.
15
20
  if safe_method?(klass, method_name)
21
+ promise.resolve(nil)
22
+
16
23
  # Init and send the method
17
- begin
24
+ promise = promise.then do
25
+ Thread.current['meta'] = meta_data
26
+
18
27
  result = klass.new(channel, self).send(method_name, *args)
19
- rescue => e
20
- # TODO: Log these errors better
21
- puts e.inspect
22
- puts e.backtrace
23
- error = e
28
+
29
+ Thread.current['meta'] = nil
30
+
31
+ result
24
32
  end
33
+
25
34
  else
26
35
  # Unsafe method
27
- error = RuntimeError.new("unsafe method: #{method_name}")
36
+ promise.reject(RuntimeError.new("unsafe method: #{method_name}"))
28
37
  end
29
38
 
30
39
  if callback_id
31
- # Callback with result
32
- channel.send_message('response', callback_id, result, error)
40
+ # Run the promise and pass the return value/error back to the client
41
+ promise.then do |result|
42
+ channel.send_message('response', callback_id, result, nil)
43
+ end.fail do |error|
44
+ channel.send_message('response', callback_id, nil, error)
45
+ Volt.logger.error(error)
46
+ end
33
47
  end
34
48
  end
35
49
 
@@ -4,7 +4,16 @@ module Volt
4
4
  # On the front-end we setup a proxy class to the backend that returns
5
5
  # promises for all calls.
6
6
  def self.method_missing(name, *args, &block)
7
- $page.tasks.call(self.name, name, *args, &block)
7
+ # Meta data is passed from the browser to the server so the server can know
8
+ # things like who's logged in.
9
+ meta_data = {}
10
+
11
+ user_id = $page.cookies._user_id
12
+ unless user_id.nil?
13
+ meta_data['user_id'] = user_id
14
+ end
15
+
16
+ $page.tasks.call(self.name, name, meta_data, *args, &block)
8
17
  end
9
18
  else
10
19
  def initialize(channel = nil, dispatcher = nil)
@@ -22,21 +31,14 @@ module Volt
22
31
  end
23
32
 
24
33
  # On the backend, we proxy all class methods like we would
25
- # on the front-end. This returns promises.
34
+ # on the front-end. This returns a promise, even if the
35
+ # original code did not.
26
36
  def self.method_missing(name, *args, &block)
27
- promise = Promise.new
28
-
29
- begin
30
- result = new(nil, nil).send(name, *args, &block)
31
-
32
- promise.resolve(result)
33
- rescue => e
34
- puts "Task Error: #{e.inspect}"
35
- puts e.backtrace
36
- promise.reject(e)
37
- end
38
-
39
- promise
37
+ # TODO: optimize: this could run the inside first to see if it
38
+ # returns a promise, so we don't have to wrap it.
39
+ return Promise.new.then do
40
+ new(nil, nil).send(name, *args, &block)
41
+ end.resolve(nil)
40
42
  end
41
43
 
42
44
  # Provide access to the store collection
@@ -3,7 +3,10 @@
3
3
  get '/bindings/{{_route_test}}', _action: 'bindings'
4
4
  get '/bindings', _action: 'bindings'
5
5
  get '/store', _action: 'store'
6
+ get '/cookie_test', _action: 'cookie_test'
7
+ get '/flash', _action: 'flash'
6
8
  get '/todos', _controller: 'todos'
9
+ get '/users_test', _controller: 'users_test'
7
10
 
8
11
  # The main route, this should be last. It will match any params not previously matched.
9
12
  get '/', {}
@@ -1,6 +1,32 @@
1
1
  class MainController < Volt::ModelController
2
2
  model :page
3
3
 
4
+ def flash_notice
5
+ flash._notices << 'A notice message'
6
+ end
7
+
8
+ def flash_success
9
+ flash._successes << 'A success message'
10
+ end
11
+
12
+ def flash_warning
13
+ flash._warnings << 'A warning message'
14
+ end
15
+
16
+ def flash_error
17
+ flash._errors << 'An error message'
18
+ end
19
+
20
+ def cookie_test
21
+ self.model = page._new_cookie.buffer
22
+ end
23
+
24
+ def add_cookie
25
+ cookies.send(:"_#{_name.to_s}=", _value)
26
+
27
+ self.model = page._new_cookie.buffer
28
+ end
29
+
4
30
  private
5
31
 
6
32
  # the main template contains a #template binding that shows another
@@ -0,0 +1,27 @@
1
+ class UsersTestController < Volt::ModelController
2
+ def index
3
+ self.model = store._users.buffer
4
+ end
5
+
6
+ def signup
7
+ puts "Signup"
8
+ model.save!.then do |a|
9
+ puts "Saved"
10
+ end.fail do |err|
11
+ puts "Fail with: #{err.inspect}"
12
+ end
13
+ end
14
+
15
+ def login
16
+ puts "USER LOGIN"
17
+ User.login('ryanstout@gmail.com', 'temppass').then do |result|
18
+ puts "Login Success"
19
+ end.fail do |err|
20
+ puts "ERR: #{err.inspect}"
21
+ end
22
+ end
23
+
24
+ def logout
25
+ User.logout
26
+ end
27
+ end
@@ -0,0 +1,25 @@
1
+ <:Title>
2
+ Cookies
3
+
4
+ <:Body>
5
+ <h1>Add Cookies</h1>
6
+
7
+ <form e-submit="add_cookie">
8
+ <div class="form-group">
9
+ <label>Name</label>
10
+ <input class="form-control" id="cookieName" type="text" value="{{ _name }}" />
11
+ </div>
12
+ <div class="form-group">
13
+ <label>Value</label>
14
+ <input class="form-control" id="cookieValue" type="text" value="{{ _value }}" />
15
+ </div>
16
+ <button>Add Cookie</button>
17
+ </form>
18
+
19
+ <h2>Cookies</h2>
20
+
21
+ <ul>
22
+ {{ cookies.keys.each do |key| }}
23
+ <li>{{ key }}: {{ cookies.read_attribute(key) }} <button class="cookieDelete" e-click="cookies.delete(key)">X</button></li>
24
+ {{ end }}
25
+ </ul>
@@ -0,0 +1,13 @@
1
+ <:Title>
2
+ Flash Messages
3
+
4
+ <:Body>
5
+ <h1>Flash Messages</h1>
6
+
7
+ <p>
8
+ <a e-click="flash_notice">Flash Notice</a><br />
9
+ <a e-click="flash_success">Flash Success</a><br />
10
+ <a e-click="flash_warning">Flash Warning</a><br />
11
+ <a e-click="flash_error">Flash Error</a><br />
12
+ </p>
13
+
@@ -8,7 +8,10 @@
8
8
  <:nav href="/" text="Home" />
9
9
  <:nav href="/bindings" text="Bindings" />
10
10
  <:nav href="/todos" text="Todos" />
11
+ <:nav href="/flash" text="Flash" />
12
+ <:nav href="/cookie_test" text="Cookies" />
11
13
  <:nav href="/store" text="Store" />
14
+ <:nav href="/users_test" text="Users" />
12
15
  </ul>
13
16
  <h3 class="text-muted">Project name</h3>
14
17
  </div>
@@ -0,0 +1,22 @@
1
+ <:Title>
2
+ Users Test
3
+
4
+ <:Body>
5
+ <h1>Users Test</h1>
6
+
7
+ <form e-submit="signup">
8
+ {{ errors.html_inspect }}<br />
9
+ <input value="{{ _username }}" /><br />
10
+ <input type="password" value="{{ _password }}" /><br />
11
+
12
+ <button>Signup</button>
13
+ </form>
14
+
15
+ <hr />
16
+
17
+ <button e-click="login">Login</button><button e-click="logout">Logout</button>
18
+
19
+ {{ if Volt.user }}
20
+ {{ Volt.user._username }}<br />
21
+ {{ Volt.user._name }}
22
+ {{ end }}
@@ -0,0 +1,3 @@
1
+ Volt.setup do |config|
2
+ config.app_secret = 'vFYUzMiPIdMw1Iox0ggDpBYorLm_d55YtRPc0eGrdCpoN4h9E5FcWySIT_D8JIEOllU'
3
+ end
@@ -188,7 +188,7 @@ if ENV['BROWSER']
188
188
  expect(find('#paramsCheck3')).to have_content('')
189
189
 
190
190
  if ENV['BROWSER'] != 'phantom'
191
- expect(current_url).to match(/\/bindings$/)
191
+ expect(current_url).to match(/\/bindings[?]check[=]false$/)
192
192
  end
193
193
  end
194
194
  end
@@ -0,0 +1,48 @@
1
+ if ENV['BROWSER']
2
+ require 'spec_helper'
3
+
4
+ describe 'cookies collection', type: :feature do
5
+ if ENV['BROWSER'] != 'phantom'
6
+ # TODO: fails in phantom for some reason
7
+ it 'should add' do
8
+ visit '/'
9
+
10
+ click_link 'Cookies'
11
+
12
+ fill_in('cookieName', with: 'one')
13
+ fill_in('cookieValue', with: 'one')
14
+ click_button 'Add Cookie'
15
+
16
+ expect(page).to have_content('one: one')
17
+
18
+ # Reload the page
19
+ page.evaluate_script('document.location.reload()')
20
+
21
+ # Check again
22
+ expect(page).to have_content('one: one')
23
+ end
24
+ end
25
+
26
+ it 'should delete cookies' do
27
+ visit '/'
28
+
29
+ click_link 'Cookies'
30
+
31
+ fill_in('cookieName', with: 'two')
32
+ fill_in('cookieValue', with: 'two')
33
+ click_button 'Add Cookie'
34
+
35
+ expect(page).to have_content('two: two')
36
+
37
+ find('.cookieDelete').click
38
+
39
+ expect(page).to_not have_content('two: two')
40
+
41
+ # Reload the page
42
+ page.evaluate_script('document.location.reload()')
43
+
44
+ expect(page).to_not have_content('two: two')
45
+
46
+ end
47
+ end
48
+ end