trick_serial 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,17 @@
1
+ source "http://rubygems.org"
2
+ # Add dependencies required to use your gem here.
3
+ # Example:
4
+ # gem "activesupport", ">= 2.3.5"
5
+
6
+ # Add dependencies to develop your gem here.
7
+ # Include everything needed to run rake, tests, features, etc.
8
+ group :development do
9
+ gem "rspec", "~> 2.3.0"
10
+ gem "yard", "~> 0.6.0"
11
+ gem "bundler", "~> 1.1.0"
12
+ gem "jeweler", "~> 1.8.0"
13
+ gem "rcov", ">= 0"
14
+ gem 'ruby-debug', ">= 0"
15
+ gem 'rails', "~> 1.2.6"
16
+ gem 'memcache-client', ">= 1.7.0"
17
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Kurt Stephens
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,128 @@
1
+ = trick_serial
2
+
3
+ Trick Serializers using Proxies
4
+
5
+ Serialize objects using Proxies to trick serializers (e.g.: Marshal) to
6
+ not store entire object graphs (e.g.: ActiveRecord object).
7
+
8
+ trick_serial supports CGI::Session and Rack sessions to allow ActiveRecord::Base objects
9
+ to be stored directly in a session. If the object has an id, it is stored in the session
10
+ as a proxy object composed of the object's class name and id. The object is restored from
11
+ the database only if referenced through the session.
12
+
13
+ trick_serial can be used independently of ActiveRecord and Session. It can be configured to
14
+ use serialization proxies for any classes.
15
+
16
+ trick_serial maintains object identity during serialization.
17
+
18
+ == Features
19
+
20
+ Support for CGI::Session::FileStore, CGI::Session::PStore and Rails 1.2 CGI::Session::MemCacheStore.
21
+
22
+ == Usage
23
+
24
+ === ActionController session example
25
+
26
+ module AuthenticatedController
27
+ def user
28
+ # User.find(...) is executed on the demand of session[:user].
29
+ @user ||= session[:user]
30
+ end
31
+
32
+ def user= x
33
+ # [ x.class.name, x.id ] is serialized in place of User object in session[:user].
34
+ @user = session[:user] = x
35
+ end
36
+ end
37
+
38
+ class UserController < ActionController::Base
39
+ include AuthenticatedController
40
+
41
+ def login
42
+ if u = User.find(:first, :conditions => [ 'name = ? AND password = ?', params[:login], params[:password] ])
43
+ self.user = u
44
+ redirect_to :action => :home
45
+ end
46
+ end
47
+
48
+ def logout
49
+ self.user = nil
50
+ redirect_to :action => :login
51
+ end
52
+
53
+ def home
54
+ render :text => "You are #{user ? user.login : "NOT LOGGED IN!"}"
55
+ end
56
+
57
+ def update
58
+ raise unless user
59
+ user.attributes = params[:user]
60
+ user.save!
61
+ end
62
+ end
63
+
64
+ == Configuration
65
+
66
+ === Rails 1.2
67
+
68
+ Inside your Rails::Initializer block:
69
+
70
+ Rails::Initializer.run do |config|
71
+ ...
72
+ # Configure trick_serial for Rails 1.2 MemCache Sessions:
73
+ begin
74
+ require 'action_controller/session/mem_cache_store'
75
+ require 'trick_serial/serializer'
76
+ require 'trick_serial/serializer/cgi_session'
77
+
78
+ # This must be done after all session-related code is loaded:
79
+ TrickSerial::Serializer::CgiSession.activate!
80
+
81
+ # Create a serializer instance.
82
+ serializer = TrickSerial::Serializer.new
83
+
84
+ # Enable logging:
85
+ if false
86
+ serializer.logger = Log4r::Logger[:mylogger]
87
+ serializer.logger_level = :debug
88
+ end
89
+
90
+ # Instruct the serializer to create proxies for instances of any subclasses of:
91
+ serializer.proxy_class_map = {
92
+ ActiveRecord::Base => TrickSerial::Serializer::ActiveRecordProxy,
93
+ }
94
+
95
+ # Options used in CGI::Session.new(@cgi, options) from ActionController::Base.
96
+ # See ActionController::SessionManagement for details.
97
+ ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS.
98
+ merge!({
99
+ 'database_manager' => TrickSerial::Serializer::CgiSession::Store,
100
+ 'TrickSerial.database_manager' => ::CGI::Session::MemCacheStore,
101
+ 'TrickSerial.serializer' => serializer,
102
+ 'TrickSerial.logger' => Log4r::Logger[:mylogger],
103
+ 'TrickSerial.logger_level' => :info,
104
+ })
105
+ end
106
+ config.action_controller.session_store = TrickSerial::Serializer::CgiSession::Store
107
+ ...
108
+ end
109
+
110
+ === Rails 3
111
+
112
+ COMING SOON!
113
+
114
+ == Contributing to trick_serial
115
+
116
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
117
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
118
+ * Fork the project
119
+ * Start a feature/bugfix branch
120
+ * Commit and push until you are happy with your contribution
121
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
122
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
123
+
124
+ == Copyright
125
+
126
+ Copyright (c) 2011 Kurt Stephens. See LICENSE.txt for
127
+ further details.
128
+
data/Rakefile ADDED
@@ -0,0 +1,43 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'rake'
11
+
12
+ require 'jeweler'
13
+ Jeweler::Tasks.new do |gem|
14
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
15
+ gem.name = "trick_serial"
16
+ gem.homepage = "http://github.com/kstephens/trick_serial"
17
+ gem.license = "MIT"
18
+ gem.summary = %Q{A Serialization Framework.}
19
+ gem.description = %Q{Trick Serializers using Proxies.}
20
+ gem.email = "ks.github@kurtstephens.com"
21
+ gem.authors = ["Kurt Stephens"]
22
+ # Include your dependencies below. Runtime dependencies are required when using your gem,
23
+ # and development dependencies are only needed for development (ie running rake tasks, tests, etc)
24
+ # gem.add_runtime_dependency 'jabber4r', '> 0.1'
25
+ # gem.add_development_dependency 'rspec', '> 1.2.3'
26
+ end
27
+ Jeweler::RubygemsDotOrgTasks.new
28
+
29
+ require 'rspec/core'
30
+ require 'rspec/core/rake_task'
31
+ RSpec::Core::RakeTask.new(:spec) do |spec|
32
+ spec.pattern = FileList['spec/**/*_spec.rb']
33
+ end
34
+
35
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
36
+ spec.pattern = 'spec/**/*_spec.rb'
37
+ spec.rcov = true
38
+ end
39
+
40
+ task :default => :spec
41
+
42
+ require 'yard'
43
+ YARD::Rake::YardocTask.new
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.0
data/cabar.yml ADDED
@@ -0,0 +1,17 @@
1
+ cabar:
2
+ version: '1.0'
3
+
4
+ component:
5
+ version: v0.1
6
+ description: 'TrickSerial - Trick Serializers with Object Proxies'
7
+
8
+ provides:
9
+ lib/ruby: lib
10
+ action:
11
+ test: "rake"
12
+ tests: spec
13
+
14
+
15
+ requires:
16
+ component:
17
+ ruby: true
@@ -0,0 +1,3 @@
1
+ module TrickSerial
2
+ EMPTY_Hash = { }.freeze
3
+ end
@@ -0,0 +1,427 @@
1
+ require 'trick_serial'
2
+
3
+ module TrickSerial
4
+ # Serializes objects using proxies for classes defined in #proxy_class_map.
5
+ # Instances of the keys in #proxy_class_map are replaced by proxies if
6
+ # the proxy class returns true for #can_proxy?(instance).
7
+ #
8
+ # Container classes are extended with ProxySwizzling to automatically replace
9
+ # the Proxy objects with their #object when accessed.
10
+ #
11
+ # The result of this class does not require explicit decoding. However,
12
+ # this particular class only works with serializers that can handle
13
+ # Hash and Array objects extended with Modules.
14
+ #
15
+ # See Serializer::Simple for support for simpler encode/decode behavior
16
+ # without ProxySwizzling support.
17
+ class Serializer
18
+ # Boolean or Proc.
19
+ attr_accessor :enabled
20
+
21
+ attr_accessor :logger, :logger_level
22
+ attr_accessor :verbose, :debug
23
+ attr_reader :root
24
+
25
+ attr_accessor :class_option_map
26
+ @@class_option_map = nil
27
+ def self.class_option_map
28
+ @@class_option_map
29
+ end
30
+ def self.class_option_map= x
31
+ @@class_option_map = x
32
+ end
33
+
34
+ @@default = nil
35
+ def self.default
36
+ Thread.current[:'TrickSerial::Serializer.default'] ||
37
+ @@default
38
+ end
39
+ def self.default= x
40
+ @@default = x
41
+ end
42
+
43
+ def initialize
44
+ @class_option_map ||= @@class_option_map || EMPTY_Hash
45
+ @enabled = true
46
+ @debug = 0
47
+ end
48
+
49
+ def enabled?
50
+ case @enabled
51
+ when Proc
52
+ @enabled.call
53
+ else
54
+ @enabled
55
+ end
56
+ end
57
+
58
+ # Same as #encode!, but copies Array and Hash structures
59
+ # recursively.
60
+ # Does not copy structure if #enabled? is false.
61
+ def encode x
62
+ return x unless enabled?
63
+ @copy = true
64
+ encode! x
65
+ end
66
+
67
+ # Encodes using #proxy_class_map in-place.
68
+ def encode! x
69
+ _prepare x do
70
+ _encode! x
71
+ end
72
+ end
73
+
74
+ # Same as #decode!, but copies Array and Hash structures
75
+ # recursively.
76
+ # Does not copy structure if #enabled? is false.
77
+ # Only implemented by some subclasses.
78
+ def decode x
79
+ return x unless enabled?
80
+ @copy = true
81
+ decode! x
82
+ end
83
+
84
+ # Decodes using #proxy_class_map in-place.
85
+ # Only implemented by some subclasses.
86
+ def decode! x
87
+ _prepare x do
88
+ _decode! x
89
+ end
90
+ end
91
+
92
+ def _prepare x
93
+ return x unless enabled?
94
+ proxyable
95
+ @root = x
96
+ @visited = { }
97
+ @object_to_proxy_map = { }
98
+ # debugger
99
+ yield
100
+ ensure
101
+ @visited.clear if @visited
102
+ @object_to_proxy_map.clear if @object_to_proxy_map
103
+ @copy =
104
+ @visited =
105
+ @object_to_proxy_map =
106
+ @root = nil
107
+ end
108
+
109
+ # Returns a list of Modules that are proxable based on the configuration.
110
+ def proxyable
111
+ unless @proxyable
112
+ @proxyable = @class_option_map.keys.select{|cls| ! @class_option_map[cls][:do_not_traverse]}
113
+ @do_not_traverse ||= @class_option_map.keys.select{|cls| @class_option_map[cls][:do_not_traverse]} << ObjectProxy
114
+ @class_option_cache ||= { }
115
+ @proxyable.freeze
116
+ end
117
+ @proxyable
118
+ end
119
+
120
+
121
+ ##################################################################
122
+
123
+
124
+ def _encode! x
125
+ # pp [ :_encode!, x.class, x.object_id, x.to_s ] if @debug >= 1
126
+
127
+ case x
128
+ when *@do_not_traverse
129
+ # NOTHING
130
+
131
+ when ObjectProxy
132
+ # NOTHING
133
+
134
+ when Struct
135
+ if o = @visited[x.object_id]
136
+ return o.first
137
+ end
138
+ o = x
139
+ x = _copy_with_extensions(x)
140
+ @visited[o.object_id] = [ x, o ]
141
+ x = o
142
+ x.class.members.each do | m |
143
+ v = x.send(m)
144
+ v = _encode! v
145
+ x.send(:"#{m}=", v)
146
+ end
147
+
148
+ when OpenStruct
149
+ if o = @visited[x.object_id]
150
+ return o.first
151
+ end
152
+ o = x
153
+ x = _copy_with_extensions(x)
154
+ @visited[o.object_id] = [ x, o ]
155
+ x = o
156
+ t = x.instance_variable_get("@table")
157
+ t.each do | k, v |
158
+ t[k] = _encode! v
159
+ end
160
+
161
+ when Array
162
+ if o = @visited[x.object_id]
163
+ return o.first
164
+ end
165
+ o = x
166
+ x = _copy_with_extensions(x)
167
+ @visited[o.object_id] = [ x, o ]
168
+ extended = false
169
+ x.map! do | v |
170
+ v = _encode! v
171
+ if ! extended && ObjectProxy === v
172
+ x.extend ProxySwizzlingArray
173
+ extended = true
174
+ end
175
+ v
176
+ end
177
+
178
+ when Hash
179
+ if o = @visited[x.object_id]
180
+ return o.first
181
+ end
182
+ o = x
183
+ x = _copy_with_extensions(x)
184
+ @visited[o.object_id] = [ x, o ]
185
+ extended = false
186
+ x.keys.to_a.each do | k |
187
+ # pp [ :Hash_key, k ] if @debug >= 1
188
+ v = x[k] = _encode!(x[k])
189
+ if ! extended && ObjectProxy === v
190
+ x.extend ProxySwizzlingHash
191
+ extended = true
192
+ end
193
+ end
194
+
195
+ when *@proxyable
196
+ if proxy = @object_to_proxy_map[x.object_id]
197
+ # if @debug >= 1
198
+ # o = proxy.first
199
+ # $stderr.puts " #{x.class} #{x.object_id} ==>> (#{o.class} #{o.object_id})"
200
+ # end
201
+ return proxy.first
202
+ end
203
+ # debugger
204
+
205
+ o = x
206
+ proxy_x = proxy_cls = nil
207
+ if class_option = _class_option(x)
208
+ proxy_cls = class_option[:proxy_class]
209
+ # Deeply encode instance vars?
210
+ if ivs = class_option[:instance_vars]
211
+ ivs = x.instance_variables if ivs == true
212
+ x = _copy_with_extensions x
213
+ proxy_x = _make_proxy o, x, proxy_cls
214
+ ivs.each do | ivar |
215
+ v = x.instance_variable_get(ivar)
216
+ v = _encode!(v)
217
+ if ObjectProxy === v
218
+ ivar.freeze
219
+ v = ProxySwizzlingIvar.new(x, ivar, v)
220
+ end
221
+ x.instance_variable_set(ivar, v)
222
+ end
223
+ else
224
+ proxy_x = _make_proxy o, x, proxy_cls
225
+ end
226
+ end
227
+ x = proxy_x if proxy_cls
228
+ end
229
+
230
+ # pp [ :"_encode!=>", x.class, x.object_id, x.to_s ] if @debug >= 1
231
+
232
+ x
233
+ end # def
234
+
235
+ def _class_option x
236
+ (@class_option_cache[x.class] ||=
237
+ [
238
+ x.class.ancestors.
239
+ map { |c| @class_option_map[c] }.
240
+ find { |c| c }
241
+ ]).first
242
+ end
243
+
244
+ # Create a proxy for x for original object o.
245
+ # x may be a dup of o.
246
+ def _make_proxy o, x, proxy_cls
247
+ # Can the object x be proxied for the original object o?
248
+ # i.e. does it have an id?
249
+ if proxy_cls && proxy_cls.can_proxy?(x)
250
+ x = proxy_cls.new(x, self)
251
+ _log { "created proxy #{x} for #{o.class} #{o.id}" }
252
+ end
253
+ @object_to_proxy_map[o.object_id] = [ x, o ]
254
+ x
255
+ end
256
+
257
+ def _copy_with_extensions x
258
+ if @copy
259
+ o = x.dup
260
+ (_extended_by(x) - _extended_by(o)).reverse_each do | m |
261
+ o.extend(m)
262
+ end rescue nil # :symbol.extend(m) => TypeError: can't define singleton
263
+ x = o
264
+ end
265
+ x
266
+ end
267
+
268
+ # This is similar to Rails Object#extended_by.
269
+ def _extended_by x
270
+ # Note: if Symbol === x this happens:
271
+ # #<TypeError: no virtual class for Symbol>
272
+ (class << x; ancestors; end) rescue [ ]
273
+ end
274
+
275
+ def _log msg = nil
276
+ if @logger
277
+ msg ||= yield if block_given?
278
+ @logger.send(@logger_level, msg) if msg
279
+ end
280
+ end
281
+
282
+
283
+ module ObjectProxy
284
+ class Error < ::Exception
285
+ class DisappearingObject < self; end
286
+ end
287
+
288
+ attr_reader :cls, :id
289
+
290
+ def self.included target
291
+ super
292
+ end
293
+
294
+ def initialize obj, serializer
295
+ self.object = obj
296
+ end
297
+
298
+ def resolve_class
299
+ @resolve_class ||=
300
+ eval("::#{@cls.to_s}")
301
+ end
302
+
303
+ def object= x
304
+ # @object = x
305
+ @cls = x && x.class.name.to_sym
306
+ @id = x && x.id
307
+ end
308
+ end # module
309
+
310
+ class ActiveRecordProxy
311
+ include ObjectProxy
312
+
313
+ def self.can_proxy?(obj)
314
+ obj.id
315
+ end
316
+
317
+ def object
318
+ # STDERR.puts "#{self}#object find #{@cls.inspect} #{@id.inspect}" unless @object
319
+ @object ||=
320
+ resolve_class.find(@id) ||
321
+ (raise Error::DisappearingObject, "#{@cls.inspect} #{@id.inspect}")
322
+ end
323
+ end # class
324
+
325
+ ##################################################################
326
+
327
+ # Base module for all ProxySwizzling.
328
+ # http://en.wikipedia.org/wiki/Pointer_swizzling
329
+ module ProxySwizzling
330
+ end
331
+
332
+ class ProxySwizzlingIvar
333
+ include ProxySwizzling
334
+ alias :_proxy_class :class
335
+ def class
336
+ method_missing :class
337
+ end
338
+
339
+ alias :_proxy_object_id :object_id
340
+ def object_id
341
+ method_missing :object_id
342
+ end
343
+
344
+ alias :_proxy_id :id
345
+ def id
346
+ method_missing :id
347
+ end
348
+
349
+ def initialize owner, name, value
350
+ @owner, @name, @value = owner, name, value
351
+ end
352
+ private :initialize
353
+
354
+ def method_missing sel, *args, &blk
355
+ if @owner
356
+ if ObjectProxy === @value
357
+ @value = @value.object
358
+ end
359
+ @owner.instance_variable_set(@name, @value)
360
+ @owner = @name = nil
361
+ end
362
+ @value.__send__(sel, *args, &blk)
363
+ end
364
+ end # class
365
+
366
+ module ProxySwizzlingArray
367
+ include ProxySwizzling
368
+ def [](i)
369
+ p = super
370
+ if ! @does_not_have_proxies && ObjectProxy === p
371
+ p = self[i] = p.object
372
+ end
373
+ p
374
+ end
375
+
376
+ def each
377
+ unless @does_not_have_proxies
378
+ size.times do | i |
379
+ self[i]
380
+ end
381
+ @does_not_have_proxies = false
382
+ end
383
+ super
384
+ end
385
+
386
+ def map!
387
+ each { | e | e }
388
+ super
389
+ end
390
+
391
+ def select
392
+ each { | e | }
393
+ super
394
+ end
395
+ end # module
396
+
397
+ module ProxySwizzlingHash
398
+ include ProxySwizzling
399
+ def [](i)
400
+ if ObjectProxy === (p = super)
401
+ p = self[i] = p.object
402
+ end
403
+ p
404
+ end
405
+
406
+ def each
407
+ values
408
+ super
409
+ end
410
+
411
+ def each_pair
412
+ values
413
+ super
414
+ end
415
+
416
+ def values
417
+ keys.to_a.each do | k |
418
+ self[k]
419
+ end
420
+ super
421
+ end
422
+ end # module
423
+
424
+ end # class
425
+ end # module
426
+
427
+