Skip to content

Instantly share code, notes, and snippets.

@netzpirat
Created March 3, 2011 21:48
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save netzpirat/853675 to your computer and use it in GitHub Desktop.
Save netzpirat/853675 to your computer and use it in GitHub Desktop.
The Sencha responder adds generic JSON response handling for the Ext JS framework.
require 'siren'
# The JsonHelper adds a possibility to query JSON data with a simple query language.
# Read more about the Siren JSONQuery implementation at: https://github.com/jcoglan/siren
#
# @author Michael Kessler
#
module JsonHelper
def json(query)
Siren.query(query, json_response)
end
private
def json_response
@json_response ||= Siren.parse(@response.body)
end
end
module Extranett
module Responders
# The Sencha responder adds generic JSON response handling for the Ext JS framework.
#
# @example Example collection response
# { :success => true,
# :total => 2,
# :data => [{ :id => 1, :name => 'netzpirat' },
# { :id => 2, :name => 'effkay' }] }
#
# @example Example single resource response
# { :success => true,
# :data => { :id => 1, :name => 'netzpirat' }}
#
# @example Example update error response
# { :success => true,
# :message => 'The resource could not be updated',
# :errors => { :name => ['too short', 'already taken'] }}
#
# TODO
# * Pagination
# * Filtering
#
# @author Michael Kessler
#
module SenchaResponder
# Render the json response with a HTTP status code and an optional location header
#
def to_json
render :json => compose_json, :status => http_status, :location => location
end
protected
# Compose the final JSON response from the different possible parts:
#
# * success flag
# * plain text message
# * total collection count
# * resource data
# * resource errors
#
# @return [Hash]
#
def compose_json
[:success, :message, :total, :data, :errors].inject({}) { |json, part| json.merge(send(part)) }
end
# The error hash is always returned when the resource has errors.
#
# @example Error hash response
# { email: ["can't be blank"], username: ["is too long", "is already taken"] }
#
# @return [Hash] the resource errors
#
def errors
has_errors? ? { :errors => resource.errors } : {}
end
# The resource data will only be returned when the resource has no errors
# and has not been destroyed.
#
# @example Single resource data response
# { :data => { :id => 1, name => 'netzpirat' }
#
# @example Collection resource data response
# { :data => [{ :id => 1, name => 'netzpirat' }, { :id => 2, name => 'effkay' }]
#
# @return [Hash] the resource itself
#
def data
!has_errors? && (controller.action_name != 'destroy') ? { :data => resource } : {}
end
# The total resource count will be added to every index action.
#
# @example Total data response
# { :total => 123 }
#
# @return [Hash] the total resource count
#
def total
controller.action_name == 'index' && resource.present? ? { :total => resource.count } : {}
end
# The success response is a simplification of the HTTP status code and
# always false if the HTTP status code is greater or equal than 400
#
# @example Success response
# { :success => true }
#
# @return [Hash] the success status
#
def success
http_status_code >= 400 ? { :success => false } : { :success => true }
end
# Renders an additional, optional plain text message with
# I18n support like the flash messages from the
# [Responders](https://github.com/plataformatec/responders) Gem,
# where in fact the code has been taken from.
#
# The only difference is that the first key is 'message' instead of
# 'flash' and the last key the HTTP status code name instead of the
# flash key.
#
# @example I18n file
# message:
# actions:
# create:
# unprocessable_entity: "%{count} error(s) prohibited this %{resource_name} from being saved"
# tokens:
# create:
# unauthorized: "Login failed - check your email and password."
# forbidden: "You're not allowed to access this account."
# registrations:
# create:
# created: "The account has been created and an activation email has been sent."
#
# @example Message response
# { :message => 'The account could not be created.' }
#
# @return [Hash] the response message
#
def message
options = interpolations(http_status)
message = I18n.t options[:default].shift, options
message.present? ? { :message => message } : {}
end
# Return the URL from the :location option or the URL
# from any created resource.
#
# @return [String] the new location
#
def location
options[:location] || default_location
end
# Return the code from the :status option or the default
# HTTP status code. The following default statuses are known:
#
# * unprocessable_entity (422) if there are errors
# * created (201) if a resource has been created
# * ok (200) for any other requests
#
# @return [Symbol] the HTTP status code
#
def http_status
options[:status] || default_http_status
end
private
def default_location
resource.present? && controller.action_name == 'create' && http_status == :created ? controller.url_for(resource) : nil
end
def default_http_status
if has_errors?
:unprocessable_entity
else
controller.action_name == 'create' ? :created : :ok
end
end
def http_status_code
http_status.class == Symbol ? Rack::Utils::SYMBOL_TO_STATUS_CODE[http_status] : http_status
end
# I18n code below taken from the Responders gem: https://github.com/plataformatec/responders
# and slightly refactored.
def interpolations(status)
interpolations = {
:default => message_defaults_by_namespace(status),
:resource_name => resource_name,
:downcase_resource_name => resource_name.downcase
}
if has_errors?
interpolations.merge!({ :count => resource.errors.size })
end
if controller.respond_to?(:interpolation_options, true)
interpolations.merge!(controller.send(:interpolation_options))
end
interpolations
end
def resource_name
if resource.class.respond_to?(:model_name)
resource.class.model_name.human
else
resource.class.name.underscore.humanize
end
end
def message_defaults_by_namespace(status)
defaults = []
slices = controller.controller_path.split('/')
while slices.size > 0
defaults << :"message.#{ slices.fill(controller.controller_name, -1).join('.') }.#{ controller.action_name }.#{ status }"
defaults << :"message.#{ slices.fill(:actions, -1).join('.') }.#{ controller.action_name }.#{ status }"
slices.shift
end
defaults << ""
end
end
end
end
require 'spec_helper'
class TestApplicationResponder < ActionController::Responder
include Extranett::Responders::SenchaResponder
end
class TestApplicationController < ActionController::Base
self.responder = TestApplicationResponder
respond_to :json
attr_accessor :resource
end
describe Extranett::Responders::SenchaResponder, :type => :controller do
context 'the success flag' do
controller(TestApplicationController) do
layout nil
def update
respond_with resource
end
end
context 'for a resource without errors' do
before { controller.resource = {} }
it 'returns true' do
xhr :put, :update, :id => 1
json('$.success').should eql true
end
end
context 'for a resource with errors' do
before do
resource = mock_model('User')
resource.stub(:errors).and_return({ :name => ['cant be blank'] })
controller.resource = resource
end
it 'returns false' do
xhr :put, :update, :id => 1
json('$.success').should eql false
end
end
end
context 'the text message' do
context 'for a successful response' do
controller(TestApplicationController) do
layout nil
def index
respond_with {}
end
alias :show :index
alias :create :index
alias :update :index
alias :destroy :index
end
it "returns the action index ok message" do
I18n.should_receive(:translate).with(:'message.actions.index.ok', anything()).and_return 'index ok message'
xhr :get, :index
json('$.message').should eql 'index ok message'
end
it 'returns the action show ok message' do
I18n.should_receive(:translate).with(:'message.actions.show.ok', anything()).and_return 'show ok message'
xhr :get, :show, :id => 1
json('$.message').should eql 'show ok message'
end
it 'returns the action create created message' do
I18n.should_receive(:translate).with(:'message.actions.create.created', anything()).and_return 'create created message'
xhr :post, :create
json('$.message').should eql 'create created message'
end
it 'returns the action update ok message' do
I18n.should_receive(:translate).with(:'message.actions.update.ok', anything()).and_return 'update ok message'
xhr :put, :update, :id => 1
json('$.message').should eql 'update ok message'
end
it 'returns the action destroy ok message' do
I18n.should_receive(:translate).with(:'message.actions.destroy.ok', anything()).and_return 'destroy ok message'
xhr :delete, :destroy, :id => 1
json('$.message').should eql 'destroy ok message'
end
end
context 'for a failed response' do
controller(TestApplicationController) do
layout nil
def index
respond_with({}, :status => :unprocessable_entity)
end
alias :show :index
alias :create :index
alias :update :index
alias :destroy :index
end
it "returns the action index unprocessable_entity message" do
I18n.should_receive(:translate).with(:'message.actions.index.unprocessable_entity', anything()).and_return 'index unprocessable entity message'
xhr :get, :index
json('$.message').should eql 'index unprocessable entity message'
end
it 'returns the action show unprocessable_entity message' do
I18n.should_receive(:translate).with(:'message.actions.show.unprocessable_entity', anything()).and_return 'show unprocessable entity message'
xhr :get, :show, :id => 1
json('$.message').should eql 'show unprocessable entity message'
end
it 'returns the action create unprocessable_entity message' do
I18n.should_receive(:translate).with(:'message.actions.create.unprocessable_entity', anything()).and_return 'create unprocessable entity message'
xhr :post, :create
json('$.message').should eql 'create unprocessable entity message'
end
it 'returns the action update unprocessable_entity message' do
I18n.should_receive(:translate).with(:'message.actions.update.unprocessable_entity', anything()).and_return 'update unprocessable entity message'
xhr :put, :update, :id => 1
json('$.message').should eql 'update unprocessable entity message'
end
it 'returns the action destroy unprocessable_entity message' do
I18n.should_receive(:translate).with(:'message.actions.destroy.unprocessable_entity', anything()).and_return 'destroy unprocessable entity message'
xhr :delete, :destroy, :id => 1
json('$.message').should eql 'destroy unprocessable entity message'
end
end
end
context 'the total collection count' do
controller(TestApplicationController) do
layout nil
def index
respond_with [{ :id => 1 }, { :id => 2 }]
end
def show
respond_with :id => 1
end
end
it 'is not returned for a single resource' do
xhr :get, :show, :id => 1
json('$.total').should_not be_present
end
it 'returns the count for a collection' do
xhr :get, :index
json('$.total').should eql 2
end
end
context 'the resource data' do
controller(TestApplicationController) do
layout nil
def index
respond_with [{ :id => 1 }, { :id => 2 }]
end
def show
respond_with :id => 1
end
def update
respond_with(params[:id] == 1 ? { :id => 1 } : resource)
end
end
it 'returns the collection resource data' do
xhr :get, :index
json('$.data').should eql [{ 'id' => 1 }, { 'id' => 2 }]
end
it 'returns the single resource data' do
xhr :get, :show, :id => 1
json('$.data').should eql({ 'id' => 1 })
end
context 'for an update without any errors' do
it 'returns the updated resource data' do
xhr :put, :update, :id => 1
json('$.data').should eql({ 'id' => 1 })
end
end
context 'for an update with some errors' do
before do
resource = mock_model('User')
resource.stub(:errors).and_return({ :name => ['cant be blank'] })
controller.resource = resource
xhr :put, :update, :id => 2
end
it 'does not return the resource data' do
json('$.data').should_not be_present
end
it 'does return the errors' do
json('$.errors').should be_present
end
end
end
context 'the resource errors' do
controller(TestApplicationController) do
layout nil
def create
respond_with resource
end
def update
respond_with resource
end
end
before do
resource = mock_model('User')
resource.stub(:errors).and_return({ :name => ['cant be blank'] })
controller.resource = resource
end
context 'on a failed creation' do
it 'returns the errors' do
xhr :post, :create, :id => 1
json('$.errors').should eql({ 'name' => ['cant be blank'] })
end
it 'does not return the data' do
xhr :post, :create, :id => 1
json('$.data').should_not be_present
end
end
context 'on a failed update' do
it 'returns the errors' do
xhr :put, :update, :id => 1
json('$.errors').should eql({ 'name' => ['cant be blank'] })
end
it 'does not return the data' do
xhr :put, :update, :id => 1
json('$.data').should_not be_present
end
end
end
context 'the http status code' do
context 'without an explicit set return status code' do
controller(TestApplicationController) do
layout nil
def index
respond_with [{}, {}]
end
def show
respond_with {}
end
def create
respond_with resource
end
def update
respond_with resource
end
end
context 'on creation' do
context 'without errors' do
before { controller.resource = {} }
it 'responses with :created' do
xhr :post, :create
response.status.should eql 201
end
end
context 'with errors' do
before do
resource = mock_model('User')
resource.stub(:errors).and_return({ :name => ['cant be blank'] })
controller.resource = resource
end
it 'responses with :unprocessable_entity' do
xhr :post, :create
response.status.should eql 422
end
end
end
context 'on update' do
context 'without errors' do
before { controller.resource = {} }
it 'responses with :ok' do
xhr :put, :update, :id => 1
response.status.should eql 200
end
end
context 'with errors' do
before do
resource = mock_model('User')
resource.stub(:errors).and_return({ :name => ['cant be blank'] })
controller.resource = resource
end
it 'responses with :unprocessable_entity' do
xhr :put, :update, :id => 1
response.status.should eql 422
end
end
end
context 'on retrieval' do
context 'of the collection' do
it 'responses with :ok' do
xhr :get, :index
response.status.should eql 200
end
end
context 'of the resource' do
it 'responses with :ok' do
xhr :get, :show, :id => 1
response.status.should eql 200
end
end
end
end
context 'with an explicit set return status code' do
controller(TestApplicationController) do
layout nil
def index
respond_with [{}, {}], :status => :unauthorized
end
end
it 'responses with the given status' do
xhr :get, :index
response.status.should eql 401
end
end
end
context 'location header' do
context 'without a location option given' do
controller(TestApplicationController) do
layout nil
def show
end
def create
# url_for on a string returns the string itself
respond_with 'http://example.com/users/1'
end
end
context 'on creation' do
it 'returns the location of the created resource' do
xhr :post, :create
response.headers['Location'].should eql 'http://example.com/users/1'
end
end
end
context 'with a location option' do
controller(TestApplicationController) do
layout nil
def index
respond_with({}, :location => 'http://www.example.com')
end
end
it 'uses the given location' do
xhr :get, :index
response.headers['Location'].should eql 'http://www.example.com'
end
end
end
end
@netzpirat
Copy link
Author

You may want to check out the Model based form validation and row editor for Ext JS 4 that uses this responder.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment