From 16fa9c01554fe421226d507df2166e9ca8b3346f Mon Sep 17 00:00:00 2001 From: Julian Fahrer Date: Sun, 25 Oct 2015 12:11:59 +0100 Subject: [PATCH 1/2] HttpController with helpers for restful resources --- .../controllers/restful_base_controller.rb | 63 +++++++ lib/volt/volt/core.rb | 1 + .../kitchen_sink/app/main/models/issue.rb | 3 + .../restful_base_controller_spec.rb | 171 ++++++++++++++++++ 4 files changed, 238 insertions(+) create mode 100644 lib/volt/controllers/restful_base_controller.rb create mode 100644 spec/apps/kitchen_sink/app/main/models/issue.rb create mode 100644 spec/controllers/restful_base_controller_spec.rb diff --git a/lib/volt/controllers/restful_base_controller.rb b/lib/volt/controllers/restful_base_controller.rb new file mode 100644 index 00000000..bf85219c --- /dev/null +++ b/lib/volt/controllers/restful_base_controller.rb @@ -0,0 +1,63 @@ +require 'volt/controllers/http_controller' + +module Volt + # Allow you to create controllers that act as http endpoints + class RestfulBaseController < HttpController + + before_action :setup_model + before_action :setup_new_resource, only: [:create] + before_action :setup_buffered_resource, only: [:update] + before_action :setup_resource, only: [:show, :destroy] + + private + + attr_reader :model, :resource + + def self.model(model = :not_set) + if model == :not_set + @model + else + @model = model.to_sym + end + end + + def collection_name + model.pluralize + end + + def collection + store.send(collection_name) + end + + def resource_params + params.send(:"_#{model}") + end + + def setup_model + @model = self.class.model || params._model.try(:to_sym) + unless @model + render text: "No model given", status: :internal_server_error + stop_chain + end + end + + def setup_new_resource + @resource = collection.new(resource_params) + end + + def setup_resource + @resource = collection.where(id: params.id).first.sync + unless @resource + head :not_found + stop_chain + end + end + + def setup_buffered_resource + setup_resource + @resource = @resource.buffer + end + + end +end + diff --git a/lib/volt/volt/core.rb b/lib/volt/volt/core.rb index a8c9bb15..b70ed4ae 100644 --- a/lib/volt/volt/core.rb +++ b/lib/volt/volt/core.rb @@ -1,3 +1,4 @@ # Require in the core volt classes that get used on the server require 'volt/controllers/http_controller' +require 'volt/controllers/restful_base_controller' require 'volt/server/rack/http_request' diff --git a/spec/apps/kitchen_sink/app/main/models/issue.rb b/spec/apps/kitchen_sink/app/main/models/issue.rb new file mode 100644 index 00000000..a2c95855 --- /dev/null +++ b/spec/apps/kitchen_sink/app/main/models/issue.rb @@ -0,0 +1,3 @@ +class Issue < Volt::Model + field :name, String +end \ No newline at end of file diff --git a/spec/controllers/restful_base_controller_spec.rb b/spec/controllers/restful_base_controller_spec.rb new file mode 100644 index 00000000..cc0b14db --- /dev/null +++ b/spec/controllers/restful_base_controller_spec.rb @@ -0,0 +1,171 @@ +require 'spec_helper' + +if RUBY_PLATFORM != 'opal' + require 'volt/controllers/restful_base_controller' + require 'volt/server/rack/http_request' + require 'volt/server/rack/http_resource' + + describe Volt::RestfulBaseController do + class TestRestfulController < Volt::RestfulBaseController + + attr_accessor :the_model, :the_collection, :the_collection_name, :the_resource_params + + def model_test + self.the_model = model + end + + def collection_test + self.the_collection = collection + self.the_collection_name = collection_name + end + + def params_test + self.the_resource_params = resource_params + end + + end + + class StaticRestfulController < Volt::RestfulBaseController + attr_accessor :the_model + + model :issue + + def model_test + self.the_model = model + end + end + + class ImplementedRestfulController < Volt::RestfulBaseController + attr_accessor :the_resource + + def create + self.the_resource = resource + end + + def update + self.the_resource = resource + end + + def show + self.the_resource = resource + end + + def destroy + self.the_resource = resource + end + end + + let(:app) { ->(env) { [404, env, 'app'] } } + + def request(url='http://example.com/issues') + Volt::HttpRequest.new( + Rack::MockRequest.env_for(url, 'CONTENT_TYPE' => 'application/json;charset=utf-8')) + end + + + let(:controller) { TestRestfulController.new(volt_app, {}, request) } + + before(:each) do + store.issues.reverse.each do |issue| + issue.destroy + end + end + + it 'should set the model from the params' do + controller = TestRestfulController.new( + volt_app, {model: 'issue'}, request + ) + controller.perform(:model_test) + expect(controller.the_model).to eq(:issue) + end + + it 'should use a static model if set' do + controller = StaticRestfulController.new( + volt_app, {model: 'none'}, request + ) + controller.perform(:model_test) + expect(controller.the_model).to eq(:issue) + end + + it 'should set the collection based on the model name' do + controller = TestRestfulController.new( + volt_app, {model: 'issue'}, request) + + controller.perform(:collection_test) + expect(controller.the_collection_name).to eq(:issues) + expect(controller.the_collection).to be_kind_of(Volt::ArrayModel) + expect(controller.the_collection.new).to be_kind_of(Issue) + end + + it 'should set the correct resource_params based on the model name' do + issue = { name: 'test' } + controller = TestRestfulController.new( + volt_app, {model: 'issue', issue: issue }, request + ) + controller.perform(:params_test) + expect(controller.the_resource_params).to eq(issue) + end + + it 'should setup a new instance of a model with the given params for the create action' do + issue = { name: 'test' } + controller = ImplementedRestfulController.new( + volt_app, {model: 'issue', issue: issue }, request + ) + controller.perform(:create) + expect(controller.the_resource).to be_kind_of(Issue) + expect(controller.the_resource.root).to be_kind_of(Issue) + new_issue = controller.the_resource.to_h + new_issue.delete(:id) + expect(new_issue).to eq(issue) + end + + it 'should setup a buffer of a model for the update action' do + issue = store.issues.create({ name: 'test' }).sync + controller = ImplementedRestfulController.new( + volt_app, { model: 'issue', id: issue.id }, request + ) + controller.perform(:update) + expect(controller.the_resource.to_h).to eq(issue.to_h) + expect(controller.the_resource.root).to eq(store) + expect(controller.the_resource.buffer?).to be(true) + end + + it 'should setup the model for the show action' do + issue = store.issues.create({ name: 'test' }).sync + controller = ImplementedRestfulController.new( + volt_app, { model: 'issue', id: issue.id }, request + ) + controller.perform(:show) + expect(controller.the_resource.to_h).to eq(issue.to_h) + expect(controller.the_resource.root).to eq(store) + expect(controller.the_resource.buffer?).to be(false) + end + + it 'should setup the model for the delete action' do + issue = store.issues.create({ name: 'test' }).sync + controller = ImplementedRestfulController.new( + volt_app, { model: 'issue', id: issue.id }, request + ) + controller.perform(:show) + expect(controller.the_resource.to_h).to eq(issue.to_h) + expect(controller.the_resource.root).to eq(store) + expect(controller.the_resource.buffer?).to be(false) + end + + it 'should respond with http 404 not found if the resource could not be found' do + controller = ImplementedRestfulController.new( + volt_app, { model: 'issue', id: 0 }, request + ) + response = controller.perform(:show) + expect(response.status).to eq(404) + end + + it 'should respond with http 500 internal server error no model is set' do + controller = ImplementedRestfulController.new( + volt_app, { }, request + ) + response = controller.perform(:show) + expect(response.status).to eq(500) + end + end +end From 9904331fcdf10c55f47a3ce22207379aa595d29e Mon Sep 17 00:00:00 2001 From: Julian Fahrer Date: Sun, 25 Oct 2015 12:12:57 +0100 Subject: [PATCH 2/2] Implementation of a HttpController for JSON APIs generation --- ...troller.rb => restfull_base_controller.rb} | 2 +- .../controllers/simple_json_controller.rb | 42 ++++++++++ lib/volt/volt/core.rb | 3 +- .../app/api/config/dependencies.rb | 2 + .../app/api/config/initializers/boot.rb | 10 +++ .../kitchen_sink/app/api/config/routes.rb | 1 + .../app/api/controllers/main_controller.rb | 20 +++++ .../api/controllers/server/api_controller.rb | 5 ++ .../app/api/views/main/index.html | 5 ++ .../kitchen_sink/app/main/config/routes.rb | 3 + ...ec.rb => restfull_base_controller_spec.rb} | 10 +-- .../simple_json_controller_spec.rb | 77 +++++++++++++++++++ 12 files changed, 173 insertions(+), 7 deletions(-) rename lib/volt/controllers/{restful_base_controller.rb => restfull_base_controller.rb} (96%) create mode 100644 lib/volt/controllers/simple_json_controller.rb create mode 100644 spec/apps/kitchen_sink/app/api/config/dependencies.rb create mode 100644 spec/apps/kitchen_sink/app/api/config/initializers/boot.rb create mode 100644 spec/apps/kitchen_sink/app/api/config/routes.rb create mode 100644 spec/apps/kitchen_sink/app/api/controllers/main_controller.rb create mode 100644 spec/apps/kitchen_sink/app/api/controllers/server/api_controller.rb create mode 100644 spec/apps/kitchen_sink/app/api/views/main/index.html rename spec/controllers/{restful_base_controller_spec.rb => restfull_base_controller_spec.rb} (94%) create mode 100644 spec/controllers/simple_json_controller_spec.rb diff --git a/lib/volt/controllers/restful_base_controller.rb b/lib/volt/controllers/restfull_base_controller.rb similarity index 96% rename from lib/volt/controllers/restful_base_controller.rb rename to lib/volt/controllers/restfull_base_controller.rb index bf85219c..2cd9d666 100644 --- a/lib/volt/controllers/restful_base_controller.rb +++ b/lib/volt/controllers/restfull_base_controller.rb @@ -2,7 +2,7 @@ module Volt # Allow you to create controllers that act as http endpoints - class RestfulBaseController < HttpController + class RestfullBaseController < HttpController before_action :setup_model before_action :setup_new_resource, only: [:create] diff --git a/lib/volt/controllers/simple_json_controller.rb b/lib/volt/controllers/simple_json_controller.rb new file mode 100644 index 00000000..9c321d3b --- /dev/null +++ b/lib/volt/controllers/simple_json_controller.rb @@ -0,0 +1,42 @@ +require 'volt/controllers/restfull_base_controller' + +module Volt + # Allow you to create controllers that act as http endpoints + class SimpleJsonController < RestfullBaseController + + def index + render json: { collection_name => collection.all.to_a.sync } + end + + def show + render json: { model => resource.to_h } + end + + def create + collection.append(resource).then do + # TODO http_controllers should be able to get routes via params + head :created #, location: params_to_url(:get, component: params._component, controller: params._controller, id: resource.id) + end.fail do |err| + + end + end + + def update + resource.update(resource_params) + resource.save!.then do + head :no_content + end.fail do + + end + end + + def destroy + resource.destroy.then do + head :no_content + end.fail do |err| + + end + end + + end +end diff --git a/lib/volt/volt/core.rb b/lib/volt/volt/core.rb index b70ed4ae..c675fb10 100644 --- a/lib/volt/volt/core.rb +++ b/lib/volt/volt/core.rb @@ -1,4 +1,5 @@ # Require in the core volt classes that get used on the server require 'volt/controllers/http_controller' -require 'volt/controllers/restful_base_controller' +require 'volt/controllers/restfull_base_controller' +require 'volt/controllers/simple_json_controller' require 'volt/server/rack/http_request' diff --git a/spec/apps/kitchen_sink/app/api/config/dependencies.rb b/spec/apps/kitchen_sink/app/api/config/dependencies.rb new file mode 100644 index 00000000..9c908a18 --- /dev/null +++ b/spec/apps/kitchen_sink/app/api/config/dependencies.rb @@ -0,0 +1,2 @@ +# Specify which components you wish to include when +# this component loads. diff --git a/spec/apps/kitchen_sink/app/api/config/initializers/boot.rb b/spec/apps/kitchen_sink/app/api/config/initializers/boot.rb new file mode 100644 index 00000000..a4bbae35 --- /dev/null +++ b/spec/apps/kitchen_sink/app/api/config/initializers/boot.rb @@ -0,0 +1,10 @@ +# Place any code you want to run when the component is included on the client +# or server. + +# To include code only on the client use: +# if RUBY_PLATFORM == 'opal' +# +# To include code only on the server, use: +# unless RUBY_PLATFORM == 'opal' +# ^^ this will not send compile in code in the conditional to the client. +# ^^ this include code required in the conditional. \ No newline at end of file diff --git a/spec/apps/kitchen_sink/app/api/config/routes.rb b/spec/apps/kitchen_sink/app/api/config/routes.rb new file mode 100644 index 00000000..6b4b3e87 --- /dev/null +++ b/spec/apps/kitchen_sink/app/api/config/routes.rb @@ -0,0 +1 @@ +# See https://github.com/voltrb/volt#routes for more info on routes diff --git a/spec/apps/kitchen_sink/app/api/controllers/main_controller.rb b/spec/apps/kitchen_sink/app/api/controllers/main_controller.rb new file mode 100644 index 00000000..cb394f4f --- /dev/null +++ b/spec/apps/kitchen_sink/app/api/controllers/main_controller.rb @@ -0,0 +1,20 @@ +module Api + class MainController < Volt::ModelController + def index + # Add code for when the index view is loaded + end + + def about + # Add code for when the about view is loaded + end + + private + + # the main template contains a #template binding that shows another + # template. This is the path to that template. It may change based + # on the params._controller and params._action values. + def main_path + "#{params._component || 'main'}/#{params._controller || 'main'}/#{params._action || 'index'}" + end + end +end \ No newline at end of file diff --git a/spec/apps/kitchen_sink/app/api/controllers/server/api_controller.rb b/spec/apps/kitchen_sink/app/api/controllers/server/api_controller.rb new file mode 100644 index 00000000..9fd92331 --- /dev/null +++ b/spec/apps/kitchen_sink/app/api/controllers/server/api_controller.rb @@ -0,0 +1,5 @@ +module Api + class ApiController < Volt::SimpleJsonController + + end +end diff --git a/spec/apps/kitchen_sink/app/api/views/main/index.html b/spec/apps/kitchen_sink/app/api/views/main/index.html new file mode 100644 index 00000000..da57fc22 --- /dev/null +++ b/spec/apps/kitchen_sink/app/api/views/main/index.html @@ -0,0 +1,5 @@ +<:Title> + Component Index + +<:Body> +

Component Index

\ No newline at end of file diff --git a/spec/apps/kitchen_sink/app/main/config/routes.rb b/spec/apps/kitchen_sink/app/main/config/routes.rb index 99b28935..fc49441e 100644 --- a/spec/apps/kitchen_sink/app/main/config/routes.rb +++ b/spec/apps/kitchen_sink/app/main/config/routes.rb @@ -34,5 +34,8 @@ # Route for file uploads client '/upload', controller: 'upload', action: 'index' +# Simple JSON API specs +rest '/api/issues', component: 'api', controller: 'api', model: 'issue' + # The main route, this should be last. It will match any params not previously matched. client '/', {} diff --git a/spec/controllers/restful_base_controller_spec.rb b/spec/controllers/restfull_base_controller_spec.rb similarity index 94% rename from spec/controllers/restful_base_controller_spec.rb rename to spec/controllers/restfull_base_controller_spec.rb index cc0b14db..abf7d72d 100644 --- a/spec/controllers/restful_base_controller_spec.rb +++ b/spec/controllers/restfull_base_controller_spec.rb @@ -1,12 +1,12 @@ require 'spec_helper' if RUBY_PLATFORM != 'opal' - require 'volt/controllers/restful_base_controller' + require 'volt/controllers/restfull_base_controller' require 'volt/server/rack/http_request' require 'volt/server/rack/http_resource' - describe Volt::RestfulBaseController do - class TestRestfulController < Volt::RestfulBaseController + describe Volt::RestfullBaseController do + class TestRestfulController < Volt::RestfullBaseController attr_accessor :the_model, :the_collection, :the_collection_name, :the_resource_params @@ -25,7 +25,7 @@ def params_test end - class StaticRestfulController < Volt::RestfulBaseController + class StaticRestfulController < Volt::RestfullBaseController attr_accessor :the_model model :issue @@ -35,7 +35,7 @@ def model_test end end - class ImplementedRestfulController < Volt::RestfulBaseController + class ImplementedRestfulController < Volt::RestfullBaseController attr_accessor :the_resource def create diff --git a/spec/controllers/simple_json_controller_spec.rb b/spec/controllers/simple_json_controller_spec.rb new file mode 100644 index 00000000..dc29d6ae --- /dev/null +++ b/spec/controllers/simple_json_controller_spec.rb @@ -0,0 +1,77 @@ +require 'spec_helper' + +if RUBY_PLATFORM != 'opal' + describe "SimpleJsonApiController" do + include Rack::Test::Methods + + let(:app) { Volt.current_app.middleware } + + def json_response + JSON.parse(last_response.body, symbolize_names: true) + end + + def headers + { 'CONTENT_TYPE' => 'application/json' } + end + + before(:each) do + store.issues.reverse.each do |issue| + issue.destroy + end + end + + it 'should return a list of resources as json' do + store.issues << Issue.new(name: 'test') + get '/api/issues' + expect(last_response.status).to eq(200) + expect(last_response.headers['Content-Type']).to eq('application/json') + expect(json_response).to eq(issues: store.issues) + end + + it 'should returns a single resource as json' do + store.issues << Issue.new(name: 'first') + issue = store.issues.create(name: 'test').sync + get '/api/issues/' + issue.id + expect(last_response.status).to eq(200) + expect(last_response.headers['Content-Type']).to eq('application/json') + expect(json_response).to eq(issue: issue.to_h) + end + + it 'should respond with 404 if the resource can not be found' do + get '/api/issues/0' + expect(last_response.status).to eq(404) + end + + it 'destroys a resource' do + first = store.issues.create(name: 'first').sync + issue = store.issues.create(name: 'second').sync + delete '/api/issues/' + issue.id + expect(last_response.status).to eq(204) + expect(store.issues.to_a.sync).to eq([first.to_h]) + end + + it 'creates a resource' do + payload = JSON.dump({ issue: {name: 'a issue' } }) + expect(store.issues.count.sync).to eq(0) + post '/api/issues', payload, headers + puts last_response.body + expect(last_response.status).to eq(201) + expect(store.issues.count.sync).to eq(1) + issue = store.issues.first.sync + expect(last_response.body).to be_empty + expect(issue.name).to eq('a issue') + # TODO http_controllers should be able to get routes via params + #expect(last_response.header['Location']).to match("/api/issues/" + issue.id) + end + + it 'updates a resource' do + issue = store.issues.create(name: 'test').sync + payload = JSON.dump({ issue: {name: 'new name' } }) + put '/api/issues/' + issue.id, payload, headers + puts last_response.body + expect(last_response.status).to eq(204) + issue = store.issues.first.sync + expect(issue.name).to eq 'new name' + end + end +end