diff --git a/activeresource/README b/activeresource/README
new file mode 100644
index 0000000000..f1013b54e0
--- /dev/null
+++ b/activeresource/README
@@ -0,0 +1 @@
+= Active Resource -- Object-oriented REST services
\ No newline at end of file
diff --git a/activeresource/Rakefile b/activeresource/Rakefile
new file mode 100644
index 0000000000..30c57cf8ed
--- /dev/null
+++ b/activeresource/Rakefile
@@ -0,0 +1,137 @@
+require 'rubygems'
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+require 'rake/packagetask'
+require 'rake/gempackagetask'
+require 'rake/contrib/rubyforgepublisher'
+require File.join(File.dirname(__FILE__), 'lib', 'active_resource', 'version')
+
+PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
+PKG_NAME = 'activeresource'
+PKG_VERSION = ActiveResource::VERSION::STRING + PKG_BUILD
+PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
+
+RELEASE_NAME = "REL #{PKG_VERSION}"
+
+RUBY_FORGE_PROJECT = "activerecord"
+RUBY_FORGE_USER = "webster132"
+
+PKG_FILES = FileList[
+ "lib/**/*", "test/**/*", "examples/**/*", "doc/**/*", "[A-Z]*", "install.rb", "Rakefile"
+].exclude(/\bCVS\b|~$/)
+
+desc "Default Task"
+task :default => [ :test ]
+
+# Run the unit tests
+
+Rake::TestTask.new { |t|
+ t.libs << "test"
+ t.pattern = 'test/*_test.rb'
+ t.verbose = true
+}
+
+
+# Generate the RDoc documentation
+
+Rake::RDocTask.new { |rdoc|
+ rdoc.rdoc_dir = 'doc'
+ rdoc.title = "Active Resource -- Object-oriented REST services"
+ rdoc.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object'
+ rdoc.template = "#{ENV['template']}.rb" if ENV['template']
+ rdoc.rdoc_files.include('README', 'RUNNING_UNIT_TESTS', 'CHANGELOG')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+ rdoc.rdoc_files.exclude('lib/active_record/vendor/*')
+ rdoc.rdoc_files.include('dev-utils/*.rb')
+}
+
+
+# Create compressed packages
+
+dist_dirs = [ "lib", "test", "examples", "dev-utils" ]
+
+spec = Gem::Specification.new do |s|
+ s.name = PKG_NAME
+ s.version = PKG_VERSION
+ s.summary = "Implements the ActiveRecord pattern for ORM."
+ s.description = %q{Implements the ActiveRecord pattern (Fowler, PoEAA) for ORM. It ties database tables and classes together for business objects, like Customer or Subscription, that can find, save, and destroy themselves without resorting to manual SQL.}
+
+ s.files = [ "Rakefile", "install.rb", "README", "RUNNING_UNIT_TESTS", "CHANGELOG" ]
+ dist_dirs.each do |dir|
+ s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "\.svn" ) }
+ end
+
+ s.add_dependency('activesupport', '= 1.3.1' + PKG_BUILD)
+
+ s.files.delete "test/fixtures/fixture_database.sqlite"
+ s.files.delete "test/fixtures/fixture_database_2.sqlite"
+ s.files.delete "test/fixtures/fixture_database.sqlite3"
+ s.files.delete "test/fixtures/fixture_database_2.sqlite3"
+ s.require_path = 'lib'
+ s.autorequire = 'active_record'
+
+ s.has_rdoc = true
+ s.extra_rdoc_files = %w( README )
+ s.rdoc_options.concat ['--main', 'README']
+
+ s.author = "David Heinemeier Hansson"
+ s.email = "david@loudthinking.com"
+ s.homepage = "http://www.rubyonrails.org"
+ s.rubyforge_project = "activerecord"
+end
+
+Rake::GemPackageTask.new(spec) do |p|
+ p.gem_spec = spec
+ p.need_tar = true
+ p.need_zip = true
+end
+
+task :lines do
+ lines, codelines, total_lines, total_codelines = 0, 0, 0, 0
+
+ for file_name in FileList["lib/active_record/**/*.rb"]
+ next if file_name =~ /vendor/
+ f = File.open(file_name)
+
+ while line = f.gets
+ lines += 1
+ next if line =~ /^\s*$/
+ next if line =~ /^\s*#/
+ codelines += 1
+ end
+ puts "L: #{sprintf("%4d", lines)}, LOC #{sprintf("%4d", codelines)} | #{file_name}"
+
+ total_lines += lines
+ total_codelines += codelines
+
+ lines, codelines = 0, 0
+ end
+
+ puts "Total: Lines #{total_lines}, LOC #{total_codelines}"
+end
+
+
+# Publishing ------------------------------------------------------
+
+desc "Publish the beta gem"
+task :pgem => [:package] do
+ Rake::SshFilePublisher.new("davidhh@wrath.rubyonrails.org", "public_html/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload
+ `ssh davidhh@wrath.rubyonrails.org './gemupdate.sh'`
+end
+
+desc "Publish the API documentation"
+task :pdoc => [:rdoc] do
+ Rake::SshDirPublisher.new("davidhh@wrath.rubyonrails.org", "public_html/ar", "doc").upload
+end
+
+desc "Publish the release files to RubyForge."
+task :release => [ :package ] do
+ `rubyforge login`
+
+ for ext in %w( gem tgz zip )
+ release_command = "rubyforge add_release #{PKG_NAME} #{PKG_NAME} 'REL #{PKG_VERSION}' pkg/#{PKG_NAME}-#{PKG_VERSION}.#{ext}"
+ puts release_command
+ system(release_command)
+ end
+end
diff --git a/activeresource/lib/active_resource.rb b/activeresource/lib/active_resource.rb
new file mode 100644
index 0000000000..529675a59c
--- /dev/null
+++ b/activeresource/lib/active_resource.rb
@@ -0,0 +1,38 @@
+#--
+# Copyright (c) 2006 David Heinemeier Hansson
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#++
+
+$:.unshift(File.dirname(__FILE__)) unless
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
+
+unless defined?(ActiveSupport)
+ begin
+ $:.unshift(File.dirname(__FILE__) + "/../../activesupport/lib")
+ require 'active_support'
+ rescue LoadError
+ require 'rubygems'
+ require_gem 'activesupport'
+ end
+end
+
+require 'active_resource/base'
+require 'active_resource/struct'
\ No newline at end of file
diff --git a/activeresource/lib/active_resource/base.rb b/activeresource/lib/active_resource/base.rb
new file mode 100644
index 0000000000..2d4025e5dd
--- /dev/null
+++ b/activeresource/lib/active_resource/base.rb
@@ -0,0 +1,99 @@
+require 'active_resource/connection'
+
+module ActiveResource
+ class Base
+ class << self
+ def site=(site)
+ @@site = URI.parse(site)
+ end
+
+ def site
+ @@site
+ end
+
+ def connection(refresh = false)
+ @connection = Connection.new(site) if refresh || @connection.nil?
+ @connection
+ end
+
+ def element_name
+ self.to_s.underscore
+ end
+
+ def collection_name
+ element_name.pluralize
+ end
+
+ def element_path(id)
+ "/#{collection_name}/#{id}.xml"
+ end
+
+ def collection_path
+ "/#{collection_name}.xml"
+ end
+
+ def find(*arguments)
+ scope = arguments.slice!(0)
+
+ case scope
+ when Fixnum
+ # { :person => person1 }
+ new(connection.get(element_path(scope)).values.first)
+ when :all
+ # { :people => { :person => [ person1, person2 ] } }
+ connection.get(collection_path).values.first.values.first.collect { |element| new(element) }
+ when :first
+ find(:all, *arguments).first
+ end
+ end
+ end
+
+ attr_accessor :attributes
+
+ def initialize(attributes = {})
+ @attributes = attributes
+ end
+
+ def id
+ attributes["id"]
+ end
+
+ def id=(id)
+ attributes["id"] = id
+ end
+
+ def save
+ update
+ end
+
+ def destroy
+ connection.delete(self.class.element_path(id))
+ end
+
+ def to_xml
+ attributes.to_xml(:root => self.class.element_name)
+ end
+
+ protected
+ def connection(refresh = false)
+ self.class.connection(refresh)
+ end
+
+ def update
+ connection.put(self.class.element_path(id), to_xml)
+ end
+
+ def method_missing(method_symbol, *arguments)
+ method_name = method_symbol.to_s
+
+ case method_name.last
+ when "="
+ attributes[method_name.first(-1)] = arguments.first
+ when "?"
+ # TODO
+ else
+ attributes[method_name] || super
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/activeresource/lib/active_resource/connection.rb b/activeresource/lib/active_resource/connection.rb
new file mode 100644
index 0000000000..7318e7256f
--- /dev/null
+++ b/activeresource/lib/active_resource/connection.rb
@@ -0,0 +1,86 @@
+require 'net/https'
+require 'date'
+require 'time'
+require 'uri'
+
+module ActiveResource
+ class ConnectionError < StandardError
+ attr_reader :response
+
+ def initialize(response, message = nil)
+ @response = response
+ @message = message
+ end
+
+ def to_s
+ "Failed with #{response.code}"
+ end
+ end
+
+ class ClientError < ConnectionError
+ end
+
+ class ServerError < ConnectionError
+ end
+
+ class ResourceNotFound < ClientError
+ end
+
+ class Connection
+ attr_accessor :uri
+
+ class << self
+ def requests
+ @@requests ||= []
+ end
+ end
+
+ def initialize(site)
+ @site = site
+ end
+
+ def get(path)
+ Hash.create_from_xml(request(:get, path).body)
+ end
+
+ def delete(path)
+ request(:delete, path)
+ end
+
+ def put(path, body)
+ request(:put, path, body)
+ end
+
+ def post(path, body)
+ request(:post, path, body)
+ end
+
+ private
+ def request(method, *arguments)
+ response = http.send(method, *arguments)
+
+ case response.code.to_i
+ when 200...300
+ response
+ when 404
+ raise(ResourceNotFound.new(response))
+ when 400...500
+ raise(ClientError.new(response))
+ when 500...600
+ raise(ServerError.new(response))
+ else
+ raise(ConnectionError.new(response, "Unknown response code: #{response.code}"))
+ end
+ end
+
+ def http
+ unless @http
+ @http = Net::HTTP.new(@site.host, @site.port)
+ @http.use_ssl = @site.is_a?(URI::HTTPS)
+ @http.verify_mode = OpenSSL::SSL::VERIFY_NONE if @http.use_ssl
+ end
+
+ @http
+ end
+ end
+end
\ No newline at end of file
diff --git a/activeresource/lib/active_resource/struct.rb b/activeresource/lib/active_resource/struct.rb
new file mode 100644
index 0000000000..6f4ffecc20
--- /dev/null
+++ b/activeresource/lib/active_resource/struct.rb
@@ -0,0 +1,7 @@
+module ActiveResource
+ class Struct
+ def self.create
+ Class.new(Base)
+ end
+ end
+end
\ No newline at end of file
diff --git a/activeresource/lib/active_resource/version.rb b/activeresource/lib/active_resource/version.rb
new file mode 100644
index 0000000000..f4e3220932
--- /dev/null
+++ b/activeresource/lib/active_resource/version.rb
@@ -0,0 +1,9 @@
+module ActiveResource
+ module VERSION #:nodoc:
+ MAJOR = 0
+ MINOR = 5
+ TINY = 0
+
+ STRING = [MAJOR, MINOR, TINY].join('.')
+ end
+end
diff --git a/activeresource/test/abstract_unit.rb b/activeresource/test/abstract_unit.rb
new file mode 100644
index 0000000000..ddc00e307c
--- /dev/null
+++ b/activeresource/test/abstract_unit.rb
@@ -0,0 +1,8 @@
+$:.unshift(File.dirname(__FILE__) + '/../lib')
+$:.unshift(File.dirname(__FILE__) + '/.')
+
+require 'active_resource'
+require 'test/unit'
+require 'active_support/breakpoint'
+
+require "#{File.dirname(__FILE__)}/http_mock"
\ No newline at end of file
diff --git a/activeresource/test/base_test.rb b/activeresource/test/base_test.rb
new file mode 100644
index 0000000000..3c87ca532c
--- /dev/null
+++ b/activeresource/test/base_test.rb
@@ -0,0 +1,60 @@
+require "#{File.dirname(__FILE__)}/abstract_unit"
+require "fixtures/person"
+
+class BaseTest < Test::Unit::TestCase
+ def setup
+ ActiveResource::HttpMock.respond_to(
+ ActiveResource::Request.new(:get, "/people/1.xml") => ActiveResource::Response.new("Matz1"),
+ ActiveResource::Request.new(:get, "/people/2.xml") => ActiveResource::Response.new("David2"),
+ ActiveResource::Request.new(:put, "/people/1.xml") => ActiveResource::Response.new({}, 200),
+ ActiveResource::Request.new(:delete, "/people/1.xml") => ActiveResource::Response.new({}, 200),
+ ActiveResource::Request.new(:delete, "/people/2.xml") => ActiveResource::Response.new({}, 400),
+ ActiveResource::Request.new(:post, "/people.xml") => ActiveResource::Response.new({}, 200),
+ ActiveResource::Request.new(:get, "/people/99.xml") => ActiveResource::Response.new({}, 404),
+ ActiveResource::Request.new(:get, "/people.xml") => ActiveResource::Response.new(
+ "Matz1David2"
+ )
+ )
+ end
+
+ def test_collection_name
+ assert_equal "people", Person.collection_name
+ end
+
+ def test_find_by_id
+ matz = Person.find(1)
+ assert_kind_of Person, matz
+ assert_equal "Matz", matz.name
+ end
+
+ def test_find_all
+ all = Person.find(:all)
+ assert_equal 2, all.size
+ assert_kind_of Person, all.first
+ assert_equal "Matz", all.first.name
+ assert_equal "David", all.last.name
+ end
+
+ def test_find_first
+ matz = Person.find(:first)
+ assert_kind_of Person, matz
+ assert_equal "Matz", matz.name
+ end
+
+ def test_find_by_id_not_found
+ assert_raises(ActiveResource::ResourceNotFound) { Person.find(99) }
+ end
+
+ def test_update
+ matz = Person.find(:first)
+ matz.name = "David"
+ assert_kind_of Person, matz
+ assert_equal "David", matz.name
+ matz.save
+ end
+
+ def test_destroy
+ assert Person.find(1).destroy
+ assert_raises(ActiveResource::ClientError) { Person.find(2).destroy }
+ end
+end
\ No newline at end of file
diff --git a/activeresource/test/connection_test.rb b/activeresource/test/connection_test.rb
new file mode 100644
index 0000000000..1c5951ca91
--- /dev/null
+++ b/activeresource/test/connection_test.rb
@@ -0,0 +1,11 @@
+require "#{File.dirname(__FILE__)}/abstract_unit"
+require "fixtures/person"
+
+class ConnectionTest < Test::Unit::TestCase
+ def setup
+ end
+
+ def test_something
+ true
+ end
+end
\ No newline at end of file
diff --git a/activeresource/test/fixtures/person.rb b/activeresource/test/fixtures/person.rb
new file mode 100644
index 0000000000..4914863230
--- /dev/null
+++ b/activeresource/test/fixtures/person.rb
@@ -0,0 +1,3 @@
+class Person < ActiveResource::Base
+ self.site = "http://37s.sunrise.i:3000/"
+end
\ No newline at end of file
diff --git a/activeresource/test/http_mock.rb b/activeresource/test/http_mock.rb
new file mode 100644
index 0000000000..a4bc7e7cb2
--- /dev/null
+++ b/activeresource/test/http_mock.rb
@@ -0,0 +1,84 @@
+require 'active_resource/connection'
+
+module ActiveResource
+ class HttpMock
+ class << self
+ def requests
+ @@requests ||= []
+ end
+
+ def responses
+ @@responses ||= {}
+ end
+
+ def respond_to(pairs)
+ reset!
+ pairs.each do |(path, response)|
+ responses[path] = response
+ end
+ end
+
+ def reset!
+ requests.clear
+ responses.clear
+ end
+ end
+
+ for method in [ :post, :put, :get, :delete ]
+ module_eval <<-EOE
+ def #{method}(*arguments)
+ request = ActiveResource::Request.new(:#{method}, *arguments)
+ self.class.requests << request
+ self.class.responses[request] || raise("No response recorded for: \#{request}")
+ end
+ EOE
+ end
+
+ def initialize(site)
+ @site = site
+ end
+ end
+
+ class Request
+ attr_accessor :path, :method, :body
+
+ def initialize(method, path, body = nil)
+ @method, @path, @body = method, path, body
+ end
+
+ def ==(other_request)
+ other_request.hash == hash
+ end
+
+ def eql?(other_request)
+ self == other_request
+ end
+
+ def to_s
+ "<#{method.to_s.upcase}: #{path} (#{body})>"
+ end
+
+ def hash
+ "#{path}#{method}".hash
+ end
+ end
+
+ class Response
+ attr_accessor :body, :code
+
+ def initialize(body, code = 200)
+ @body, @code = body, code
+ end
+
+ def success?
+ (200..299).include?(code)
+ end
+ end
+
+ class Connection
+ private
+ def http
+ @http ||= HttpMock.new(@site)
+ end
+ end
+end
\ No newline at end of file