cookies_manager 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +6 -0
- data/.rspec +1 -0
- data/.rvmrc +1 -0
- data/CHANGELOG.rdoc +4 -0
- data/Gemfile +4 -0
- data/LICENSE +20 -0
- data/README.rdoc +66 -0
- data/Rakefile +14 -0
- data/cookies_manager.gemspec +25 -0
- data/init.rb +1 -0
- data/lib/cookies_manager/base.rb +178 -0
- data/lib/cookies_manager/controller_additions.rb +29 -0
- data/lib/cookies_manager/version.rb +3 -0
- data/lib/cookies_manager.rb +4 -0
- data/spec/README.rdoc +21 -0
- data/spec/cookies_manager/base_spec.rb +319 -0
- data/spec/cookies_manager/controller_additions_spec.rb +24 -0
- data/spec/spec_helper.rb +29 -0
- metadata +148 -0
data/.gitignore
ADDED
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
data/Gemfile
ADDED
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
|
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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|
+
|