Tweaked the REST API for ActiveResource compatibility. Introduced a plugin I'm calling to_xml_rails2_style that patches Rails 1.2.x to make Hash#to_xml and Array#to_xml have the same behavior as in Rails 2.0. This means that people can use ActiveResource as a client to consume their Tracks data. See the new example client in doc/tracks_api_wrapper.rb.

git-svn-id: http://www.rousette.org.uk/svn/tracks-repos/trunk@672 a4c988fc-2ded-0310-b66e-134b36920a42
This commit is contained in:
lukemelia 2007-12-04 06:24:23 +00:00
parent 1d8a9c877b
commit 36c35a7a86
11 changed files with 213 additions and 45 deletions

View file

@ -64,7 +64,7 @@ class ContextsController < ApplicationController
elsif @context.new_record?
render_failure @context.errors.to_xml, 409
else
render :xml => @context.to_xml( :except => :user_id ), :status => 201
head :created, :location => context_url(@context)
end
end
end

View file

@ -71,7 +71,7 @@ class ProjectsController < ApplicationController
elsif @project.new_record?
render_failure @project.errors.full_messages.join(', ')
else
render :xml => @project.to_xml( :except => :user_id )
head :created, :location => project_url(@project)
end
end
end

View file

@ -18,7 +18,7 @@ class TodosController < ApplicationController
respond_to do |format|
format.html &render_todos_html
format.m &render_todos_mobile
format.xml { render :action => 'list.rxml', :layout => false }
format.xml { render :xml => @todos.to_xml( :except => :user_id ) }
format.rss &render_rss_feed
format.atom &render_atom_feed
format.text &render_text_feed
@ -78,7 +78,7 @@ class TodosController < ApplicationController
end
format.xml do
if @saved
render :xml => @todo.to_xml( :root => 'todo', :except => :user_id ), :status => 201
head :created, :location => todo_url(@todo)
else
render :xml => @todo.errors.to_xml, :status => 422
end

View file

@ -1,14 +0,0 @@
@headers["Content-Type"] = "text/xml; charset=utf-8"
xml.todos_by_context do
@contexts_to_show.each do |c|
xml.context do
xml.name c.name
xml.hide c.hide, :type => :boolean
xml.id c.id, :type => :integer
xml.position c.position, :type => :integer
c.not_done_todos.each do |t|
xml << t.to_xml( :skip_instruct => true, :root => 'todo', :except => [:user_id, :context_id] )
end
end
end
end

View file

@ -11,7 +11,7 @@ ActionController::Routing::Routes.draw do |map|
login.open_id_complete 'complete', :action => 'complete'
login.formatted_open_id_complete 'complete.:format', :action => 'complete'
end
map.resources :users,
:member => {:change_password => :get, :update_password => :post,
:change_auth_type => :get, :update_auth_type => :post, :complete => :get,

View file

@ -0,0 +1,39 @@
require 'activeresource'
# Install the ActiveResource gem if you don't already have it:
#
# sudo gem install activeresource --source http://gems.rubyonrails.org --include-dependencies
# $ SITE="http://myusername:p4ssw0rd@mytracksinstallation.com" irb -r tracks_api_wrapper.rb
#
# >> my_pc = Tracks::Context.find(:first)
# => #<Tracks::Context:0x139c3c0 @prefix_options={}, @attributes={"name"=>"my pc", "updated_at"=>Mon Aug 13 02:56:18 UTC 2007, "hide"=>0, "id"=>8, "position"=>1, "created_at"=>Wed Feb 28 07:07:28 UTC 2007}
# >> my_pc.name
# => "my pc"
# >> my_pc.todos
# => [#<Tracks::Todo:0x1e16b84 @prefix_options={}, @attributes={"context_id"=>8, "completed_at"=>Tue Apr 10 12:57:24 UTC 2007, "project_id"=>nil, "show_from"=>nil, "id"=>1432, "notes"=>nil, "description"=>"check rhino mocks bug", "due"=>Mon, 09 Apr 2007, "created_at"=>Sun Apr 08 04:56:35 UTC 2007, "state"=>"completed"}, #<Tracks::Todo:0x1e16b70 @prefix_options={}, @attributes={"context_id"=>8, "completed_at"=>Mon Oct 10 13:10:21 UTC 2005, "project_id"=>10, "show_from"=>nil, "id"=>17, "notes"=>"fusion problem", "description"=>"Fix Client Installer", "due"=>nil, "created_at"=>Sat Oct 08 00:19:33 UTC 2005, "state"=>"completed"}]
module Tracks
class Base < ActiveResource::Base
self.site = ENV["SITE"] || "http://username:password@0.0.0.0:3000/"
end
class Todo < Base
end
class Context < Base
def todos
return attributes["todos"] if attributes.keys.include?("todos")
return Todo.find(:all, :params => {:context_id => id})
end
end
class Project < Base
def todos
return attributes["todos"] if attributes.keys.include?("todos")
return Todo.find(:all, :params => {:project_id => id})
end
end
end

View file

@ -82,10 +82,9 @@ class TodosControllerTest < Test::Rails::TestCase
def test_create_todo_via_xml
login_as(:admin_user)
put :create, :format => "xml", "request" => { "context_name"=>"library", "project_name"=>"Build a working time machine", "todo"=>{"notes"=>"", "description"=>"Call Warren Buffet to find out how much he makes per day", "due"=>"30/11/2006"}, "tag_list"=>"foo bar" }
assert_response 201
assert_xml_select "todo" do
assert_xml_select "id", /\d+/
assert_difference Todo, :count do
put :create, :format => "xml", "request" => { "context_name"=>"library", "project_name"=>"Build a working time machine", "todo"=>{"notes"=>"", "description"=>"Call Warren Buffet to find out how much he makes per day", "due"=>"30/11/2006"}, "tag_list"=>"foo bar" }
assert_response 201
end
end

View file

@ -55,18 +55,10 @@ class ContextXmlApiTest < ActionController::IntegrationTest
end
def test_creates_new_context
initial_count = Context.count
authenticated_post_xml_to_context_create
assert_response 201
assert_xml_select 'context' do
assert_select 'created-at', /\d{4}+-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z/
assert_select 'hide', /false|0/ #TODO: Figure out schema issues
assert_select 'id', /\d+/
assert_select 'name', @@context_name
assert_select 'position', '3'
assert_select 'updated-at', /\d{4}+-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z/
assert_difference Context, :count do
authenticated_post_xml_to_context_create
assert_response 201
end
assert_equal initial_count + 1, Context.count
context1 = Context.find_by_name(@@context_name)
assert_not_nil context1, "expected context '#{@@context_name}' to be created"
end

View file

@ -49,18 +49,10 @@ class ProjectXmlApiTest < ActionController::IntegrationTest
end
def test_creates_new_project
initial_count = Project.count
authenticated_post_xml_to_project_create
assert_response :success
assert_xml_select 'project' do
assert_xml_select "description"
assert_xml_select 'id[type="integer"]', /[0-9]+/
assert_xml_select 'name', @@project_name
assert_xml_select 'position[type="integer"]', 1
assert_xml_select 'state', 'active'
assert_difference Project, :count do
authenticated_post_xml_to_project_create
assert_response :created
end
#assert_response_and_body_matches 200, %r|^<\?xml version="1\.0" encoding="UTF-8"\?>\n<project>\n <description></description>\n <id type=\"integer\">[0-9]+</id>\n <name>#{@@project_name}</name>\n <position type=\"integer\">1</position>\n <state>active</state>\n</project>$|
assert_equal initial_count + 1, Project.count
project1 = Project.find_by_name(@@project_name)
assert_not_nil project1, "expected project '#{@@project_name}' to be created"
end

View file

@ -0,0 +1 @@
require 'to_xml_patches'

View file

@ -0,0 +1,159 @@
# Extensions needed for Hash#to_query
class Array
def to_query(key) #:nodoc:
collect { |value| value.to_query("#{key}[]") } * '&'
end
end
module ActiveSupport #:nodoc:
module CoreExtensions #:nodoc:
module Array #:nodoc:
module Conversions
def to_xml(options = {})
raise "Not all elements respond to to_xml" unless all? { |e| e.respond_to? :to_xml }
options[:root] ||= all? { |e| e.is_a?(first.class) && first.class.to_s != "Hash" } ? first.class.to_s.underscore.pluralize : "records"
options[:children] ||= options[:root].singularize
options[:indent] ||= 2
options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
root = options.delete(:root).to_s
children = options.delete(:children)
if !options.has_key?(:dasherize) || options[:dasherize]
root = root.dasherize
end
options[:builder].instruct! unless options.delete(:skip_instruct)
opts = options.merge({ :root => children })
xml = options[:builder]
if empty?
xml.tag!(root, options[:skip_types] ? {} : {:type => "array"})
else
xml.tag!(root, options[:skip_types] ? {} : {:type => "array"}) {
yield xml if block_given?
each { |e| e.to_xml(opts.merge!({ :skip_instruct => true })) }
}
end
end
end
end
end
end
module ActiveSupport #:nodoc:
module CoreExtensions #:nodoc:
module Hash #:nodoc:
module Conversions
XML_TYPE_NAMES["Symbol"] = "symbol"
XML_TYPE_NAMES["BigDecimal"] = "decimal"
XML_FORMATTING["symbol"] = Proc.new { |symbol| symbol.to_s }
XML_FORMATTING["yaml"] = Proc.new { |yaml| yaml.to_yaml }
unless defined?(XML_PARSING)
XML_PARSING = {
"symbol" => Proc.new { |symbol| symbol.to_sym },
"date" => Proc.new { |date| ::Date.parse(date) },
"datetime" => Proc.new { |time| ::Time.parse(time).utc },
"integer" => Proc.new { |integer| integer.to_i },
"float" => Proc.new { |float| float.to_f },
"decimal" => Proc.new { |number| BigDecimal(number) },
"boolean" => Proc.new { |boolean| %w(1 true).include?(boolean.strip) },
"string" => Proc.new { |string| string.to_s },
"yaml" => Proc.new { |yaml| YAML::load(yaml) rescue yaml },
"base64Binary" => Proc.new { |bin| Base64.decode64(bin) },
# FIXME: Get rid of eval and institute a proper decorator here
"file" => Proc.new do |file, entity|
f = StringIO.new(Base64.decode64(file))
eval "def f.original_filename() '#{entity["name"]}' || 'untitled' end"
eval "def f.content_type() '#{entity["content_type"]}' || 'application/octet-stream' end"
f
end
}
XML_PARSING.update(
"double" => XML_PARSING["float"],
"dateTime" => XML_PARSING["datetime"]
)
end
module ClassMethods
def from_xml(xml)
# TODO: Refactor this into something much cleaner that doesn't rely on XmlSimple
typecast_xml_value(undasherize_keys(XmlSimple.xml_in_string(xml,
'forcearray' => false,
'forcecontent' => true,
'keeproot' => true,
'contentkey' => '__content__')
))
end
private
def typecast_xml_value(value)
case value.class.to_s
when "Hash"
if value.has_key?("__content__")
content = translate_xml_entities(value["__content__"])
if parser = XML_PARSING[value["type"]]
if parser.arity == 2
XML_PARSING[value["type"]].call(content, value)
else
XML_PARSING[value["type"]].call(content)
end
else
content
end
elsif value['type'] == 'array'
child_key, entries = value.detect { |k,v| k != 'type' } # child_key is throwaway
if entries.nil?
[]
else
case entries.class.to_s # something weird with classes not matching here. maybe singleton methods breaking is_a?
when "Array"
entries.collect { |v| typecast_xml_value(v) }
when "Hash"
[typecast_xml_value(entries)]
else
raise "can't typecast #{entries.inspect}"
end
end
elsif value['type'] == 'string' && value['nil'] != 'true'
""
else
xml_value = (value.blank? || value['type'] || value['nil'] == 'true') ? nil : value.inject({}) do |h,(k,v)|
h[k] = typecast_xml_value(v)
h
end
# Turn { :files => { :file => #<StringIO> } into { :files => #<StringIO> } so it is compatible with
# how multipart uploaded files from HTML appear
if xml_value.is_a?(Hash) && xml_value["file"].is_a?(StringIO)
xml_value["file"]
else
xml_value
end
end
when "Array"
value.map! { |i| typecast_xml_value(i) }
case value.length
when 0 then nil
when 1 then value.first
else value
end
when "String"
value
else
raise "can't typecast #{value.inspect}"
end
end
end
end
end
end
end