├── .rspec ├── .gitignore ├── lib ├── streama │ ├── version.rb │ ├── definition_dsl.rb │ ├── errors.rb │ ├── definition.rb │ ├── actor.rb │ └── activity.rb └── streama.rb ├── .document ├── spec ├── app │ └── models │ │ ├── album.rb │ │ ├── photo.rb │ │ ├── no_mongoid.rb │ │ ├── user.rb │ │ ├── mars │ │ └── user.rb │ │ └── activity.rb ├── lib │ ├── definition_dsl_spec.rb │ ├── definition_spec.rb │ ├── actor_spec.rb │ └── activity_spec.rb └── spec_helper.rb ├── Rakefile ├── Gemfile ├── .travis.yml ├── streama.gemspec ├── LICENSE.txt ├── Gemfile.lock └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | Gemfile.lock 4 | pkg/* 5 | -------------------------------------------------------------------------------- /lib/streama/version.rb: -------------------------------------------------------------------------------- 1 | module Streama 2 | VERSION = "0.3.8" 3 | end 4 | -------------------------------------------------------------------------------- /.document: -------------------------------------------------------------------------------- 1 | lib/**/*.rb 2 | bin/* 3 | - 4 | features/**/*.feature 5 | LICENSE.txt 6 | -------------------------------------------------------------------------------- /spec/app/models/album.rb: -------------------------------------------------------------------------------- 1 | class Album 2 | include Mongoid::Document 3 | 4 | field :title 5 | 6 | end -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task default: :spec 7 | -------------------------------------------------------------------------------- /spec/app/models/photo.rb: -------------------------------------------------------------------------------- 1 | class Photo 2 | include Mongoid::Document 3 | include Mongoid::Attributes::Dynamic 4 | 5 | field :file 6 | 7 | end 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in streama.gemspec 4 | gemspec 5 | 6 | gem 'mongoid', '~> 4', github: 'mongoid/mongoid' 7 | -------------------------------------------------------------------------------- /spec/app/models/no_mongoid.rb: -------------------------------------------------------------------------------- 1 | class NoMongoid 2 | include Streama::Actor 3 | 4 | field :full_name 5 | 6 | def followers 7 | self.class.all 8 | end 9 | 10 | end -------------------------------------------------------------------------------- /lib/streama.rb: -------------------------------------------------------------------------------- 1 | require "mongoid" 2 | require "streama/version" 3 | require "streama/actor" 4 | require "streama/activity" 5 | require "streama/definition" 6 | require "streama/definition_dsl" 7 | require "streama/errors" -------------------------------------------------------------------------------- /spec/app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User 2 | include Mongoid::Document 3 | include Streama::Actor 4 | 5 | field :full_name 6 | 7 | def friends 8 | self.class.all 9 | end 10 | 11 | def followers 12 | self.class.all 13 | end 14 | 15 | end -------------------------------------------------------------------------------- /spec/app/models/mars/user.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | module Mars 3 | class User 4 | include Mongoid::Document 5 | include Streama::Actor 6 | 7 | field :full_name 8 | 9 | def followers 10 | self.class.all 11 | end 12 | 13 | end 14 | 15 | end 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | services: mongodb 3 | matrix: 4 | include: 5 | - rvm: 1.9.3 6 | - rvm: ruby-head 7 | - rvm: jruby-19mode 8 | jdk: oraclejdk7 9 | env: JRUBY_OPTS="-Xmx512m" 10 | - rvm: jruby-19mode 11 | jdk: openjdk7 12 | env: JRUBY_OPTS="-Xmx512m" 13 | - rvm: jruby-head 14 | jdk: oraclejdk7 15 | env: JRUBY_OPTS="-Xmx512m" 16 | - rvm: jruby-head 17 | jdk: openjdk7 18 | env: JRUBY_OPTS="-Xmx512m" -------------------------------------------------------------------------------- /spec/app/models/activity.rb: -------------------------------------------------------------------------------- 1 | class Activity 2 | include Streama::Activity 3 | 4 | activity :new_photo do 5 | actor :user, :cache => [:full_name] 6 | object :photo, :cache => [:file] 7 | target_object :album, :cache => [:title] 8 | end 9 | 10 | activity :new_photo_without_cache do 11 | actor :user 12 | object :photo 13 | target_object :album 14 | end 15 | 16 | activity :new_comment do 17 | actor :user, :cache => [:full_name] 18 | object :photo 19 | end 20 | 21 | activity :new_tag do 22 | actor :user, :cache => [:full_name] 23 | object :photo 24 | end 25 | 26 | activity :new_mars_photo do 27 | actor :user, :cache => [:full_name], :class_name => 'Mars::User' 28 | object :photo 29 | target_object :album, :cache => [:title] 30 | end 31 | 32 | end 33 | -------------------------------------------------------------------------------- /lib/streama/definition_dsl.rb: -------------------------------------------------------------------------------- 1 | module Streama 2 | 3 | class DefinitionDSL 4 | 5 | attr_reader :attributes 6 | 7 | def initialize(name) 8 | @attributes = { 9 | :name => name.to_sym, 10 | :actor => {}, 11 | :object => {}, 12 | :target_object => {} 13 | } 14 | end 15 | 16 | delegate :[], :to => :@attributes 17 | 18 | def self.data_methods(*args) 19 | args.each do |method| 20 | define_method method do |*args| 21 | class_sym = if class_name = args[1].try(:delete,:class_name) 22 | class_name.underscore.to_sym 23 | else 24 | args[0].is_a?(Symbol) ? args[0] : args[0].class.to_sym 25 | end 26 | @attributes[method].store(class_sym, args[1]) 27 | end 28 | end 29 | end 30 | data_methods :actor, :object, :target_object 31 | 32 | end 33 | 34 | end 35 | -------------------------------------------------------------------------------- /streama.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "streama/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "streama" 7 | s.version = Streama::VERSION 8 | s.authors = ["Christos Pappas"] 9 | s.email = ["christos.pappas@gmail.com"] 10 | s.homepage = "" 11 | s.summary = %q{Activity Streams for Mongoid} 12 | s.description = %q{Streama is a simple activity stream gem for use with the Mongoid ODM framework} 13 | 14 | s.rubyforge_project = "streama" 15 | 16 | s.files = `git ls-files`.split("\n") 17 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 18 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 19 | s.require_paths = ["lib"] 20 | 21 | s.add_dependency("mongoid", ">= 3.0", "<= 5") 22 | 23 | s.add_development_dependency "rspec", "~> 2.5" 24 | s.add_development_dependency "database_cleaner", "~> 0.8" 25 | s.add_development_dependency "pry" 26 | s.add_development_dependency "rake" 27 | end 28 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Christos Pappas 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. 21 | -------------------------------------------------------------------------------- /lib/streama/errors.rb: -------------------------------------------------------------------------------- 1 | module Streama 2 | 3 | module Errors 4 | 5 | class StreamaError < StandardError 6 | end 7 | 8 | class InvalidActivity < StreamaError 9 | end 10 | 11 | # This error is raised when an object isn't defined 12 | # as an actor, object or target 13 | # 14 | # Example: 15 | # 16 | # InvalidField.new('field_name') 17 | class InvalidData < StreamaError 18 | attr_reader :message 19 | 20 | def initialize message 21 | @message = "Invalid Data: #{message}" 22 | end 23 | 24 | end 25 | 26 | # This error is raised when trying to store a field that doesn't exist 27 | # 28 | # Example: 29 | # 30 | # InvalidField.new('field_name') 31 | class InvalidField < StreamaError 32 | attr_reader :message 33 | 34 | def initialize message 35 | @message = "Invalid Field: #{message}" 36 | end 37 | 38 | end 39 | 40 | class ActivityNotSaved < StreamaError 41 | end 42 | 43 | class NoFollowersDefined < StreamaError 44 | end 45 | 46 | class NotMongoid < StreamaError 47 | end 48 | 49 | end 50 | 51 | end -------------------------------------------------------------------------------- /spec/lib/definition_dsl_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Definition" do 4 | 5 | let(:definition_dsl) {Streama::DefinitionDSL.new(:new_enquiry)} 6 | 7 | it "initializes with name" do 8 | definition_dsl.attributes[:name].should eq :new_enquiry 9 | end 10 | 11 | it "adds an actor to the definition" do 12 | dsl = definition_dsl 13 | dsl.actor(:user, :cache => [:id, :full_name]) 14 | dsl.attributes[:actor].should eq :user => { :cache=>[:id, :full_name] } 15 | end 16 | 17 | it "adds multiple actors to the definition" do 18 | dsl = definition_dsl 19 | dsl.actor(:user, :cache => [:id, :full_name]) 20 | dsl.actor(:company, :cache => [:id, :name]) 21 | dsl.attributes[:actor].should eq :user => { :cache=>[:id, :full_name] }, :company => { :cache=>[:id, :name] } 22 | end 23 | 24 | it "adds an object to the definition" do 25 | dsl = definition_dsl 26 | dsl.object(:listing, :cache => [:id, :title]) 27 | dsl.attributes[:object].should eq :listing => { :cache=>[:id, :title] } 28 | end 29 | 30 | it "adds a target to the definition" do 31 | dsl = definition_dsl 32 | dsl.target_object(:company, :cache => [:id, :name]) 33 | dsl.attributes[:target_object].should eq :company => { :cache=>[:id, :name] } 34 | end 35 | 36 | end 37 | -------------------------------------------------------------------------------- /lib/streama/definition.rb: -------------------------------------------------------------------------------- 1 | module Streama 2 | 3 | class Definition 4 | 5 | attr_reader :name, :actor, :object, :target_object, :receivers 6 | 7 | # @param dsl [Streama::DefinitionDSL] A DSL object 8 | def initialize(definition) 9 | @name = definition[:name] 10 | @actor = definition[:actor] || {} 11 | @object = definition[:object] || {} 12 | @target_object = definition[:target_object] || {} 13 | end 14 | 15 | # 16 | # Registers a new definition 17 | # 18 | # @param definition [Definition] The definition to register 19 | # @return [Definition] Returns the registered definition 20 | def self.register(definition) 21 | return false unless definition.is_a? DefinitionDSL 22 | definition = new(definition) 23 | self.registered << definition 24 | return definition || false 25 | end 26 | 27 | # List of registered definitions 28 | # @return [Array] 29 | def self.registered 30 | @definitions ||= [] 31 | end 32 | 33 | def self.find(name) 34 | unless definition = registered.find{|definition| definition.name == name.to_sym} 35 | raise Streama::Errors::InvalidActivity, "Could not find a definition for `#{name}`" 36 | else 37 | definition 38 | end 39 | end 40 | 41 | end 42 | 43 | end -------------------------------------------------------------------------------- /lib/streama/actor.rb: -------------------------------------------------------------------------------- 1 | module Streama 2 | 3 | module Actor 4 | extend ActiveSupport::Concern 5 | 6 | included do 7 | raise Errors::NotMongoid, "Must be included in a Mongoid::Document" unless self.ancestors.include? Mongoid::Document 8 | 9 | cattr_accessor :activity_klass 10 | end 11 | 12 | module ClassMethods 13 | 14 | def activity_class(klass) 15 | self.activity_klass = klass.to_s 16 | end 17 | 18 | end 19 | 20 | # Publishes the activity to the receivers 21 | # 22 | # @param [ Hash ] options The options to publish with. 23 | # 24 | # @example publish an activity with a object and target 25 | # current_user.publish_activity(:enquiry, :object => @enquiry, :target => @listing) 26 | # 27 | def publish_activity(name, options={}) 28 | options[:receivers] = self.send(options[:receivers]) if options[:receivers].is_a?(Symbol) 29 | activity = activity_class.publish(name, {:actor => self}.merge(options)) 30 | end 31 | 32 | def activity_stream(options = {}) 33 | activity_class.stream_for(self, options) 34 | end 35 | 36 | def published_activities(options = {}) 37 | activity_class.stream_of(self, options) 38 | end 39 | 40 | def activity_class 41 | @activity_klass ||= activity_klass ? activity_klass.classify.constantize : ::Activity 42 | end 43 | end 44 | 45 | end -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "pry" 2 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 3 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "lib")) 4 | 5 | MODELS = File.join(File.dirname(__FILE__), "app/models") 6 | SUPPORT = File.join(File.dirname(__FILE__), "support") 7 | $LOAD_PATH.unshift(MODELS) 8 | $LOAD_PATH.unshift(SUPPORT) 9 | 10 | require 'streama' 11 | require 'mongoid' 12 | require 'rspec' 13 | require 'database_cleaner' 14 | 15 | LOGGER = Logger.new($stdout) 16 | 17 | DatabaseCleaner.strategy = :truncation 18 | 19 | def database_id 20 | ENV["CI"] ? "mongoid_#{Process.pid}" : "mongoid_test" 21 | end 22 | 23 | Mongoid.configure do |config| 24 | config.connect_to(database_id) 25 | end 26 | 27 | Dir[ File.join(MODELS, "*.rb") ].sort.each do |file| 28 | name = File.basename(file, ".rb") 29 | autoload name.camelize.to_sym, name 30 | end 31 | require File.join(MODELS,"mars","user.rb") 32 | 33 | Dir[ File.join(SUPPORT, "*.rb") ].each do |file| 34 | require File.basename(file) 35 | end 36 | 37 | RSpec.configure do |config| 38 | config.include RSpec::Matchers 39 | config.mock_with :rspec 40 | 41 | config.before(:each) do 42 | DatabaseCleaner.start 43 | Mongoid::IdentityMap.clear 44 | end 45 | 46 | config.after(:each) do 47 | DatabaseCleaner.clean 48 | end 49 | 50 | config.after(:suite) do 51 | if ENV["CI"] 52 | Mongoid::Threaded.sessions[:default].drop 53 | end 54 | end 55 | 56 | end 57 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: git://github.com/mongoid/mongoid.git 3 | revision: 9b3bc1264032209b7a6c0e82d0ca656f401e476b 4 | specs: 5 | mongoid (4.0.0) 6 | activemodel (~> 4.0.0) 7 | moped (~> 2.0.beta3) 8 | origin (~> 1.0) 9 | tzinfo (~> 0.3.37) 10 | 11 | PATH 12 | remote: . 13 | specs: 14 | streama (0.3.8) 15 | mongoid (>= 3.0, <= 5) 16 | 17 | GEM 18 | remote: http://rubygems.org/ 19 | specs: 20 | activemodel (4.0.0) 21 | activesupport (= 4.0.0) 22 | builder (~> 3.1.0) 23 | activesupport (4.0.0) 24 | i18n (~> 0.6, >= 0.6.4) 25 | minitest (~> 4.2) 26 | multi_json (~> 1.3) 27 | thread_safe (~> 0.1) 28 | tzinfo (~> 0.3.37) 29 | atomic (1.1.14) 30 | bson (3.2.7) 31 | builder (3.1.4) 32 | coderay (1.0.8) 33 | concurrent-ruby (1.1.6) 34 | connection_pool (2.2.2) 35 | database_cleaner (0.8.0) 36 | diff-lcs (1.1.3) 37 | i18n (0.9.5) 38 | concurrent-ruby (~> 1.0) 39 | method_source (0.8.1) 40 | minitest (4.7.5) 41 | moped (2.0.7) 42 | bson (~> 3.0) 43 | connection_pool (~> 2.0) 44 | optionable (~> 0.2.0) 45 | multi_json (1.8.2) 46 | optionable (0.2.0) 47 | origin (1.1.0) 48 | pry (0.9.10) 49 | coderay (~> 1.0.5) 50 | method_source (~> 0.8) 51 | slop (~> 3.3.1) 52 | rake (13.0.1) 53 | rspec (2.6.0) 54 | rspec-core (~> 2.6.0) 55 | rspec-expectations (~> 2.6.0) 56 | rspec-mocks (~> 2.6.0) 57 | rspec-core (2.6.4) 58 | rspec-expectations (2.6.0) 59 | diff-lcs (~> 1.1.2) 60 | rspec-mocks (2.6.0) 61 | slop (3.3.3) 62 | thread_safe (0.1.3) 63 | atomic 64 | tzinfo (0.3.38) 65 | 66 | PLATFORMS 67 | ruby 68 | 69 | DEPENDENCIES 70 | database_cleaner (~> 0.8) 71 | mongoid (~> 4)! 72 | pry 73 | rake 74 | rspec (~> 2.5) 75 | streama! 76 | -------------------------------------------------------------------------------- /spec/lib/definition_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Definition" do 4 | 5 | let(:definition_dsl) do 6 | dsl = Streama::DefinitionDSL.new(:new_photo) 7 | dsl.actor(:user, :cache => [:id, :full_name]) 8 | dsl.object(:photo, :cache => [:id, :full_name]) 9 | dsl.target_object(:album, :cache => [:id, :name, :full_address]) 10 | dsl 11 | end 12 | 13 | describe '#initialize' do 14 | before(:all) do 15 | @definition_dsl = definition_dsl 16 | @definition = Streama::Definition.new(@definition_dsl) 17 | end 18 | 19 | it "assigns @actor" do 20 | @definition.actor.has_key?(:user).should be true 21 | end 22 | it "assigns @object" do 23 | @definition.object.has_key?(:photo).should be true 24 | end 25 | 26 | it "assigns @target" do 27 | @definition.target_object.has_key?(:album).should be true 28 | end 29 | 30 | end 31 | 32 | describe '.register' do 33 | 34 | it "registers a definition and return new definition" do 35 | Streama::Definition.register(definition_dsl).is_a?(Streama::Definition).should eq true 36 | end 37 | 38 | it "returns false if invalid definition" do 39 | Streama::Definition.register(false).should be false 40 | end 41 | 42 | end 43 | 44 | describe '.registered' do 45 | 46 | it "returns registered definitions" do 47 | Streama::Definition.register(definition_dsl) 48 | Streama::Definition.registered.size.should be > 0 49 | end 50 | 51 | end 52 | 53 | describe '.find' do 54 | 55 | it "returns the definition by name" do 56 | Streama::Definition.find(:new_photo).name.should eq :new_photo 57 | end 58 | 59 | it "raises an exception if invalid activity" do 60 | lambda { Streama::Definition.find(:unknown_activity) }.should raise_error Streama::Errors::InvalidActivity 61 | end 62 | 63 | end 64 | 65 | end 66 | -------------------------------------------------------------------------------- /spec/lib/actor_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Actor" do 4 | 5 | let(:photo) { Photo.create(:comment => "I'm interested") } 6 | let(:album) { Album.create(:title => "A test album") } 7 | let(:user) { User.create(:full_name => "Christos") } 8 | 9 | it "raises an exception if the class is not a mongoid document" do 10 | lambda { NoMongoid.new }.should raise_error Streama::Errors::NotMongoid 11 | end 12 | 13 | describe "#publish_activity" do 14 | before :each do 15 | 2.times { |n| User.create(:full_name => "Receiver #{n}") } 16 | end 17 | 18 | it "pushes activity to receivers" do 19 | activity = user.publish_activity(:new_photo, :object => photo, :target_object => album) 20 | activity.receivers.size == 6 21 | end 22 | 23 | it "pushes to a defined stream" do 24 | activity = user.publish_activity(:new_photo, :object => photo, :target_object => album, :receivers => :friends) 25 | activity.receivers.size == 6 26 | end 27 | 28 | end 29 | 30 | describe "#activity_stream" do 31 | 32 | before :each do 33 | user.publish_activity(:new_photo, :object => photo, :target_object => album) 34 | user.publish_activity(:new_comment, :object => photo) 35 | 36 | u = User.create(:full_name => "Other User") 37 | u.publish_activity(:new_photo, :object => photo, :target_object => album) 38 | u.publish_activity(:new_tag, :object => photo) 39 | 40 | end 41 | 42 | it "retrieves the stream for an actor" do 43 | user.activity_stream.size.should eq 4 44 | end 45 | 46 | it "retrieves the stream and filters to a particular activity type" do 47 | user.activity_stream(:type => :new_photo).size.should eq 2 48 | end 49 | 50 | it "retrieves the stream and filters to a couple particular activity types" do 51 | user.activity_stream(:type => [:new_tag, :new_comment]).size.should eq 2 52 | end 53 | 54 | end 55 | 56 | describe "#published_activities" do 57 | before :each do 58 | user.publish_activity(:new_photo, :object => photo, :target_object => album) 59 | user.publish_activity(:new_comment, :object => photo) 60 | user.publish_activity(:new_tag, :object => photo) 61 | 62 | u = User.create(:full_name => "Other User") 63 | u.publish_activity(:new_photo, :object => photo, :target_object => album) 64 | end 65 | 66 | it "retrieves published activities for the actor" do 67 | user.published_activities.size.should eq 3 68 | end 69 | 70 | it "retrieves and filters published activities by type for the actor" do 71 | user.published_activities(:type => :new_photo).size.should eq 1 72 | end 73 | 74 | it "retrieves and filters published activities by a couple types for the actor" do 75 | user.published_activities(:type => [:new_comment, :new_tag]).size.should eq 2 76 | end 77 | 78 | end 79 | 80 | 81 | end 82 | -------------------------------------------------------------------------------- /spec/lib/activity_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Activity" do 4 | 5 | let(:photo) { Photo.create(:file => "image.jpg") } 6 | let(:album) { Album.create(:title => "A test album") } 7 | let(:user) { User.create(:full_name => "Christos") } 8 | let(:mars_user) { Mars::User.create(:full_name => "Mars User") } 9 | 10 | describe ".activity" do 11 | it "registers and return a valid definition" do 12 | @definition = Activity.activity(:test_activity) do 13 | actor :user, :cache => [:full_name] 14 | object :photo, :cache => [:file] 15 | target_object :album, :cache => [:title] 16 | end 17 | 18 | @definition.is_a?(Streama::Definition).should be true 19 | end 20 | 21 | end 22 | 23 | describe "#publish" do 24 | 25 | before :each do 26 | @send_to = [] 27 | 2.times { |n| @send_to << User.create(:full_name => "Custom Receiver #{n}") } 28 | 5.times { |n| User.create(:full_name => "Receiver #{n}") } 29 | end 30 | 31 | it "pushes activity to receivers" do 32 | @activity = Activity.publish(:new_photo, {:actor => user, :object => photo, :target_object => album, :receivers => @send_to}) 33 | @activity.receivers.size.should == 2 34 | end 35 | 36 | 37 | context "when activity not cached" do 38 | 39 | it "pushes activity to receivers" do 40 | @activity = Activity.publish(:new_photo_without_cache, {:actor => user, :object => photo, :target_object => album, :receivers => @send_to}) 41 | @activity.receivers.size.should == 2 42 | end 43 | 44 | end 45 | 46 | it "overrides the recievers if option passed" do 47 | @activity = Activity.publish(:new_photo, {:actor => user, :object => photo, :target_object => album, :receivers => @send_to}) 48 | @activity.receivers.size.should == 2 49 | end 50 | 51 | 52 | 53 | context "when republishing" 54 | before :each do 55 | @actor = user 56 | @activity = Activity.publish(:new_photo, {:actor => @actor, :object => photo, :target_object => album}) 57 | @activity.publish 58 | end 59 | 60 | it "updates metadata" do 61 | @actor.full_name = "testing" 62 | @actor.save 63 | @activity.publish 64 | @activity.actor['full_name'].should eq "testing" 65 | end 66 | end 67 | 68 | describe ".publish" do 69 | it "creates a new activity" do 70 | activity = Activity.publish(:new_photo, {:actor => user, :object => photo, :target_object => album}) 71 | activity.should be_an_instance_of Activity 72 | end 73 | 74 | it " creates a new activity when actor has namespace" do 75 | activity = Activity.publish(:new_mars_photo, {:actor => mars_user, :object => photo, :target_object => album}) 76 | activity.should be_an_instance_of Activity 77 | end 78 | 79 | end 80 | 81 | describe "#refresh" do 82 | 83 | before :each do 84 | @user = user 85 | @activity = Activity.publish(:new_photo, {:actor => @user, :object => photo, :target_object => album}) 86 | end 87 | 88 | it "reloads instances and updates activities stored data" do 89 | @activity.save 90 | @activity = Activity.last 91 | 92 | expect do 93 | @user.update_attribute(:full_name, "Test") 94 | @activity.refresh_data 95 | end.to change{ @activity.load_instance(:actor).full_name}.from("Christos").to("Test") 96 | end 97 | 98 | end 99 | 100 | describe "#load_instance" do 101 | 102 | before :each do 103 | @activity = Activity.publish(:new_photo, {:actor => user, :object => photo, :target_object => album}) 104 | @activity = Activity.last 105 | end 106 | 107 | it "loads an actor instance" do 108 | @activity.load_instance(:actor).should be_instance_of User 109 | end 110 | 111 | it "loads an object instance" do 112 | @activity.load_instance(:object).should be_instance_of Photo 113 | end 114 | 115 | it "loads a target instance" do 116 | @activity.load_instance(:target_object).should be_instance_of Album 117 | end 118 | 119 | end 120 | 121 | end 122 | -------------------------------------------------------------------------------- /lib/streama/activity.rb: -------------------------------------------------------------------------------- 1 | module Streama 2 | module Activity 3 | extend ActiveSupport::Concern 4 | 5 | included do 6 | 7 | include Mongoid::Document 8 | include Mongoid::Timestamps 9 | 10 | field :verb, :type => Symbol 11 | field :actor 12 | field :object 13 | field :target_object 14 | field :receivers, :type => Array 15 | 16 | index({ 'actor._id' => 1, 'actor._type' => 1 }) 17 | index({ 'object._id' => 1, 'object._type' => 1 }) 18 | index({ 'target_object._id' => 1, 'target_object._type' => 1 }) 19 | index({ 'receivers.id' => 1, 'receivers.type' => 1 }) 20 | 21 | validates_presence_of :actor, :verb 22 | before_save :assign_data 23 | 24 | end 25 | 26 | module ClassMethods 27 | 28 | # Defines a new activity type and registers a definition 29 | # 30 | # @param [ String ] name The name of the activity 31 | # 32 | # @example Define a new activity 33 | # activity(:enquiry) do 34 | # actor :user, :cache => [:full_name] 35 | # object :enquiry, :cache => [:subject] 36 | # target_object :listing, :cache => [:title] 37 | # end 38 | # 39 | # @return [Definition] Returns the registered definition 40 | def activity(name, &block) 41 | definition = Streama::DefinitionDSL.new(name) 42 | definition.instance_eval(&block) 43 | Streama::Definition.register(definition) 44 | end 45 | 46 | # Publishes an activity using an activity name and data 47 | # 48 | # @param [ String ] verb The verb of the activity 49 | # @param [ Hash ] data The data to initialize the activity with. 50 | # 51 | # @return [Streama::Activity] An Activity instance with data 52 | def publish(verb, data) 53 | receivers = data.delete(:receivers) 54 | new({:verb => verb}.merge(data)).publish(:receivers => receivers) 55 | end 56 | 57 | def stream_for(actor, options={}) 58 | query = {:receivers => {'$elemMatch' => {:id => actor.id, :type => actor.class.to_s}}} 59 | query.merge!({:verb.in => [*options[:type]]}) if options[:type] 60 | self.where(query).desc(:created_at) 61 | end 62 | 63 | def stream_of(actor, options={}) 64 | query = {'actor.id' => actor.id, 'actor.type' => actor.class.to_s} 65 | query.merge!({:verb.in => [*options[:type]]}) if options[:type] 66 | self.where(query).desc(:created_at) 67 | end 68 | 69 | end 70 | 71 | 72 | # Publishes the activity to the receivers 73 | # 74 | # @param [ Hash ] options The options to publish with. 75 | # 76 | def publish(options = {}) 77 | actor = load_instance(:actor) 78 | self.receivers = (options[:receivers] || actor.followers).map { |r| { :id => r.id, :type => r.class.to_s } } 79 | self.save 80 | self 81 | end 82 | 83 | # Returns an instance of an actor, object or target 84 | # 85 | # @param [ Symbol ] type The data type (actor, object, target) to return an instance for. 86 | # 87 | # @return [Mongoid::Document] document A mongoid document instance 88 | def load_instance(type) 89 | (data = self.read_attribute(type)).is_a?(Hash) ? data['type'].to_s.camelcase.constantize.find(data['id']) : data 90 | end 91 | 92 | def refresh_data 93 | assign_data 94 | save(:validates_presence_of => false) 95 | end 96 | 97 | protected 98 | 99 | def assign_data 100 | 101 | [:actor, :object, :target_object].each do |type| 102 | next unless object = load_instance(type) 103 | 104 | class_sym = object.class.name.underscore.to_sym 105 | 106 | raise Errors::InvalidData.new(class_sym) unless definition.send(type).has_key?(class_sym) 107 | 108 | hash = {'id' => object.id, 'type' => object.class.name} 109 | 110 | if fields = definition.send(type)[class_sym].try(:[],:cache) 111 | fields.each do |field| 112 | raise Errors::InvalidField.new(field) unless object.respond_to?(field) 113 | hash[field.to_s] = object.send(field) 114 | end 115 | end 116 | write_attribute(type, hash) 117 | end 118 | end 119 | 120 | def definition 121 | @definition ||= Streama::Definition.find(verb) 122 | end 123 | 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Streama 2 | 3 | * THIS PROJECT IS NO LONGER MAINTAINED * 4 | 5 | Streama is a simple Ruby activity stream gem for use with the Mongoid ODM framework. 6 | 7 | It works by posting to and querying from a firehose of individual activity items. 8 | 9 | **Currently Streama uses a Fan Out On Read approach. This is great for single instance databases, however if you plan on Sharding then please be aware that it'll hit every shard when querying. I plan on changing the schema soon so that it Fans Out On Write with bucketing.** 10 | 11 | [Data Modeling Examples from the real world](http://www.10gen.com/presentations/data-modeling-examples-real-world) 12 | 13 | [![travis](https://secure.travis-ci.org/christospappas/streama.png)](http://travis-ci.org/christospappas/streama) 14 | [![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/christospappas/streama) 15 | 16 | ## Project Tracking 17 | 18 | * [Streama Google Group](http://groups.google.com/group/streama) 19 | * [Code Climate](https://codeclimate.com/github/christospappas/streama) 20 | * [Website Demo](http://streamaweb.info) 21 | 22 | ## Install 23 | 24 | gem install streama 25 | 26 | ## Usage 27 | 28 | ### Define Activities 29 | 30 | Create an Activity model and define the activities and the fields you would like to cache within the activity. 31 | 32 | An activity consists of an actor, a verb, an object, and a target. 33 | 34 | ``` ruby 35 | class Activity 36 | include Streama::Activity 37 | 38 | activity :new_photo do 39 | actor :user, :cache => [:full_name] 40 | object :photo, :cache => [:subject, :comment] 41 | target_object :album, :cache => [:title] 42 | end 43 | 44 | end 45 | ``` 46 | 47 | The activity verb is implied from the activity name, in the above example the verb is :new_photo 48 | 49 | The object may be the entity performing the activity, or the entity on which the activity was performed. 50 | e.g John(actor) shared a video(object) 51 | 52 | The target is the object that the verb is enacted on. 53 | e.g. Geraldine(actor) posted a photo(object) to her album(target) 54 | 55 | This is based on the Activity Streams 1.0 specification (http://activitystrea.ms) 56 | 57 | ### Setup Actors 58 | 59 | Include the Actor module in a class and override the default followers method. 60 | 61 | ``` ruby 62 | class User 63 | include Mongoid::Document 64 | include Streama::Actor 65 | 66 | field :full_name, :type => String 67 | 68 | def followers 69 | User.excludes(:id => self.id).all 70 | end 71 | end 72 | ``` 73 | 74 | ### Setup Indexes 75 | 76 | Create the indexes for the Activities collection. You can do so by calling the create_indexes method. 77 | 78 | ``` ruby 79 | Activity.create_indexes 80 | ``` 81 | 82 | ### Publishing Activity 83 | 84 | In your controller or background worker: 85 | 86 | ``` ruby 87 | current_user.publish_activity(:new_photo, :object => @photo, :target_object => @album) 88 | ``` 89 | 90 | This will publish the activity to the mongoid objects returned by the #followers method in the Actor. 91 | 92 | To send your activity to different receievers, pass in an additional :receivers parameter. 93 | 94 | ``` ruby 95 | current_user.publish_activity(:new_photo, :object => @photo, :target_object => @album, :receivers => :friends) # calls friends method 96 | ``` 97 | 98 | ``` ruby 99 | current_user.publish_activity(:new_photo, :object => @photo, :target_object => @album, :receivers => current_user.find(:all, :conditions => {:group_id => mygroup})) 100 | ``` 101 | 102 | ## Retrieving Activity 103 | 104 | To retrieve the activity stream for an actor 105 | 106 | ``` ruby 107 | current_user.activity_stream 108 | ``` 109 | 110 | To retrieve the activity stream and filter by activity type 111 | 112 | ``` ruby 113 | current_user.activity_stream(:type => :activity_verb) 114 | ``` 115 | 116 | To retrieve all activities published by an actor 117 | 118 | ``` ruby 119 | current_user.published_activities 120 | ``` 121 | 122 | To retrieve all activities published by an actor and filtered by activity type 123 | 124 | ``` ruby 125 | current_user.published_activities(:type => :activity_verb) 126 | ``` 127 | 128 | If you need to return the instance of an :actor, :object or :target_object from an activity call the Activity#load_instance method 129 | 130 | ``` ruby 131 | activity.load_instance(:actor) 132 | ``` 133 | 134 | You can also refresh the cached activity data by calling the Activity#refresh_data method 135 | 136 | ``` ruby 137 | activity.refresh_data 138 | ``` 139 | 140 | ## Upgrading 141 | 142 | ### 0.3.8 143 | 144 | Mongoid 4 support added. 145 | 146 | ### 0.3.6 147 | 148 | Mongoid 3.0 support added. 149 | 150 | ### 0.3.3 151 | 152 | The Activity "target" field was renamed to "target_object". If you are upgrading from a previous version of Streama you will need to rename the field in existing documents. 153 | 154 | http://www.mongodb.org/display/DOCS/Updating#Updating-%24rename 155 | 156 | ## Contributing 157 | 158 | Once you've made your great commits 159 | 160 | 1. Fork 161 | 1. Create a topic branch - git checkout -b my_branch 162 | 1. Push to your branch - git push origin my_branch 163 | 1. Create a Pull Request from your branch 164 | 1. That's it! 165 | --------------------------------------------------------------------------------