appcast 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: