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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -3
- data/VERSION +1 -1
- data/app/volt/models/user.rb +1 -1
- data/app/volt/tasks/query_tasks.rb +2 -2
- data/app/volt/tasks/store_tasks.rb +14 -4
- data/app/volt/tasks/user_tasks.rb +1 -1
- data/lib/volt/controllers/http_controller.rb +60 -0
- data/lib/volt/controllers/model_controller.rb +5 -1
- data/lib/volt/extra_core/string.rb +6 -0
- data/lib/volt/models/array_model.rb +6 -2
- data/lib/volt/models/associations.rb +1 -1
- data/lib/volt/models/buffer.rb +14 -3
- data/lib/volt/models/model.rb +28 -60
- data/lib/volt/models/permissions.rb +4 -4
- data/lib/volt/reactive/computation.rb +15 -15
- data/lib/volt/reactive/reactive_array.rb +1 -0
- data/lib/volt/router/routes.rb +67 -27
- data/lib/volt/server.rb +37 -6
- data/lib/volt/server/component_templates.rb +2 -2
- data/lib/volt/server/rack/http_request.rb +50 -0
- data/lib/volt/server/rack/http_resource.rb +41 -0
- data/lib/volt/server/rack/http_response_header.rb +33 -0
- data/lib/volt/server/rack/http_response_renderer.rb +41 -0
- data/lib/volt/spec/setup.rb +4 -2
- data/lib/volt/tasks/dispatcher.rb +7 -7
- data/lib/volt/tasks/task_handler.rb +1 -1
- data/lib/volt/volt/users.rb +12 -6
- data/spec/apps/kitchen_sink/app/main/config/routes.rb +18 -10
- data/spec/apps/kitchen_sink/app/main/controllers/main_controller.rb +2 -2
- data/spec/apps/kitchen_sink/app/main/controllers/server/simple_http_controller.rb +15 -0
- data/spec/apps/kitchen_sink/app/main/controllers/upload_controller.rb +22 -0
- data/spec/apps/kitchen_sink/app/main/views/main/yield.html +2 -2
- data/spec/apps/kitchen_sink/app/main/views/upload/index.html +15 -0
- data/spec/controllers/http_controller_spec.rb +130 -0
- data/spec/extra_core/string_transformation_test_cases.rb +8 -0
- data/spec/extra_core/string_transformations_spec.rb +12 -0
- data/spec/integration/http_endpoints_spec.rb +29 -0
- data/spec/integration/user_spec.rb +42 -42
- data/spec/models/associations_spec.rb +4 -4
- data/spec/models/buffer_spec.rb +15 -0
- data/spec/models/model_spec.rb +70 -25
- data/spec/models/model_state_spec.rb +1 -1
- data/spec/models/permissions_spec.rb +64 -2
- data/spec/models/persistors/params_spec.rb +8 -8
- data/spec/models/persistors/store_spec.rb +1 -1
- data/spec/models/user_validation_spec.rb +1 -1
- data/spec/router/routes_spec.rb +111 -43
- data/spec/server/rack/http_request_spec.rb +50 -0
- data/spec/server/rack/http_resource_spec.rb +59 -0
- data/spec/server/rack/http_response_header_spec.rb +34 -0
- data/spec/server/rack/http_response_renderer_spec.rb +33 -0
- data/spec/tasks/dispatcher_spec.rb +2 -2
- data/templates/component/config/routes.rb +2 -2
- data/templates/project/Gemfile.tt +3 -5
- data/templates/project/app/main/config/routes.rb +4 -4
- data/volt.gemspec +2 -2
- 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
|
data/lib/volt/spec/setup.rb
CHANGED
@@ -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
|
-
#
|
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
|
22
|
-
#
|
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
|
65
|
-
return false unless klass.ancestors.include?(
|
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 ==
|
73
|
-
# We made it to
|
74
|
-
# was defined above
|
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
|
data/lib/volt/volt/users.rb
CHANGED
@@ -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
|
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
|
56
|
-
!!
|
55
|
+
def current_user?
|
56
|
+
!!current_user
|
57
57
|
end
|
58
58
|
|
59
59
|
# Return the current user.
|
60
|
-
def
|
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
|
-
|
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.
|
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
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
-
|
13
|
-
|
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
|
-
|
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
|
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
|
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
|
@@ -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
|