Rails to_xml - arrays with non-ActiveRecord objects

Posted on December 29, 2007

If you've got an array that contains objects with nested class names like this:

module Foo
  class Bar
    def to_xml options = {} # :nodoc:
      builder = options[:builder] || Builder::XmlMarkup.new(options)
      builder.some_tag "some value"
    end
  end
end

def test_array_with_to_xml
  f = Foo::Bar.new
  puts [f].to_xml
end

test_array_with_to_xml

You'll end up with invalid xml:

<?xml version="1.0" encoding="UTF-8"?>
<foo/bars type="array">
  <some_tag>some value</some_tag>
</foo/bars>

That's because the array #to_xml (it's in /usr/local/lib/ruby/gems/1.8/gems/activesupport-2.0.2/lib/activesupport/coreext/array/conversions.rb on my machine) looks at the class of every object it contains, and then just calls #underscore and #pluralize on it like so:

options[:root]     ||= all? { |e| e.is_a?(first.class) && first.class.to_s != "Hash" } ? first.class.to_s.underscore.pluralize : "records"

That doesn't work if the class name is nested.

The workaround is to specify the root:

puts [f].to_xml(:root => "bars")

Rails and HTTP status codes

Posted on December 16, 2007

Status codes

Curious about how Rails deals with HTTP status codes? Take a look at statuscodes.rb (on my system, it's in /usr/local/lib/ruby/gems/1.8/gems/actionpack-2.0.1/lib/actioncontroller/status_codes.rb). When you do a

render :status => :reset_content

It's just looking up the string "reset content" in it's hash of codes-to-error messages. The error message strings are converted into stringswithunderscores, so "Reset Content" ends up as match for :reset_content.

Exceptions

In rescue.rb (on my system, /usr/local/lib/ruby/gems/1.8/gems/actionpack-2.0.1/lib/action_controller/rescue.rb), you'll see where some of the Rails exceptions are turned into status codes for you.

This is where you'll see things like ActiveRecord::RecordNotFound turning into a :not_found, which finally turns into a 404.

Controller status codes

In addition to the status codes for exceptions, Rails uses these status codes when it generates a controller for you:

  • :created (201) when #create is successful
  • :unprocessable_entity (422) when #create or #update fails

Sucessfull methods return :ok (200) otherwise.

The Rails code

status_codes.rb

module ActionController
  module StatusCodes #:nodoc:
    # Defines the standard HTTP status codes, by integer, with their
    # corresponding default message texts.
    # Source: http://www.iana.org/assignments/http-status-codes
    STATUS_CODES = {
      100 => "Continue",
      101 => "Switching Protocols",
      102 => "Processing",

      200 => "OK",
      201 => "Created",
      202 => "Accepted",
      203 => "Non-Authoritative Information",
      204 => "No Content",
      205 => "Reset Content",
      206 => "Partial Content",
      207 => "Multi-Status",
      226 => "IM Used",

      300 => "Multiple Choices",
      301 => "Moved Permanently",
      302 => "Found",
      303 => "See Other",
      304 => "Not Modified",
      305 => "Use Proxy",
      307 => "Temporary Redirect",

      400 => "Bad Request",
      401 => "Unauthorized",
      402 => "Payment Required",
      403 => "Forbidden",
      404 => "Not Found",
      405 => "Method Not Allowed",
      406 => "Not Acceptable",
      407 => "Proxy Authentication Required",
      408 => "Request Timeout",
      409 => "Conflict",
      410 => "Gone",
      411 => "Length Required",
      412 => "Precondition Failed",
      413 => "Request Entity Too Large",
      414 => "Request-URI Too Long",
      415 => "Unsupported Media Type",
      416 => "Requested Range Not Satisfiable",
      417 => "Expectation Failed",
      422 => "Unprocessable Entity",
      423 => "Locked",
      424 => "Failed Dependency",
      426 => "Upgrade Required",

      500 => "Internal Server Error",
      501 => "Not Implemented",
      502 => "Bad Gateway",
      503 => "Service Unavailable",
      504 => "Gateway Timeout",
      505 => "HTTP Version Not Supported",
      507 => "Insufficient Storage",
      510 => "Not Extended"
    }

    # Provides a symbol-to-fixnum lookup for converting a symbol (like
    # :created or :not_implemented) into its corresponding HTTP status
    # code (like 200 or 501).
    SYMBOL_TO_STATUS_CODE = STATUS_CODES.inject({}) do |hash, (code, message)|
      hash[message.gsub(/ /, "").underscore.to_sym] = code
      hash
    end

    # Given a status parameter, determine whether it needs to be converted
    # to a string. If it is a fixnum, use the STATUS_CODES hash to lookup
    # the default message. If it is a symbol, use the SYMBOL_TO_STATUS_CODE
    # hash to convert it.
    def interpret_status(status)
      case status
      when Fixnum then
        "#{status} #{STATUS_CODES[status]}".strip
      when Symbol then
        interpret_status(SYMBOL_TO_STATUS_CODE[status] ||
          "500 Unknown Status #{status.inspect}")
      else
        status.to_s
      end
    end
    private :interpret_status

  end
end

rescue.rb (excerpt)

DEFAULT_RESCUE_RESPONSE = :internal_server_error
DEFAULT_RESCUE_RESPONSES = {
  'ActionController::RoutingError'             => :not_found,
  'ActionController::UnknownAction'            => :not_found,
  'ActiveRecord::RecordNotFound'               => :not_found,
  'ActiveRecord::StaleObjectError'             => :conflict,
  'ActiveRecord::RecordInvalid'                => :unprocessable_entity,
  'ActiveRecord::RecordNotSaved'               => :unprocessable_entity,
  'ActionController::MethodNotAllowed'         => :method_not_allowed,
  'ActionController::NotImplemented'           => :not_implemented,
  'ActionController::InvalidAuthenticityToken' => :unprocessable_entity
}

DEFAULT_RESCUE_TEMPLATE = 'diagnostics'
DEFAULT_RESCUE_TEMPLATES = {
  'ActionController::MissingTemplate' => 'missing_template',
  'ActionController::RoutingError'    => 'routing_error',
  'ActionController::UnknownAction'   => 'unknown_action',
  'ActionView::TemplateError'         => 'template_error'
}

vlad and mongrel

Posted on December 13, 2007

vlad will create mongrel configuration files for you, but you're probably going to need to set the right mongrel variables.

For example, I have a demo that looks like:

set :mongrel_port, 4001
set :mongrel_servers, 3
set :mongrel_user, :restphone

Ruby for the Asterisk AJAM manager HTTP API

Posted on December 09, 2007

I'm not a big fan of line-oriented interfaces, so when I found the new (well, new-ish) Asterisk AJAX Manager Interface, AKA "AJAM," I figured it was a better fit for Ruby than the original.

My needs right now are very simple - set variables and redirect to a new extension. The variables that are set send Asterisk off to a new RESTPhone menu, where all the heavy lifting is done.

At some point this should become a gem, but here's the current, very simple, incarnation of how to talk to Asterisk via AJAM.

require 'rubygems'
require 'net/http'
require 'uri'
require 'xmlsimple'
require 'cgi'
require File.dirname(__FILE__) + '/rest_phone_utilities/uri_mixin'
require 'pp'
require 'pathname'

class AsteriskAjaxManager
  # The authorization cookie from the manager.
  attr_accessor :auth_cookie

  # The URI to send commands to.
  attr_accessor :uri

  # The Asterisk username
  attr_accessor :username
  # The Asterisk secret
  attr_accessor :secret

  # Takes:
  # 
  #   :uri => The URI for the Asterisk http server, not including /asterisk
  #   :path => The AJAM path (default: '/asterisk')
  #   :username => The Asterisk manager username (default: mark)
  #   :secret => The Asterisk manager password (default: mysecret)
  #   
  # (mark/mysecret is the default user in the Asterisk configuration file
  # samples.)
  def initialize(args = {})
    final_uri = args[:uri]
    final_uri ||= 'http://localhost:8088'
    final_uri = URI.parse(final_uri) unless URI === final_uri
    final_uri.path = '/asterisk' if final_uri.path.empty?
    final_uri.path = '/' if final_uri.path.empty?
    self.uri = final_uri
    self.username = args[:username] || 'mark'
    self.secret = args[:secret] || 'mysecret'
  end  

  def login
    login_uri = uri
    login_uri.path = (Pathname.new(uri.path) + 'manager').to_s
    login_uri.query = "action=login&username=#{username}&secret=#{secret}"

    response = Net::HTTP.get_response(login_uri)

    # The response should include an authorization cookie that we use 
    # on future requests.
    self.auth_cookie = response.response['set-cookie']
  end

  def execute
    login
    yield self
    logout
  end

  def logout

  end

  # ActionQuery is the http query sent off to AJAM.
  class ActionQuery
    attr_accessor :query
    # Takes:
    # 
    # [<tt>:uri</tt>]
    #   The uri (usually 'http://localhost:8088')
    # [<tt>:action</tt>]
    #   The action.  (Case-insensitive string or symbol)
    # [<tt>:params</tt>]
    #   Parameters for the action
    #   
    # Actions are:
    # 
    # status::
    # sippeers::
    def initialize(args = {})
      u = args[:uri]
      u = UriWithQueryCreation.parse(u.to_s)
      u.path = '/asterisk/mxml'
      u.append_hash_to_query 'action' => args[:action]
      u.append_hash_to_query args[:params]
      self.query = u
    end

    def to_s
      query.to_s
    end

    def net_http_obj
      Net::HTTP::Get.new("#{query.path}?#{query.query}")
    end
  end

  def sip_peers
    args = append_standard_arguments(:action => :sippeers)
    result = do_ajam_cmd_return_xml(args)
    result['response'][1..-2].map {|m| m['generic'].first}
  end

  def extension_state args = {}
    args = append_standard_arguments(:action => :extensionstate, :params => args)
    result = do_ajam_cmd_return_xml(args)
    result['response'].first['generic'].first
  end

  def redirect args = {}
    args = append_standard_arguments(
      :action => :redirect,
      :params => args
    )
    result = do_ajam_cmd_return_xml(args)
    result = result['response'].first['generic'].first
    if result['response'] =~ /Error/
      raise AsteriskAjaxManagerError, result['message']
    end
  end

  def setvar args = {}
    args = append_standard_arguments(
      :action => :setvar,
      :params => args
    )
    result = do_ajam_cmd_return_xml(args)
    result = result['response'].first['generic'].first
    if result['response'] =~ /Error/
      raise AsteriskAjaxManagerError, result['message']
    end
  end

  def append_standard_arguments(args = {})
    args.merge(:uri => uri, :cookie => auth_cookie)
  end

  # Takes:
  #   :uri => the uri (usually http://localhost) as a String or URI object
  #   :action => the action (status, SIPpeers, etc) as a symbol
  #   :cookie => the authorization cookie
  def do_ajam_cmd(args = {})
    u = args[:uri]
    u = UriWithQueryCreation.parse(u.to_s)
    result = Net::HTTP.start(u.host, u.port) do |h|
      q = ActionQuery.new :uri => 'http://localhost:8088/asterisk/mxml', :action => args[:action].to_s,
        :params => args[:params]
      req = q.net_http_obj
      req.add_field('Cookie', args[:cookie])
      h.request(req)
    end
    result.body
  end

  def do_ajam_cmd_return_xml(args = {})
    result = do_ajam_cmd(args)
    XmlSimple.xml_in(result)
  end

  class AsteriskAjaxManagerError < RuntimeError
    class LoginError < AsteriskAjaxManagerError
    end
  end
end

MACRO_RECURSION and the Asterisk manager (AMI)

Posted on December 08, 2007

If you're using the Asterisk manager REDIRECT command, and your calls spend a lot of their time in macros, you'll probably need to change MACRO_RECURSION. I was seeing messages like this:

[Dec  9 14:23:29] ERROR[19509]: app_macro.c:193 _macro_exec: Macro():  possible infinite loop detected.  Returning early.

The macro code is deciding that's it too deep, since the REDIRECT just pushes another level onto the stack.

I'm working around this by just making the MACRO_RECURSION large in my extensions.ael file:

s = {
  MACRO_RECURSION=10000;
  ...

Which, according to "dialplan show" turns into the old style:

Set(MACRO_RECURSION=$[10000])

Note about adding the variable