Archive for the 'rails' Category

Rails Layered Dispatching Hints

For my current Rails project I’m setting up my web services with layered dispatch, which nicely organizes all the methods while keeping them at the same endpoint URI. There’s some good documentation already out there in the Agile Web Development with Rails book as well as the ActionWebServices manual, but I have a few extra tips that could be helpful.

If we’re using a before_invocation interceptor, like say for authentication, we can clean things up by creating a subclass of ActionWebService::Base that includes the authentication method:

app/apis/authenticated_web_service.rb

class AuthenticatedWebService < ActionWebService::Base
  def authenticate name, args
    @authenticated_user = User.find_by_login args[0]
    unless @authenticated_user.authenticated?(args[1])
      raise "Not authenticated"
    end
  end
end

Because we are actually using an instance variable here, @authenticated_user, we have to set up our layered web service controller to use instances of the services rather than the classes.

app/controllers/services_controller.rb

class ServicesController < ApplicationController
  web_service_dispatching_mode :layered
  wsdl_service_name 'contract_services'
  web_service_scaffold :invoke

  # Here we use instances...
  web_service(:contract) {ContractService.new}
  web_service(:account) {AccountService.new}

  # rather than classes...
  # web_service :contract, ContractService
  # web_service :account, AccountService
end

Then your various services can inherit from this “abstract” AuthenticatedWebService. Each can make use of the authenticate method and the @authenticated_user variable.

app/apis/contract_service.rb

class ContractApi < ActionWebService::API::Base

  api_method :all,
             :returns => [[Contract]]
  api_method :new,
             :expects => [{:login => :string},
                          {:password => :string},
                          {:description => :string}],
             :returns => [Contract]
end

class ContractService < AuthenticatedWebService
  web_service_api ContractApi

  def all
    Contract.find :all
  end

  def find contract_id
    c = Contract.find(contract_id)
    raise "Contract not found." if c.nil?
    c
  end

  ## Authenticated API Methods
  before_invocation :authenticate, \:only => [:new]

  def new l, p, description
    c = Contract.new :creator => @authenticated_user,
                     :description => description
    c.save!
    c
  end
end

Once your service is put together, keep in mind that XML-RPC requests will use a method of the form service.method. So for example, my contract service’s find method is contract.find. I have to admit that this really threw me off for a little while when I was testing out the API in XML-RPC Client and I kept getting back a “no such web service ‘api’” error message. The manual does point this out, but I missed it the first time, and only on a close reread did I figure this out, at which point I realized how much good sense this approach makes. Note that for SOAP requests the service API is encoded in the header.

One more final note is that Rails logger object is not available in the layered services. This could be a deal breaker in certain cases.