appcast 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,5 @@
1
+ == 1.0.0 / 2007-08-25
2
+
3
+ * 1 major enhancement
4
+ * Birthday!
5
+
data/Manifest.txt ADDED
@@ -0,0 +1,14 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ bin/mongrel_appcast
6
+ lib/appcast.rb
7
+ lib/appcast/client.rb
8
+ lib/appcast/handlers.rb
9
+ lib/appcast/message.rb
10
+ lib/database.sqlite
11
+ test/appcast.yml
12
+ test/client_test.rb
13
+ test/message_test.rb
14
+ test/test_helper.rb
data/README.txt ADDED
@@ -0,0 +1,48 @@
1
+ appcast
2
+ by FIX (your name)
3
+ FIX (url)
4
+
5
+ == DESCRIPTION:
6
+
7
+ FIX (describe your package)
8
+
9
+ == FEATURES/PROBLEMS:
10
+
11
+ * FIX (list of features or problems)
12
+
13
+ == SYNOPSIS:
14
+
15
+ FIX (code sample of usage)
16
+
17
+ == REQUIREMENTS:
18
+
19
+ * FIX (list of requirements)
20
+
21
+ == INSTALL:
22
+
23
+ * FIX (sudo gem install, anything else)
24
+
25
+ == LICENSE:
26
+
27
+ (The MIT License)
28
+
29
+ Copyright (c) 2007 Rick Olson
30
+
31
+ Permission is hereby granted, free of charge, to any person obtaining
32
+ a copy of this software and associated documentation files (the
33
+ 'Software'), to deal in the Software without restriction, including
34
+ without limitation the rights to use, copy, modify, merge, publish,
35
+ distribute, sublicense, and/or sell copies of the Software, and to
36
+ permit persons to whom the Software is furnished to do so, subject to
37
+ the following conditions:
38
+
39
+ The above copyright notice and this permission notice shall be
40
+ included in all copies or substantial portions of the Software.
41
+
42
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
43
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
44
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
45
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
46
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
47
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
48
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,24 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+ $: << 'lib'
6
+ require 'appcast'
7
+
8
+ Hoe.new('appcast', Appcast::VERSION) do |p|
9
+ p.rubyforge_name = 'appcast'
10
+ p.author = ['Tobi Lütke', 'Rick Olson']
11
+ p.email = 'technoweenie@gmail.com'
12
+ # p.summary = 'FIX'
13
+ # p.description = p.paragraphs_of('README.txt', 2..5).join("\n\n")
14
+ # p.url = p.paragraphs_of('README.txt', 0).first.split(/\n/)[1..-1]
15
+ p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
16
+ p.extra_deps << 'mongrel'
17
+ p.extra_deps << 'builder'
18
+ p.extra_deps << 'activesupport'
19
+ p.extra_deps << 'activerecord'
20
+ p.test_globs << 'test/**/*_test.rb'
21
+ p.bin_files = ['mongrel_appcast']
22
+ end
23
+
24
+ # vim: syntax=Ruby
@@ -0,0 +1,241 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'yaml'
5
+ require 'mongrel'
6
+ require 'appcast'
7
+ require 'etc'
8
+ require 'cgi_multipart_eof_fix' rescue nil
9
+
10
+ module Mongrel
11
+ class Start < GemPlugin::Plugin "/commands"
12
+ include Mongrel::Command::Base
13
+
14
+ def configure
15
+ options [
16
+ ["-d", "--daemonize", "Run daemonized in the background", :@daemon, false],
17
+ ['-p', '--port PORT', "Which port to bind to", :@port, 3000],
18
+ ['-a', '--address ADDR', "Address to bind to", :@address, "0.0.0.0"],
19
+ ['-l', '--log FILE', "Where to write log messages", :@log_file, "log/appcast.log"],
20
+ ['-P', '--pid FILE', "Where to write the PID", :@pid_file, "log/appcast.pid"],
21
+ ['-n', '--num-procs INT', "Number of processors active before clients denied", :@num_procs, 1024],
22
+ ['-t', '--timeout TIME', "Timeout all requests after 100th seconds time", :@timeout, 0],
23
+ ['-c', '--chdir PATH', "Change to dir before starting (will be expanded)", :@cwd, Dir.pwd],
24
+ ['-B', '--debug', "Enable debugging mode", :@debug, false],
25
+ ['-C', '--config PATH', "Use a config file", :@config_file, nil],
26
+ ['-S', '--script PATH', "Load the given file as an extra config script", :@config_script, nil],
27
+ ['-G', '--generate PATH', "Generate a config file for use with -C", :@generate, nil],
28
+ ['', '--user USER', "User to run as", :@user, nil],
29
+ ['', '--group GROUP', "Group to run as", :@group, nil],
30
+ ['', '--prefix PATH', "URL prefix for Rails app", :@prefix, nil],
31
+ ['', '--stats-path PATH', "Path for the stats handler", :@stats_path, '/'],
32
+ ['', '--queue-path PATH', "Path for the queue handler", :@queue_path, '/queue'],
33
+ ['', '--messages-path PATH', "Path for the messages handler", :@messages_path, '/messages']
34
+ ]
35
+ end
36
+
37
+ def validate
38
+ @cwd = File.expand_path(@cwd)
39
+ valid_dir? @cwd, "Invalid path to change to during daemon mode: #@cwd"
40
+
41
+ # Change there to start, then we'll have to come back after daemonize
42
+ Dir.chdir(@cwd)
43
+
44
+ valid?(@prefix[0].chr == "/" && @prefix[-1].chr != "/", "Prefix must begin with / and not end in /") if @prefix
45
+ valid_dir? File.dirname(@log_file), "Path to log file not valid: #@log_file"
46
+ valid_dir? File.dirname(@pid_file), "Path to pid file not valid: #@pid_file"
47
+ valid_exists? @config_file, "Config file not there: #@config_file" if @config_file
48
+ valid_dir? File.dirname(@generate), "Problem accessing directory to #@generate" if @generate
49
+ valid_user? @user if @user
50
+ valid_group? @group if @group
51
+
52
+ return @valid
53
+ end
54
+
55
+ def run
56
+ # Config file settings will override command line settings
57
+ settings = { :host => @address, :port => @port, :cwd => @cwd,
58
+ :log_file => @log_file, :pid_file => @pid_file, :daemon => @daemon,
59
+ :includes => ["mongrel"], :config_script => @config_script,
60
+ :num_processors => @num_procs, :timeout => @timeout, :debug => @debug,
61
+ :user => @user, :group => @group, :prefix => @prefix, :config_file => @config_file,
62
+ :stats_path => @stats_path, :queue_path => @queue_path, :messages_path => @messages_path,
63
+ :db => { 'adapter' => nil, 'database' => 'database.sqlite'}
64
+ }
65
+
66
+ if @generate
67
+ STDERR.puts "** Writing config to \"#@generate\"."
68
+ open(@generate, "w") {|f| f.write(settings.to_yaml) }
69
+ STDERR.puts "** Finished. Run \"mongrel_appcast -C #@generate\" to use the config file."
70
+ exit 0
71
+ end
72
+
73
+ if @config_file
74
+ settings.merge! YAML.load_file(@config_file)
75
+ STDERR.puts "** Loading settings from #{@config_file} (they override command line)." unless settings[:daemon]
76
+ end
77
+
78
+ if settings[:db]['adapter'].nil?
79
+ begin
80
+ require 'sqlite'
81
+ settings[:db]['adapter'] ||= 'sqlite'
82
+ rescue LoadError
83
+ end
84
+ end
85
+ if settings[:db]['adapter'].nil?
86
+ begin
87
+ require 'sqlite3'
88
+ settings[:db]['adapter'] ||= 'sqlite3'
89
+ rescue LoadError
90
+ end
91
+ end
92
+ if settings[:db]['adapter'].nil?
93
+ STDERR.puts "** No default adapter selected. sqlite and sqlite3 are not available."
94
+ exit 1
95
+ end
96
+
97
+ config = Mongrel::Configurator.new(settings) do
98
+ if defaults[:daemon]
99
+ if File.exist? defaults[:pid_file]
100
+ log "!!! PID file #{defaults[:pid_file]} already exists. Mongrel could be running already. Check your #{defaults[:log_file]} for errors."
101
+ log "!!! Exiting with error. You must stop mongrel and clear the .pid before I'll attempt a start."
102
+ exit 1
103
+ end
104
+
105
+ daemonize
106
+ log "Daemonized, any open files are closed. Look at #{defaults[:pid_file]} and #{defaults[:log_file]} for info."
107
+ log "Settings loaded from #{@config_file} (they override command line)." if @config_file
108
+ end
109
+
110
+ log "Starting Mongrel listening at #{defaults[:host]}:#{defaults[:port]}"
111
+
112
+ listener do
113
+ Appcast.configure_database defaults[:db], defaults[:log_file], defaults[:debug] ? 'DEBUG' : nil
114
+ uri defaults[:stats_path], :handler => Appcast::StatsHandler.new
115
+ uri defaults[:queue_path], :handler => Appcast::QueueHandler.new
116
+ uri defaults[:messages_path], :handler => Appcast::MessageHandler.new
117
+ log "Appcast v#{Appcast::VERSION} started."
118
+
119
+ if defaults[:config_script]
120
+ log "Loading #{defaults[:config_script]} external config script"
121
+ run_config(defaults[:config_script])
122
+ end
123
+ end
124
+ end
125
+
126
+ config.run
127
+ config.log "Mongrel available at #{settings[:host]}:#{settings[:port]}"
128
+
129
+ if config.defaults[:daemon]
130
+ config.write_pid_file
131
+ else
132
+ config.log "Use CTRL-C to stop."
133
+ end
134
+
135
+ config.join
136
+
137
+ if config.needs_restart
138
+ if RUBY_PLATFORM !~ /mswin/
139
+ cmd = "ruby #{__FILE__} start #{original_args.join(' ')}"
140
+ config.log "Restarting with arguments: #{cmd}"
141
+ config.stop
142
+ config.remove_pid_file
143
+
144
+ if config.defaults[:daemon]
145
+ system cmd
146
+ else
147
+ STDERR.puts "Can't restart unless in daemon mode."
148
+ exit 1
149
+ end
150
+ else
151
+ config.log "Win32 does not support restarts. Exiting."
152
+ end
153
+ end
154
+ end
155
+ end
156
+
157
+ def Mongrel::send_signal(signal, pid_file)
158
+ pid = open(pid_file).read.to_i
159
+ print "Sending #{signal} to Mongrel at PID #{pid}..."
160
+ begin
161
+ Process.kill(signal, pid)
162
+ rescue Errno::ESRCH
163
+ puts "Process does not exist. Not running."
164
+ end
165
+
166
+ puts "Done."
167
+ end
168
+
169
+
170
+ class Stop < GemPlugin::Plugin "/commands"
171
+ include Mongrel::Command::Base
172
+
173
+ def configure
174
+ options [
175
+ ['-c', '--chdir PATH', "Change to dir before starting (will be expanded).", :@cwd, "."],
176
+ ['-f', '--force', "Force the shutdown (kill -9).", :@force, false],
177
+ ['-w', '--wait SECONDS', "Wait SECONDS before forcing shutdown", :@wait, "0"],
178
+ ['-P', '--pid FILE', "Where the PID file is located.", :@pid_file, "log/mongrel.pid"]
179
+ ]
180
+ end
181
+
182
+ def validate
183
+ @cwd = File.expand_path(@cwd)
184
+ valid_dir? @cwd, "Invalid path to change to during daemon mode: #@cwd"
185
+
186
+ Dir.chdir @cwd
187
+
188
+ valid_exists? @pid_file, "PID file #@pid_file does not exist. Not running?"
189
+ return @valid
190
+ end
191
+
192
+ def run
193
+ if @force
194
+ @wait.to_i.times do |waiting|
195
+ exit(0) if not File.exist? @pid_file
196
+ sleep 1
197
+ end
198
+
199
+ Mongrel::send_signal("KILL", @pid_file) if File.exist? @pid_file
200
+ else
201
+ Mongrel::send_signal("TERM", @pid_file)
202
+ end
203
+ end
204
+ end
205
+
206
+
207
+ class Restart < GemPlugin::Plugin "/commands"
208
+ include Mongrel::Command::Base
209
+
210
+ def configure
211
+ options [
212
+ ['-c', '--chdir PATH', "Change to dir before starting (will be expanded)", :@cwd, '.'],
213
+ ['-s', '--soft', "Do a soft restart rather than a process exit restart", :@soft, false],
214
+ ['-P', '--pid FILE', "Where the PID file is located", :@pid_file, "log/mongrel.pid"]
215
+ ]
216
+ end
217
+
218
+ def validate
219
+ @cwd = File.expand_path(@cwd)
220
+ valid_dir? @cwd, "Invalid path to change to during daemon mode: #@cwd"
221
+
222
+ Dir.chdir @cwd
223
+
224
+ valid_exists? @pid_file, "PID file #@pid_file does not exist. Not running?"
225
+ return @valid
226
+ end
227
+
228
+ def run
229
+ if @soft
230
+ Mongrel::send_signal("HUP", @pid_file)
231
+ else
232
+ Mongrel::send_signal("USR2", @pid_file)
233
+ end
234
+ end
235
+ end
236
+ end
237
+
238
+
239
+ if not Mongrel::Command::Registry.instance.run ARGV
240
+ exit 1
241
+ end
data/lib/appcast.rb ADDED
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Appcast is a simple message server somewhat similar to Amazon's SQS but based
4
+ # around simple REST principles
5
+ #
6
+ # There is no UI for the server. All the communication is done using HTTP verbs.
7
+ #
8
+ # == Adding messages
9
+ #
10
+ # You can add messages to the server using simple HTTP post
11
+ #
12
+ # curl http://localhost:3000/queue/products/updates -d 'id:5454'
13
+ # => 201 Created
14
+ # => Location: http://localhost:3000/messages/1
15
+ #
16
+ # To delete a message you can use
17
+ #
18
+ # curl http://localhost:3000/messages/1 -x DELETE
19
+ # => 200 OK
20
+ #
21
+ # You can get new messages from a namespace with simple GET requests
22
+ # note that you will always receive all descendent messages. if you posted your messages
23
+ # to /products/updates
24
+ #
25
+ # curl http://localhost:3000/queue/products
26
+ # => 200 OK
27
+ # => XML feed of new messages
28
+ #
29
+ # curl http://localhost:3000/queue/products?limit=1&lock_for=300
30
+ # => 200 OK
31
+ # => XML feed with one message which will be locked for 5 minutes during which you should hangle and delete it.
32
+ #
33
+ # Once you receive messages you can DELETE or PUT to their location parameter to remove the
34
+ # message from the server or clear any locks on the message so that it will show up again
35
+ # in GET queries.
36
+
37
+ require 'appcast/message'
38
+ require 'appcast/handlers'
39
+
40
+ module Appcast
41
+ VERSION = '1.0.0'
42
+
43
+ def self.configure_database(config, log_file, log_level = nil)
44
+ raise "Missing database config" if config.empty?
45
+ ActiveRecord::Base.logger = Logger.new(log_file)
46
+ ActiveRecord::Base.logger.level = Logger.const_get((log_level || "INFO").upcase)
47
+ ActiveRecord::Base.establish_connection(config)
48
+ ActiveRecord::Base.default_timezone = :utc
49
+
50
+ unless config['adapter'] !~ /sqlite/ || File.exists?(config['database'])
51
+ Appcast::Message.create_table
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,67 @@
1
+ require 'rubygems'
2
+ require 'net/http'
3
+ require 'uri'
4
+ require 'active_support'
5
+
6
+
7
+ module Appcast
8
+ class ClientError < StandardError; end
9
+ class Client
10
+ class Message
11
+ attr_reader :connection
12
+ attr_accessor :id, :name, :content
13
+
14
+ def initialize(connection, attributes = {})
15
+ @connection = connection
16
+ attributes.each do |key, value|
17
+ send("#{key}=", value)
18
+ end if attributes
19
+ end
20
+
21
+ def location=(value)
22
+ self.id = value.to_s.scan(/\d+$/).first
23
+ end
24
+
25
+ def location
26
+ "/messages/#{id}"
27
+ end
28
+
29
+ def destroy
30
+ connection.delete(URI.escape(location)).code == '200'
31
+ end
32
+ end
33
+
34
+ attr_reader :connection
35
+
36
+ # Symbolized option hash
37
+ def initialize(options = {})
38
+ raise ClientError, "No :host option given." unless options.key?(:host)
39
+ @connection = Net::HTTP.new(options[:host], options[:port])
40
+ end
41
+
42
+ def create(name, content)
43
+ response = connection.post(URI.escape("/queue/#{name}"), content)
44
+ Message.new(connection, :id => response["Location"].scan(/\d+$/).first, :name => name, :content => content)
45
+ end
46
+
47
+ def list(name, options = {})
48
+ params = options.inject([]) do |memo, (key, value)|
49
+ memo << "#{key}=#{value}" if [:limit, :lock_for].include?(key)
50
+ memo
51
+ end
52
+ params = params.empty? ? '' : "?#{params * "&"}"
53
+
54
+ response = connection.get(URI.escape("/queue/#{name}#{params}"))
55
+ messages = Hash.from_xml(response.body)['messages']
56
+ return [] if messages.nil? || !messages.key?('message')
57
+ messages = messages['message']
58
+ if messages.respond_to?(:collect!)
59
+ messages.collect! do |hash|
60
+ Message.new(connection, hash)
61
+ end
62
+ else
63
+ [Message.new(connection, messages)]
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,82 @@
1
+ require 'rubygems'
2
+ require 'mongrel'
3
+
4
+ class Mongrel::HttpRequest
5
+ def method; params[Mongrel::Const::REQUEST_METHOD]; end
6
+ def path; params[Mongrel::Const::PATH_INFO]; end
7
+ def uri; params[Mongrel::Const::REQUEST_URI]; end
8
+ def query_params; @query_params ||= Mongrel::HttpRequest.query_parse(params['QUERY_STRING']); end
9
+ end
10
+
11
+ module Appcast
12
+ class BaseHandler < Mongrel::HttpHandler
13
+ def initialize
14
+ @guard = Mutex.new
15
+ end
16
+ end
17
+
18
+ # Stats handler which just displays some
19
+ # basic stats on whats going on in the server
20
+ #
21
+ class StatsHandler < BaseHandler
22
+ LINES = "%-30s %-8s\n"
23
+
24
+ def process(request, response)
25
+ response.start do |h, out|
26
+ h['Content-Type'] = 'text/plain'
27
+ @guard.synchronize do
28
+ Message.stats.each do |k, v|
29
+ out.write(LINES % [k.strip, v])
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ class QueueHandler < BaseHandler
37
+ def process(request, response)
38
+ case request.method
39
+ when 'POST'
40
+ msg = nil
41
+ @guard.synchronize { msg = Message.create(:name => request.path, :content => request.body.read ) }
42
+ response.start(201) { |head,out| head['Location'] = msg.location; out.write(msg.to_xml) }
43
+ puts "Added message at #{request.path}"
44
+ when 'GET'
45
+ msgs = nil
46
+ @guard.synchronize { msgs = Message.get(request.path, request.query_params['limit'] || 10, request.query_params['lock_for'] || false) }
47
+ response.start { |head,out| out.write(msgs.to_xml(:root => 'messages')) }
48
+ end
49
+ rescue => e
50
+ puts e.message
51
+ puts e.backtrace.join("\n")
52
+ response.start(500) {}
53
+ end
54
+ end
55
+
56
+ class MessageHandler < BaseHandler
57
+ ID_SCANNER = /\/(\d+)$/.freeze
58
+
59
+ def process(request, response)
60
+ id = request.path.scan(ID_SCANNER)[0].first
61
+
62
+ case request.method
63
+ when 'DELETE'
64
+ @guard.synchronize { Message.destroy(id) }
65
+ response.start(200) {}
66
+ when 'PUT'
67
+ @guard.synchronize { Message.find(id).unlock }
68
+ response.start(200) {}
69
+ when 'GET'
70
+ msg = nil
71
+ @guard.synchronize { msg = Message.find(id) }
72
+ response.start { |h,out| h['Location'] = msg.location; out.write(msg.to_xml) }
73
+ end
74
+ rescue ActiveRecord::RecordNotFound
75
+ response.start(404) { |h,out| out.write("Unknown message with id #{id}") }
76
+ rescue => e
77
+ puts e.message
78
+ puts e.backtrace.join("\n")
79
+ response.start(500) {}
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,54 @@
1
+ require 'rubygems'
2
+ require 'active_record'
3
+ require 'builder'
4
+
5
+ module Appcast
6
+ class Message < ActiveRecord::Base
7
+ def self.create_table
8
+ connection.create_table :messages, :force => true do |t|
9
+ t.column :name, :string
10
+ t.column :content, :text
11
+ t.column :locked_until, :datetime
12
+ t.column :created_at, :datetime
13
+ end
14
+
15
+ connection.add_index :messages, :name
16
+ end
17
+
18
+ def self.get(path, limit, lock_seconds)
19
+ now = Time.now.utc
20
+
21
+ returning records = find(:all, :conditions => ["name LIKE ? AND (locked_until IS NULL OR locked_until < ?)", "#{path}%", now], :order => 'id', :limit => limit) do
22
+ if lock_seconds and !records.empty?
23
+ update_all(['locked_until = ?', now + lock_seconds.to_f], ['id IN (?)', records.collect(&:id)])
24
+ end
25
+ end
26
+ end
27
+
28
+ def unlock
29
+ update_attribute :locked_until, nil
30
+ end
31
+
32
+ def location
33
+ "/messages/#{id}"
34
+ end
35
+
36
+ def to_xml(options = {})
37
+ options[:only] = [:name, :content]
38
+ options[:methods] = [:location]
39
+ super
40
+ end
41
+
42
+ def self.stats
43
+ s = connection.select_all("SELECT name, count(*) as count FROM messages GROUP by name").collect(&:values)
44
+ s.unshift '--------------------------------------------'
45
+ if recent = find(:first, :order => 'id ASC')
46
+ s.unshift ['Most recent', recent.created_at.to_s(:short)]
47
+ end
48
+ s.unshift ['Total', count]
49
+ s
50
+ end
51
+
52
+ validates_presence_of :name, :content
53
+ end
54
+ end
Binary file
data/test/appcast.yml ADDED
@@ -0,0 +1,9 @@
1
+ ip: 0.0.0.0
2
+ port: 3000
3
+ stats: /
4
+ queue: /queue
5
+ messages: /messages
6
+ log_level: debug
7
+ db:
8
+ adapter: sqlite3
9
+ database: test-database.sqlite
@@ -0,0 +1,90 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+ require 'appcast/client'
3
+
4
+ context "Client" do
5
+ setup do
6
+ @client = Appcast::Client.new(:host => 'test.host')
7
+ end
8
+
9
+ specify "should initialize with http object" do
10
+ @client.connection.class.should == Net::HTTP
11
+ end
12
+
13
+ specify "should require http host option" do
14
+ assert_raises Appcast::ClientError do
15
+ Appcast::Client.new
16
+ end
17
+ end
18
+
19
+ specify "should create message" do
20
+ @client.connection.expects(:post).with("/queue/test", "foobar").returns({'Location' => '/messages/55'})
21
+ message = @client.create('test', 'foobar')
22
+ message.class.should == Appcast::Client::Message
23
+ message.id.should == '55'
24
+ message.name.should == 'test'
25
+ message.content.should == 'foobar'
26
+ end
27
+
28
+ specify "should list messages" do
29
+ mock_response = OpenStruct.new
30
+ mock_response.body = <<-ENDXML
31
+ <?xml version="1.0" encoding="UTF-8"?>
32
+ <messages>
33
+ <message><content>test 1</content><name>/test</name><location>/messages/1</location></message>
34
+ <message><content>test 2</content><name>/test</name><location>/messages/2</location></message>
35
+ </messages>
36
+ ENDXML
37
+
38
+ @client.connection.expects(:get).with("/queue/test").returns(mock_response)
39
+
40
+ messages = @client.list 'test'
41
+ messages.size == 2
42
+ messages[0].content.should == 'test 1'
43
+ messages[0].id.should == '1'
44
+ messages[1].content.should == 'test 2'
45
+ messages[1].id.should == '2'
46
+ end
47
+
48
+ specify "should list message" do
49
+ mock_response = OpenStruct.new
50
+ mock_response.body = <<-ENDXML
51
+ <?xml version="1.0" encoding="UTF-8"?>
52
+ <messages>
53
+ <message><content>test 1</content><name>/test</name><location>/messages/1</location></message>
54
+ </messages>
55
+ ENDXML
56
+
57
+ @client.connection.expects(:get).with("/queue/test").returns(mock_response)
58
+
59
+ messages = @client.list 'test'
60
+ messages.size == 1
61
+ messages[0].content.should == 'test 1'
62
+ messages[0].id.should == '1'
63
+ end
64
+
65
+ specify "should list no messages" do
66
+ mock_response = OpenStruct.new(:body => %(<?xml version="1.0" encoding="UTF-8"?><messages></messages>))
67
+ @client.connection.expects(:get).with("/queue/test").returns(mock_response)
68
+
69
+ messages = @client.list 'test'
70
+ messages.size == 0
71
+ end
72
+
73
+ specify "should list messages with limit parameter" do
74
+ @client.connection.expects(:get).with("/queue/test?limit=5").returns \
75
+ OpenStruct.new(:body => %(<?xml version="1.0" encoding="UTF-8"?><messages></messages>))
76
+ @client.list 'test', :limit => 5
77
+ end
78
+
79
+ specify "should list messages with lock_for parameter" do
80
+ @client.connection.expects(:get).with("/queue/test?lock_for=5").returns \
81
+ OpenStruct.new(:body => %(<?xml version="1.0" encoding="UTF-8"?><messages></messages>))
82
+ @client.list 'test', :lock_for => 5, :ignored => 7
83
+ end
84
+
85
+ specify "should delete message" do
86
+ message = Appcast::Client::Message.new(Object.new, :id => 5)
87
+ message.connection.expects(:delete).with("/messages/5").returns(OpenStruct.new(:code => '200'))
88
+ message.destroy.should == true
89
+ end
90
+ end
@@ -0,0 +1,15 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+ require 'appcast/message'
3
+
4
+ config = YAML.load_file(File.join(File.dirname(__FILE__), 'appcast.yml'))
5
+ ActiveRecord::Base.establish_connection(config['db'])
6
+ ActiveRecord::Base.default_timezone = :utc
7
+ Appcast::Message.create_table unless File.exists?(File.join(File.dirname(__FILE__), config['db']['database']))
8
+
9
+ context "Message" do
10
+ specify "should return correct location" do
11
+ msg = Appcast::Message.new
12
+ msg.expects(:id).returns(5)
13
+ msg.location.should == '/messages/5'
14
+ end
15
+ end
@@ -0,0 +1,10 @@
1
+ require 'test/unit'
2
+ require 'rubygems'
3
+ require 'test/spec'
4
+ require 'fileutils'
5
+ require 'active_support'
6
+ require 'mocha'
7
+ require 'logger'
8
+ require 'ostruct'
9
+
10
+ $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
metadata ADDED
@@ -0,0 +1,109 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.9.2
3
+ specification_version: 1
4
+ name: appcast
5
+ version: !ruby/object:Gem::Version
6
+ version: 1.0.0
7
+ date: 2007-08-26 00:00:00 -05:00
8
+ summary: The author was too lazy to write a summary
9
+ require_paths:
10
+ - lib
11
+ email: technoweenie@gmail.com
12
+ homepage: http://www.zenspider.com/ZSS/Products/appcast/
13
+ rubyforge_project: appcast
14
+ description: The author was too lazy to write a description
15
+ autorequire:
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ post_install_message:
29
+ authors:
30
+ - "Tobi L\xC3\xBCtke"
31
+ - Rick Olson
32
+ files:
33
+ - History.txt
34
+ - Manifest.txt
35
+ - README.txt
36
+ - Rakefile
37
+ - bin/mongrel_appcast
38
+ - lib/appcast.rb
39
+ - lib/appcast/client.rb
40
+ - lib/appcast/handlers.rb
41
+ - lib/appcast/message.rb
42
+ - lib/database.sqlite
43
+ - test/appcast.yml
44
+ - test/client_test.rb
45
+ - test/message_test.rb
46
+ - test/test_helper.rb
47
+ test_files:
48
+ - test/test_helper.rb
49
+ - test/client_test.rb
50
+ - test/message_test.rb
51
+ rdoc_options:
52
+ - --main
53
+ - README.txt
54
+ extra_rdoc_files:
55
+ - History.txt
56
+ - Manifest.txt
57
+ - README.txt
58
+ executables:
59
+ - mongrel_appcast
60
+ extensions: []
61
+
62
+ requirements: []
63
+
64
+ dependencies:
65
+ - !ruby/object:Gem::Dependency
66
+ name: mongrel
67
+ version_requirement:
68
+ version_requirements: !ruby/object:Gem::Version::Requirement
69
+ requirements:
70
+ - - ">"
71
+ - !ruby/object:Gem::Version
72
+ version: 0.0.0
73
+ version:
74
+ - !ruby/object:Gem::Dependency
75
+ name: builder
76
+ version_requirement:
77
+ version_requirements: !ruby/object:Gem::Version::Requirement
78
+ requirements:
79
+ - - ">"
80
+ - !ruby/object:Gem::Version
81
+ version: 0.0.0
82
+ version:
83
+ - !ruby/object:Gem::Dependency
84
+ name: activesupport
85
+ version_requirement:
86
+ version_requirements: !ruby/object:Gem::Version::Requirement
87
+ requirements:
88
+ - - ">"
89
+ - !ruby/object:Gem::Version
90
+ version: 0.0.0
91
+ version:
92
+ - !ruby/object:Gem::Dependency
93
+ name: activerecord
94
+ version_requirement:
95
+ version_requirements: !ruby/object:Gem::Version::Requirement
96
+ requirements:
97
+ - - ">"
98
+ - !ruby/object:Gem::Version
99
+ version: 0.0.0
100
+ version:
101
+ - !ruby/object:Gem::Dependency
102
+ name: hoe
103
+ version_requirement:
104
+ version_requirements: !ruby/object:Gem::Version::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: 1.3.0
109
+ version: