mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Initial check-in of Active Resourse
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@4492 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
This commit is contained in:
parent
a55265132b
commit
99d268c853
12 changed files with 543 additions and 0 deletions
1
activeresource/README
Normal file
1
activeresource/README
Normal file
|
@ -0,0 +1 @@
|
|||
= Active Resource -- Object-oriented REST services
|
137
activeresource/Rakefile
Normal file
137
activeresource/Rakefile
Normal file
|
@ -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
|
38
activeresource/lib/active_resource.rb
Normal file
38
activeresource/lib/active_resource.rb
Normal file
|
@ -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'
|
99
activeresource/lib/active_resource/base.rb
Normal file
99
activeresource/lib/active_resource/base.rb
Normal file
|
@ -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
|
86
activeresource/lib/active_resource/connection.rb
Normal file
86
activeresource/lib/active_resource/connection.rb
Normal file
|
@ -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
|
7
activeresource/lib/active_resource/struct.rb
Normal file
7
activeresource/lib/active_resource/struct.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
module ActiveResource
|
||||
class Struct
|
||||
def self.create
|
||||
Class.new(Base)
|
||||
end
|
||||
end
|
||||
end
|
9
activeresource/lib/active_resource/version.rb
Normal file
9
activeresource/lib/active_resource/version.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
module ActiveResource
|
||||
module VERSION #:nodoc:
|
||||
MAJOR = 0
|
||||
MINOR = 5
|
||||
TINY = 0
|
||||
|
||||
STRING = [MAJOR, MINOR, TINY].join('.')
|
||||
end
|
||||
end
|
8
activeresource/test/abstract_unit.rb
Normal file
8
activeresource/test/abstract_unit.rb
Normal file
|
@ -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"
|
60
activeresource/test/base_test.rb
Normal file
60
activeresource/test/base_test.rb
Normal file
|
@ -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("<person><name>Matz</name><id type='integer'>1</id></person>"),
|
||||
ActiveResource::Request.new(:get, "/people/2.xml") => ActiveResource::Response.new("<person><name>David</name><id type='integer'>2</id></person>"),
|
||||
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(
|
||||
"<people><person><name>Matz</name><id type='integer'>1</id></person><person><name>David</name><id type='integer'>2</id></person></people>"
|
||||
)
|
||||
)
|
||||
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
|
11
activeresource/test/connection_test.rb
Normal file
11
activeresource/test/connection_test.rb
Normal file
|
@ -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
|
3
activeresource/test/fixtures/person.rb
vendored
Normal file
3
activeresource/test/fixtures/person.rb
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
class Person < ActiveResource::Base
|
||||
self.site = "http://37s.sunrise.i:3000/"
|
||||
end
|
84
activeresource/test/http_mock.rb
Normal file
84
activeresource/test/http_mock.rb
Normal file
|
@ -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
|
Loading…
Reference in a new issue