cookies_manager 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .yardoc
6
+ doc
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use 1.8.7@cookies_manager --create
data/CHANGELOG.rdoc ADDED
@@ -0,0 +1,4 @@
1
+ 0.1.0 (Nov 07, 2011)
2
+
3
+ * Initial release
4
+
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in cookies_manager.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Christophe Levand
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.
data/README.rdoc ADDED
@@ -0,0 +1,66 @@
1
+ = CookiesManager
2
+
3
+ CookiesManager is a simple tool that provides a convenient way to manage any kind of data in the cookies (strings, arrays, hashes, etc.):
4
+ - The data you store in the cookies is automatically marshalled, zipped, and base64-encoded.
5
+ - The data you ask to retrieve from the cookies is read from a cache, which spares the need for reverse transformation as long as the cache is in sync with the cookies. When the cache is out of sync, CookiesManager automatically resynchronizes the cache by reading the data from the cookies and processing reverse transformation (base64-decoding, unzipping, unmarshalling).
6
+ - The cache is defined somehow at the controller instance level. Thus, each HTTP request has its own cache for its whole lifetime.
7
+
8
+ == Installation
9
+
10
+ In <b>Rails 2</b>, add this to your environment.rb file.
11
+
12
+ config.gem "cookies_manager"
13
+
14
+ == Getting started
15
+
16
+ === Basic usage
17
+
18
+ To activate the feature, simply call +load_cookies_manager+ on your controller class:
19
+
20
+ class YourController < ActionController::Base
21
+ load_cookies_manager # This instantiates a new CookiesManager accessible through the cookies_manager helper method
22
+
23
+ <i>Hint: You can call +load_cookies_manager+ on your ApplicationController class to enable the feature for all your controllers.</i>
24
+
25
+ Next, you can refer the CookiesManager instance by calling the +cookies_manager+ helper method from your controllers and views:
26
+
27
+ len_bytes = cookies_manager.write('a_key', ['an', 'array']) # store the array as is in the cache, and as a base-64 string in the cookies
28
+ my_array = cookies_manager.read('a_key') # retrieves the array from the cache (or from the cookies if the cache is not in sync)
29
+ my_deleted_array = cookies_manager.delete('a_key') # removes the array from both the cookies and the cache
30
+
31
+ === Supported options
32
+
33
+ All supported options are fully described along with some examples in the {file:../CookiesManager/Base.html CookiesManager::Base documentation}.
34
+
35
+ == When to use it?
36
+
37
+ === CookiesManager vs native cookies
38
+
39
+ Whenever you need to store in the cookies some data somewhat more complex than just simple US-ASCII strings (ex: hashes, arrays, etc.), the CookiesManager may be
40
+ more convenient to use than the native cookies hash.
41
+
42
+ Note that you can use both CookiesManager and the cookies hash. If needed, the CookiesManager cache is automatically resynchronized from the cookies.
43
+
44
+ === CookiesManager vs session
45
+
46
+ Whenever a cookies storage management is more suited to your needs than a session storage management, the CookiesManager may be a good option if the data you want to
47
+ store is more complex than just simple US-ASCII strings. For example, there may be times when you need to persist in the cookies some non-sensitive data much longer
48
+ than the session length, such as user display preferences.
49
+
50
+ == Important notes
51
+
52
+ - If your application is multi-threaded, using CookiesManager is threadsafe as long as your threads refer the same CookiesManager instance.
53
+ - Unless you know what you are really doing, do not store large data in your cookies, since these are included in HTTP request headers.
54
+ - Do not store model objects in cookies (nor in session). For more explanation, check {http://railscasts.com/episodes/13-dangers-of-model-in-session Ryan Bates's screencast about dangers of model in session}.
55
+
56
+ == Running the tests
57
+
58
+ RSpec 2 is used for testing. To get the specs running, check {file:spec/README spec/README}.
59
+
60
+ == Coming up next
61
+ - Adapt the gem to support Ruby 1.9.x and Rails 3.
62
+
63
+ == Changes
64
+
65
+ {include:file:CHANGELOG}
66
+
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ desc "Run RSpec"
5
+ RSpec::Core::RakeTask.new do |t|
6
+ t.verbose = false
7
+ end
8
+
9
+ desc "Run all specs except slow tests"
10
+ task :skip_slow do
11
+ system "rake spec SKIP_SLOW=true"
12
+ end
13
+
14
+ task :default => :spec
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "cookies_manager/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "cookies_manager"
7
+ s.version = CookiesManager::VERSION
8
+ s.authors = ["Christophe Levand"]
9
+ s.email = ["levandch@gmail.com"]
10
+ s.homepage = ""
11
+ s.summary = %q{Simple cookies management tool for Rails}
12
+ s.description = %q{Simple cookies management tool for Rails that provides a convenient way to manage any kind of data in the cookies (strings, arrays, hashes, etc.)}
13
+
14
+ s.rubyforge_project = "cookies_manager"
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_development_dependency 'rspec', '~> 2.7.0'
22
+ s.add_development_dependency 'actionpack', '~> 2.3.14'
23
+ s.add_development_dependency 'rr', '~> 1.0.4'
24
+ s.add_development_dependency 'aquarium', '~> 0.4.4'
25
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'cookies_manager'
@@ -0,0 +1,178 @@
1
+ require 'aquarium'
2
+
3
+ module CookiesManager
4
+ # The base class of CookiesManager
5
+ class Base
6
+ include Aquarium::DSL # AOP Filters are defined at the end of the class
7
+
8
+ attr_accessor :cookies # The cookies hash to be based on
9
+
10
+ # Constructs a new CookiesManager instance based on a cookies hash
11
+ #
12
+ # The CookiesManager instance is created automatically when you call the +load_cookies_manager+ class method
13
+ # on your controller, so you don't need to instantiate it directly.
14
+ #
15
+ # === Example:
16
+ # Inside your controller, call the +load_cookies_manager+ method:
17
+ #
18
+ # class YourController < ActionController::Base
19
+ # load_cookies_manager
20
+ #
21
+ def initialize(input_cookies)
22
+ self.cookies = input_cookies
23
+ end
24
+
25
+ # Reads the data object corresponding to the key.
26
+ #
27
+ # Reads from the cache instance variable, or from the cookies if the cache is not in sync with the cookies.
28
+ # Cache desynchronization can occur when the cookies hash is modified directly.
29
+ # The cache is automatically re-synchronized if out of sync.
30
+ #
31
+ # If option +:unpack+ is set to true, data will be successively base64-decoded, unzipped, and unmarshalled. This option is used only when the data
32
+ # is retrieved from the cookies hash (i.e. the cache is not in sync with the cookies).
33
+ #
34
+ # === Example:
35
+ # data = cookies_manager.read('my_key') # reads the data associated with the key 'my_key'
36
+ #
37
+ # @param [String or Symbol] key a unique key corresponding to the data to read
38
+ # @option opts [Boolean] :unpack if true, successively base64-decode, unzip, and unmarshall the data. Default is false.
39
+ # @return [Object] the data associated with the key
40
+ #
41
+ def read(key, opts = {})
42
+ result = nil
43
+ getMutex(key).synchronize do
44
+ result = read_from_cache_or_cookies(key, opts)
45
+ end
46
+ return result
47
+ end
48
+
49
+ # Writes the data object and associates it with the key.
50
+ #
51
+ # Data is stored in both the cookies and the cache.
52
+ #
53
+ # By default, before being stored in the cookies, data is marshalled, zipped, and base64-encoded. Although this feature is recommended, you can disable it by passing the option +:skip_pack+ if you consider your data can be stored as is in the cookies (ex: US-ASCII string).
54
+ #
55
+ # === Examples:
56
+ #
57
+ # array_data = {:some_item => "an item", :some_array => ['This', 'is', 'an', 'array']}
58
+ # #store the data in the cookies as a base64-encoded string, for one hour:
59
+ # len_bytes1 = cookies_manager.write('key_for_my_array', data, :expires => 1.hour.from_now)
60
+ #
61
+ # simple_data = "a simple string"
62
+ # # store the data as in in the cookies, and keep it as long as the browser remains open:
63
+ # len_bytes2 = cookies_manager.write('key_for_my_simple_data', simple_data, :skip_pack => true)
64
+ #
65
+ # @param [String or Symbol] key a unique key to associate the data with
66
+ # @param [Hash] opts a customizable set of options, built on top of the native set of options supported when {http://api.rubyonrails.org/v2.3.8/classes/ActionController/Cookies.html setting cookies}
67
+ # @option opts [Boolean] :skip_pack if true, DO NOT marshall, zip, nor base64-encode the data. Default is false.
68
+ # @option opts [String] :path the path for which this cookie applies. Defaults to the root of the application.
69
+ # @option opts [String] :domain the domain for which this cookie applies.
70
+ # @option opts [String] :expires the time at which this cookie expires, as a Time object.
71
+ # @option opts [String] :secure whether this cookie is a only transmitted to HTTPS servers. Default is +false+.
72
+ # @option opts [String] :httponly whether this cookie is accessible via scripting or only HTTP. Defaults to +false+.
73
+ # @return [Integer] the number of bytes written in the cookies
74
+ #
75
+ def write(key, data, opts = {})
76
+ unpacked_data = data
77
+ data = pack(data) unless opts[:skip_pack]
78
+ result = nil
79
+ getMutex(key).synchronize do
80
+ cache[key] ||= {}
81
+ result = cookies[key] = {:value => data}.merge(opts) # store the packed data in the cookies hash
82
+ cache[key][:unpacked_data] = unpacked_data # store the unpacked data in the cache for fast read in the read method
83
+ cache[key][:packed_data] = data # store the packed data in the cache for fast change diff in the read method
84
+ end
85
+ return result[:value].try(:bytesize) || 0
86
+ end
87
+
88
+ # Deletes the data corresponding to the key.
89
+ #
90
+ # Removes the data from both the cookies and the cache, and return it.
91
+ # The returned value is read from the cache if this is in sync with the cookies. Otherwise, the data is read from the cookies, in which case it is successively
92
+ # base64-decoded, unzipped, and unmarshalled if option +:unpack+ is set to true.
93
+ #
94
+ # === Example:
95
+ # data = cookies_manager.delete('my_key') # deletes the data associated with the key 'my_key'
96
+ #
97
+ # @param [String or Symbol] key a unique key corresponding to the data to delete
98
+ # @option opts (see #read)
99
+ # @return (see #read)
100
+ #
101
+ def delete(key, opts = {})
102
+ result = nil
103
+ getMutex(key).synchronize do
104
+ result = read_from_cache_or_cookies(key, opts)
105
+ cookies.delete(key)
106
+ cache.delete(key)
107
+ end
108
+ return result
109
+ end
110
+
111
+ #=====#
112
+ private
113
+ #=====#
114
+
115
+ def cache
116
+ @cache ||= {} # The cookies cache
117
+ end
118
+
119
+ def mutexes
120
+ @mutexes ||= {} # A hash composed of {key;mutex} pairs where each mutex is used to synchronize operations on the data associated with the key
121
+ end
122
+
123
+ def global_mutex
124
+ @global_mutex ||= Mutex.new # A global mutex to synchronize accesses to the mutexes hash
125
+ end
126
+
127
+ # reads from the cache if in sync. Otherwise, reads from the cookies and resynchronizes the cache for the given key
128
+ def read_from_cache_or_cookies(key, opts)
129
+ result = nil
130
+ data_from_cookies = cookies[key]
131
+ cache[key] ||= {}
132
+ if cache[key][:packed_data] == data_from_cookies # checks whether cache is in sync with cookies
133
+ result = cache[key][:unpacked_data] # reads from cache
134
+ else # cache not in sync
135
+ result = opts[:unpack] ? unpack(data_from_cookies) : data_from_cookies # read from cookies
136
+ # updates the cache
137
+ cache[key][:packed_data] = data_from_cookies
138
+ cache[key][:unpacked] = result
139
+ end
140
+ return result
141
+ end
142
+
143
+ def getMutex(key)
144
+ global_mutex.synchronize do # synchronize accesses to the mutexes hash
145
+ mutexes[key] ||= Mutex.new
146
+ end
147
+ end
148
+
149
+ def pack(data)
150
+ Base64.encode64(ActiveSupport::Gzip.compress(Marshal.dump(data)))
151
+ end
152
+
153
+ def unpack(data)
154
+ Marshal.load(ActiveSupport::Gzip.decompress(Base64.decode64(data)))
155
+ end
156
+
157
+ #=================#
158
+ #== AOP Filters ==#
159
+ #=================#
160
+
161
+ # Since Aquarium sets the arities of observered methods to -1, we need to save the methods arities in a hash declared as a class variable
162
+ self.instance_methods(false).each { |method| (@method_arities ||= {})[method.to_sym] = instance_method(method).arity }
163
+
164
+ around :methods => [:read, :write, :delete] do |join_point, object, *args|
165
+ key = (args[0] = args[0].to_s) # we should stick with string keys since the cookies hash does not support indifferent access (i.e. :foo and "foo" are different keys), although this has changed in rails 3.1
166
+ opts = args[last_arg_index(join_point.method_name)] # retrieve the options arg (last argument)
167
+ opts.symbolize_keys! if opts.is_a?(Hash)
168
+ join_point.proceed(*args)
169
+ end
170
+
171
+ # Returns the index of the last arg in the method signature
172
+ def self.last_arg_index(method_name)
173
+ instance_eval { @method_arities[method_name] }.abs - 1
174
+ end
175
+
176
+ end
177
+
178
+ end
@@ -0,0 +1,29 @@
1
+ module CookiesManager
2
+
3
+ # This module provides a CookiesManager facility for your controllers.
4
+ # It is automatically extended by all controllers.
5
+ module ControllerAdditions
6
+
7
+ # Sets up a before filter that creates a new CookiesManager into an instance variable,
8
+ # which is made available to all views through the +cookies_manager+ helper method.
9
+ #
10
+ # You can call this method on your controller class as follows:
11
+ #
12
+ # class YourController < ApplicationController
13
+ # load_cookies_manager
14
+ #
15
+ def load_cookies_manager
16
+ self.before_filter do |controller|
17
+ # defines a CookiesManager instance variable, based on the cookies hash
18
+ controller.instance_variable_set(:@_cookies_manager, CookiesManager::Base.new(controller.cookies))
19
+ # wraps the instance variable in a the +cookies_manager+ method
20
+ define_method :cookies_manager, proc { controller.instance_variable_get(:@_cookies_manager) }
21
+ # makes the +cookies_manager+ method available to all views as a helper method
22
+ helper_method :cookies_manager
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ # Automatically add all ControllerAdditions methods to controller classes as class methods
29
+ ActionController::Base.extend CookiesManager::ControllerAdditions
@@ -0,0 +1,3 @@
1
+ module CookiesManager
2
+ VERSION = "0.1.1"
3
+ end
@@ -0,0 +1,4 @@
1
+ require 'cookies_manager/version'
2
+ require 'cookies_manager/base'
3
+ require 'cookies_manager/controller_additions'
4
+
data/spec/README.rdoc ADDED
@@ -0,0 +1,21 @@
1
+ = CookiesManager Specs
2
+
3
+ == Running the specs
4
+
5
+ 1. Run the +bundle+ command to install the necessary gems
6
+ 2. Run the +rake+ command to run all the specs
7
+
8
+ bundle
9
+ rake
10
+
11
+ Some of the specs, covering race conditions in multithreaded contexts, pause in purpose some threads for a few seconds through AOP to test critical sections. Those "slow" tests can be easily skipped by passing +skip_slow+ as an argument to the +rake+ command:
12
+
13
+ rake skip_slow
14
+
15
+ However, I recommend running all the specs whenever you change the code.
16
+
17
+ == Ruby versions
18
+
19
+ Currently, the specs require Ruby 1.8.7.
20
+
21
+ Ruby 1.9.x support is coming up next.
@@ -0,0 +1,319 @@
1
+ require 'spec_helper'
2
+
3
+ # Useful macros for testing
4
+ module MacrosForCookiesManager
5
+ def pack(data)
6
+ Base64.encode64(ActiveSupport::Gzip.compress(Marshal.dump(data)))
7
+ end
8
+
9
+ def unpack(data)
10
+ Marshal.load(ActiveSupport::Gzip.decompress(Base64.decode64(data)))
11
+ end
12
+ end
13
+
14
+ include MacrosForCookiesManager
15
+ include Aquarium::Aspects
16
+
17
+ describe CookiesManager::Base do
18
+ before(:all) do
19
+ @complex_data = {:some_array => [1,2,3], :some_hash => {:a => 1, :b => 2}}
20
+ @simple_data = "this is a simple string"
21
+ end
22
+
23
+ let(:cookies) { TestController.new.cookies }
24
+ subject { CookiesManager::Base.new(cookies) }
25
+
26
+ describe "#write" do
27
+ describe "#write + pack data" do
28
+ context "when write complex data" do
29
+ before { @bytesize = subject.write('my_key', @complex_data) }
30
+ specify { @bytesize.should == pack(@complex_data).bytesize }
31
+ specify { unpack(cookies['my_key']).should eql @complex_data }
32
+ end
33
+ context "when write nil value" do
34
+ before { @bytesize = subject.write('my_key', nil) }
35
+ specify { @bytesize.should == pack(nil).bytesize }
36
+ specify { unpack(cookies['my_key']).should be_nil}
37
+ end
38
+ end
39
+ describe "write without packing data" do
40
+ context "when write simple data" do
41
+ before { @bytesize = subject.write('my_key', @simple_data, :skip_pack => true) }
42
+ specify { @bytesize.should == @simple_data.bytesize }
43
+ specify { cookies['my_key'].should eql @simple_data }
44
+ end
45
+ context "when write nil value" do
46
+ before { @bytesize = subject.write('my_key', nil, :skip_pack => true) }
47
+ specify { @bytesize.should == 0 }
48
+ specify { cookies['my_key'].should be_nil}
49
+ end
50
+ end
51
+ describe "#write + set expiration date" do
52
+ before { subject.write(@key = 'my_key', @complex_data, :expires => (@expiration_date = 2.hours.from_now)) }
53
+ specify { Time.parse(cookies.controller.response["Set-Cookie"].select { |cookie_str| cookie_str =~ /\A#{@key}=/ }.last[/expires=(.*?)(;|\Z)/, 1]).to_i.should == @expiration_date.to_i } #parse the expiration date from the cookies string in the response header using some simple non-greedy regex and convert it to epochs to test the time equality. This conversion technique works only for timestamps between 1901-12-13 and 2038-01-19, but is acceptable for our tests.
54
+ end
55
+ describe "#write with nil key" do
56
+ before { subject.write(nil, @complex_data) }
57
+ specify { unpack(cookies[nil]).should eql @complex_data }
58
+ specify { unpack(cookies['']).should eql @complex_data }
59
+ end
60
+ describe "#write with empty string key" do
61
+ before { subject.write('', @complex_data) }
62
+ specify { unpack(cookies[nil]).should eql @complex_data }
63
+ specify { unpack(cookies['']).should eql @complex_data }
64
+ end
65
+ end
66
+
67
+ describe "#read" do
68
+ describe "#read with unknown key" do
69
+ specify { subject.read('unknown_key').should be_nil }
70
+ end
71
+ describe "#read some data previously stored through CookiesManager" do
72
+ context "when reading non-nil data" do
73
+ before { subject.write('my_key', @complex_data) }
74
+ specify { subject.read('my_key').should eql @complex_data }
75
+ end
76
+ context "when reading a nil value" do
77
+ before { subject.write('my_key', nil) }
78
+ specify { subject.read('my_key').should be_nil }
79
+ end
80
+ context "when reading some data stored with a nil key" do
81
+ before { subject.write(nil, @complex_data) }
82
+ specify { subject.read(nil).should eql @complex_data }
83
+ specify { subject.read('').should eql @complex_data }
84
+ end
85
+ end
86
+ describe "#read some data previously stored directly into the cookies hash" do
87
+ context "when reading non-nil data" do
88
+ before { cookies['my_key'] = {:value => @complex_data} }
89
+ specify { subject.read('my_key').should eql @complex_data }
90
+ end
91
+ context "when reading a nil value" do
92
+ before { cookies['my_key'] = {:value => nil } }
93
+ specify { subject.read('my_key').should be_nil }
94
+ end
95
+ context "when reading some data stored with a nil key" do
96
+ before { cookies[nil] = {:value => @complex_data} }
97
+ specify { subject.read(nil).should eql @complex_data }
98
+ specify { subject.read('').should eql @complex_data }
99
+ end
100
+ context "when reading some data stored with an empty string key" do
101
+ before { cookies[''] = {:value => @complex_data} }
102
+ specify { subject.read(nil).should eql @complex_data }
103
+ specify { subject.read('').should eql @complex_data }
104
+ end
105
+ end
106
+ describe "#read some data previously stored through CookiesManager but later modified directly inside the cookies hash" do
107
+ context "when read some simple data" do
108
+ before do
109
+ subject.write('my_key', @simple_data)
110
+ cookies['my_key'] = {:value => (@new_simple_data = "some new data")}
111
+ end
112
+ specify { subject.read('my_key').should eql @new_simple_data }
113
+ end
114
+ context "when reading some complex data" do
115
+ before do
116
+ subject.write('my_key', @complex_data)
117
+ @new_complex_data = @complex_data.merge(:some_new_item => 'it modifies the data')
118
+ cookies['my_key'] = {:value => pack(@new_complex_data)}
119
+ end
120
+ specify { subject.read('my_key', :unpack => true).should eql @new_complex_data }
121
+ specify { unpack(subject.read('my_key')).should eql @new_complex_data } #if :unpack option is omitted, needs to unpack manually
122
+ end
123
+ end
124
+ describe "read with key symbol/string indifferent access (ex: :foo, 'foo')" do
125
+ shared_examples_for "reading with indifferent access key" do
126
+ specify { subject.read(:my_key).should eql @complex_data }
127
+ specify { subject.read('my_key').should eql @complex_data }
128
+ specify { unpack(cookies['my_key']).should eql @complex_data }
129
+ end
130
+ context "when data has been written with a key of type symbol" do
131
+ before { subject.write(:my_key, @complex_data) }
132
+ it_should_behave_like "reading with indifferent access key"
133
+ end
134
+ context "when data has been written with a key of type string" do
135
+ before { subject.write('my_key', @complex_data) }
136
+ it_should_behave_like "reading with indifferent access key"
137
+ end
138
+ end
139
+ end
140
+
141
+ describe "#delete" do
142
+ describe "#delete existing data" do
143
+ shared_examples_for "when deleting existing data" do
144
+ before { @result = subject.delete('my_key') }
145
+ specify { @result.should eql @complex_data }
146
+ specify { subject.read('my_key').should be_nil }
147
+ end
148
+ context "when data has been previously stored through CookiesManager" do
149
+ before { subject.write('my_key', @complex_data) }
150
+ it_should_behave_like "when deleting existing data"
151
+ end
152
+ context "when data has been previously stored directly into the cookies hash" do
153
+ before { cookies['my_key'] = {:value => @complex_data} }
154
+ it_should_behave_like "when deleting existing data"
155
+ end
156
+ end
157
+ describe "#delete non-existent data" do
158
+ specify { subject.delete('unknown_key').should be_nil }
159
+ end
160
+ describe "#delete with key symbol/string indifferent access (ex: :foo, 'foo')" do
161
+ shared_examples_for "when deleting with key of type symbol or string" do
162
+ specify { @result.should eql @complex_data }
163
+ specify { subject.read(:my_key).should be_nil }
164
+ specify { subject.read('my_key').should be_nil }
165
+ end
166
+ shared_examples_for "when data has been written with a key of type symbol or string" do
167
+ context "when deleting with key of type symbol" do
168
+ before { @result = subject.delete(:my_key) }
169
+ it_should_behave_like "when deleting with key of type symbol or string"
170
+ end
171
+ context "when deleting with key of type string" do
172
+ before { @result = subject.delete('my_key') }
173
+ it_should_behave_like "when deleting with key of type symbol or string"
174
+ end
175
+ end
176
+ context "when data has been written with a key of type symbol" do
177
+ before { subject.write(:my_key, @complex_data) }
178
+ it_should_behave_like "when data has been written with a key of type symbol or string"
179
+ end
180
+ context "when data has been written with a key of type string" do
181
+ before { subject.write('my_key',@complex_data) }
182
+ it_should_behave_like "when data has been written with a key of type symbol or string"
183
+ end
184
+ end
185
+ end
186
+
187
+ describe "#symbol/string indifferent keys in options hash" do
188
+ describe "#read" do
189
+ before { cookies['my_key'] = pack(@complex_data) }
190
+ specify { subject.read('my_key', :unpack => true).should eql @complex_data }
191
+ specify { subject.read('my_key', 'unpack' => true).should eql @complex_data }
192
+ end
193
+ describe "#write" do
194
+ before do
195
+ subject.write('key1', @simple_data, :skip_pack => true)
196
+ subject.write('key2', @simple_data, 'skip_pack' => true)
197
+ end
198
+ specify { cookies['key1'].should eql @simple_data }
199
+ specify { cookies['key2'].should eql @simple_data }
200
+ end
201
+ describe "#delete" do
202
+ before { cookies['my_key'] = pack(@complex_data) }
203
+ specify { subject.delete('my_key', :unpack => true).should eql @complex_data }
204
+ specify { subject.delete('my_key', 'unpack' => true).should eql @complex_data }
205
+ end
206
+ end
207
+
208
+ describe "#multi-threading", :slow do
209
+ def print_inside_critical_section(method_name)
210
+ p "Inside the critical section, when calling cookies#{method_name}, the #{thread_name} thread pauses for #{sleep(2)} seconds, to make the other thread wait at the entrance of the critical section..."
211
+ end
212
+
213
+ def print_wait_before_action(action)
214
+ p "Before calling ##{action}, wait for #{sleep(1)} seconds to let the #{thread_name} thread lock the critical section..."
215
+ end
216
+
217
+ def run_thread
218
+ Thread.new do
219
+ Thread.current["name"] = thread_name
220
+ yield
221
+ end
222
+ end
223
+
224
+ def build_aspect(method_name)
225
+ Aspect.new :around, :calls_to => method_name, :on_objects => subject.cookies do |join_point, object, *args|
226
+ print_inside_critical_section(join_point.method_name) if Thread.current["name"] == thread_name
227
+ join_point.proceed
228
+ end
229
+ end
230
+
231
+ before { subject.write('my_key', @original_data = 'original data') }
232
+ after { @aspect.unadvise }
233
+
234
+ describe "#a thread is reading with a key" do
235
+ let(:thread_name) { :reader }
236
+ before do
237
+ @aspect = build_aspect('[]')
238
+ @reader = run_thread { @result = subject.read('my_key') }
239
+ end
240
+ shared_examples_for 'when another thread wants to access the data with the same key while reading' do
241
+ it "should wait until the reader finishes reading" do
242
+ @reader.join
243
+ @result.should eql @original_data
244
+ end
245
+ end
246
+ context "when another thread wants to write some new data with the same key" do
247
+ before do
248
+ print_wait_before_action(:write)
249
+ subject.write('my_key', @complex_data)
250
+ end
251
+ it_should_behave_like 'when another thread wants to access the data with the same key while reading'
252
+ end
253
+ context "when another thread wants to delete with the same key" do
254
+ before do
255
+ print_wait_before_action(:delete)
256
+ subject.delete('my_key')
257
+ end
258
+ it_should_behave_like 'when another thread wants to access the data with the same key while reading'
259
+ end
260
+ end
261
+ describe "#a thread is writing with a key" do
262
+ let(:thread_name) { :writer }
263
+ before do
264
+ @aspect = build_aspect('[]=')
265
+ @writer = run_thread { subject.write('my_key', @complex_data) }
266
+ end
267
+ shared_examples_for 'when another thread wants to access the data with the same key while writing' do
268
+ it 'should wait until the writer finishes writing' do
269
+ @writer.join
270
+ @result.should eql @complex_data
271
+ end
272
+ end
273
+ context "when another thread wants to read with same key" do
274
+ before do
275
+ print_wait_before_action(:read)
276
+ @result = subject.read('my_key')
277
+ end
278
+ it_should_behave_like 'when another thread wants to access the data with the same key while writing'
279
+ end
280
+ context "when another thread wants to delete with the same key" do
281
+ before do
282
+ print_wait_before_action(:delete)
283
+ @result = subject.delete('my_key')
284
+ end
285
+ it_should_behave_like 'when another thread wants to access the data with the same key while writing'
286
+ end
287
+ end
288
+ describe "#a thread is deleting with a key" do
289
+ let(:thread_name) { :deletor }
290
+ before do
291
+ @aspect = build_aspect(:delete)
292
+ @deletor = run_thread { @delete_result = subject.delete('my_key') }
293
+ end
294
+ shared_examples_for 'when another thread wants to access the data with the same key while deleting' do
295
+ it 'should wait until the deletor finishes deleting' do
296
+ @deletor.join
297
+ @delete_result.should eql @original_data
298
+ end
299
+ end
300
+ context "when another thread wants to read with the same key" do
301
+ before do
302
+ print_wait_before_action(:read)
303
+ @read_result = subject.read('my_key')
304
+ end
305
+ it_should_behave_like 'when another thread wants to access the data with the same key while deleting'
306
+ specify { @read_result.should be_nil }
307
+ end
308
+ context "when another thread wants to write with the same key" do
309
+ before do
310
+ print_wait_before_action(:write)
311
+ subject.write('my_key', @complex_data)
312
+ end
313
+ it_should_behave_like 'when another thread wants to access the data with the same key while deleting'
314
+ end
315
+ end
316
+ end
317
+
318
+ end
319
+
@@ -0,0 +1,24 @@
1
+ require 'spec_helper'
2
+
3
+ describe CookiesManager::ControllerAdditions do
4
+ let(:controller) { TestController.new }
5
+ context "when calling the class method :load_cookies_manager on the controller class" do
6
+ before do
7
+ mock(TestController).helper_method(:cookies_manager) # makes sure the :cookies_manager method is declared as a helper (to make it available to the views, for example)
8
+ mock(TestController).before_filter { |block| block.call(controller) } # makes sure a before_filter is set AND runs the filter block with our controller as an argument
9
+ TestController.load_cookies_manager # the class method we want to test
10
+ end
11
+ it "should create a :cookies_manager instance method" do
12
+ controller.method(:cookies_manager).should_not be_nil
13
+ end
14
+ it "the :cookies_manager method should return a CookiesManager object" do
15
+ controller.cookies_manager.is_a? CookiesManager
16
+ end
17
+ it "consecutives calls to the :cookies_manager method should return the SAME CookiesManager object" do
18
+ controller.cookies_manager.should equal controller.cookies_manager #strict equality required
19
+ end
20
+ it "the CookiesManager object should be based on the native controller's cookies hash" do
21
+ controller.cookies_manager.cookies.should equal controller.cookies #strict equality required
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,29 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ require 'action_controller'
4
+ Bundler.require(:default)
5
+
6
+ RSpec.configure do |config|
7
+ config.treat_symbols_as_metadata_keys_with_true_values = true
8
+ config.filter_run :focus => true
9
+ config.filter_run_excluding :slow => ENV["SKIP_SLOW"].present?
10
+ config.run_all_when_everything_filtered = true
11
+ config.mock_with :rr
12
+ end
13
+
14
+ # A test controller with a cookies hash (this works in rails 2.3.14 but needs to be adapted for versions of rails >= 3.x.x)
15
+ class TestController < ActionController::Base
16
+ attr_accessor :cookies
17
+
18
+ def request
19
+ @request ||= ActionController::Request.new('test')
20
+ end
21
+
22
+ def response
23
+ @response ||= ActionController::Response.new
24
+ end
25
+
26
+ def initialize
27
+ self.cookies = ActionController::CookieJar.new(self)
28
+ end
29
+ end
metadata ADDED
@@ -0,0 +1,148 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cookies_manager
3
+ version: !ruby/object:Gem::Version
4
+ hash: 25
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 1
10
+ version: 0.1.1
11
+ platform: ruby
12
+ authors:
13
+ - Christophe Levand
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-11-07 00:00:00 +01:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: rspec
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ hash: 19
30
+ segments:
31
+ - 2
32
+ - 7
33
+ - 0
34
+ version: 2.7.0
35
+ type: :development
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: actionpack
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ hash: 31
46
+ segments:
47
+ - 2
48
+ - 3
49
+ - 14
50
+ version: 2.3.14
51
+ type: :development
52
+ version_requirements: *id002
53
+ - !ruby/object:Gem::Dependency
54
+ name: rr
55
+ prerelease: false
56
+ requirement: &id003 !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ hash: 31
62
+ segments:
63
+ - 1
64
+ - 0
65
+ - 4
66
+ version: 1.0.4
67
+ type: :development
68
+ version_requirements: *id003
69
+ - !ruby/object:Gem::Dependency
70
+ name: aquarium
71
+ prerelease: false
72
+ requirement: &id004 !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ hash: 7
78
+ segments:
79
+ - 0
80
+ - 4
81
+ - 4
82
+ version: 0.4.4
83
+ type: :development
84
+ version_requirements: *id004
85
+ description: Simple cookies management tool for Rails that provides a convenient way to manage any kind of data in the cookies (strings, arrays, hashes, etc.)
86
+ email:
87
+ - levandch@gmail.com
88
+ executables: []
89
+
90
+ extensions: []
91
+
92
+ extra_rdoc_files: []
93
+
94
+ files:
95
+ - .gitignore
96
+ - .rspec
97
+ - .rvmrc
98
+ - CHANGELOG.rdoc
99
+ - Gemfile
100
+ - LICENSE
101
+ - README.rdoc
102
+ - Rakefile
103
+ - cookies_manager.gemspec
104
+ - init.rb
105
+ - lib/cookies_manager.rb
106
+ - lib/cookies_manager/base.rb
107
+ - lib/cookies_manager/controller_additions.rb
108
+ - lib/cookies_manager/version.rb
109
+ - spec/README.rdoc
110
+ - spec/cookies_manager/base_spec.rb
111
+ - spec/cookies_manager/controller_additions_spec.rb
112
+ - spec/spec_helper.rb
113
+ has_rdoc: true
114
+ homepage: ""
115
+ licenses: []
116
+
117
+ post_install_message:
118
+ rdoc_options: []
119
+
120
+ require_paths:
121
+ - lib
122
+ required_ruby_version: !ruby/object:Gem::Requirement
123
+ none: false
124
+ requirements:
125
+ - - ">="
126
+ - !ruby/object:Gem::Version
127
+ hash: 3
128
+ segments:
129
+ - 0
130
+ version: "0"
131
+ required_rubygems_version: !ruby/object:Gem::Requirement
132
+ none: false
133
+ requirements:
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ hash: 3
137
+ segments:
138
+ - 0
139
+ version: "0"
140
+ requirements: []
141
+
142
+ rubyforge_project: cookies_manager
143
+ rubygems_version: 1.6.0
144
+ signing_key:
145
+ specification_version: 3
146
+ summary: Simple cookies management tool for Rails
147
+ test_files: []
148
+