Adds a branch for unlimited project hierarchy.
git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/branches/work@2148 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
@@ -0,0 +1,265 @@
|
||||
*SVN*
|
||||
|
||||
* Documentation for ActionWebService::API::Base. Closes #7275. [zackchandler]
|
||||
|
||||
* Allow action_web_service to handle various HTTP methods including GET. Closes #7011. [zackchandler]
|
||||
|
||||
* Ensure that DispatcherError is being thrown when a malformed request is received. [Kent Sibilev]
|
||||
|
||||
* Added support for decimal types. Closes #6676. [Kent Sibilev]
|
||||
|
||||
* Removed deprecated end_form_tag helper. [Kent Sibilev]
|
||||
|
||||
* Removed deprecated @request and @response usages. [Kent Sibilev]
|
||||
|
||||
* Removed invocation of deprecated before_action and around_action filter methods. Corresponding before_invocation and after_invocation methods should be used instead. #6275 [Kent Sibilev]
|
||||
|
||||
* Provide access to the underlying SOAP driver. #6212 [bmilekic, Kent Sibilev]
|
||||
|
||||
* Deprecation: update docs. #5998 [jakob@mentalized.net, Kevin Clark]
|
||||
|
||||
* ActionWebService WSDL generation ignores HTTP_X_FORWARDED_HOST [Paul Butcher <paul@paulbutcher.com>]
|
||||
|
||||
* Tighten rescue clauses. #5985 [james@grayproductions.net]
|
||||
|
||||
* Fixed XMLRPC multicall when one of the called methods returns a struct object. [Kent Sibilev]
|
||||
|
||||
* Replace Reloadable with Reloadable::Deprecated. [Nicholas Seckar]
|
||||
|
||||
* Fix invoke_layered since api_method didn't declare :expects. Closes #4720. [Kevin Ballard <kevin@sb.org>, Kent Sibilev]
|
||||
|
||||
* Replace alias method chaining with Module#alias_method_chain. [Marcel Molina Jr.]
|
||||
|
||||
* Replace Ruby's deprecated append_features in favor of included. [Marcel Molina Jr.]
|
||||
|
||||
* Fix test database name typo. [Marcel Molina Jr.]
|
||||
|
||||
*1.1.2* (April 9th, 2006)
|
||||
|
||||
* Rely on Active Record 1.14.2
|
||||
|
||||
|
||||
*1.1.1* (April 6th, 2006)
|
||||
|
||||
* Do not convert driver options to strings (#4499)
|
||||
|
||||
|
||||
*1.1.0* (March 27th, 2006)
|
||||
|
||||
* Make ActiveWebService::Struct type reloadable
|
||||
|
||||
* Fix scaffolding action when one of the members of a structural type has date or time type
|
||||
|
||||
* Remove extra index hash when generating scaffold html for parameters of structural type #4374 [joe@mjg2.com]
|
||||
|
||||
* Fix Scaffold Fails with Struct as a Parameter #4363 [joe@mjg2.com]
|
||||
|
||||
* Fix soap type registration of multidimensional arrays (#4232)
|
||||
|
||||
* Fix that marshaler couldn't handle ActiveRecord models defined in a different namespace (#2392).
|
||||
|
||||
* Fix that marshaler couldn't handle structs with members of ActiveRecord type (#1889).
|
||||
|
||||
* Fix that marshaler couldn't handle nil values for inner structs (#3576).
|
||||
|
||||
* Fix that changes to ActiveWebService::API::Base required restarting of the server (#2390).
|
||||
|
||||
* Fix scaffolding for signatures with :date, :time and :base64 types (#3321, #2769, #2078).
|
||||
|
||||
* Fix for incorrect casting of TrueClass/FalseClass instances (#2633, #3421).
|
||||
|
||||
* Fix for incompatibility problems with SOAP4R 1.5.5 (#2553) [Kent Sibilev]
|
||||
|
||||
|
||||
*1.0.0* (December 13th, 2005)
|
||||
|
||||
* Become part of Rails 1.0
|
||||
|
||||
*0.9.4* (December 7th, 2005)
|
||||
|
||||
* Update from LGPL to MIT license as per Minero Aoki's permission. [Marcel Molina Jr.]
|
||||
|
||||
* Rename Version constant to VERSION. #2802 [Marcel Molina Jr.]
|
||||
|
||||
* Fix that XML-RPC date/time values did not have well-defined behaviour (#2516, #2534). This fix has one caveat, in that we can't support pre-1970 dates from XML-RPC clients.
|
||||
|
||||
*0.9.3* (November 7th, 2005)
|
||||
|
||||
* Upgraded to Action Pack 1.11.0 and Active Record 1.13.0
|
||||
|
||||
|
||||
*0.9.2* (October 26th, 2005)
|
||||
|
||||
* Upgraded to Action Pack 1.10.2 and Active Record 1.12.2
|
||||
|
||||
|
||||
*0.9.1* (October 19th, 2005)
|
||||
|
||||
* Upgraded to Action Pack 1.10.1 and Active Record 1.12.1
|
||||
|
||||
|
||||
*0.9.0* (October 16th, 2005)
|
||||
|
||||
* Fix invalid XML request generation bug in test_invoke [Ken Barker]
|
||||
|
||||
* Add XML-RPC 'system.multicall' support #1941 [jbonnar]
|
||||
|
||||
* Fix duplicate XSD entries for custom types shared across delegated/layered services #1729 [Tyler Kovacs]
|
||||
|
||||
* Allow multiple invocations in the same test method #1720 [dkhawk]
|
||||
|
||||
* Added ActionWebService::API::Base.soap_client and ActionWebService::API::Base.xmlrpc_client helper methods to create the internal clients for an API, useful for testing from ./script/console
|
||||
|
||||
* ActionWebService now always returns UTF-8 responses.
|
||||
|
||||
|
||||
*0.8.1* (11 July, 2005)
|
||||
|
||||
* Fix scaffolding for Action Pack controller changes
|
||||
|
||||
|
||||
*0.8.0* (6 July, 2005)
|
||||
|
||||
* Fix WSDL generation by aliasing #inherited instead of trying to overwrite it, or the WSDL action may end up not being defined in the controller
|
||||
|
||||
* Add ActionController::Base.wsdl_namespace option, to allow overriding of the namespace used in generated WSDL and SOAP messages. This is equivalent to the [WebService(Namespace = "Value")] attribute in .NET.
|
||||
|
||||
* Add workaround for Ruby 1.8.3's SOAP4R changing the return value of SOAP::Mapping::Registry#find_mapped_soap_class #1414 [Shugo Maeda]
|
||||
|
||||
* Fix moduled controller URLs in WSDL, and add unit test to verify the generated URL #1428
|
||||
|
||||
* Fix scaffolding template paths, it was broken on Win32
|
||||
|
||||
* Fix that functional testing of :layered controllers failed when using the SOAP protocol
|
||||
|
||||
* Allow invocation filters in :direct controllers as well, as they have access to more information regarding the web service request than ActionPack filters
|
||||
|
||||
* Add support for a :base64 signature type #1272 [Shugo Maeda]
|
||||
|
||||
* Fix that boolean fields were not rendered correctly in scaffolding
|
||||
|
||||
* Fix that scaffolding was not working for :delegated dispatching
|
||||
|
||||
* Add support for structured types as input parameters to scaffolding, this should let one test the blogging APIs using scaffolding as well
|
||||
|
||||
* Fix that generated WSDL was not using relative_url_root for base URI #1210 [Shugo Maeda]
|
||||
|
||||
* Use UTF-8 encoding by default for SOAP responses, but if an encoding is supplied by caller, use that for the response #1211 [Shugo Maeda, NAKAMURA Hiroshi]
|
||||
|
||||
* If the WSDL was retrieved over HTTPS, use HTTPS URLs in the WSDL too
|
||||
|
||||
* Fix that casting change in 0.7.0 would convert nil values to the default value for the type instead of leaving it as nil
|
||||
|
||||
|
||||
*0.7.1* (20th April, 2005)
|
||||
|
||||
* Depend on Active Record 1.10.1 and Action Pack 1.8.1
|
||||
|
||||
|
||||
*0.7.0* (19th April, 2005)
|
||||
|
||||
* When casting structured types, don't try to send obj.name= unless obj responds to it, causes casting to be less likely to fail for XML-RPC
|
||||
|
||||
* Add scaffolding via ActionController::Base.web_service_scaffold for quick testing using a web browser
|
||||
|
||||
* ActionWebService::API::Base#api_methods now returns a hash containing ActionWebService::API::Method objects instead of hashes. However, ActionWebService::API::Method defines a #[]() backwards compatibility method so any existing code utilizing this will still work.
|
||||
|
||||
* The :layered dispatching mode can now be used with SOAP as well, allowing you to support SOAP and XML-RPC clients for APIs like the metaWeblog API
|
||||
|
||||
* Remove ActiveRecordSoapMarshallable workaround, see #912 for details
|
||||
|
||||
* Generalize casting code to be used by both SOAP and XML-RPC (previously, it was only XML-RPC)
|
||||
|
||||
* Ensure return value is properly cast as well, fixes XML-RPC interoperability with Ecto and possibly other clients
|
||||
|
||||
* Include backtraces in 500 error responses for failed request parsing, and remove "rescue nil" statements obscuring real errors for XML-RPC
|
||||
|
||||
* Perform casting of struct members even if the structure is already of the correct type, so that the type we specify for the struct member is always the type of the value seen by the API implementation
|
||||
|
||||
|
||||
*0.6.2* (27th March, 2005)
|
||||
|
||||
* Allow method declarations for direct dispatching to declare parameters as well. We treat an arity of < 0 or > 0 as an indication that we should send through parameters. Closes #939.
|
||||
|
||||
|
||||
*0.6.1* (22th March, 2005)
|
||||
|
||||
* Fix that method response QNames mismatched with that declared in the WSDL, makes SOAP::WSDLDriverFactory work against AWS again
|
||||
|
||||
* Fix that @request.env was being modified, instead, dup the value gotten from env
|
||||
|
||||
* Fix XML-RPC example to use :layered mode, so it works again
|
||||
|
||||
* Support casting '0' or 0 into false, and '1' or 1 into true, when expecting a boolean value
|
||||
|
||||
* Fix that SOAP fault response fault code values were not QName's #804
|
||||
|
||||
|
||||
*0.6.0* (7th March, 2005)
|
||||
|
||||
* Add action_controller/test_invoke, used for integrating AWS with the Rails testing infrastructure
|
||||
|
||||
* Allow passing through options to the SOAP RPC driver for the SOAP client
|
||||
|
||||
* Make the SOAP WS marshaler use #columns to decide which fields to marshal as well, avoids providing attributes brought in by associations
|
||||
|
||||
* Add <tt>ActionWebService::API::Base.allow_active_record_expects</tt> option, with a default of false. Setting this to true will allow specifying ActiveRecord::Base model classes in <tt>:expects</tt>. API writers should take care to validate the received ActiveRecord model objects when turning it on, and/or have an authentication mechanism in place to reduce the security risk.
|
||||
|
||||
* Improve error message reporting. Bugs in either AWS or the web service itself will send back a protocol-specific error report message if possible, otherwise, provide as much detail as possible.
|
||||
|
||||
* Removed type checking of received parameters, and perform casting for XML-RPC if possible, but fallback to the received parameters if casting fails, closes #677
|
||||
|
||||
* Refactored SOAP and XML-RPC marshaling and encoding into a small library devoted exclusively to protocol specifics, also cleaned up the SOAP marshaling approach, so that array and custom type marshaling should be a bit faster.
|
||||
|
||||
* Add namespaced XML-RPC method name support, closes #678
|
||||
|
||||
* Replace '::' with '..' in fully qualified type names for marshaling and WSDL. This improves interoperability with .NET, and closes #676.
|
||||
|
||||
|
||||
*0.5.0* (24th February, 2005)
|
||||
|
||||
* lib/action_service/dispatcher*: replace "router" fragments with
|
||||
one file for Action Controllers, moves dispatching work out of
|
||||
the container
|
||||
* lib/*,test/*,examples/*: rename project to
|
||||
ActionWebService. prefix all generic "service" type names with web_.
|
||||
update all using code as well as the RDoc.
|
||||
* lib/action_service/router/wsdl.rb: ensure that #wsdl is
|
||||
defined in the final container class, or the new ActionPack
|
||||
filtering will exclude it
|
||||
* lib/action_service/struct.rb,test/struct_test.rb: create a
|
||||
default #initialize on inherit that accepts a Hash containing
|
||||
the default member values
|
||||
* lib/action_service/api/action_controller.rb: add support and
|
||||
tests for #client_api in controller
|
||||
* test/router_wsdl_test.rb: add tests to ensure declared
|
||||
service names don't contain ':', as ':' causes interoperability
|
||||
issues
|
||||
* lib/*, test/*: rename "interface" concept to "api", and change all
|
||||
related uses to reflect this change. update all uses of Inflector
|
||||
to call the method on String instead.
|
||||
* test/api_test.rb: add test to ensure API definition not
|
||||
instantiatable
|
||||
* lib/action_service/invocation.rb: change @invocation_params to
|
||||
@method_params
|
||||
* lib/*: update RDoc
|
||||
* lib/action_service/struct.rb: update to support base types
|
||||
* lib/action_service/support/signature.rb: support the notion of
|
||||
"base types" in signatures, with well-known unambiguous names such as :int,
|
||||
:bool, etc, which map to the correct Ruby class. accept the same names
|
||||
used by ActiveRecord as well as longer versions of each, as aliases.
|
||||
* examples/*: update for seperate API definition updates
|
||||
* lib/action_service/*, test/*: extensive refactoring: define API methods in
|
||||
a seperate class, and specify it wherever used with 'service_api'.
|
||||
this makes writing a client API for accessing defined API methods
|
||||
with ActionWebService really easy.
|
||||
* lib/action_service/container.rb: fix a bug in default call
|
||||
handling for direct dispatching, and add ActionController filter
|
||||
support for direct dispatching.
|
||||
* test/router_action_controller_test.rb: add tests to ensure
|
||||
ActionController filters are actually called.
|
||||
* test/protocol_soap_test.rb: add more tests for direct dispatching.
|
||||
|
||||
0.3.0
|
||||
|
||||
* First public release
|
||||
@@ -0,0 +1,21 @@
|
||||
Copyright (C) 2005 Leon Breedt
|
||||
|
||||
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.
|
||||
|
||||
@@ -0,0 +1,364 @@
|
||||
= Action Web Service -- Serving APIs on rails
|
||||
|
||||
Action Web Service provides a way to publish interoperable web service APIs with
|
||||
Rails without spending a lot of time delving into protocol details.
|
||||
|
||||
|
||||
== Features
|
||||
|
||||
* SOAP RPC protocol support
|
||||
* Dynamic WSDL generation for APIs
|
||||
* XML-RPC protocol support
|
||||
* Clients that use the same API definitions as the server for
|
||||
easy interoperability with other Action Web Service based applications
|
||||
* Type signature hints to improve interoperability with static languages
|
||||
* Active Record model class support in signatures
|
||||
|
||||
|
||||
== Defining your APIs
|
||||
|
||||
You specify the methods you want to make available as API methods in an
|
||||
ActionWebService::API::Base derivative, and then specify this API
|
||||
definition class wherever you want to use that API.
|
||||
|
||||
The implementation of the methods is done separately from the API
|
||||
specification.
|
||||
|
||||
|
||||
==== Method name inflection
|
||||
|
||||
Action Web Service will camelcase the method names according to Rails Inflector
|
||||
rules for the API visible to public callers. What this means, for example,
|
||||
is that the method names in generated WSDL will be camelcased, and callers will
|
||||
have to supply the camelcased name in their requests for the request to
|
||||
succeed.
|
||||
|
||||
If you do not desire this behaviour, you can turn it off with the
|
||||
ActionWebService::API::Base +inflect_names+ option.
|
||||
|
||||
|
||||
==== Inflection examples
|
||||
|
||||
:add => Add
|
||||
:find_all => FindAll
|
||||
|
||||
|
||||
==== Disabling inflection
|
||||
|
||||
class PersonAPI < ActionWebService::API::Base
|
||||
inflect_names false
|
||||
end
|
||||
|
||||
|
||||
==== API definition example
|
||||
|
||||
class PersonAPI < ActionWebService::API::Base
|
||||
api_method :add, :expects => [:string, :string, :bool], :returns => [:int]
|
||||
api_method :remove, :expects => [:int], :returns => [:bool]
|
||||
end
|
||||
|
||||
==== API usage example
|
||||
|
||||
class PersonController < ActionController::Base
|
||||
web_service_api PersonAPI
|
||||
|
||||
def add
|
||||
end
|
||||
|
||||
def remove
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
== Publishing your APIs
|
||||
|
||||
Action Web Service uses Action Pack to process protocol requests. There are two
|
||||
modes of dispatching protocol requests, _Direct_, and _Delegated_.
|
||||
|
||||
|
||||
=== Direct dispatching
|
||||
|
||||
This is the default mode. In this mode, public controller instance methods
|
||||
implement the API methods, and parameters are passed through to the methods in
|
||||
accordance with the API specification.
|
||||
|
||||
The return value of the method is sent back as the return value to the
|
||||
caller.
|
||||
|
||||
In this mode, a special <tt>api</tt> action is generated in the target
|
||||
controller to unwrap the protocol request, forward it on to the relevant method
|
||||
and send back the wrapped return value. <em>This action must not be
|
||||
overridden.</em>
|
||||
|
||||
==== Direct dispatching example
|
||||
|
||||
class PersonController < ApplicationController
|
||||
web_service_api PersonAPI
|
||||
|
||||
def add
|
||||
end
|
||||
|
||||
def remove
|
||||
end
|
||||
end
|
||||
|
||||
class PersonAPI < ActionWebService::API::Base
|
||||
...
|
||||
end
|
||||
|
||||
|
||||
For this example, protocol requests for +Add+ and +Remove+ methods sent to
|
||||
<tt>/person/api</tt> will be routed to the controller methods +add+ and +remove+.
|
||||
|
||||
|
||||
=== Delegated dispatching
|
||||
|
||||
This mode can be turned on by setting the +web_service_dispatching_mode+ option
|
||||
in a controller to <tt>:delegated</tt>.
|
||||
|
||||
In this mode, the controller contains one or more web service objects (objects
|
||||
that implement an ActionWebService::API::Base definition). These web service
|
||||
objects are each mapped onto one controller action only.
|
||||
|
||||
==== Delegated dispatching example
|
||||
|
||||
class ApiController < ApplicationController
|
||||
web_service_dispatching_mode :delegated
|
||||
|
||||
web_service :person, PersonService.new
|
||||
end
|
||||
|
||||
class PersonService < ActionWebService::Base
|
||||
web_service_api PersonAPI
|
||||
|
||||
def add
|
||||
end
|
||||
|
||||
def remove
|
||||
end
|
||||
end
|
||||
|
||||
class PersonAPI < ActionWebService::API::Base
|
||||
...
|
||||
end
|
||||
|
||||
|
||||
For this example, all protocol requests for +PersonService+ are
|
||||
sent to the <tt>/api/person</tt> action.
|
||||
|
||||
The <tt>/api/person</tt> action is generated when the +web_service+
|
||||
method is called. <em>This action must not be overridden.</em>
|
||||
|
||||
Other controller actions (actions that aren't the target of a +web_service+ call)
|
||||
are ignored for ActionWebService purposes, and can do normal action tasks.
|
||||
|
||||
|
||||
=== Layered dispatching
|
||||
|
||||
This mode can be turned on by setting the +web_service_dispatching_mode+ option
|
||||
in a controller to <tt>:layered</tt>.
|
||||
|
||||
This mode is similar to _delegated_ mode, in that multiple web service objects
|
||||
can be attached to one controller, however, all protocol requests are sent to a
|
||||
single endpoint.
|
||||
|
||||
Use this mode when you want to share code between XML-RPC and SOAP clients,
|
||||
for APIs where the XML-RPC method names have prefixes. An example of such
|
||||
a method name would be <tt>blogger.newPost</tt>.
|
||||
|
||||
|
||||
==== Layered dispatching example
|
||||
|
||||
|
||||
class ApiController < ApplicationController
|
||||
web_service_dispatching_mode :layered
|
||||
|
||||
web_service :mt, MovableTypeService.new
|
||||
web_service :blogger, BloggerService.new
|
||||
web_service :metaWeblog, MetaWeblogService.new
|
||||
end
|
||||
|
||||
class MovableTypeService < ActionWebService::Base
|
||||
...
|
||||
end
|
||||
|
||||
class BloggerService < ActionWebService::Base
|
||||
...
|
||||
end
|
||||
|
||||
class MetaWeblogService < ActionWebService::API::Base
|
||||
...
|
||||
end
|
||||
|
||||
|
||||
For this example, an XML-RPC call for a method with a name like
|
||||
<tt>mt.getCategories</tt> will be sent to the <tt>getCategories</tt>
|
||||
method on the <tt>:mt</tt> service.
|
||||
|
||||
|
||||
== Customizing WSDL generation
|
||||
|
||||
You can customize the names used for the SOAP bindings in the generated
|
||||
WSDL by using the wsdl_service_name option in a controller:
|
||||
|
||||
class WsController < ApplicationController
|
||||
wsdl_service_name 'MyApp'
|
||||
end
|
||||
|
||||
You can also customize the namespace used in the generated WSDL for
|
||||
custom types and message definition types:
|
||||
|
||||
class WsController < ApplicationController
|
||||
wsdl_namespace 'http://my.company.com/app/wsapi'
|
||||
end
|
||||
|
||||
The default namespace used is 'urn:ActionWebService', if you don't supply
|
||||
one.
|
||||
|
||||
|
||||
== ActionWebService and UTF-8
|
||||
|
||||
If you're going to be sending back strings containing non-ASCII UTF-8
|
||||
characters using the <tt>:string</tt> data type, you need to make sure that
|
||||
Ruby is using UTF-8 as the default encoding for its strings.
|
||||
|
||||
The default in Ruby is to use US-ASCII encoding for strings, which causes a string
|
||||
validation check in the Ruby SOAP library to fail and your string to be sent
|
||||
back as a Base-64 value, which may confuse clients that expected strings
|
||||
because of the WSDL.
|
||||
|
||||
Two ways of setting the default string encoding are:
|
||||
|
||||
* Start Ruby using the <tt>-Ku</tt> command-line option to the Ruby executable
|
||||
* Set the <tt>$KCODE</tt> flag in <tt>config/environment.rb</tt> to the
|
||||
string <tt>'UTF8'</tt>
|
||||
|
||||
|
||||
== Testing your APIs
|
||||
|
||||
|
||||
=== Functional testing
|
||||
|
||||
You can perform testing of your APIs by creating a functional test for the
|
||||
controller dispatching the API, and calling #invoke in the test case to
|
||||
perform the invocation.
|
||||
|
||||
Example:
|
||||
|
||||
class PersonApiControllerTest < Test::Unit::TestCase
|
||||
def setup
|
||||
@controller = PersonController.new
|
||||
@request = ActionController::TestRequest.new
|
||||
@response = ActionController::TestResponse.new
|
||||
end
|
||||
|
||||
def test_add
|
||||
result = invoke :remove, 1
|
||||
assert_equal true, result
|
||||
end
|
||||
end
|
||||
|
||||
This example invokes the API method <tt>test</tt>, defined on
|
||||
the PersonController, and returns the result.
|
||||
|
||||
|
||||
=== Scaffolding
|
||||
|
||||
You can also test your APIs with a web browser by attaching scaffolding
|
||||
to the controller.
|
||||
|
||||
Example:
|
||||
|
||||
class PersonController
|
||||
web_service_scaffold :invocation
|
||||
end
|
||||
|
||||
This creates an action named <tt>invocation</tt> on the PersonController.
|
||||
|
||||
Navigating to this action lets you select the method to invoke, supply the parameters,
|
||||
and view the result of the invocation.
|
||||
|
||||
|
||||
== Using the client support
|
||||
|
||||
Action Web Service includes client classes that can use the same API
|
||||
definition as the server. The advantage of this approach is that your client
|
||||
will have the same support for Active Record and structured types as the
|
||||
server, and can just use them directly, and rely on the marshaling to Do The
|
||||
Right Thing.
|
||||
|
||||
*Note*: The client support is intended for communication between Ruby on Rails
|
||||
applications that both use Action Web Service. It may work with other servers, but
|
||||
that is not its intended use, and interoperability can't be guaranteed, especially
|
||||
not for .NET web services.
|
||||
|
||||
Web services protocol specifications are complex, and Action Web Service client
|
||||
support can only be guaranteed to work with a subset.
|
||||
|
||||
|
||||
==== Factory created client example
|
||||
|
||||
class BlogManagerController < ApplicationController
|
||||
web_client_api :blogger, :xmlrpc, 'http://url/to/blog/api/RPC2', :handler_name => 'blogger'
|
||||
end
|
||||
|
||||
class SearchingController < ApplicationController
|
||||
web_client_api :google, :soap, 'http://url/to/blog/api/beta', :service_name => 'GoogleSearch'
|
||||
end
|
||||
|
||||
See ActionWebService::API::ActionController::ClassMethods for more details.
|
||||
|
||||
==== Manually created client example
|
||||
|
||||
class PersonAPI < ActionWebService::API::Base
|
||||
api_method :find_all, :returns => [[Person]]
|
||||
end
|
||||
|
||||
soap_client = ActionWebService::Client::Soap.new(PersonAPI, "http://...")
|
||||
persons = soap_client.find_all
|
||||
|
||||
class BloggerAPI < ActionWebService::API::Base
|
||||
inflect_names false
|
||||
api_method :getRecentPosts, :returns => [[Blog::Post]]
|
||||
end
|
||||
|
||||
blog = ActionWebService::Client::XmlRpc.new(BloggerAPI, "http://.../xmlrpc", :handler_name => "blogger")
|
||||
posts = blog.getRecentPosts
|
||||
|
||||
|
||||
See ActionWebService::Client::Soap and ActionWebService::Client::XmlRpc for more details.
|
||||
|
||||
== Dependencies
|
||||
|
||||
Action Web Service requires that the Action Pack and Active Record are either
|
||||
available to be required immediately or are accessible as GEMs.
|
||||
|
||||
It also requires a version of Ruby that includes SOAP support in the standard
|
||||
library. At least version 1.8.2 final (2004-12-25) of Ruby is recommended; this
|
||||
is the version tested against.
|
||||
|
||||
|
||||
== Download
|
||||
|
||||
The latest Action Web Service version can be downloaded from
|
||||
http://rubyforge.org/projects/actionservice
|
||||
|
||||
|
||||
== Installation
|
||||
|
||||
You can install Action Web Service with the following command.
|
||||
|
||||
% [sudo] ruby setup.rb
|
||||
|
||||
|
||||
== License
|
||||
|
||||
Action Web Service is released under the MIT license.
|
||||
|
||||
|
||||
== Support
|
||||
|
||||
The Ruby on Rails mailing list
|
||||
|
||||
Or, to contact the author, send mail to bitserf@gmail.com
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
require 'rubygems'
|
||||
require 'rake'
|
||||
require 'rake/testtask'
|
||||
require 'rake/rdoctask'
|
||||
require 'rake/packagetask'
|
||||
require 'rake/gempackagetask'
|
||||
require 'rake/contrib/rubyforgepublisher'
|
||||
require 'fileutils'
|
||||
require File.join(File.dirname(__FILE__), 'lib', 'action_web_service', 'version')
|
||||
|
||||
PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
|
||||
PKG_NAME = 'actionwebservice'
|
||||
PKG_VERSION = ActionWebService::VERSION::STRING + PKG_BUILD
|
||||
PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
|
||||
PKG_DESTINATION = ENV["RAILS_PKG_DESTINATION"] || "../#{PKG_NAME}"
|
||||
|
||||
RELEASE_NAME = "REL #{PKG_VERSION}"
|
||||
|
||||
RUBY_FORGE_PROJECT = "aws"
|
||||
RUBY_FORGE_USER = "webster132"
|
||||
|
||||
desc "Default Task"
|
||||
task :default => [ :test ]
|
||||
|
||||
|
||||
# Run the unit tests
|
||||
Rake::TestTask.new { |t|
|
||||
t.libs << "test"
|
||||
t.test_files = Dir['test/*_test.rb']
|
||||
t.verbose = true
|
||||
}
|
||||
|
||||
SCHEMA_PATH = File.join(File.dirname(__FILE__), *%w(test fixtures db_definitions))
|
||||
|
||||
desc 'Build the MySQL test database'
|
||||
task :build_database do
|
||||
%x( mysqladmin create actionwebservice_unittest )
|
||||
%x( mysql actionwebservice_unittest < #{File.join(SCHEMA_PATH, 'mysql.sql')} )
|
||||
end
|
||||
|
||||
|
||||
# Generate the RDoc documentation
|
||||
Rake::RDocTask.new { |rdoc|
|
||||
rdoc.rdoc_dir = 'doc'
|
||||
rdoc.title = "Action Web Service -- Web services for Action Pack"
|
||||
rdoc.options << '--line-numbers' << '--inline-source'
|
||||
rdoc.options << '--charset' << 'utf-8'
|
||||
rdoc.template = "#{ENV['template']}.rb" if ENV['template']
|
||||
rdoc.rdoc_files.include('README')
|
||||
rdoc.rdoc_files.include('CHANGELOG')
|
||||
rdoc.rdoc_files.include('lib/action_web_service.rb')
|
||||
rdoc.rdoc_files.include('lib/action_web_service/*.rb')
|
||||
rdoc.rdoc_files.include('lib/action_web_service/api/*.rb')
|
||||
rdoc.rdoc_files.include('lib/action_web_service/client/*.rb')
|
||||
rdoc.rdoc_files.include('lib/action_web_service/container/*.rb')
|
||||
rdoc.rdoc_files.include('lib/action_web_service/dispatcher/*.rb')
|
||||
rdoc.rdoc_files.include('lib/action_web_service/protocol/*.rb')
|
||||
rdoc.rdoc_files.include('lib/action_web_service/support/*.rb')
|
||||
}
|
||||
|
||||
|
||||
# Create compressed packages
|
||||
spec = Gem::Specification.new do |s|
|
||||
s.platform = Gem::Platform::RUBY
|
||||
s.name = PKG_NAME
|
||||
s.summary = "Web service support for Action Pack."
|
||||
s.description = %q{Adds WSDL/SOAP and XML-RPC web service support to Action Pack}
|
||||
s.version = PKG_VERSION
|
||||
|
||||
s.author = "Leon Breedt"
|
||||
s.email = "bitserf@gmail.com"
|
||||
s.rubyforge_project = "aws"
|
||||
s.homepage = "http://www.rubyonrails.org"
|
||||
|
||||
s.add_dependency('actionpack', '= 1.13.5' + PKG_BUILD)
|
||||
s.add_dependency('activerecord', '= 1.15.5' + PKG_BUILD)
|
||||
|
||||
s.has_rdoc = true
|
||||
s.requirements << 'none'
|
||||
s.require_path = 'lib'
|
||||
s.autorequire = 'action_web_service'
|
||||
|
||||
s.files = [ "Rakefile", "setup.rb", "README", "TODO", "CHANGELOG", "MIT-LICENSE" ]
|
||||
s.files = s.files + Dir.glob( "examples/**/*" ).delete_if { |item| item.include?( "\.svn" ) }
|
||||
s.files = s.files + Dir.glob( "lib/**/*" ).delete_if { |item| item.include?( "\.svn" ) }
|
||||
s.files = s.files + Dir.glob( "test/**/*" ).delete_if { |item| item.include?( "\.svn" ) }
|
||||
end
|
||||
Rake::GemPackageTask.new(spec) do |p|
|
||||
p.gem_spec = spec
|
||||
p.need_tar = true
|
||||
p.need_zip = true
|
||||
end
|
||||
|
||||
|
||||
# Publish beta gem
|
||||
desc "Publish the API documentation"
|
||||
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
|
||||
|
||||
# Publish documentation
|
||||
desc "Publish the API documentation"
|
||||
task :pdoc => [:rdoc] do
|
||||
Rake::SshDirPublisher.new("davidhh@wrath.rubyonrails.org", "public_html/aws", "doc").upload
|
||||
end
|
||||
|
||||
|
||||
def each_source_file(*args)
|
||||
prefix, includes, excludes, open_file = args
|
||||
prefix ||= File.dirname(__FILE__)
|
||||
open_file = true if open_file.nil?
|
||||
includes ||= %w[lib\/action_web_service\.rb$ lib\/action_web_service\/.*\.rb$]
|
||||
excludes ||= %w[lib\/action_web_service\/vendor]
|
||||
Find.find(prefix) do |file_name|
|
||||
next if file_name =~ /\.svn/
|
||||
file_name.gsub!(/^\.\//, '')
|
||||
continue = false
|
||||
includes.each do |inc|
|
||||
if file_name.match(/#{inc}/)
|
||||
continue = true
|
||||
break
|
||||
end
|
||||
end
|
||||
next unless continue
|
||||
excludes.each do |exc|
|
||||
if file_name.match(/#{exc}/)
|
||||
continue = false
|
||||
break
|
||||
end
|
||||
end
|
||||
next unless continue
|
||||
if open_file
|
||||
File.open(file_name) do |f|
|
||||
yield file_name, f
|
||||
end
|
||||
else
|
||||
yield file_name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
desc "Count lines of the AWS source code"
|
||||
task :lines do
|
||||
total_lines = total_loc = 0
|
||||
puts "Per File:"
|
||||
each_source_file do |file_name, f|
|
||||
file_lines = file_loc = 0
|
||||
while line = f.gets
|
||||
file_lines += 1
|
||||
next if line =~ /^\s*$/
|
||||
next if line =~ /^\s*#/
|
||||
file_loc += 1
|
||||
end
|
||||
puts " #{file_name}: Lines #{file_lines}, LOC #{file_loc}"
|
||||
total_lines += file_lines
|
||||
total_loc += file_loc
|
||||
end
|
||||
puts "Total:"
|
||||
puts " Lines #{total_lines}, LOC #{total_loc}"
|
||||
end
|
||||
|
||||
desc "Publish the release files to RubyForge."
|
||||
task :release => [ :package ] do
|
||||
require 'rubyforge'
|
||||
|
||||
packages = %w( gem tgz zip ).collect{ |ext| "pkg/#{PKG_NAME}-#{PKG_VERSION}.#{ext}" }
|
||||
|
||||
rubyforge = RubyForge.new
|
||||
rubyforge.login
|
||||
rubyforge.add_release(PKG_NAME, PKG_NAME, "REL #{PKG_VERSION}", *packages)
|
||||
end
|
||||
@@ -0,0 +1,32 @@
|
||||
= Post-1.0
|
||||
- Document/Literal SOAP support
|
||||
- URL-based dispatching, URL identifies method
|
||||
|
||||
- Add :rest dispatching mode, a.l.a. Backpack API. Clean up dispatching
|
||||
in general. Support vanilla XML-format as a "Rails" protocol?
|
||||
XML::Simple deserialization into params?
|
||||
|
||||
web_service_dispatching_mode :rest
|
||||
|
||||
def method1(params)
|
||||
end
|
||||
|
||||
def method2(params)
|
||||
end
|
||||
|
||||
|
||||
/ws/method1
|
||||
<xml>
|
||||
/ws/method2
|
||||
<yaml>
|
||||
|
||||
- Allow locking down a controller to only accept messages for a particular
|
||||
protocol. This will allow us to generate fully conformant error messages
|
||||
in cases where we currently fudge it if we don't know the protocol.
|
||||
|
||||
- Allow AWS user to participate in typecasting, so they can centralize
|
||||
workarounds for buggy input in one place
|
||||
|
||||
= Refactoring
|
||||
- Don't have clean way to go from SOAP Class object to the xsd:NAME type
|
||||
string -- NaHi possibly looking at remedying this situation
|
||||
@@ -0,0 +1,7 @@
|
||||
require 'action_web_service'
|
||||
|
||||
# These need to be in the load path for action_web_service to work
|
||||
ActiveSupport::Dependencies.load_paths += ["#{RAILS_ROOT}/app/apis"]
|
||||
|
||||
# AWS Test helpers
|
||||
require 'action_web_service/test_invoke' if ENV['RAILS_ENV'] && ENV['RAILS_ENV'] =~ /^test/
|
||||
@@ -0,0 +1,30 @@
|
||||
require 'rbconfig'
|
||||
require 'find'
|
||||
require 'ftools'
|
||||
|
||||
include Config
|
||||
|
||||
# this was adapted from rdoc's install.rb by way of Log4r
|
||||
|
||||
$sitedir = CONFIG["sitelibdir"]
|
||||
unless $sitedir
|
||||
version = CONFIG["MAJOR"] + "." + CONFIG["MINOR"]
|
||||
$libdir = File.join(CONFIG["libdir"], "ruby", version)
|
||||
$sitedir = $:.find {|x| x =~ /site_ruby/ }
|
||||
if !$sitedir
|
||||
$sitedir = File.join($libdir, "site_ruby")
|
||||
elsif $sitedir !~ Regexp.quote(version)
|
||||
$sitedir = File.join($sitedir, version)
|
||||
end
|
||||
end
|
||||
|
||||
# the actual gruntwork
|
||||
Dir.chdir("lib")
|
||||
|
||||
Find.find("action_web_service", "action_web_service.rb") { |f|
|
||||
if f[-3..-1] == ".rb"
|
||||
File::install(f, File.join($sitedir, *f.split(/\//)), 0644, true)
|
||||
else
|
||||
File::makedirs(File.join($sitedir, *f.split(/\//)))
|
||||
end
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
#--
|
||||
# Copyright (C) 2005 Leon Breedt
|
||||
#
|
||||
# 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.
|
||||
#++
|
||||
|
||||
begin
|
||||
require 'active_support'
|
||||
require 'action_controller'
|
||||
require 'active_record'
|
||||
rescue LoadError
|
||||
require 'rubygems'
|
||||
gem 'activesupport', '>= 1.0.2'
|
||||
gem 'actionpack', '>= 1.6.0'
|
||||
gem 'activerecord', '>= 1.9.0'
|
||||
end
|
||||
|
||||
$:.unshift(File.dirname(__FILE__) + "/action_web_service/vendor/")
|
||||
|
||||
require 'action_web_service/support/class_inheritable_options'
|
||||
require 'action_web_service/support/signature_types'
|
||||
require 'action_web_service/base'
|
||||
require 'action_web_service/client'
|
||||
require 'action_web_service/invocation'
|
||||
require 'action_web_service/api'
|
||||
require 'action_web_service/casting'
|
||||
require 'action_web_service/struct'
|
||||
require 'action_web_service/container'
|
||||
require 'action_web_service/protocol'
|
||||
require 'action_web_service/dispatcher'
|
||||
require 'action_web_service/scaffolding'
|
||||
|
||||
ActionWebService::Base.class_eval do
|
||||
include ActionWebService::Container::Direct
|
||||
include ActionWebService::Invocation
|
||||
end
|
||||
|
||||
ActionController::Base.class_eval do
|
||||
include ActionWebService::Protocol::Discovery
|
||||
include ActionWebService::Protocol::Soap
|
||||
include ActionWebService::Protocol::XmlRpc
|
||||
include ActionWebService::Container::Direct
|
||||
include ActionWebService::Container::Delegated
|
||||
include ActionWebService::Container::ActionController
|
||||
include ActionWebService::Invocation
|
||||
include ActionWebService::Dispatcher
|
||||
include ActionWebService::Dispatcher::ActionController
|
||||
include ActionWebService::Scaffolding
|
||||
end
|
||||
+297
@@ -0,0 +1,297 @@
|
||||
module ActionWebService # :nodoc:
|
||||
module API # :nodoc:
|
||||
# A web service API class specifies the methods that will be available for
|
||||
# invocation for an API. It also contains metadata such as the method type
|
||||
# signature hints.
|
||||
#
|
||||
# It is not intended to be instantiated.
|
||||
#
|
||||
# It is attached to web service implementation classes like
|
||||
# ActionWebService::Base and ActionController::Base derivatives by using
|
||||
# <tt>container.web_service_api</tt>, where <tt>container</tt> is an
|
||||
# ActionController::Base or a ActionWebService::Base.
|
||||
#
|
||||
# See ActionWebService::Container::Direct::ClassMethods for an example
|
||||
# of use.
|
||||
class Base
|
||||
# Whether to transform the public API method names into camel-cased names
|
||||
class_inheritable_option :inflect_names, true
|
||||
|
||||
# By default only HTTP POST requests are processed
|
||||
class_inheritable_option :allowed_http_methods, [ :post ]
|
||||
|
||||
# Whether to allow ActiveRecord::Base models in <tt>:expects</tt>.
|
||||
# The default is +false+; you should be aware of the security implications
|
||||
# of allowing this, and ensure that you don't allow remote callers to
|
||||
# easily overwrite data they should not have access to.
|
||||
class_inheritable_option :allow_active_record_expects, false
|
||||
|
||||
# If present, the name of a method to call when the remote caller
|
||||
# tried to call a nonexistent method. Semantically equivalent to
|
||||
# +method_missing+.
|
||||
class_inheritable_option :default_api_method
|
||||
|
||||
# Disallow instantiation
|
||||
private_class_method :new, :allocate
|
||||
|
||||
class << self
|
||||
include ActionWebService::SignatureTypes
|
||||
|
||||
# API methods have a +name+, which must be the Ruby method name to use when
|
||||
# performing the invocation on the web service object.
|
||||
#
|
||||
# The signatures for the method input parameters and return value can
|
||||
# by specified in +options+.
|
||||
#
|
||||
# A signature is an array of one or more parameter specifiers.
|
||||
# A parameter specifier can be one of the following:
|
||||
#
|
||||
# * A symbol or string representing one of the Action Web Service base types.
|
||||
# See ActionWebService::SignatureTypes for a canonical list of the base types.
|
||||
# * The Class object of the parameter type
|
||||
# * A single-element Array containing one of the two preceding items. This
|
||||
# will cause Action Web Service to treat the parameter at that position
|
||||
# as an array containing only values of the given type.
|
||||
# * A Hash containing as key the name of the parameter, and as value
|
||||
# one of the three preceding items
|
||||
#
|
||||
# If no method input parameter or method return value signatures are given,
|
||||
# the method is assumed to take no parameters and/or return no values of
|
||||
# interest, and any values that are received by the server will be
|
||||
# discarded and ignored.
|
||||
#
|
||||
# Valid options:
|
||||
# [<tt>:expects</tt>] Signature for the method input parameters
|
||||
# [<tt>:returns</tt>] Signature for the method return value
|
||||
# [<tt>:expects_and_returns</tt>] Signature for both input parameters and return value
|
||||
def api_method(name, options={})
|
||||
unless options.is_a?(Hash)
|
||||
raise(ActionWebServiceError, "Expected a Hash for options")
|
||||
end
|
||||
validate_options([:expects, :returns, :expects_and_returns], options.keys)
|
||||
if options[:expects_and_returns]
|
||||
expects = options[:expects_and_returns]
|
||||
returns = options[:expects_and_returns]
|
||||
else
|
||||
expects = options[:expects]
|
||||
returns = options[:returns]
|
||||
end
|
||||
expects = canonical_signature(expects)
|
||||
returns = canonical_signature(returns)
|
||||
if expects
|
||||
expects.each do |type|
|
||||
type = type.element_type if type.is_a?(ArrayType)
|
||||
if type.type_class.ancestors.include?(ActiveRecord::Base) && !allow_active_record_expects
|
||||
raise(ActionWebServiceError, "ActiveRecord model classes not allowed in :expects")
|
||||
end
|
||||
end
|
||||
end
|
||||
name = name.to_sym
|
||||
public_name = public_api_method_name(name)
|
||||
method = Method.new(name, public_name, expects, returns)
|
||||
write_inheritable_hash("api_methods", name => method)
|
||||
write_inheritable_hash("api_public_method_names", public_name => name)
|
||||
end
|
||||
|
||||
# Whether the given method name is a service method on this API
|
||||
#
|
||||
# class ProjectsApi < ActionWebService::API::Base
|
||||
# api_method :getCount, :returns => [:int]
|
||||
# end
|
||||
#
|
||||
# ProjectsApi.has_api_method?('GetCount') #=> false
|
||||
# ProjectsApi.has_api_method?(:getCount) #=> true
|
||||
def has_api_method?(name)
|
||||
api_methods.has_key?(name)
|
||||
end
|
||||
|
||||
# Whether the given public method name has a corresponding service method
|
||||
# on this API
|
||||
#
|
||||
# class ProjectsApi < ActionWebService::API::Base
|
||||
# api_method :getCount, :returns => [:int]
|
||||
# end
|
||||
#
|
||||
# ProjectsApi.has_api_method?(:getCount) #=> false
|
||||
# ProjectsApi.has_api_method?('GetCount') #=> true
|
||||
def has_public_api_method?(public_name)
|
||||
api_public_method_names.has_key?(public_name)
|
||||
end
|
||||
|
||||
# The corresponding public method name for the given service method name
|
||||
#
|
||||
# ProjectsApi.public_api_method_name('GetCount') #=> "GetCount"
|
||||
# ProjectsApi.public_api_method_name(:getCount) #=> "GetCount"
|
||||
def public_api_method_name(name)
|
||||
if inflect_names
|
||||
name.to_s.camelize
|
||||
else
|
||||
name.to_s
|
||||
end
|
||||
end
|
||||
|
||||
# The corresponding service method name for the given public method name
|
||||
#
|
||||
# class ProjectsApi < ActionWebService::API::Base
|
||||
# api_method :getCount, :returns => [:int]
|
||||
# end
|
||||
#
|
||||
# ProjectsApi.api_method_name('GetCount') #=> :getCount
|
||||
def api_method_name(public_name)
|
||||
api_public_method_names[public_name]
|
||||
end
|
||||
|
||||
# A Hash containing all service methods on this API, and their
|
||||
# associated metadata.
|
||||
#
|
||||
# class ProjectsApi < ActionWebService::API::Base
|
||||
# api_method :getCount, :returns => [:int]
|
||||
# api_method :getCompletedCount, :returns => [:int]
|
||||
# end
|
||||
#
|
||||
# ProjectsApi.api_methods #=>
|
||||
# {:getCount=>#<ActionWebService::API::Method:0x24379d8 ...>,
|
||||
# :getCompletedCount=>#<ActionWebService::API::Method:0x2437794 ...>}
|
||||
# ProjectsApi.api_methods[:getCount].public_name #=> "GetCount"
|
||||
def api_methods
|
||||
read_inheritable_attribute("api_methods") || {}
|
||||
end
|
||||
|
||||
# The Method instance for the given public API method name, if any
|
||||
#
|
||||
# class ProjectsApi < ActionWebService::API::Base
|
||||
# api_method :getCount, :returns => [:int]
|
||||
# api_method :getCompletedCount, :returns => [:int]
|
||||
# end
|
||||
#
|
||||
# ProjectsApi.public_api_method_instance('GetCount') #=> <#<ActionWebService::API::Method:0x24379d8 ...>
|
||||
# ProjectsApi.public_api_method_instance(:getCount) #=> nil
|
||||
def public_api_method_instance(public_method_name)
|
||||
api_method_instance(api_method_name(public_method_name))
|
||||
end
|
||||
|
||||
# The Method instance for the given API method name, if any
|
||||
#
|
||||
# class ProjectsApi < ActionWebService::API::Base
|
||||
# api_method :getCount, :returns => [:int]
|
||||
# api_method :getCompletedCount, :returns => [:int]
|
||||
# end
|
||||
#
|
||||
# ProjectsApi.api_method_instance(:getCount) #=> <ActionWebService::API::Method:0x24379d8 ...>
|
||||
# ProjectsApi.api_method_instance('GetCount') #=> <ActionWebService::API::Method:0x24379d8 ...>
|
||||
def api_method_instance(method_name)
|
||||
api_methods[method_name]
|
||||
end
|
||||
|
||||
# The Method instance for the default API method, if any
|
||||
def default_api_method_instance
|
||||
return nil unless name = default_api_method
|
||||
instance = read_inheritable_attribute("default_api_method_instance")
|
||||
if instance && instance.name == name
|
||||
return instance
|
||||
end
|
||||
instance = Method.new(name, public_api_method_name(name), nil, nil)
|
||||
write_inheritable_attribute("default_api_method_instance", instance)
|
||||
instance
|
||||
end
|
||||
|
||||
private
|
||||
def api_public_method_names
|
||||
read_inheritable_attribute("api_public_method_names") || {}
|
||||
end
|
||||
|
||||
def validate_options(valid_option_keys, supplied_option_keys)
|
||||
unknown_option_keys = supplied_option_keys - valid_option_keys
|
||||
unless unknown_option_keys.empty?
|
||||
raise(ActionWebServiceError, "Unknown options: #{unknown_option_keys}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Represents an API method and its associated metadata, and provides functionality
|
||||
# to assist in commonly performed API method tasks.
|
||||
class Method
|
||||
attr :name
|
||||
attr :public_name
|
||||
attr :expects
|
||||
attr :returns
|
||||
|
||||
def initialize(name, public_name, expects, returns)
|
||||
@name = name
|
||||
@public_name = public_name
|
||||
@expects = expects
|
||||
@returns = returns
|
||||
@caster = ActionWebService::Casting::BaseCaster.new(self)
|
||||
end
|
||||
|
||||
# The list of parameter names for this method
|
||||
def param_names
|
||||
return [] unless @expects
|
||||
@expects.map{ |type| type.name }
|
||||
end
|
||||
|
||||
# Casts a set of Ruby values into the expected Ruby values
|
||||
def cast_expects(params)
|
||||
@caster.cast_expects(params)
|
||||
end
|
||||
|
||||
# Cast a Ruby return value into the expected Ruby value
|
||||
def cast_returns(return_value)
|
||||
@caster.cast_returns(return_value)
|
||||
end
|
||||
|
||||
# Returns the index of the first expected parameter
|
||||
# with the given name
|
||||
def expects_index_of(param_name)
|
||||
return -1 if @expects.nil?
|
||||
(0..(@expects.length-1)).each do |i|
|
||||
return i if @expects[i].name.to_s == param_name.to_s
|
||||
end
|
||||
-1
|
||||
end
|
||||
|
||||
# Returns a hash keyed by parameter name for the given
|
||||
# parameter list
|
||||
def expects_to_hash(params)
|
||||
return {} if @expects.nil?
|
||||
h = {}
|
||||
@expects.zip(params){ |type, param| h[type.name] = param }
|
||||
h
|
||||
end
|
||||
|
||||
# Backwards compatibility with previous API
|
||||
def [](sig_type)
|
||||
case sig_type
|
||||
when :expects
|
||||
@expects.map{|x| compat_signature_entry(x)}
|
||||
when :returns
|
||||
@returns.map{|x| compat_signature_entry(x)}
|
||||
end
|
||||
end
|
||||
|
||||
# String representation of this method
|
||||
def to_s
|
||||
fqn = ""
|
||||
fqn << (@returns ? (@returns[0].human_name(false) + " ") : "void ")
|
||||
fqn << "#{@public_name}("
|
||||
fqn << @expects.map{ |p| p.human_name }.join(", ") if @expects
|
||||
fqn << ")"
|
||||
fqn
|
||||
end
|
||||
|
||||
private
|
||||
def compat_signature_entry(entry)
|
||||
if entry.array?
|
||||
[compat_signature_entry(entry.element_type)]
|
||||
else
|
||||
if entry.spec.is_a?(Hash)
|
||||
{entry.spec.keys.first => entry.type_class}
|
||||
else
|
||||
entry.type_class
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
module ActionWebService # :nodoc:
|
||||
class ActionWebServiceError < StandardError # :nodoc:
|
||||
end
|
||||
|
||||
# An Action Web Service object implements a specified API.
|
||||
#
|
||||
# Used by controllers operating in _Delegated_ dispatching mode.
|
||||
#
|
||||
# ==== Example
|
||||
#
|
||||
# class PersonService < ActionWebService::Base
|
||||
# web_service_api PersonAPI
|
||||
#
|
||||
# def find_person(criteria)
|
||||
# Person.find(:all) [...]
|
||||
# end
|
||||
#
|
||||
# def delete_person(id)
|
||||
# Person.find_by_id(id).destroy
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# class PersonAPI < ActionWebService::API::Base
|
||||
# api_method :find_person, :expects => [SearchCriteria], :returns => [[Person]]
|
||||
# api_method :delete_person, :expects => [:int]
|
||||
# end
|
||||
#
|
||||
# class SearchCriteria < ActionWebService::Struct
|
||||
# member :firstname, :string
|
||||
# member :lastname, :string
|
||||
# member :email, :string
|
||||
# end
|
||||
class Base
|
||||
# Whether to report exceptions back to the caller in the protocol's exception
|
||||
# format
|
||||
class_inheritable_option :web_service_exception_reporting, true
|
||||
end
|
||||
end
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
require 'time'
|
||||
require 'date'
|
||||
require 'xmlrpc/datetime'
|
||||
|
||||
module ActionWebService # :nodoc:
|
||||
module Casting # :nodoc:
|
||||
class CastingError < ActionWebServiceError # :nodoc:
|
||||
end
|
||||
|
||||
# Performs casting of arbitrary values into the correct types for the signature
|
||||
class BaseCaster # :nodoc:
|
||||
def initialize(api_method)
|
||||
@api_method = api_method
|
||||
end
|
||||
|
||||
# Coerces the parameters in +params+ (an Enumerable) into the types
|
||||
# this method expects
|
||||
def cast_expects(params)
|
||||
self.class.cast_expects(@api_method, params)
|
||||
end
|
||||
|
||||
# Coerces the given +return_value+ into the type returned by this
|
||||
# method
|
||||
def cast_returns(return_value)
|
||||
self.class.cast_returns(@api_method, return_value)
|
||||
end
|
||||
|
||||
class << self
|
||||
include ActionWebService::SignatureTypes
|
||||
|
||||
def cast_expects(api_method, params) # :nodoc:
|
||||
return [] if api_method.expects.nil?
|
||||
api_method.expects.zip(params).map{ |type, param| cast(param, type) }
|
||||
end
|
||||
|
||||
def cast_returns(api_method, return_value) # :nodoc:
|
||||
return nil if api_method.returns.nil?
|
||||
cast(return_value, api_method.returns[0])
|
||||
end
|
||||
|
||||
def cast(value, signature_type) # :nodoc:
|
||||
return value if signature_type.nil? # signature.length != params.length
|
||||
return nil if value.nil?
|
||||
# XMLRPC protocol doesn't support nil values. It uses false instead.
|
||||
# It should never happen for SOAP.
|
||||
if signature_type.structured? && value.equal?(false)
|
||||
return nil
|
||||
end
|
||||
unless signature_type.array? || signature_type.structured?
|
||||
return value if canonical_type(value.class) == signature_type.type
|
||||
end
|
||||
if signature_type.array?
|
||||
unless value.respond_to?(:entries) && !value.is_a?(String)
|
||||
raise CastingError, "Don't know how to cast #{value.class} into #{signature_type.type.inspect}"
|
||||
end
|
||||
value.entries.map do |entry|
|
||||
cast(entry, signature_type.element_type)
|
||||
end
|
||||
elsif signature_type.structured?
|
||||
cast_to_structured_type(value, signature_type)
|
||||
elsif !signature_type.custom?
|
||||
cast_base_type(value, signature_type)
|
||||
end
|
||||
end
|
||||
|
||||
def cast_base_type(value, signature_type) # :nodoc:
|
||||
# This is a work-around for the fact that XML-RPC special-cases DateTime values into its own DateTime type
|
||||
# in order to support iso8601 dates. This doesn't work too well for us, so we'll convert it into a Time,
|
||||
# with the caveat that we won't be able to handle pre-1970 dates that are sent to us.
|
||||
#
|
||||
# See http://dev.rubyonrails.com/ticket/2516
|
||||
value = value.to_time if value.is_a?(XMLRPC::DateTime)
|
||||
|
||||
case signature_type.type
|
||||
when :int
|
||||
Integer(value)
|
||||
when :string
|
||||
value.to_s
|
||||
when :base64
|
||||
if value.is_a?(ActionWebService::Base64)
|
||||
value
|
||||
else
|
||||
ActionWebService::Base64.new(value.to_s)
|
||||
end
|
||||
when :bool
|
||||
return false if value.nil?
|
||||
return value if value == true || value == false
|
||||
case value.to_s.downcase
|
||||
when '1', 'true', 'y', 'yes'
|
||||
true
|
||||
when '0', 'false', 'n', 'no'
|
||||
false
|
||||
else
|
||||
raise CastingError, "Don't know how to cast #{value.class} into Boolean"
|
||||
end
|
||||
when :float
|
||||
Float(value)
|
||||
when :decimal
|
||||
BigDecimal(value.to_s)
|
||||
when :time
|
||||
value = "%s/%s/%s %s:%s:%s" % value.values_at(*%w[2 3 1 4 5 6]) if value.kind_of?(Hash)
|
||||
value.kind_of?(Time) ? value : Time.parse(value.to_s)
|
||||
when :date
|
||||
value = "%s/%s/%s" % value.values_at(*%w[2 3 1]) if value.kind_of?(Hash)
|
||||
value.kind_of?(Date) ? value : Date.parse(value.to_s)
|
||||
when :datetime
|
||||
value = "%s/%s/%s %s:%s:%s" % value.values_at(*%w[2 3 1 4 5 6]) if value.kind_of?(Hash)
|
||||
value.kind_of?(DateTime) ? value : DateTime.parse(value.to_s)
|
||||
end
|
||||
end
|
||||
|
||||
def cast_to_structured_type(value, signature_type) # :nodoc:
|
||||
obj = nil
|
||||
obj = value if canonical_type(value.class) == canonical_type(signature_type.type)
|
||||
obj ||= signature_type.type_class.new
|
||||
if value.respond_to?(:each_pair)
|
||||
klass = signature_type.type_class
|
||||
value.each_pair do |name, val|
|
||||
type = klass.respond_to?(:member_type) ? klass.member_type(name) : nil
|
||||
val = cast(val, type) if type
|
||||
# See http://dev.rubyonrails.com/ticket/3567
|
||||
val = val.to_time if val.is_a?(XMLRPC::DateTime)
|
||||
obj.__send__("#{name}=", val) if obj.respond_to?(name)
|
||||
end
|
||||
elsif value.respond_to?(:attributes)
|
||||
signature_type.each_member do |name, type|
|
||||
val = value.__send__(name)
|
||||
obj.__send__("#{name}=", cast(val, type)) if obj.respond_to?(name)
|
||||
end
|
||||
else
|
||||
raise CastingError, "Don't know how to cast #{value.class} to #{signature_type.type_class}"
|
||||
end
|
||||
obj
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
require 'action_web_service/client/base'
|
||||
require 'action_web_service/client/soap_client'
|
||||
require 'action_web_service/client/xmlrpc_client'
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
module ActionWebService # :nodoc:
|
||||
module Client # :nodoc:
|
||||
class ClientError < StandardError # :nodoc:
|
||||
end
|
||||
|
||||
class Base # :nodoc:
|
||||
def initialize(api, endpoint_uri)
|
||||
@api = api
|
||||
@endpoint_uri = endpoint_uri
|
||||
end
|
||||
|
||||
def method_missing(name, *args) # :nodoc:
|
||||
call_name = method_name(name)
|
||||
return super(name, *args) if call_name.nil?
|
||||
self.perform_invocation(call_name, args)
|
||||
end
|
||||
|
||||
private
|
||||
def method_name(name)
|
||||
if @api.has_api_method?(name.to_sym)
|
||||
name.to_s
|
||||
elsif @api.has_public_api_method?(name.to_s)
|
||||
@api.api_method_name(name.to_s).to_s
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Vendored
+113
@@ -0,0 +1,113 @@
|
||||
require 'soap/rpc/driver'
|
||||
require 'uri'
|
||||
|
||||
module ActionWebService # :nodoc:
|
||||
module Client # :nodoc:
|
||||
|
||||
# Implements SOAP client support (using RPC encoding for the messages).
|
||||
#
|
||||
# ==== Example Usage
|
||||
#
|
||||
# class PersonAPI < ActionWebService::API::Base
|
||||
# api_method :find_all, :returns => [[Person]]
|
||||
# end
|
||||
#
|
||||
# soap_client = ActionWebService::Client::Soap.new(PersonAPI, "http://...")
|
||||
# persons = soap_client.find_all
|
||||
#
|
||||
class Soap < Base
|
||||
# provides access to the underlying soap driver
|
||||
attr_reader :driver
|
||||
|
||||
# Creates a new web service client using the SOAP RPC protocol.
|
||||
#
|
||||
# +api+ must be an ActionWebService::API::Base derivative, and
|
||||
# +endpoint_uri+ must point at the relevant URL to which protocol requests
|
||||
# will be sent with HTTP POST.
|
||||
#
|
||||
# Valid options:
|
||||
# [<tt>:namespace</tt>] If the remote server has used a custom namespace to
|
||||
# declare its custom types, you can specify it here. This would
|
||||
# be the namespace declared with a [WebService(Namespace = "http://namespace")] attribute
|
||||
# in .NET, for example.
|
||||
# [<tt>:driver_options</tt>] If you want to supply any custom SOAP RPC driver
|
||||
# options, you can provide them as a Hash here
|
||||
#
|
||||
# The <tt>:driver_options</tt> option can be used to configure the backend SOAP
|
||||
# RPC driver. An example of configuring the SOAP backend to do
|
||||
# client-certificate authenticated SSL connections to the server:
|
||||
#
|
||||
# opts = {}
|
||||
# opts['protocol.http.ssl_config.verify_mode'] = 'OpenSSL::SSL::VERIFY_PEER'
|
||||
# opts['protocol.http.ssl_config.client_cert'] = client_cert_file_path
|
||||
# opts['protocol.http.ssl_config.client_key'] = client_key_file_path
|
||||
# opts['protocol.http.ssl_config.ca_file'] = ca_cert_file_path
|
||||
# client = ActionWebService::Client::Soap.new(api, 'https://some/service', :driver_options => opts)
|
||||
def initialize(api, endpoint_uri, options={})
|
||||
super(api, endpoint_uri)
|
||||
@namespace = options[:namespace] || 'urn:ActionWebService'
|
||||
@driver_options = options[:driver_options] || {}
|
||||
@protocol = ActionWebService::Protocol::Soap::SoapProtocol.new @namespace
|
||||
@soap_action_base = options[:soap_action_base]
|
||||
@soap_action_base ||= URI.parse(endpoint_uri).path
|
||||
@driver = create_soap_rpc_driver(api, endpoint_uri)
|
||||
@driver_options.each do |name, value|
|
||||
@driver.options[name.to_s] = value
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
def perform_invocation(method_name, args)
|
||||
method = @api.api_methods[method_name.to_sym]
|
||||
args = method.cast_expects(args.dup) rescue args
|
||||
return_value = @driver.send(method_name, *args)
|
||||
method.cast_returns(return_value.dup) rescue return_value
|
||||
end
|
||||
|
||||
def soap_action(method_name)
|
||||
"#{@soap_action_base}/#{method_name}"
|
||||
end
|
||||
|
||||
private
|
||||
def create_soap_rpc_driver(api, endpoint_uri)
|
||||
@protocol.register_api(api)
|
||||
driver = SoapDriver.new(endpoint_uri, nil)
|
||||
driver.mapping_registry = @protocol.marshaler.registry
|
||||
api.api_methods.each do |name, method|
|
||||
qname = XSD::QName.new(@namespace, method.public_name)
|
||||
action = soap_action(method.public_name)
|
||||
expects = method.expects
|
||||
returns = method.returns
|
||||
param_def = []
|
||||
if expects
|
||||
expects.each do |type|
|
||||
type_binding = @protocol.marshaler.lookup_type(type)
|
||||
if SOAP::Version >= "1.5.5"
|
||||
param_def << ['in', type.name.to_s, [type_binding.type.type_class.to_s]]
|
||||
else
|
||||
param_def << ['in', type.name, type_binding.mapping]
|
||||
end
|
||||
end
|
||||
end
|
||||
if returns
|
||||
type_binding = @protocol.marshaler.lookup_type(returns[0])
|
||||
if SOAP::Version >= "1.5.5"
|
||||
param_def << ['retval', 'return', [type_binding.type.type_class.to_s]]
|
||||
else
|
||||
param_def << ['retval', 'return', type_binding.mapping]
|
||||
end
|
||||
end
|
||||
driver.add_method(qname, action, method.name.to_s, param_def)
|
||||
end
|
||||
driver
|
||||
end
|
||||
|
||||
class SoapDriver < SOAP::RPC::Driver # :nodoc:
|
||||
def add_method(qname, soapaction, name, param_def)
|
||||
@proxy.add_rpc_method(qname, soapaction, name, param_def)
|
||||
add_rpc_method_interface(name, param_def)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Vendored
+58
@@ -0,0 +1,58 @@
|
||||
require 'uri'
|
||||
require 'xmlrpc/client'
|
||||
|
||||
module ActionWebService # :nodoc:
|
||||
module Client # :nodoc:
|
||||
|
||||
# Implements XML-RPC client support
|
||||
#
|
||||
# ==== Example Usage
|
||||
#
|
||||
# class BloggerAPI < ActionWebService::API::Base
|
||||
# inflect_names false
|
||||
# api_method :getRecentPosts, :returns => [[Blog::Post]]
|
||||
# end
|
||||
#
|
||||
# blog = ActionWebService::Client::XmlRpc.new(BloggerAPI, "http://.../RPC", :handler_name => "blogger")
|
||||
# posts = blog.getRecentPosts
|
||||
class XmlRpc < Base
|
||||
|
||||
# Creates a new web service client using the XML-RPC protocol.
|
||||
#
|
||||
# +api+ must be an ActionWebService::API::Base derivative, and
|
||||
# +endpoint_uri+ must point at the relevant URL to which protocol requests
|
||||
# will be sent with HTTP POST.
|
||||
#
|
||||
# Valid options:
|
||||
# [<tt>:handler_name</tt>] If the remote server defines its services inside special
|
||||
# handler (the Blogger API uses a <tt>"blogger"</tt> handler name for example),
|
||||
# provide it here, or your method calls will fail
|
||||
def initialize(api, endpoint_uri, options={})
|
||||
@api = api
|
||||
@handler_name = options[:handler_name]
|
||||
@protocol = ActionWebService::Protocol::XmlRpc::XmlRpcProtocol.new
|
||||
@client = XMLRPC::Client.new2(endpoint_uri, options[:proxy], options[:timeout])
|
||||
end
|
||||
|
||||
protected
|
||||
def perform_invocation(method_name, args)
|
||||
method = @api.api_methods[method_name.to_sym]
|
||||
if method.expects && method.expects.length != args.length
|
||||
raise(ArgumentError, "#{method.public_name}: wrong number of arguments (#{args.length} for #{method.expects.length})")
|
||||
end
|
||||
args = method.cast_expects(args.dup) rescue args
|
||||
if method.expects
|
||||
method.expects.each_with_index{ |type, i| args[i] = @protocol.value_to_xmlrpc_wire_format(args[i], type) }
|
||||
end
|
||||
ok, return_value = @client.call2(public_name(method_name), *args)
|
||||
return (method.cast_returns(return_value.dup) rescue return_value) if ok
|
||||
raise(ClientError, "#{return_value.faultCode}: #{return_value.faultString}")
|
||||
end
|
||||
|
||||
def public_name(method_name)
|
||||
public_name = @api.public_api_method_name(method_name)
|
||||
@handler_name ? "#{@handler_name}.#{public_name}" : public_name
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
require 'action_web_service/container/direct_container'
|
||||
require 'action_web_service/container/delegated_container'
|
||||
require 'action_web_service/container/action_controller_container'
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
module ActionWebService # :nodoc:
|
||||
module Container # :nodoc:
|
||||
module ActionController # :nodoc:
|
||||
def self.included(base) # :nodoc:
|
||||
class << base
|
||||
include ClassMethods
|
||||
alias_method_chain :inherited, :api
|
||||
alias_method_chain :web_service_api, :require
|
||||
end
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
# Creates a client for accessing remote web services, using the
|
||||
# given +protocol+ to communicate with the +endpoint_uri+.
|
||||
#
|
||||
# ==== Example
|
||||
#
|
||||
# class MyController < ActionController::Base
|
||||
# web_client_api :blogger, :xmlrpc, "http://blogger.com/myblog/api/RPC2", :handler_name => 'blogger'
|
||||
# end
|
||||
#
|
||||
# In this example, a protected method named <tt>blogger</tt> will
|
||||
# now exist on the controller, and calling it will return the
|
||||
# XML-RPC client object for working with that remote service.
|
||||
#
|
||||
# +options+ is the set of protocol client specific options (see
|
||||
# a protocol client class for details).
|
||||
#
|
||||
# If your API definition does not exist on the load path with the
|
||||
# correct rules for it to be found using +name+, you can pass in
|
||||
# the API definition class via +options+, using a key of <tt>:api</tt>
|
||||
def web_client_api(name, protocol, endpoint_uri, options={})
|
||||
unless method_defined?(name)
|
||||
api_klass = options.delete(:api) || require_web_service_api(name)
|
||||
class_eval do
|
||||
define_method(name) do
|
||||
create_web_service_client(api_klass, protocol, endpoint_uri, options)
|
||||
end
|
||||
protected name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def web_service_api_with_require(definition=nil) # :nodoc:
|
||||
return web_service_api_without_require if definition.nil?
|
||||
case definition
|
||||
when String, Symbol
|
||||
klass = require_web_service_api(definition)
|
||||
else
|
||||
klass = definition
|
||||
end
|
||||
web_service_api_without_require(klass)
|
||||
end
|
||||
|
||||
def require_web_service_api(name) # :nodoc:
|
||||
case name
|
||||
when String, Symbol
|
||||
file_name = name.to_s.underscore + "_api"
|
||||
class_name = file_name.camelize
|
||||
class_names = [class_name, class_name.sub(/Api$/, 'API')]
|
||||
begin
|
||||
require_dependency(file_name)
|
||||
rescue LoadError => load_error
|
||||
requiree = / -- (.*?)(\.rb)?$/.match(load_error).to_a[1]
|
||||
msg = requiree == file_name ? "Missing API definition file in apis/#{file_name}.rb" : "Can't load file: #{requiree}"
|
||||
raise LoadError.new(msg).copy_blame!(load_error)
|
||||
end
|
||||
klass = nil
|
||||
class_names.each do |name|
|
||||
klass = name.constantize rescue nil
|
||||
break unless klass.nil?
|
||||
end
|
||||
unless klass
|
||||
raise(NameError, "neither #{class_names[0]} or #{class_names[1]} found")
|
||||
end
|
||||
klass
|
||||
else
|
||||
raise(ArgumentError, "expected String or Symbol argument")
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def inherited_with_api(child)
|
||||
inherited_without_api(child)
|
||||
begin child.web_service_api(child.controller_path)
|
||||
rescue MissingSourceFile => e
|
||||
raise unless e.is_missing?("apis/#{child.controller_path}_api")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
module ActionWebService # :nodoc:
|
||||
module Container # :nodoc:
|
||||
module Delegated # :nodoc:
|
||||
class ContainerError < ActionWebServiceError # :nodoc:
|
||||
end
|
||||
|
||||
def self.included(base) # :nodoc:
|
||||
base.extend(ClassMethods)
|
||||
base.send(:include, ActionWebService::Container::Delegated::InstanceMethods)
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
# Declares a web service that will provide access to the API of the given
|
||||
# +object+. +object+ must be an ActionWebService::Base derivative.
|
||||
#
|
||||
# Web service object creation can either be _immediate_, where the object
|
||||
# instance is given at class definition time, or _deferred_, where
|
||||
# object instantiation is delayed until request time.
|
||||
#
|
||||
# ==== Immediate web service object example
|
||||
#
|
||||
# class ApiController < ApplicationController
|
||||
# web_service_dispatching_mode :delegated
|
||||
#
|
||||
# web_service :person, PersonService.new
|
||||
# end
|
||||
#
|
||||
# For deferred instantiation, a block should be given instead of an
|
||||
# object instance. This block will be executed in controller instance
|
||||
# context, so it can rely on controller instance variables being present.
|
||||
#
|
||||
# ==== Deferred web service object example
|
||||
#
|
||||
# class ApiController < ApplicationController
|
||||
# web_service_dispatching_mode :delegated
|
||||
#
|
||||
# web_service(:person) { PersonService.new(request.env) }
|
||||
# end
|
||||
def web_service(name, object=nil, &block)
|
||||
if (object && block_given?) || (object.nil? && block.nil?)
|
||||
raise(ContainerError, "either service, or a block must be given")
|
||||
end
|
||||
name = name.to_sym
|
||||
if block_given?
|
||||
info = { name => { :block => block } }
|
||||
else
|
||||
info = { name => { :object => object } }
|
||||
end
|
||||
write_inheritable_hash("web_services", info)
|
||||
call_web_service_definition_callbacks(self, name, info)
|
||||
end
|
||||
|
||||
# Whether this service contains a service with the given +name+
|
||||
def has_web_service?(name)
|
||||
web_services.has_key?(name.to_sym)
|
||||
end
|
||||
|
||||
def web_services # :nodoc:
|
||||
read_inheritable_attribute("web_services") || {}
|
||||
end
|
||||
|
||||
def add_web_service_definition_callback(&block) # :nodoc:
|
||||
write_inheritable_array("web_service_definition_callbacks", [block])
|
||||
end
|
||||
|
||||
private
|
||||
def call_web_service_definition_callbacks(container_class, web_service_name, service_info)
|
||||
(read_inheritable_attribute("web_service_definition_callbacks") || []).each do |block|
|
||||
block.call(container_class, web_service_name, service_info)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module InstanceMethods # :nodoc:
|
||||
def web_service_object(web_service_name)
|
||||
info = self.class.web_services[web_service_name.to_sym]
|
||||
unless info
|
||||
raise(ContainerError, "no such web service '#{web_service_name}'")
|
||||
end
|
||||
service = info[:block]
|
||||
service ? self.instance_eval(&service) : info[:object]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
nested_projects/vendor/plugins/actionwebservice/lib/action_web_service/container/direct_container.rb
Vendored
+69
@@ -0,0 +1,69 @@
|
||||
module ActionWebService # :nodoc:
|
||||
module Container # :nodoc:
|
||||
module Direct # :nodoc:
|
||||
class ContainerError < ActionWebServiceError # :nodoc:
|
||||
end
|
||||
|
||||
def self.included(base) # :nodoc:
|
||||
base.extend(ClassMethods)
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
# Attaches ActionWebService API +definition+ to the calling class.
|
||||
#
|
||||
# Action Controllers can have a default associated API, removing the need
|
||||
# to call this method if you follow the Action Web Service naming conventions.
|
||||
#
|
||||
# A controller with a class name of GoogleSearchController will
|
||||
# implicitly load <tt>app/apis/google_search_api.rb</tt>, and expect the
|
||||
# API definition class to be named <tt>GoogleSearchAPI</tt> or
|
||||
# <tt>GoogleSearchApi</tt>.
|
||||
#
|
||||
# ==== Service class example
|
||||
#
|
||||
# class MyService < ActionWebService::Base
|
||||
# web_service_api MyAPI
|
||||
# end
|
||||
#
|
||||
# class MyAPI < ActionWebService::API::Base
|
||||
# ...
|
||||
# end
|
||||
#
|
||||
# ==== Controller class example
|
||||
#
|
||||
# class MyController < ActionController::Base
|
||||
# web_service_api MyAPI
|
||||
# end
|
||||
#
|
||||
# class MyAPI < ActionWebService::API::Base
|
||||
# ...
|
||||
# end
|
||||
def web_service_api(definition=nil)
|
||||
if definition.nil?
|
||||
read_inheritable_attribute("web_service_api")
|
||||
else
|
||||
if definition.is_a?(Symbol)
|
||||
raise(ContainerError, "symbols can only be used for #web_service_api inside of a controller")
|
||||
end
|
||||
unless definition.respond_to?(:ancestors) && definition.ancestors.include?(ActionWebService::API::Base)
|
||||
raise(ContainerError, "#{definition.to_s} is not a valid API definition")
|
||||
end
|
||||
write_inheritable_attribute("web_service_api", definition)
|
||||
call_web_service_api_callbacks(self, definition)
|
||||
end
|
||||
end
|
||||
|
||||
def add_web_service_api_callback(&block) # :nodoc:
|
||||
write_inheritable_array("web_service_api_callbacks", [block])
|
||||
end
|
||||
|
||||
private
|
||||
def call_web_service_api_callbacks(container_class, definition)
|
||||
(read_inheritable_attribute("web_service_api_callbacks") || []).each do |block|
|
||||
block.call(container_class, definition)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
require 'action_web_service/dispatcher/abstract'
|
||||
require 'action_web_service/dispatcher/action_controller_dispatcher'
|
||||
Vendored
+207
@@ -0,0 +1,207 @@
|
||||
require 'benchmark'
|
||||
|
||||
module ActionWebService # :nodoc:
|
||||
module Dispatcher # :nodoc:
|
||||
class DispatcherError < ActionWebService::ActionWebServiceError # :nodoc:
|
||||
def initialize(*args)
|
||||
super
|
||||
set_backtrace(caller)
|
||||
end
|
||||
end
|
||||
|
||||
def self.included(base) # :nodoc:
|
||||
base.class_inheritable_option(:web_service_dispatching_mode, :direct)
|
||||
base.class_inheritable_option(:web_service_exception_reporting, true)
|
||||
base.send(:include, ActionWebService::Dispatcher::InstanceMethods)
|
||||
end
|
||||
|
||||
module InstanceMethods # :nodoc:
|
||||
private
|
||||
def invoke_web_service_request(protocol_request)
|
||||
invocation = web_service_invocation(protocol_request)
|
||||
if invocation.is_a?(Array) && protocol_request.protocol.is_a?(Protocol::XmlRpc::XmlRpcProtocol)
|
||||
xmlrpc_multicall_invoke(invocation)
|
||||
else
|
||||
web_service_invoke(invocation)
|
||||
end
|
||||
end
|
||||
|
||||
def web_service_direct_invoke(invocation)
|
||||
@method_params = invocation.method_ordered_params
|
||||
arity = method(invocation.api_method.name).arity rescue 0
|
||||
if arity < 0 || arity > 0
|
||||
params = @method_params
|
||||
else
|
||||
params = []
|
||||
end
|
||||
web_service_filtered_invoke(invocation, params)
|
||||
end
|
||||
|
||||
def web_service_delegated_invoke(invocation)
|
||||
web_service_filtered_invoke(invocation, invocation.method_ordered_params)
|
||||
end
|
||||
|
||||
def web_service_filtered_invoke(invocation, params)
|
||||
cancellation_reason = nil
|
||||
return_value = invocation.service.perform_invocation(invocation.api_method.name, params) do |x|
|
||||
cancellation_reason = x
|
||||
end
|
||||
if cancellation_reason
|
||||
raise(DispatcherError, "request canceled: #{cancellation_reason}")
|
||||
end
|
||||
return_value
|
||||
end
|
||||
|
||||
def web_service_invoke(invocation)
|
||||
case web_service_dispatching_mode
|
||||
when :direct
|
||||
return_value = web_service_direct_invoke(invocation)
|
||||
when :delegated, :layered
|
||||
return_value = web_service_delegated_invoke(invocation)
|
||||
end
|
||||
web_service_create_response(invocation.protocol, invocation.protocol_options, invocation.api, invocation.api_method, return_value)
|
||||
end
|
||||
|
||||
def xmlrpc_multicall_invoke(invocations)
|
||||
responses = []
|
||||
invocations.each do |invocation|
|
||||
if invocation.is_a?(Hash)
|
||||
responses << [invocation, nil]
|
||||
next
|
||||
end
|
||||
begin
|
||||
case web_service_dispatching_mode
|
||||
when :direct
|
||||
return_value = web_service_direct_invoke(invocation)
|
||||
when :delegated, :layered
|
||||
return_value = web_service_delegated_invoke(invocation)
|
||||
end
|
||||
api_method = invocation.api_method
|
||||
if invocation.api.has_api_method?(api_method.name)
|
||||
response_type = (api_method.returns ? api_method.returns[0] : nil)
|
||||
return_value = api_method.cast_returns(return_value)
|
||||
else
|
||||
response_type = ActionWebService::SignatureTypes.canonical_signature_entry(return_value.class, 0)
|
||||
end
|
||||
responses << [return_value, response_type]
|
||||
rescue Exception => e
|
||||
responses << [{ 'faultCode' => 3, 'faultString' => e.message }, nil]
|
||||
end
|
||||
end
|
||||
invocation = invocations[0]
|
||||
invocation.protocol.encode_multicall_response(responses, invocation.protocol_options)
|
||||
end
|
||||
|
||||
def web_service_invocation(request, level = 0)
|
||||
public_method_name = request.method_name
|
||||
invocation = Invocation.new
|
||||
invocation.protocol = request.protocol
|
||||
invocation.protocol_options = request.protocol_options
|
||||
invocation.service_name = request.service_name
|
||||
if web_service_dispatching_mode == :layered
|
||||
case invocation.protocol
|
||||
when Protocol::Soap::SoapProtocol
|
||||
soap_action = request.protocol_options[:soap_action]
|
||||
if soap_action && soap_action =~ /^\/\w+\/(\w+)\//
|
||||
invocation.service_name = $1
|
||||
end
|
||||
when Protocol::XmlRpc::XmlRpcProtocol
|
||||
if request.method_name =~ /^([^\.]+)\.(.*)$/
|
||||
public_method_name = $2
|
||||
invocation.service_name = $1
|
||||
end
|
||||
end
|
||||
end
|
||||
if invocation.protocol.is_a? Protocol::XmlRpc::XmlRpcProtocol
|
||||
if public_method_name == 'multicall' && invocation.service_name == 'system'
|
||||
if level > 0
|
||||
raise(DispatcherError, "Recursive system.multicall invocations not allowed")
|
||||
end
|
||||
multicall = request.method_params.dup
|
||||
unless multicall.is_a?(Array) && multicall[0].is_a?(Array)
|
||||
raise(DispatcherError, "Malformed multicall (expected array of Hash elements)")
|
||||
end
|
||||
multicall = multicall[0]
|
||||
return multicall.map do |item|
|
||||
raise(DispatcherError, "Multicall elements must be Hash") unless item.is_a?(Hash)
|
||||
raise(DispatcherError, "Multicall elements must contain a 'methodName' key") unless item.has_key?('methodName')
|
||||
method_name = item['methodName']
|
||||
params = item.has_key?('params') ? item['params'] : []
|
||||
multicall_request = request.dup
|
||||
multicall_request.method_name = method_name
|
||||
multicall_request.method_params = params
|
||||
begin
|
||||
web_service_invocation(multicall_request, level + 1)
|
||||
rescue Exception => e
|
||||
{'faultCode' => 4, 'faultMessage' => e.message}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
case web_service_dispatching_mode
|
||||
when :direct
|
||||
invocation.api = self.class.web_service_api
|
||||
invocation.service = self
|
||||
when :delegated, :layered
|
||||
invocation.service = web_service_object(invocation.service_name)
|
||||
invocation.api = invocation.service.class.web_service_api
|
||||
end
|
||||
if invocation.api.nil?
|
||||
raise(DispatcherError, "no API attached to #{invocation.service.class}")
|
||||
end
|
||||
invocation.protocol.register_api(invocation.api)
|
||||
request.api = invocation.api
|
||||
if invocation.api.has_public_api_method?(public_method_name)
|
||||
invocation.api_method = invocation.api.public_api_method_instance(public_method_name)
|
||||
else
|
||||
if invocation.api.default_api_method.nil?
|
||||
raise(DispatcherError, "no such method '#{public_method_name}' on API #{invocation.api}")
|
||||
else
|
||||
invocation.api_method = invocation.api.default_api_method_instance
|
||||
end
|
||||
end
|
||||
if invocation.service.nil?
|
||||
raise(DispatcherError, "no service available for service name #{invocation.service_name}")
|
||||
end
|
||||
unless invocation.service.respond_to?(invocation.api_method.name)
|
||||
raise(DispatcherError, "no such method '#{public_method_name}' on API #{invocation.api} (#{invocation.api_method.name})")
|
||||
end
|
||||
request.api_method = invocation.api_method
|
||||
begin
|
||||
invocation.method_ordered_params = invocation.api_method.cast_expects(request.method_params.dup)
|
||||
rescue
|
||||
logger.warn "Casting of method parameters failed" unless logger.nil?
|
||||
invocation.method_ordered_params = request.method_params
|
||||
end
|
||||
request.method_params = invocation.method_ordered_params
|
||||
invocation.method_named_params = {}
|
||||
invocation.api_method.param_names.inject(0) do |m, n|
|
||||
invocation.method_named_params[n] = invocation.method_ordered_params[m]
|
||||
m + 1
|
||||
end
|
||||
invocation
|
||||
end
|
||||
|
||||
def web_service_create_response(protocol, protocol_options, api, api_method, return_value)
|
||||
if api.has_api_method?(api_method.name)
|
||||
return_type = api_method.returns ? api_method.returns[0] : nil
|
||||
return_value = api_method.cast_returns(return_value)
|
||||
else
|
||||
return_type = ActionWebService::SignatureTypes.canonical_signature_entry(return_value.class, 0)
|
||||
end
|
||||
protocol.encode_response(api_method.public_name + 'Response', return_value, return_type, protocol_options)
|
||||
end
|
||||
|
||||
class Invocation # :nodoc:
|
||||
attr_accessor :protocol
|
||||
attr_accessor :protocol_options
|
||||
attr_accessor :service_name
|
||||
attr_accessor :api
|
||||
attr_accessor :api_method
|
||||
attr_accessor :method_ordered_params
|
||||
attr_accessor :method_named_params
|
||||
attr_accessor :service
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
+379
@@ -0,0 +1,379 @@
|
||||
require 'benchmark'
|
||||
require 'builder/xmlmarkup'
|
||||
|
||||
module ActionWebService # :nodoc:
|
||||
module Dispatcher # :nodoc:
|
||||
module ActionController # :nodoc:
|
||||
def self.included(base) # :nodoc:
|
||||
class << base
|
||||
include ClassMethods
|
||||
alias_method_chain :inherited, :action_controller
|
||||
end
|
||||
base.class_eval do
|
||||
alias_method :web_service_direct_invoke_without_controller, :web_service_direct_invoke
|
||||
end
|
||||
base.add_web_service_api_callback do |klass, api|
|
||||
if klass.web_service_dispatching_mode == :direct
|
||||
klass.class_eval 'def api; dispatch_web_service_request; end'
|
||||
end
|
||||
end
|
||||
base.add_web_service_definition_callback do |klass, name, info|
|
||||
if klass.web_service_dispatching_mode == :delegated
|
||||
klass.class_eval "def #{name}; dispatch_web_service_request; end"
|
||||
elsif klass.web_service_dispatching_mode == :layered
|
||||
klass.class_eval 'def api; dispatch_web_service_request; end'
|
||||
end
|
||||
end
|
||||
base.send(:include, ActionWebService::Dispatcher::ActionController::InstanceMethods)
|
||||
end
|
||||
|
||||
module ClassMethods # :nodoc:
|
||||
def inherited_with_action_controller(child)
|
||||
inherited_without_action_controller(child)
|
||||
child.send(:include, ActionWebService::Dispatcher::ActionController::WsdlAction)
|
||||
end
|
||||
end
|
||||
|
||||
module InstanceMethods # :nodoc:
|
||||
private
|
||||
def dispatch_web_service_request
|
||||
method = request.method.to_s.upcase
|
||||
allowed_methods = self.class.web_service_api ? (self.class.web_service_api.allowed_http_methods || []) : [ :post ]
|
||||
allowed_methods = allowed_methods.map{|m| m.to_s.upcase }
|
||||
if !allowed_methods.include?(method)
|
||||
render :text => "#{method} not supported", :status=>500
|
||||
return
|
||||
end
|
||||
exception = nil
|
||||
begin
|
||||
ws_request = discover_web_service_request(request)
|
||||
rescue Exception => e
|
||||
exception = e
|
||||
end
|
||||
if ws_request
|
||||
ws_response = nil
|
||||
exception = nil
|
||||
bm = Benchmark.measure do
|
||||
begin
|
||||
ws_response = invoke_web_service_request(ws_request)
|
||||
rescue Exception => e
|
||||
exception = e
|
||||
end
|
||||
end
|
||||
log_request(ws_request, request.raw_post)
|
||||
if exception
|
||||
log_error(exception) unless logger.nil?
|
||||
send_web_service_error_response(ws_request, exception)
|
||||
else
|
||||
send_web_service_response(ws_response, bm.real)
|
||||
end
|
||||
else
|
||||
exception ||= DispatcherError.new("Malformed SOAP or XML-RPC protocol message")
|
||||
log_error(exception) unless logger.nil?
|
||||
send_web_service_error_response(ws_request, exception)
|
||||
end
|
||||
rescue Exception => e
|
||||
log_error(e) unless logger.nil?
|
||||
send_web_service_error_response(ws_request, e)
|
||||
end
|
||||
|
||||
def send_web_service_response(ws_response, elapsed=nil)
|
||||
log_response(ws_response, elapsed)
|
||||
options = { :type => ws_response.content_type, :disposition => 'inline' }
|
||||
send_data(ws_response.body, options)
|
||||
end
|
||||
|
||||
def send_web_service_error_response(ws_request, exception)
|
||||
if ws_request
|
||||
unless self.class.web_service_exception_reporting
|
||||
exception = DispatcherError.new("Internal server error (exception raised)")
|
||||
end
|
||||
api_method = ws_request.api_method
|
||||
public_method_name = api_method ? api_method.public_name : ws_request.method_name
|
||||
return_type = ActionWebService::SignatureTypes.canonical_signature_entry(Exception, 0)
|
||||
ws_response = ws_request.protocol.encode_response(public_method_name + 'Response', exception, return_type, ws_request.protocol_options)
|
||||
send_web_service_response(ws_response)
|
||||
else
|
||||
if self.class.web_service_exception_reporting
|
||||
message = exception.message
|
||||
backtrace = "\nBacktrace:\n#{exception.backtrace.join("\n")}"
|
||||
else
|
||||
message = "Exception raised"
|
||||
backtrace = ""
|
||||
end
|
||||
render :text => "Internal protocol error: #{message}#{backtrace}", :status => 500
|
||||
end
|
||||
end
|
||||
|
||||
def web_service_direct_invoke(invocation)
|
||||
invocation.method_named_params.each do |name, value|
|
||||
params[name] = value
|
||||
end
|
||||
web_service_direct_invoke_without_controller(invocation)
|
||||
end
|
||||
|
||||
def log_request(ws_request, body)
|
||||
unless logger.nil?
|
||||
name = ws_request.method_name
|
||||
api_method = ws_request.api_method
|
||||
params = ws_request.method_params
|
||||
if api_method && api_method.expects
|
||||
params = api_method.expects.zip(params).map{ |type, param| "#{type.name}=>#{param.inspect}" }
|
||||
else
|
||||
params = params.map{ |param| param.inspect }
|
||||
end
|
||||
service = ws_request.service_name
|
||||
logger.debug("\nWeb Service Request: #{name}(#{params.join(", ")}) Entrypoint: #{service}")
|
||||
logger.debug(indent(body))
|
||||
end
|
||||
end
|
||||
|
||||
def log_response(ws_response, elapsed=nil)
|
||||
unless logger.nil?
|
||||
elapsed = (elapsed ? " (%f):" % elapsed : ":")
|
||||
logger.debug("\nWeb Service Response" + elapsed + " => #{ws_response.return_value.inspect}")
|
||||
logger.debug(indent(ws_response.body))
|
||||
end
|
||||
end
|
||||
|
||||
def indent(body)
|
||||
body.split(/\n/).map{|x| " #{x}"}.join("\n")
|
||||
end
|
||||
end
|
||||
|
||||
module WsdlAction # :nodoc:
|
||||
XsdNs = 'http://www.w3.org/2001/XMLSchema'
|
||||
WsdlNs = 'http://schemas.xmlsoap.org/wsdl/'
|
||||
SoapNs = 'http://schemas.xmlsoap.org/wsdl/soap/'
|
||||
SoapEncodingNs = 'http://schemas.xmlsoap.org/soap/encoding/'
|
||||
SoapHttpTransport = 'http://schemas.xmlsoap.org/soap/http'
|
||||
|
||||
def wsdl
|
||||
case request.method
|
||||
when :get
|
||||
begin
|
||||
options = { :type => 'text/xml', :disposition => 'inline' }
|
||||
send_data(to_wsdl, options)
|
||||
rescue Exception => e
|
||||
log_error(e) unless logger.nil?
|
||||
end
|
||||
when :post
|
||||
render :text => 'POST not supported', :status => 500
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def base_uri
|
||||
host = request.host_with_port
|
||||
relative_url_root = request.relative_url_root
|
||||
scheme = request.ssl? ? 'https' : 'http'
|
||||
'%s://%s%s/%s/' % [scheme, host, relative_url_root, self.class.controller_path]
|
||||
end
|
||||
|
||||
def to_wsdl
|
||||
xml = ''
|
||||
dispatching_mode = web_service_dispatching_mode
|
||||
global_service_name = wsdl_service_name
|
||||
namespace = wsdl_namespace || 'urn:ActionWebService'
|
||||
soap_action_base = "/#{controller_name}"
|
||||
|
||||
marshaler = ActionWebService::Protocol::Soap::SoapMarshaler.new(namespace)
|
||||
apis = {}
|
||||
case dispatching_mode
|
||||
when :direct
|
||||
api = self.class.web_service_api
|
||||
web_service_name = controller_class_name.sub(/Controller$/, '').underscore
|
||||
apis[web_service_name] = [api, register_api(api, marshaler)]
|
||||
when :delegated, :layered
|
||||
self.class.web_services.each do |web_service_name, info|
|
||||
service = web_service_object(web_service_name)
|
||||
api = service.class.web_service_api
|
||||
apis[web_service_name] = [api, register_api(api, marshaler)]
|
||||
end
|
||||
end
|
||||
custom_types = []
|
||||
apis.values.each do |api, bindings|
|
||||
bindings.each do |b|
|
||||
custom_types << b unless custom_types.include?(b)
|
||||
end
|
||||
end
|
||||
|
||||
xm = Builder::XmlMarkup.new(:target => xml, :indent => 2)
|
||||
xm.instruct!
|
||||
xm.definitions('name' => wsdl_service_name,
|
||||
'targetNamespace' => namespace,
|
||||
'xmlns:typens' => namespace,
|
||||
'xmlns:xsd' => XsdNs,
|
||||
'xmlns:soap' => SoapNs,
|
||||
'xmlns:soapenc' => SoapEncodingNs,
|
||||
'xmlns:wsdl' => WsdlNs,
|
||||
'xmlns' => WsdlNs) do
|
||||
# Generate XSD
|
||||
if custom_types.size > 0
|
||||
xm.types do
|
||||
xm.xsd(:schema, 'xmlns' => XsdNs, 'targetNamespace' => namespace) do
|
||||
custom_types.each do |binding|
|
||||
case
|
||||
when binding.type.array?
|
||||
xm.xsd(:complexType, 'name' => binding.type_name) do
|
||||
xm.xsd(:complexContent) do
|
||||
xm.xsd(:restriction, 'base' => 'soapenc:Array') do
|
||||
xm.xsd(:attribute, 'ref' => 'soapenc:arrayType',
|
||||
'wsdl:arrayType' => binding.element_binding.qualified_type_name('typens') + '[]')
|
||||
end
|
||||
end
|
||||
end
|
||||
when binding.type.structured?
|
||||
xm.xsd(:complexType, 'name' => binding.type_name) do
|
||||
xm.xsd(:all) do
|
||||
binding.type.each_member do |name, type|
|
||||
b = marshaler.register_type(type)
|
||||
xm.xsd(:element, 'name' => name, 'type' => b.qualified_type_name('typens'))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# APIs
|
||||
apis.each do |api_name, values|
|
||||
api = values[0]
|
||||
api.api_methods.each do |name, method|
|
||||
gen = lambda do |msg_name, direction|
|
||||
xm.message('name' => message_name_for(api_name, msg_name)) do
|
||||
sym = nil
|
||||
if direction == :out
|
||||
returns = method.returns
|
||||
if returns
|
||||
binding = marshaler.register_type(returns[0])
|
||||
xm.part('name' => 'return', 'type' => binding.qualified_type_name('typens'))
|
||||
end
|
||||
else
|
||||
expects = method.expects
|
||||
expects.each do |type|
|
||||
binding = marshaler.register_type(type)
|
||||
xm.part('name' => type.name, 'type' => binding.qualified_type_name('typens'))
|
||||
end if expects
|
||||
end
|
||||
end
|
||||
end
|
||||
public_name = method.public_name
|
||||
gen.call(public_name, :in)
|
||||
gen.call("#{public_name}Response", :out)
|
||||
end
|
||||
|
||||
# Port
|
||||
port_name = port_name_for(global_service_name, api_name)
|
||||
xm.portType('name' => port_name) do
|
||||
api.api_methods.each do |name, method|
|
||||
xm.operation('name' => method.public_name) do
|
||||
xm.input('message' => "typens:" + message_name_for(api_name, method.public_name))
|
||||
xm.output('message' => "typens:" + message_name_for(api_name, "#{method.public_name}Response"))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Bind it
|
||||
binding_name = binding_name_for(global_service_name, api_name)
|
||||
xm.binding('name' => binding_name, 'type' => "typens:#{port_name}") do
|
||||
xm.soap(:binding, 'style' => 'rpc', 'transport' => SoapHttpTransport)
|
||||
api.api_methods.each do |name, method|
|
||||
xm.operation('name' => method.public_name) do
|
||||
case web_service_dispatching_mode
|
||||
when :direct
|
||||
soap_action = soap_action_base + "/api/" + method.public_name
|
||||
when :delegated, :layered
|
||||
soap_action = soap_action_base \
|
||||
+ "/" + api_name.to_s \
|
||||
+ "/" + method.public_name
|
||||
end
|
||||
xm.soap(:operation, 'soapAction' => soap_action)
|
||||
xm.input do
|
||||
xm.soap(:body,
|
||||
'use' => 'encoded',
|
||||
'namespace' => namespace,
|
||||
'encodingStyle' => SoapEncodingNs)
|
||||
end
|
||||
xm.output do
|
||||
xm.soap(:body,
|
||||
'use' => 'encoded',
|
||||
'namespace' => namespace,
|
||||
'encodingStyle' => SoapEncodingNs)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Define it
|
||||
xm.service('name' => "#{global_service_name}Service") do
|
||||
apis.each do |api_name, values|
|
||||
port_name = port_name_for(global_service_name, api_name)
|
||||
binding_name = binding_name_for(global_service_name, api_name)
|
||||
case web_service_dispatching_mode
|
||||
when :direct, :layered
|
||||
binding_target = 'api'
|
||||
when :delegated
|
||||
binding_target = api_name.to_s
|
||||
end
|
||||
xm.port('name' => port_name, 'binding' => "typens:#{binding_name}") do
|
||||
xm.soap(:address, 'location' => "#{base_uri}#{binding_target}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def port_name_for(global_service, service)
|
||||
"#{global_service}#{service.to_s.camelize}Port"
|
||||
end
|
||||
|
||||
def binding_name_for(global_service, service)
|
||||
"#{global_service}#{service.to_s.camelize}Binding"
|
||||
end
|
||||
|
||||
def message_name_for(api_name, message_name)
|
||||
mode = web_service_dispatching_mode
|
||||
if mode == :layered || mode == :delegated
|
||||
api_name.to_s + '-' + message_name
|
||||
else
|
||||
message_name
|
||||
end
|
||||
end
|
||||
|
||||
def register_api(api, marshaler)
|
||||
bindings = {}
|
||||
traverse_custom_types(api, marshaler, bindings) do |binding|
|
||||
bindings[binding] = nil unless bindings.has_key?(binding)
|
||||
element_binding = binding.element_binding
|
||||
bindings[element_binding] = nil if element_binding && !bindings.has_key?(element_binding)
|
||||
end
|
||||
bindings.keys
|
||||
end
|
||||
|
||||
def traverse_custom_types(api, marshaler, bindings, &block)
|
||||
api.api_methods.each do |name, method|
|
||||
expects, returns = method.expects, method.returns
|
||||
expects.each{ |type| traverse_type(marshaler, type, bindings, &block) if type.custom? } if expects
|
||||
returns.each{ |type| traverse_type(marshaler, type, bindings, &block) if type.custom? } if returns
|
||||
end
|
||||
end
|
||||
|
||||
def traverse_type(marshaler, type, bindings, &block)
|
||||
binding = marshaler.register_type(type)
|
||||
return if bindings.has_key?(binding)
|
||||
bindings[binding] = nil
|
||||
yield binding
|
||||
if type.array?
|
||||
yield marshaler.register_type(type.element_type)
|
||||
type = type.element_type
|
||||
end
|
||||
type.each_member{ |name, type| traverse_type(marshaler, type, bindings, &block) } if type.structured?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
module ActionWebService # :nodoc:
|
||||
module Invocation # :nodoc:
|
||||
class InvocationError < ActionWebService::ActionWebServiceError # :nodoc:
|
||||
end
|
||||
|
||||
def self.included(base) # :nodoc:
|
||||
base.extend(ClassMethods)
|
||||
base.send(:include, ActionWebService::Invocation::InstanceMethods)
|
||||
end
|
||||
|
||||
# Invocation interceptors provide a means to execute custom code before
|
||||
# and after method invocations on ActionWebService::Base objects.
|
||||
#
|
||||
# When running in _Direct_ dispatching mode, ActionController filters
|
||||
# should be used for this functionality instead.
|
||||
#
|
||||
# The semantics of invocation interceptors are the same as ActionController
|
||||
# filters, and accept the same parameters and options.
|
||||
#
|
||||
# A _before_ interceptor can also cancel execution by returning +false+,
|
||||
# or returning a <tt>[false, "cancel reason"]</tt> array if it wishes to supply
|
||||
# a reason for canceling the request.
|
||||
#
|
||||
# === Example
|
||||
#
|
||||
# class CustomService < ActionWebService::Base
|
||||
# before_invocation :intercept_add, :only => [:add]
|
||||
#
|
||||
# def add(a, b)
|
||||
# a + b
|
||||
# end
|
||||
#
|
||||
# private
|
||||
# def intercept_add
|
||||
# return [false, "permission denied"] # cancel it
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# Options:
|
||||
# [<tt>:except</tt>] A list of methods for which the interceptor will NOT be called
|
||||
# [<tt>:only</tt>] A list of methods for which the interceptor WILL be called
|
||||
module ClassMethods
|
||||
# Appends the given +interceptors+ to be called
|
||||
# _before_ method invocation.
|
||||
def append_before_invocation(*interceptors, &block)
|
||||
conditions = extract_conditions!(interceptors)
|
||||
interceptors << block if block_given?
|
||||
add_interception_conditions(interceptors, conditions)
|
||||
append_interceptors_to_chain("before", interceptors)
|
||||
end
|
||||
|
||||
# Prepends the given +interceptors+ to be called
|
||||
# _before_ method invocation.
|
||||
def prepend_before_invocation(*interceptors, &block)
|
||||
conditions = extract_conditions!(interceptors)
|
||||
interceptors << block if block_given?
|
||||
add_interception_conditions(interceptors, conditions)
|
||||
prepend_interceptors_to_chain("before", interceptors)
|
||||
end
|
||||
|
||||
alias :before_invocation :append_before_invocation
|
||||
|
||||
# Appends the given +interceptors+ to be called
|
||||
# _after_ method invocation.
|
||||
def append_after_invocation(*interceptors, &block)
|
||||
conditions = extract_conditions!(interceptors)
|
||||
interceptors << block if block_given?
|
||||
add_interception_conditions(interceptors, conditions)
|
||||
append_interceptors_to_chain("after", interceptors)
|
||||
end
|
||||
|
||||
# Prepends the given +interceptors+ to be called
|
||||
# _after_ method invocation.
|
||||
def prepend_after_invocation(*interceptors, &block)
|
||||
conditions = extract_conditions!(interceptors)
|
||||
interceptors << block if block_given?
|
||||
add_interception_conditions(interceptors, conditions)
|
||||
prepend_interceptors_to_chain("after", interceptors)
|
||||
end
|
||||
|
||||
alias :after_invocation :append_after_invocation
|
||||
|
||||
def before_invocation_interceptors # :nodoc:
|
||||
read_inheritable_attribute("before_invocation_interceptors")
|
||||
end
|
||||
|
||||
def after_invocation_interceptors # :nodoc:
|
||||
read_inheritable_attribute("after_invocation_interceptors")
|
||||
end
|
||||
|
||||
def included_intercepted_methods # :nodoc:
|
||||
read_inheritable_attribute("included_intercepted_methods") || {}
|
||||
end
|
||||
|
||||
def excluded_intercepted_methods # :nodoc:
|
||||
read_inheritable_attribute("excluded_intercepted_methods") || {}
|
||||
end
|
||||
|
||||
private
|
||||
def append_interceptors_to_chain(condition, interceptors)
|
||||
write_inheritable_array("#{condition}_invocation_interceptors", interceptors)
|
||||
end
|
||||
|
||||
def prepend_interceptors_to_chain(condition, interceptors)
|
||||
interceptors = interceptors + read_inheritable_attribute("#{condition}_invocation_interceptors")
|
||||
write_inheritable_attribute("#{condition}_invocation_interceptors", interceptors)
|
||||
end
|
||||
|
||||
def extract_conditions!(interceptors)
|
||||
return nil unless interceptors.last.is_a? Hash
|
||||
interceptors.pop
|
||||
end
|
||||
|
||||
def add_interception_conditions(interceptors, conditions)
|
||||
return unless conditions
|
||||
included, excluded = conditions[:only], conditions[:except]
|
||||
write_inheritable_hash("included_intercepted_methods", condition_hash(interceptors, included)) && return if included
|
||||
write_inheritable_hash("excluded_intercepted_methods", condition_hash(interceptors, excluded)) if excluded
|
||||
end
|
||||
|
||||
def condition_hash(interceptors, *methods)
|
||||
interceptors.inject({}) {|hash, interceptor| hash.merge(interceptor => methods.flatten.map {|method| method.to_s})}
|
||||
end
|
||||
end
|
||||
|
||||
module InstanceMethods # :nodoc:
|
||||
def self.included(base)
|
||||
base.class_eval do
|
||||
alias_method_chain :perform_invocation, :interception
|
||||
end
|
||||
end
|
||||
|
||||
def perform_invocation_with_interception(method_name, params, &block)
|
||||
return if before_invocation(method_name, params, &block) == false
|
||||
return_value = perform_invocation_without_interception(method_name, params)
|
||||
after_invocation(method_name, params, return_value)
|
||||
return_value
|
||||
end
|
||||
|
||||
def perform_invocation(method_name, params)
|
||||
send(method_name, *params)
|
||||
end
|
||||
|
||||
def before_invocation(name, args, &block)
|
||||
call_interceptors(self.class.before_invocation_interceptors, [name, args], &block)
|
||||
end
|
||||
|
||||
def after_invocation(name, args, result)
|
||||
call_interceptors(self.class.after_invocation_interceptors, [name, args, result])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def call_interceptors(interceptors, interceptor_args, &block)
|
||||
if interceptors and not interceptors.empty?
|
||||
interceptors.each do |interceptor|
|
||||
next if method_exempted?(interceptor, interceptor_args[0].to_s)
|
||||
result = case
|
||||
when interceptor.is_a?(Symbol)
|
||||
self.send(interceptor, *interceptor_args)
|
||||
when interceptor_block?(interceptor)
|
||||
interceptor.call(self, *interceptor_args)
|
||||
when interceptor_class?(interceptor)
|
||||
interceptor.intercept(self, *interceptor_args)
|
||||
else
|
||||
raise(
|
||||
InvocationError,
|
||||
"Interceptors need to be either a symbol, proc/method, or a class implementing a static intercept method"
|
||||
)
|
||||
end
|
||||
reason = nil
|
||||
if result.is_a?(Array)
|
||||
reason = result[1] if result[1]
|
||||
result = result[0]
|
||||
end
|
||||
if result == false
|
||||
block.call(reason) if block && reason
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def interceptor_block?(interceptor)
|
||||
interceptor.respond_to?("call") && (interceptor.arity == 3 || interceptor.arity == -1)
|
||||
end
|
||||
|
||||
def interceptor_class?(interceptor)
|
||||
interceptor.respond_to?("intercept")
|
||||
end
|
||||
|
||||
def method_exempted?(interceptor, method_name)
|
||||
case
|
||||
when self.class.included_intercepted_methods[interceptor]
|
||||
!self.class.included_intercepted_methods[interceptor].include?(method_name)
|
||||
when self.class.excluded_intercepted_methods[interceptor]
|
||||
self.class.excluded_intercepted_methods[interceptor].include?(method_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
require 'action_web_service/protocol/abstract'
|
||||
require 'action_web_service/protocol/discovery'
|
||||
require 'action_web_service/protocol/soap_protocol'
|
||||
require 'action_web_service/protocol/xmlrpc_protocol'
|
||||
Vendored
+112
@@ -0,0 +1,112 @@
|
||||
module ActionWebService # :nodoc:
|
||||
module Protocol # :nodoc:
|
||||
class ProtocolError < ActionWebServiceError # :nodoc:
|
||||
end
|
||||
|
||||
class AbstractProtocol # :nodoc:
|
||||
def setup(controller)
|
||||
end
|
||||
|
||||
def decode_action_pack_request(action_pack_request)
|
||||
end
|
||||
|
||||
def encode_action_pack_request(service_name, public_method_name, raw_body, options={})
|
||||
klass = options[:request_class] || SimpleActionPackRequest
|
||||
request = klass.new
|
||||
request.request_parameters['action'] = service_name.to_s
|
||||
request.env['RAW_POST_DATA'] = raw_body
|
||||
request.env['REQUEST_METHOD'] = 'POST'
|
||||
request.env['HTTP_CONTENT_TYPE'] = 'text/xml'
|
||||
request
|
||||
end
|
||||
|
||||
def decode_request(raw_request, service_name, protocol_options={})
|
||||
end
|
||||
|
||||
def encode_request(method_name, params, param_types)
|
||||
end
|
||||
|
||||
def decode_response(raw_response)
|
||||
end
|
||||
|
||||
def encode_response(method_name, return_value, return_type, protocol_options={})
|
||||
end
|
||||
|
||||
def protocol_client(api, protocol_name, endpoint_uri, options)
|
||||
end
|
||||
|
||||
def register_api(api)
|
||||
end
|
||||
end
|
||||
|
||||
class Request # :nodoc:
|
||||
attr :protocol
|
||||
attr_accessor :method_name
|
||||
attr_accessor :method_params
|
||||
attr :service_name
|
||||
attr_accessor :api
|
||||
attr_accessor :api_method
|
||||
attr :protocol_options
|
||||
|
||||
def initialize(protocol, method_name, method_params, service_name, api=nil, api_method=nil, protocol_options=nil)
|
||||
@protocol = protocol
|
||||
@method_name = method_name
|
||||
@method_params = method_params
|
||||
@service_name = service_name
|
||||
@api = api
|
||||
@api_method = api_method
|
||||
@protocol_options = protocol_options || {}
|
||||
end
|
||||
end
|
||||
|
||||
class Response # :nodoc:
|
||||
attr :body
|
||||
attr :content_type
|
||||
attr :return_value
|
||||
|
||||
def initialize(body, content_type, return_value)
|
||||
@body = body
|
||||
@content_type = content_type
|
||||
@return_value = return_value
|
||||
end
|
||||
end
|
||||
|
||||
class SimpleActionPackRequest < ActionController::AbstractRequest # :nodoc:
|
||||
def initialize
|
||||
@env = {}
|
||||
@qparams = {}
|
||||
@rparams = {}
|
||||
@cookies = {}
|
||||
reset_session
|
||||
end
|
||||
|
||||
def query_parameters
|
||||
@qparams
|
||||
end
|
||||
|
||||
def request_parameters
|
||||
@rparams
|
||||
end
|
||||
|
||||
def env
|
||||
@env
|
||||
end
|
||||
|
||||
def host
|
||||
''
|
||||
end
|
||||
|
||||
def cookies
|
||||
@cookies
|
||||
end
|
||||
|
||||
def session
|
||||
@session
|
||||
end
|
||||
|
||||
def reset_session
|
||||
@session = {}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Vendored
+37
@@ -0,0 +1,37 @@
|
||||
module ActionWebService # :nodoc:
|
||||
module Protocol # :nodoc:
|
||||
module Discovery # :nodoc:
|
||||
def self.included(base)
|
||||
base.extend(ClassMethods)
|
||||
base.send(:include, ActionWebService::Protocol::Discovery::InstanceMethods)
|
||||
end
|
||||
|
||||
module ClassMethods # :nodoc:
|
||||
def register_protocol(klass)
|
||||
write_inheritable_array("web_service_protocols", [klass])
|
||||
end
|
||||
end
|
||||
|
||||
module InstanceMethods # :nodoc:
|
||||
private
|
||||
def discover_web_service_request(action_pack_request)
|
||||
(self.class.read_inheritable_attribute("web_service_protocols") || []).each do |protocol|
|
||||
protocol = protocol.create(self)
|
||||
request = protocol.decode_action_pack_request(action_pack_request)
|
||||
return request unless request.nil?
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
def create_web_service_client(api, protocol_name, endpoint_uri, options)
|
||||
(self.class.read_inheritable_attribute("web_service_protocols") || []).each do |protocol|
|
||||
protocol = protocol.create(self)
|
||||
client = protocol.protocol_client(api, protocol_name, endpoint_uri, options)
|
||||
return client unless client.nil?
|
||||
end
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Vendored
+176
@@ -0,0 +1,176 @@
|
||||
require 'action_web_service/protocol/soap_protocol/marshaler'
|
||||
require 'soap/streamHandler'
|
||||
require 'action_web_service/client/soap_client'
|
||||
|
||||
module ActionWebService # :nodoc:
|
||||
module API # :nodoc:
|
||||
class Base # :nodoc:
|
||||
def self.soap_client(endpoint_uri, options={})
|
||||
ActionWebService::Client::Soap.new self, endpoint_uri, options
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module Protocol # :nodoc:
|
||||
module Soap # :nodoc:
|
||||
def self.included(base)
|
||||
base.register_protocol(SoapProtocol)
|
||||
base.class_inheritable_option(:wsdl_service_name)
|
||||
base.class_inheritable_option(:wsdl_namespace)
|
||||
end
|
||||
|
||||
class SoapProtocol < AbstractProtocol # :nodoc:
|
||||
AWSEncoding = 'UTF-8'
|
||||
XSDEncoding = 'UTF8'
|
||||
|
||||
attr :marshaler
|
||||
|
||||
def initialize(namespace=nil)
|
||||
namespace ||= 'urn:ActionWebService'
|
||||
@marshaler = SoapMarshaler.new namespace
|
||||
end
|
||||
|
||||
def self.create(controller)
|
||||
SoapProtocol.new(controller.wsdl_namespace)
|
||||
end
|
||||
|
||||
def decode_action_pack_request(action_pack_request)
|
||||
return nil unless soap_action = has_valid_soap_action?(action_pack_request)
|
||||
service_name = action_pack_request.parameters['action']
|
||||
input_encoding = parse_charset(action_pack_request.env['HTTP_CONTENT_TYPE'])
|
||||
protocol_options = {
|
||||
:soap_action => soap_action,
|
||||
:charset => input_encoding
|
||||
}
|
||||
decode_request(action_pack_request.raw_post, service_name, protocol_options)
|
||||
end
|
||||
|
||||
def encode_action_pack_request(service_name, public_method_name, raw_body, options={})
|
||||
request = super
|
||||
request.env['HTTP_SOAPACTION'] = '/soap/%s/%s' % [service_name, public_method_name]
|
||||
request
|
||||
end
|
||||
|
||||
def decode_request(raw_request, service_name, protocol_options={})
|
||||
envelope = SOAP::Processor.unmarshal(raw_request, :charset => protocol_options[:charset])
|
||||
unless envelope
|
||||
raise ProtocolError, "Failed to parse SOAP request message"
|
||||
end
|
||||
request = envelope.body.request
|
||||
method_name = request.elename.name
|
||||
params = request.collect{ |k, v| marshaler.soap_to_ruby(request[k]) }
|
||||
Request.new(self, method_name, params, service_name, nil, nil, protocol_options)
|
||||
end
|
||||
|
||||
def encode_request(method_name, params, param_types)
|
||||
param_types.each{ |type| marshaler.register_type(type) } if param_types
|
||||
qname = XSD::QName.new(marshaler.namespace, method_name)
|
||||
param_def = []
|
||||
if param_types
|
||||
params = param_types.zip(params).map do |type, param|
|
||||
param_def << ['in', type.name, marshaler.lookup_type(type).mapping]
|
||||
[type.name, marshaler.ruby_to_soap(param)]
|
||||
end
|
||||
else
|
||||
params = []
|
||||
end
|
||||
request = SOAP::RPC::SOAPMethodRequest.new(qname, param_def)
|
||||
request.set_param(params)
|
||||
envelope = create_soap_envelope(request)
|
||||
SOAP::Processor.marshal(envelope)
|
||||
end
|
||||
|
||||
def decode_response(raw_response)
|
||||
envelope = SOAP::Processor.unmarshal(raw_response)
|
||||
unless envelope
|
||||
raise ProtocolError, "Failed to parse SOAP request message"
|
||||
end
|
||||
method_name = envelope.body.request.elename.name
|
||||
return_value = envelope.body.response
|
||||
return_value = marshaler.soap_to_ruby(return_value) unless return_value.nil?
|
||||
[method_name, return_value]
|
||||
end
|
||||
|
||||
def encode_response(method_name, return_value, return_type, protocol_options={})
|
||||
if return_type
|
||||
return_binding = marshaler.register_type(return_type)
|
||||
marshaler.annotate_arrays(return_binding, return_value)
|
||||
end
|
||||
qname = XSD::QName.new(marshaler.namespace, method_name)
|
||||
if return_value.nil?
|
||||
response = SOAP::RPC::SOAPMethodResponse.new(qname, nil)
|
||||
else
|
||||
if return_value.is_a?(Exception)
|
||||
detail = SOAP::Mapping::SOAPException.new(return_value)
|
||||
response = SOAP::SOAPFault.new(
|
||||
SOAP::SOAPQName.new('%s:%s' % [SOAP::SOAPNamespaceTag, 'Server']),
|
||||
SOAP::SOAPString.new(return_value.to_s),
|
||||
SOAP::SOAPString.new(self.class.name),
|
||||
marshaler.ruby_to_soap(detail))
|
||||
else
|
||||
if return_type
|
||||
param_def = [['retval', 'return', marshaler.lookup_type(return_type).mapping]]
|
||||
response = SOAP::RPC::SOAPMethodResponse.new(qname, param_def)
|
||||
response.retval = marshaler.ruby_to_soap(return_value)
|
||||
else
|
||||
response = SOAP::RPC::SOAPMethodResponse.new(qname, nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
envelope = create_soap_envelope(response)
|
||||
|
||||
# FIXME: This is not thread-safe, but StringFactory_ in SOAP4R only
|
||||
# reads target encoding from the XSD::Charset.encoding variable.
|
||||
# This is required to ensure $KCODE strings are converted
|
||||
# correctly to UTF-8 for any values of $KCODE.
|
||||
previous_encoding = XSD::Charset.encoding
|
||||
XSD::Charset.encoding = XSDEncoding
|
||||
response_body = SOAP::Processor.marshal(envelope, :charset => AWSEncoding)
|
||||
XSD::Charset.encoding = previous_encoding
|
||||
|
||||
Response.new(response_body, "text/xml; charset=#{AWSEncoding}", return_value)
|
||||
end
|
||||
|
||||
def protocol_client(api, protocol_name, endpoint_uri, options={})
|
||||
return nil unless protocol_name == :soap
|
||||
ActionWebService::Client::Soap.new(api, endpoint_uri, options)
|
||||
end
|
||||
|
||||
def register_api(api)
|
||||
api.api_methods.each do |name, method|
|
||||
method.expects.each{ |type| marshaler.register_type(type) } if method.expects
|
||||
method.returns.each{ |type| marshaler.register_type(type) } if method.returns
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def has_valid_soap_action?(request)
|
||||
return nil unless request.method == :post
|
||||
soap_action = request.env['HTTP_SOAPACTION']
|
||||
return nil unless soap_action
|
||||
soap_action = soap_action.dup
|
||||
soap_action.gsub!(/^"/, '')
|
||||
soap_action.gsub!(/"$/, '')
|
||||
soap_action.strip!
|
||||
return nil if soap_action.empty?
|
||||
soap_action
|
||||
end
|
||||
|
||||
def create_soap_envelope(body)
|
||||
header = SOAP::SOAPHeader.new
|
||||
body = SOAP::SOAPBody.new(body)
|
||||
SOAP::SOAPEnvelope.new(header, body)
|
||||
end
|
||||
|
||||
def parse_charset(content_type)
|
||||
return AWSEncoding if content_type.nil?
|
||||
if /^text\/xml(?:\s*;\s*charset=([^"]+|"[^"]+"))$/i =~ content_type
|
||||
$1
|
||||
else
|
||||
AWSEncoding
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
require 'soap/mapping'
|
||||
|
||||
module ActionWebService
|
||||
module Protocol
|
||||
module Soap
|
||||
# Workaround for SOAP4R return values changing
|
||||
class Registry < SOAP::Mapping::Registry
|
||||
if SOAP::Version >= "1.5.4"
|
||||
def find_mapped_soap_class(obj_class)
|
||||
return @map.instance_eval { @obj2soap[obj_class][0] }
|
||||
end
|
||||
|
||||
def find_mapped_obj_class(soap_class)
|
||||
return @map.instance_eval { @soap2obj[soap_class][0] }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class SoapMarshaler
|
||||
attr :namespace
|
||||
attr :registry
|
||||
|
||||
def initialize(namespace=nil)
|
||||
@namespace = namespace || 'urn:ActionWebService'
|
||||
@registry = Registry.new
|
||||
@type2binding = {}
|
||||
register_static_factories
|
||||
end
|
||||
|
||||
def soap_to_ruby(obj)
|
||||
SOAP::Mapping.soap2obj(obj, @registry)
|
||||
end
|
||||
|
||||
def ruby_to_soap(obj)
|
||||
soap = SOAP::Mapping.obj2soap(obj, @registry)
|
||||
soap.elename = XSD::QName.new if SOAP::Version >= "1.5.5" && soap.elename == XSD::QName::EMPTY
|
||||
soap
|
||||
end
|
||||
|
||||
def register_type(type)
|
||||
return @type2binding[type] if @type2binding.has_key?(type)
|
||||
|
||||
if type.array?
|
||||
array_mapping = @registry.find_mapped_soap_class(Array)
|
||||
qname = XSD::QName.new(@namespace, soap_type_name(type.element_type.type_class.name) + 'Array')
|
||||
element_type_binding = register_type(type.element_type)
|
||||
@type2binding[type] = SoapBinding.new(self, qname, type, array_mapping, element_type_binding)
|
||||
elsif (mapping = @registry.find_mapped_soap_class(type.type_class) rescue nil)
|
||||
qname = mapping[2] ? mapping[2][:type] : nil
|
||||
qname ||= soap_base_type_name(mapping[0])
|
||||
@type2binding[type] = SoapBinding.new(self, qname, type, mapping)
|
||||
else
|
||||
qname = XSD::QName.new(@namespace, soap_type_name(type.type_class.name))
|
||||
@registry.add(type.type_class,
|
||||
SOAP::SOAPStruct,
|
||||
typed_struct_factory(type.type_class),
|
||||
{ :type => qname })
|
||||
mapping = @registry.find_mapped_soap_class(type.type_class)
|
||||
@type2binding[type] = SoapBinding.new(self, qname, type, mapping)
|
||||
end
|
||||
|
||||
if type.structured?
|
||||
type.each_member do |m_name, m_type|
|
||||
register_type(m_type)
|
||||
end
|
||||
end
|
||||
|
||||
@type2binding[type]
|
||||
end
|
||||
alias :lookup_type :register_type
|
||||
|
||||
def annotate_arrays(binding, value)
|
||||
if value.nil?
|
||||
return
|
||||
elsif binding.type.array?
|
||||
mark_typed_array(value, binding.element_binding.qname)
|
||||
if binding.element_binding.type.custom?
|
||||
value.each do |element|
|
||||
annotate_arrays(binding.element_binding, element)
|
||||
end
|
||||
end
|
||||
elsif binding.type.structured?
|
||||
binding.type.each_member do |name, type|
|
||||
member_binding = register_type(type)
|
||||
member_value = value.respond_to?('[]') ? value[name] : value.send(name)
|
||||
annotate_arrays(member_binding, member_value) if type.custom?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def typed_struct_factory(type_class)
|
||||
if Object.const_defined?('ActiveRecord')
|
||||
if type_class.ancestors.include?(ActiveRecord::Base)
|
||||
qname = XSD::QName.new(@namespace, soap_type_name(type_class.name))
|
||||
type_class.instance_variable_set('@qname', qname)
|
||||
return SoapActiveRecordStructFactory.new
|
||||
end
|
||||
end
|
||||
SOAP::Mapping::Registry::TypedStructFactory
|
||||
end
|
||||
|
||||
def mark_typed_array(array, qname)
|
||||
(class << array; self; end).class_eval do
|
||||
define_method(:arytype) do
|
||||
qname
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def soap_base_type_name(type)
|
||||
xsd_type = type.ancestors.find{ |c| c.const_defined? 'Type' }
|
||||
xsd_type ? xsd_type.const_get('Type') : XSD::XSDAnySimpleType::Type
|
||||
end
|
||||
|
||||
def soap_type_name(type_name)
|
||||
type_name.gsub(/::/, '..')
|
||||
end
|
||||
|
||||
def register_static_factories
|
||||
@registry.add(ActionWebService::Base64, SOAP::SOAPBase64, SoapBase64Factory.new, nil)
|
||||
mapping = @registry.find_mapped_soap_class(ActionWebService::Base64)
|
||||
@type2binding[ActionWebService::Base64] =
|
||||
SoapBinding.new(self, SOAP::SOAPBase64::Type, ActionWebService::Base64, mapping)
|
||||
@registry.add(Array, SOAP::SOAPArray, SoapTypedArrayFactory.new, nil)
|
||||
@registry.add(::BigDecimal, SOAP::SOAPDouble, SOAP::Mapping::Registry::BasetypeFactory, {:derived_class => true})
|
||||
end
|
||||
end
|
||||
|
||||
class SoapBinding
|
||||
attr :qname
|
||||
attr :type
|
||||
attr :mapping
|
||||
attr :element_binding
|
||||
|
||||
def initialize(marshaler, qname, type, mapping, element_binding=nil)
|
||||
@marshaler = marshaler
|
||||
@qname = qname
|
||||
@type = type
|
||||
@mapping = mapping
|
||||
@element_binding = element_binding
|
||||
end
|
||||
|
||||
def type_name
|
||||
@type.custom? ? @qname.name : nil
|
||||
end
|
||||
|
||||
def qualified_type_name(ns=nil)
|
||||
if @type.custom?
|
||||
"#{ns ? ns : @qname.namespace}:#{@qname.name}"
|
||||
else
|
||||
ns = XSD::NS.new
|
||||
ns.assign(XSD::Namespace, SOAP::XSDNamespaceTag)
|
||||
ns.assign(SOAP::EncodingNamespace, "soapenc")
|
||||
xsd_klass = mapping[0].ancestors.find{|c| c.const_defined?('Type')}
|
||||
return ns.name(XSD::AnyTypeName) unless xsd_klass
|
||||
ns.name(xsd_klass.const_get('Type'))
|
||||
end
|
||||
end
|
||||
|
||||
def eql?(other)
|
||||
@qname == other.qname
|
||||
end
|
||||
alias :== :eql?
|
||||
|
||||
def hash
|
||||
@qname.hash
|
||||
end
|
||||
end
|
||||
|
||||
class SoapActiveRecordStructFactory < SOAP::Mapping::Factory
|
||||
def obj2soap(soap_class, obj, info, map)
|
||||
unless obj.is_a?(ActiveRecord::Base)
|
||||
return nil
|
||||
end
|
||||
soap_obj = soap_class.new(obj.class.instance_variable_get('@qname'))
|
||||
obj.class.columns.each do |column|
|
||||
key = column.name.to_s
|
||||
value = obj.send(key)
|
||||
soap_obj[key] = SOAP::Mapping._obj2soap(value, map)
|
||||
end
|
||||
soap_obj
|
||||
end
|
||||
|
||||
def soap2obj(obj_class, node, info, map)
|
||||
unless node.type == obj_class.instance_variable_get('@qname')
|
||||
return false
|
||||
end
|
||||
obj = obj_class.new
|
||||
node.each do |key, value|
|
||||
obj[key] = value.data
|
||||
end
|
||||
obj.instance_variable_set('@new_record', false)
|
||||
return true, obj
|
||||
end
|
||||
end
|
||||
|
||||
class SoapTypedArrayFactory < SOAP::Mapping::Factory
|
||||
def obj2soap(soap_class, obj, info, map)
|
||||
unless obj.respond_to?(:arytype)
|
||||
return nil
|
||||
end
|
||||
soap_obj = soap_class.new(SOAP::ValueArrayName, 1, obj.arytype)
|
||||
mark_marshalled_obj(obj, soap_obj)
|
||||
obj.each do |item|
|
||||
child = SOAP::Mapping._obj2soap(item, map)
|
||||
soap_obj.add(child)
|
||||
end
|
||||
soap_obj
|
||||
end
|
||||
|
||||
def soap2obj(obj_class, node, info, map)
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
class SoapBase64Factory < SOAP::Mapping::Factory
|
||||
def obj2soap(soap_class, obj, info, map)
|
||||
unless obj.is_a?(ActionWebService::Base64)
|
||||
return nil
|
||||
end
|
||||
return soap_class.new(obj)
|
||||
end
|
||||
|
||||
def soap2obj(obj_class, node, info, map)
|
||||
unless node.type == SOAP::SOAPBase64::Type
|
||||
return false
|
||||
end
|
||||
return true, obj_class.new(node.string)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
Vendored
+122
@@ -0,0 +1,122 @@
|
||||
require 'xmlrpc/marshal'
|
||||
require 'action_web_service/client/xmlrpc_client'
|
||||
|
||||
module XMLRPC # :nodoc:
|
||||
class FaultException # :nodoc:
|
||||
alias :message :faultString
|
||||
end
|
||||
|
||||
class Create
|
||||
def wrong_type(value)
|
||||
if BigDecimal === value
|
||||
[true, value.to_f]
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module ActionWebService # :nodoc:
|
||||
module API # :nodoc:
|
||||
class Base # :nodoc:
|
||||
def self.xmlrpc_client(endpoint_uri, options={})
|
||||
ActionWebService::Client::XmlRpc.new self, endpoint_uri, options
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module Protocol # :nodoc:
|
||||
module XmlRpc # :nodoc:
|
||||
def self.included(base)
|
||||
base.register_protocol(XmlRpcProtocol)
|
||||
end
|
||||
|
||||
class XmlRpcProtocol < AbstractProtocol # :nodoc:
|
||||
def self.create(controller)
|
||||
XmlRpcProtocol.new
|
||||
end
|
||||
|
||||
def decode_action_pack_request(action_pack_request)
|
||||
service_name = action_pack_request.parameters['action']
|
||||
decode_request(action_pack_request.raw_post, service_name)
|
||||
end
|
||||
|
||||
def decode_request(raw_request, service_name)
|
||||
method_name, params = XMLRPC::Marshal.load_call(raw_request)
|
||||
Request.new(self, method_name, params, service_name)
|
||||
rescue
|
||||
return nil
|
||||
end
|
||||
|
||||
def encode_request(method_name, params, param_types)
|
||||
if param_types
|
||||
params = params.dup
|
||||
param_types.each_with_index{ |type, i| params[i] = value_to_xmlrpc_wire_format(params[i], type) }
|
||||
end
|
||||
XMLRPC::Marshal.dump_call(method_name, *params)
|
||||
end
|
||||
|
||||
def decode_response(raw_response)
|
||||
[nil, XMLRPC::Marshal.load_response(raw_response)]
|
||||
end
|
||||
|
||||
def encode_response(method_name, return_value, return_type, protocol_options={})
|
||||
if return_value && return_type
|
||||
return_value = value_to_xmlrpc_wire_format(return_value, return_type)
|
||||
end
|
||||
return_value = false if return_value.nil?
|
||||
raw_response = XMLRPC::Marshal.dump_response(return_value)
|
||||
Response.new(raw_response, 'text/xml', return_value)
|
||||
end
|
||||
|
||||
def encode_multicall_response(responses, protocol_options={})
|
||||
result = responses.map do |return_value, return_type|
|
||||
if return_value && return_type
|
||||
return_value = value_to_xmlrpc_wire_format(return_value, return_type)
|
||||
return_value = [return_value] unless return_value.nil?
|
||||
end
|
||||
return_value = false if return_value.nil?
|
||||
return_value
|
||||
end
|
||||
raw_response = XMLRPC::Marshal.dump_response(result)
|
||||
Response.new(raw_response, 'text/xml', result)
|
||||
end
|
||||
|
||||
def protocol_client(api, protocol_name, endpoint_uri, options={})
|
||||
return nil unless protocol_name == :xmlrpc
|
||||
ActionWebService::Client::XmlRpc.new(api, endpoint_uri, options)
|
||||
end
|
||||
|
||||
def value_to_xmlrpc_wire_format(value, value_type)
|
||||
if value_type.array?
|
||||
value.map{ |val| value_to_xmlrpc_wire_format(val, value_type.element_type) }
|
||||
else
|
||||
if value.is_a?(ActionWebService::Struct)
|
||||
struct = {}
|
||||
value.class.members.each do |name, type|
|
||||
member_value = value[name]
|
||||
next if member_value.nil?
|
||||
struct[name.to_s] = value_to_xmlrpc_wire_format(member_value, type)
|
||||
end
|
||||
struct
|
||||
elsif value.is_a?(ActiveRecord::Base)
|
||||
struct = {}
|
||||
value.attributes.each do |key, member_value|
|
||||
next if member_value.nil?
|
||||
struct[key.to_s] = member_value
|
||||
end
|
||||
struct
|
||||
elsif value.is_a?(ActionWebService::Base64)
|
||||
XMLRPC::Base64.new(value)
|
||||
elsif value.is_a?(Exception) && !value.is_a?(XMLRPC::FaultException)
|
||||
XMLRPC::FaultException.new(2, value.message)
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
+283
@@ -0,0 +1,283 @@
|
||||
require 'benchmark'
|
||||
require 'pathname'
|
||||
|
||||
module ActionWebService
|
||||
module Scaffolding # :nodoc:
|
||||
class ScaffoldingError < ActionWebServiceError # :nodoc:
|
||||
end
|
||||
|
||||
def self.included(base)
|
||||
base.extend(ClassMethods)
|
||||
end
|
||||
|
||||
# Web service invocation scaffolding provides a way to quickly invoke web service methods in a controller. The
|
||||
# generated scaffold actions have default views to let you enter the method parameters and view the
|
||||
# results.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# class ApiController < ActionController
|
||||
# web_service_scaffold :invoke
|
||||
# end
|
||||
#
|
||||
# This example generates an +invoke+ action in the +ApiController+ that you can navigate to from
|
||||
# your browser, select the API method, enter its parameters, and perform the invocation.
|
||||
#
|
||||
# If you want to customize the default views, create the following views in "app/views":
|
||||
#
|
||||
# * <tt>action_name/methods.erb</tt>
|
||||
# * <tt>action_name/parameters.erb</tt>
|
||||
# * <tt>action_name/result.erb</tt>
|
||||
# * <tt>action_name/layout.erb</tt>
|
||||
#
|
||||
# Where <tt>action_name</tt> is the name of the action you gave to ClassMethods#web_service_scaffold.
|
||||
#
|
||||
# You can use the default views in <tt>RAILS_DIR/lib/action_web_service/templates/scaffolds</tt> as
|
||||
# a guide.
|
||||
module ClassMethods
|
||||
# Generates web service invocation scaffolding for the current controller. The given action name
|
||||
# can then be used as the entry point for invoking API methods from a web browser.
|
||||
def web_service_scaffold(action_name)
|
||||
add_template_helper(Helpers)
|
||||
module_eval <<-"end_eval", __FILE__, __LINE__ + 1
|
||||
def #{action_name}
|
||||
if request.method == :get
|
||||
setup_invocation_assigns
|
||||
render_invocation_scaffold 'methods'
|
||||
end
|
||||
end
|
||||
|
||||
def #{action_name}_method_params
|
||||
if request.method == :get
|
||||
setup_invocation_assigns
|
||||
render_invocation_scaffold 'parameters'
|
||||
end
|
||||
end
|
||||
|
||||
def #{action_name}_submit
|
||||
if request.method == :post
|
||||
setup_invocation_assigns
|
||||
protocol_name = params['protocol'] ? params['protocol'].to_sym : :soap
|
||||
case protocol_name
|
||||
when :soap
|
||||
@protocol = Protocol::Soap::SoapProtocol.create(self)
|
||||
when :xmlrpc
|
||||
@protocol = Protocol::XmlRpc::XmlRpcProtocol.create(self)
|
||||
end
|
||||
bm = Benchmark.measure do
|
||||
@protocol.register_api(@scaffold_service.api)
|
||||
post_params = params['method_params'] ? params['method_params'].dup : nil
|
||||
params = []
|
||||
@scaffold_method.expects.each_with_index do |spec, i|
|
||||
params << post_params[i.to_s]
|
||||
end if @scaffold_method.expects
|
||||
params = @scaffold_method.cast_expects(params)
|
||||
method_name = public_method_name(@scaffold_service.name, @scaffold_method.public_name)
|
||||
@method_request_xml = @protocol.encode_request(method_name, params, @scaffold_method.expects)
|
||||
new_request = @protocol.encode_action_pack_request(@scaffold_service.name, @scaffold_method.public_name, @method_request_xml)
|
||||
prepare_request(new_request, @scaffold_service.name, @scaffold_method.public_name)
|
||||
self.request = new_request
|
||||
if @scaffold_container.dispatching_mode != :direct
|
||||
request.parameters['action'] = @scaffold_service.name
|
||||
end
|
||||
dispatch_web_service_request
|
||||
@method_response_xml = response.body
|
||||
method_name, obj = @protocol.decode_response(@method_response_xml)
|
||||
return if handle_invocation_exception(obj)
|
||||
@method_return_value = @scaffold_method.cast_returns(obj)
|
||||
end
|
||||
@method_elapsed = bm.real
|
||||
add_instance_variables_to_assigns
|
||||
reset_invocation_response
|
||||
render_invocation_scaffold 'result'
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def setup_invocation_assigns
|
||||
@scaffold_class = self.class
|
||||
@scaffold_action_name = "#{action_name}"
|
||||
@scaffold_container = WebServiceModel::Container.new(self)
|
||||
if params['service'] && params['method']
|
||||
@scaffold_service = @scaffold_container.services.find{ |x| x.name == params['service'] }
|
||||
@scaffold_method = @scaffold_service.api_methods[params['method']]
|
||||
end
|
||||
add_instance_variables_to_assigns
|
||||
end
|
||||
|
||||
def render_invocation_scaffold(action)
|
||||
customized_template = "\#{self.class.controller_path}/#{action_name}/\#{action}"
|
||||
default_template = scaffold_path(action)
|
||||
if template_exists?(customized_template)
|
||||
content = @template.render :file => customized_template
|
||||
else
|
||||
content = @template.render :file => default_template
|
||||
end
|
||||
@template.instance_variable_set("@content_for_layout", content)
|
||||
if self.active_layout.nil?
|
||||
render :file => scaffold_path("layout")
|
||||
else
|
||||
render :file => self.active_layout
|
||||
end
|
||||
end
|
||||
|
||||
def scaffold_path(template_name)
|
||||
File.dirname(__FILE__) + "/templates/scaffolds/" + template_name + ".erb"
|
||||
end
|
||||
|
||||
def reset_invocation_response
|
||||
erase_render_results
|
||||
response.headers = ::ActionController::AbstractResponse::DEFAULT_HEADERS.merge("cookie" => [])
|
||||
end
|
||||
|
||||
def public_method_name(service_name, method_name)
|
||||
if web_service_dispatching_mode == :layered && @protocol.is_a?(ActionWebService::Protocol::XmlRpc::XmlRpcProtocol)
|
||||
service_name + '.' + method_name
|
||||
else
|
||||
method_name
|
||||
end
|
||||
end
|
||||
|
||||
def prepare_request(new_request, service_name, method_name)
|
||||
new_request.parameters.update(request.parameters)
|
||||
request.env.each{ |k, v| new_request.env[k] = v unless new_request.env.has_key?(k) }
|
||||
if web_service_dispatching_mode == :layered && @protocol.is_a?(ActionWebService::Protocol::Soap::SoapProtocol)
|
||||
new_request.env['HTTP_SOAPACTION'] = "/\#{controller_name()}/\#{service_name}/\#{method_name}"
|
||||
end
|
||||
end
|
||||
|
||||
def handle_invocation_exception(obj)
|
||||
exception = nil
|
||||
if obj.respond_to?(:detail) && obj.detail.respond_to?(:cause) && obj.detail.cause.is_a?(Exception)
|
||||
exception = obj.detail.cause
|
||||
elsif obj.is_a?(XMLRPC::FaultException)
|
||||
exception = obj
|
||||
end
|
||||
return unless exception
|
||||
reset_invocation_response
|
||||
rescue_action(exception)
|
||||
true
|
||||
end
|
||||
end_eval
|
||||
end
|
||||
end
|
||||
|
||||
module Helpers # :nodoc:
|
||||
def method_parameter_input_fields(method, type, field_name_base, idx, was_structured=false)
|
||||
if type.array?
|
||||
return content_tag('em', "Typed array input fields not supported yet (#{type.name})")
|
||||
end
|
||||
if type.structured?
|
||||
return content_tag('em', "Nested structural types not supported yet (#{type.name})") if was_structured
|
||||
parameters = ""
|
||||
type.each_member do |member_name, member_type|
|
||||
label = method_parameter_label(member_name, member_type)
|
||||
nested_content = method_parameter_input_fields(
|
||||
method,
|
||||
member_type,
|
||||
"#{field_name_base}[#{idx}][#{member_name}]",
|
||||
idx,
|
||||
true)
|
||||
if member_type.custom?
|
||||
parameters << content_tag('li', label)
|
||||
parameters << content_tag('ul', nested_content)
|
||||
else
|
||||
parameters << content_tag('li', label + ' ' + nested_content)
|
||||
end
|
||||
end
|
||||
content_tag('ul', parameters)
|
||||
else
|
||||
# If the data source was structured previously we already have the index set
|
||||
field_name_base = "#{field_name_base}[#{idx}]" unless was_structured
|
||||
|
||||
case type.type
|
||||
when :int
|
||||
text_field_tag "#{field_name_base}"
|
||||
when :string
|
||||
text_field_tag "#{field_name_base}"
|
||||
when :base64
|
||||
text_area_tag "#{field_name_base}", nil, :size => "40x5"
|
||||
when :bool
|
||||
radio_button_tag("#{field_name_base}", "true") + " True" +
|
||||
radio_button_tag("#{field_name_base}", "false") + "False"
|
||||
when :float
|
||||
text_field_tag "#{field_name_base}"
|
||||
when :time, :datetime
|
||||
time = Time.now
|
||||
i = 0
|
||||
%w|year month day hour minute second|.map do |name|
|
||||
i += 1
|
||||
send("select_#{name}", time, :prefix => "#{field_name_base}[#{i}]", :discard_type => true)
|
||||
end.join
|
||||
when :date
|
||||
date = Date.today
|
||||
i = 0
|
||||
%w|year month day|.map do |name|
|
||||
i += 1
|
||||
send("select_#{name}", date, :prefix => "#{field_name_base}[#{i}]", :discard_type => true)
|
||||
end.join
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def method_parameter_label(name, type)
|
||||
name.to_s.capitalize + ' (' + type.human_name(false) + ')'
|
||||
end
|
||||
|
||||
def service_method_list(service)
|
||||
action = @scaffold_action_name + '_method_params'
|
||||
methods = service.api_methods_full.map do |desc, name|
|
||||
content_tag("li", link_to(desc, :action => action, :service => service.name, :method => name))
|
||||
end
|
||||
content_tag("ul", methods.join("\n"))
|
||||
end
|
||||
end
|
||||
|
||||
module WebServiceModel # :nodoc:
|
||||
class Container # :nodoc:
|
||||
attr :services
|
||||
attr :dispatching_mode
|
||||
|
||||
def initialize(real_container)
|
||||
@real_container = real_container
|
||||
@dispatching_mode = @real_container.class.web_service_dispatching_mode
|
||||
@services = []
|
||||
if @dispatching_mode == :direct
|
||||
@services << Service.new(@real_container.controller_name, @real_container)
|
||||
else
|
||||
@real_container.class.web_services.each do |name, obj|
|
||||
@services << Service.new(name, @real_container.instance_eval{ web_service_object(name) })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Service # :nodoc:
|
||||
attr :name
|
||||
attr :object
|
||||
attr :api
|
||||
attr :api_methods
|
||||
attr :api_methods_full
|
||||
|
||||
def initialize(name, real_service)
|
||||
@name = name.to_s
|
||||
@object = real_service
|
||||
@api = @object.class.web_service_api
|
||||
if @api.nil?
|
||||
raise ScaffoldingError, "No web service API attached to #{object.class}"
|
||||
end
|
||||
@api_methods = {}
|
||||
@api_methods_full = []
|
||||
@api.api_methods.each do |name, method|
|
||||
@api_methods[method.public_name.to_s] = method
|
||||
@api_methods_full << [method.to_s, method.public_name.to_s]
|
||||
end
|
||||
end
|
||||
|
||||
def to_s
|
||||
self.name.camelize
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
module ActionWebService
|
||||
# To send structured types across the wire, derive from ActionWebService::Struct,
|
||||
# and use +member+ to declare structure members.
|
||||
#
|
||||
# ActionWebService::Struct should be used in method signatures when you want to accept or return
|
||||
# structured types that have no Active Record model class representations, or you don't
|
||||
# want to expose your entire Active Record model to remote callers.
|
||||
#
|
||||
# === Example
|
||||
#
|
||||
# class Person < ActionWebService::Struct
|
||||
# member :id, :int
|
||||
# member :firstnames, [:string]
|
||||
# member :lastname, :string
|
||||
# member :email, :string
|
||||
# end
|
||||
# person = Person.new(:id => 5, :firstname => 'john', :lastname => 'doe')
|
||||
#
|
||||
# Active Record model classes are already implicitly supported in method
|
||||
# signatures.
|
||||
class Struct
|
||||
# If a Hash is given as argument to an ActionWebService::Struct constructor,
|
||||
# it can contain initial values for the structure member.
|
||||
def initialize(values={})
|
||||
if values.is_a?(Hash)
|
||||
values.map{|k,v| __send__('%s=' % k.to_s, v)}
|
||||
end
|
||||
end
|
||||
|
||||
# The member with the given name
|
||||
def [](name)
|
||||
send(name.to_s)
|
||||
end
|
||||
|
||||
# Iterates through each member
|
||||
def each_pair(&block)
|
||||
self.class.members.each do |name, type|
|
||||
yield name, self.__send__(name)
|
||||
end
|
||||
end
|
||||
|
||||
class << self
|
||||
# Creates a structure member with the specified +name+ and +type+. Generates
|
||||
# accessor methods for reading and writing the member value.
|
||||
def member(name, type)
|
||||
name = name.to_sym
|
||||
type = ActionWebService::SignatureTypes.canonical_signature_entry({ name => type }, 0)
|
||||
write_inheritable_hash("struct_members", name => type)
|
||||
class_eval <<-END
|
||||
def #{name}; @#{name}; end
|
||||
def #{name}=(value); @#{name} = value; end
|
||||
END
|
||||
end
|
||||
|
||||
def members # :nodoc:
|
||||
read_inheritable_attribute("struct_members") || {}
|
||||
end
|
||||
|
||||
def member_type(name) # :nodoc:
|
||||
members[name.to_sym]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
class Class # :nodoc:
|
||||
def class_inheritable_option(sym, default_value=nil)
|
||||
write_inheritable_attribute sym, default_value
|
||||
class_eval <<-EOS
|
||||
def self.#{sym}(value=nil)
|
||||
if !value.nil?
|
||||
write_inheritable_attribute(:#{sym}, value)
|
||||
else
|
||||
read_inheritable_attribute(:#{sym})
|
||||
end
|
||||
end
|
||||
|
||||
def self.#{sym}=(value)
|
||||
write_inheritable_attribute(:#{sym}, value)
|
||||
end
|
||||
|
||||
def #{sym}
|
||||
self.class.#{sym}
|
||||
end
|
||||
|
||||
def #{sym}=(value)
|
||||
self.class.#{sym} = value
|
||||
end
|
||||
EOS
|
||||
end
|
||||
end
|
||||
Vendored
+226
@@ -0,0 +1,226 @@
|
||||
module ActionWebService # :nodoc:
|
||||
# Action Web Service supports the following base types in a signature:
|
||||
#
|
||||
# [<tt>:int</tt>] Represents an integer value, will be cast to an integer using <tt>Integer(value)</tt>
|
||||
# [<tt>:string</tt>] Represents a string value, will be cast to an string using the <tt>to_s</tt> method on an object
|
||||
# [<tt>:base64</tt>] Represents a Base 64 value, will contain the binary bytes of a Base 64 value sent by the caller
|
||||
# [<tt>:bool</tt>] Represents a boolean value, whatever is passed will be cast to boolean (<tt>true</tt>, '1', 'true', 'y', 'yes' are taken to represent true; <tt>false</tt>, '0', 'false', 'n', 'no' and <tt>nil</tt> represent false)
|
||||
# [<tt>:float</tt>] Represents a floating point value, will be cast to a float using <tt>Float(value)</tt>
|
||||
# [<tt>:time</tt>] Represents a timestamp, will be cast to a <tt>Time</tt> object
|
||||
# [<tt>:datetime</tt>] Represents a timestamp, will be cast to a <tt>DateTime</tt> object
|
||||
# [<tt>:date</tt>] Represents a date, will be cast to a <tt>Date</tt> object
|
||||
#
|
||||
# For structured types, you'll need to pass in the Class objects of
|
||||
# ActionWebService::Struct and ActiveRecord::Base derivatives.
|
||||
module SignatureTypes
|
||||
def canonical_signature(signature) # :nodoc:
|
||||
return nil if signature.nil?
|
||||
unless signature.is_a?(Array)
|
||||
raise(ActionWebServiceError, "Expected signature to be an Array")
|
||||
end
|
||||
i = -1
|
||||
signature.map{ |spec| canonical_signature_entry(spec, i += 1) }
|
||||
end
|
||||
|
||||
def canonical_signature_entry(spec, i) # :nodoc:
|
||||
orig_spec = spec
|
||||
name = "param#{i}"
|
||||
if spec.is_a?(Hash)
|
||||
name, spec = spec.keys.first, spec.values.first
|
||||
end
|
||||
type = spec
|
||||
if spec.is_a?(Array)
|
||||
ArrayType.new(orig_spec, canonical_signature_entry(spec[0], 0), name)
|
||||
else
|
||||
type = canonical_type(type)
|
||||
if type.is_a?(Symbol)
|
||||
BaseType.new(orig_spec, type, name)
|
||||
else
|
||||
StructuredType.new(orig_spec, type, name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def canonical_type(type) # :nodoc:
|
||||
type_name = symbol_name(type) || class_to_type_name(type)
|
||||
type = type_name || type
|
||||
return canonical_type_name(type) if type.is_a?(Symbol)
|
||||
type
|
||||
end
|
||||
|
||||
def canonical_type_name(name) # :nodoc:
|
||||
name = name.to_sym
|
||||
case name
|
||||
when :int, :integer, :fixnum, :bignum
|
||||
:int
|
||||
when :string, :text
|
||||
:string
|
||||
when :base64, :binary
|
||||
:base64
|
||||
when :bool, :boolean
|
||||
:bool
|
||||
when :float, :double
|
||||
:float
|
||||
when :decimal
|
||||
:decimal
|
||||
when :time, :timestamp
|
||||
:time
|
||||
when :datetime
|
||||
:datetime
|
||||
when :date
|
||||
:date
|
||||
else
|
||||
raise(TypeError, "#{name} is not a valid base type")
|
||||
end
|
||||
end
|
||||
|
||||
def canonical_type_class(type) # :nodoc:
|
||||
type = canonical_type(type)
|
||||
type.is_a?(Symbol) ? type_name_to_class(type) : type
|
||||
end
|
||||
|
||||
def symbol_name(name) # :nodoc:
|
||||
return name.to_sym if name.is_a?(Symbol) || name.is_a?(String)
|
||||
nil
|
||||
end
|
||||
|
||||
def class_to_type_name(klass) # :nodoc:
|
||||
klass = klass.class unless klass.is_a?(Class)
|
||||
if derived_from?(Integer, klass) || derived_from?(Fixnum, klass) || derived_from?(Bignum, klass)
|
||||
:int
|
||||
elsif klass == String
|
||||
:string
|
||||
elsif klass == Base64
|
||||
:base64
|
||||
elsif klass == TrueClass || klass == FalseClass
|
||||
:bool
|
||||
elsif derived_from?(Float, klass) || derived_from?(Precision, klass) || derived_from?(Numeric, klass)
|
||||
:float
|
||||
elsif klass == Time
|
||||
:time
|
||||
elsif klass == DateTime
|
||||
:datetime
|
||||
elsif klass == Date
|
||||
:date
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def type_name_to_class(name) # :nodoc:
|
||||
case canonical_type_name(name)
|
||||
when :int
|
||||
Integer
|
||||
when :string
|
||||
String
|
||||
when :base64
|
||||
Base64
|
||||
when :bool
|
||||
TrueClass
|
||||
when :float
|
||||
Float
|
||||
when :decimal
|
||||
BigDecimal
|
||||
when :time
|
||||
Time
|
||||
when :date
|
||||
Date
|
||||
when :datetime
|
||||
DateTime
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def derived_from?(ancestor, child) # :nodoc:
|
||||
child.ancestors.include?(ancestor)
|
||||
end
|
||||
|
||||
module_function :type_name_to_class
|
||||
module_function :class_to_type_name
|
||||
module_function :symbol_name
|
||||
module_function :canonical_type_class
|
||||
module_function :canonical_type_name
|
||||
module_function :canonical_type
|
||||
module_function :canonical_signature_entry
|
||||
module_function :canonical_signature
|
||||
module_function :derived_from?
|
||||
end
|
||||
|
||||
class BaseType # :nodoc:
|
||||
include SignatureTypes
|
||||
|
||||
attr :spec
|
||||
attr :type
|
||||
attr :type_class
|
||||
attr :name
|
||||
|
||||
def initialize(spec, type, name)
|
||||
@spec = spec
|
||||
@type = canonical_type(type)
|
||||
@type_class = canonical_type_class(@type)
|
||||
@name = name
|
||||
end
|
||||
|
||||
def custom?
|
||||
false
|
||||
end
|
||||
|
||||
def array?
|
||||
false
|
||||
end
|
||||
|
||||
def structured?
|
||||
false
|
||||
end
|
||||
|
||||
def human_name(show_name=true)
|
||||
type_type = array? ? element_type.type.to_s : self.type.to_s
|
||||
str = array? ? (type_type + '[]') : type_type
|
||||
show_name ? (str + " " + name.to_s) : str
|
||||
end
|
||||
end
|
||||
|
||||
class ArrayType < BaseType # :nodoc:
|
||||
attr :element_type
|
||||
|
||||
def initialize(spec, element_type, name)
|
||||
super(spec, Array, name)
|
||||
@element_type = element_type
|
||||
end
|
||||
|
||||
def custom?
|
||||
true
|
||||
end
|
||||
|
||||
def array?
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
class StructuredType < BaseType # :nodoc:
|
||||
def each_member
|
||||
if @type_class.respond_to?(:members)
|
||||
@type_class.members.each do |name, type|
|
||||
yield name, type
|
||||
end
|
||||
elsif @type_class.respond_to?(:columns)
|
||||
i = -1
|
||||
@type_class.columns.each do |column|
|
||||
yield column.name, canonical_signature_entry(column.type, i += 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def custom?
|
||||
true
|
||||
end
|
||||
|
||||
def structured?
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
class Base64 < String # :nodoc:
|
||||
end
|
||||
end
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
<html>
|
||||
<head>
|
||||
<title><%= @scaffold_class.wsdl_service_name %> Web Service</title>
|
||||
<style>
|
||||
body { background-color: #fff; color: #333; }
|
||||
|
||||
body, p, ol, ul, td {
|
||||
font-family: verdana, arial, helvetica, sans-serif;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: #eee;
|
||||
padding: 10px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
a { color: #000; }
|
||||
a:visited { color: #666; }
|
||||
a:hover { color: #fff; background-color:#000; }
|
||||
|
||||
.fieldWithErrors {
|
||||
padding: 2px;
|
||||
background-color: red;
|
||||
display: table;
|
||||
}
|
||||
|
||||
#errorExplanation {
|
||||
width: 400px;
|
||||
border: 2px solid red;
|
||||
padding: 7px;
|
||||
padding-bottom: 12px;
|
||||
margin-bottom: 20px;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
#errorExplanation h2 {
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
padding: 5px 5px 5px 15px;
|
||||
font-size: 12px;
|
||||
margin: -7px;
|
||||
background-color: #c00;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#errorExplanation p {
|
||||
color: #333;
|
||||
margin-bottom: 0;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
#errorExplanation ul li {
|
||||
font-size: 12px;
|
||||
list-style: square;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<%= @content_for_layout %>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
<% @scaffold_container.services.each do |service| %>
|
||||
|
||||
<h4>API Methods for <%= service %></h4>
|
||||
<%= service_method_list(service) %>
|
||||
|
||||
<% end %>
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
<h4>Method Invocation Details for <em><%= @scaffold_service %>#<%= @scaffold_method.public_name %></em></h4>
|
||||
|
||||
<% form_tag(:action => @scaffold_action_name + '_submit') do -%>
|
||||
<%= hidden_field_tag "service", @scaffold_service.name %>
|
||||
<%= hidden_field_tag "method", @scaffold_method.public_name %>
|
||||
|
||||
<p>
|
||||
<label for="protocol">Protocol:</label><br />
|
||||
<%= select_tag 'protocol', options_for_select([['SOAP', 'soap'], ['XML-RPC', 'xmlrpc']], params['protocol']) %>
|
||||
</p>
|
||||
|
||||
<% if @scaffold_method.expects %>
|
||||
|
||||
<strong>Method Parameters:</strong><br />
|
||||
<% @scaffold_method.expects.each_with_index do |type, i| %>
|
||||
<p>
|
||||
<label for="method_params[<%= i %>]"><%= method_parameter_label(type.name, type) %> </label><br />
|
||||
<%= method_parameter_input_fields(@scaffold_method, type, "method_params", i) %>
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
<% end %>
|
||||
|
||||
<%= submit_tag "Invoke" %>
|
||||
<% end -%>
|
||||
|
||||
<p>
|
||||
<%= link_to "Back", :action => @scaffold_action_name %>
|
||||
</p>
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
<h4>Method Invocation Result for <em><%= @scaffold_service %>#<%= @scaffold_method.public_name %></em></h4>
|
||||
|
||||
<p>
|
||||
Invocation took <tt><%= '%f' % @method_elapsed %></tt> seconds
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Return Value:</strong><br />
|
||||
<pre>
|
||||
<%= h @method_return_value.inspect %>
|
||||
</pre>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Request XML:</strong><br />
|
||||
<pre>
|
||||
<%= h @method_request_xml %>
|
||||
</pre>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Response XML:</strong><br />
|
||||
<pre>
|
||||
<%= h @method_response_xml %>
|
||||
</pre>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<%= link_to "Back", :action => @scaffold_action_name + '_method_params', :method => @scaffold_method.public_name, :service => @scaffold_service.name %>
|
||||
</p>
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
require 'test/unit'
|
||||
|
||||
module Test # :nodoc:
|
||||
module Unit # :nodoc:
|
||||
class TestCase # :nodoc:
|
||||
private
|
||||
# invoke the specified API method
|
||||
def invoke_direct(method_name, *args)
|
||||
prepare_request('api', 'api', method_name, *args)
|
||||
@controller.process(@request, @response)
|
||||
decode_rpc_response
|
||||
end
|
||||
alias_method :invoke, :invoke_direct
|
||||
|
||||
# invoke the specified API method on the specified service
|
||||
def invoke_delegated(service_name, method_name, *args)
|
||||
prepare_request(service_name.to_s, service_name, method_name, *args)
|
||||
@controller.process(@request, @response)
|
||||
decode_rpc_response
|
||||
end
|
||||
|
||||
# invoke the specified layered API method on the correct service
|
||||
def invoke_layered(service_name, method_name, *args)
|
||||
prepare_request('api', service_name, method_name, *args)
|
||||
@controller.process(@request, @response)
|
||||
decode_rpc_response
|
||||
end
|
||||
|
||||
# ---------------------- internal ---------------------------
|
||||
|
||||
def prepare_request(action, service_name, api_method_name, *args)
|
||||
@request.recycle!
|
||||
@request.request_parameters['action'] = action
|
||||
@request.env['REQUEST_METHOD'] = 'POST'
|
||||
@request.env['HTTP_CONTENT_TYPE'] = 'text/xml'
|
||||
@request.env['RAW_POST_DATA'] = encode_rpc_call(service_name, api_method_name, *args)
|
||||
case protocol
|
||||
when ActionWebService::Protocol::Soap::SoapProtocol
|
||||
soap_action = "/#{@controller.controller_name}/#{service_name}/#{public_method_name(service_name, api_method_name)}"
|
||||
@request.env['HTTP_SOAPACTION'] = soap_action
|
||||
when ActionWebService::Protocol::XmlRpc::XmlRpcProtocol
|
||||
@request.env.delete('HTTP_SOAPACTION')
|
||||
end
|
||||
end
|
||||
|
||||
def encode_rpc_call(service_name, api_method_name, *args)
|
||||
case @controller.web_service_dispatching_mode
|
||||
when :direct
|
||||
api = @controller.class.web_service_api
|
||||
when :delegated, :layered
|
||||
api = @controller.web_service_object(service_name.to_sym).class.web_service_api
|
||||
end
|
||||
protocol.register_api(api)
|
||||
method = api.api_methods[api_method_name.to_sym]
|
||||
raise ArgumentError, "wrong number of arguments for rpc call (#{args.length} for #{method.expects.length})" if method && method.expects && args.length != method.expects.length
|
||||
protocol.encode_request(public_method_name(service_name, api_method_name), args.dup, method.expects)
|
||||
end
|
||||
|
||||
def decode_rpc_response
|
||||
public_method_name, return_value = protocol.decode_response(@response.body)
|
||||
exception = is_exception?(return_value)
|
||||
raise exception if exception
|
||||
return_value
|
||||
end
|
||||
|
||||
def public_method_name(service_name, api_method_name)
|
||||
public_name = service_api(service_name).public_api_method_name(api_method_name)
|
||||
if @controller.web_service_dispatching_mode == :layered && protocol.is_a?(ActionWebService::Protocol::XmlRpc::XmlRpcProtocol)
|
||||
'%s.%s' % [service_name.to_s, public_name]
|
||||
else
|
||||
public_name
|
||||
end
|
||||
end
|
||||
|
||||
def service_api(service_name)
|
||||
case @controller.web_service_dispatching_mode
|
||||
when :direct
|
||||
@controller.class.web_service_api
|
||||
when :delegated, :layered
|
||||
@controller.web_service_object(service_name.to_sym).class.web_service_api
|
||||
end
|
||||
end
|
||||
|
||||
def protocol
|
||||
if @protocol.nil?
|
||||
@protocol ||= ActionWebService::Protocol::Soap::SoapProtocol.create(@controller)
|
||||
else
|
||||
case @protocol
|
||||
when :xmlrpc
|
||||
@protocol = ActionWebService::Protocol::XmlRpc::XmlRpcProtocol.create(@controller)
|
||||
when :soap
|
||||
@protocol = ActionWebService::Protocol::Soap::SoapProtocol.create(@controller)
|
||||
else
|
||||
@protocol
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def is_exception?(obj)
|
||||
case protocol
|
||||
when :soap, ActionWebService::Protocol::Soap::SoapProtocol
|
||||
(obj.respond_to?(:detail) && obj.detail.respond_to?(:cause) && \
|
||||
obj.detail.cause.is_a?(Exception)) ? obj.detail.cause : nil
|
||||
when :xmlrpc, ActionWebService::Protocol::XmlRpc::XmlRpcProtocol
|
||||
obj.is_a?(XMLRPC::FaultException) ? obj : nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
module ActionWebService
|
||||
module VERSION #:nodoc:
|
||||
MAJOR = 1
|
||||
MINOR = 2
|
||||
TINY = 5
|
||||
|
||||
STRING = [MAJOR, MINOR, TINY].join('.')
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1 @@
|
||||
require 'action_web_service'
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
require File.dirname(__FILE__) + '/lib/acts_as_activity_provider'
|
||||
ActiveRecord::Base.send(:include, Redmine::Acts::ActivityProvider)
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2008 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
module Redmine
|
||||
module Acts
|
||||
module ActivityProvider
|
||||
def self.included(base)
|
||||
base.extend ClassMethods
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
def acts_as_activity_provider(options = {})
|
||||
unless self.included_modules.include?(Redmine::Acts::ActivityProvider::InstanceMethods)
|
||||
cattr_accessor :activity_provider_options
|
||||
send :include, Redmine::Acts::ActivityProvider::InstanceMethods
|
||||
end
|
||||
|
||||
options.assert_valid_keys(:type, :permission, :timestamp, :author_key, :find_options)
|
||||
self.activity_provider_options ||= {}
|
||||
|
||||
# One model can provide different event types
|
||||
# We store these options in activity_provider_options hash
|
||||
event_type = options.delete(:type) || self.name.underscore.pluralize
|
||||
|
||||
options[:permission] = "view_#{self.name.underscore.pluralize}".to_sym unless options.has_key?(:permission)
|
||||
options[:timestamp] ||= "#{table_name}.created_on"
|
||||
options[:find_options] ||= {}
|
||||
options[:author_key] = "#{table_name}.#{options[:author_key]}" if options[:author_key].is_a?(Symbol)
|
||||
self.activity_provider_options[event_type] = options
|
||||
end
|
||||
end
|
||||
|
||||
module InstanceMethods
|
||||
def self.included(base)
|
||||
base.extend ClassMethods
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
# Returns events of type event_type visible by user that occured between from and to
|
||||
def find_events(event_type, user, from, to, options)
|
||||
provider_options = activity_provider_options[event_type]
|
||||
raise "#{self.name} can not provide #{event_type} events." if provider_options.nil?
|
||||
|
||||
scope_options = {}
|
||||
cond = ARCondition.new
|
||||
if from && to
|
||||
cond.add(["#{provider_options[:timestamp]} BETWEEN ? AND ?", from, to])
|
||||
end
|
||||
if options[:author]
|
||||
return [] if provider_options[:author_key].nil?
|
||||
cond.add(["#{provider_options[:author_key]} = ?", options[:author].id])
|
||||
end
|
||||
cond.add(Project.allowed_to_condition(user, provider_options[:permission], options)) if provider_options[:permission]
|
||||
scope_options[:conditions] = cond.conditions
|
||||
if options[:limit]
|
||||
# id and creation time should be in same order in most cases
|
||||
scope_options[:order] = "#{table_name}.id DESC"
|
||||
scope_options[:limit] = options[:limit]
|
||||
end
|
||||
|
||||
with_scope(:find => scope_options) do
|
||||
find(:all, provider_options[:find_options].dup)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,2 @@
|
||||
require File.dirname(__FILE__) + '/lib/acts_as_attachable'
|
||||
ActiveRecord::Base.send(:include, Redmine::Acts::Attachable)
|
||||
@@ -0,0 +1,57 @@
|
||||
# Redmine - project management software
|
||||
# Copyright (C) 2006-2008 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
module Redmine
|
||||
module Acts
|
||||
module Attachable
|
||||
def self.included(base)
|
||||
base.extend ClassMethods
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
def acts_as_attachable(options = {})
|
||||
cattr_accessor :attachable_options
|
||||
self.attachable_options = {}
|
||||
attachable_options[:view_permission] = options.delete(:view_permission) || "view_#{self.name.pluralize.underscore}".to_sym
|
||||
attachable_options[:delete_permission] = options.delete(:delete_permission) || "edit_#{self.name.pluralize.underscore}".to_sym
|
||||
|
||||
has_many :attachments, options.merge(:as => :container,
|
||||
:order => "#{Attachment.table_name}.created_on",
|
||||
:dependent => :destroy)
|
||||
send :include, Redmine::Acts::Attachable::InstanceMethods
|
||||
end
|
||||
end
|
||||
|
||||
module InstanceMethods
|
||||
def self.included(base)
|
||||
base.extend ClassMethods
|
||||
end
|
||||
|
||||
def attachments_visible?(user=User.current)
|
||||
user.allowed_to?(self.class.attachable_options[:view_permission], self.project)
|
||||
end
|
||||
|
||||
def attachments_deletable?(user=User.current)
|
||||
user.allowed_to?(self.class.attachable_options[:delete_permission], self.project)
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,2 @@
|
||||
require File.dirname(__FILE__) + '/lib/acts_as_customizable'
|
||||
ActiveRecord::Base.send(:include, Redmine::Acts::Customizable)
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2008 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
module Redmine
|
||||
module Acts
|
||||
module Customizable
|
||||
def self.included(base)
|
||||
base.extend ClassMethods
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
def acts_as_customizable(options = {})
|
||||
return if self.included_modules.include?(Redmine::Acts::Customizable::InstanceMethods)
|
||||
cattr_accessor :customizable_options
|
||||
self.customizable_options = options
|
||||
has_many :custom_values, :as => :customized,
|
||||
:include => :custom_field,
|
||||
:order => "#{CustomField.table_name}.position",
|
||||
:dependent => :delete_all
|
||||
before_validation_on_create { |customized| customized.custom_field_values }
|
||||
# Trigger validation only if custom values were changed
|
||||
validates_associated :custom_values, :on => :update, :if => Proc.new { |customized| customized.custom_field_values_changed? }
|
||||
send :include, Redmine::Acts::Customizable::InstanceMethods
|
||||
# Save custom values when saving the customized object
|
||||
after_save :save_custom_field_values
|
||||
end
|
||||
end
|
||||
|
||||
module InstanceMethods
|
||||
def self.included(base)
|
||||
base.extend ClassMethods
|
||||
end
|
||||
|
||||
def available_custom_fields
|
||||
CustomField.find(:all, :conditions => "type = '#{self.class.name}CustomField'",
|
||||
:order => 'position')
|
||||
end
|
||||
|
||||
def custom_field_values=(values)
|
||||
@custom_field_values_changed = true
|
||||
values = values.stringify_keys
|
||||
custom_field_values.each do |custom_value|
|
||||
custom_value.value = values[custom_value.custom_field_id.to_s] if values.has_key?(custom_value.custom_field_id.to_s)
|
||||
end if values.is_a?(Hash)
|
||||
end
|
||||
|
||||
def custom_field_values
|
||||
@custom_field_values ||= available_custom_fields.collect { |x| custom_values.detect { |v| v.custom_field == x } || custom_values.build(:custom_field => x, :value => nil) }
|
||||
end
|
||||
|
||||
def custom_field_values_changed?
|
||||
@custom_field_values_changed == true
|
||||
end
|
||||
|
||||
def custom_value_for(c)
|
||||
field_id = (c.is_a?(CustomField) ? c.id : c.to_i)
|
||||
custom_values.detect {|v| v.custom_field_id == field_id }
|
||||
end
|
||||
|
||||
def save_custom_field_values
|
||||
custom_field_values.each(&:save)
|
||||
@custom_field_values_changed = false
|
||||
@custom_field_values = nil
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,2 @@
|
||||
require File.dirname(__FILE__) + '/lib/acts_as_event'
|
||||
ActiveRecord::Base.send(:include, Redmine::Acts::Event)
|
||||
@@ -0,0 +1,76 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
module Redmine
|
||||
module Acts
|
||||
module Event
|
||||
def self.included(base)
|
||||
base.extend ClassMethods
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
def acts_as_event(options = {})
|
||||
return if self.included_modules.include?(Redmine::Acts::Event::InstanceMethods)
|
||||
default_options = { :datetime => :created_on,
|
||||
:title => :title,
|
||||
:description => :description,
|
||||
:author => :author,
|
||||
:url => {:controller => 'welcome'},
|
||||
:type => self.name.underscore.dasherize }
|
||||
|
||||
cattr_accessor :event_options
|
||||
self.event_options = default_options.merge(options)
|
||||
send :include, Redmine::Acts::Event::InstanceMethods
|
||||
end
|
||||
end
|
||||
|
||||
module InstanceMethods
|
||||
def self.included(base)
|
||||
base.extend ClassMethods
|
||||
end
|
||||
|
||||
%w(datetime title description author type).each do |attr|
|
||||
src = <<-END_SRC
|
||||
def event_#{attr}
|
||||
option = event_options[:#{attr}]
|
||||
if option.is_a?(Proc)
|
||||
option.call(self)
|
||||
elsif option.is_a?(Symbol)
|
||||
send(option)
|
||||
else
|
||||
option
|
||||
end
|
||||
end
|
||||
END_SRC
|
||||
class_eval src, __FILE__, __LINE__
|
||||
end
|
||||
|
||||
def event_date
|
||||
event_datetime.to_date
|
||||
end
|
||||
|
||||
def event_url(options = {})
|
||||
option = event_options[:url]
|
||||
(option.is_a?(Proc) ? option.call(self) : send(option)).merge(options)
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,23 @@
|
||||
ActsAsList
|
||||
==========
|
||||
|
||||
This acts_as extension provides the capabilities for sorting and reordering a number of objects in a list. The class that has this specified needs to have a +position+ column defined as an integer on the mapped database table.
|
||||
|
||||
|
||||
Example
|
||||
=======
|
||||
|
||||
class TodoList < ActiveRecord::Base
|
||||
has_many :todo_items, :order => "position"
|
||||
end
|
||||
|
||||
class TodoItem < ActiveRecord::Base
|
||||
belongs_to :todo_list
|
||||
acts_as_list :scope => :todo_list
|
||||
end
|
||||
|
||||
todo_list.first.move_to_bottom
|
||||
todo_list.last.move_higher
|
||||
|
||||
|
||||
Copyright (c) 2007 David Heinemeier Hansson, released under the MIT license
|
||||
@@ -0,0 +1,3 @@
|
||||
$:.unshift "#{File.dirname(__FILE__)}/lib"
|
||||
require 'active_record/acts/list'
|
||||
ActiveRecord::Base.class_eval { include ActiveRecord::Acts::List }
|
||||
@@ -0,0 +1,256 @@
|
||||
module ActiveRecord
|
||||
module Acts #:nodoc:
|
||||
module List #:nodoc:
|
||||
def self.included(base)
|
||||
base.extend(ClassMethods)
|
||||
end
|
||||
|
||||
# This +acts_as+ extension provides the capabilities for sorting and reordering a number of objects in a list.
|
||||
# The class that has this specified needs to have a +position+ column defined as an integer on
|
||||
# the mapped database table.
|
||||
#
|
||||
# Todo list example:
|
||||
#
|
||||
# class TodoList < ActiveRecord::Base
|
||||
# has_many :todo_items, :order => "position"
|
||||
# end
|
||||
#
|
||||
# class TodoItem < ActiveRecord::Base
|
||||
# belongs_to :todo_list
|
||||
# acts_as_list :scope => :todo_list
|
||||
# end
|
||||
#
|
||||
# todo_list.first.move_to_bottom
|
||||
# todo_list.last.move_higher
|
||||
module ClassMethods
|
||||
# Configuration options are:
|
||||
#
|
||||
# * +column+ - specifies the column name to use for keeping the position integer (default: +position+)
|
||||
# * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach <tt>_id</tt>
|
||||
# (if it hasn't already been added) and use that as the foreign key restriction. It's also possible
|
||||
# to give it an entire string that is interpolated if you need a tighter scope than just a foreign key.
|
||||
# Example: <tt>acts_as_list :scope => 'todo_list_id = #{todo_list_id} AND completed = 0'</tt>
|
||||
def acts_as_list(options = {})
|
||||
configuration = { :column => "position", :scope => "1 = 1" }
|
||||
configuration.update(options) if options.is_a?(Hash)
|
||||
|
||||
configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/
|
||||
|
||||
if configuration[:scope].is_a?(Symbol)
|
||||
scope_condition_method = %(
|
||||
def scope_condition
|
||||
if #{configuration[:scope].to_s}.nil?
|
||||
"#{configuration[:scope].to_s} IS NULL"
|
||||
else
|
||||
"#{configuration[:scope].to_s} = \#{#{configuration[:scope].to_s}}"
|
||||
end
|
||||
end
|
||||
)
|
||||
else
|
||||
scope_condition_method = "def scope_condition() \"#{configuration[:scope]}\" end"
|
||||
end
|
||||
|
||||
class_eval <<-EOV
|
||||
include ActiveRecord::Acts::List::InstanceMethods
|
||||
|
||||
def acts_as_list_class
|
||||
::#{self.name}
|
||||
end
|
||||
|
||||
def position_column
|
||||
'#{configuration[:column]}'
|
||||
end
|
||||
|
||||
#{scope_condition_method}
|
||||
|
||||
before_destroy :remove_from_list
|
||||
before_create :add_to_list_bottom
|
||||
EOV
|
||||
end
|
||||
end
|
||||
|
||||
# All the methods available to a record that has had <tt>acts_as_list</tt> specified. Each method works
|
||||
# by assuming the object to be the item in the list, so <tt>chapter.move_lower</tt> would move that chapter
|
||||
# lower in the list of all chapters. Likewise, <tt>chapter.first?</tt> would return +true+ if that chapter is
|
||||
# the first in the list of all chapters.
|
||||
module InstanceMethods
|
||||
# Insert the item at the given position (defaults to the top position of 1).
|
||||
def insert_at(position = 1)
|
||||
insert_at_position(position)
|
||||
end
|
||||
|
||||
# Swap positions with the next lower item, if one exists.
|
||||
def move_lower
|
||||
return unless lower_item
|
||||
|
||||
acts_as_list_class.transaction do
|
||||
lower_item.decrement_position
|
||||
increment_position
|
||||
end
|
||||
end
|
||||
|
||||
# Swap positions with the next higher item, if one exists.
|
||||
def move_higher
|
||||
return unless higher_item
|
||||
|
||||
acts_as_list_class.transaction do
|
||||
higher_item.increment_position
|
||||
decrement_position
|
||||
end
|
||||
end
|
||||
|
||||
# Move to the bottom of the list. If the item is already in the list, the items below it have their
|
||||
# position adjusted accordingly.
|
||||
def move_to_bottom
|
||||
return unless in_list?
|
||||
acts_as_list_class.transaction do
|
||||
decrement_positions_on_lower_items
|
||||
assume_bottom_position
|
||||
end
|
||||
end
|
||||
|
||||
# Move to the top of the list. If the item is already in the list, the items above it have their
|
||||
# position adjusted accordingly.
|
||||
def move_to_top
|
||||
return unless in_list?
|
||||
acts_as_list_class.transaction do
|
||||
increment_positions_on_higher_items
|
||||
assume_top_position
|
||||
end
|
||||
end
|
||||
|
||||
# Removes the item from the list.
|
||||
def remove_from_list
|
||||
if in_list?
|
||||
decrement_positions_on_lower_items
|
||||
update_attribute position_column, nil
|
||||
end
|
||||
end
|
||||
|
||||
# Increase the position of this item without adjusting the rest of the list.
|
||||
def increment_position
|
||||
return unless in_list?
|
||||
update_attribute position_column, self.send(position_column).to_i + 1
|
||||
end
|
||||
|
||||
# Decrease the position of this item without adjusting the rest of the list.
|
||||
def decrement_position
|
||||
return unless in_list?
|
||||
update_attribute position_column, self.send(position_column).to_i - 1
|
||||
end
|
||||
|
||||
# Return +true+ if this object is the first in the list.
|
||||
def first?
|
||||
return false unless in_list?
|
||||
self.send(position_column) == 1
|
||||
end
|
||||
|
||||
# Return +true+ if this object is the last in the list.
|
||||
def last?
|
||||
return false unless in_list?
|
||||
self.send(position_column) == bottom_position_in_list
|
||||
end
|
||||
|
||||
# Return the next higher item in the list.
|
||||
def higher_item
|
||||
return nil unless in_list?
|
||||
acts_as_list_class.find(:first, :conditions =>
|
||||
"#{scope_condition} AND #{position_column} = #{(send(position_column).to_i - 1).to_s}"
|
||||
)
|
||||
end
|
||||
|
||||
# Return the next lower item in the list.
|
||||
def lower_item
|
||||
return nil unless in_list?
|
||||
acts_as_list_class.find(:first, :conditions =>
|
||||
"#{scope_condition} AND #{position_column} = #{(send(position_column).to_i + 1).to_s}"
|
||||
)
|
||||
end
|
||||
|
||||
# Test if this record is in a list
|
||||
def in_list?
|
||||
!send(position_column).nil?
|
||||
end
|
||||
|
||||
private
|
||||
def add_to_list_top
|
||||
increment_positions_on_all_items
|
||||
end
|
||||
|
||||
def add_to_list_bottom
|
||||
self[position_column] = bottom_position_in_list.to_i + 1
|
||||
end
|
||||
|
||||
# Overwrite this method to define the scope of the list changes
|
||||
def scope_condition() "1" end
|
||||
|
||||
# Returns the bottom position number in the list.
|
||||
# bottom_position_in_list # => 2
|
||||
def bottom_position_in_list(except = nil)
|
||||
item = bottom_item(except)
|
||||
item ? item.send(position_column) : 0
|
||||
end
|
||||
|
||||
# Returns the bottom item
|
||||
def bottom_item(except = nil)
|
||||
conditions = scope_condition
|
||||
conditions = "#{conditions} AND #{self.class.primary_key} != #{except.id}" if except
|
||||
acts_as_list_class.find(:first, :conditions => conditions, :order => "#{position_column} DESC")
|
||||
end
|
||||
|
||||
# Forces item to assume the bottom position in the list.
|
||||
def assume_bottom_position
|
||||
update_attribute(position_column, bottom_position_in_list(self).to_i + 1)
|
||||
end
|
||||
|
||||
# Forces item to assume the top position in the list.
|
||||
def assume_top_position
|
||||
update_attribute(position_column, 1)
|
||||
end
|
||||
|
||||
# This has the effect of moving all the higher items up one.
|
||||
def decrement_positions_on_higher_items(position)
|
||||
acts_as_list_class.update_all(
|
||||
"#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} <= #{position}"
|
||||
)
|
||||
end
|
||||
|
||||
# This has the effect of moving all the lower items up one.
|
||||
def decrement_positions_on_lower_items
|
||||
return unless in_list?
|
||||
acts_as_list_class.update_all(
|
||||
"#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} > #{send(position_column).to_i}"
|
||||
)
|
||||
end
|
||||
|
||||
# This has the effect of moving all the higher items down one.
|
||||
def increment_positions_on_higher_items
|
||||
return unless in_list?
|
||||
acts_as_list_class.update_all(
|
||||
"#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} < #{send(position_column).to_i}"
|
||||
)
|
||||
end
|
||||
|
||||
# This has the effect of moving all the lower items down one.
|
||||
def increment_positions_on_lower_items(position)
|
||||
acts_as_list_class.update_all(
|
||||
"#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} >= #{position}"
|
||||
)
|
||||
end
|
||||
|
||||
# Increments position (<tt>position_column</tt>) of all items in the list.
|
||||
def increment_positions_on_all_items
|
||||
acts_as_list_class.update_all(
|
||||
"#{position_column} = (#{position_column} + 1)", "#{scope_condition}"
|
||||
)
|
||||
end
|
||||
|
||||
def insert_at_position(position)
|
||||
remove_from_list
|
||||
increment_positions_on_lower_items(position)
|
||||
self.update_attribute(position_column, position)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,332 @@
|
||||
require 'test/unit'
|
||||
|
||||
require 'rubygems'
|
||||
gem 'activerecord', '>= 1.15.4.7794'
|
||||
require 'active_record'
|
||||
|
||||
require "#{File.dirname(__FILE__)}/../init"
|
||||
|
||||
ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:")
|
||||
|
||||
def setup_db
|
||||
ActiveRecord::Schema.define(:version => 1) do
|
||||
create_table :mixins do |t|
|
||||
t.column :pos, :integer
|
||||
t.column :parent_id, :integer
|
||||
t.column :created_at, :datetime
|
||||
t.column :updated_at, :datetime
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def teardown_db
|
||||
ActiveRecord::Base.connection.tables.each do |table|
|
||||
ActiveRecord::Base.connection.drop_table(table)
|
||||
end
|
||||
end
|
||||
|
||||
class Mixin < ActiveRecord::Base
|
||||
end
|
||||
|
||||
class ListMixin < Mixin
|
||||
acts_as_list :column => "pos", :scope => :parent
|
||||
|
||||
def self.table_name() "mixins" end
|
||||
end
|
||||
|
||||
class ListMixinSub1 < ListMixin
|
||||
end
|
||||
|
||||
class ListMixinSub2 < ListMixin
|
||||
end
|
||||
|
||||
class ListWithStringScopeMixin < ActiveRecord::Base
|
||||
acts_as_list :column => "pos", :scope => 'parent_id = #{parent_id}'
|
||||
|
||||
def self.table_name() "mixins" end
|
||||
end
|
||||
|
||||
|
||||
class ListTest < Test::Unit::TestCase
|
||||
|
||||
def setup
|
||||
setup_db
|
||||
(1..4).each { |counter| ListMixin.create! :pos => counter, :parent_id => 5 }
|
||||
end
|
||||
|
||||
def teardown
|
||||
teardown_db
|
||||
end
|
||||
|
||||
def test_reordering
|
||||
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
|
||||
|
||||
ListMixin.find(2).move_lower
|
||||
assert_equal [1, 3, 2, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
|
||||
|
||||
ListMixin.find(2).move_higher
|
||||
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
|
||||
|
||||
ListMixin.find(1).move_to_bottom
|
||||
assert_equal [2, 3, 4, 1], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
|
||||
|
||||
ListMixin.find(1).move_to_top
|
||||
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
|
||||
|
||||
ListMixin.find(2).move_to_bottom
|
||||
assert_equal [1, 3, 4, 2], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
|
||||
|
||||
ListMixin.find(4).move_to_top
|
||||
assert_equal [4, 1, 3, 2], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
|
||||
end
|
||||
|
||||
def test_move_to_bottom_with_next_to_last_item
|
||||
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
|
||||
ListMixin.find(3).move_to_bottom
|
||||
assert_equal [1, 2, 4, 3], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
|
||||
end
|
||||
|
||||
def test_next_prev
|
||||
assert_equal ListMixin.find(2), ListMixin.find(1).lower_item
|
||||
assert_nil ListMixin.find(1).higher_item
|
||||
assert_equal ListMixin.find(3), ListMixin.find(4).higher_item
|
||||
assert_nil ListMixin.find(4).lower_item
|
||||
end
|
||||
|
||||
def test_injection
|
||||
item = ListMixin.new(:parent_id => 1)
|
||||
assert_equal "parent_id = 1", item.scope_condition
|
||||
assert_equal "pos", item.position_column
|
||||
end
|
||||
|
||||
def test_insert
|
||||
new = ListMixin.create(:parent_id => 20)
|
||||
assert_equal 1, new.pos
|
||||
assert new.first?
|
||||
assert new.last?
|
||||
|
||||
new = ListMixin.create(:parent_id => 20)
|
||||
assert_equal 2, new.pos
|
||||
assert !new.first?
|
||||
assert new.last?
|
||||
|
||||
new = ListMixin.create(:parent_id => 20)
|
||||
assert_equal 3, new.pos
|
||||
assert !new.first?
|
||||
assert new.last?
|
||||
|
||||
new = ListMixin.create(:parent_id => 0)
|
||||
assert_equal 1, new.pos
|
||||
assert new.first?
|
||||
assert new.last?
|
||||
end
|
||||
|
||||
def test_insert_at
|
||||
new = ListMixin.create(:parent_id => 20)
|
||||
assert_equal 1, new.pos
|
||||
|
||||
new = ListMixin.create(:parent_id => 20)
|
||||
assert_equal 2, new.pos
|
||||
|
||||
new = ListMixin.create(:parent_id => 20)
|
||||
assert_equal 3, new.pos
|
||||
|
||||
new4 = ListMixin.create(:parent_id => 20)
|
||||
assert_equal 4, new4.pos
|
||||
|
||||
new4.insert_at(3)
|
||||
assert_equal 3, new4.pos
|
||||
|
||||
new.reload
|
||||
assert_equal 4, new.pos
|
||||
|
||||
new.insert_at(2)
|
||||
assert_equal 2, new.pos
|
||||
|
||||
new4.reload
|
||||
assert_equal 4, new4.pos
|
||||
|
||||
new5 = ListMixin.create(:parent_id => 20)
|
||||
assert_equal 5, new5.pos
|
||||
|
||||
new5.insert_at(1)
|
||||
assert_equal 1, new5.pos
|
||||
|
||||
new4.reload
|
||||
assert_equal 5, new4.pos
|
||||
end
|
||||
|
||||
def test_delete_middle
|
||||
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
|
||||
|
||||
ListMixin.find(2).destroy
|
||||
|
||||
assert_equal [1, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
|
||||
|
||||
assert_equal 1, ListMixin.find(1).pos
|
||||
assert_equal 2, ListMixin.find(3).pos
|
||||
assert_equal 3, ListMixin.find(4).pos
|
||||
|
||||
ListMixin.find(1).destroy
|
||||
|
||||
assert_equal [3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
|
||||
|
||||
assert_equal 1, ListMixin.find(3).pos
|
||||
assert_equal 2, ListMixin.find(4).pos
|
||||
end
|
||||
|
||||
def test_with_string_based_scope
|
||||
new = ListWithStringScopeMixin.create(:parent_id => 500)
|
||||
assert_equal 1, new.pos
|
||||
assert new.first?
|
||||
assert new.last?
|
||||
end
|
||||
|
||||
def test_nil_scope
|
||||
new1, new2, new3 = ListMixin.create, ListMixin.create, ListMixin.create
|
||||
new2.move_higher
|
||||
assert_equal [new2, new1, new3], ListMixin.find(:all, :conditions => 'parent_id IS NULL', :order => 'pos')
|
||||
end
|
||||
|
||||
|
||||
def test_remove_from_list_should_then_fail_in_list?
|
||||
assert_equal true, ListMixin.find(1).in_list?
|
||||
ListMixin.find(1).remove_from_list
|
||||
assert_equal false, ListMixin.find(1).in_list?
|
||||
end
|
||||
|
||||
def test_remove_from_list_should_set_position_to_nil
|
||||
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
|
||||
|
||||
ListMixin.find(2).remove_from_list
|
||||
|
||||
assert_equal [2, 1, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
|
||||
|
||||
assert_equal 1, ListMixin.find(1).pos
|
||||
assert_equal nil, ListMixin.find(2).pos
|
||||
assert_equal 2, ListMixin.find(3).pos
|
||||
assert_equal 3, ListMixin.find(4).pos
|
||||
end
|
||||
|
||||
def test_remove_before_destroy_does_not_shift_lower_items_twice
|
||||
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
|
||||
|
||||
ListMixin.find(2).remove_from_list
|
||||
ListMixin.find(2).destroy
|
||||
|
||||
assert_equal [1, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
|
||||
|
||||
assert_equal 1, ListMixin.find(1).pos
|
||||
assert_equal 2, ListMixin.find(3).pos
|
||||
assert_equal 3, ListMixin.find(4).pos
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
class ListSubTest < Test::Unit::TestCase
|
||||
|
||||
def setup
|
||||
setup_db
|
||||
(1..4).each { |i| ((i % 2 == 1) ? ListMixinSub1 : ListMixinSub2).create! :pos => i, :parent_id => 5000 }
|
||||
end
|
||||
|
||||
def teardown
|
||||
teardown_db
|
||||
end
|
||||
|
||||
def test_reordering
|
||||
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
|
||||
|
||||
ListMixin.find(2).move_lower
|
||||
assert_equal [1, 3, 2, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
|
||||
|
||||
ListMixin.find(2).move_higher
|
||||
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
|
||||
|
||||
ListMixin.find(1).move_to_bottom
|
||||
assert_equal [2, 3, 4, 1], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
|
||||
|
||||
ListMixin.find(1).move_to_top
|
||||
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
|
||||
|
||||
ListMixin.find(2).move_to_bottom
|
||||
assert_equal [1, 3, 4, 2], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
|
||||
|
||||
ListMixin.find(4).move_to_top
|
||||
assert_equal [4, 1, 3, 2], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
|
||||
end
|
||||
|
||||
def test_move_to_bottom_with_next_to_last_item
|
||||
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
|
||||
ListMixin.find(3).move_to_bottom
|
||||
assert_equal [1, 2, 4, 3], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
|
||||
end
|
||||
|
||||
def test_next_prev
|
||||
assert_equal ListMixin.find(2), ListMixin.find(1).lower_item
|
||||
assert_nil ListMixin.find(1).higher_item
|
||||
assert_equal ListMixin.find(3), ListMixin.find(4).higher_item
|
||||
assert_nil ListMixin.find(4).lower_item
|
||||
end
|
||||
|
||||
def test_injection
|
||||
item = ListMixin.new("parent_id"=>1)
|
||||
assert_equal "parent_id = 1", item.scope_condition
|
||||
assert_equal "pos", item.position_column
|
||||
end
|
||||
|
||||
def test_insert_at
|
||||
new = ListMixin.create("parent_id" => 20)
|
||||
assert_equal 1, new.pos
|
||||
|
||||
new = ListMixinSub1.create("parent_id" => 20)
|
||||
assert_equal 2, new.pos
|
||||
|
||||
new = ListMixinSub2.create("parent_id" => 20)
|
||||
assert_equal 3, new.pos
|
||||
|
||||
new4 = ListMixin.create("parent_id" => 20)
|
||||
assert_equal 4, new4.pos
|
||||
|
||||
new4.insert_at(3)
|
||||
assert_equal 3, new4.pos
|
||||
|
||||
new.reload
|
||||
assert_equal 4, new.pos
|
||||
|
||||
new.insert_at(2)
|
||||
assert_equal 2, new.pos
|
||||
|
||||
new4.reload
|
||||
assert_equal 4, new4.pos
|
||||
|
||||
new5 = ListMixinSub1.create("parent_id" => 20)
|
||||
assert_equal 5, new5.pos
|
||||
|
||||
new5.insert_at(1)
|
||||
assert_equal 1, new5.pos
|
||||
|
||||
new4.reload
|
||||
assert_equal 5, new4.pos
|
||||
end
|
||||
|
||||
def test_delete_middle
|
||||
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
|
||||
|
||||
ListMixin.find(2).destroy
|
||||
|
||||
assert_equal [1, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
|
||||
|
||||
assert_equal 1, ListMixin.find(1).pos
|
||||
assert_equal 2, ListMixin.find(3).pos
|
||||
assert_equal 3, ListMixin.find(4).pos
|
||||
|
||||
ListMixin.find(1).destroy
|
||||
|
||||
assert_equal [3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
|
||||
|
||||
assert_equal 1, ListMixin.find(3).pos
|
||||
assert_equal 2, ListMixin.find(4).pos
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,2 @@
|
||||
require File.dirname(__FILE__) + '/lib/acts_as_searchable'
|
||||
ActiveRecord::Base.send(:include, Redmine::Acts::Searchable)
|
||||
@@ -0,0 +1,122 @@
|
||||
# redMine - project management software
|
||||
# Copyright (C) 2006-2007 Jean-Philippe Lang
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
module Redmine
|
||||
module Acts
|
||||
module Searchable
|
||||
def self.included(base)
|
||||
base.extend ClassMethods
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
# Options:
|
||||
# * :columns - a column or an array of columns to search
|
||||
# * :project_key - project foreign key (default to project_id)
|
||||
# * :date_column - name of the datetime column (default to created_on)
|
||||
# * :sort_order - name of the column used to sort results (default to :date_column or created_on)
|
||||
# * :permission - permission required to search the model (default to :view_"objects")
|
||||
def acts_as_searchable(options = {})
|
||||
return if self.included_modules.include?(Redmine::Acts::Searchable::InstanceMethods)
|
||||
|
||||
cattr_accessor :searchable_options
|
||||
self.searchable_options = options
|
||||
|
||||
if searchable_options[:columns].nil?
|
||||
raise 'No searchable column defined.'
|
||||
elsif !searchable_options[:columns].is_a?(Array)
|
||||
searchable_options[:columns] = [] << searchable_options[:columns]
|
||||
end
|
||||
|
||||
searchable_options[:project_key] ||= "#{table_name}.project_id"
|
||||
searchable_options[:date_column] ||= "#{table_name}.created_on"
|
||||
searchable_options[:order_column] ||= searchable_options[:date_column]
|
||||
|
||||
# Permission needed to search this model
|
||||
searchable_options[:permission] = "view_#{self.name.underscore.pluralize}".to_sym unless searchable_options.has_key?(:permission)
|
||||
|
||||
# Should we search custom fields on this model ?
|
||||
searchable_options[:search_custom_fields] = !reflect_on_association(:custom_values).nil?
|
||||
|
||||
send :include, Redmine::Acts::Searchable::InstanceMethods
|
||||
end
|
||||
end
|
||||
|
||||
module InstanceMethods
|
||||
def self.included(base)
|
||||
base.extend ClassMethods
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
# Searches the model for the given tokens
|
||||
# projects argument can be either nil (will search all projects), a project or an array of projects
|
||||
# Returns the results and the results count
|
||||
def search(tokens, projects=nil, options={})
|
||||
tokens = [] << tokens unless tokens.is_a?(Array)
|
||||
projects = [] << projects unless projects.nil? || projects.is_a?(Array)
|
||||
|
||||
find_options = {:include => searchable_options[:include]}
|
||||
find_options[:order] = "#{searchable_options[:order_column]} " + (options[:before] ? 'DESC' : 'ASC')
|
||||
|
||||
limit_options = {}
|
||||
limit_options[:limit] = options[:limit] if options[:limit]
|
||||
if options[:offset]
|
||||
limit_options[:conditions] = "(#{searchable_options[:date_column]} " + (options[:before] ? '<' : '>') + "'#{connection.quoted_date(options[:offset])}')"
|
||||
end
|
||||
|
||||
columns = searchable_options[:columns]
|
||||
columns = columns[0..0] if options[:titles_only]
|
||||
|
||||
token_clauses = columns.collect {|column| "(LOWER(#{column}) LIKE ?)"}
|
||||
|
||||
if !options[:titles_only] && searchable_options[:search_custom_fields]
|
||||
searchable_custom_field_ids = CustomField.find(:all,
|
||||
:select => 'id',
|
||||
:conditions => { :type => "#{self.name}CustomField",
|
||||
:searchable => true }).collect(&:id)
|
||||
if searchable_custom_field_ids.any?
|
||||
custom_field_sql = "#{table_name}.id IN (SELECT customized_id FROM #{CustomValue.table_name}" +
|
||||
" WHERE customized_type='#{self.name}' AND customized_id=#{table_name}.id AND LOWER(value) LIKE ?" +
|
||||
" AND #{CustomValue.table_name}.custom_field_id IN (#{searchable_custom_field_ids.join(',')}))"
|
||||
token_clauses << custom_field_sql
|
||||
end
|
||||
end
|
||||
|
||||
sql = (['(' + token_clauses.join(' OR ') + ')'] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ')
|
||||
|
||||
find_options[:conditions] = [sql, * (tokens * token_clauses.size).sort]
|
||||
|
||||
project_conditions = []
|
||||
project_conditions << (searchable_options[:permission].nil? ? Project.visible_by(User.current) :
|
||||
Project.allowed_to_condition(User.current, searchable_options[:permission]))
|
||||
project_conditions << "#{searchable_options[:project_key]} IN (#{projects.collect(&:id).join(',')})" unless projects.nil?
|
||||
|
||||
results = []
|
||||
results_count = 0
|
||||
|
||||
with_scope(:find => {:conditions => project_conditions.join(' AND ')}) do
|
||||
with_scope(:find => find_options) do
|
||||
results_count = count(:all)
|
||||
results = find(:all, limit_options)
|
||||
end
|
||||
end
|
||||
[results, results_count]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,26 @@
|
||||
acts_as_tree
|
||||
============
|
||||
|
||||
Specify this +acts_as+ extension if you want to model a tree structure by providing a parent association and a children
|
||||
association. This requires that you have a foreign key column, which by default is called +parent_id+.
|
||||
|
||||
class Category < ActiveRecord::Base
|
||||
acts_as_tree :order => "name"
|
||||
end
|
||||
|
||||
Example:
|
||||
root
|
||||
\_ child1
|
||||
\_ subchild1
|
||||
\_ subchild2
|
||||
|
||||
root = Category.create("name" => "root")
|
||||
child1 = root.children.create("name" => "child1")
|
||||
subchild1 = child1.children.create("name" => "subchild1")
|
||||
|
||||
root.parent # => nil
|
||||
child1.parent # => root
|
||||
root.children # => [child1]
|
||||
root.children.first.children.first # => subchild1
|
||||
|
||||
Copyright (c) 2007 David Heinemeier Hansson, released under the MIT license
|
||||
@@ -0,0 +1,22 @@
|
||||
require 'rake'
|
||||
require 'rake/testtask'
|
||||
require 'rake/rdoctask'
|
||||
|
||||
desc 'Default: run unit tests.'
|
||||
task :default => :test
|
||||
|
||||
desc 'Test acts_as_tree plugin.'
|
||||
Rake::TestTask.new(:test) do |t|
|
||||
t.libs << 'lib'
|
||||
t.pattern = 'test/**/*_test.rb'
|
||||
t.verbose = true
|
||||
end
|
||||
|
||||
desc 'Generate documentation for acts_as_tree plugin.'
|
||||
Rake::RDocTask.new(:rdoc) do |rdoc|
|
||||
rdoc.rdoc_dir = 'rdoc'
|
||||
rdoc.title = 'acts_as_tree'
|
||||
rdoc.options << '--line-numbers' << '--inline-source'
|
||||
rdoc.rdoc_files.include('README')
|
||||
rdoc.rdoc_files.include('lib/**/*.rb')
|
||||
end
|
||||
@@ -0,0 +1 @@
|
||||
ActiveRecord::Base.send :include, ActiveRecord::Acts::Tree
|
||||
@@ -0,0 +1,103 @@
|
||||
module ActiveRecord
|
||||
module Acts
|
||||
module Tree
|
||||
def self.included(base)
|
||||
base.extend(ClassMethods)
|
||||
end
|
||||
|
||||
# Specify this +acts_as+ extension if you want to model a tree structure by providing a parent association and a children
|
||||
# association. This requires that you have a foreign key column, which by default is called +parent_id+.
|
||||
#
|
||||
# class Category < ActiveRecord::Base
|
||||
# acts_as_tree :order => "name"
|
||||
# end
|
||||
#
|
||||
# Example:
|
||||
# root
|
||||
# \_ child1
|
||||
# \_ subchild1
|
||||
# \_ subchild2
|
||||
#
|
||||
# root = Category.create("name" => "root")
|
||||
# child1 = root.children.create("name" => "child1")
|
||||
# subchild1 = child1.children.create("name" => "subchild1")
|
||||
#
|
||||
# root.parent # => nil
|
||||
# child1.parent # => root
|
||||
# root.children # => [child1]
|
||||
# root.children.first.children.first # => subchild1
|
||||
#
|
||||
# In addition to the parent and children associations, the following instance methods are added to the class
|
||||
# after calling <tt>acts_as_tree</tt>:
|
||||
# * <tt>siblings</tt> - Returns all the children of the parent, excluding the current node (<tt>[subchild2]</tt> when called on <tt>subchild1</tt>)
|
||||
# * <tt>self_and_siblings</tt> - Returns all the children of the parent, including the current node (<tt>[subchild1, subchild2]</tt> when called on <tt>subchild1</tt>)
|
||||
# * <tt>ancestors</tt> - Returns all the ancestors of the current node (<tt>[child1, root]</tt> when called on <tt>subchild2</tt>)
|
||||
# * <tt>root</tt> - Returns the root of the current node (<tt>root</tt> when called on <tt>subchild2</tt>)
|
||||
module ClassMethods
|
||||
# Configuration options are:
|
||||
#
|
||||
# * <tt>foreign_key</tt> - specifies the column name to use for tracking of the tree (default: +parent_id+)
|
||||
# * <tt>order</tt> - makes it possible to sort the children according to this SQL snippet.
|
||||
# * <tt>counter_cache</tt> - keeps a count in a +children_count+ column if set to +true+ (default: +false+).
|
||||
def acts_as_tree(options = {})
|
||||
configuration = { :foreign_key => "parent_id", :order => nil, :counter_cache => nil }
|
||||
configuration.update(options) if options.is_a?(Hash)
|
||||
|
||||
belongs_to :parent, :class_name => name, :foreign_key => configuration[:foreign_key], :counter_cache => configuration[:counter_cache]
|
||||
has_many :children, :class_name => name, :foreign_key => configuration[:foreign_key], :order => configuration[:order], :dependent => :destroy
|
||||
|
||||
class_eval <<-EOV
|
||||
include ActiveRecord::Acts::Tree::InstanceMethods
|
||||
|
||||
def self.roots
|
||||
find(:all, :conditions => "#{configuration[:foreign_key]} IS NULL", :order => #{configuration[:order].nil? ? "nil" : %Q{"#{configuration[:order]}"}})
|
||||
end
|
||||
|
||||
def self.root
|
||||
find(:first, :conditions => "#{configuration[:foreign_key]} IS NULL", :order => #{configuration[:order].nil? ? "nil" : %Q{"#{configuration[:order]}"}})
|
||||
end
|
||||
EOV
|
||||
end
|
||||
end
|
||||
|
||||
module InstanceMethods
|
||||
# Returns list of ancestors, starting from parent until root.
|
||||
#
|
||||
# subchild1.ancestors # => [child1, root]
|
||||
def ancestors
|
||||
node, nodes = self, []
|
||||
nodes << node = node.parent while node.parent
|
||||
nodes
|
||||
end
|
||||
|
||||
# Returns list of descendants.
|
||||
#
|
||||
# root.descendants # => [child1, subchild1, subchild2]
|
||||
def descendants
|
||||
children + children.collect(&:children).flatten
|
||||
end
|
||||
|
||||
# Returns the root node of the tree.
|
||||
def root
|
||||
node = self
|
||||
node = node.parent while node.parent
|
||||
node
|
||||
end
|
||||
|
||||
# Returns all siblings of the current node.
|
||||
#
|
||||
# subchild1.siblings # => [subchild2]
|
||||
def siblings
|
||||
self_and_siblings - [self]
|
||||
end
|
||||
|
||||
# Returns all siblings and a reference to the current node.
|
||||
#
|
||||
# subchild1.self_and_siblings # => [subchild1, subchild2]
|
||||
def self_and_siblings
|
||||
parent ? parent.children : self.class.roots
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,219 @@
|
||||
require 'test/unit'
|
||||
|
||||
require 'rubygems'
|
||||
require 'active_record'
|
||||
|
||||
$:.unshift File.dirname(__FILE__) + '/../lib'
|
||||
require File.dirname(__FILE__) + '/../init'
|
||||
|
||||
class Test::Unit::TestCase
|
||||
def assert_queries(num = 1)
|
||||
$query_count = 0
|
||||
yield
|
||||
ensure
|
||||
assert_equal num, $query_count, "#{$query_count} instead of #{num} queries were executed."
|
||||
end
|
||||
|
||||
def assert_no_queries(&block)
|
||||
assert_queries(0, &block)
|
||||
end
|
||||
end
|
||||
|
||||
ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:")
|
||||
|
||||
# AR keeps printing annoying schema statements
|
||||
$stdout = StringIO.new
|
||||
|
||||
def setup_db
|
||||
ActiveRecord::Base.logger
|
||||
ActiveRecord::Schema.define(:version => 1) do
|
||||
create_table :mixins do |t|
|
||||
t.column :type, :string
|
||||
t.column :parent_id, :integer
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def teardown_db
|
||||
ActiveRecord::Base.connection.tables.each do |table|
|
||||
ActiveRecord::Base.connection.drop_table(table)
|
||||
end
|
||||
end
|
||||
|
||||
class Mixin < ActiveRecord::Base
|
||||
end
|
||||
|
||||
class TreeMixin < Mixin
|
||||
acts_as_tree :foreign_key => "parent_id", :order => "id"
|
||||
end
|
||||
|
||||
class TreeMixinWithoutOrder < Mixin
|
||||
acts_as_tree :foreign_key => "parent_id"
|
||||
end
|
||||
|
||||
class RecursivelyCascadedTreeMixin < Mixin
|
||||
acts_as_tree :foreign_key => "parent_id"
|
||||
has_one :first_child, :class_name => 'RecursivelyCascadedTreeMixin', :foreign_key => :parent_id
|
||||
end
|
||||
|
||||
class TreeTest < Test::Unit::TestCase
|
||||
|
||||
def setup
|
||||
setup_db
|
||||
@root1 = TreeMixin.create!
|
||||
@root_child1 = TreeMixin.create! :parent_id => @root1.id
|
||||
@child1_child = TreeMixin.create! :parent_id => @root_child1.id
|
||||
@root_child2 = TreeMixin.create! :parent_id => @root1.id
|
||||
@root2 = TreeMixin.create!
|
||||
@root3 = TreeMixin.create!
|
||||
end
|
||||
|
||||
def teardown
|
||||
teardown_db
|
||||
end
|
||||
|
||||
def test_children
|
||||
assert_equal @root1.children, [@root_child1, @root_child2]
|
||||
assert_equal @root_child1.children, [@child1_child]
|
||||
assert_equal @child1_child.children, []
|
||||
assert_equal @root_child2.children, []
|
||||
end
|
||||
|
||||
def test_parent
|
||||
assert_equal @root_child1.parent, @root1
|
||||
assert_equal @root_child1.parent, @root_child2.parent
|
||||
assert_nil @root1.parent
|
||||
end
|
||||
|
||||
def test_delete
|
||||
assert_equal 6, TreeMixin.count
|
||||
@root1.destroy
|
||||
assert_equal 2, TreeMixin.count
|
||||
@root2.destroy
|
||||
@root3.destroy
|
||||
assert_equal 0, TreeMixin.count
|
||||
end
|
||||
|
||||
def test_insert
|
||||
@extra = @root1.children.create
|
||||
|
||||
assert @extra
|
||||
|
||||
assert_equal @extra.parent, @root1
|
||||
|
||||
assert_equal 3, @root1.children.size
|
||||
assert @root1.children.include?(@extra)
|
||||
assert @root1.children.include?(@root_child1)
|
||||
assert @root1.children.include?(@root_child2)
|
||||
end
|
||||
|
||||
def test_ancestors
|
||||
assert_equal [], @root1.ancestors
|
||||
assert_equal [@root1], @root_child1.ancestors
|
||||
assert_equal [@root_child1, @root1], @child1_child.ancestors
|
||||
assert_equal [@root1], @root_child2.ancestors
|
||||
assert_equal [], @root2.ancestors
|
||||
assert_equal [], @root3.ancestors
|
||||
end
|
||||
|
||||
def test_root
|
||||
assert_equal @root1, TreeMixin.root
|
||||
assert_equal @root1, @root1.root
|
||||
assert_equal @root1, @root_child1.root
|
||||
assert_equal @root1, @child1_child.root
|
||||
assert_equal @root1, @root_child2.root
|
||||
assert_equal @root2, @root2.root
|
||||
assert_equal @root3, @root3.root
|
||||
end
|
||||
|
||||
def test_roots
|
||||
assert_equal [@root1, @root2, @root3], TreeMixin.roots
|
||||
end
|
||||
|
||||
def test_siblings
|
||||
assert_equal [@root2, @root3], @root1.siblings
|
||||
assert_equal [@root_child2], @root_child1.siblings
|
||||
assert_equal [], @child1_child.siblings
|
||||
assert_equal [@root_child1], @root_child2.siblings
|
||||
assert_equal [@root1, @root3], @root2.siblings
|
||||
assert_equal [@root1, @root2], @root3.siblings
|
||||
end
|
||||
|
||||
def test_self_and_siblings
|
||||
assert_equal [@root1, @root2, @root3], @root1.self_and_siblings
|
||||
assert_equal [@root_child1, @root_child2], @root_child1.self_and_siblings
|
||||
assert_equal [@child1_child], @child1_child.self_and_siblings
|
||||
assert_equal [@root_child1, @root_child2], @root_child2.self_and_siblings
|
||||
assert_equal [@root1, @root2, @root3], @root2.self_and_siblings
|
||||
assert_equal [@root1, @root2, @root3], @root3.self_and_siblings
|
||||
end
|
||||
end
|
||||
|
||||
class TreeTestWithEagerLoading < Test::Unit::TestCase
|
||||
|
||||
def setup
|
||||
teardown_db
|
||||
setup_db
|
||||
@root1 = TreeMixin.create!
|
||||
@root_child1 = TreeMixin.create! :parent_id => @root1.id
|
||||
@child1_child = TreeMixin.create! :parent_id => @root_child1.id
|
||||
@root_child2 = TreeMixin.create! :parent_id => @root1.id
|
||||
@root2 = TreeMixin.create!
|
||||
@root3 = TreeMixin.create!
|
||||
|
||||
@rc1 = RecursivelyCascadedTreeMixin.create!
|
||||
@rc2 = RecursivelyCascadedTreeMixin.create! :parent_id => @rc1.id
|
||||
@rc3 = RecursivelyCascadedTreeMixin.create! :parent_id => @rc2.id
|
||||
@rc4 = RecursivelyCascadedTreeMixin.create! :parent_id => @rc3.id
|
||||
end
|
||||
|
||||
def teardown
|
||||
teardown_db
|
||||
end
|
||||
|
||||
def test_eager_association_loading
|
||||
roots = TreeMixin.find(:all, :include => :children, :conditions => "mixins.parent_id IS NULL", :order => "mixins.id")
|
||||
assert_equal [@root1, @root2, @root3], roots
|
||||
assert_no_queries do
|
||||
assert_equal 2, roots[0].children.size
|
||||
assert_equal 0, roots[1].children.size
|
||||
assert_equal 0, roots[2].children.size
|
||||
end
|
||||
end
|
||||
|
||||
def test_eager_association_loading_with_recursive_cascading_three_levels_has_many
|
||||
root_node = RecursivelyCascadedTreeMixin.find(:first, :include => { :children => { :children => :children } }, :order => 'mixins.id')
|
||||
assert_equal @rc4, assert_no_queries { root_node.children.first.children.first.children.first }
|
||||
end
|
||||
|
||||
def test_eager_association_loading_with_recursive_cascading_three_levels_has_one
|
||||
root_node = RecursivelyCascadedTreeMixin.find(:first, :include => { :first_child => { :first_child => :first_child } }, :order => 'mixins.id')
|
||||
assert_equal @rc4, assert_no_queries { root_node.first_child.first_child.first_child }
|
||||
end
|
||||
|
||||
def test_eager_association_loading_with_recursive_cascading_three_levels_belongs_to
|
||||
leaf_node = RecursivelyCascadedTreeMixin.find(:first, :include => { :parent => { :parent => :parent } }, :order => 'mixins.id DESC')
|
||||
assert_equal @rc1, assert_no_queries { leaf_node.parent.parent.parent }
|
||||
end
|
||||
end
|
||||
|
||||
class TreeTestWithoutOrder < Test::Unit::TestCase
|
||||
|
||||
def setup
|
||||
setup_db
|
||||
@root1 = TreeMixinWithoutOrder.create!
|
||||
@root2 = TreeMixinWithoutOrder.create!
|
||||
end
|
||||
|
||||
def teardown
|
||||
teardown_db
|
||||
end
|
||||
|
||||
def test_root
|
||||
assert [@root1, @root2].include?(TreeMixinWithoutOrder.root)
|
||||
end
|
||||
|
||||
def test_roots
|
||||
assert_equal [], [@root1, @root2] - TreeMixinWithoutOrder.roots
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,74 @@
|
||||
*SVN* (version numbers are overrated)
|
||||
|
||||
* (5 Oct 2006) Allow customization of #versions association options [Dan Peterson]
|
||||
|
||||
*0.5.1*
|
||||
|
||||
* (8 Aug 2006) Versioned models now belong to the unversioned model. @article_version.article.class => Article [Aslak Hellesoy]
|
||||
|
||||
*0.5* # do versions even matter for plugins?
|
||||
|
||||
* (21 Apr 2006) Added without_locking and without_revision methods.
|
||||
|
||||
Foo.without_revision do
|
||||
@foo.update_attributes ...
|
||||
end
|
||||
|
||||
*0.4*
|
||||
|
||||
* (28 March 2006) Rename non_versioned_fields to non_versioned_columns (old one is kept for compatibility).
|
||||
* (28 March 2006) Made explicit documentation note that string column names are required for non_versioned_columns.
|
||||
|
||||
*0.3.1*
|
||||
|
||||
* (7 Jan 2006) explicitly set :foreign_key option for the versioned model's belongs_to assocation for STI [Caged]
|
||||
* (7 Jan 2006) added tests to prove has_many :through joins work
|
||||
|
||||
*0.3*
|
||||
|
||||
* (2 Jan 2006) added ability to share a mixin with versioned class
|
||||
* (2 Jan 2006) changed the dynamic version model to MyModel::Version
|
||||
|
||||
*0.2.4*
|
||||
|
||||
* (27 Nov 2005) added note about possible destructive behavior of if_changed? [Michael Schuerig]
|
||||
|
||||
*0.2.3*
|
||||
|
||||
* (12 Nov 2005) fixed bug with old behavior of #blank? [Michael Schuerig]
|
||||
* (12 Nov 2005) updated tests to use ActiveRecord Schema
|
||||
|
||||
*0.2.2*
|
||||
|
||||
* (3 Nov 2005) added documentation note to #acts_as_versioned [Martin Jul]
|
||||
|
||||
*0.2.1*
|
||||
|
||||
* (6 Oct 2005) renamed dirty? to changed? to keep it uniform. it was aliased to keep it backwards compatible.
|
||||
|
||||
*0.2*
|
||||
|
||||
* (6 Oct 2005) added find_versions and find_version class methods.
|
||||
|
||||
* (6 Oct 2005) removed transaction from create_versioned_table().
|
||||
this way you can specify your own transaction around a group of operations.
|
||||
|
||||
* (30 Sep 2005) fixed bug where find_versions() would order by 'version' twice. (found by Joe Clark)
|
||||
|
||||
* (26 Sep 2005) added :sequence_name option to acts_as_versioned to set the sequence name on the versioned model
|
||||
|
||||
*0.1.3* (18 Sep 2005)
|
||||
|
||||
* First RubyForge release
|
||||
|
||||
*0.1.2*
|
||||
|
||||
* check if module is already included when acts_as_versioned is called
|
||||
|
||||
*0.1.1*
|
||||
|
||||
* Adding tests and rdocs
|
||||
|
||||
*0.1*
|
||||
|
||||
* Initial transfer from Rails ticket: http://dev.rubyonrails.com/ticket/1974
|
||||
@@ -0,0 +1,20 @@
|
||||
Copyright (c) 2005 Rick Olson
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,28 @@
|
||||
= acts_as_versioned
|
||||
|
||||
This library adds simple versioning to an ActiveRecord module. ActiveRecord is required.
|
||||
|
||||
== Resources
|
||||
|
||||
Install
|
||||
|
||||
* gem install acts_as_versioned
|
||||
|
||||
Rubyforge project
|
||||
|
||||
* http://rubyforge.org/projects/ar-versioned
|
||||
|
||||
RDocs
|
||||
|
||||
* http://ar-versioned.rubyforge.org
|
||||
|
||||
Subversion
|
||||
|
||||
* http://techno-weenie.net/svn/projects/acts_as_versioned
|
||||
|
||||
Collaboa
|
||||
|
||||
* http://collaboa.techno-weenie.net/repository/browse/acts_as_versioned
|
||||
|
||||
Special thanks to Dreamer on ##rubyonrails for help in early testing. His ServerSideWiki (http://serversidewiki.com)
|
||||
was the first project to use acts_as_versioned <em>in the wild</em>.
|
||||
@@ -0,0 +1,41 @@
|
||||
== Creating the test database
|
||||
|
||||
The default name for the test databases is "activerecord_versioned". If you
|
||||
want to use another database name then be sure to update the connection
|
||||
adapter setups you want to test with in test/connections/<your database>/connection.rb.
|
||||
When you have the database online, you can import the fixture tables with
|
||||
the test/fixtures/db_definitions/*.sql files.
|
||||
|
||||
Make sure that you create database objects with the same user that you specified in i
|
||||
connection.rb otherwise (on Postgres, at least) tests for default values will fail.
|
||||
|
||||
== Running with Rake
|
||||
|
||||
The easiest way to run the unit tests is through Rake. The default task runs
|
||||
the entire test suite for all the adapters. You can also run the suite on just
|
||||
one adapter by using the tasks test_mysql_ruby, test_ruby_mysql, test_sqlite,
|
||||
or test_postresql. For more information, checkout the full array of rake tasks with "rake -T"
|
||||
|
||||
Rake can be found at http://rake.rubyforge.org
|
||||
|
||||
== Running by hand
|
||||
|
||||
Unit tests are located in test directory. If you only want to run a single test suite,
|
||||
or don't want to bother with Rake, you can do so with something like:
|
||||
|
||||
cd test; ruby -I "connections/native_mysql" base_test.rb
|
||||
|
||||
That'll run the base suite using the MySQL-Ruby adapter. Change the adapter
|
||||
and test suite name as needed.
|
||||
|
||||
== Faster tests
|
||||
|
||||
If you are using a database that supports transactions, you can set the
|
||||
"AR_TX_FIXTURES" environment variable to "yes" to use transactional fixtures.
|
||||
This gives a very large speed boost. With rake:
|
||||
|
||||
rake AR_TX_FIXTURES=yes
|
||||
|
||||
Or, by hand:
|
||||
|
||||
AR_TX_FIXTURES=yes ruby -I connections/native_sqlite3 base_test.rb
|
||||
@@ -0,0 +1,182 @@
|
||||
require 'rubygems'
|
||||
|
||||
Gem::manage_gems
|
||||
|
||||
require 'rake/rdoctask'
|
||||
require 'rake/packagetask'
|
||||
require 'rake/gempackagetask'
|
||||
require 'rake/testtask'
|
||||
require 'rake/contrib/rubyforgepublisher'
|
||||
|
||||
PKG_NAME = 'acts_as_versioned'
|
||||
PKG_VERSION = '0.3.1'
|
||||
PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
|
||||
PROD_HOST = "technoweenie@bidwell.textdrive.com"
|
||||
RUBY_FORGE_PROJECT = 'ar-versioned'
|
||||
RUBY_FORGE_USER = 'technoweenie'
|
||||
|
||||
desc 'Default: run unit tests.'
|
||||
task :default => :test
|
||||
|
||||
desc 'Test the calculations plugin.'
|
||||
Rake::TestTask.new(:test) do |t|
|
||||
t.libs << 'lib'
|
||||
t.pattern = 'test/**/*_test.rb'
|
||||
t.verbose = true
|
||||
end
|
||||
|
||||
desc 'Generate documentation for the calculations plugin.'
|
||||
Rake::RDocTask.new(:rdoc) do |rdoc|
|
||||
rdoc.rdoc_dir = 'rdoc'
|
||||
rdoc.title = "#{PKG_NAME} -- Simple versioning with active record models"
|
||||
rdoc.options << '--line-numbers --inline-source'
|
||||
rdoc.rdoc_files.include('README', 'CHANGELOG', 'RUNNING_UNIT_TESTS')
|
||||
rdoc.rdoc_files.include('lib/**/*.rb')
|
||||
end
|
||||
|
||||
spec = Gem::Specification.new do |s|
|
||||
s.name = PKG_NAME
|
||||
s.version = PKG_VERSION
|
||||
s.platform = Gem::Platform::RUBY
|
||||
s.summary = "Simple versioning with active record models"
|
||||
s.files = FileList["{lib,test}/**/*"].to_a + %w(README MIT-LICENSE CHANGELOG RUNNING_UNIT_TESTS)
|
||||
s.files.delete "acts_as_versioned_plugin.sqlite.db"
|
||||
s.files.delete "acts_as_versioned_plugin.sqlite3.db"
|
||||
s.files.delete "test/debug.log"
|
||||
s.require_path = 'lib'
|
||||
s.autorequire = 'acts_as_versioned'
|
||||
s.has_rdoc = true
|
||||
s.test_files = Dir['test/**/*_test.rb']
|
||||
s.add_dependency 'activerecord', '>= 1.10.1'
|
||||
s.add_dependency 'activesupport', '>= 1.1.1'
|
||||
s.author = "Rick Olson"
|
||||
s.email = "technoweenie@gmail.com"
|
||||
s.homepage = "http://techno-weenie.net"
|
||||
end
|
||||
|
||||
Rake::GemPackageTask.new(spec) do |pkg|
|
||||
pkg.need_tar = true
|
||||
end
|
||||
|
||||
desc "Publish the API documentation"
|
||||
task :pdoc => [:rdoc] do
|
||||
Rake::RubyForgePublisher.new(RUBY_FORGE_PROJECT, RUBY_FORGE_USER).upload
|
||||
end
|
||||
|
||||
desc 'Publish the gem and API docs'
|
||||
task :publish => [:pdoc, :rubyforge_upload]
|
||||
|
||||
desc "Publish the release files to RubyForge."
|
||||
task :rubyforge_upload => :package do
|
||||
files = %w(gem tgz).map { |ext| "pkg/#{PKG_FILE_NAME}.#{ext}" }
|
||||
|
||||
if RUBY_FORGE_PROJECT then
|
||||
require 'net/http'
|
||||
require 'open-uri'
|
||||
|
||||
project_uri = "http://rubyforge.org/projects/#{RUBY_FORGE_PROJECT}/"
|
||||
project_data = open(project_uri) { |data| data.read }
|
||||
group_id = project_data[/[?&]group_id=(\d+)/, 1]
|
||||
raise "Couldn't get group id" unless group_id
|
||||
|
||||
# This echos password to shell which is a bit sucky
|
||||
if ENV["RUBY_FORGE_PASSWORD"]
|
||||
password = ENV["RUBY_FORGE_PASSWORD"]
|
||||
else
|
||||
print "#{RUBY_FORGE_USER}@rubyforge.org's password: "
|
||||
password = STDIN.gets.chomp
|
||||
end
|
||||
|
||||
login_response = Net::HTTP.start("rubyforge.org", 80) do |http|
|
||||
data = [
|
||||
"login=1",
|
||||
"form_loginname=#{RUBY_FORGE_USER}",
|
||||
"form_pw=#{password}"
|
||||
].join("&")
|
||||
http.post("/account/login.php", data)
|
||||
end
|
||||
|
||||
cookie = login_response["set-cookie"]
|
||||
raise "Login failed" unless cookie
|
||||
headers = { "Cookie" => cookie }
|
||||
|
||||
release_uri = "http://rubyforge.org/frs/admin/?group_id=#{group_id}"
|
||||
release_data = open(release_uri, headers) { |data| data.read }
|
||||
package_id = release_data[/[?&]package_id=(\d+)/, 1]
|
||||
raise "Couldn't get package id" unless package_id
|
||||
|
||||
first_file = true
|
||||
release_id = ""
|
||||
|
||||
files.each do |filename|
|
||||
basename = File.basename(filename)
|
||||
file_ext = File.extname(filename)
|
||||
file_data = File.open(filename, "rb") { |file| file.read }
|
||||
|
||||
puts "Releasing #{basename}..."
|
||||
|
||||
release_response = Net::HTTP.start("rubyforge.org", 80) do |http|
|
||||
release_date = Time.now.strftime("%Y-%m-%d %H:%M")
|
||||
type_map = {
|
||||
".zip" => "3000",
|
||||
".tgz" => "3110",
|
||||
".gz" => "3110",
|
||||
".gem" => "1400"
|
||||
}; type_map.default = "9999"
|
||||
type = type_map[file_ext]
|
||||
boundary = "rubyqMY6QN9bp6e4kS21H4y0zxcvoor"
|
||||
|
||||
query_hash = if first_file then
|
||||
{
|
||||
"group_id" => group_id,
|
||||
"package_id" => package_id,
|
||||
"release_name" => PKG_FILE_NAME,
|
||||
"release_date" => release_date,
|
||||
"type_id" => type,
|
||||
"processor_id" => "8000", # Any
|
||||
"release_notes" => "",
|
||||
"release_changes" => "",
|
||||
"preformatted" => "1",
|
||||
"submit" => "1"
|
||||
}
|
||||
else
|
||||
{
|
||||
"group_id" => group_id,
|
||||
"release_id" => release_id,
|
||||
"package_id" => package_id,
|
||||
"step2" => "1",
|
||||
"type_id" => type,
|
||||
"processor_id" => "8000", # Any
|
||||
"submit" => "Add This File"
|
||||
}
|
||||
end
|
||||
|
||||
query = "?" + query_hash.map do |(name, value)|
|
||||
[name, URI.encode(value)].join("=")
|
||||
end.join("&")
|
||||
|
||||
data = [
|
||||
"--" + boundary,
|
||||
"Content-Disposition: form-data; name=\"userfile\"; filename=\"#{basename}\"",
|
||||
"Content-Type: application/octet-stream",
|
||||
"Content-Transfer-Encoding: binary",
|
||||
"", file_data, ""
|
||||
].join("\x0D\x0A")
|
||||
|
||||
release_headers = headers.merge(
|
||||
"Content-Type" => "multipart/form-data; boundary=#{boundary}"
|
||||
)
|
||||
|
||||
target = first_file ? "/frs/admin/qrs.php" : "/frs/admin/editrelease.php"
|
||||
http.post(target + query, data, release_headers)
|
||||
end
|
||||
|
||||
if first_file then
|
||||
release_id = release_response.body[/release_id=(\d+)/, 1]
|
||||
raise("Couldn't get release id") unless release_id
|
||||
end
|
||||
|
||||
first_file = false
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1 @@
|
||||
require 'acts_as_versioned'
|
||||
@@ -0,0 +1,564 @@
|
||||
# Copyright (c) 2005 Rick Olson
|
||||
#
|
||||
# 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.
|
||||
|
||||
module ActiveRecord #:nodoc:
|
||||
module Acts #:nodoc:
|
||||
# Specify this act if you want to save a copy of the row in a versioned table. This assumes there is a
|
||||
# versioned table ready and that your model has a version field. This works with optimistic locking if the lock_version
|
||||
# column is present as well.
|
||||
#
|
||||
# The class for the versioned model is derived the first time it is seen. Therefore, if you change your database schema you have to restart
|
||||
# your container for the changes to be reflected. In development mode this usually means restarting WEBrick.
|
||||
#
|
||||
# class Page < ActiveRecord::Base
|
||||
# # assumes pages_versions table
|
||||
# acts_as_versioned
|
||||
# end
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# page = Page.create(:title => 'hello world!')
|
||||
# page.version # => 1
|
||||
#
|
||||
# page.title = 'hello world'
|
||||
# page.save
|
||||
# page.version # => 2
|
||||
# page.versions.size # => 2
|
||||
#
|
||||
# page.revert_to(1) # using version number
|
||||
# page.title # => 'hello world!'
|
||||
#
|
||||
# page.revert_to(page.versions.last) # using versioned instance
|
||||
# page.title # => 'hello world'
|
||||
#
|
||||
# page.versions.earliest # efficient query to find the first version
|
||||
# page.versions.latest # efficient query to find the most recently created version
|
||||
#
|
||||
#
|
||||
# Simple Queries to page between versions
|
||||
#
|
||||
# page.versions.before(version)
|
||||
# page.versions.after(version)
|
||||
#
|
||||
# Access the previous/next versions from the versioned model itself
|
||||
#
|
||||
# version = page.versions.latest
|
||||
# version.previous # go back one version
|
||||
# version.next # go forward one version
|
||||
#
|
||||
# See ActiveRecord::Acts::Versioned::ClassMethods#acts_as_versioned for configuration options
|
||||
module Versioned
|
||||
CALLBACKS = [:set_new_version, :save_version_on_create, :save_version?, :clear_altered_attributes]
|
||||
def self.included(base) # :nodoc:
|
||||
base.extend ClassMethods
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
# == Configuration options
|
||||
#
|
||||
# * <tt>class_name</tt> - versioned model class name (default: PageVersion in the above example)
|
||||
# * <tt>table_name</tt> - versioned model table name (default: page_versions in the above example)
|
||||
# * <tt>foreign_key</tt> - foreign key used to relate the versioned model to the original model (default: page_id in the above example)
|
||||
# * <tt>inheritance_column</tt> - name of the column to save the model's inheritance_column value for STI. (default: versioned_type)
|
||||
# * <tt>version_column</tt> - name of the column in the model that keeps the version number (default: version)
|
||||
# * <tt>sequence_name</tt> - name of the custom sequence to be used by the versioned model.
|
||||
# * <tt>limit</tt> - number of revisions to keep, defaults to unlimited
|
||||
# * <tt>if</tt> - symbol of method to check before saving a new version. If this method returns false, a new version is not saved.
|
||||
# For finer control, pass either a Proc or modify Model#version_condition_met?
|
||||
#
|
||||
# acts_as_versioned :if => Proc.new { |auction| !auction.expired? }
|
||||
#
|
||||
# or...
|
||||
#
|
||||
# class Auction
|
||||
# def version_condition_met? # totally bypasses the <tt>:if</tt> option
|
||||
# !expired?
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# * <tt>if_changed</tt> - Simple way of specifying attributes that are required to be changed before saving a model. This takes
|
||||
# either a symbol or array of symbols. WARNING - This will attempt to overwrite any attribute setters you may have.
|
||||
# Use this instead if you want to write your own attribute setters (and ignore if_changed):
|
||||
#
|
||||
# def name=(new_name)
|
||||
# write_changed_attribute :name, new_name
|
||||
# end
|
||||
#
|
||||
# * <tt>extend</tt> - Lets you specify a module to be mixed in both the original and versioned models. You can also just pass a block
|
||||
# to create an anonymous mixin:
|
||||
#
|
||||
# class Auction
|
||||
# acts_as_versioned do
|
||||
# def started?
|
||||
# !started_at.nil?
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# or...
|
||||
#
|
||||
# module AuctionExtension
|
||||
# def started?
|
||||
# !started_at.nil?
|
||||
# end
|
||||
# end
|
||||
# class Auction
|
||||
# acts_as_versioned :extend => AuctionExtension
|
||||
# end
|
||||
#
|
||||
# Example code:
|
||||
#
|
||||
# @auction = Auction.find(1)
|
||||
# @auction.started?
|
||||
# @auction.versions.first.started?
|
||||
#
|
||||
# == Database Schema
|
||||
#
|
||||
# The model that you're versioning needs to have a 'version' attribute. The model is versioned
|
||||
# into a table called #{model}_versions where the model name is singlular. The _versions table should
|
||||
# contain all the fields you want versioned, the same version column, and a #{model}_id foreign key field.
|
||||
#
|
||||
# A lock_version field is also accepted if your model uses Optimistic Locking. If your table uses Single Table inheritance,
|
||||
# then that field is reflected in the versioned model as 'versioned_type' by default.
|
||||
#
|
||||
# Acts_as_versioned comes prepared with the ActiveRecord::Acts::Versioned::ActMethods::ClassMethods#create_versioned_table
|
||||
# method, perfect for a migration. It will also create the version column if the main model does not already have it.
|
||||
#
|
||||
# class AddVersions < ActiveRecord::Migration
|
||||
# def self.up
|
||||
# # create_versioned_table takes the same options hash
|
||||
# # that create_table does
|
||||
# Post.create_versioned_table
|
||||
# end
|
||||
#
|
||||
# def self.down
|
||||
# Post.drop_versioned_table
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# == Changing What Fields Are Versioned
|
||||
#
|
||||
# By default, acts_as_versioned will version all but these fields:
|
||||
#
|
||||
# [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
|
||||
#
|
||||
# You can add or change those by modifying #non_versioned_columns. Note that this takes strings and not symbols.
|
||||
#
|
||||
# class Post < ActiveRecord::Base
|
||||
# acts_as_versioned
|
||||
# self.non_versioned_columns << 'comments_count'
|
||||
# end
|
||||
#
|
||||
def acts_as_versioned(options = {}, &extension)
|
||||
# don't allow multiple calls
|
||||
return if self.included_modules.include?(ActiveRecord::Acts::Versioned::ActMethods)
|
||||
|
||||
send :include, ActiveRecord::Acts::Versioned::ActMethods
|
||||
|
||||
cattr_accessor :versioned_class_name, :versioned_foreign_key, :versioned_table_name, :versioned_inheritance_column,
|
||||
:version_column, :max_version_limit, :track_altered_attributes, :version_condition, :version_sequence_name, :non_versioned_columns,
|
||||
:version_association_options
|
||||
|
||||
# legacy
|
||||
alias_method :non_versioned_fields, :non_versioned_columns
|
||||
alias_method :non_versioned_fields=, :non_versioned_columns=
|
||||
|
||||
class << self
|
||||
alias_method :non_versioned_fields, :non_versioned_columns
|
||||
alias_method :non_versioned_fields=, :non_versioned_columns=
|
||||
end
|
||||
|
||||
send :attr_accessor, :altered_attributes
|
||||
|
||||
self.versioned_class_name = options[:class_name] || "Version"
|
||||
self.versioned_foreign_key = options[:foreign_key] || self.to_s.foreign_key
|
||||
self.versioned_table_name = options[:table_name] || "#{table_name_prefix}#{base_class.name.demodulize.underscore}_versions#{table_name_suffix}"
|
||||
self.versioned_inheritance_column = options[:inheritance_column] || "versioned_#{inheritance_column}"
|
||||
self.version_column = options[:version_column] || 'version'
|
||||
self.version_sequence_name = options[:sequence_name]
|
||||
self.max_version_limit = options[:limit].to_i
|
||||
self.version_condition = options[:if] || true
|
||||
self.non_versioned_columns = [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
|
||||
self.version_association_options = {
|
||||
:class_name => "#{self.to_s}::#{versioned_class_name}",
|
||||
:foreign_key => versioned_foreign_key,
|
||||
:dependent => :delete_all
|
||||
}.merge(options[:association_options] || {})
|
||||
|
||||
if block_given?
|
||||
extension_module_name = "#{versioned_class_name}Extension"
|
||||
silence_warnings do
|
||||
self.const_set(extension_module_name, Module.new(&extension))
|
||||
end
|
||||
|
||||
options[:extend] = self.const_get(extension_module_name)
|
||||
end
|
||||
|
||||
class_eval do
|
||||
has_many :versions, version_association_options do
|
||||
# finds earliest version of this record
|
||||
def earliest
|
||||
@earliest ||= find(:first, :order => 'version')
|
||||
end
|
||||
|
||||
# find latest version of this record
|
||||
def latest
|
||||
@latest ||= find(:first, :order => 'version desc')
|
||||
end
|
||||
end
|
||||
before_save :set_new_version
|
||||
after_create :save_version_on_create
|
||||
after_update :save_version
|
||||
after_save :clear_old_versions
|
||||
after_save :clear_altered_attributes
|
||||
|
||||
unless options[:if_changed].nil?
|
||||
self.track_altered_attributes = true
|
||||
options[:if_changed] = [options[:if_changed]] unless options[:if_changed].is_a?(Array)
|
||||
options[:if_changed].each do |attr_name|
|
||||
define_method("#{attr_name}=") do |value|
|
||||
write_changed_attribute attr_name, value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
include options[:extend] if options[:extend].is_a?(Module)
|
||||
end
|
||||
|
||||
# create the dynamic versioned model
|
||||
const_set(versioned_class_name, Class.new(ActiveRecord::Base)).class_eval do
|
||||
def self.reloadable? ; false ; end
|
||||
# find first version before the given version
|
||||
def self.before(version)
|
||||
find :first, :order => 'version desc',
|
||||
:conditions => ["#{original_class.versioned_foreign_key} = ? and version < ?", version.send(original_class.versioned_foreign_key), version.version]
|
||||
end
|
||||
|
||||
# find first version after the given version.
|
||||
def self.after(version)
|
||||
find :first, :order => 'version',
|
||||
:conditions => ["#{original_class.versioned_foreign_key} = ? and version > ?", version.send(original_class.versioned_foreign_key), version.version]
|
||||
end
|
||||
|
||||
def previous
|
||||
self.class.before(self)
|
||||
end
|
||||
|
||||
def next
|
||||
self.class.after(self)
|
||||
end
|
||||
|
||||
def versions_count
|
||||
page.version
|
||||
end
|
||||
end
|
||||
|
||||
versioned_class.cattr_accessor :original_class
|
||||
versioned_class.original_class = self
|
||||
versioned_class.set_table_name versioned_table_name
|
||||
versioned_class.belongs_to self.to_s.demodulize.underscore.to_sym,
|
||||
:class_name => "::#{self.to_s}",
|
||||
:foreign_key => versioned_foreign_key
|
||||
versioned_class.send :include, options[:extend] if options[:extend].is_a?(Module)
|
||||
versioned_class.set_sequence_name version_sequence_name if version_sequence_name
|
||||
end
|
||||
end
|
||||
|
||||
module ActMethods
|
||||
def self.included(base) # :nodoc:
|
||||
base.extend ClassMethods
|
||||
end
|
||||
|
||||
# Finds a specific version of this record
|
||||
def find_version(version = nil)
|
||||
self.class.find_version(id, version)
|
||||
end
|
||||
|
||||
# Saves a version of the model if applicable
|
||||
def save_version
|
||||
save_version_on_create if save_version?
|
||||
end
|
||||
|
||||
# Saves a version of the model in the versioned table. This is called in the after_save callback by default
|
||||
def save_version_on_create
|
||||
rev = self.class.versioned_class.new
|
||||
self.clone_versioned_model(self, rev)
|
||||
rev.version = send(self.class.version_column)
|
||||
rev.send("#{self.class.versioned_foreign_key}=", self.id)
|
||||
rev.save
|
||||
end
|
||||
|
||||
# Clears old revisions if a limit is set with the :limit option in <tt>acts_as_versioned</tt>.
|
||||
# Override this method to set your own criteria for clearing old versions.
|
||||
def clear_old_versions
|
||||
return if self.class.max_version_limit == 0
|
||||
excess_baggage = send(self.class.version_column).to_i - self.class.max_version_limit
|
||||
if excess_baggage > 0
|
||||
sql = "DELETE FROM #{self.class.versioned_table_name} WHERE version <= #{excess_baggage} AND #{self.class.versioned_foreign_key} = #{self.id}"
|
||||
self.class.versioned_class.connection.execute sql
|
||||
end
|
||||
end
|
||||
|
||||
def versions_count
|
||||
version
|
||||
end
|
||||
|
||||
# Reverts a model to a given version. Takes either a version number or an instance of the versioned model
|
||||
def revert_to(version)
|
||||
if version.is_a?(self.class.versioned_class)
|
||||
return false unless version.send(self.class.versioned_foreign_key) == self.id and !version.new_record?
|
||||
else
|
||||
return false unless version = versions.find_by_version(version)
|
||||
end
|
||||
self.clone_versioned_model(version, self)
|
||||
self.send("#{self.class.version_column}=", version.version)
|
||||
true
|
||||
end
|
||||
|
||||
# Reverts a model to a given version and saves the model.
|
||||
# Takes either a version number or an instance of the versioned model
|
||||
def revert_to!(version)
|
||||
revert_to(version) ? save_without_revision : false
|
||||
end
|
||||
|
||||
# Temporarily turns off Optimistic Locking while saving. Used when reverting so that a new version is not created.
|
||||
def save_without_revision
|
||||
save_without_revision!
|
||||
true
|
||||
rescue
|
||||
false
|
||||
end
|
||||
|
||||
def save_without_revision!
|
||||
without_locking do
|
||||
without_revision do
|
||||
save!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Returns an array of attribute keys that are versioned. See non_versioned_columns
|
||||
def versioned_attributes
|
||||
self.attributes.keys.select { |k| !self.class.non_versioned_columns.include?(k) }
|
||||
end
|
||||
|
||||
# If called with no parameters, gets whether the current model has changed and needs to be versioned.
|
||||
# If called with a single parameter, gets whether the parameter has changed.
|
||||
def changed?(attr_name = nil)
|
||||
attr_name.nil? ?
|
||||
(!self.class.track_altered_attributes || (altered_attributes && altered_attributes.length > 0)) :
|
||||
(altered_attributes && altered_attributes.include?(attr_name.to_s))
|
||||
end
|
||||
|
||||
# keep old dirty? method
|
||||
alias_method :dirty?, :changed?
|
||||
|
||||
# Clones a model. Used when saving a new version or reverting a model's version.
|
||||
def clone_versioned_model(orig_model, new_model)
|
||||
self.versioned_attributes.each do |key|
|
||||
new_model.send("#{key}=", orig_model.send(key)) if orig_model.has_attribute?(key)
|
||||
end
|
||||
|
||||
if orig_model.is_a?(self.class.versioned_class)
|
||||
new_model[new_model.class.inheritance_column] = orig_model[self.class.versioned_inheritance_column]
|
||||
elsif new_model.is_a?(self.class.versioned_class)
|
||||
new_model[self.class.versioned_inheritance_column] = orig_model[orig_model.class.inheritance_column]
|
||||
end
|
||||
end
|
||||
|
||||
# Checks whether a new version shall be saved or not. Calls <tt>version_condition_met?</tt> and <tt>changed?</tt>.
|
||||
def save_version?
|
||||
version_condition_met? && changed?
|
||||
end
|
||||
|
||||
# Checks condition set in the :if option to check whether a revision should be created or not. Override this for
|
||||
# custom version condition checking.
|
||||
def version_condition_met?
|
||||
case
|
||||
when version_condition.is_a?(Symbol)
|
||||
send(version_condition)
|
||||
when version_condition.respond_to?(:call) && (version_condition.arity == 1 || version_condition.arity == -1)
|
||||
version_condition.call(self)
|
||||
else
|
||||
version_condition
|
||||
end
|
||||
end
|
||||
|
||||
# Executes the block with the versioning callbacks disabled.
|
||||
#
|
||||
# @foo.without_revision do
|
||||
# @foo.save
|
||||
# end
|
||||
#
|
||||
def without_revision(&block)
|
||||
self.class.without_revision(&block)
|
||||
end
|
||||
|
||||
# Turns off optimistic locking for the duration of the block
|
||||
#
|
||||
# @foo.without_locking do
|
||||
# @foo.save
|
||||
# end
|
||||
#
|
||||
def without_locking(&block)
|
||||
self.class.without_locking(&block)
|
||||
end
|
||||
|
||||
def empty_callback() end #:nodoc:
|
||||
|
||||
protected
|
||||
# sets the new version before saving, unless you're using optimistic locking. In that case, let it take care of the version.
|
||||
def set_new_version
|
||||
self.send("#{self.class.version_column}=", self.next_version) if new_record? || (!locking_enabled? && save_version?)
|
||||
end
|
||||
|
||||
# Gets the next available version for the current record, or 1 for a new record
|
||||
def next_version
|
||||
return 1 if new_record?
|
||||
(versions.calculate(:max, :version) || 0) + 1
|
||||
end
|
||||
|
||||
# clears current changed attributes. Called after save.
|
||||
def clear_altered_attributes
|
||||
self.altered_attributes = []
|
||||
end
|
||||
|
||||
def write_changed_attribute(attr_name, attr_value)
|
||||
# Convert to db type for comparison. Avoids failing Float<=>String comparisons.
|
||||
attr_value_for_db = self.class.columns_hash[attr_name.to_s].type_cast(attr_value)
|
||||
(self.altered_attributes ||= []) << attr_name.to_s unless self.changed?(attr_name) || self.send(attr_name) == attr_value_for_db
|
||||
write_attribute(attr_name, attr_value_for_db)
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
# Finds a specific version of a specific row of this model
|
||||
def find_version(id, version = nil)
|
||||
return find(id) unless version
|
||||
|
||||
conditions = ["#{versioned_foreign_key} = ? AND version = ?", id, version]
|
||||
options = { :conditions => conditions, :limit => 1 }
|
||||
|
||||
if result = find_versions(id, options).first
|
||||
result
|
||||
else
|
||||
raise RecordNotFound, "Couldn't find #{name} with ID=#{id} and VERSION=#{version}"
|
||||
end
|
||||
end
|
||||
|
||||
# Finds versions of a specific model. Takes an options hash like <tt>find</tt>
|
||||
def find_versions(id, options = {})
|
||||
versioned_class.find :all, {
|
||||
:conditions => ["#{versioned_foreign_key} = ?", id],
|
||||
:order => 'version' }.merge(options)
|
||||
end
|
||||
|
||||
# Returns an array of columns that are versioned. See non_versioned_columns
|
||||
def versioned_columns
|
||||
self.columns.select { |c| !non_versioned_columns.include?(c.name) }
|
||||
end
|
||||
|
||||
# Returns an instance of the dynamic versioned model
|
||||
def versioned_class
|
||||
const_get versioned_class_name
|
||||
end
|
||||
|
||||
# Rake migration task to create the versioned table using options passed to acts_as_versioned
|
||||
def create_versioned_table(create_table_options = {})
|
||||
# create version column in main table if it does not exist
|
||||
if !self.content_columns.find { |c| %w(version lock_version).include? c.name }
|
||||
self.connection.add_column table_name, :version, :integer
|
||||
end
|
||||
|
||||
self.connection.create_table(versioned_table_name, create_table_options) do |t|
|
||||
t.column versioned_foreign_key, :integer
|
||||
t.column :version, :integer
|
||||
end
|
||||
|
||||
updated_col = nil
|
||||
self.versioned_columns.each do |col|
|
||||
updated_col = col if !updated_col && %(updated_at updated_on).include?(col.name)
|
||||
self.connection.add_column versioned_table_name, col.name, col.type,
|
||||
:limit => col.limit,
|
||||
:default => col.default,
|
||||
:scale => col.scale,
|
||||
:precision => col.precision
|
||||
end
|
||||
|
||||
if type_col = self.columns_hash[inheritance_column]
|
||||
self.connection.add_column versioned_table_name, versioned_inheritance_column, type_col.type,
|
||||
:limit => type_col.limit,
|
||||
:default => type_col.default,
|
||||
:scale => type_col.scale,
|
||||
:precision => type_col.precision
|
||||
end
|
||||
|
||||
if updated_col.nil?
|
||||
self.connection.add_column versioned_table_name, :updated_at, :timestamp
|
||||
end
|
||||
end
|
||||
|
||||
# Rake migration task to drop the versioned table
|
||||
def drop_versioned_table
|
||||
self.connection.drop_table versioned_table_name
|
||||
end
|
||||
|
||||
# Executes the block with the versioning callbacks disabled.
|
||||
#
|
||||
# Foo.without_revision do
|
||||
# @foo.save
|
||||
# end
|
||||
#
|
||||
def without_revision(&block)
|
||||
class_eval do
|
||||
CALLBACKS.each do |attr_name|
|
||||
alias_method "orig_#{attr_name}".to_sym, attr_name
|
||||
alias_method attr_name, :empty_callback
|
||||
end
|
||||
end
|
||||
block.call
|
||||
ensure
|
||||
class_eval do
|
||||
CALLBACKS.each do |attr_name|
|
||||
alias_method attr_name, "orig_#{attr_name}".to_sym
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Turns off optimistic locking for the duration of the block
|
||||
#
|
||||
# Foo.without_locking do
|
||||
# @foo.save
|
||||
# end
|
||||
#
|
||||
def without_locking(&block)
|
||||
current = ActiveRecord::Base.lock_optimistically
|
||||
ActiveRecord::Base.lock_optimistically = false if current
|
||||
result = block.call
|
||||
ActiveRecord::Base.lock_optimistically = true if current
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
ActiveRecord::Base.send :include, ActiveRecord::Acts::Versioned
|
||||
@@ -0,0 +1,41 @@
|
||||
$:.unshift(File.dirname(__FILE__) + '/../../../rails/activesupport/lib')
|
||||
$:.unshift(File.dirname(__FILE__) + '/../../../rails/activerecord/lib')
|
||||
$:.unshift(File.dirname(__FILE__) + '/../lib')
|
||||
require 'test/unit'
|
||||
begin
|
||||
require 'active_support'
|
||||
require 'active_record'
|
||||
require 'active_record/fixtures'
|
||||
rescue LoadError
|
||||
require 'rubygems'
|
||||
retry
|
||||
end
|
||||
require 'acts_as_versioned'
|
||||
|
||||
config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
|
||||
ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
|
||||
ActiveRecord::Base.configurations = {'test' => config[ENV['DB'] || 'sqlite3']}
|
||||
ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test'])
|
||||
|
||||
load(File.dirname(__FILE__) + "/schema.rb")
|
||||
|
||||
# set up custom sequence on widget_versions for DBs that support sequences
|
||||
if ENV['DB'] == 'postgresql'
|
||||
ActiveRecord::Base.connection.execute "DROP SEQUENCE widgets_seq;" rescue nil
|
||||
ActiveRecord::Base.connection.remove_column :widget_versions, :id
|
||||
ActiveRecord::Base.connection.execute "CREATE SEQUENCE widgets_seq START 101;"
|
||||
ActiveRecord::Base.connection.execute "ALTER TABLE widget_versions ADD COLUMN id INTEGER PRIMARY KEY DEFAULT nextval('widgets_seq');"
|
||||
end
|
||||
|
||||
Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/"
|
||||
$:.unshift(Test::Unit::TestCase.fixture_path)
|
||||
|
||||
class Test::Unit::TestCase #:nodoc:
|
||||
# Turn off transactional fixtures if you're working with MyISAM tables in MySQL
|
||||
self.use_transactional_fixtures = true
|
||||
|
||||
# Instantiated fixtures are slow, but give you @david where you otherwise would need people(:david)
|
||||
self.use_instantiated_fixtures = false
|
||||
|
||||
# Add more helper methods to be used by all tests here...
|
||||
end
|
||||
@@ -0,0 +1,18 @@
|
||||
sqlite:
|
||||
:adapter: sqlite
|
||||
:dbfile: acts_as_versioned_plugin.sqlite.db
|
||||
sqlite3:
|
||||
:adapter: sqlite3
|
||||
:dbfile: acts_as_versioned_plugin.sqlite3.db
|
||||
postgresql:
|
||||
:adapter: postgresql
|
||||
:username: postgres
|
||||
:password: postgres
|
||||
:database: acts_as_versioned_plugin_test
|
||||
:min_messages: ERROR
|
||||
mysql:
|
||||
:adapter: mysql
|
||||
:host: localhost
|
||||
:username: rails
|
||||
:password:
|
||||
:database: acts_as_versioned_plugin_test
|
||||
@@ -0,0 +1,6 @@
|
||||
caged:
|
||||
id: 1
|
||||
name: caged
|
||||
mly:
|
||||
id: 2
|
||||
name: mly
|
||||
@@ -0,0 +1,3 @@
|
||||
class Landmark < ActiveRecord::Base
|
||||
acts_as_versioned :if_changed => [ :name, :longitude, :latitude ]
|
||||
end
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
washington:
|
||||
id: 1
|
||||
landmark_id: 1
|
||||
version: 1
|
||||
name: Washington, D.C.
|
||||
latitude: 38.895
|
||||
longitude: -77.036667
|
||||
@@ -0,0 +1,6 @@
|
||||
washington:
|
||||
id: 1
|
||||
name: Washington, D.C.
|
||||
latitude: 38.895
|
||||
longitude: -77.036667
|
||||
version: 1
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
welcome:
|
||||
id: 1
|
||||
title: Welcome to the weblog
|
||||
lock_version: 24
|
||||
type: LockedPage
|
||||
thinking:
|
||||
id: 2
|
||||
title: So I was thinking
|
||||
lock_version: 24
|
||||
type: SpecialLockedPage
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
welcome_1:
|
||||
id: 1
|
||||
page_id: 1
|
||||
title: Welcome to the weblg
|
||||
version: 23
|
||||
version_type: LockedPage
|
||||
|
||||
welcome_2:
|
||||
id: 2
|
||||
page_id: 1
|
||||
title: Welcome to the weblog
|
||||
version: 24
|
||||
version_type: LockedPage
|
||||
|
||||
thinking_1:
|
||||
id: 3
|
||||
page_id: 2
|
||||
title: So I was thinking!!!
|
||||
version: 23
|
||||
version_type: SpecialLockedPage
|
||||
|
||||
thinking_2:
|
||||
id: 4
|
||||
page_id: 2
|
||||
title: So I was thinking
|
||||
version: 24
|
||||
version_type: SpecialLockedPage
|
||||
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
class AddVersionedTables < ActiveRecord::Migration
|
||||
def self.up
|
||||
create_table("things") do |t|
|
||||
t.column :title, :text
|
||||
end
|
||||
Thing.create_versioned_table
|
||||
end
|
||||
|
||||
def self.down
|
||||
Thing.drop_versioned_table
|
||||
drop_table "things" rescue nil
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,43 @@
|
||||
class Page < ActiveRecord::Base
|
||||
belongs_to :author
|
||||
has_many :authors, :through => :versions, :order => 'name'
|
||||
belongs_to :revisor, :class_name => 'Author'
|
||||
has_many :revisors, :class_name => 'Author', :through => :versions, :order => 'name'
|
||||
acts_as_versioned :if => :feeling_good? do
|
||||
def self.included(base)
|
||||
base.cattr_accessor :feeling_good
|
||||
base.feeling_good = true
|
||||
base.belongs_to :author
|
||||
base.belongs_to :revisor, :class_name => 'Author'
|
||||
end
|
||||
|
||||
def feeling_good?
|
||||
@@feeling_good == true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module LockedPageExtension
|
||||
def hello_world
|
||||
'hello_world'
|
||||
end
|
||||
end
|
||||
|
||||
class LockedPage < ActiveRecord::Base
|
||||
acts_as_versioned \
|
||||
:inheritance_column => :version_type,
|
||||
:foreign_key => :page_id,
|
||||
:table_name => :locked_pages_revisions,
|
||||
:class_name => 'LockedPageRevision',
|
||||
:version_column => :lock_version,
|
||||
:limit => 2,
|
||||
:if_changed => :title,
|
||||
:extend => LockedPageExtension
|
||||
end
|
||||
|
||||
class SpecialLockedPage < LockedPage
|
||||
end
|
||||
|
||||
class Author < ActiveRecord::Base
|
||||
has_many :pages
|
||||
end
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
welcome_2:
|
||||
id: 1
|
||||
page_id: 1
|
||||
title: Welcome to the weblog
|
||||
body: Such a lovely day
|
||||
version: 24
|
||||
author_id: 1
|
||||
revisor_id: 1
|
||||
welcome_1:
|
||||
id: 2
|
||||
page_id: 1
|
||||
title: Welcome to the weblg
|
||||
body: Such a lovely day
|
||||
version: 23
|
||||
author_id: 2
|
||||
revisor_id: 2
|
||||
@@ -0,0 +1,7 @@
|
||||
welcome:
|
||||
id: 1
|
||||
title: Welcome to the weblog
|
||||
body: Such a lovely day
|
||||
version: 24
|
||||
author_id: 1
|
||||
revisor_id: 1
|
||||
@@ -0,0 +1,6 @@
|
||||
class Widget < ActiveRecord::Base
|
||||
acts_as_versioned :sequence_name => 'widgets_seq', :association_options => {
|
||||
:dependent => :nullify, :order => 'version desc'
|
||||
}
|
||||
non_versioned_columns << 'foo'
|
||||
end
|
||||
@@ -0,0 +1,46 @@
|
||||
require File.join(File.dirname(__FILE__), 'abstract_unit')
|
||||
|
||||
if ActiveRecord::Base.connection.supports_migrations?
|
||||
class Thing < ActiveRecord::Base
|
||||
attr_accessor :version
|
||||
acts_as_versioned
|
||||
end
|
||||
|
||||
class MigrationTest < Test::Unit::TestCase
|
||||
self.use_transactional_fixtures = false
|
||||
def teardown
|
||||
if ActiveRecord::Base.connection.respond_to?(:initialize_schema_information)
|
||||
ActiveRecord::Base.connection.initialize_schema_information
|
||||
ActiveRecord::Base.connection.update "UPDATE schema_info SET version = 0"
|
||||
else
|
||||
ActiveRecord::Base.connection.initialize_schema_migrations_table
|
||||
ActiveRecord::Base.connection.assume_migrated_upto_version(0)
|
||||
end
|
||||
|
||||
Thing.connection.drop_table "things" rescue nil
|
||||
Thing.connection.drop_table "thing_versions" rescue nil
|
||||
Thing.reset_column_information
|
||||
end
|
||||
|
||||
def test_versioned_migration
|
||||
assert_raises(ActiveRecord::StatementInvalid) { Thing.create :title => 'blah blah' }
|
||||
# take 'er up
|
||||
ActiveRecord::Migrator.up(File.dirname(__FILE__) + '/fixtures/migrations/')
|
||||
t = Thing.create :title => 'blah blah', :price => 123.45, :type => 'Thing'
|
||||
assert_equal 1, t.versions.size
|
||||
|
||||
# check that the price column has remembered its value correctly
|
||||
assert_equal t.price, t.versions.first.price
|
||||
assert_equal t.title, t.versions.first.title
|
||||
assert_equal t[:type], t.versions.first[:type]
|
||||
|
||||
# make sure that the precision of the price column has been preserved
|
||||
assert_equal 7, Thing::Version.columns.find{|c| c.name == "price"}.precision
|
||||
assert_equal 2, Thing::Version.columns.find{|c| c.name == "price"}.scale
|
||||
|
||||
# now lets take 'er back down
|
||||
ActiveRecord::Migrator.down(File.dirname(__FILE__) + '/fixtures/migrations/')
|
||||
assert_raises(ActiveRecord::StatementInvalid) { Thing.create :title => 'blah blah' }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,68 @@
|
||||
ActiveRecord::Schema.define(:version => 0) do
|
||||
create_table :pages, :force => true do |t|
|
||||
t.column :version, :integer
|
||||
t.column :title, :string, :limit => 255
|
||||
t.column :body, :text
|
||||
t.column :updated_on, :datetime
|
||||
t.column :author_id, :integer
|
||||
t.column :revisor_id, :integer
|
||||
end
|
||||
|
||||
create_table :page_versions, :force => true do |t|
|
||||
t.column :page_id, :integer
|
||||
t.column :version, :integer
|
||||
t.column :title, :string, :limit => 255
|
||||
t.column :body, :text
|
||||
t.column :updated_on, :datetime
|
||||
t.column :author_id, :integer
|
||||
t.column :revisor_id, :integer
|
||||
end
|
||||
|
||||
create_table :authors, :force => true do |t|
|
||||
t.column :page_id, :integer
|
||||
t.column :name, :string
|
||||
end
|
||||
|
||||
create_table :locked_pages, :force => true do |t|
|
||||
t.column :lock_version, :integer
|
||||
t.column :title, :string, :limit => 255
|
||||
t.column :type, :string, :limit => 255
|
||||
end
|
||||
|
||||
create_table :locked_pages_revisions, :force => true do |t|
|
||||
t.column :page_id, :integer
|
||||
t.column :version, :integer
|
||||
t.column :title, :string, :limit => 255
|
||||
t.column :version_type, :string, :limit => 255
|
||||
t.column :updated_at, :datetime
|
||||
end
|
||||
|
||||
create_table :widgets, :force => true do |t|
|
||||
t.column :name, :string, :limit => 50
|
||||
t.column :foo, :string
|
||||
t.column :version, :integer
|
||||
t.column :updated_at, :datetime
|
||||
end
|
||||
|
||||
create_table :widget_versions, :force => true do |t|
|
||||
t.column :widget_id, :integer
|
||||
t.column :name, :string, :limit => 50
|
||||
t.column :version, :integer
|
||||
t.column :updated_at, :datetime
|
||||
end
|
||||
|
||||
create_table :landmarks, :force => true do |t|
|
||||
t.column :name, :string
|
||||
t.column :latitude, :float
|
||||
t.column :longitude, :float
|
||||
t.column :version, :integer
|
||||
end
|
||||
|
||||
create_table :landmark_versions, :force => true do |t|
|
||||
t.column :landmark_id, :integer
|
||||
t.column :name, :string
|
||||
t.column :latitude, :float
|
||||
t.column :longitude, :float
|
||||
t.column :version, :integer
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,347 @@
|
||||
require File.join(File.dirname(__FILE__), 'abstract_unit')
|
||||
require File.join(File.dirname(__FILE__), 'fixtures/page')
|
||||
require File.join(File.dirname(__FILE__), 'fixtures/widget')
|
||||
|
||||
class VersionedTest < Test::Unit::TestCase
|
||||
fixtures :pages, :page_versions, :locked_pages, :locked_pages_revisions, :authors, :landmarks, :landmark_versions
|
||||
set_fixture_class :page_versions => Page::Version
|
||||
|
||||
def test_saves_versioned_copy
|
||||
p = Page.create! :title => 'first title', :body => 'first body'
|
||||
assert !p.new_record?
|
||||
assert_equal 1, p.versions.size
|
||||
assert_equal 1, p.version
|
||||
assert_instance_of Page.versioned_class, p.versions.first
|
||||
end
|
||||
|
||||
def test_saves_without_revision
|
||||
p = pages(:welcome)
|
||||
old_versions = p.versions.count
|
||||
|
||||
p.save_without_revision
|
||||
|
||||
p.without_revision do
|
||||
p.update_attributes :title => 'changed'
|
||||
end
|
||||
|
||||
assert_equal old_versions, p.versions.count
|
||||
end
|
||||
|
||||
def test_rollback_with_version_number
|
||||
p = pages(:welcome)
|
||||
assert_equal 24, p.version
|
||||
assert_equal 'Welcome to the weblog', p.title
|
||||
|
||||
assert p.revert_to!(p.versions.first.version), "Couldn't revert to 23"
|
||||
assert_equal 23, p.version
|
||||
assert_equal 'Welcome to the weblg', p.title
|
||||
end
|
||||
|
||||
def test_versioned_class_name
|
||||
assert_equal 'Version', Page.versioned_class_name
|
||||
assert_equal 'LockedPageRevision', LockedPage.versioned_class_name
|
||||
end
|
||||
|
||||
def test_versioned_class
|
||||
assert_equal Page::Version, Page.versioned_class
|
||||
assert_equal LockedPage::LockedPageRevision, LockedPage.versioned_class
|
||||
end
|
||||
|
||||
def test_special_methods
|
||||
assert_nothing_raised { pages(:welcome).feeling_good? }
|
||||
assert_nothing_raised { pages(:welcome).versions.first.feeling_good? }
|
||||
assert_nothing_raised { locked_pages(:welcome).hello_world }
|
||||
assert_nothing_raised { locked_pages(:welcome).versions.first.hello_world }
|
||||
end
|
||||
|
||||
def test_rollback_with_version_class
|
||||
p = pages(:welcome)
|
||||
assert_equal 24, p.version
|
||||
assert_equal 'Welcome to the weblog', p.title
|
||||
|
||||
assert p.revert_to!(p.versions.first), "Couldn't revert to 23"
|
||||
assert_equal 23, p.version
|
||||
assert_equal 'Welcome to the weblg', p.title
|
||||
end
|
||||
|
||||
def test_rollback_fails_with_invalid_revision
|
||||
p = locked_pages(:welcome)
|
||||
assert !p.revert_to!(locked_pages(:thinking))
|
||||
end
|
||||
|
||||
def test_saves_versioned_copy_with_options
|
||||
p = LockedPage.create! :title => 'first title'
|
||||
assert !p.new_record?
|
||||
assert_equal 1, p.versions.size
|
||||
assert_instance_of LockedPage.versioned_class, p.versions.first
|
||||
end
|
||||
|
||||
def test_rollback_with_version_number_with_options
|
||||
p = locked_pages(:welcome)
|
||||
assert_equal 'Welcome to the weblog', p.title
|
||||
assert_equal 'LockedPage', p.versions.first.version_type
|
||||
|
||||
assert p.revert_to!(p.versions.first.version), "Couldn't revert to 23"
|
||||
assert_equal 'Welcome to the weblg', p.title
|
||||
assert_equal 'LockedPage', p.versions.first.version_type
|
||||
end
|
||||
|
||||
def test_rollback_with_version_class_with_options
|
||||
p = locked_pages(:welcome)
|
||||
assert_equal 'Welcome to the weblog', p.title
|
||||
assert_equal 'LockedPage', p.versions.first.version_type
|
||||
|
||||
assert p.revert_to!(p.versions.first), "Couldn't revert to 1"
|
||||
assert_equal 'Welcome to the weblg', p.title
|
||||
assert_equal 'LockedPage', p.versions.first.version_type
|
||||
end
|
||||
|
||||
def test_saves_versioned_copy_with_sti
|
||||
p = SpecialLockedPage.create! :title => 'first title'
|
||||
assert !p.new_record?
|
||||
assert_equal 1, p.versions.size
|
||||
assert_instance_of LockedPage.versioned_class, p.versions.first
|
||||
assert_equal 'SpecialLockedPage', p.versions.first.version_type
|
||||
end
|
||||
|
||||
def test_rollback_with_version_number_with_sti
|
||||
p = locked_pages(:thinking)
|
||||
assert_equal 'So I was thinking', p.title
|
||||
|
||||
assert p.revert_to!(p.versions.first.version), "Couldn't revert to 1"
|
||||
assert_equal 'So I was thinking!!!', p.title
|
||||
assert_equal 'SpecialLockedPage', p.versions.first.version_type
|
||||
end
|
||||
|
||||
def test_lock_version_works_with_versioning
|
||||
p = locked_pages(:thinking)
|
||||
p2 = LockedPage.find(p.id)
|
||||
|
||||
p.title = 'fresh title'
|
||||
p.save
|
||||
assert_equal 2, p.versions.size # limit!
|
||||
|
||||
assert_raises(ActiveRecord::StaleObjectError) do
|
||||
p2.title = 'stale title'
|
||||
p2.save
|
||||
end
|
||||
end
|
||||
|
||||
def test_version_if_condition
|
||||
p = Page.create! :title => "title"
|
||||
assert_equal 1, p.version
|
||||
|
||||
Page.feeling_good = false
|
||||
p.save
|
||||
assert_equal 1, p.version
|
||||
Page.feeling_good = true
|
||||
end
|
||||
|
||||
def test_version_if_condition2
|
||||
# set new if condition
|
||||
Page.class_eval do
|
||||
def new_feeling_good() title[0..0] == 'a'; end
|
||||
alias_method :old_feeling_good, :feeling_good?
|
||||
alias_method :feeling_good?, :new_feeling_good
|
||||
end
|
||||
|
||||
p = Page.create! :title => "title"
|
||||
assert_equal 1, p.version # version does not increment
|
||||
assert_equal 1, p.versions(true).size
|
||||
|
||||
p.update_attributes(:title => 'new title')
|
||||
assert_equal 1, p.version # version does not increment
|
||||
assert_equal 1, p.versions(true).size
|
||||
|
||||
p.update_attributes(:title => 'a title')
|
||||
assert_equal 2, p.version
|
||||
assert_equal 2, p.versions(true).size
|
||||
|
||||
# reset original if condition
|
||||
Page.class_eval { alias_method :feeling_good?, :old_feeling_good }
|
||||
end
|
||||
|
||||
def test_version_if_condition_with_block
|
||||
# set new if condition
|
||||
old_condition = Page.version_condition
|
||||
Page.version_condition = Proc.new { |page| page.title[0..0] == 'b' }
|
||||
|
||||
p = Page.create! :title => "title"
|
||||
assert_equal 1, p.version # version does not increment
|
||||
assert_equal 1, p.versions(true).size
|
||||
|
||||
p.update_attributes(:title => 'a title')
|
||||
assert_equal 1, p.version # version does not increment
|
||||
assert_equal 1, p.versions(true).size
|
||||
|
||||
p.update_attributes(:title => 'b title')
|
||||
assert_equal 2, p.version
|
||||
assert_equal 2, p.versions(true).size
|
||||
|
||||
# reset original if condition
|
||||
Page.version_condition = old_condition
|
||||
end
|
||||
|
||||
def test_version_no_limit
|
||||
p = Page.create! :title => "title", :body => 'first body'
|
||||
p.save
|
||||
p.save
|
||||
5.times do |i|
|
||||
assert_page_title p, i
|
||||
end
|
||||
end
|
||||
|
||||
def test_version_max_limit
|
||||
p = LockedPage.create! :title => "title"
|
||||
p.update_attributes(:title => "title1")
|
||||
p.update_attributes(:title => "title2")
|
||||
5.times do |i|
|
||||
assert_page_title p, i, :lock_version
|
||||
assert p.versions(true).size <= 2, "locked version can only store 2 versions"
|
||||
end
|
||||
end
|
||||
|
||||
def test_track_altered_attributes_default_value
|
||||
assert !Page.track_altered_attributes
|
||||
assert LockedPage.track_altered_attributes
|
||||
assert SpecialLockedPage.track_altered_attributes
|
||||
end
|
||||
|
||||
def test_version_order
|
||||
assert_equal 23, pages(:welcome).versions.first.version
|
||||
assert_equal 24, pages(:welcome).versions.last.version
|
||||
end
|
||||
|
||||
def test_track_altered_attributes
|
||||
p = LockedPage.create! :title => "title"
|
||||
assert_equal 1, p.lock_version
|
||||
assert_equal 1, p.versions(true).size
|
||||
|
||||
p.title = 'title'
|
||||
assert !p.save_version?
|
||||
p.save
|
||||
assert_equal 2, p.lock_version # still increments version because of optimistic locking
|
||||
assert_equal 1, p.versions(true).size
|
||||
|
||||
p.title = 'updated title'
|
||||
assert p.save_version?
|
||||
p.save
|
||||
assert_equal 3, p.lock_version
|
||||
assert_equal 1, p.versions(true).size # version 1 deleted
|
||||
|
||||
p.title = 'updated title!'
|
||||
assert p.save_version?
|
||||
p.save
|
||||
assert_equal 4, p.lock_version
|
||||
assert_equal 2, p.versions(true).size # version 1 deleted
|
||||
end
|
||||
|
||||
def assert_page_title(p, i, version_field = :version)
|
||||
p.title = "title#{i}"
|
||||
p.save
|
||||
assert_equal "title#{i}", p.title
|
||||
assert_equal (i+4), p.send(version_field)
|
||||
end
|
||||
|
||||
def test_find_versions
|
||||
assert_equal 2, locked_pages(:welcome).versions.size
|
||||
assert_equal 1, locked_pages(:welcome).versions.find(:all, :conditions => ['title LIKE ?', '%weblog%']).length
|
||||
assert_equal 2, locked_pages(:welcome).versions.find(:all, :conditions => ['title LIKE ?', '%web%']).length
|
||||
assert_equal 0, locked_pages(:thinking).versions.find(:all, :conditions => ['title LIKE ?', '%web%']).length
|
||||
assert_equal 2, locked_pages(:welcome).versions.length
|
||||
end
|
||||
|
||||
def test_find_version
|
||||
assert_equal page_versions(:welcome_1), Page.find_version(pages(:welcome).id, 23)
|
||||
assert_equal page_versions(:welcome_2), Page.find_version(pages(:welcome).id, 24)
|
||||
assert_equal pages(:welcome), Page.find_version(pages(:welcome).id)
|
||||
|
||||
assert_equal page_versions(:welcome_1), pages(:welcome).find_version(23)
|
||||
assert_equal page_versions(:welcome_2), pages(:welcome).find_version(24)
|
||||
assert_equal pages(:welcome), pages(:welcome).find_version
|
||||
|
||||
assert_raise(ActiveRecord::RecordNotFound) { Page.find_version(pages(:welcome).id, 1) }
|
||||
assert_raise(ActiveRecord::RecordNotFound) { Page.find_version(0, 23) }
|
||||
end
|
||||
|
||||
def test_with_sequence
|
||||
assert_equal 'widgets_seq', Widget.versioned_class.sequence_name
|
||||
3.times { Widget.create! :name => 'new widget' }
|
||||
assert_equal 3, Widget.count
|
||||
assert_equal 3, Widget.versioned_class.count
|
||||
end
|
||||
|
||||
def test_has_many_through
|
||||
assert_equal [authors(:caged), authors(:mly)], pages(:welcome).authors
|
||||
end
|
||||
|
||||
def test_has_many_through_with_custom_association
|
||||
assert_equal [authors(:caged), authors(:mly)], pages(:welcome).revisors
|
||||
end
|
||||
|
||||
def test_referential_integrity
|
||||
pages(:welcome).destroy
|
||||
assert_equal 0, Page.count
|
||||
assert_equal 0, Page::Version.count
|
||||
end
|
||||
|
||||
def test_association_options
|
||||
association = Page.reflect_on_association(:versions)
|
||||
options = association.options
|
||||
assert_equal :delete_all, options[:dependent]
|
||||
assert_equal 'version', options[:order]
|
||||
|
||||
association = Widget.reflect_on_association(:versions)
|
||||
options = association.options
|
||||
assert_equal :nullify, options[:dependent]
|
||||
assert_equal 'version desc', options[:order]
|
||||
assert_equal 'widget_id', options[:foreign_key]
|
||||
|
||||
widget = Widget.create! :name => 'new widget'
|
||||
assert_equal 1, Widget.count
|
||||
assert_equal 1, Widget.versioned_class.count
|
||||
widget.destroy
|
||||
assert_equal 0, Widget.count
|
||||
assert_equal 1, Widget.versioned_class.count
|
||||
end
|
||||
|
||||
def test_versioned_records_should_belong_to_parent
|
||||
page = pages(:welcome)
|
||||
page_version = page.versions.last
|
||||
assert_equal page, page_version.page
|
||||
end
|
||||
|
||||
def test_unaltered_attributes
|
||||
landmarks(:washington).attributes = landmarks(:washington).attributes.except("id")
|
||||
assert !landmarks(:washington).changed?
|
||||
end
|
||||
|
||||
def test_unchanged_string_attributes
|
||||
landmarks(:washington).attributes = landmarks(:washington).attributes.except("id").inject({}) { |params, (key, value)| params.update(key => value.to_s) }
|
||||
assert !landmarks(:washington).changed?
|
||||
end
|
||||
|
||||
def test_should_find_earliest_version
|
||||
assert_equal page_versions(:welcome_1), pages(:welcome).versions.earliest
|
||||
end
|
||||
|
||||
def test_should_find_latest_version
|
||||
assert_equal page_versions(:welcome_2), pages(:welcome).versions.latest
|
||||
end
|
||||
|
||||
def test_should_find_previous_version
|
||||
assert_equal page_versions(:welcome_1), page_versions(:welcome_2).previous
|
||||
assert_equal page_versions(:welcome_1), pages(:welcome).versions.before(page_versions(:welcome_2))
|
||||
end
|
||||
|
||||
def test_should_find_next_version
|
||||
assert_equal page_versions(:welcome_2), page_versions(:welcome_1).next
|
||||
assert_equal page_versions(:welcome_2), pages(:welcome).versions.after(page_versions(:welcome_1))
|
||||
end
|
||||
|
||||
def test_should_find_version_count
|
||||
assert_equal 24, pages(:welcome).versions_count
|
||||
assert_equal 24, page_versions(:welcome_1).versions_count
|
||||
assert_equal 24, page_versions(:welcome_2).versions_count
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,3 @@
|
||||
# Include hook code here
|
||||
require File.dirname(__FILE__) + '/lib/acts_as_watchable'
|
||||
ActiveRecord::Base.send(:include, Redmine::Acts::Watchable)
|
||||
@@ -0,0 +1,69 @@
|
||||
# ActsAsWatchable
|
||||
module Redmine
|
||||
module Acts
|
||||
module Watchable
|
||||
def self.included(base)
|
||||
base.extend ClassMethods
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
def acts_as_watchable(options = {})
|
||||
return if self.included_modules.include?(Redmine::Acts::Watchable::InstanceMethods)
|
||||
send :include, Redmine::Acts::Watchable::InstanceMethods
|
||||
|
||||
class_eval do
|
||||
has_many :watchers, :as => :watchable, :dependent => :delete_all
|
||||
has_many :watcher_users, :through => :watchers, :source => :user
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module InstanceMethods
|
||||
def self.included(base)
|
||||
base.extend ClassMethods
|
||||
end
|
||||
|
||||
# Returns an array of users that are proposed as watchers
|
||||
def addable_watcher_users
|
||||
self.project.users.sort - self.watcher_users
|
||||
end
|
||||
|
||||
# Adds user as a watcher
|
||||
def add_watcher(user)
|
||||
self.watchers << Watcher.new(:user => user)
|
||||
end
|
||||
|
||||
# Removes user from the watchers list
|
||||
def remove_watcher(user)
|
||||
return nil unless user && user.is_a?(User)
|
||||
Watcher.delete_all "watchable_type = '#{self.class}' AND watchable_id = #{self.id} AND user_id = #{user.id}"
|
||||
end
|
||||
|
||||
# Adds/removes watcher
|
||||
def set_watcher(user, watching=true)
|
||||
watching ? add_watcher(user) : remove_watcher(user)
|
||||
end
|
||||
|
||||
# Returns if object is watched by user
|
||||
def watched_by?(user)
|
||||
!self.watchers.find(:first,
|
||||
:conditions => ["#{Watcher.table_name}.user_id = ?", user.id]).nil?
|
||||
end
|
||||
|
||||
# Returns an array of watchers' email addresses
|
||||
def watcher_recipients
|
||||
self.watchers.collect { |w| w.user.mail if w.user.active? }.compact
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
# Returns the objects that are watched by user
|
||||
def watched_by(user)
|
||||
find(:all,
|
||||
:include => :watchers,
|
||||
:conditions => ["#{Watcher.table_name}.user_id = ?", user.id])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,152 @@
|
||||
* Exported the changelog of Pagination code for historical reference.
|
||||
|
||||
* Imported some patches from Rails Trac (others closed as "wontfix"):
|
||||
#8176, #7325, #7028, #4113. Documentation is much cleaner now and there
|
||||
are some new unobtrusive features!
|
||||
|
||||
* Extracted Pagination from Rails trunk (r6795)
|
||||
|
||||
#
|
||||
# ChangeLog for /trunk/actionpack/lib/action_controller/pagination.rb
|
||||
#
|
||||
# Generated by Trac 0.10.3
|
||||
# 05/20/07 23:48:02
|
||||
#
|
||||
|
||||
09/03/06 23:28:54 david [4953]
|
||||
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
|
||||
Docs and deprecation
|
||||
|
||||
08/07/06 12:40:14 bitsweat [4715]
|
||||
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
|
||||
Deprecate direct usage of @params. Update ActionView::Base for
|
||||
instance var deprecation.
|
||||
|
||||
06/21/06 02:16:11 rick [4476]
|
||||
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
|
||||
Fix indent in pagination documentation. Closes #4990. [Kevin Clark]
|
||||
|
||||
04/25/06 17:42:48 marcel [4268]
|
||||
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
|
||||
Remove all remaining references to @params in the documentation.
|
||||
|
||||
03/16/06 06:38:08 rick [3899]
|
||||
* trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified)
|
||||
trivial documentation patch for #pagination_links [Francois
|
||||
Beausoleil] closes #4258
|
||||
|
||||
02/20/06 03:15:22 david [3620]
|
||||
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
|
||||
* trunk/actionpack/test/activerecord/pagination_test.rb (modified)
|
||||
* trunk/activerecord/CHANGELOG (modified)
|
||||
* trunk/activerecord/lib/active_record/base.rb (modified)
|
||||
* trunk/activerecord/test/base_test.rb (modified)
|
||||
Added :count option to pagination that'll make it possible for the
|
||||
ActiveRecord::Base.count call to using something else than * for the
|
||||
count. Especially important for count queries using DISTINCT #3839
|
||||
[skaes]. Added :select option to Base.count that'll allow you to
|
||||
select something else than * to be counted on. Especially important
|
||||
for count queries using DISTINCT (closes #3839) [skaes].
|
||||
|
||||
02/09/06 09:17:40 nzkoz [3553]
|
||||
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
|
||||
* trunk/actionpack/test/active_record_unit.rb (added)
|
||||
* trunk/actionpack/test/activerecord (added)
|
||||
* trunk/actionpack/test/activerecord/active_record_assertions_test.rb (added)
|
||||
* trunk/actionpack/test/activerecord/pagination_test.rb (added)
|
||||
* trunk/actionpack/test/controller/active_record_assertions_test.rb (deleted)
|
||||
* trunk/actionpack/test/fixtures/companies.yml (added)
|
||||
* trunk/actionpack/test/fixtures/company.rb (added)
|
||||
* trunk/actionpack/test/fixtures/db_definitions (added)
|
||||
* trunk/actionpack/test/fixtures/db_definitions/sqlite.sql (added)
|
||||
* trunk/actionpack/test/fixtures/developer.rb (added)
|
||||
* trunk/actionpack/test/fixtures/developers_projects.yml (added)
|
||||
* trunk/actionpack/test/fixtures/developers.yml (added)
|
||||
* trunk/actionpack/test/fixtures/project.rb (added)
|
||||
* trunk/actionpack/test/fixtures/projects.yml (added)
|
||||
* trunk/actionpack/test/fixtures/replies.yml (added)
|
||||
* trunk/actionpack/test/fixtures/reply.rb (added)
|
||||
* trunk/actionpack/test/fixtures/topic.rb (added)
|
||||
* trunk/actionpack/test/fixtures/topics.yml (added)
|
||||
* Fix pagination problems when using include
|
||||
* Introduce Unit Tests for pagination
|
||||
* Allow count to work with :include by using count distinct.
|
||||
|
||||
[Kevin Clark & Jeremy Hopple]
|
||||
|
||||
11/05/05 02:10:29 bitsweat [2878]
|
||||
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
|
||||
Update paginator docs. Closes #2744.
|
||||
|
||||
10/16/05 15:42:03 minam [2649]
|
||||
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
|
||||
Update/clean up AP documentation (rdoc)
|
||||
|
||||
08/31/05 00:13:10 ulysses [2078]
|
||||
* trunk/actionpack/CHANGELOG (modified)
|
||||
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
|
||||
Add option to specify the singular name used by pagination. Closes
|
||||
#1960
|
||||
|
||||
08/23/05 14:24:15 minam [2041]
|
||||
* trunk/actionpack/CHANGELOG (modified)
|
||||
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
|
||||
Add support for :include with pagination (subject to existing
|
||||
constraints for :include with :limit and :offset) #1478
|
||||
[michael@schubert.cx]
|
||||
|
||||
07/15/05 20:27:38 david [1839]
|
||||
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
|
||||
* trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified)
|
||||
More pagination speed #1334 [Stefan Kaes]
|
||||
|
||||
07/14/05 08:02:01 david [1832]
|
||||
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
|
||||
* trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified)
|
||||
* trunk/actionpack/test/controller/addresses_render_test.rb (modified)
|
||||
Made pagination faster #1334 [Stefan Kaes]
|
||||
|
||||
04/13/05 05:40:22 david [1159]
|
||||
* trunk/actionpack/CHANGELOG (modified)
|
||||
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
|
||||
* trunk/activerecord/lib/active_record/base.rb (modified)
|
||||
Fixed pagination to work with joins #1034 [scott@sigkill.org]
|
||||
|
||||
04/02/05 09:11:17 david [1067]
|
||||
* trunk/actionpack/CHANGELOG (modified)
|
||||
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
|
||||
* trunk/actionpack/lib/action_controller/scaffolding.rb (modified)
|
||||
* trunk/actionpack/lib/action_controller/templates/scaffolds/list.rhtml (modified)
|
||||
* trunk/railties/lib/rails_generator/generators/components/scaffold/templates/controller.rb (modified)
|
||||
* trunk/railties/lib/rails_generator/generators/components/scaffold/templates/view_list.rhtml (modified)
|
||||
Added pagination for scaffolding (10 items per page) #964
|
||||
[mortonda@dgrmm.net]
|
||||
|
||||
03/31/05 14:46:11 david [1048]
|
||||
* trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified)
|
||||
Improved the message display on the exception handler pages #963
|
||||
[Johan Sorensen]
|
||||
|
||||
03/27/05 00:04:07 david [1017]
|
||||
* trunk/actionpack/CHANGELOG (modified)
|
||||
* trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified)
|
||||
Fixed that pagination_helper would ignore :params #947 [Sebastian
|
||||
Kanthak]
|
||||
|
||||
03/22/05 13:09:44 david [976]
|
||||
* trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified)
|
||||
Fixed documentation and prepared for 0.11.0 release
|
||||
|
||||
03/21/05 14:35:36 david [967]
|
||||
* trunk/actionpack/lib/action_controller/pagination.rb (modified)
|
||||
* trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (modified)
|
||||
Tweaked the documentation
|
||||
|
||||
03/20/05 23:12:05 david [949]
|
||||
* trunk/actionpack/CHANGELOG (modified)
|
||||
* trunk/actionpack/lib/action_controller.rb (modified)
|
||||
* trunk/actionpack/lib/action_controller/pagination.rb (added)
|
||||
* trunk/actionpack/lib/action_view/helpers/pagination_helper.rb (added)
|
||||
* trunk/activesupport/lib/active_support/core_ext/kernel.rb (added)
|
||||
Added pagination support through both a controller and helper add-on
|
||||
#817 [Sam Stephenson]
|
||||
@@ -0,0 +1,18 @@
|
||||
Pagination
|
||||
==========
|
||||
|
||||
To install:
|
||||
|
||||
script/plugin install svn://errtheblog.com/svn/plugins/classic_pagination
|
||||
|
||||
This code was extracted from Rails trunk after the release 1.2.3.
|
||||
WARNING: this code is dead. It is unmaintained, untested and full of cruft.
|
||||
|
||||
There is a much better pagination plugin called will_paginate.
|
||||
Install it like this and glance through the README:
|
||||
|
||||
script/plugin install svn://errtheblog.com/svn/plugins/will_paginate
|
||||
|
||||
It doesn't have the same API, but is in fact much nicer. You can
|
||||
have both plugins installed until you change your controller/view code that
|
||||
handles pagination. Then, simply uninstall classic_pagination.
|
||||
@@ -0,0 +1,22 @@
|
||||
require 'rake'
|
||||
require 'rake/testtask'
|
||||
require 'rake/rdoctask'
|
||||
|
||||
desc 'Default: run unit tests.'
|
||||
task :default => :test
|
||||
|
||||
desc 'Test the classic_pagination plugin.'
|
||||
Rake::TestTask.new(:test) do |t|
|
||||
t.libs << 'lib'
|
||||
t.pattern = 'test/**/*_test.rb'
|
||||
t.verbose = true
|
||||
end
|
||||
|
||||
desc 'Generate documentation for the classic_pagination plugin.'
|
||||
Rake::RDocTask.new(:rdoc) do |rdoc|
|
||||
rdoc.rdoc_dir = 'rdoc'
|
||||
rdoc.title = 'Pagination'
|
||||
rdoc.options << '--line-numbers' << '--inline-source'
|
||||
rdoc.rdoc_files.include('README')
|
||||
rdoc.rdoc_files.include('lib/**/*.rb')
|
||||
end
|
||||
@@ -0,0 +1,33 @@
|
||||
#--
|
||||
# Copyright (c) 2004-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.
|
||||
#++
|
||||
|
||||
require 'pagination'
|
||||
require 'pagination_helper'
|
||||
|
||||
ActionController::Base.class_eval do
|
||||
include ActionController::Pagination
|
||||
end
|
||||
|
||||
ActionView::Base.class_eval do
|
||||
include ActionView::Helpers::PaginationHelper
|
||||
end
|
||||
@@ -0,0 +1 @@
|
||||
puts "\n\n" + File.read(File.dirname(__FILE__) + '/README')
|
||||
@@ -0,0 +1,405 @@
|
||||
module ActionController
|
||||
# === Action Pack pagination for Active Record collections
|
||||
#
|
||||
# The Pagination module aids in the process of paging large collections of
|
||||
# Active Record objects. It offers macro-style automatic fetching of your
|
||||
# model for multiple views, or explicit fetching for single actions. And if
|
||||
# the magic isn't flexible enough for your needs, you can create your own
|
||||
# paginators with a minimal amount of code.
|
||||
#
|
||||
# The Pagination module can handle as much or as little as you wish. In the
|
||||
# controller, have it automatically query your model for pagination; or,
|
||||
# if you prefer, create Paginator objects yourself.
|
||||
#
|
||||
# Pagination is included automatically for all controllers.
|
||||
#
|
||||
# For help rendering pagination links, see
|
||||
# ActionView::Helpers::PaginationHelper.
|
||||
#
|
||||
# ==== Automatic pagination for every action in a controller
|
||||
#
|
||||
# class PersonController < ApplicationController
|
||||
# model :person
|
||||
#
|
||||
# paginate :people, :order => 'last_name, first_name',
|
||||
# :per_page => 20
|
||||
#
|
||||
# # ...
|
||||
# end
|
||||
#
|
||||
# Each action in this controller now has access to a <tt>@people</tt>
|
||||
# instance variable, which is an ordered collection of model objects for the
|
||||
# current page (at most 20, sorted by last name and first name), and a
|
||||
# <tt>@person_pages</tt> Paginator instance. The current page is determined
|
||||
# by the <tt>params[:page]</tt> variable.
|
||||
#
|
||||
# ==== Pagination for a single action
|
||||
#
|
||||
# def list
|
||||
# @person_pages, @people =
|
||||
# paginate :people, :order => 'last_name, first_name'
|
||||
# end
|
||||
#
|
||||
# Like the previous example, but explicitly creates <tt>@person_pages</tt>
|
||||
# and <tt>@people</tt> for a single action, and uses the default of 10 items
|
||||
# per page.
|
||||
#
|
||||
# ==== Custom/"classic" pagination
|
||||
#
|
||||
# def list
|
||||
# @person_pages = Paginator.new self, Person.count, 10, params[:page]
|
||||
# @people = Person.find :all, :order => 'last_name, first_name',
|
||||
# :limit => @person_pages.items_per_page,
|
||||
# :offset => @person_pages.current.offset
|
||||
# end
|
||||
#
|
||||
# Explicitly creates the paginator from the previous example and uses
|
||||
# Paginator#to_sql to retrieve <tt>@people</tt> from the model.
|
||||
#
|
||||
module Pagination
|
||||
unless const_defined?(:OPTIONS)
|
||||
# A hash holding options for controllers using macro-style pagination
|
||||
OPTIONS = Hash.new
|
||||
|
||||
# The default options for pagination
|
||||
DEFAULT_OPTIONS = {
|
||||
:class_name => nil,
|
||||
:singular_name => nil,
|
||||
:per_page => 10,
|
||||
:conditions => nil,
|
||||
:order_by => nil,
|
||||
:order => nil,
|
||||
:join => nil,
|
||||
:joins => nil,
|
||||
:count => nil,
|
||||
:include => nil,
|
||||
:select => nil,
|
||||
:group => nil,
|
||||
:parameter => 'page'
|
||||
}
|
||||
else
|
||||
DEFAULT_OPTIONS[:group] = nil
|
||||
end
|
||||
|
||||
def self.included(base) #:nodoc:
|
||||
super
|
||||
base.extend(ClassMethods)
|
||||
end
|
||||
|
||||
def self.validate_options!(collection_id, options, in_action) #:nodoc:
|
||||
options.merge!(DEFAULT_OPTIONS) {|key, old, new| old}
|
||||
|
||||
valid_options = DEFAULT_OPTIONS.keys
|
||||
valid_options << :actions unless in_action
|
||||
|
||||
unknown_option_keys = options.keys - valid_options
|
||||
raise ActionController::ActionControllerError,
|
||||
"Unknown options: #{unknown_option_keys.join(', ')}" unless
|
||||
unknown_option_keys.empty?
|
||||
|
||||
options[:singular_name] ||= ActiveSupport::Inflector.singularize(collection_id.to_s)
|
||||
options[:class_name] ||= ActiveSupport::Inflector.camelize(options[:singular_name])
|
||||
end
|
||||
|
||||
# Returns a paginator and a collection of Active Record model instances
|
||||
# for the paginator's current page. This is designed to be used in a
|
||||
# single action; to automatically paginate multiple actions, consider
|
||||
# ClassMethods#paginate.
|
||||
#
|
||||
# +options+ are:
|
||||
# <tt>:singular_name</tt>:: the singular name to use, if it can't be inferred by singularizing the collection name
|
||||
# <tt>:class_name</tt>:: the class name to use, if it can't be inferred by
|
||||
# camelizing the singular name
|
||||
# <tt>:per_page</tt>:: the maximum number of items to include in a
|
||||
# single page. Defaults to 10
|
||||
# <tt>:conditions</tt>:: optional conditions passed to Model.find(:all, *params) and
|
||||
# Model.count
|
||||
# <tt>:order</tt>:: optional order parameter passed to Model.find(:all, *params)
|
||||
# <tt>:order_by</tt>:: (deprecated, used :order) optional order parameter passed to Model.find(:all, *params)
|
||||
# <tt>:joins</tt>:: optional joins parameter passed to Model.find(:all, *params)
|
||||
# and Model.count
|
||||
# <tt>:join</tt>:: (deprecated, used :joins or :include) optional join parameter passed to Model.find(:all, *params)
|
||||
# and Model.count
|
||||
# <tt>:include</tt>:: optional eager loading parameter passed to Model.find(:all, *params)
|
||||
# and Model.count
|
||||
# <tt>:select</tt>:: :select parameter passed to Model.find(:all, *params)
|
||||
#
|
||||
# <tt>:count</tt>:: parameter passed as :select option to Model.count(*params)
|
||||
#
|
||||
# <tt>:group</tt>:: :group parameter passed to Model.find(:all, *params). It forces the use of DISTINCT instead of plain COUNT to come up with the total number of records
|
||||
#
|
||||
def paginate(collection_id, options={})
|
||||
Pagination.validate_options!(collection_id, options, true)
|
||||
paginator_and_collection_for(collection_id, options)
|
||||
end
|
||||
|
||||
# These methods become class methods on any controller
|
||||
module ClassMethods
|
||||
# Creates a +before_filter+ which automatically paginates an Active
|
||||
# Record model for all actions in a controller (or certain actions if
|
||||
# specified with the <tt>:actions</tt> option).
|
||||
#
|
||||
# +options+ are the same as PaginationHelper#paginate, with the addition
|
||||
# of:
|
||||
# <tt>:actions</tt>:: an array of actions for which the pagination is
|
||||
# active. Defaults to +nil+ (i.e., every action)
|
||||
def paginate(collection_id, options={})
|
||||
Pagination.validate_options!(collection_id, options, false)
|
||||
module_eval do
|
||||
before_filter :create_paginators_and_retrieve_collections
|
||||
OPTIONS[self] ||= Hash.new
|
||||
OPTIONS[self][collection_id] = options
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def create_paginators_and_retrieve_collections #:nodoc:
|
||||
Pagination::OPTIONS[self.class].each do |collection_id, options|
|
||||
next unless options[:actions].include? action_name if
|
||||
options[:actions]
|
||||
|
||||
paginator, collection =
|
||||
paginator_and_collection_for(collection_id, options)
|
||||
|
||||
paginator_name = "@#{options[:singular_name]}_pages"
|
||||
self.instance_variable_set(paginator_name, paginator)
|
||||
|
||||
collection_name = "@#{collection_id.to_s}"
|
||||
self.instance_variable_set(collection_name, collection)
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the total number of items in the collection to be paginated for
|
||||
# the +model+ and given +conditions+. Override this method to implement a
|
||||
# custom counter.
|
||||
def count_collection_for_pagination(model, options)
|
||||
model.count(:conditions => options[:conditions],
|
||||
:joins => options[:join] || options[:joins],
|
||||
:include => options[:include],
|
||||
:select => (options[:group] ? "DISTINCT #{options[:group]}" : options[:count]))
|
||||
end
|
||||
|
||||
# Returns a collection of items for the given +model+ and +options[conditions]+,
|
||||
# ordered by +options[order]+, for the current page in the given +paginator+.
|
||||
# Override this method to implement a custom finder.
|
||||
def find_collection_for_pagination(model, options, paginator)
|
||||
model.find(:all, :conditions => options[:conditions],
|
||||
:order => options[:order_by] || options[:order],
|
||||
:joins => options[:join] || options[:joins], :include => options[:include],
|
||||
:select => options[:select], :limit => options[:per_page],
|
||||
:group => options[:group], :offset => paginator.current.offset)
|
||||
end
|
||||
|
||||
protected :create_paginators_and_retrieve_collections,
|
||||
:count_collection_for_pagination,
|
||||
:find_collection_for_pagination
|
||||
|
||||
def paginator_and_collection_for(collection_id, options) #:nodoc:
|
||||
klass = options[:class_name].constantize
|
||||
page = params[options[:parameter]]
|
||||
count = count_collection_for_pagination(klass, options)
|
||||
paginator = Paginator.new(self, count, options[:per_page], page)
|
||||
collection = find_collection_for_pagination(klass, options, paginator)
|
||||
|
||||
return paginator, collection
|
||||
end
|
||||
|
||||
private :paginator_and_collection_for
|
||||
|
||||
# A class representing a paginator for an Active Record collection.
|
||||
class Paginator
|
||||
include Enumerable
|
||||
|
||||
# Creates a new Paginator on the given +controller+ for a set of items
|
||||
# of size +item_count+ and having +items_per_page+ items per page.
|
||||
# Raises ArgumentError if items_per_page is out of bounds (i.e., less
|
||||
# than or equal to zero). The page CGI parameter for links defaults to
|
||||
# "page" and can be overridden with +page_parameter+.
|
||||
def initialize(controller, item_count, items_per_page, current_page=1)
|
||||
raise ArgumentError, 'must have at least one item per page' if
|
||||
items_per_page <= 0
|
||||
|
||||
@controller = controller
|
||||
@item_count = item_count || 0
|
||||
@items_per_page = items_per_page
|
||||
@pages = {}
|
||||
|
||||
self.current_page = current_page
|
||||
end
|
||||
attr_reader :controller, :item_count, :items_per_page
|
||||
|
||||
# Sets the current page number of this paginator. If +page+ is a Page
|
||||
# object, its +number+ attribute is used as the value; if the page does
|
||||
# not belong to this Paginator, an ArgumentError is raised.
|
||||
def current_page=(page)
|
||||
if page.is_a? Page
|
||||
raise ArgumentError, 'Page/Paginator mismatch' unless
|
||||
page.paginator == self
|
||||
end
|
||||
page = page.to_i
|
||||
@current_page_number = has_page_number?(page) ? page : 1
|
||||
end
|
||||
|
||||
# Returns a Page object representing this paginator's current page.
|
||||
def current_page
|
||||
@current_page ||= self[@current_page_number]
|
||||
end
|
||||
alias current :current_page
|
||||
|
||||
# Returns a new Page representing the first page in this paginator.
|
||||
def first_page
|
||||
@first_page ||= self[1]
|
||||
end
|
||||
alias first :first_page
|
||||
|
||||
# Returns a new Page representing the last page in this paginator.
|
||||
def last_page
|
||||
@last_page ||= self[page_count]
|
||||
end
|
||||
alias last :last_page
|
||||
|
||||
# Returns the number of pages in this paginator.
|
||||
def page_count
|
||||
@page_count ||= @item_count.zero? ? 1 :
|
||||
(q,r=@item_count.divmod(@items_per_page); r==0? q : q+1)
|
||||
end
|
||||
|
||||
alias length :page_count
|
||||
|
||||
# Returns true if this paginator contains the page of index +number+.
|
||||
def has_page_number?(number)
|
||||
number >= 1 and number <= page_count
|
||||
end
|
||||
|
||||
# Returns a new Page representing the page with the given index
|
||||
# +number+.
|
||||
def [](number)
|
||||
@pages[number] ||= Page.new(self, number)
|
||||
end
|
||||
|
||||
# Successively yields all the paginator's pages to the given block.
|
||||
def each(&block)
|
||||
page_count.times do |n|
|
||||
yield self[n+1]
|
||||
end
|
||||
end
|
||||
|
||||
# A class representing a single page in a paginator.
|
||||
class Page
|
||||
include Comparable
|
||||
|
||||
# Creates a new Page for the given +paginator+ with the index
|
||||
# +number+. If +number+ is not in the range of valid page numbers or
|
||||
# is not a number at all, it defaults to 1.
|
||||
def initialize(paginator, number)
|
||||
@paginator = paginator
|
||||
@number = number.to_i
|
||||
@number = 1 unless @paginator.has_page_number? @number
|
||||
end
|
||||
attr_reader :paginator, :number
|
||||
alias to_i :number
|
||||
|
||||
# Compares two Page objects and returns true when they represent the
|
||||
# same page (i.e., their paginators are the same and they have the
|
||||
# same page number).
|
||||
def ==(page)
|
||||
return false if page.nil?
|
||||
@paginator == page.paginator and
|
||||
@number == page.number
|
||||
end
|
||||
|
||||
# Compares two Page objects and returns -1 if the left-hand page comes
|
||||
# before the right-hand page, 0 if the pages are equal, and 1 if the
|
||||
# left-hand page comes after the right-hand page. Raises ArgumentError
|
||||
# if the pages do not belong to the same Paginator object.
|
||||
def <=>(page)
|
||||
raise ArgumentError unless @paginator == page.paginator
|
||||
@number <=> page.number
|
||||
end
|
||||
|
||||
# Returns the item offset for the first item in this page.
|
||||
def offset
|
||||
@paginator.items_per_page * (@number - 1)
|
||||
end
|
||||
|
||||
# Returns the number of the first item displayed.
|
||||
def first_item
|
||||
offset + 1
|
||||
end
|
||||
|
||||
# Returns the number of the last item displayed.
|
||||
def last_item
|
||||
[@paginator.items_per_page * @number, @paginator.item_count].min
|
||||
end
|
||||
|
||||
# Returns true if this page is the first page in the paginator.
|
||||
def first?
|
||||
self == @paginator.first
|
||||
end
|
||||
|
||||
# Returns true if this page is the last page in the paginator.
|
||||
def last?
|
||||
self == @paginator.last
|
||||
end
|
||||
|
||||
# Returns a new Page object representing the page just before this
|
||||
# page, or nil if this is the first page.
|
||||
def previous
|
||||
if first? then nil else @paginator[@number - 1] end
|
||||
end
|
||||
|
||||
# Returns a new Page object representing the page just after this
|
||||
# page, or nil if this is the last page.
|
||||
def next
|
||||
if last? then nil else @paginator[@number + 1] end
|
||||
end
|
||||
|
||||
# Returns a new Window object for this page with the specified
|
||||
# +padding+.
|
||||
def window(padding=2)
|
||||
Window.new(self, padding)
|
||||
end
|
||||
|
||||
# Returns the limit/offset array for this page.
|
||||
def to_sql
|
||||
[@paginator.items_per_page, offset]
|
||||
end
|
||||
|
||||
def to_param #:nodoc:
|
||||
@number.to_s
|
||||
end
|
||||
end
|
||||
|
||||
# A class for representing ranges around a given page.
|
||||
class Window
|
||||
# Creates a new Window object for the given +page+ with the specified
|
||||
# +padding+.
|
||||
def initialize(page, padding=2)
|
||||
@paginator = page.paginator
|
||||
@page = page
|
||||
self.padding = padding
|
||||
end
|
||||
attr_reader :paginator, :page
|
||||
|
||||
# Sets the window's padding (the number of pages on either side of the
|
||||
# window page).
|
||||
def padding=(padding)
|
||||
@padding = padding < 0 ? 0 : padding
|
||||
# Find the beginning and end pages of the window
|
||||
@first = @paginator.has_page_number?(@page.number - @padding) ?
|
||||
@paginator[@page.number - @padding] : @paginator.first
|
||||
@last = @paginator.has_page_number?(@page.number + @padding) ?
|
||||
@paginator[@page.number + @padding] : @paginator.last
|
||||
end
|
||||
attr_reader :padding, :first, :last
|
||||
|
||||
# Returns an array of Page objects in the current window.
|
||||
def pages
|
||||
(@first.number..@last.number).to_a.collect! {|n| @paginator[n]}
|
||||
end
|
||||
alias to_a :pages
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user