Skip to content
Browse files

add strat

  • Loading branch information...
1 parent da59653 commit c6d6711cc25662fe65d710fb4401a182dfca1d8e @technoweenie committed
View
2 Gemfile
@@ -15,8 +15,10 @@ group :server do
gem "redis"
gem "riak-client"
gem "sinatra"
+ gem "stratocaster", :path => "vendor/stratocaster"
end
group :importers do
gem "twitter"
+ gem "instagram"
end
View
12 Gemfile.lock
@@ -1,3 +1,8 @@
+PATH
+ remote: ./vendor/stratocaster
+ specs:
+ stratocaster (0.0.1)
+
GEM
remote: http://rubygems.org/
specs:
@@ -25,9 +30,14 @@ GEM
faraday (~> 0.5.3)
hashie (0.4.0)
i18n (0.5.0)
+ instagram (0.3.2)
+ addressable
+ nibbler (>= 1.2.0)
+ yajl-ruby
multi_json (0.0.5)
multi_xml (0.2.0)
multipart-post (1.1.0)
+ nibbler (1.2.1)
rack (1.2.1)
redis (2.1.1)
riak-client (0.8.3)
@@ -62,9 +72,11 @@ DEPENDENCIES
adapter-riak
excon
faraday
+ instagram
redis
riak-client
sinatra
+ stratocaster!
toystore
twitter
yajl-ruby
View
4 Rakefile
@@ -24,6 +24,10 @@ namespace :import do
import_tweet ENV['TWEET'].to_i
end
+ task :instagram => :init do
+ #import_instagram
+ end
+
# STARS_URL - String URL to AllOfTheStars instance. Defaults to
# http://allofthestars.com
# CLUSTER_ID - String Cluster ID
View
4 lib/allofthestars/server.rb
@@ -1,3 +1,7 @@
+require 'bundler'
+
+Bundler.setup(:default, :server)
+
require 'toystore'
require 'adapter'
require 'adapter/riak'
View
22 vendor/stratocaster/LICENSE
@@ -0,0 +1,22 @@
+The MIT License
+
+Copyright (c) GitHub, Inc
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
View
110 vendor/stratocaster/README.md
@@ -0,0 +1,110 @@
+# Stratocaster
+
+## Overview
+
+Stratocaster is a system for storing and retrieving messages on
+timelines. A message can contain any arbitrary payload. A timeline is a
+filtered stream of messages. Complex querying is replaced in favor of
+creating multiple timelines as filters for the messages. Stratocaster
+uses abstract adapters to persist the data, instead of being bound to
+any one type of data store.
+
+Some of these ideas came from the way [FriendFeed uses MySQL to store
+schemaless objects][friendfeed].
+
+[friendfeed]: http://bret.appspot.com/entry/how-friendfeed-uses-mysql
+
+### Message
+
+A Message is a schema-less entity intended to be delivered to one or
+more timelines. Stratocaster will take any ActiveModel compatible model
+(ActiveRecord, [ToyStore][toys], etc) and convert it to a Hash with
+these required keys:
+
+* `id` - the unique identifier.
+* `created_at` - the timestamp the Message was created.
+* `actor` - an object with at least an `id` property to identify the
+ user that created the message.
+* `payload` - an object with custom values for the Message.
+
+A Message would look something like this as a Ruby hash:
+
+ {'id' => 123,
+ 'created_at' => <time>,
+ 'actor' => {'id' => 12, 'name' => 'bob'},
+ 'payload' => {'repository' => {'id' => 1, 'name' => 'user/repo'},
+ 'title' => '...', ...},
+ ...
+ }
+
+[toys]: https://github.com/newtoy/toystore
+
+### Timeline
+
+A Timeline is a pre-computed view of messages that meet a certain
+criteria. A Stratocaster instance knows which possible timelines a
+message can be delivered to. As each message comes in, Stratocaster
+finds the timelines that are applicable to the message.
+
+Each timeline is responsible for persisting the Message by its Id and
+retrieving the Ids in pages. These Ids are then passed to the data
+store that Messages are persisted.
+
+## Ruby API
+
+First, you need to define the Timelines:
+
+ class RepositoryTimeline < Stratocaster::Timeline
+ # Timelines can use multiple adapters
+ adapter :redis, :host => '...', :default => true
+ adapter :mysql, ...
+
+ # This method is used to determine if this Timeline should receive
+ # the incoming Message.
+ def self.accept?(message)
+ !message.repository
+ end
+
+ def initialize(repository_id)
+ @key = "repo:#{repository_id}"
+ end
+
+ def self.deliver(message)
+ adapters.each do |adapter|
+ adapter.deliver(message.id)
+ end
+ end
+ end
+
+Create an instance of Stratocaster to start processing messages.
+
+ strat = Stratocaster.new PublicTimeline, RepositoryTimeline, ...
+
+ # Add or remove them on the instance.
+ strat.timelines << ActorTimeline
+ strat.timelines.unshift RecipientTimeline
+
+Now, you can start processing messages!
+
+ strat.receive(message)
+
+Internally, this calls:
+
+ strat.timelines.each do |timeline|
+ timeline.deliver(message) if timeline.accept?(message)
+ end
+
+To query a timeline, create an instance of the Timeline.
+
+ repo = Repository.find(1)
+ redis_timeline = RepositoryTimeline.new(repo.id, :per_page => 50)
+
+ # Get the first page of the most recent messages.
+ redis_timeline.page(1) # uses the default adapter
+ # => [12, 26, 230]
+
+ # specify the adapter
+ mysql_timeline = RepositoryTimeline.new(repo.id, :adapter => :mysql)
+
+ # or change it on an existing timeline
+ redis_timeline.adapter = :mysql
View
151 vendor/stratocaster/Rakefile
@@ -0,0 +1,151 @@
+require 'rubygems'
+require 'rake'
+require 'date'
+
+#############################################################################
+#
+# Helper functions
+#
+#############################################################################
+
+def name
+ @name ||= Dir['*.gemspec'].first.split('.').first
+end
+
+def version
+ line = File.read("lib/#{name}.rb")[/^\s*VERSION\s*=\s*.*/]
+ line.match(/.*VERSION\s*=\s*['"](.*)['"]/)[1]
+end
+
+def date
+ Date.today.to_s
+end
+
+def rubyforge_project
+ name
+end
+
+def gemspec_file
+ "#{name}.gemspec"
+end
+
+def gem_file
+ "#{name}-#{version}.gem"
+end
+
+def replace_header(head, header_name)
+ head.sub!(/(\.#{header_name}\s*= ').*'/) { "#{$1}#{send(header_name)}'"}
+end
+
+#############################################################################
+#
+# Standard tasks
+#
+#############################################################################
+
+task :default => :test
+
+require 'rake/testtask'
+Rake::TestTask.new(:test) do |test|
+ test.libs << 'lib' << 'test'
+ test.pattern = 'test/**/*_test.rb'
+ test.verbose = true
+end
+
+desc "Generate RCov test coverage and open in your browser"
+task :coverage do
+ require 'rcov'
+ sh "rm -fr coverage"
+ sh "rcov test/test_*.rb"
+ sh "open coverage/index.html"
+end
+
+require 'rake/rdoctask'
+Rake::RDocTask.new do |rdoc|
+ rdoc.rdoc_dir = 'rdoc'
+ rdoc.title = "#{name} #{version}"
+ rdoc.rdoc_files.include('README*')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+end
+
+desc "Open an irb session preloaded with this library"
+task :console do
+ sh "irb -rubygems -I ./lib -r #{name}.rb"
+end
+
+#############################################################################
+#
+# Custom tasks (add your own tasks here)
+#
+#############################################################################
+
+
+
+#############################################################################
+#
+# Packaging tasks
+#
+#############################################################################
+
+desc "Create tag v#{version} and build and push #{gem_file} to Rubygems"
+task :release => :build do
+ unless `git branch` =~ /^\* master$/
+ puts "You must be on the master branch to release!"
+ exit!
+ end
+ sh "git commit --allow-empty -a -m 'Release #{version}'"
+ sh "git tag v#{version}"
+ sh "git push origin master"
+ sh "git push origin v#{version}"
+ sh "gem push pkg/#{name}-#{version}.gem"
+end
+
+desc "Build #{gem_file} into the pkg directory"
+task :build => :gemspec do
+ sh "mkdir -p pkg"
+ sh "gem build #{gemspec_file}"
+ sh "mv #{gem_file} pkg"
+end
+
+desc "Generate #{gemspec_file}"
+task :gemspec => :validate do
+ # read spec file and split out manifest section
+ spec = File.read(gemspec_file)
+ head, manifest, tail = spec.split(" # = MANIFEST =\n")
+
+ # replace name version and date
+ replace_header(head, :name)
+ replace_header(head, :version)
+ replace_header(head, :date)
+ #comment this out if your rubyforge_project has a different name
+ replace_header(head, :rubyforge_project)
+
+ # determine file list from git ls-files
+ files = `git ls-files`.
+ split("\n").
+ sort.
+ reject { |file| file =~ /^\./ }.
+ reject { |file| file =~ /^(rdoc|pkg)/ }.
+ map { |file| " #{file}" }.
+ join("\n")
+
+ # piece file back together and write
+ manifest = " s.files = %w[\n#{files}\n ]\n"
+ spec = [head, manifest, tail].join(" # = MANIFEST =\n")
+ File.open(gemspec_file, 'w') { |io| io.write(spec) }
+ puts "Updated #{gemspec_file}"
+end
+
+desc "Validate #{gemspec_file}"
+task :validate do
+ libfiles = Dir['lib/*'] - ["lib/#{name}.rb", "lib/#{name}"]
+ unless libfiles.empty?
+ puts "Directory `lib` should only contain a `#{name}.rb` file and `#{name}` dir."
+ exit!
+ end
+ unless Dir['VERSION*'].empty?
+ puts "A `VERSION` file at root level violates Gem best practices."
+ exit!
+ end
+end
+
View
42 vendor/stratocaster/lib/stratocaster.rb
@@ -0,0 +1,42 @@
+# Stratocaster is a system for storing and retrieving Messages in Timelines.
+# A Timeline is a secondary index of Messages. Complex SQL queries are
+# replaced in favor of multiple/overlapping Timelines to filter Messages.
+# Abstract adapters are used to persist the data.
+class Stratocaster
+ VERSION = "0.0.1"
+
+ # Public: Each Stratocaster instance tracks which possible Timeline classes
+ # it can deliver Messages to.
+ attr_reader :timelines
+
+ def initialize(*timelines)
+ timelines.flatten!
+ @timelines = timelines
+ end
+
+ # Public: Processes a received Message. Since Stratocaster only stores the
+ # ID (usually, depends on the adapter), assume that the message is already
+ # stored in some other ActiveModel-compatible store (ActiveRecord, ToyStore,
+ # etc).
+ #
+ # message - A Hash:
+ # id - The String Message ID.
+ # created_at - The Time the Message was created.
+ # actor - A Hash with at least an `id` property to identify the
+ # user that created the message.
+ # payload - A Hash holding custom values for the Message.
+ #
+ # Returns an Array of String keys of Timelines that this message was
+ # delivered to.
+ def receive(message)
+ keys = @timelines.map do |timeline|
+ timeline.deliver(message) if timeline.accept?(message)
+ end
+ keys.compact!
+ keys
+ end
+end
+
+# Load up the rest of Stratocaster.
+require 'stratocaster/adapter'
+require 'stratocaster/timeline'
View
93 vendor/stratocaster/lib/stratocaster/adapter.rb
@@ -0,0 +1,93 @@
+# An Adapter handles the dirty job of actually persisting Message IDs
+# and retrieving them.
+class Stratocaster::Adapter
+ class << self
+ # A global default page size for Timelines.
+ attr_accessor :per_page,
+
+ # A global default total size for Timelines. Not always used by
+ # Adapters.
+ :max
+ end
+
+ self.per_page = 50
+ self.max = 300
+
+ # Subclasses get the same defaults.
+ def self.inherited(klass)
+ klass.per_page = self.per_page
+ klass.max = self.max
+ end
+
+ # Returns a reference to the raw DB driver.
+ attr_reader :client
+
+ # Returns a Hash of options to customize the Timeline behavior.
+ # per_page - A Fixnum specifying the page size for individual query
+ # results.
+ attr_reader :options
+
+ def initialize(client, options = {})
+ @client = client
+ @options = options
+ @options[:per_page] ||= self.class.per_page
+ @options[:max] ||= self.class.max
+ end
+
+ # Public: Stores the given Message ID in the Timeline identified by
+ # the given key.
+ #
+ # key - The String key of the Timeline.
+ # message - The same Hash from Stratocaster#receive.
+ #
+ # Returns nothing.
+ def store(key, message)
+ raise NotImplementedError
+ end
+
+ # Public: Queries the Timeline for a page of Message IDs.
+ #
+ # key - The String key of the Timeline.
+ # num - The Fixnum page number.
+ #
+ # Returns an Array of Message IDs.
+ def page(key, num)
+ raise NotImplementedError
+ end
+
+ # Public: Counts the Messages stored in a Timeline.
+ #
+ # key - The String key of the Timeline.
+ #
+ # Returns a Fixnum size.
+ def count(key)
+ raise NotImplementedError
+ end
+
+ # Public: Clears all Messages stored in a Timeline.
+ #
+ # key - The String key of the Timeline.
+ #
+ # Returns nothing.
+ def clear(key)
+ raise NotImplementedError
+ end
+
+ # Calculates the offset for the given page.
+ #
+ # page_num - The Fixnum page number.
+ #
+ # Returns a Fixnum offset.
+ def offset_for(page_num)
+ [page_num-1, 0].max * @options[:per_page]
+ end
+
+ def dup
+ self.class.new(@client, @options)
+ end
+end
+
+module Stratocaster::Adapters
+ autoload :Memory, 'stratocaster/adapters/memory'
+ autoload :Redis, 'stratocaster/adapters/redis'
+end
View
25 vendor/stratocaster/lib/stratocaster/adapters/memory.rb
@@ -0,0 +1,25 @@
+class Stratocaster::Adapters::Memory < Stratocaster::Adapter
+ def store(key, message)
+ id = message['id']
+ (@client[key] ||= []).unshift id.to_s
+ end
+
+ def page(key, num)
+ arr = @client[key]
+ return [] if !arr
+ offset = offset_for(num)
+ arr[offset...@options[:per_page]*num]
+ end
+
+ def count(key)
+ if arr = @client[key]
+ arr.size
+ else
+ 0
+ end
+ end
+
+ def clear(key)
+ @client.delete(key)
+ end
+end
View
29 vendor/stratocaster/lib/stratocaster/adapters/redis.rb
@@ -0,0 +1,29 @@
+class Stratocaster::Adapters::Redis < Stratocaster::Adapter
+ def store(key, message)
+ id = message['id']
+ redis_key = key_for(key)
+ @client.pipelined do
+ @client.lpush(redis_key, id)
+ if (max = @options[:max]) > 0
+ @client.ltrim(redis_key, 0, max-1)
+ end
+ end
+ end
+
+ def page(key, num)
+ off = offset_for(num)
+ @client.lrange(key_for(key), off, off + @options[:per_page] - 1) || []
+ end
+
+ def key_for(key)
+ [@options[:prefix], key].compact.join(":")
+ end
+
+ def count(key)
+ @client.llen(key)
+ end
+
+ def clear(key)
+ @client.del(key)
+ end
+end
View
92 vendor/stratocaster/lib/stratocaster/timeline.rb
@@ -0,0 +1,92 @@
+# A Timeline is responsible for deciding which Messages it can receive,
+# storing the Message IDs in the Adapters, and retrieving them.
+# Timelines should be subclassed to define custom key names and
+# acceptance conditions.
+class Stratocaster::Timeline
+ class << self
+ attr_writer :adapters
+ end
+
+ self.adapters = []
+
+ # Public: A Timeline can store Messages in multiple Adapters. Each
+ # Adapter is for a specific data store (Redis, MySQL, etc).
+ def self.adapters
+ @adapters ||= []
+ end
+
+ # Public: Determines if this message is valid for this Timeline. This
+ # should be modified in subclasses of this Timeline.
+ #
+ # message - The same Hash from Stratocaster#receive.
+ #
+ # Returns true if the Message is acceptable, or false.
+ def self.accept?(message)
+ true
+ end
+
+
+ # Public: Delivers the Message to this Timeline's adapters.
+ #
+ # message - The same Hash from Stratocaster#receive.
+ #
+ # Returns the String key of the Timeline.
+ def self.deliver(message)
+ key = key_for(message)
+ adapters.each do |adapter|
+ adapter.store(key, message)
+ end
+ key
+ end
+
+ # Public: Creates a unique Timeline key. The key is used to identify
+ # where the Message is added in the Adapter. In Redis, it'd be used
+ # to build the key of a Redis List. The generated key of the same
+ # Message in two Timelines are probably going to be different.
+ #
+ # message - The same Hash from Stratocaster#receive.
+ #
+ # Returns a String key.
+ def self.key_for(message)
+ "actor:#{message['actor']['id']}"
+ end
+
+ # Returns the String key of this Timeline instance.
+ attr_reader :key
+
+ # Returns the Adapter used for queries on this Timeline instance.
+ attr_reader :default_adapter
+
+ # Initializes a new Timeline instance with this Message for querying
+ # purposes.
+ #
+ # message - The same Hash from Stratocaster#receive.
+ # options - Optional Hash (reserved for future use).
+ def initialize(message, options = {})
+ @key = self.class.key_for(message)
+ @default_adapter = self.class.adapters.first
+ end
+
+ # Public: Queries the default Adapter for the _n_ page of Message IDs.
+ #
+ # num - A Fixnum page number.
+ #
+ # Returns an Array of Message IDs.
+ def page(num)
+ default_adapter.page(@key, num)
+ end
+
+ # Public: Counts the number of Messages in this Timeline.
+ #
+ # Returns a Fixnum size.
+ def count
+ default_adapter.count(@key)
+ end
+
+ # Public: Clears the Messages in this Timeline.
+ #
+ # Returns nothing.
+ def clear
+ default_adapter.clear(@key)
+ end
+end
View
83 vendor/stratocaster/stratocaster.gemspec
@@ -0,0 +1,83 @@
+## This is the rakegem gemspec template. Make sure you read and understand
+## all of the comments. Some sections require modification, and others can
+## be deleted if you don't need them. Once you understand the contents of
+## this file, feel free to delete any comments that begin with two hash marks.
+## You can find comprehensive Gem::Specification documentation, at
+## http://docs.rubygems.org/read/chapter/20
+Gem::Specification.new do |s|
+ s.specification_version = 2 if s.respond_to? :specification_version=
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
+ s.rubygems_version = '1.3.5'
+
+ ## Leave these as is they will be modified for you by the rake gemspec task.
+ ## If your rubyforge_project name is different, then edit it and comment out
+ ## the sub! line in the Rakefile
+ s.name = 'stratocaster'
+ s.version = '0.0.1'
+ s.date = '2011-03-17'
+ s.rubyforge_project = 'stratocaster'
+
+ ## Make sure your summary is short. The description may be as long
+ ## as you like.
+ s.summary = "Short description used in Gem listings."
+ s.summary = "A system for storing and retrieving messages on timelines."
+ s.description = "Long description. Maybe copied from the README."
+ s.description = <<-END
+ Stratocaster is a system for storing and retrieving messages on
+ timelines. A message can contain any arbitrary payload. A timeline is a
+ filtered stream of messages. Complex querying is replaced in favor of
+ creating multiple timelines as filters for the messages. Stratocaster
+ uses abstract adapters to persist the data, instead of being bound to
+ any one type of data store.
+ END
+
+ ## List the primary authors. If there are a bunch of authors, it's probably
+ ## better to set the email to an email list or something. If you don't have
+ ## a custom homepage, consider using your GitHub URL or the like.
+ s.authors = ["John Doe"]
+ s.email = 'jdoe@example.com'
+ s.homepage = 'http://example.com/NAME'
+
+ ## This gets added to the $LOAD_PATH so that 'lib/NAME.rb' can be required as
+ ## require 'NAME.rb' or'/lib/NAME/file.rb' can be as require 'NAME/file.rb'
+ s.require_paths = %w[lib]
+
+ ## Specify any RDoc options here. You'll want to add your README and
+ ## LICENSE files to the extra_rdoc_files list.
+ s.rdoc_options = ["--charset=UTF-8"]
+ s.extra_rdoc_files = %w[README.md LICENSE]
+
+ ## List your runtime dependencies here. Runtime dependencies are those
+ ## that are needed for an end user to actually USE your code.
+ #s.add_dependency('DEPNAME', [">= 1.1.0", "< 2.0.0"])
+
+ ## List your development dependencies here. Development dependencies are
+ ## those that are only needed during development
+ #s.add_development_dependency('DEVDEPNAME', [">= 1.1.0", "< 2.0.0"])
+
+ ## Leave this section as-is. It will be automatically generated from the
+ ## contents of your Git repository via the gemspec task. DO NOT REMOVE
+ ## THE MANIFEST COMMENTS, they are used as delimiters by the task.
+ # = MANIFEST =
+ s.files = %w[
+ LICENSE
+ README.md
+ Rakefile
+ lib/stratocaster.rb
+ lib/stratocaster/adapter.rb
+ lib/stratocaster/adapters/memory.rb
+ lib/stratocaster/adapters/redis.rb
+ lib/stratocaster/timeline.rb
+ stratocaster.gemspec
+ test/adapter_test.rb
+ test/helper.rb
+ test/stratocaster_test.rb
+ test/timeline_test.rb
+ ]
+ # = MANIFEST =
+
+ ## Test files will be grabbed from the file list. Make sure the path glob
+ ## matches what you actually use.
+ s.test_files = s.files.select { |path| path =~ /^test\/.*_test\.rb/ }
+end
+
View
60 vendor/stratocaster/test/adapter_test.rb
@@ -0,0 +1,60 @@
+require File.expand_path('../helper', __FILE__)
+
+class AdapterTest < Test::Unit::TestCase
+ class << self
+ attr_reader :adapter_block
+
+ def adapter
+ @adapter_block = Proc.new
+ end
+ end
+
+ adapter do |options|
+ Stratocaster::Adapters::Memory.new({}, options)
+ end
+
+ def setup
+ adapter_options = {:per_page => 2}
+ @adapter = self.class.adapter_block.call(adapter_options)
+ @adapter.clear "abc"
+ @adapter.store "abc", 'id' => 1
+ @adapter.store "abc", 'id' => 2
+ @adapter.store "abc", 'id' => 3
+ end
+
+ def test_retrieves_latest_message_ids
+ assert_equal %w(3 2), @adapter.page('abc', 1)
+ end
+
+ def test_retrieves_paginated_ids
+ assert_equal %w(1), @adapter.page('abc', 2)
+ end
+
+ def test_counts_messages
+ assert_equal 3, @adapter.count('abc')
+ assert_equal 0, @adapter.count('def')
+ end
+end
+
+require 'rubygems'
+
+begin
+ require 'redis'
+
+ class RedisTest < AdapterTest
+ adapter do |options|
+ Stratocaster::Adapters::Redis.new Redis.new, options
+ end
+
+ def test_doesnt_truncate_list_with_zero_max
+ no_max_adapter = @adapter.dup
+ no_max_adapter.options[:max] = 0
+ assert_equal 3, no_max_adapter.count('abc')
+ no_max_adapter.store 'abc', 'id' => 4
+ assert_equal 4, no_max_adapter.count('abc')
+ end
+ end
+
+rescue LoadError
+ puts "No Redis tests"
+end
View
4 vendor/stratocaster/test/helper.rb
@@ -0,0 +1,4 @@
+$: << File.expand_path('../../lib', __FILE__)
+
+require 'test/unit'
+require 'stratocaster'
View
34 vendor/stratocaster/test/stratocaster_test.rb
@@ -0,0 +1,34 @@
+require File.expand_path("../helper", __FILE__)
+
+class StratocasterTest < Test::Unit::TestCase
+ class CommentTimeline < Stratocaster::Timeline
+ self.adapters << Stratocaster::Adapters::Memory.new({})
+
+ def self.accept?(message)
+ !!message['payload']['comment']
+ end
+
+ def self.key_for(message)
+ "comment:#{message['payload']['comment']}"
+ end
+
+ def initialize(comment_id, options = {})
+ super({'payload' => {'comment' => comment_id}}, options)
+ end
+ end
+
+ def setup
+ @strat = Stratocaster.new CommentTimeline, Stratocaster::Timeline
+ @message = {'id' => 123, 'actor' => {'id' => 321}, 'payload' => {}}
+ @comment = {'id' => 124, 'actor' => {'id' => 320}, 'payload' => {'comment' => 5}}
+ end
+
+ def test_tracks_timeline_classes
+ assert_equal [CommentTimeline, Stratocaster::Timeline], @strat.timelines
+ end
+
+ def test_returns_list_of_delivered_timeline_keys
+ assert_equal %w(actor:321), @strat.receive(@message)
+ assert_equal %w(comment:5 actor:320), @strat.receive(@comment)
+ end
+end
View
52 vendor/stratocaster/test/timeline_test.rb
@@ -0,0 +1,52 @@
+require File.expand_path('../helper', __FILE__)
+
+class TimelineTest < Test::Unit::TestCase
+ class CommentTimeline < Stratocaster::Timeline
+ self.adapters << Stratocaster::Adapters::Memory.new({})
+
+ def self.accept?(message)
+ !!message['payload']['comment']
+ end
+
+ def self.key_for(message)
+ "comment:#{message['payload']['comment']}"
+ end
+
+ def initialize(comment_id, options = {})
+ super({'payload' => {'comment' => comment_id}}, options)
+ end
+ end
+
+ def setup
+ @message = {'id' => 123, 'actor' => {'id' => 321}, 'payload' => {}}
+ @comment = @message.merge('payload' => {'comment' => 5})
+
+ CommentTimeline.adapters.first.client.clear
+
+ CommentTimeline.deliver(@comment)
+ end
+
+ def test_deliver_returns_key
+ assert_equal 'actor:321', Stratocaster::Timeline.deliver(@message)
+ assert_equal 'comment:5', CommentTimeline.deliver(@comment)
+ end
+
+ def test_checks_if_timeline_accepts_message
+ assert !CommentTimeline.accept?(@message)
+ assert Stratocaster::Timeline.accept?(@message)
+ end
+
+ def test_queries_adapter_for_message_ids
+ assert_equal %w(123), CommentTimeline.new(5).page(1)
+ assert_equal %w(123), CommentTimeline.adapters.first.page('comment:5', 1)
+ end
+
+ def test_counts_messages_in_adapter
+ assert_equal 1, CommentTimeline.new(5).count
+ end
+
+ def test_uses_first_adapter_by_default
+ assert_equal "Stratocaster::Adapters::Memory",
+ CommentTimeline.new(5).default_adapter.class.name
+ end
+end

0 comments on commit c6d6711

Please sign in to comment.
Something went wrong with that request. Please try again.