diff --git a/merb_activerecord/lib/active_record/merbtasks.rb b/merb_activerecord/lib/active_record/merbtasks.rb index 520f5eb..a7b6241 100644 --- a/merb_activerecord/lib/active_record/merbtasks.rb +++ b/merb_activerecord/lib/active_record/merbtasks.rb @@ -316,11 +316,6 @@ def local_database?(config, &block) end namespace :sessions do - desc "Create sessions table" - task :create => :merb_start do - Merb::ActiveRecordSession.create_table! - end - desc "Clear the sessions table" task :clear => :merb_start do session_table = 'session' @@ -330,10 +325,6 @@ def local_database?(config, &block) end end -def session_table_name - ActiveRecord::Base.pluralize_table_names ? :sessions : :session -end - def set_firebird_env(config) ENV["ISC_USER"] = config["username"].to_s if config["username"] ENV["ISC_PASSWORD"] = config["password"].to_s if config["password"] diff --git a/merb_activerecord/lib/generators/session_migration.rb b/merb_activerecord/lib/generators/session_migration.rb index 4289241..7987ccb 100644 --- a/merb_activerecord/lib/generators/session_migration.rb +++ b/merb_activerecord/lib/generators/session_migration.rb @@ -1,4 +1,4 @@ Merb::Generators::SessionMigrationGenerator.template :session_migration_activerecord, :orm => :activerecord do - source(File.dirname(__FILE__), 'templates/session_migration/schema/migrations/%version%_sessions.rb') - destination("schema/migrations/#{version}_sessions.rb") + source(File.dirname(__FILE__), 'templates/session_migration/schema/migrations/%version%_database_sessions.rb') + destination("schema/migrations/#{version}_database_sessions.rb") end \ No newline at end of file diff --git a/merb_activerecord/lib/generators/templates/session_migration/schema/migrations/%version%_sessions.rb b/merb_activerecord/lib/generators/templates/session_migration/schema/migrations/%version%_database_sessions.rb similarity index 88% rename from merb_activerecord/lib/generators/templates/session_migration/schema/migrations/%version%_sessions.rb rename to merb_activerecord/lib/generators/templates/session_migration/schema/migrations/%version%_database_sessions.rb index 082796b..c382223 100644 --- a/merb_activerecord/lib/generators/templates/session_migration/schema/migrations/%version%_sessions.rb +++ b/merb_activerecord/lib/generators/templates/session_migration/schema/migrations/%version%_database_sessions.rb @@ -3,6 +3,7 @@ def self.up create_table :sessions do |t| t.column :session_id, :string t.column :data, :text + t.column :created_at, :datetime end add_index :sessions, :session_id end diff --git a/merb_activerecord/lib/merb/orms/active_record/connection.rb b/merb_activerecord/lib/merb/orms/active_record/connection.rb index 8e1e589..31cca2d 100644 --- a/merb_activerecord/lib/merb/orms/active_record/connection.rb +++ b/merb_activerecord/lib/merb/orms/active_record/connection.rb @@ -73,13 +73,6 @@ def connect end end - # Registering this ORM lets the user choose active_record as a session - # in merb.yml's session_store: option. - def register_session_type - Merb.register_session_type("activerecord", - "merb/session/active_record_session", - "Using ActiveRecord database sessions") - end end end end diff --git a/merb_activerecord/lib/merb/session/active_record_session.rb b/merb_activerecord/lib/merb/session/active_record_session.rb index 36e965c..150cbc3 100644 --- a/merb_activerecord/lib/merb/session/active_record_session.rb +++ b/merb_activerecord/lib/merb/session/active_record_session.rb @@ -1,142 +1,65 @@ -require "active_record" +require 'active_record' +require 'merb-core/dispatch/session' +require 'base64' module Merb - module SessionMixin - def setup_session - before = cookies[_session_id_key] - request.session, cookies[_session_id_key] = Merb::ActiveRecordSession.persist(cookies[_session_id_key]) - @_fingerprint = Marshal.dump(request.session.data).hash - @_new_cookie = cookies[_session_id_key] != before - end - def finalize_session - request.session.save if @_fingerprint != Marshal.dump(request.session.data).hash - set_cookie(_session_id_key, request.session.session_id, Time.now + _session_expiry) if (@_new_cookie || request.session.needs_new_cookie) - end + # Sessions stored in ActiveRecord model. + # + # To use ActiveRecord based sessions add the following to config/init.rb: + # + # Merb::Config[:session_store] = 'activerecord' + + class ActiveRecordSessionStore < ::ActiveRecord::Base + + table_name = (Merb::Plugins.config[:merb_active_record][:session_table_name] || "sessions") + + set_table_name table_name - def session_store_type - "activerecord" - end - end # ActiveRecordMixin - - class ActiveRecordSession < ::ActiveRecord::Base - set_table_name 'sessions' - # Customizable data column name. Defaults to 'data'. - cattr_accessor :data_column_name - self.data_column_name = 'data' - before_save :marshal_data! - before_save :raise_on_session_data_overflow! - attr_accessor :needs_new_cookie - + serialize :data + class << self - # Generates a new session ID and creates a row for the new session in the database. - def generate - create(:session_id => Merb::SessionMixin::rand_uuid, :data => {}) - end - - # Gets the existing session based on the session_id available in cookies. - # If none is found, generates a new session. - def persist(session_id) - if session_id - session = find_by_session_id(session_id) - end - unless session - session = generate + + # ==== Parameters + # session_id:: ID of the session to retrieve. + # + # ==== Returns + # ContainerSession:: The session corresponding to the ID. + def retrieve_session(session_id) + if item = find_by_session_id(session_id) + item.data end - [session, session.session_id] - end - - # Don't try to reload ARStore::Session in dev mode. - def reloadable? - false - end - - def data_column_size_limit - @data_column_size_limit ||= columns_hash[@@data_column_name].limit end - def marshal(data) Base64.encode64(Marshal.dump(data)) if data end - def unmarshal(data) Marshal.load(Base64.decode64(data)) if data end - - def create_table! - connection.execute <<-end_sql - CREATE TABLE #{table_name} ( - id INTEGER PRIMARY KEY, - #{connection.quote_column_name('session_id')} TEXT UNIQUE, - #{connection.quote_column_name(@@data_column_name)} TEXT(255) - ) - end_sql + # ==== Parameters + # session_id:: ID of the session to set. + # data:: The session to set. + def store_session(session_id, data) + if item = find_by_session_id(session_id) + item.update_attributes!(:data => data) + else + create(:session_id => session_id, :data => data) + end end - def drop_table! - connection.execute "DROP TABLE #{table_name}" + # ==== Parameters + # session_id:: ID of the session to delete. + def delete_session(session_id) + delete_all(["#{connection.quote_column_name('session_id')} IN (?)", session_id]) end - end - - # Regenerate the Session ID - def regenerate - update_attributes(:session_id => Merb::SessionMixin::rand_uuid) - self.needs_new_cookie = true - end - - # Recreates the cookie with the default expiration time - # Useful during log in for pushing back the expiration date - def refresh_expiration - self.needs_new_cookie = true - end - - # Lazy-delete of session data - def delete(key = nil) - key ? self.data.delete(key) : self.data.clear - end - - def [](key) - data[key] - end - - def empty? - data.empty? - end - - def each(&b) - data.each(&b) + end - def each_with_index(&b) - data.each_with_index(&b) - end + end - def []=(key, val) - data[key] = val - end + class ActiveRecordSession < SessionStoreContainer - # Lazy-unmarshal session state. - def data - @data ||= self.class.unmarshal(read_attribute(@@data_column_name)) || {} - end - - # Has the session been loaded yet? - def loaded? - !! @data - end - - private - attr_writer :data - - def marshal_data! - return false if !loaded? - write_attribute(@@data_column_name, self.class.marshal(self.data)) - end - - # Ensures that the data about to be stored in the database is not - # larger than the data storage column. Raises - # ActionController::SessionOverflowError. - def raise_on_session_data_overflow! - return false if !loaded? - limit = self.class.data_column_size_limit - if loaded? and limit and read_attribute(@@data_column_name).size > limit - raise MerbController::SessionOverflowError - end - end - end # ActiveRecordSessionMixin -end # Merb + # The session store type + self.session_store_type = :activerecord + + # The store object is the model class itself + self.store = ActiveRecordSessionStore + + end + +end \ No newline at end of file diff --git a/merb_activerecord/lib/merb_activerecord.rb b/merb_activerecord/lib/merb_activerecord.rb index 149d1fc..823a6bb 100644 --- a/merb_activerecord/lib/merb_activerecord.rb +++ b/merb_activerecord/lib/merb_activerecord.rb @@ -1,5 +1,7 @@ if defined?(Merb::Plugins) + dependency "activerecord" + require File.join(File.dirname(__FILE__) / "merb" / "orms" / "active_record" / "connection") Merb::Plugins.add_rakefiles(File.join(File.dirname(__FILE__) / "active_record" / "merbtasks")) @@ -9,7 +11,10 @@ class Merb::Orms::ActiveRecord::Connect < Merb::BootLoader def self.run Merb::Orms::ActiveRecord.connect - Merb::Orms::ActiveRecord.register_session_type + if Merb::Config.session_stores.include?(:activerecord) + Merb.logger.debug "Using ActiveRecord sessions" + require File.join(File.dirname(__FILE__) / "merb" / "session" / "active_record_session") + end end end diff --git a/merb_activerecord/specs/merb_active_record_session_spec.rb b/merb_activerecord/specs/merb_active_record_session_spec.rb new file mode 100644 index 0000000..3865423 --- /dev/null +++ b/merb_activerecord/specs/merb_active_record_session_spec.rb @@ -0,0 +1,57 @@ +$:.push File.join(File.dirname(__FILE__), '..', 'lib') +require 'merb-core' +require 'merb-core/test' +require 'merb-core/test/helpers' + +Merb::BootLoader.before_app_loads do + require "merb/session/active_record_session" +end + +Merb.start_environment( :environment => 'test', :adapter => 'runner', + :session_store => 'activerecord') + +Spec::Runner.configure do |config| + config.include Merb::Test::RequestHelper +end + +require 'merb_activerecord' +ActiveRecord::Base.establish_connection(:adapter => "sqlite3", + :dbfile => ":memory:") + +ActiveRecord::Schema.define do + create_table :sessions do |t| + t.column :session_id, :string + t.column :data, :text + t.column :created_at, :datetime + end +end + +# Load up the shared specs from merb-core +if (gem_spec = Gem.source_index.search('merb-core').last) && + gem_spec.files.include?('spec/public/session/controllers/sessions.rb') + require gem_spec.full_gem_path / 'spec/public/session/controllers/sessions.rb' + require gem_spec.full_gem_path / 'spec/public/session/session_spec.rb' +end + +describe Merb::ActiveRecordSession do + + before do + @session_class = Merb::ActiveRecordSession + @session = @session_class.generate + end + + it_should_behave_like "All session-store backends" + + it "should have a session_store_type class attribute" do + @session.class.session_store_type.should == :activerecord + end + +end + +describe Merb::ActiveRecordSession, "mixed into Merb::Controller" do + + before(:all) { @session_class = Merb::ActiveRecordSession } + + it_should_behave_like "All session-stores mixed into Merb::Controller" + +end \ No newline at end of file diff --git a/merb_activerecord/specs/merb_active_record_spec.rb b/merb_activerecord/specs/merb_active_record_spec.rb index 07c0b41..2a9a8fc 100644 --- a/merb_activerecord/specs/merb_active_record_spec.rb +++ b/merb_activerecord/specs/merb_active_record_spec.rb @@ -10,14 +10,12 @@ end end - - describe "Merb ActiveRecord extension" do before :all do @wd = Dir.pwd Merb.stub!(:dir_for).with(:config).and_return(@wd) - @config_file_path = @wd / "database.yml" - @sample_file_path = @wd / "database.yml.sample" + @config_file_path = @wd / "config" / "database.yml" + @sample_file_path = @wd / "config" / "database.yml.sample" @sample_source = Merb::Orms::ActiveRecord.sample_source @config_sample = Erubis.load_yaml_file(@sample_source) @@ -52,11 +50,6 @@ @config_sample[:development][:encoding].should == "utf8" end - it "stores configurations from config file" do - Erubis.should_receive(:load_yaml_file).with(@config_file_path).and_return(@config_sample) - Merb::Orms::ActiveRecord.configurations[:development][:database].should == "sample_development" - end - it "provides Rack with a way to start a transcantion" do Merb::Orms::ActiveRecord.should respond_to(:open_sandbox!) end diff --git a/merb_activerecord/specs/spec_helper.rb b/merb_activerecord/specs/spec_helper.rb index b6eaa87..496172e 100644 --- a/merb_activerecord/specs/spec_helper.rb +++ b/merb_activerecord/specs/spec_helper.rb @@ -1,5 +1,4 @@ $TESTING = true $:.push File.join(File.dirname(__FILE__), '..', 'lib') require 'merb-core' -require 'merb_activerecord' -require 'merb/test/model_helper/active_record' +require 'merb_activerecord' \ No newline at end of file diff --git a/merb_sequel/lib/merb/orms/sequel/connection.rb b/merb_sequel/lib/merb/orms/sequel/connection.rb index 15b2b2b..e0efc84 100644 --- a/merb_sequel/lib/merb/orms/sequel/connection.rb +++ b/merb_sequel/lib/merb/orms/sequel/connection.rb @@ -1,4 +1,5 @@ require "fileutils" +require "sequel" module Merb module Orms @@ -15,9 +16,7 @@ def copy_sample_config end def config - - @config ||= - begin + @config ||= begin # Convert string keys to symbols full_config = Erubis.load_yaml_file(config_file) config = (Merb::Plugins.config[:merb_sequel] = {}) @@ -26,14 +25,10 @@ def config end config end - end # Database connects as soon as the gem is loaded def connect - - require "sequel" - if File.exists?(config_file) Merb.logger.info!("Connecting to the '#{config[:database]}' database on '#{config[:host]}' using '#{config[:adapter]}' ...") connection = ::Sequel.connect(config_options(config)) @@ -46,11 +41,9 @@ def connect Merb.logger.error! "A sample file was created called config/database.yml.sample for you to copy and edit." exit(1) end - end def config_options(config = {}) - options = {} options[:adapter] = (config[:adapter] || "sqlite") options[:host] = (config[:host] || "localhost") @@ -63,16 +56,6 @@ def config_options(config = {}) options[:logger] = Merb.logger options end - - # Registering this ORM lets the user choose sequel as a session store - # in merb.yml's session_store: option. - def register_session_type - Merb.register_session_type( - "sequel", - "merb/session/sequel_session", - "Using Sequel database sessions" - ) - end end diff --git a/merb_sequel/lib/merb/session/sequel_session.rb b/merb_sequel/lib/merb/session/sequel_session.rb index a761c11..604212d 100644 --- a/merb_sequel/lib/merb/session/sequel_session.rb +++ b/merb_sequel/lib/merb/session/sequel_session.rb @@ -1,156 +1,106 @@ -require "base64" +require 'sequel' +require 'merb-core/dispatch/session' +require 'base64' module Merb - module SessionMixin - def setup_session - before = cookies[_session_id_key] - request.session, cookies[_session_id_key] = Merb::SequelSession.persist(cookies[_session_id_key]) - @_fingerprint = Marshal.dump(request.session.data).hash - @_new_cookie = cookies[_session_id_key] != before - end - - def finalize_session - request.session.save if @_fingerprint != Marshal.dump(request.session.data).hash - set_cookie(_session_id_key, request.session.values[:session_id], Time.now + _session_expiry) if (@_new_cookie || request.session.needs_new_cookie) - end - - def session_store_type - "sequel" - end - - end - table_name = (Merb::Plugins.config[:merb_sequel][:session_table_name] || "sessions") - class SequelSession < Sequel::Model(table_name.to_sym) + # Sessions stored in Sequel model. + # + # To use Sequel based sessions add the following to config/init.rb: + # + # Merb::Config[:session_store] = 'sequel' + + class SequelSessionStore < Sequel::Model(table_name.to_sym) set_schema do primary_key :id varchar :session_id - varchar :data + text :data timestamp :created_at end - attr_accessor :needs_new_cookie - class << self - # Generates a new session ID and creates a row for the new session in the database. - def generate - create(:session_id => Merb::SessionMixin::rand_uuid, - :data => marshal({}), :created_at => Time.now) + + # ==== Parameters + # session_id:: ID of the session to retrieve. + # + # ==== Returns + # ContainerSession:: The session corresponding to the ID. + def retrieve_session(session_id) + if item = find(:session_id => session_id) + item.data + end end - # Gets the existing session based on the session_id available in cookies. - # If none is found, generates a new session. - def persist(session_id) - if session_id - session = find(:session_id => session_id) + # ==== Parameters + # session_id:: ID of the session to set. + # data:: The session to set. + def store_session(session_id, data) + if item = find(:session_id => session_id) + item.update(:data => data) + else + create(:session_id => session_id, :data => data, :created_at => Time.now) end - unless session - session = generate - end - [session, session.values[:session_id]] end - # Don't try to reload ARStore::Session in dev mode. - def reloadable? - false + # ==== Parameters + # session_id:: ID of the session to delete. + def delete_session(session_id) + if item = find(:session_id => session_id) + item.delete + end end - + + # ==== Returns + # Integer:: The maximum length of the 'data' column. def data_column_size_limit - 255 - end - - def marshal(data) - Base64.encode64(Marshal.dump(data)) if data - end - - def unmarshal(data) - Marshal.load(Base64.decode64(data)) if data + 512 # TODO - figure out how much space we actually have end alias :create_table! :create_table alias :drop_table! :drop_table end - # Regenerate the Session ID - def regenerate - update_attributes(:session_id => Merb::SessionMixin::rand_uuid) - self.needs_new_cookie = true - end - - # Recreates the cookie with the default expiration time - # Useful during log in for pushing back the expiration date - def refresh_expiration - self.needs_new_cookie = true - end - - # Lazy-delete of session data - def delete(key = nil) - key ? self.data.delete(key) : self.data.clear - end - - def [](key) - data[key] - end - - def []=(key, val) - data[key] = val - end - - def empty? - data.empty? - end - - def each(&b) - data.each(&b) + # Lazy-unserialize session state. + def data + @data ||= (@values[:data] ? Marshal.load(@values[:data]) : {}) end - def each_with_index(&b) - data.each_with_index(&b) - end - - # Lazy-unmarshal session state. - def data - @data ||= self.class.unmarshal(@values[:data]) || {} + # Virtual attribute writer - override. + def data=(hsh) + @data = hsh if hsh.is_a?(Hash) end # Has the session been loaded yet? def loaded? - !! @data + !!@data end - private - - attr_writer :data - - before_save do # marshal_data! - # return false if !loaded? - @values[:data] = self.class.marshal(self.data) + before_save do + @values[:data] = Marshal.dump(self.data) + if @values[:data].size > self.class.data_column_size_limit + raise Merb::SessionMixin::SessionOverflow + end end - - # Ensures that the data about to be stored in the database is not - # larger than the data storage column. Raises - # ActionController::SessionOverflowError. - # before_save do # raise_on_session_data_overflow! - # return false if !loaded? - # limit = self.class.data_column_size_limit - # if loaded? and limit and read_attribute(@@data_column_name).size > limit - # raise MerbController::SessionOverflowError - # end - # end end unless Sequel::Model.db.table_exists?(table_name.to_sym) puts "Warning: The database did not contain a '#{table_name}' table for sessions." - - SequelSession.class_eval do - create_table unless table_exists? - end - + SequelSessionStore.class_eval { create_table unless table_exists? } puts "Created sessions table." end + + class SequelSession < SessionStoreContainer + + # The session store type + self.session_store_type = :sequel + + # The store object is the model class itself + self.store = SequelSessionStore + + end end diff --git a/merb_sequel/lib/merb_sequel.rb b/merb_sequel/lib/merb_sequel.rb index 197fcc6..e332ae1 100644 --- a/merb_sequel/lib/merb_sequel.rb +++ b/merb_sequel/lib/merb_sequel.rb @@ -9,7 +9,10 @@ class Merb::Orms::Sequel::Connect < Merb::BootLoader def self.run Merb::Orms::Sequel.connect - Merb::Orms::Sequel.register_session_type + if Merb::Config.session_stores.include?(:sequel) + Merb.logger.debug "Using Sequel sessions" + require File.join(File.dirname(__FILE__) / "merb" / "session" / "sequel_session") + end end end diff --git a/merb_sequel/specs/merb_sequel_session_spec.rb b/merb_sequel/specs/merb_sequel_session_spec.rb new file mode 100644 index 0000000..49c31d4 --- /dev/null +++ b/merb_sequel/specs/merb_sequel_session_spec.rb @@ -0,0 +1,46 @@ +$:.push File.join(File.dirname(__FILE__), '..', 'lib') +require 'merb-core' +require 'merb-core/test' +require 'merb-core/test/helpers' + +Merb::BootLoader.before_app_loads do + require 'sequel' + DB = Sequel.sqlite + require "merb/session/sequel_session" +end + +Merb.start :environment => 'test', :adapter => 'runner', :session_store => 'sequel' + +Spec::Runner.configure do |config| + config.include Merb::Test::RequestHelper +end + +# Load up the shared specs from merb-core +if (gem_spec = Gem.source_index.search('merb-core').last) && + gem_spec.files.include?('spec/public/session/controllers/sessions.rb') + require gem_spec.full_gem_path / 'spec/public/session/controllers/sessions.rb' + require gem_spec.full_gem_path / 'spec/public/session/session_spec.rb' +end + +describe Merb::SequelSession do + + before do + @session_class = Merb::SequelSession + @session = @session_class.generate + end + + it_should_behave_like "All session-store backends" + + it "should have a session_store_type class attribute" do + @session.class.session_store_type.should == :sequel + end + +end + +describe Merb::SequelSession, "mixed into Merb::Controller" do + + before(:all) { @session_class = Merb::SequelSession } + + it_should_behave_like "All session-stores mixed into Merb::Controller" + +end \ No newline at end of file