mirror of
https://github.com/TracksApp/tracks.git
synced 2025-09-22 05:50:47 +02:00
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:
parent
1d8a9c877b
commit
36c35a7a86
11 changed files with 213 additions and 45 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
39
tracks/doc/tracks_api_wrapper.rb
Normal file
39
tracks/doc/tracks_api_wrapper.rb
Normal 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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
1
tracks/vendor/plugins/to_xml_rails2_style/init.rb
vendored
Normal file
1
tracks/vendor/plugins/to_xml_rails2_style/init.rb
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
require 'to_xml_patches'
|
159
tracks/vendor/plugins/to_xml_rails2_style/lib/to_xml_patches.rb
vendored
Normal file
159
tracks/vendor/plugins/to_xml_rails2_style/lib/to_xml_patches.rb
vendored
Normal 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
|
Loading…
Add table
Add a link
Reference in a new issue