methodmissing-scrooge 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. data/README.textile +262 -0
  2. data/lib/scrooge/core/string.rb +29 -0
  3. data/lib/scrooge/core/symbol.rb +21 -0
  4. data/lib/scrooge/core/thread.rb +26 -0
  5. data/lib/scrooge/framework/base.rb +304 -0
  6. data/lib/scrooge/framework/rails.rb +107 -0
  7. data/lib/scrooge/middleware/tracker.rb +47 -0
  8. data/lib/scrooge/orm/active_record.rb +143 -0
  9. data/lib/scrooge/orm/base.rb +102 -0
  10. data/lib/scrooge/profile.rb +204 -0
  11. data/lib/scrooge/storage/base.rb +47 -0
  12. data/lib/scrooge/storage/memory.rb +25 -0
  13. data/lib/scrooge/tracker/app.rb +101 -0
  14. data/lib/scrooge/tracker/base.rb +56 -0
  15. data/lib/scrooge/tracker/model.rb +90 -0
  16. data/lib/scrooge/tracker/resource.rb +201 -0
  17. data/lib/scrooge.rb +62 -0
  18. data/spec/fixtures/config/scrooge/scopes/1234567891/scope.yml +2 -0
  19. data/spec/fixtures/config/scrooge.yml +12 -0
  20. data/spec/helpers/framework/rails/cache.rb +25 -0
  21. data/spec/spec_helper.rb +51 -0
  22. data/spec/units/scrooge/core/string_spec.rb +21 -0
  23. data/spec/units/scrooge/core/symbol_spec.rb +13 -0
  24. data/spec/units/scrooge/core/thread_spec.rb +15 -0
  25. data/spec/units/scrooge/framework/base_spec.rb +154 -0
  26. data/spec/units/scrooge/framework/rails_spec.rb +40 -0
  27. data/spec/units/scrooge/orm/base_spec.rb +61 -0
  28. data/spec/units/scrooge/profile_spec.rb +73 -0
  29. data/spec/units/scrooge/storage/base_spec.rb +35 -0
  30. data/spec/units/scrooge/storage/memory_spec.rb +20 -0
  31. data/spec/units/scrooge/tracker/app_spec.rb +62 -0
  32. data/spec/units/scrooge/tracker/base_spec.rb +21 -0
  33. data/spec/units/scrooge/tracker/model_spec.rb +50 -0
  34. data/spec/units/scrooge/tracker/resource_spec.rb +83 -0
  35. data/spec/units/scrooge_spec.rb +13 -0
  36. metadata +110 -0
@@ -0,0 +1,107 @@
1
+ module Scrooge
2
+ module Framework
3
+ class Rails < Base
4
+
5
+ # Look for RAILS_ROOT and Rails.
6
+
7
+ signature do
8
+ defined?(RAILS_ROOT)
9
+ end
10
+
11
+ signature do
12
+ Object.const_defined?( "Rails" )
13
+ end
14
+
15
+ def environment
16
+ ::RAILS_ENV
17
+ end
18
+
19
+ def root
20
+ ::Rails.root
21
+ end
22
+
23
+ def tmp
24
+ @tmp ||= File.join( ::Rails.root, 'tmp' )
25
+ end
26
+
27
+ def config
28
+ @config ||= File.join( ::Rails.root, 'config' )
29
+ end
30
+
31
+ def logger
32
+ ::Rails.logger
33
+ end
34
+
35
+ def resource( env, request = nil )
36
+ GUARD.synchronize do
37
+ # TODO: Wonky practice to piggy back on this current Edge / 2.3 hack
38
+ request = request || env['action_controller.rescue.request']
39
+ supplement_current_resource!( request )
40
+ Thread.scrooge_resource = Scrooge::Base.profile.tracker.resource_for( Thread.scrooge_resource )
41
+ end
42
+ end
43
+
44
+ def read_cache( key )
45
+ ::Rails.cache.read( key )
46
+ end
47
+
48
+ def write_cache( key, value )
49
+ ::Rails.cache.write( key, value )
50
+ end
51
+
52
+ def middleware
53
+ ::Rails.configuration.middleware
54
+ end
55
+
56
+ # Push the Tracking middleware into the first slot.
57
+ #
58
+ def install_tracking_middleware
59
+ GUARD.synchronize do
60
+ ActionController::Dispatcher.to_prepare( :scrooge_install_tracking_middleware ) do
61
+ ApplicationController.prepend_around_filter Scrooge::Middleware::Tracker
62
+ end
63
+ end
64
+ end
65
+
66
+ # Install per Resource scoping middleware.
67
+ #
68
+ def install_scope_middleware( tracker )
69
+ GUARD.synchronize do
70
+ ActionController::Dispatcher.to_prepare( :scrooge_install_scope_middleware ) do
71
+ tracker.resources.each do |resource|
72
+ install_scope_middleware_for_resource!( resource )
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ def initialized( &block )
79
+ begin
80
+ ::Rails.configuration.after_initialize( &block )
81
+ rescue NameError
82
+ # No cofig initialized - plugin installation etc.
83
+ end
84
+ end
85
+
86
+ def controller( resource )
87
+ "#{resource.controller}_controller".classify.constantize
88
+ end
89
+
90
+ private
91
+
92
+ def install_scope_middleware_for_resource!( resource ) #:nodoc:
93
+ resource.middleware.each do |resource_middleware|
94
+ controller( resource ).prepend_around_filter resource_middleware, :only => resource.action
95
+ end
96
+ end
97
+
98
+ def supplement_current_resource!( request ) #:nodoc:
99
+ Thread.scrooge_resource.controller = request.path_parameters['controller']
100
+ Thread.scrooge_resource.action = request.path_parameters['action']
101
+ Thread.scrooge_resource.method = request.method
102
+ Thread.scrooge_resource.format = request.format.to_s
103
+ end
104
+
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,47 @@
1
+ module Scrooge
2
+ module Middleware
3
+ class Tracker < Scrooge::Base
4
+
5
+ class << self
6
+
7
+ # Around Filter compatible implementation for Rails as Dispatcher is
8
+ # the root Rack application and as such don't provide access to the Rails
9
+ # Routing internals from other middleware.
10
+ #
11
+ def filter( controller, &block )
12
+ Scrooge::Base.profile.tracker.track( Thread.scrooge_resource ) do
13
+ begin
14
+ Scrooge::Base.profile.framework.resource( {}, controller.request )
15
+ Scrooge::Base.profile.log "Track for Resource #{Thread.scrooge_resource.inspect}"
16
+ block.call
17
+ ensure
18
+ Thread.reset_scrooge_resource!
19
+ end
20
+ end
21
+ end
22
+
23
+ end
24
+
25
+ def initialize(app, options = {})
26
+ @app = app
27
+ end
28
+
29
+ # Assign a default Resource Tracker instance to Thread.current[:scrooge_resource]
30
+ # and supplement it with request specific details ( format, action && controller )
31
+ # after yielding to the app.Flush Thread.current[:scrooge_resource] on completion.
32
+ #
33
+ def call(env)
34
+ Scrooge::Base.profile.tracker.track( Thread.scrooge_resource ) do
35
+ begin
36
+ result = @app.call(env)
37
+ Scrooge::Base.profile.framework.resource( env )
38
+ result
39
+ ensure
40
+ Thread.reset_scrooge_resource!
41
+ end
42
+ end
43
+ end
44
+
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,143 @@
1
+ module Scrooge
2
+ module Orm
3
+ class ActiveRecord < Base
4
+ module ScroogeAttributes
5
+
6
+ module SingletonMethods
7
+
8
+ private
9
+
10
+ # Attach to generated attribute reader methods.
11
+
12
+ def define_read_method(symbol, attr_name, column)
13
+ register_with_scrooge!( attr_name, 'define read method' )
14
+ super(symbol, attr_name, column)
15
+ end
16
+
17
+ def define_read_method_for_time_zone_conversion(attr_name)
18
+ register_with_scrooge!( attr_name, 'define read method for time zone conversion' )
19
+ super(attr_name)
20
+ end
21
+
22
+ def define_read_method_for_serialized_attribute(attr_name)
23
+ register_with_scrooge!( attr_name, 'define read method for serialized attribute' )
24
+ super(attr_name)
25
+ end
26
+
27
+ private
28
+
29
+ def register_with_scrooge!( attr_name, caller ) #:nodoc:
30
+ if ::Scrooge::Base.profile.orm.track?
31
+ logger.info "[Scrooge] #{caller} #{attr_name.to_s}"
32
+ Thread.scrooge_resource << [self.base_class, attr_name]
33
+ end
34
+ end
35
+
36
+ end
37
+
38
+ module InstanceMethods
39
+
40
+ # Attach to AR::Base#read_attribute.
41
+ #
42
+ def read_attribute(attr_name)
43
+ register_with_scrooge!( attr_name, 'read attribute' )
44
+ super( attr_name )
45
+ end
46
+
47
+ # Attach to AR::Base#read_attribute_before_typecast.
48
+ #
49
+ def read_attribute_before_type_cast(attr_name)
50
+ register_with_scrooge!( attr_name, 'read attribute before type cast' )
51
+ super(attr_name)
52
+ end
53
+
54
+ private
55
+
56
+ def register_with_scrooge!( attr_name, caller ) #:nodoc:
57
+ if ::Scrooge::Base.profile.orm.track?
58
+ logger.info "[Scrooge] #{caller} #{attr_name.to_s}"
59
+ Thread.scrooge_resource << [self.class.base_class, attr_name]
60
+ end
61
+ end
62
+
63
+ def missing_attribute(attr_name, stack) #:nodoc:
64
+ if Scrooge::Base.profile.raise_on_missing_attribute?
65
+ super(attr_name, stack)
66
+ else
67
+ logger.info "[Scrooge] missing attribute #{attr_name.to_s}"
68
+ reload( :select => '*' )
69
+ end
70
+ end
71
+
72
+ end
73
+
74
+ end
75
+
76
+ class << self
77
+
78
+ # Inject Scrooge ActiveRecord attribute tracking.
79
+ #
80
+ def install!
81
+ ::ActiveRecord::Base.send( :extend, Scrooge::Orm::ActiveRecord::ScroogeAttributes::SingletonMethods )
82
+ ::ActiveRecord::Base.send( :include, Scrooge::Orm::ActiveRecord::ScroogeAttributes::InstanceMethods )
83
+ end
84
+
85
+ # Determine if the ActiveRecord attribute tracker has already been installed.
86
+ #
87
+ def installed?
88
+ begin
89
+ ::ActiveRecord::Base.included_modules.include?( Scrooge::Orm::ActiveRecord::ScroogeAttributes )
90
+ rescue => exception
91
+ end
92
+ end
93
+
94
+ end
95
+
96
+ # Generate scope helpers for a given resource.
97
+ #
98
+ def scope_to( resource )
99
+ resource.models.each do |model|
100
+ scope_resource_to_model( resource, model )
101
+ end
102
+ end
103
+
104
+ # Generate scope helpers for a given model and resource.
105
+ #
106
+ def scope_resource_to_model( resource, model )
107
+ method_name = resource_scope_method( resource )
108
+ klass = model.model.to_const!(false) if model.model.is_a?(String)
109
+ unless resource_scope_method?( resource, klass )
110
+ klass.instance_eval(<<-EOS, __FILE__, __LINE__)
111
+ def #{method_name}(&block)
112
+ with_scope( { :find => { :select => '#{model.to_sql}' } }) do
113
+ block.call
114
+ end
115
+ end
116
+ EOS
117
+ end
118
+ end
119
+
120
+ # Returns a lookup key from a given String or AR klass
121
+ #
122
+ def name( model )
123
+ model = model.to_const!(false) if model.is_a?(String)
124
+ model.respond_to?(:base_class) ? model.base_class.to_s : model.to_s
125
+ end
126
+
127
+ # Returns a table name from a given String or AR klass
128
+ #
129
+ def table_name( model )
130
+ model = model.to_const!(false) if model.is_a?(String)
131
+ model.table_name
132
+ end
133
+
134
+ # Returns a primary key from a given String or AR klass
135
+ #
136
+ def primary_key( model )
137
+ model = model.to_const!(false) if model.is_a?(String)
138
+ model.primary_key
139
+ end
140
+
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,102 @@
1
+ module Scrooge
2
+ module Orm
3
+
4
+ autoload :ActiveRecord, 'scrooge/orm/active_record'
5
+
6
+ class Base < Scrooge::Base
7
+
8
+ # ORM agnostic base class.
9
+
10
+ class NotImplemented < StandardError
11
+ end
12
+
13
+ class << self
14
+
15
+ # Instantiate and optionally install from a given ORM signature.
16
+ #
17
+ def instantiate( orm_signature )
18
+ orm_instance = "scrooge/orm/#{orm_signature.to_s}".to_const!
19
+ orm_instance.class.install! unless orm_instance.class.installed?
20
+ orm_instance
21
+ end
22
+
23
+ # Subclasses is required to implemented an installation hook.
24
+ #
25
+ def install!
26
+ raise NotImplemented
27
+ end
28
+
29
+ # Subclasses should be able to determine if it's already been injected into
30
+ # the underlying ORM package.
31
+ #
32
+ def installed?
33
+ raise NotImplemented
34
+ end
35
+
36
+ end
37
+
38
+ # Generate scope helpers for a Resource and it's related Model trackers.
39
+ #
40
+ def scope_to( resource )
41
+ raise NotImplemented
42
+ end
43
+
44
+ # Generate scope helpers for a given Resource and Model tracker.
45
+ #
46
+ def scope_resource_to_model( resource, model )
47
+ raise NotImplemented
48
+ end
49
+
50
+ # Returns a lookup key from a given String or class
51
+ #
52
+ def name( model )
53
+ raise NotImplemented
54
+ end
55
+
56
+ # Returns a table name from a given String or class
57
+ #
58
+ def table_name( model )
59
+ raise NotImplemented
60
+ end
61
+
62
+ # Returns a primary key from a given String or class
63
+ #
64
+ def primary_key( model )
65
+ raise NotImplemented
66
+ end
67
+
68
+ # Generates a sanitized method name for a given resource.
69
+ #
70
+ def resource_scope_method( resource )
71
+ "scope_to_#{resource.signature}".to_sym
72
+ end
73
+
74
+ # Determine if a scope method has already been generated for a given
75
+ # Resource and klass.
76
+ #
77
+ def resource_scope_method?( resource, klass )
78
+ klass.respond_to?( resource_scope_method( resource ) )
79
+ end
80
+
81
+ # Only track if the current profile is configured for tracking and a tracker
82
+ # resource is active, iow. we're in the scope of a request.
83
+ #
84
+ def track?
85
+ profile.track? && resource?
86
+ end
87
+
88
+ # Do we have access to Resource Tracker instance ?
89
+ #
90
+ def resource?
91
+ !resource().nil?
92
+ end
93
+
94
+ # Delegate to Thread.current
95
+ #
96
+ def resource
97
+ Thread.current[:scrooge_resource]
98
+ end
99
+
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,204 @@
1
+ require 'yaml'
2
+
3
+ module Scrooge
4
+ class Profile
5
+
6
+ # A Profile for Scrooge that holds configuration, determines if we're tracking or
7
+ # scoping and provides access to a Tracker, ORM, Storage and Framework instance.
8
+
9
+ class Signature
10
+ end
11
+
12
+ class << self
13
+
14
+ # Setup a new instance from the path to a YAML configuration file and a
15
+ # given environment.
16
+ #
17
+ def setup( path, environment )
18
+ begin
19
+ new( read_config( path, environment ) )
20
+ rescue Errno::ENOENT
21
+ puts "Scrooge Configuration file not available."
22
+ end
23
+ end
24
+
25
+ # Pairs profile setup with the host framework.
26
+ #
27
+ def setup!
28
+ setup( framework.configuration_file, framework.environment )
29
+ end
30
+
31
+ # Yields an instance to a wrapper of the host framework.
32
+ #
33
+ def framework
34
+ @@framework ||= Scrooge::Framework::Base.instantiate
35
+ end
36
+
37
+ private
38
+
39
+ def read_config( path, environment ) #:nodoc:
40
+ YAML.load( IO.read( path ) )[environment.to_s]
41
+ end
42
+
43
+ end
44
+
45
+ attr_reader :options
46
+
47
+ def initialize( options = {} )
48
+ self.options = options
49
+ end
50
+
51
+ # options writer that attempts to reconfigure for any configuration changes.
52
+ #
53
+ def options=( options )
54
+ @options = options
55
+ configure!
56
+ end
57
+
58
+ # Delegates to the underlying ORM framework.
59
+ #
60
+ def orm
61
+ @orm_instance ||= Scrooge::Orm::Base.instantiate( @orm )
62
+ end
63
+
64
+ # Delegates to the current storage backend.
65
+ #
66
+ def storage
67
+ @storage_instance ||= Scrooge::Storage::Base.instantiate( @storage )
68
+ end
69
+
70
+ # Delegates to the current framework.
71
+ #
72
+ def framework
73
+ self.class.framework
74
+ end
75
+
76
+ # Log a message to the framework's logger.
77
+ #
78
+ def log( message )
79
+ framework.log( message ) rescue ''
80
+ end
81
+
82
+ # Delegates to the Application Tracker.
83
+ #
84
+ def tracker
85
+ @tracker_instance ||= Scrooge::Tracker::App.new
86
+ end
87
+
88
+ # Determine if this is a tracking or scope profile.
89
+ #
90
+ def track_or_scope!
91
+ if enabled?
92
+ track? ? track! : scope!
93
+ end
94
+ end
95
+
96
+ # Are we tracking ?
97
+ #
98
+ def track?
99
+ if enabled?
100
+ @track ||= (@scope || '').match( /\d{10}/ ).nil?
101
+ else
102
+ false
103
+ end
104
+ end
105
+
106
+ def track!
107
+ if track?
108
+ log "Tracking"
109
+ framework.install_tracking_middleware()
110
+ shutdown_hook!
111
+ end
112
+ end
113
+
114
+ # The active scope signature
115
+ #
116
+ def scope_to
117
+ @scope
118
+ end
119
+
120
+ # Are we scoping ?
121
+ #
122
+ def scope?
123
+ if enabled?
124
+ !track?
125
+ else
126
+ false
127
+ end
128
+ end
129
+
130
+ # Scope the tracker environment to a given scope signature.
131
+ #
132
+ def scope_to_signature!( scope_signature )
133
+ log "Scope to #{scope_signature}"
134
+ @tracker_instance = framework.from_scope!( scope_signature )
135
+ end
136
+
137
+ # Scope the tracker environment to a given scope signature and install
138
+ # scoping middleware.
139
+ #
140
+ def scope_to!
141
+ if scope?
142
+ scope_to_signature!( @scope )
143
+ framework.install_scope_middleware( tracker )
144
+ end
145
+ end
146
+ alias :scope! :scope_to!
147
+
148
+ # Should Scrooge inject itself ?
149
+ #
150
+ def enabled?
151
+ @enabled
152
+ end
153
+
154
+ # Should we raise on missing attributes ?
155
+ #
156
+ def raise_on_missing_attribute?
157
+ @on_missing_attribute == :raise
158
+ end
159
+
160
+ private
161
+
162
+ def configure! #:nodoc:
163
+ @orm = configure_with( @options['orm'], [:active_record], :active_record )
164
+ @storage = configure_with( @options['storage'], [:memory], :memory )
165
+ @scope = configure_with( @options['scope'].to_s, framework.scopes, ENV['scope'] )
166
+ @enabled = configure_with( @options['enabled'], [true, false], false )
167
+ @on_missing_attribute = configure_with( @options['on_missing_attribute'], [:reload, :raise], :reload )
168
+ reset_backends!
169
+ memoize_backends!
170
+ end
171
+
172
+ def configure_with( given, valid, default ) #:nodoc:
173
+ if given
174
+ valid.include?( given ) ? given : default
175
+ else
176
+ default
177
+ end
178
+ end
179
+
180
+ def reset_backends! #:nodoc:
181
+ @orm_instance = nil
182
+ @tracker_instance = nil
183
+ @storage_instance = nil
184
+ end
185
+
186
+ # Force constant lookups as autoload is not threadsafe.
187
+ #
188
+ def memoize_backends! #:nodoc:
189
+ framework()
190
+ orm()
191
+ storage()
192
+ tracker()
193
+ end
194
+
195
+ def shutdown_hook! #:nodoc:
196
+ # Registers an at_exit hook to persist the current application scope.
197
+ ::Kernel.at_exit do
198
+ log "shutdown ..."
199
+ framework.scope! if tracker.any?
200
+ end
201
+ end
202
+
203
+ end
204
+ end
@@ -0,0 +1,47 @@
1
+ module Scrooge
2
+ module Storage
3
+
4
+ autoload :Buffer, 'scrooge/storage/buffer'
5
+ autoload :Memory, 'scrooge/storage/memory'
6
+
7
+ class Base < Scrooge::Base
8
+ class NotImplemented < StandardError
9
+ end
10
+
11
+ # A Single Mutex for all Storage subclasses as their's only one storage instance.
12
+ #
13
+ GUARD = Mutex.new
14
+
15
+ NAMESPACE = 'scrooge_storage'.freeze
16
+
17
+ class << self
18
+
19
+ # Yields a storage instance from a given signature.
20
+ #
21
+ def instantiate( storage_signature )
22
+ "scrooge/storage/#{storage_signature.to_s}".to_const!
23
+ end
24
+
25
+ end
26
+
27
+ # Retrieve a given tracker from storage.
28
+ #
29
+ def read( tracker )
30
+ raise NotImplemented
31
+ end
32
+
33
+ # Persist a given tracker to storage.
34
+ #
35
+ def write( tracker, buffered = true )
36
+ raise NotImplemented
37
+ end
38
+
39
+ # Namespace lookup keys.
40
+ #
41
+ def expand_key( key )
42
+ "#{NAMESPACE}/#{key}"
43
+ end
44
+
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,25 @@
1
+ module Scrooge
2
+ module Storage
3
+ class Memory < Base
4
+
5
+ attr_reader :storage
6
+
7
+ def initialize
8
+ @storage = {}
9
+ end
10
+
11
+ def read( tracker )
12
+ GUARD.synchronize do
13
+ @storage[tracker.signature]
14
+ end
15
+ end
16
+
17
+ def write( tracker, buffered = true )
18
+ GUARD.synchronize do
19
+ @storage[tracker.signature] = tracker
20
+ end
21
+ end
22
+
23
+ end
24
+ end
25
+ end