volt 0.8.27.beta6 → 0.8.27.beta7

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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -3
  3. data/VERSION +1 -1
  4. data/app/volt/models/user.rb +1 -1
  5. data/app/volt/tasks/query_tasks.rb +2 -2
  6. data/app/volt/tasks/store_tasks.rb +14 -4
  7. data/app/volt/tasks/user_tasks.rb +1 -1
  8. data/lib/volt/controllers/http_controller.rb +60 -0
  9. data/lib/volt/controllers/model_controller.rb +5 -1
  10. data/lib/volt/extra_core/string.rb +6 -0
  11. data/lib/volt/models/array_model.rb +6 -2
  12. data/lib/volt/models/associations.rb +1 -1
  13. data/lib/volt/models/buffer.rb +14 -3
  14. data/lib/volt/models/model.rb +28 -60
  15. data/lib/volt/models/permissions.rb +4 -4
  16. data/lib/volt/reactive/computation.rb +15 -15
  17. data/lib/volt/reactive/reactive_array.rb +1 -0
  18. data/lib/volt/router/routes.rb +67 -27
  19. data/lib/volt/server.rb +37 -6
  20. data/lib/volt/server/component_templates.rb +2 -2
  21. data/lib/volt/server/rack/http_request.rb +50 -0
  22. data/lib/volt/server/rack/http_resource.rb +41 -0
  23. data/lib/volt/server/rack/http_response_header.rb +33 -0
  24. data/lib/volt/server/rack/http_response_renderer.rb +41 -0
  25. data/lib/volt/spec/setup.rb +4 -2
  26. data/lib/volt/tasks/dispatcher.rb +7 -7
  27. data/lib/volt/tasks/task_handler.rb +1 -1
  28. data/lib/volt/volt/users.rb +12 -6
  29. data/spec/apps/kitchen_sink/app/main/config/routes.rb +18 -10
  30. data/spec/apps/kitchen_sink/app/main/controllers/main_controller.rb +2 -2
  31. data/spec/apps/kitchen_sink/app/main/controllers/server/simple_http_controller.rb +15 -0
  32. data/spec/apps/kitchen_sink/app/main/controllers/upload_controller.rb +22 -0
  33. data/spec/apps/kitchen_sink/app/main/views/main/yield.html +2 -2
  34. data/spec/apps/kitchen_sink/app/main/views/upload/index.html +15 -0
  35. data/spec/controllers/http_controller_spec.rb +130 -0
  36. data/spec/extra_core/string_transformation_test_cases.rb +8 -0
  37. data/spec/extra_core/string_transformations_spec.rb +12 -0
  38. data/spec/integration/http_endpoints_spec.rb +29 -0
  39. data/spec/integration/user_spec.rb +42 -42
  40. data/spec/models/associations_spec.rb +4 -4
  41. data/spec/models/buffer_spec.rb +15 -0
  42. data/spec/models/model_spec.rb +70 -25
  43. data/spec/models/model_state_spec.rb +1 -1
  44. data/spec/models/permissions_spec.rb +64 -2
  45. data/spec/models/persistors/params_spec.rb +8 -8
  46. data/spec/models/persistors/store_spec.rb +1 -1
  47. data/spec/models/user_validation_spec.rb +1 -1
  48. data/spec/router/routes_spec.rb +111 -43
  49. data/spec/server/rack/http_request_spec.rb +50 -0
  50. data/spec/server/rack/http_resource_spec.rb +59 -0
  51. data/spec/server/rack/http_response_header_spec.rb +34 -0
  52. data/spec/server/rack/http_response_renderer_spec.rb +33 -0
  53. data/spec/tasks/dispatcher_spec.rb +2 -2
  54. data/templates/component/config/routes.rb +2 -2
  55. data/templates/project/Gemfile.tt +3 -5
  56. data/templates/project/app/main/config/routes.rb +4 -4
  57. data/volt.gemspec +2 -2
  58. metadata +33 -8
@@ -0,0 +1,41 @@
1
+ require 'json'
2
+ require 'volt'
3
+
4
+ module Volt
5
+ # Renders responses for HttpController actions
6
+ class HttpResponseRenderer
7
+ @renderers = {}
8
+
9
+ def self.renderers
10
+ @renderers
11
+ end
12
+
13
+ # Register renderers.
14
+ def self.register_renderer(name, content_type, proc)
15
+ @renderers[name.to_sym] = { proc: proc, content_type: content_type }
16
+ end
17
+
18
+ # Default renderers for json and plain text
19
+ register_renderer(:json, 'application/json', proc { |data| data.to_json })
20
+ register_renderer(:text, 'text/plain', proc { |data| data.to_s })
21
+
22
+ # Iterate through @renderes to find a matching renderer for the given
23
+ # content and call the given proc.
24
+ # Other params fromt he content are returned as additional headers
25
+ # Returns an empty string if no renderer could be found
26
+ def render(content)
27
+ content = content.symbolize_keys
28
+ self.class.renderers.keys.each do |renderer_name|
29
+ if content.key?(renderer_name)
30
+ renderer = self.class.renderers[renderer_name]
31
+ to_render = content.delete(renderer_name)
32
+ rendered = renderer[:proc].call(to_render)
33
+ return [rendered, content.merge(content_type: renderer[:content_type])]
34
+ end
35
+ end
36
+
37
+ # If we couldn't find a renderer - just render an empty string
38
+ ['', content_type: 'text/plain']
39
+ end
40
+ end
41
+ end
@@ -21,8 +21,10 @@ module Volt
21
21
  # Setup the spec collection accessors
22
22
  # RSpec.shared_context "volt collections", {} do
23
23
  RSpec.shared_examples_for 'volt collections', {} do
24
- # Page conflicts with capybara's page method
25
- # let(:page) { Model.new }
24
+ # Page conflicts with capybara's page method, so we call it the_page for now.
25
+ # TODO: we need a better solution for page
26
+
27
+ let(:the_page) { Model.new }
26
28
  let(:store) do
27
29
  @__store_accessed = true
28
30
  $page ||= Page.new
@@ -18,8 +18,8 @@ module Volt
18
18
 
19
19
  start_time = Time.now.to_f
20
20
 
21
- # Check that we are calling on a TaskHandler class and a method provide at
22
- # TaskHandler or above in the ancestor chain.
21
+ # Check that we are calling on a Task class and a method provide at
22
+ # Task or above in the ancestor chain.
23
23
  if safe_method?(klass, method_name)
24
24
  promise.resolve(nil)
25
25
 
@@ -61,17 +61,17 @@ module Volt
61
61
 
62
62
  # Check if it is safe to use this method
63
63
  def safe_method?(klass, method_name)
64
- # Make sure the class being called is a TaskHandler.
65
- return false unless klass.ancestors.include?(TaskHandler)
64
+ # Make sure the class being called is a Task.
65
+ return false unless klass.ancestors.include?(Task)
66
66
 
67
67
  # Make sure the method is defined on the klass we're using and not up the hiearchy.
68
68
  # ^ This check prevents methods like #send, #eval, #instance_eval, #class_eval, etc...
69
69
  klass.ancestors.each do |ancestor_klass|
70
70
  if ancestor_klass.instance_methods(false).include?(method_name)
71
71
  return true
72
- elsif ancestor_klass == TaskHandler
73
- # We made it to TaskHandler and didn't find the method, that means it
74
- # was defined above TaskHandler, so we reject the call.
72
+ elsif ancestor_klass == Task
73
+ # We made it to Task and didn't find the method, that means it
74
+ # was defined above Task, so we reject the call.
75
75
  return false
76
76
  end
77
77
  end
@@ -1,5 +1,5 @@
1
1
  module Volt
2
- class TaskHandler
2
+ class Task
3
3
  if RUBY_PLATFORM == 'opal'
4
4
  # On the front-end we setup a proxy class to the backend that returns
5
5
  # promises for all calls.
@@ -3,7 +3,7 @@ require 'thread'
3
3
  module Volt
4
4
  class << self
5
5
  # Get the user_id from the cookie
6
- def user_id
6
+ def current_user_id
7
7
  # Check for a user_id from with_user
8
8
  if (user_id = Thread.current['with_user_id'])
9
9
  return user_id
@@ -52,17 +52,23 @@ module Volt
52
52
  end
53
53
 
54
54
  # True if the user is logged in and the user is loaded
55
- def user?
56
- !!user
55
+ def current_user?
56
+ !!current_user
57
57
  end
58
58
 
59
59
  # Return the current user.
60
- def user
60
+ def current_user
61
61
  # Run first on the query, or return nil
62
62
  user_query.try(:first)
63
63
  end
64
64
 
65
- def fetch_user
65
+ # Put in a deprecation placeholder
66
+ def user
67
+ Volt.logger.warning("deprication: Volt.user has been renamed to Volt.current_user (to be more clear about what it returns). Volt.user will be deprecated in the future.")
68
+ current_user
69
+ end
70
+
71
+ def fetch_current_user
66
72
  u_query = user_query
67
73
  if u_query
68
74
  u_query.fetch_first
@@ -108,7 +114,7 @@ module Volt
108
114
  private
109
115
  # Returns a query for the current user_id or nil if there is no user_id
110
116
  def user_query
111
- user_id = self.user_id
117
+ user_id = self.current_user_id
112
118
  if user_id
113
119
  $page.store._users.where(_id: user_id)
114
120
  else
@@ -1,16 +1,24 @@
1
1
  # See https://github.com/voltrb/volt#routes for more info on routes
2
2
 
3
- get '/bindings/{{ route_test }}', action: 'bindings'
4
- get '/bindings', action: 'bindings'
5
- get '/store', action: 'store'
6
- get '/cookie_test', action: 'cookie_test'
7
- get '/flash', action: 'flash'
8
- get '/yield', action: 'yield'
9
- get '/todos', controller: 'todos'
3
+ client '/bindings/{{ route_test }}', action: 'bindings'
4
+ client '/bindings', action: 'bindings'
5
+ client '/store', action: 'store'
6
+ client '/cookie_test', action: 'cookie_test'
7
+ client '/flash', action: 'flash'
8
+ client '/yield', action: 'yield'
9
+ client '/todos', controller: 'todos'
10
10
 
11
11
  # Signup/login routes
12
- get '/signup', controller: 'user-templates', action: 'signup'
13
- get '/login', controller: 'user-templates', action: 'login'
12
+ client '/signup', controller: 'user-templates', action: 'signup'
13
+ client '/login', controller: 'user-templates', action: 'login'
14
+
15
+ # HTTP endpoints
16
+ get '/simple_http', controller: 'simple_http', action: 'index'
17
+ get '/simple_http/store', controller: 'simple_http', action: 'show'
18
+ post '/simple_http/upload', controller: 'simple_http', action: 'upload'
19
+
20
+ # Route for file uploads
21
+ client '/upload', controller: 'upload', action: 'index'
14
22
 
15
23
  # The main route, this should be last. It will match any params not previously matched.
16
- get '/', {}
24
+ client '/', {}
@@ -23,13 +23,13 @@ class MainController < Volt::ModelController
23
23
  end
24
24
 
25
25
  def cookie_test
26
- self.model = page._new_cookie.buffer
26
+ self.model = page._new_cookie!.buffer
27
27
  end
28
28
 
29
29
  def add_cookie
30
30
  cookies.send(:"_#{_name.to_s}=", _value)
31
31
 
32
- self.model = page._new_cookie.buffer
32
+ self.model = page._new_cookie!.buffer
33
33
  end
34
34
 
35
35
  def content_string
@@ -0,0 +1,15 @@
1
+ class SimpleHttpController < Volt::HttpController
2
+ def index
3
+ render text: 'this is just some text'
4
+ end
5
+
6
+ def show
7
+ render text: "You had me at #{store._simple_http_tests.first._name}"
8
+ end
9
+
10
+ def upload
11
+ uploaded = params[:file][:tempfile]
12
+ File.open('tmp/uploaded_file', 'wb') { |f| f.write(uploaded.read) }
13
+ render text: 'Thanks for uploading'
14
+ end
15
+ end
@@ -0,0 +1,22 @@
1
+ class UploadController < Volt::ModelController
2
+ model :page
3
+
4
+ def index
5
+ # Nothing to setup here
6
+ end
7
+
8
+ def upload
9
+ `form_data = new FormData();
10
+ form_data.append("file", $('#file')[0].files[0]);
11
+ $.ajax({
12
+ url: '/simple_http/upload',
13
+ data: form_data,
14
+ processData: false,
15
+ contentType: false,
16
+ type: 'POST',
17
+ success: function(data){
18
+ $('#status').html("successfully uploaded");
19
+ }
20
+ });`
21
+ end
22
+ end
@@ -13,6 +13,6 @@
13
13
  <p>{{ yield }}</p>
14
14
 
15
15
 
16
- {{ if Volt.user }}
17
- Email: {{ Volt.user._email }}
16
+ {{ if Volt.current_user }}
17
+ Email: {{ Volt.current_user._email }}
18
18
  {{ end }}
@@ -0,0 +1,15 @@
1
+ <:Title>
2
+ File Upload
3
+
4
+ <:Body>
5
+ <h1>File Upload Example</h1>
6
+
7
+ <form e-submit="upload">
8
+ <label>File</label>
9
+ <input class="form-control" id="file" type="file" />
10
+ <input type="submit" id="submit_file_upload">
11
+ </form>
12
+
13
+ <div id="status">
14
+ waiting...
15
+ </div>
@@ -0,0 +1,130 @@
1
+ if RUBY_PLATFORM != 'opal'
2
+ require 'volt/controllers/http_controller'
3
+ require 'volt/server/rack/http_request'
4
+ require 'volt/server/rack/http_resource'
5
+
6
+ describe Volt::HttpController do
7
+ class TestHttpController < Volt::HttpController
8
+ attr_reader :action_called
9
+
10
+ def just_call_an_action
11
+ @action_called = true
12
+ end
13
+
14
+ def ok_head_action
15
+ head :ok, location: 'http://example.com'
16
+ end
17
+
18
+ def created_head_action
19
+ head :created
20
+ end
21
+
22
+ def head_with_http_headers
23
+ head :ok, location: 'http://path.to/example'
24
+ end
25
+
26
+ def redirect_action
27
+ redirect_to 'http://path.to/example'
28
+ end
29
+
30
+ def render_plain_text
31
+ render text: 'just plain text'
32
+ end
33
+
34
+ def render_json
35
+ render json: { 'this' => 'is_json', 'another' => 'pair' }
36
+ end
37
+
38
+ def render_json_with_custom_headers
39
+ render json: { some: 'json' },
40
+ status: :created, location: '/test/location'
41
+ end
42
+
43
+ def access_body
44
+ render json: JSON.parse(request.body.read)
45
+ end
46
+ end
47
+
48
+ let(:app) { ->(env) { [404, env, 'app'] } }
49
+
50
+ let(:request) do
51
+ Volt::HttpRequest.new(
52
+ Rack::MockRequest.env_for('http://example.com/test.html',
53
+ 'CONTENT_TYPE' => 'text/plain;charset=utf-8'))
54
+ end
55
+
56
+ let(:controller) { TestHttpController.new({}, request) }
57
+
58
+ it 'should merge the request params and the url params' do
59
+ request = Volt::HttpRequest.new(
60
+ Rack::MockRequest.env_for('http://example.com/test.html?this=is_a&test=param'))
61
+ controller = TestHttpController.new(
62
+ { another: 'params', 'and_a' => 'string' }, request)
63
+ expect(controller.params.size).to eq(4)
64
+ expect(controller.params[:and_a]).to eq('string')
65
+ expect(controller.params[:this]).to eq('is_a')
66
+ end
67
+
68
+ it 'should perform the correct action' do
69
+ expect(controller.action_called).not_to be(true)
70
+ controller.perform(:just_call_an_action)
71
+ expect(controller.action_called).to be(true)
72
+ end
73
+
74
+ it 'should redirect' do
75
+ expect(controller.action_called).not_to be(true)
76
+ response = controller.perform(:redirect_action)
77
+ expect(response.location).to eq('http://path.to/example')
78
+ expect(response.status).to eq(302)
79
+ end
80
+
81
+ it 'should respond with head' do
82
+ response = controller.perform(:ok_head_action)
83
+ expect(response.status).to eq(200)
84
+ expect(response.body).to eq([])
85
+
86
+ response = controller.perform(:created_head_action)
87
+ expect(response.status).to eq(201)
88
+
89
+ response = controller.perform(:head_with_http_headers)
90
+ expect(response.headers['Location']).to eq('http://path.to/example')
91
+ expect(response.location).to eq('http://path.to/example')
92
+ end
93
+
94
+ it 'should render plain text' do
95
+ response = controller.perform(:render_plain_text)
96
+ expect(response.status).to eq(200)
97
+ expect(response['Content-Type']).to eq('text/plain')
98
+ expect(response.body).to eq(['just plain text'])
99
+ end
100
+
101
+ it 'should render json' do
102
+ response = controller.perform(:render_json)
103
+ expect(response.status).to eq(200)
104
+ expect(response['Content-Type']).to eq('application/json')
105
+ expect(JSON.parse(response.body.first)).to eq('this' => 'is_json',
106
+ 'another' => 'pair')
107
+ end
108
+
109
+ it 'should set the correct status for rendered responses' do
110
+ response = controller.perform(:render_json_with_custom_headers)
111
+ expect(response.status).to eq(201)
112
+ end
113
+
114
+ it 'should include the custum headers' do
115
+ response = controller.perform(:render_json_with_custom_headers)
116
+ expect(response['Location']).to eq('/test/location')
117
+ end
118
+
119
+ it 'should have access to the body' do
120
+ http_app = Volt::HttpResource.new(app, nil)
121
+ allow(http_app).to receive(:routes_match?)
122
+ .and_return(controller: 'test_http',
123
+ action: 'access_body')
124
+ request = Rack::MockRequest.new(http_app)
125
+ response = request.post('http://example.com/test.html', input:
126
+ { test: 'params' }.to_json)
127
+ expect(response.body).to eq({ test: 'params' }.to_json)
128
+ end
129
+ end
130
+ end
@@ -17,3 +17,11 @@
17
17
  'street_address' => 'street-address',
18
18
  'person_street_address' => 'person-street-address'
19
19
  }
20
+
21
+ UnderscoresToHeaders = {
22
+ 'proxy_authenticate' => 'Proxy-Authenticate',
23
+ 'set_cookie' => 'Set-Cookie',
24
+ 'set-cookie' => 'Set-Cookie',
25
+ 'via' => 'Via',
26
+ 'WWW_Authenticate' => 'WWW-Authenticate'
27
+ }
@@ -45,3 +45,15 @@ describe '#camelize' do
45
45
  expect('HTMLTidyGenerator'.underscore).to eq('html_tidy_generator')
46
46
  end
47
47
  end
48
+
49
+ describe '#headerize' do
50
+ it "headerizes" do
51
+ expect('test_case'.headerize).to eq('Test-Case')
52
+ end
53
+
54
+ UnderscoresToHeaders.each do |underscored, headerized|
55
+ it 'underscores' do
56
+ expect(underscored.headerize).to eq(headerized)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,29 @@
1
+ if ENV['BROWSER']
2
+ require 'spec_helper'
3
+
4
+ describe 'http endpoints', type: :feature, sauce: true do
5
+ it 'should show the page' do
6
+ visit '/simple_http'
7
+ expect(page).to have_content('this is just some text')
8
+ end
9
+
10
+ it 'should have access to the store' do
11
+ DataStore.new.drop_database
12
+ $page.store._simple_http_tests << { name: 'hello' }
13
+ visit '/simple_http/store'
14
+ expect(page).to have_content('You had me at hello')
15
+ DataStore.new.drop_database
16
+ end
17
+
18
+ it 'should upload and store a file' do
19
+ file = 'tmp/uploaded_file'
20
+ FileUtils.rm(file) if File.exist?(file)
21
+ visit '/upload'
22
+ attach_file('file', __FILE__)
23
+ find('#submit_file_upload').click
24
+ expect(page).to have_content('successfully uploaded')
25
+ expect(File.exist?(file)).to be(true)
26
+ FileUtils.rm(file) if File.exist?(file)
27
+ end
28
+ end
29
+ end