volt 0.8.27.beta6 → 0.8.27.beta7

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