actionservice 0.2.102 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/ChangeLog +1 -1
- data/README +48 -14
- data/Rakefile +6 -15
- data/TODO +2 -0
- data/examples/{soap → googlesearch}/README +26 -10
- data/examples/{soap/lib → googlesearch/delegated}/google_search_service.rb +22 -9
- data/examples/{soap/app/controllers → googlesearch/delegated}/search_controller.rb +0 -0
- data/examples/googlesearch/direct/search_controller.rb +106 -0
- data/examples/metaWeblog/README +28 -0
- data/examples/metaWeblog/blog_controller.rb +117 -0
- data/lib/action_service/base.rb +40 -2
- data/lib/action_service/container.rb +87 -18
- data/lib/action_service/exporting.rb +6 -12
- data/lib/action_service/invocation.rb +51 -10
- data/lib/action_service/protocol/abstract.rb +11 -11
- data/lib/action_service/protocol/registry.rb +7 -7
- data/lib/action_service/protocol/soap.rb +18 -10
- data/lib/action_service/protocol/xmlrpc.rb +65 -32
- data/lib/action_service/router/action_controller.rb +24 -10
- data/lib/action_service/router/wsdl.rb +5 -5
- data/lib/action_service/struct.rb +17 -1
- data/lib/action_service/support/class_inheritable_options.rb +1 -1
- data/test/protocol_xmlrpc_test.rb +23 -3
- metadata +12 -9
data/ChangeLog
CHANGED
data/README
CHANGED
@@ -1,18 +1,16 @@
|
|
1
1
|
= Action Service -- Serving APIs on rails
|
2
2
|
|
3
|
-
Action Service provides a way to publish
|
3
|
+
Action Service provides a way to publish interoperable web service APIs with
|
4
4
|
Rails without wasting time delving into protocol details.
|
5
5
|
|
6
6
|
|
7
7
|
== Features
|
8
8
|
|
9
9
|
* SOAP RPC protocol support
|
10
|
-
* Dynamic WSDL generation
|
10
|
+
* Dynamic WSDL generation for exported APIs
|
11
11
|
* XML-RPC protocol support
|
12
|
-
*
|
13
|
-
|
14
|
-
* Using Active Record derivatives in return signatures automatically generates
|
15
|
-
a structured type equivalent that can be sent over the wire
|
12
|
+
* Type signature hints to improve interoperability with static languages
|
13
|
+
* Active Record model class support in signatures
|
16
14
|
|
17
15
|
|
18
16
|
== Integration with Action Pack
|
@@ -21,21 +19,57 @@ Action Service can be integrated in two different dispatching modes, _Direct_ an
|
|
21
19
|
_Delegated_.
|
22
20
|
|
23
21
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
22
|
+
== Direct dispatching
|
23
|
+
|
24
|
+
This is the default mode. In this mode, controller actions can be exported as
|
25
|
+
API methods, and parameters for incoming method calls will be placed in
|
26
|
+
<tt>@params</tt>. The return value of the action will be sent back as the
|
27
|
+
return value to the caller.
|
28
|
+
|
29
|
+
Requests from callers will be sent to <tt>/controller_name/api</tt> using
|
30
|
+
HTTP POST.
|
31
|
+
|
32
|
+
==== Direct dispatching example
|
33
|
+
|
34
|
+
class PersonApiController < ApplicationController
|
35
|
+
def add
|
36
|
+
end
|
37
|
+
|
38
|
+
def remove
|
39
|
+
end
|
40
|
+
|
41
|
+
export :add
|
42
|
+
export :remove
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
== Delegated dispatching
|
47
|
+
|
48
|
+
This mode can be turned on by setting the +service_dispatching_mode+ option.
|
49
|
+
In this mode, a controller is a container for one or more service objects,
|
50
|
+
each implementing a seperate API.
|
51
|
+
|
52
|
+
Requests from callers will be sent to <tt>/controller_name/service_name</tt>
|
53
|
+
using HTTP POST.
|
54
|
+
|
55
|
+
==== Delegated dispatching example
|
56
|
+
|
57
|
+
class ApiController < ApplicationController
|
58
|
+
service_dispatching_mode :delegated
|
59
|
+
|
60
|
+
service :person, PersonApi.new
|
61
|
+
service :customer, CustomerApi.new
|
62
|
+
end
|
30
63
|
|
31
64
|
|
32
65
|
== Dependencies
|
33
66
|
|
34
|
-
Action Service requires that the Action Pack and Active Record
|
67
|
+
Action Service requires that the Action Pack and Active Record are either
|
35
68
|
available to be required immediately or are accessible as GEMs.
|
36
69
|
|
37
70
|
It also requires a version of Ruby that includes SOAP support in the standard
|
38
|
-
library. At least version 1.8.2 final (2004-12-25) of Ruby is recommended
|
71
|
+
library. At least version 1.8.2 final (2004-12-25) of Ruby is recommended, this
|
72
|
+
is the version tested against.
|
39
73
|
|
40
74
|
|
41
75
|
== Download
|
data/Rakefile
CHANGED
@@ -5,10 +5,11 @@ require 'rake/rdoctask'
|
|
5
5
|
require 'rake/packagetask'
|
6
6
|
require 'rake/gempackagetask'
|
7
7
|
require 'rake/contrib/rubyforgepublisher'
|
8
|
+
require 'fileutils'
|
8
9
|
|
9
10
|
PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
|
10
11
|
PKG_NAME = 'actionservice'
|
11
|
-
PKG_VERSION = '0.
|
12
|
+
PKG_VERSION = '0.3.0' + PKG_BUILD
|
12
13
|
PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
|
13
14
|
|
14
15
|
desc "Default Task"
|
@@ -70,20 +71,10 @@ Rake::GemPackageTask.new(spec) do |p|
|
|
70
71
|
end
|
71
72
|
|
72
73
|
|
73
|
-
desc "Publish
|
74
|
-
task :
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
desc "Publish the API documentation"
|
80
|
-
task :pdoc => [:rdoc] do
|
81
|
-
# Rake::SshDirPublisher.new("davidhh@comox.textdrive.com", "public_html/am", "doc").upload
|
82
|
-
end
|
83
|
-
|
84
|
-
|
85
|
-
desc "Publish to RubyForge"
|
86
|
-
task :rubyforge do
|
74
|
+
desc "Publish API docs to RubyForge"
|
75
|
+
task :pdoc => [:rdoc] do
|
76
|
+
FileUtils.mkdir_p 'html'
|
77
|
+
FileUtils.mv 'doc', 'html/api'
|
87
78
|
Rake::RubyForgePublisher.new('actionservice', 'ljb').upload
|
88
79
|
end
|
89
80
|
|
data/TODO
CHANGED
@@ -2,24 +2,39 @@
|
|
2
2
|
|
3
3
|
|
4
4
|
This example shows how one would implement an API like Google
|
5
|
-
Search
|
5
|
+
Search that uses lots of structured types.
|
6
6
|
|
7
|
+
There are examples for "Direct" and "Delegated" dispatching
|
8
|
+
modes.
|
7
9
|
|
8
|
-
|
10
|
+
|
11
|
+
= Running
|
9
12
|
|
10
13
|
1. Ensure you have the 'actionservice' Gem installed. You can generate it using
|
11
14
|
this command:
|
12
15
|
|
13
16
|
$ rake package
|
14
17
|
|
15
|
-
2. Copy lib/google_search_service.rb and app/controllers/search_controller.rb
|
16
|
-
into a Rails project.
|
17
18
|
|
18
|
-
|
19
|
+
2. Edit config/environment.rb, and add the following line after the rest of the
|
19
20
|
require_gem statements:
|
20
21
|
|
21
22
|
require_gem 'actionservice'
|
22
23
|
|
24
|
+
|
25
|
+
3. "Direct" example:
|
26
|
+
|
27
|
+
* Copy direct/search_controller.rb to "app/controllers"
|
28
|
+
in a Rails project.
|
29
|
+
|
30
|
+
"Delegated" example:
|
31
|
+
|
32
|
+
* Copy delegated/search_controller.rb to "app/controllers"
|
33
|
+
in a Rails project.
|
34
|
+
* Copy delegated/google_search_service.rb to "lib"
|
35
|
+
in a Rails project.
|
36
|
+
|
37
|
+
|
23
38
|
4. Go to the url http://URL/search/wsdl in a browser, and check that it looks
|
24
39
|
correct.
|
25
40
|
|
@@ -27,7 +42,9 @@ Search, that uses lots of structured types.
|
|
27
42
|
and see how close (or not) the generated version is.
|
28
43
|
|
29
44
|
Note that I used GoogleSearch as the canonical "best practice"
|
30
|
-
interoperable example
|
45
|
+
interoperable example when implementing WSDL/SOAP support, which might
|
46
|
+
explain extreme similarities :)
|
47
|
+
|
31
48
|
|
32
49
|
5. Test that it works with .NET (Mono in this example):
|
33
50
|
|
@@ -79,8 +96,7 @@ Search, that uses lots of structured types.
|
|
79
96
|
|
80
97
|
|
81
98
|
If you had the application running (on the same host you got
|
82
|
-
the WSDL from), you should see something like this
|
83
|
-
with those in lib/google_search_service.rb):
|
99
|
+
the WSDL from), you should see something like this:
|
84
100
|
|
85
101
|
|
86
102
|
documentFiltering: True
|
@@ -108,8 +124,8 @@ Search, that uses lots of structured types.
|
|
108
124
|
|
109
125
|
|
110
126
|
Also, if an API method throws an exception, it will be sent back to the
|
111
|
-
caller in
|
112
|
-
their side with a meaningful error message.
|
127
|
+
caller in the protocol's exception format, so they should get an exception
|
128
|
+
thrown on their side with a meaningful error message.
|
113
129
|
|
114
130
|
If you don't like this behaviour, you can do:
|
115
131
|
|
@@ -80,14 +80,27 @@ class GoogleSearchService < ActionService::Base
|
|
80
80
|
result
|
81
81
|
end
|
82
82
|
|
83
|
-
def category(name, encoding)
|
84
|
-
cat = DirectoryCategory.new
|
85
|
-
cat.fullViewableName = name.dup
|
86
|
-
cat.specialEncoding = encoding.dup
|
87
|
-
cat
|
88
|
-
end
|
89
|
-
|
90
83
|
export :doGetCachedPage, :returns => [String], :expects => [{:key=>String}, {:url=>String}]
|
91
|
-
export :doGetSpellingSuggestion, :returns => [String], :expects => [String, String]
|
92
|
-
|
84
|
+
export :doGetSpellingSuggestion, :returns => [String], :expects => [{:key=>String}, {:phrase=>String}]
|
85
|
+
|
86
|
+
export :doGoogleSearch, :returns => [GoogleSearchResult], :expects => [
|
87
|
+
{:key=>String},
|
88
|
+
{:q=>String},
|
89
|
+
{:start=>Integer},
|
90
|
+
{:maxResults=>Integer},
|
91
|
+
{:filter=>TrueClass},
|
92
|
+
{:restrict=>String},
|
93
|
+
{:safeSearch=>TrueClass},
|
94
|
+
{:lr=>String},
|
95
|
+
{:ie=>String},
|
96
|
+
{:oe=>String}
|
97
|
+
]
|
98
|
+
|
99
|
+
private
|
100
|
+
def category(name, encoding)
|
101
|
+
cat = DirectoryCategory.new
|
102
|
+
cat.fullViewableName = name.dup
|
103
|
+
cat.specialEncoding = encoding.dup
|
104
|
+
cat
|
105
|
+
end
|
93
106
|
end
|
File without changes
|
@@ -0,0 +1,106 @@
|
|
1
|
+
class DirectoryCategory < ActionService::Struct
|
2
|
+
member :fullViewableName, String
|
3
|
+
member :specialEncoding, String
|
4
|
+
end
|
5
|
+
|
6
|
+
class ResultElement < ActionService::Struct
|
7
|
+
member :summary, String
|
8
|
+
member :URL, String
|
9
|
+
member :snippet, String
|
10
|
+
member :title, String
|
11
|
+
member :cachedSize, String
|
12
|
+
member :relatedInformationPresent, TrueClass
|
13
|
+
member :hostName, String
|
14
|
+
member :directoryCategory, DirectoryCategory
|
15
|
+
member :directoryTitle, String
|
16
|
+
end
|
17
|
+
|
18
|
+
class GoogleSearchResult < ActionService::Struct
|
19
|
+
member :documentFiltering, TrueClass
|
20
|
+
member :searchComments, String
|
21
|
+
member :estimatedTotalResultsCount, Integer
|
22
|
+
member :estimateIsExact, TrueClass
|
23
|
+
member :resultElements, [ResultElement]
|
24
|
+
member :searchQuery, String
|
25
|
+
member :startIndex, Integer
|
26
|
+
member :endIndex, Integer
|
27
|
+
member :searchTips, String
|
28
|
+
member :directoryCategories, [DirectoryCategory]
|
29
|
+
member :searchTime, Float
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
class SearchController < ApplicationController
|
34
|
+
wsdl_service_name 'GoogleSearch'
|
35
|
+
export_name_mangling false
|
36
|
+
|
37
|
+
def doGetCachedPage
|
38
|
+
"<html><body>i am a cached page. my key was %s, url was %s</body></html>" % [@params['key'], @params['url']]
|
39
|
+
end
|
40
|
+
|
41
|
+
def doSpellingSuggestion
|
42
|
+
"%s: Did you mean '%s'?" % [@params['key'], @params['phrase']]
|
43
|
+
end
|
44
|
+
|
45
|
+
def doGoogleSearch
|
46
|
+
resultElement = ResultElement.new
|
47
|
+
resultElement.summary = "ONlamp.com: Rolling with Ruby on Rails"
|
48
|
+
resultElement.URL = "http://www.onlamp.com/pub/a/onlamp/2005/01/20/rails.html"
|
49
|
+
resultElement.snippet = "Curt Hibbs shows off Ruby on Rails by building a simple application that requires " +
|
50
|
+
"almost no Ruby experience. ... Rolling with Ruby on Rails. ..."
|
51
|
+
resultElement.title = "Teh Railz0r"
|
52
|
+
resultElement.cachedSize = "Almost no lines of code!"
|
53
|
+
resultElement.relatedInformationPresent = true
|
54
|
+
resultElement.hostName = "rubyonrails.com"
|
55
|
+
resultElement.directoryCategory = category("Web Development", "UTF-8")
|
56
|
+
|
57
|
+
result = GoogleSearchResult.new
|
58
|
+
result.documentFiltering = @params['filter']
|
59
|
+
result.searchComments = ""
|
60
|
+
result.estimatedTotalResultsCount = 322000
|
61
|
+
result.estimateIsExact = false
|
62
|
+
result.resultElements = [resultElement]
|
63
|
+
result.searchQuery = "http://www.google.com/search?q=ruby+on+rails"
|
64
|
+
result.startIndex = @params['start']
|
65
|
+
result.endIndex = @params['start'] + @params['maxResults']
|
66
|
+
result.searchTips = "\"on\" is a very common word and was not included in your search [details]"
|
67
|
+
result.searchTime = 0.000001
|
68
|
+
|
69
|
+
# For Mono, we have to clone objects if they're referenced by more than one place, otherwise
|
70
|
+
# the Ruby SOAP collapses them into one instance and uses references all over the
|
71
|
+
# place, confusing Mono.
|
72
|
+
#
|
73
|
+
# This has recently been fixed:
|
74
|
+
# http://bugzilla.ximian.com/show_bug.cgi?id=72265
|
75
|
+
result.directoryCategories = [
|
76
|
+
category("Web Development", "UTF-8"),
|
77
|
+
category("Programming", "US-ASCII"),
|
78
|
+
]
|
79
|
+
|
80
|
+
result
|
81
|
+
end
|
82
|
+
|
83
|
+
export :doGetCachedPage, :returns => [String], :expects => [{:key=>String}, {:url=>String}]
|
84
|
+
export :doGetSpellingSuggestion, :returns => [String], :expects => [{:key=>String}, {:phrase=>String}]
|
85
|
+
|
86
|
+
export :doGoogleSearch, :returns => [GoogleSearchResult], :expects => [
|
87
|
+
{:key=>String},
|
88
|
+
{:q=>String},
|
89
|
+
{:start=>Integer},
|
90
|
+
{:maxResults=>Integer},
|
91
|
+
{:filter=>TrueClass},
|
92
|
+
{:restrict=>String},
|
93
|
+
{:safeSearch=>TrueClass},
|
94
|
+
{:lr=>String},
|
95
|
+
{:ie=>String},
|
96
|
+
{:oe=>String}
|
97
|
+
]
|
98
|
+
|
99
|
+
private
|
100
|
+
def category(name, encoding)
|
101
|
+
cat = DirectoryCategory.new
|
102
|
+
cat.fullViewableName = name.dup
|
103
|
+
cat.specialEncoding = encoding.dup
|
104
|
+
cat
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
= metaWeblog example
|
2
|
+
|
3
|
+
|
4
|
+
This example shows how one might begin to go about adding metaWeblog
|
5
|
+
(http://www.xmlrpc.com/metaWeblogApi) API support to a Rails-based
|
6
|
+
blogging application.
|
7
|
+
|
8
|
+
|
9
|
+
= Running
|
10
|
+
|
11
|
+
1. Ensure you have the 'actionservice' Gem installed. You can generate it using
|
12
|
+
this command:
|
13
|
+
|
14
|
+
$ rake package
|
15
|
+
|
16
|
+
|
17
|
+
2. Edit config/environment.rb, and add the following line after the rest of the
|
18
|
+
require_gem statements:
|
19
|
+
|
20
|
+
require_gem 'actionservice'
|
21
|
+
|
22
|
+
|
23
|
+
3. Copy blog_controller.rb to "app/controllers" in a Rails project.
|
24
|
+
|
25
|
+
|
26
|
+
4. Fire up a desktop blogging application (such as BloGTK on Linux),
|
27
|
+
point it at http://localhost:3000/blog/api, and try creating or
|
28
|
+
editing blog posts.
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# structures as defined by the metaWeblog/blogger
|
2
|
+
# specifications.
|
3
|
+
module Blog
|
4
|
+
class Enclosure < ActionService::Struct
|
5
|
+
member :url, String
|
6
|
+
member :length, Integer
|
7
|
+
member :type, String
|
8
|
+
end
|
9
|
+
|
10
|
+
class Source < ActionService::Struct
|
11
|
+
member :url, String
|
12
|
+
member :name, String
|
13
|
+
end
|
14
|
+
|
15
|
+
class Post < ActionService::Struct
|
16
|
+
member :title, String
|
17
|
+
member :link, String
|
18
|
+
member :description, String
|
19
|
+
member :author, String
|
20
|
+
member :category, String
|
21
|
+
member :comments, String
|
22
|
+
member :enclosure, Enclosure
|
23
|
+
member :guid, String
|
24
|
+
member :pubDate, String
|
25
|
+
member :source, Source
|
26
|
+
end
|
27
|
+
|
28
|
+
class Blog < ActionService::Struct
|
29
|
+
member :url, String
|
30
|
+
member :blogid, String
|
31
|
+
member :blogName, String
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
class BlogController < ApplicationController
|
37
|
+
export_name_mangling false
|
38
|
+
|
39
|
+
def initialize
|
40
|
+
@postid = 0
|
41
|
+
end
|
42
|
+
|
43
|
+
def newPost
|
44
|
+
$stderr.puts 'Creating post: username=%s password=%s struct=%s' % [
|
45
|
+
@params['username'],
|
46
|
+
@params['password'],
|
47
|
+
@params['struct'].inspect
|
48
|
+
]
|
49
|
+
(@postid += 1).to_s
|
50
|
+
end
|
51
|
+
|
52
|
+
def editPost
|
53
|
+
$stderr.puts 'Editing post: username=%s password=%s struct=%s' % [
|
54
|
+
@params['username'],
|
55
|
+
@params['password'],
|
56
|
+
@params['struct'].inspect
|
57
|
+
]
|
58
|
+
true
|
59
|
+
end
|
60
|
+
|
61
|
+
def getUsersBlogs
|
62
|
+
$stderr.puts "Returning user %s's blogs" % @params['username']
|
63
|
+
blog = Blog::Blog.new
|
64
|
+
blog.url = 'http://blog.xeraph.org'
|
65
|
+
blog.blogid = 'sttm'
|
66
|
+
blog.blogName = 'slave to the machine'
|
67
|
+
[blog]
|
68
|
+
end
|
69
|
+
|
70
|
+
def getRecentPosts
|
71
|
+
$stderr.puts "Returning recent posts (%d requested)" % @params['numberOfPosts']
|
72
|
+
post1 = Blog::Post.new
|
73
|
+
post1.title = 'first post!'
|
74
|
+
post1.link = 'http://blog.xeraph.org/testOne.html'
|
75
|
+
post1.description = 'this is the first post'
|
76
|
+
post2 = Blog::Post.new
|
77
|
+
post2.title = 'second post!'
|
78
|
+
post2.link = 'http://blog.xeraph.org/testTwo.html'
|
79
|
+
post2.description = 'this is the second post'
|
80
|
+
[post1, post2]
|
81
|
+
end
|
82
|
+
|
83
|
+
export :newPost, :returns => [String], :expects => [
|
84
|
+
{:blogid=>String},
|
85
|
+
{:username=>String},
|
86
|
+
{:password=>String},
|
87
|
+
{:struct=>Blog::Post},
|
88
|
+
{:publish=>TrueClass},
|
89
|
+
]
|
90
|
+
|
91
|
+
export :editPost, :returns => [TrueClass], :expects => [
|
92
|
+
{:postid=>String},
|
93
|
+
{:username=>String},
|
94
|
+
{:password=>String},
|
95
|
+
{:struct=>Blog::Post},
|
96
|
+
{:publish=>TrueClass},
|
97
|
+
]
|
98
|
+
|
99
|
+
export :getPost, :returns => [Blog::Post], :expects => [
|
100
|
+
{:postid=>String},
|
101
|
+
{:username=>String},
|
102
|
+
{:password=>String},
|
103
|
+
]
|
104
|
+
|
105
|
+
export :getUsersBlogs, :returns => [[Blog::Blog]], :expects => [
|
106
|
+
{:appkey=>String},
|
107
|
+
{:username=>String},
|
108
|
+
{:password=>String},
|
109
|
+
]
|
110
|
+
|
111
|
+
export :getRecentPosts, :returns => [[Blog::Post]], :expects => [
|
112
|
+
{:blogid=>String},
|
113
|
+
{:username=>String},
|
114
|
+
{:password=>String},
|
115
|
+
{:numberOfPosts=>Integer},
|
116
|
+
]
|
117
|
+
end
|