blogrpc 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
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: []