blogrpc 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/Gemfile +16 -0
- data/Gemfile.lock +28 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +57 -0
- data/Rakefile +46 -0
- data/blogrpc.gemspec +65 -0
- data/lib/blogrpc/basic_handler.rb +98 -0
- data/lib/blogrpc/rack_app.rb +112 -0
- data/lib/blogrpc/sample_handler.rb +282 -0
- data/lib/blogrpc.rb +15 -0
- data/test/helper.rb +32 -0
- data/test/http_simulator.rb +62 -0
- data/test/test_blogapi.rb +40 -0
- data/test/test_rpc_handler_method_definitions.rb +51 -0
- metadata +145 -0
data/.document
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
# Add dependencies required to use your gem here.
|
3
|
+
# Example:
|
4
|
+
# gem "activesupport", ">= 2.3.5"
|
5
|
+
|
6
|
+
gem "builder", "~> 3.0.0"
|
7
|
+
|
8
|
+
|
9
|
+
# Add dependencies to develop your gem here.
|
10
|
+
# Include everything needed to run rake, tests, features, etc.
|
11
|
+
group :development do
|
12
|
+
gem "rdoc", "~> 3.12"
|
13
|
+
gem "jeweler", "~> 1.8.4"
|
14
|
+
gem "flexmock", "~>0.8"
|
15
|
+
gem "rack-test"
|
16
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
builder (3.0.0)
|
5
|
+
flexmock (0.9.0)
|
6
|
+
git (1.2.5)
|
7
|
+
jeweler (1.8.4)
|
8
|
+
bundler (~> 1.0)
|
9
|
+
git (>= 1.2.5)
|
10
|
+
rake
|
11
|
+
rdoc
|
12
|
+
json (1.7.4)
|
13
|
+
rack (1.4.1)
|
14
|
+
rack-test (0.6.1)
|
15
|
+
rack (>= 1.0)
|
16
|
+
rake (0.9.2.2)
|
17
|
+
rdoc (3.12)
|
18
|
+
json (~> 1.4)
|
19
|
+
|
20
|
+
PLATFORMS
|
21
|
+
ruby
|
22
|
+
|
23
|
+
DEPENDENCIES
|
24
|
+
builder (~> 3.0.0)
|
25
|
+
flexmock (~> 0.8)
|
26
|
+
jeweler (~> 1.8.4)
|
27
|
+
rack-test
|
28
|
+
rdoc (~> 3.12)
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2012 Julik Tarkhanov
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
= blogrpc
|
2
|
+
|
3
|
+
BlogRPC is a set of XML RPC server facilities which allow you to easily create a blogging XML-RPC backend in your Ruby web application.
|
4
|
+
Your application can then be used with blog clients like BlogJet and MarsEdit.
|
5
|
+
|
6
|
+
== Quickly defining a blog RPC handler
|
7
|
+
|
8
|
+
You can also define your blog handler right in the rackup file or elsewhere:
|
9
|
+
|
10
|
+
rpc_endpoint = BlogRPC.generate_endpoint do | handler |
|
11
|
+
handler.rpc "mt.publishPost", :in => [:int, :string, :string], :out => :bool do | postid, user, pw |
|
12
|
+
login!(user, pw)
|
13
|
+
get_entry(postid).update_attributes :draft => false
|
14
|
+
true
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
run rpc_endpoint
|
19
|
+
|
20
|
+
|
21
|
+
== More involved examples
|
22
|
+
|
23
|
+
The gem consists of two parts. The first part is a Rack application that will handle XML-RPC requests and respond to them. It handles things like
|
24
|
+
wrapping exceptions properly, detecting the needed parameters and configuring all of the IO so that Ruby's XML-RPC facilities can get at it.
|
25
|
+
You use it like this:
|
26
|
+
|
27
|
+
rpc_endpoint = BlogRPC::RackApp.new(MyBlogHandler.new)
|
28
|
+
rpc_endpoint.blog_url = "http://site.com"
|
29
|
+
rpc_endpoint.rpc_endpoint_url = "/secret-rpc-url.xml"
|
30
|
+
rpc_endpoint.call(env)
|
31
|
+
|
32
|
+
The BlogHandler object should be a more or less complete subclass of BlogHandler that you provide. When the RPC application receives a GET request
|
33
|
+
it will respond with the RSD fragment that will auto-configure your blogging client (like MarsEdit or BlogJet).
|
34
|
+
|
35
|
+
The second part of the solution is a blog handler. The handler is responsible for saving and loading entries and images, creating pages and categories
|
36
|
+
and so on. Unfortunately, you have to write this handler yourself since no two blogging systems are alike. However, we provide a SampleHandler
|
37
|
+
to get you started.
|
38
|
+
|
39
|
+
IMPORTANT: It is absolutely imperative that you review the sample handler *very thoroughly* and rewrite and double-check it ad nauseam.
|
40
|
+
We do not recommend that you inherit from the SampleHandler. Instead, make your own copy and define your methods there, and inherit your
|
41
|
+
handler class from BlogRPC::BasicHandler
|
42
|
+
|
43
|
+
== Contributing to blogrpc
|
44
|
+
|
45
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
|
46
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
|
47
|
+
* Fork the project.
|
48
|
+
* Start a feature/bugfix branch.
|
49
|
+
* Commit and push until you are happy with your contribution.
|
50
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
51
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
52
|
+
|
53
|
+
== Copyright
|
54
|
+
|
55
|
+
Copyright (c) 2012 Julik Tarkhanov. See LICENSE.txt for
|
56
|
+
further details.
|
57
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
begin
|
6
|
+
Bundler.setup(:default, :development)
|
7
|
+
rescue Bundler::BundlerError => e
|
8
|
+
$stderr.puts e.message
|
9
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
10
|
+
exit e.status_code
|
11
|
+
end
|
12
|
+
require 'rake'
|
13
|
+
require File.dirname(__FILE__) + "/lib/blogrpc"
|
14
|
+
require 'jeweler'
|
15
|
+
|
16
|
+
Jeweler::Tasks.new do |gem|
|
17
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
18
|
+
gem.version = BlogRPC::VERSION
|
19
|
+
gem.name = "blogrpc"
|
20
|
+
gem.homepage = "http://github.com/julik/blogrpc"
|
21
|
+
gem.license = "MIT"
|
22
|
+
gem.summary = %Q{ Easily construct MT and MetaWeblog XML-RPC backends}
|
23
|
+
gem.email = "me@julik.nl"
|
24
|
+
gem.authors = ["Julik Tarkhanov"]
|
25
|
+
# dependencies defined in Gemfile
|
26
|
+
end
|
27
|
+
Jeweler::RubygemsDotOrgTasks.new
|
28
|
+
|
29
|
+
require 'rake/testtask'
|
30
|
+
Rake::TestTask.new(:test) do |test|
|
31
|
+
test.libs << 'lib' << 'test'
|
32
|
+
test.pattern = 'test/**/test_*.rb'
|
33
|
+
test.verbose = true
|
34
|
+
end
|
35
|
+
|
36
|
+
task :default => :test
|
37
|
+
|
38
|
+
require 'rdoc/task'
|
39
|
+
Rake::RDocTask.new do |rdoc|
|
40
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
41
|
+
|
42
|
+
rdoc.rdoc_dir = 'rdoc'
|
43
|
+
rdoc.title = "blogrpc #{version}"
|
44
|
+
rdoc.rdoc_files.include('README*')
|
45
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
46
|
+
end
|
data/blogrpc.gemspec
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = "blogrpc"
|
8
|
+
s.version = "1.0.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Julik Tarkhanov"]
|
12
|
+
s.date = "2012-08-11"
|
13
|
+
s.email = "me@julik.nl"
|
14
|
+
s.extra_rdoc_files = [
|
15
|
+
"LICENSE.txt",
|
16
|
+
"README.rdoc"
|
17
|
+
]
|
18
|
+
s.files = [
|
19
|
+
".document",
|
20
|
+
"Gemfile",
|
21
|
+
"Gemfile.lock",
|
22
|
+
"LICENSE.txt",
|
23
|
+
"README.rdoc",
|
24
|
+
"Rakefile",
|
25
|
+
"blogrpc.gemspec",
|
26
|
+
"lib/blogrpc.rb",
|
27
|
+
"lib/blogrpc/basic_handler.rb",
|
28
|
+
"lib/blogrpc/rack_app.rb",
|
29
|
+
"lib/blogrpc/sample_handler.rb",
|
30
|
+
"test/helper.rb",
|
31
|
+
"test/http_simulator.rb",
|
32
|
+
"test/test_blogapi.rb",
|
33
|
+
"test/test_rpc_handler_method_definitions.rb"
|
34
|
+
]
|
35
|
+
s.homepage = "http://github.com/julik/blogrpc"
|
36
|
+
s.licenses = ["MIT"]
|
37
|
+
s.require_paths = ["lib"]
|
38
|
+
s.rubygems_version = "1.8.24"
|
39
|
+
s.summary = "Easily construct MT and MetaWeblog XML-RPC backends"
|
40
|
+
|
41
|
+
if s.respond_to? :specification_version then
|
42
|
+
s.specification_version = 3
|
43
|
+
|
44
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
45
|
+
s.add_runtime_dependency(%q<builder>, ["~> 3.0.0"])
|
46
|
+
s.add_development_dependency(%q<rdoc>, ["~> 3.12"])
|
47
|
+
s.add_development_dependency(%q<jeweler>, ["~> 1.8.4"])
|
48
|
+
s.add_development_dependency(%q<flexmock>, ["~> 0.8"])
|
49
|
+
s.add_development_dependency(%q<rack-test>, [">= 0"])
|
50
|
+
else
|
51
|
+
s.add_dependency(%q<builder>, ["~> 3.0.0"])
|
52
|
+
s.add_dependency(%q<rdoc>, ["~> 3.12"])
|
53
|
+
s.add_dependency(%q<jeweler>, ["~> 1.8.4"])
|
54
|
+
s.add_dependency(%q<flexmock>, ["~> 0.8"])
|
55
|
+
s.add_dependency(%q<rack-test>, [">= 0"])
|
56
|
+
end
|
57
|
+
else
|
58
|
+
s.add_dependency(%q<builder>, ["~> 3.0.0"])
|
59
|
+
s.add_dependency(%q<rdoc>, ["~> 3.12"])
|
60
|
+
s.add_dependency(%q<jeweler>, ["~> 1.8.4"])
|
61
|
+
s.add_dependency(%q<flexmock>, ["~> 0.8"])
|
62
|
+
s.add_dependency(%q<rack-test>, [">= 0"])
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require 'xmlrpc/server'
|
3
|
+
require 'fileutils'
|
4
|
+
require 'builder'
|
5
|
+
|
6
|
+
# XML writer with proper escaping. Unfortunately XMLRPC in Ruby
|
7
|
+
# has issues escaping non-UTF-8 characters, so we need to take care of that.
|
8
|
+
# The best solution is to make use of the XChar facility from Builder.
|
9
|
+
c = Class.new(XMLRPC::XMLWriter::Simple) do
|
10
|
+
def text(string)
|
11
|
+
Builder::XChar.encode(string)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Inject the proper writer into the XML-RPC module. There is no way
|
16
|
+
# to do that properly so it seems.
|
17
|
+
begin
|
18
|
+
$VERBOSE, yuck = nil, $VERBOSE
|
19
|
+
XMLRPC::Config.send(:const_set, :DEFAULT_WRITER, c)
|
20
|
+
ensure
|
21
|
+
$VERBOSE = yuck
|
22
|
+
end
|
23
|
+
|
24
|
+
# A very simple RPC interface where you can declare methods by calling rpc "methodThis" :in => [:bool], :out => :bool { truth }
|
25
|
+
# and it creates method_this(some_bool) in Ruby and wires it in
|
26
|
+
class BlogRPC::BasicHandler < XMLRPC::Service::BasicInterface
|
27
|
+
|
28
|
+
# Will contain the Rack application environment
|
29
|
+
attr_accessor :env
|
30
|
+
|
31
|
+
# The standard initializer for such a service accepts a prefix,
|
32
|
+
# but we will override that
|
33
|
+
def initialize(prefix = nil)
|
34
|
+
@prefix = 'mt'
|
35
|
+
end
|
36
|
+
|
37
|
+
# Will return a union of all of the methods defined using rpc() in this class
|
38
|
+
# and all of it's ancestors.
|
39
|
+
def self.rpc_methods_and_signatures
|
40
|
+
# Climb up the class chain
|
41
|
+
methods_and_signatures_in_ancestor_chain = {}
|
42
|
+
ancestors.reverse.each do | ancestor_module |
|
43
|
+
in_ancestor = ancestor_module.instance_variable_get('@rpc_methods_and_signatures') || {}
|
44
|
+
methods_and_signatures_in_ancestor_chain.merge!(in_ancestor)
|
45
|
+
end
|
46
|
+
methods_and_signatures_in_ancestor_chain.merge(@rpc_methods_and_signatures || {})
|
47
|
+
end
|
48
|
+
|
49
|
+
# Pass your XML request here and get the result (the request is the
|
50
|
+
# raw HTTP POST body, the way to get to it differs per framework/server)
|
51
|
+
#
|
52
|
+
# huge_xml_blob = MiniInterface.new.handle_request(StringIO.new(post_data))
|
53
|
+
#
|
54
|
+
def handle_request(post_payload)
|
55
|
+
s = XMLRPC::BasicServer.new
|
56
|
+
s.add_handler self
|
57
|
+
s.process(post_payload.respond_to?(:read) ? post_payload : StringIO.new(post_payload) )
|
58
|
+
end
|
59
|
+
|
60
|
+
# Generate an RPC method uisng a block. Accepts two parameters :in is for the input argument types (should be array)
|
61
|
+
# and :out for what it will return.
|
62
|
+
# The block defines the method.
|
63
|
+
def self.rpc(methName, options, &blk)
|
64
|
+
ruby_method = methName.split(/\./).pop.gsub(/([A-Z])/) { |m| '_' + m.downcase }
|
65
|
+
define_method(ruby_method, &blk)
|
66
|
+
|
67
|
+
options = {:in => []}.merge(options)
|
68
|
+
iface, method = methName.split(/\./)
|
69
|
+
@rpc_methods_and_signatures ||= {}
|
70
|
+
@rpc_methods_and_signatures[ruby_method] = [ methName, "#{options[:out]} #{method}(#{options[:in].join(', ')})" ]
|
71
|
+
end
|
72
|
+
|
73
|
+
# This is the magic wand. The docs of XMLRPC never explain where the fuck does obj actually ever come from,
|
74
|
+
# moreover - here WE are the object (the server sends a nil, anyways). What is important is that this method
|
75
|
+
# returns a set of method signatures that XMLRPC will call on our object when processing the request.
|
76
|
+
# Delim is omitted since internally we differentiate our namespaced methods anyway (mt. prefix for MovableType
|
77
|
+
# and wp. for Wordpress..)
|
78
|
+
def get_methods(obj, delim)
|
79
|
+
meths = []
|
80
|
+
self.class.rpc_methods_and_signatures.each_pair do | ruby_method, details |
|
81
|
+
# And bind the method
|
82
|
+
meths.unshift [
|
83
|
+
details[0], # method name (in XMLRPC terms) including prefix, like "mt.getPost"
|
84
|
+
method(ruby_method).to_proc, # the method itself, a callable Proc
|
85
|
+
details[1], # signature, like "getPost(string, string, int)"
|
86
|
+
(details[2] || "Just a method") # method help
|
87
|
+
]
|
88
|
+
end
|
89
|
+
meths
|
90
|
+
end
|
91
|
+
|
92
|
+
# The only default RPC method we declare. Returns the list of supported XML-RPC methods. This implementation can be
|
93
|
+
# left in it's default form.
|
94
|
+
rpc "mt.supportedMethods", :out => :array do
|
95
|
+
self.class.rpc_methods_and_signatures.values.map {|iface| iface[0] }
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
|
3
|
+
# An embeddable Rack handler for RPC servers
|
4
|
+
class BlogRPC::RackApp
|
5
|
+
|
6
|
+
# The root URL of the blog
|
7
|
+
attr_accessor :blog_url
|
8
|
+
|
9
|
+
# The URL of the blog API endpoint (like /backend.rpc)
|
10
|
+
attr_accessor :rpc_endpoint_url
|
11
|
+
|
12
|
+
# All the XML-RPC handlers that can respond to the request,
|
13
|
+
# if it so happens that you have more than one
|
14
|
+
attr_accessor :handlers
|
15
|
+
|
16
|
+
def initialize(blog_handler)
|
17
|
+
@handlers = [blog_handler]
|
18
|
+
end
|
19
|
+
|
20
|
+
def call(env)
|
21
|
+
@env = env
|
22
|
+
req = Rack::Request.new(env)
|
23
|
+
# If the request is a GET return the autodiscovery RSD fragment
|
24
|
+
if req.get?
|
25
|
+
return endpoint_xml
|
26
|
+
else
|
27
|
+
return post_request
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def all_supported_method_names
|
34
|
+
method_names = []
|
35
|
+
@handlers.map do |h|
|
36
|
+
methods_of_handler = h.get_methods(nil, nil)
|
37
|
+
methods_of_handler.each do | m |
|
38
|
+
method_names << m[0]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
method_names
|
42
|
+
end
|
43
|
+
|
44
|
+
def supports_mt?
|
45
|
+
all_supported_method_names.grep(/^mt\./).any?
|
46
|
+
end
|
47
|
+
|
48
|
+
def supports_metaweblog?
|
49
|
+
all_supported_method_names.grep(/^metaWeblog\./).any?
|
50
|
+
end
|
51
|
+
|
52
|
+
def supports_wordpress?
|
53
|
+
all_supported_method_names.grep(/^wp\./).any?
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns the endpoint information that smart blogging clients
|
57
|
+
# can use to detect which APIs you support.
|
58
|
+
def endpoint_xml
|
59
|
+
b = Builder::XmlMarkup.new
|
60
|
+
b.rsd :version => '1.0', :xmlns => "http://archipelago.phrasewise.com/rsd" do; b.service do
|
61
|
+
b.engineName "MovableType" # It's better to pose as MT
|
62
|
+
b.homePageLink(blog_url)
|
63
|
+
b.apis do
|
64
|
+
b.api( :name=>"MovableType", :preferred => "true", :apiLink => rpc_endpoint_url, :blogID => 1) if supports_mt?
|
65
|
+
b.api( :name=>"MetaWeblog", :apiLink => rpc_endpoint_url, :blogID => 1) if supports_metaweblog?
|
66
|
+
b.api( :name=>"WordPress", :apiLink => rpc_endpoint_url, :blogID => 1) if supports_wordpress?
|
67
|
+
end
|
68
|
+
end;end
|
69
|
+
[200, {"Content-Length"=> Rack::Utils.bytesize(b.target!), "Content-Type" => 'application/rsd+xml'}, b.target!]
|
70
|
+
end
|
71
|
+
|
72
|
+
def post_request
|
73
|
+
body = begin
|
74
|
+
# Ruby's XMLRPC does not love rack.input wrappers,
|
75
|
+
# it wants a bona-fide IO. Also, different rack input
|
76
|
+
# wrappers work differently (some are better than others),
|
77
|
+
# so to protect us from extra grief we will rebuffer everything
|
78
|
+
# in a Tempfile
|
79
|
+
@temp = Tempfile.new("rxrpc")
|
80
|
+
@env['rack.input'].each(&@temp.method(:write))
|
81
|
+
@temp.rewind
|
82
|
+
s = XMLRPC::BasicServer.new(self)
|
83
|
+
# Here we initialize the handlers
|
84
|
+
@handlers.each do |handler|
|
85
|
+
# Inject the Rack environment into the handler
|
86
|
+
handler.env = @env
|
87
|
+
s.add_handler(handler)
|
88
|
+
end
|
89
|
+
|
90
|
+
# AND PROZESSS!
|
91
|
+
s.process(@temp)
|
92
|
+
rescue Exception => e # Transform all errors, even LoadError, into a properly formatted XML-RPC fault struct
|
93
|
+
# Supports captivity logger by default
|
94
|
+
@env["captivity.logger"].fatal([e.message, e.backtrace].join("\n")) if @env['captivity.logger']
|
95
|
+
b = Builder::XmlMarkup.new
|
96
|
+
b.methodResponse do |b|; b.fault do; b.value do; b.struct do
|
97
|
+
b.member { b.name("faultCode"); b.value { b.int 1 } }
|
98
|
+
b.member { b.name("faultString"); b.value { b.string(format_exception(e)) }}
|
99
|
+
end;end;end;end
|
100
|
+
b.target!
|
101
|
+
ensure
|
102
|
+
@temp.close!
|
103
|
+
end
|
104
|
+
|
105
|
+
[200, {"Content-Length"=> Rack::Utils.bytesize(body), "Content-Type" => 'text/xml; charset=utf-8'}, body]
|
106
|
+
end
|
107
|
+
|
108
|
+
def format_exception(e)
|
109
|
+
first_line = [e.class.to_s, e.message].join(' : ')
|
110
|
+
([first_line] + e.backtrace.to_a).join("\n")
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,282 @@
|
|
1
|
+
# A sample MetaWeblog RPC handler. Bear in mind that you will need to rewrite most of it.
|
2
|
+
#
|
3
|
+
# Define the standard methods for MetaWeblog API here. If you have a valiation problem or somesuch
|
4
|
+
# within the method just raise from there, exceptions will be translated into RPC fault codes.
|
5
|
+
#
|
6
|
+
# You will need to take a look at (and override) get_entry and friends to retrofit that to your own engine.
|
7
|
+
#
|
8
|
+
# Entry struct
|
9
|
+
#
|
10
|
+
# A fundamental unit of the MT/MW API is the Entry struct (a Hash). Return that Hash
|
11
|
+
# anytime an entry struct is required. Here a short intro on the fields of the struct (you can use
|
12
|
+
# both symbols and strings for keys):
|
13
|
+
# title, for the title of the entry;
|
14
|
+
# description, for the body of the entry;
|
15
|
+
# dateCreated, to set the created-on date of the entry;
|
16
|
+
# In addition, Movable Type’s implementation allows you to pass in values for five other keys:
|
17
|
+
# int mt_allow_comments, the value for the allow_comments field;
|
18
|
+
# int mt_allow_pings, the value for the allow_pings field;
|
19
|
+
# String mt_convert_breaks, the value for the convert_breaks field;
|
20
|
+
# String mt_text_more, the value for the additional entry text;
|
21
|
+
# String mt_excerpt, the value for the excerpt field;
|
22
|
+
# String mt_keywords, the value for the keywords field;
|
23
|
+
# String mt_basename, the value for the slug field;
|
24
|
+
# array mt_tb_ping_urls, the list of TrackBack ping URLs for this entry;
|
25
|
+
#
|
26
|
+
# If specified, dateCreated should be in ISO.8601 format.
|
27
|
+
# Also note that most blogging clients will have BIG issues if you supply non-UTC timestamps, so if you
|
28
|
+
# are using ActiveRecord timezone support (and you should!) take care to do all of your RPC operations
|
29
|
+
# with all timezones switched to UTC.
|
30
|
+
class BlogRPC::SampleHandler < BlogRPC::BasicHandler
|
31
|
+
|
32
|
+
# An example mapping for the XMLRPC fieldnames to ActiveRecord fields.
|
33
|
+
STRUCT_TO_ENTRY = {
|
34
|
+
"title" => "title",
|
35
|
+
"description" => "body",
|
36
|
+
"dateCreated" => "created_at",
|
37
|
+
"mt_text_more" => "more",
|
38
|
+
"mt_basename" => "slug",
|
39
|
+
"mt_allow_comments" => "allow_comments",
|
40
|
+
"permalink" => "permalink",
|
41
|
+
"link" => "permalink",
|
42
|
+
"postId" => "id"
|
43
|
+
}
|
44
|
+
|
45
|
+
STRUCT_TO_CATEGORY = {
|
46
|
+
"categoryId" => "id",
|
47
|
+
"categoryName" => "title",
|
48
|
+
}
|
49
|
+
|
50
|
+
# Should return something like [{:key => '__markdown__', :label => "Markdown"}]
|
51
|
+
# This is used to form a formatting menu in blog clients that allow choices in formatting
|
52
|
+
rpc "mt.supportedTextFilters", :out => :array do
|
53
|
+
[
|
54
|
+
{:key => '__default__', :label => "Convert Line Breaks"},
|
55
|
+
{:key => '__markdown__', :label => "Markdown"}
|
56
|
+
]
|
57
|
+
end
|
58
|
+
|
59
|
+
# Toggle the draft flag and return true.
|
60
|
+
rpc "mt.publishPost", :in => [:int, :string, :string], :out => :bool do | postid, user, pw |
|
61
|
+
login!(user, pw)
|
62
|
+
get_entry(postid).update_attributes :draft => false
|
63
|
+
true
|
64
|
+
end
|
65
|
+
|
66
|
+
# Delete the post. Appkey can be ignored since it's Google specific.
|
67
|
+
rpc "blogger.deletePost", :in => [:string, :int, :string, :string, :bool], :out => :bool do | appkey, postid, user, pw, void |
|
68
|
+
login! user, pw
|
69
|
+
get_entry(postid).destroy
|
70
|
+
true
|
71
|
+
end
|
72
|
+
|
73
|
+
# Return an Array of Hashes from here that define available categories.
|
74
|
+
# Category hashes look like this: {categoryId: 1, categoryTitle: "Bad RPC practices"}
|
75
|
+
rpc "mt.getCategoryList", :in => [:int, :string, :string], :out => :array do | blogid, user, pw |
|
76
|
+
login! user, pw
|
77
|
+
check_blog_permission! user, blogid
|
78
|
+
find_all_categories.map do | c |
|
79
|
+
category_to_struct(c)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Get the categories of a specific entry.
|
84
|
+
# Return an Array of Hashes from here - categories assigned to the passed post.
|
85
|
+
# You can also set {isPrimary: false} or true to denote the primary category.
|
86
|
+
rpc "mt.getPostCategories", :in => [:int, :string, :string], :out => :array do | postid, user, pw |
|
87
|
+
login! user, pw
|
88
|
+
cats = get_entry(postid).categories
|
89
|
+
cats.map {|c| category_to_struct(c).merge(:isPrimary => false) }
|
90
|
+
end
|
91
|
+
|
92
|
+
# Sets the post categories. The last argument is an Array of category Hashes, see above.
|
93
|
+
rpc "mt.setPostCategories", :in => [:int, :string, :string, :array], :out => :bool do | postid, user,pw, categories |
|
94
|
+
login! user, pw
|
95
|
+
entry = get_entry(postid)
|
96
|
+
set_category_ids(entry, categories.map{|c| c["categoryId"]})
|
97
|
+
true
|
98
|
+
end
|
99
|
+
|
100
|
+
# Returns the recent post titles. This is used by blog clients to speed up listings of entries since
|
101
|
+
# this only retreives some information. By default, this method should send the following data about an entry:
|
102
|
+
# userId, dateCreated, postId, title
|
103
|
+
rpc "mt.getRecentPostTitles", :in => [:int, :string, :string, :int], :out => :array do | blogid, user, pw, num |
|
104
|
+
login! user, pw
|
105
|
+
defaults = {:userId => 1}
|
106
|
+
short_keys = %w( dateCreated postId title userid)
|
107
|
+
latest_entries(num).map do | entry |
|
108
|
+
struct = {}
|
109
|
+
short_keys.each { | k | struct[k] = entry[STRUCT_TO_ENTRY[k]] }
|
110
|
+
defaults.merge(struct)
|
111
|
+
struct
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# Returns the trackback pings of the entry. Most likely you will not be using this.
|
116
|
+
# {
|
117
|
+
# pingTitle string -- the title of the entry sent in the ping
|
118
|
+
# pingURL string -- the URL of the entry
|
119
|
+
# pingIP string -- the IP address of the host that sent the ping
|
120
|
+
# }
|
121
|
+
# rpc "mt.getTrackbackPings", :in => [:int, :string, :string], :out => :array do | entry_id, user, pw |
|
122
|
+
# login! user, pw
|
123
|
+
# []
|
124
|
+
# end
|
125
|
+
|
126
|
+
# Create a new post. The entry Hash has all the standard fields. Should return the ID of the entry created as a String
|
127
|
+
rpc "metaWeblog.newPost", :in => [:int, :string, :string, :struct, :bool], :out => :string do | blogid, user, pw, entry_struct, publish_bit |
|
128
|
+
login! user, pw
|
129
|
+
e = make_new_entry
|
130
|
+
change_entry(e, entry_struct, publish_bit)
|
131
|
+
e[e.class.primary_key].to_s
|
132
|
+
end
|
133
|
+
|
134
|
+
# Change an entry replacing fields in the passed Hash. The last argument is the "publish bit" - whether the entry is published or draft.
|
135
|
+
# Return true when change succeeded.
|
136
|
+
rpc "metaWeblog.editPost", :in => [:int, :string, :string, :struct, :bool], :out => :bool do | entry_id, user, pw, entry_struct, publish_bit |
|
137
|
+
login! user, pw
|
138
|
+
change_entry(get_entry(entry_id), entry_struct, publish_bit)
|
139
|
+
true
|
140
|
+
end
|
141
|
+
|
142
|
+
# Returns an array of blogs [{:url => blog_url, :blogid => "1", :blogName => "The only blog here"}]
|
143
|
+
# The irony is that this is the way MT supports many blog RPCs on one install, so it's a good idea
|
144
|
+
# to handle that at least as a bogus function. We will provide a reasonable default.
|
145
|
+
rpc "blogger.getUsersBlogs", :in => [:string, :string, :string], :out => :array do | void, user, pw |
|
146
|
+
login! user, pw
|
147
|
+
blog = {:url => blog_url, :blogid => "1", :blogName => "The only blog here"}
|
148
|
+
[blog]
|
149
|
+
end
|
150
|
+
|
151
|
+
# Retreive an entry by ID. Should return the entry struct.
|
152
|
+
rpc "metaWeblog.getPost", :in => [:int, :string, :string], :out => :struct do | entry, user, pw |
|
153
|
+
login! user, pw
|
154
|
+
entry_to_struct( get_entry(entry) )
|
155
|
+
end
|
156
|
+
|
157
|
+
# Retreive N last posts, but in their complete form (unlinke getRecentPostTitles)
|
158
|
+
rpc "metaWeblog.getRecentPosts", :in => [:int, :string, :string, :int], :out => :array do | blogid, user, pw, num |
|
159
|
+
login! user, pw
|
160
|
+
check_blog_permission! user, blogid
|
161
|
+
latest_entries(num).map {|e| entry_to_struct(e) }
|
162
|
+
end
|
163
|
+
|
164
|
+
# Creates a file on the server. The passed file Hash has:
|
165
|
+
# bits: the byte content of the upload, in a String
|
166
|
+
# name: the path to put the file to relative to the site root
|
167
|
+
# It should return a struct like {url: "http://z.com/img.png"}
|
168
|
+
# Note that at least for MarsEdit the URL of the uploaded image should be canonical (with host data)
|
169
|
+
# for the preview to display properly.
|
170
|
+
# Remember the Rack env object is available for resolving hosts and such!
|
171
|
+
rpc "metaWeblog.newMediaObject", :in => [:int, :string, :string, :struct], :out => :struct do | blogid, user, pw, file_struct |
|
172
|
+
login! user, pw
|
173
|
+
check_blog_permission! user, blogid
|
174
|
+
|
175
|
+
file_struct['name'] = file_struct["name"].gsub(/\.\./, '').squeeze('/').gsub(/\s/, '_')
|
176
|
+
sanitized_name = File.expand_path(File.join(site_root, file_struct["name"]))
|
177
|
+
FileUtils.mkdir_p(File.dirname(sanitized_name))
|
178
|
+
|
179
|
+
# Wind the file name if such a file exists
|
180
|
+
dir, file = File.dirname(sanitized_name), File.basename(sanitized_name)
|
181
|
+
parts = file.split(/\./)
|
182
|
+
ext = parts.pop
|
183
|
+
counter = 2
|
184
|
+
#dbg "Detected sanitized name #{sanitized_name}"
|
185
|
+
while File.exist?(sanitized_name)
|
186
|
+
#dbg "Already uploaded, winding counter"
|
187
|
+
sanitized_name = File.join(dir, [parts, counter, ext].join('.'))
|
188
|
+
#puts "Made #{sanitized_name}"
|
189
|
+
counter += 1
|
190
|
+
end
|
191
|
+
|
192
|
+
File.open(sanitized_name, 'w') { |o| o << file_struct["bits"] }
|
193
|
+
|
194
|
+
return {:url => File.join(env["HOST"], sanitized_name), :saved_to => sanitized_name }
|
195
|
+
end
|
196
|
+
|
197
|
+
private
|
198
|
+
|
199
|
+
# The following methods are just an example of how you would approach such a handler.
|
200
|
+
# Normally you would rewrite almost all of the rpc method implementations.
|
201
|
+
|
202
|
+
# Get the site root
|
203
|
+
def site_root
|
204
|
+
File.dirname(__FILE__)
|
205
|
+
end
|
206
|
+
|
207
|
+
# Get an entry by ID
|
208
|
+
def get_entry(id)
|
209
|
+
Entry.find(id)
|
210
|
+
end
|
211
|
+
|
212
|
+
# Get categories of the entry. The entry passed will be one recieved from one of your own methods
|
213
|
+
def get_categories_of(entry)
|
214
|
+
entry.categories.map{|c| category_to_struct(c) }
|
215
|
+
end
|
216
|
+
|
217
|
+
# Assign category ids to an entry
|
218
|
+
def set_category_ids(entry, ids)
|
219
|
+
entry.category_ids = ids
|
220
|
+
entry.save!
|
221
|
+
end
|
222
|
+
|
223
|
+
# Convert a category to RPC struct (hash)
|
224
|
+
def category_to_struct(c)
|
225
|
+
STRUCT_TO_CATEGORY.inject({}) do | struct, kv |
|
226
|
+
struct[kv[0]] = c[kv[1]].to_s
|
227
|
+
struct
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
def find_all_categories
|
232
|
+
Category.find(:all)
|
233
|
+
end
|
234
|
+
|
235
|
+
# Get a fresh entry
|
236
|
+
def make_new_entry
|
237
|
+
Entry.new
|
238
|
+
end
|
239
|
+
|
240
|
+
# Change an entry with data in the entry_struct, honoring the publish bit
|
241
|
+
def change_entry(entry_obj, entry_struct, publish_bit)
|
242
|
+
entry_struct.each_pair do | k, v |
|
243
|
+
# ActiveRecord YAMLifies that if we are not careful. XML-RPC gives us Time.to_gm by default
|
244
|
+
v = v.to_time if(v.is_a?(XMLRPC::DateTime))
|
245
|
+
model_field = STRUCT_TO_ENTRY[k]
|
246
|
+
entry_obj.send("#{model_field}=", v) if model_field
|
247
|
+
end
|
248
|
+
entry_obj.save!
|
249
|
+
end
|
250
|
+
|
251
|
+
# Return the latest N entries
|
252
|
+
def latest_entries(n)
|
253
|
+
Entry.find(:all, :order => 'created_at DESC', :limit => n)
|
254
|
+
end
|
255
|
+
|
256
|
+
# Transform an entry into a struct
|
257
|
+
def entry_to_struct(entry)
|
258
|
+
STRUCT_TO_ENTRY.inject({}) do | struct, kv |
|
259
|
+
k, v = kv
|
260
|
+
|
261
|
+
# Dates and times have to pass through unscathed, converted to utc (!)
|
262
|
+
struct[k] = if entry[v].respond_to?(:strftime)
|
263
|
+
entry[v].utc
|
264
|
+
else
|
265
|
+
entry[v].to_s
|
266
|
+
end
|
267
|
+
struct
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
# Raise from here if the user is illegal
|
272
|
+
def login!(user, pass)
|
273
|
+
end
|
274
|
+
|
275
|
+
# Return your blog url from here
|
276
|
+
def blog_url
|
277
|
+
end
|
278
|
+
|
279
|
+
# Raise from here if the user cannot post to this specific blog
|
280
|
+
def check_blog_permission!(blog_id, user)
|
281
|
+
end
|
282
|
+
end
|
data/lib/blogrpc.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
module BlogRPC
|
2
|
+
VERSION = "1.0.0"
|
3
|
+
|
4
|
+
# Generate an RPC Rack application and yield it's only handler class to the passed block.
|
5
|
+
# Call rpc(...) on the yielded class to define methods
|
6
|
+
def self.generate_endpoint(&blk)
|
7
|
+
handler_class = Class.new(BlogRPC::BasicHandler)
|
8
|
+
yield(handler_class)
|
9
|
+
BlogRPC::RackApp.new(handler_class.new)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
require File.dirname(__FILE__) + "/blogrpc/rack_app"
|
14
|
+
require File.dirname(__FILE__) + "/blogrpc/basic_handler"
|
15
|
+
require File.dirname(__FILE__) + "/blogrpc/sample_handler"
|
data/test/helper.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
require 'http_simulator'
|
4
|
+
require 'test/unit'
|
5
|
+
require 'flexmock'
|
6
|
+
require 'flexmock/test_unit'
|
7
|
+
require 'net/http'
|
8
|
+
|
9
|
+
# http://redmine.ruby-lang.org/issues/4882
|
10
|
+
# https://github.com/jimweirich/flexmock/issues/4
|
11
|
+
# https://github.com/julik/flexmock/commit/4acea00677e7b558bd564ec7c7630f0b27d368ca
|
12
|
+
class FlexMock::PartialMockProxy
|
13
|
+
def singleton?(method_name)
|
14
|
+
@obj.singleton_methods.include?(method_name.to_s)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
begin
|
19
|
+
Bundler.setup(:default, :development)
|
20
|
+
rescue Bundler::BundlerError => e
|
21
|
+
$stderr.puts e.message
|
22
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
23
|
+
exit e.status_code
|
24
|
+
end
|
25
|
+
require 'test/unit'
|
26
|
+
|
27
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
28
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
29
|
+
require 'blogrpc'
|
30
|
+
|
31
|
+
class Test::Unit::TestCase
|
32
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# A barebones HTTP simulator for using with XML-RPC
|
2
|
+
class HTTPSimulator
|
3
|
+
attr_reader :path
|
4
|
+
|
5
|
+
class MockResponse
|
6
|
+
attr_reader :status, :headers, :body
|
7
|
+
def initialize(s, h, b)
|
8
|
+
@status, @headers, @body = s, h, b
|
9
|
+
end
|
10
|
+
|
11
|
+
def code
|
12
|
+
@status
|
13
|
+
end
|
14
|
+
|
15
|
+
def method_missing(*a)
|
16
|
+
puts "KALLED #{a.inspect}"
|
17
|
+
super
|
18
|
+
end
|
19
|
+
|
20
|
+
def get_fields(field)
|
21
|
+
self[field]
|
22
|
+
end
|
23
|
+
|
24
|
+
def [](k)
|
25
|
+
@headers[k]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize(test_case, endpoint_url)
|
30
|
+
@path = endpoint_url
|
31
|
+
@test_case = test_case
|
32
|
+
raise "The passed TestCase should support response()" unless @test_case.respond_to?(:response)
|
33
|
+
raise "The passed TestCase should support request()" unless @test_case.respond_to?(:request)
|
34
|
+
end
|
35
|
+
|
36
|
+
def version_1_2; end
|
37
|
+
|
38
|
+
def post2(port, request_body, headers)
|
39
|
+
@test_case.post(path, params = {}, headers.merge("rack.input" => StringIO.new(request_body)))
|
40
|
+
MockResponse.new(@test_case.response.status.to_s, @test_case.response.headers, @test_case.response.body)
|
41
|
+
end
|
42
|
+
|
43
|
+
def get(path, request_body, headers)
|
44
|
+
headers.each_pair{|k,v,| @test_case.request[k] = v }
|
45
|
+
@test_case.get path, request_body
|
46
|
+
MockResponse.new(@test_case.response.status.to_s, @test_case.response.headers, @test_case.response.body)
|
47
|
+
end
|
48
|
+
|
49
|
+
def head(path, request_body, headers)
|
50
|
+
headers.each_pair{|k,v,| @test_case.request[k] = v }
|
51
|
+
@test_case.head path, request_body
|
52
|
+
MockResponse.new(@test_case.response.status.to_s, @test_case.response.headers, @test_case.response.body)
|
53
|
+
end
|
54
|
+
|
55
|
+
def start(*a)
|
56
|
+
yield(*a) if block_given?
|
57
|
+
end
|
58
|
+
|
59
|
+
def method_missing(m, *args)
|
60
|
+
# puts "Called #{m}"
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'xmlrpc/client'
|
3
|
+
require 'rack/test'
|
4
|
+
|
5
|
+
class TestRackHandler < Test::Unit::TestCase
|
6
|
+
include Rack::Test::Methods
|
7
|
+
|
8
|
+
def response
|
9
|
+
last_response
|
10
|
+
end
|
11
|
+
|
12
|
+
def app
|
13
|
+
BlogRPC.generate_endpoint do | handler |
|
14
|
+
handler.rpc "mt.getPostCategories", :in => [:int, :string, :string], :out => :array do | postid, user, pw |
|
15
|
+
[{categoryId: 1, categoryName: "Awesome posts"}]
|
16
|
+
end
|
17
|
+
|
18
|
+
handler.rpc "junk.processJunk", :in => [], :out => :boolean do
|
19
|
+
true
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_method_table
|
25
|
+
# Make sure Net::HTTP makes that request into the testcase instead of the Interwebs
|
26
|
+
http = HTTPSimulator.new(self, "/rpc.xml")
|
27
|
+
flexmock(Net::HTTP) { |mock| mock.should_receive(:new).once.and_return(http) }
|
28
|
+
client = XMLRPC::Client.new("localhost", "/rpc.xml", 80)
|
29
|
+
|
30
|
+
methods_via_rpc = client.call("mt.supportedMethods")
|
31
|
+
assert_equal %w( mt.supportedMethods mt.getPostCategories junk.processJunk ), methods_via_rpc
|
32
|
+
assert_equal "text/xml; charset=utf-8", last_response['Content-Type']
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_get_returns_rsd
|
36
|
+
get '/rpc.xml'
|
37
|
+
assert_equal "application/rsd+xml", last_response['Content-Type']
|
38
|
+
assert last_response.body.include?('api name="MovableType" preferred="true"')
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TesRpcHandlerMethodDefinitions < Test::Unit::TestCase
|
4
|
+
|
5
|
+
class Sub < BlogRPC::BasicHandler
|
6
|
+
rpc "julik.testMethod", :in => [:int], :out => :array do
|
7
|
+
[]
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class SubSub < Sub
|
12
|
+
rpc "julik.anotherMethod", :in => [:int, :struct], :out => :array do
|
13
|
+
[]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_methods_propagated_on_inheritance_chain
|
18
|
+
basics = BlogRPC::BasicHandler.rpc_methods_and_signatures
|
19
|
+
only_supported = {"supported_methods" => ["mt.supportedMethods", "array supportedMethods()"] }
|
20
|
+
assert_equal only_supported, basics
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_methods_on_inherited_class_included_into_table
|
24
|
+
all_rpc_methods = Sub.rpc_methods_and_signatures
|
25
|
+
spliced_from_two_classes = {
|
26
|
+
"supported_methods" => ["mt.supportedMethods", "array supportedMethods()"],
|
27
|
+
"test_method"=>["julik.testMethod", "array testMethod(int)"]
|
28
|
+
}
|
29
|
+
assert_equal spliced_from_two_classes, all_rpc_methods
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_methods_on_inherited_sub_sub_class_included_into_table
|
33
|
+
all_rpc_methods = SubSub.rpc_methods_and_signatures
|
34
|
+
spliced_from_two_classes = {
|
35
|
+
"supported_methods" => ["mt.supportedMethods", "array supportedMethods()"],
|
36
|
+
"test_method" => ["julik.testMethod", "array testMethod(int)"],
|
37
|
+
"another_method" => ["julik.anotherMethod", "array anotherMethod(int, struct)"]
|
38
|
+
}
|
39
|
+
assert_equal spliced_from_two_classes, all_rpc_methods
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_method_introspection_called_by_xmlrpc
|
43
|
+
sub = SubSub.new
|
44
|
+
method_table = sub.get_methods(nil, '.')
|
45
|
+
assert_equal 3, method_table.length, "The method table should include 3 method signatures"
|
46
|
+
first_method = method_table[0]
|
47
|
+
assert_equal "julik.anotherMethod", first_method[0], "The first item in the method description should be a namespaced name"
|
48
|
+
assert_kind_of Proc, first_method[1], "The second item should be the proc handling the call"
|
49
|
+
assert_equal 'array anotherMethod(int, struct)', first_method[2], "The last item should a C-like method signature with types"
|
50
|
+
end
|
51
|
+
end
|
metadata
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: blogrpc
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Julik Tarkhanov
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-08-11 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: builder
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 3.0.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 3.0.0
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rdoc
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '3.12'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '3.12'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: jeweler
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 1.8.4
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.8.4
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: flexmock
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ~>
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0.8'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ~>
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0.8'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: rack-test
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
description:
|
95
|
+
email: me@julik.nl
|
96
|
+
executables: []
|
97
|
+
extensions: []
|
98
|
+
extra_rdoc_files:
|
99
|
+
- LICENSE.txt
|
100
|
+
- README.rdoc
|
101
|
+
files:
|
102
|
+
- .document
|
103
|
+
- Gemfile
|
104
|
+
- Gemfile.lock
|
105
|
+
- LICENSE.txt
|
106
|
+
- README.rdoc
|
107
|
+
- Rakefile
|
108
|
+
- blogrpc.gemspec
|
109
|
+
- lib/blogrpc.rb
|
110
|
+
- lib/blogrpc/basic_handler.rb
|
111
|
+
- lib/blogrpc/rack_app.rb
|
112
|
+
- lib/blogrpc/sample_handler.rb
|
113
|
+
- test/helper.rb
|
114
|
+
- test/http_simulator.rb
|
115
|
+
- test/test_blogapi.rb
|
116
|
+
- test/test_rpc_handler_method_definitions.rb
|
117
|
+
homepage: http://github.com/julik/blogrpc
|
118
|
+
licenses:
|
119
|
+
- MIT
|
120
|
+
post_install_message:
|
121
|
+
rdoc_options: []
|
122
|
+
require_paths:
|
123
|
+
- lib
|
124
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
125
|
+
none: false
|
126
|
+
requirements:
|
127
|
+
- - ! '>='
|
128
|
+
- !ruby/object:Gem::Version
|
129
|
+
version: '0'
|
130
|
+
segments:
|
131
|
+
- 0
|
132
|
+
hash: -815078792002993529
|
133
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
134
|
+
none: false
|
135
|
+
requirements:
|
136
|
+
- - ! '>='
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
requirements: []
|
140
|
+
rubyforge_project:
|
141
|
+
rubygems_version: 1.8.24
|
142
|
+
signing_key:
|
143
|
+
specification_version: 3
|
144
|
+
summary: Easily construct MT and MetaWeblog XML-RPC backends
|
145
|
+
test_files: []
|