ScopedProxy 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.
@@ -0,0 +1,18 @@
1
+ == 1.0.0 / 2007-12-16
2
+
3
+ First official release.
4
+
5
+ * Added default_proxy method.
6
+
7
+ * Added delegation/answer to respond_to?
8
+
9
+ == 0.5.2
10
+
11
+ Inofficial release on our blog neotrivium.com. Fixing a few bugs due to user reports.
12
+
13
+ * v0.4 Fixed a bug with scoping (another one) pointed out to me by Severin Schoepke.
14
+ Scoping will now even work in situations where an AssociationProxy sets the context.
15
+
16
+ * v0.3 Fixed a scoping bug pointed out by fractious; now scoping should work as advertised. Thanks go
17
+ to Florian Hanke for teaming up with me on this.
18
+
@@ -0,0 +1,7 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ lib/scoped_proxy.rb
6
+ spec/scoped_proxy_spec.rb
7
+ spec/spec_helper.rb
@@ -0,0 +1,105 @@
1
+ ScopedProxy
2
+
3
+ == SUMMARY
4
+
5
+ Finally, long awaited, scoped proxies as a release. Which you say. Admitted, there are about 15 plugins
6
+ that are called scoped_proxy, but only one comes with a full suite of tests that is full 40 lines longer
7
+ than the actual code. That is the scoped proxy you are looking at. It is y2k compliant and works as a gem.
8
+ Magnificent, isn't it?
9
+
10
+ This gem is all about simplicity; you won't find a whole toolbox here, just another valued screwdriver.
11
+ But we happen to encounter a lot of screws - hopefully you do too - and find this very useful. If you
12
+ want to skip all this summary that is really longer than all the other texts in this release, have a look
13
+ at the synopsis below.
14
+
15
+ == SYNOPSIS:
16
+
17
+ Allows storing scopes as names; that way you can address subsets of your model space by meaningful names.
18
+
19
+ require 'scoped_proxy' # Railsers: You might want to call this in environment.rb
20
+
21
+ class User < ActiveRecord::Base
22
+ scoped_proxy :role do |role|
23
+ {
24
+ :find => { :conditions => ['role = ?', role] }
25
+ }
26
+ end
27
+ scoped_proxy :deleted, :find => {
28
+ :conditions => 'deleted_at is not null'
29
+ }
30
+ end
31
+
32
+ admins = User.role('admin')
33
+ admins.count # => 12
34
+ admins.find(:all) # => [ ... ]
35
+
36
+ User.deleted.count # => a number
37
+
38
+ This implementation also brings (NEW, SHINY) default proxies. That means you can have a proxy in effect
39
+ when no other proxy is in effect.
40
+
41
+ class User < ActiveRecord::Base
42
+ default_proxy :find => { :conditions => 'deleted_at is null' }
43
+ end
44
+
45
+ User.find(:all) # only finds users that aren't deleted
46
+
47
+ == REQUIREMENTS:
48
+
49
+ * gem install metaid
50
+
51
+ == TESTING
52
+
53
+ If you want to run the rspec tests, you must have a database on localhost called 'test' and a user
54
+ called 'developer' that is allowed to (write)access it. Caution: running the tests will delete at least
55
+ your 'users' table in said database. But I hope you wouldn't run this on production, nor call your
56
+ production database 'test'. Warned you.
57
+
58
+ Details can be changed in spec/spec_helper.rb
59
+
60
+ == THANKS
61
+
62
+ Special Thanks to Florian Hanke (florian at restorm dot com) and Severin Schoepke (severin at restorm dot com).
63
+ They have played an enormous role in keeping this mean and lean. Thanks also to the rest of the gang - please
64
+ visit us at labs.restorm.com.
65
+
66
+ == CONTACT
67
+
68
+ Rubyforge Project:
69
+
70
+ http://rubyforge.org/projects/swissrb
71
+
72
+ Documentation:
73
+
74
+ upcoming...
75
+
76
+ Bugs:
77
+
78
+ http://rubyforge.org/tracker/?atid=19774&group_id=5130&func=browse
79
+
80
+
81
+ == LICENSE:
82
+
83
+ (The MIT License or the Ruby License, choose)
84
+
85
+ Copyright (c) 2007 Kaspar Schiess, all rights reserved
86
+
87
+ Permission is hereby granted, free of charge, to any person obtaining
88
+ a copy of this software and associated documentation files (the
89
+ 'Software'), to deal in the Software without restriction, including
90
+ without limitation the rights to use, copy, modify, merge, publish,
91
+ distribute, sublicense, and/or sell copies of the Software, and to
92
+ permit persons to whom the Software is furnished to do so, subject to
93
+ the following conditions:
94
+
95
+ The above copyright notice and this permission notice shall be
96
+ included in all copies or substantial portions of the Software.
97
+
98
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
99
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
100
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
101
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
102
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
103
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
104
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
105
+
@@ -0,0 +1,25 @@
1
+ require 'rubygems'
2
+ require 'hoe'
3
+ require './lib/scoped_proxy'
4
+
5
+ namespace :hoe do
6
+ Hoe.new('ScopedProxy', ScopedProxy::VERSION) do |p|
7
+ p.rubyforge_name = 'swissrb'
8
+ p.author = 'Kaspar Schiess'
9
+ p.email = 'eule@space.ch'
10
+ p.summary = p.paragraphs_of('README.txt', 1..3).join("\n\n")
11
+ p.description = p.paragraphs_of('README.txt', 4..7).join(' ')
12
+ p.url = p.paragraphs_of('README.txt', 17).join(' ')
13
+ p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
14
+ end
15
+ end
16
+
17
+ require 'spec/rake/spectask'
18
+ require 'spec/translator'
19
+
20
+ task :default => :spec
21
+
22
+ desc "Run all specs in spec directory (excluding plugin specs)"
23
+ Spec::Rake::SpecTask.new(:spec) do |t|
24
+ t.spec_files = FileList['spec/**/*_spec.rb']
25
+ end
@@ -0,0 +1,142 @@
1
+ require 'metaid'
2
+ require 'active_record'
3
+
4
+ # Smart scoping for AR models
5
+ # Author: Kaspar Schiess, (c) by Neotrivium AG, 2007
6
+ # Distributed under the terms of the Ruby License
7
+
8
+ module ScopedProxy
9
+ VERSION = '1.0.0'
10
+
11
+ # Allows you to create scoped proxies in models.
12
+ #
13
+ # Example
14
+ #
15
+ # class User < ActiveRecord::Base
16
+ # scoped_proxy :woman, :find => { :conditions => ['sex=?', 'f'] }
17
+ #
18
+ # Allowed options
19
+ # * :exclusive if set to true, this proxy will not apply if there is another proxy active.
20
+ #
21
+ class Proxy
22
+ cattr_accessor :active_proxy
23
+ @@active_proxy = false
24
+
25
+ def initialize(klass, scope={}, old_scope=nil, options={})
26
+ @klass, @scope, @old_scope = klass, scope, old_scope
27
+ @options = options
28
+ end
29
+
30
+ def method_missing(message, *args, &block)
31
+ exclusive_scope = @options[:exclusive] || false
32
+ activate_proxy = (! exclusive_scope) || (exclusive_scope && no_other_proxy_active?)
33
+
34
+ conditional_scope(@old_scope) do # Install previous scope
35
+ conditional_scope(@scope, activate_proxy) do # Install this scope
36
+ with_proxy do # Track active proxies
37
+ @klass.send(message, *args, &block)
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ def respond_to_with_delegation?(symbol)
44
+ self.respond_to_without_delegation?(symbol) || @klass.respond_to?(symbol)
45
+ end
46
+ alias_method_chain :respond_to?, :delegation
47
+
48
+ # Mark the installation of a proxy. This information can be used to make proxies dependent upon context, as in
49
+ # the default proxy.
50
+ #
51
+ def with_proxy
52
+ begin
53
+ old_value = self.class.active_proxy
54
+ self.class.active_proxy = true
55
+ yield
56
+ ensure
57
+ self.class.active_proxy = old_value
58
+ end
59
+ end
60
+
61
+ # Is there no other proxy than this one active?
62
+ #
63
+ def no_other_proxy_active?
64
+ ! self.class.active_proxy
65
+ end
66
+
67
+ # Install given scope if it is not nil and the predicate is true. Then yield.
68
+ #
69
+ def conditional_scope(scope, pred=true)
70
+ if scope && pred
71
+ klass_with_scope(@klass, scope) do
72
+ yield
73
+ end
74
+ else
75
+ yield
76
+ end
77
+ end
78
+
79
+ # Permit access to with_scope from outside the AR::Base class. This is an exception to the rule ;)
80
+ #
81
+ def klass_with_scope(klass, scope, &block)
82
+ klass.send(:with_scope, scope, &block)
83
+ end
84
+ end
85
+
86
+ def self.included(base)
87
+ base.extend(ClassMethods)
88
+ end
89
+
90
+ module ClassMethods
91
+
92
+ private
93
+
94
+ # Add a default proxy to the object. A default proxy is a proxy that is always in effect, unless another proxy is
95
+ # used.
96
+ #
97
+ # Example
98
+ #
99
+ # class User
100
+ # default_proxy :find => { :conditions => { :deleted_at => nil } }
101
+ # end
102
+ #
103
+ # The example only returns users that haven't been marked as deleted - unless you use another proxy on the User
104
+ # object.
105
+ #
106
+ def default_proxy(scope=nil)
107
+ scoped_proxy :default_proxy, scope, :exclusive => true
108
+
109
+ [:find, :count].each do |meth|
110
+ meth_name = "#{meth}_with_default_proxy"
111
+ meth_without = "#{meth}_without_default_proxy"
112
+ metaclass.class_eval %Q{
113
+ def #{meth_name}(*args, &block)
114
+ default_proxy.#{meth_without}(*args, &block)
115
+ end
116
+ alias_method_chain :#{meth}, :default_proxy
117
+ }
118
+ end
119
+ end
120
+
121
+ # Install a scoped proxy with the given name.
122
+ #
123
+ # Example
124
+ #
125
+ # class User < ActiveRecord::Base
126
+ # scoped_proxy :woman, :find => { :conditions => ['sex=?', 'f'] }
127
+ #
128
+ def scoped_proxy(name, scope=nil, opts={}, &block)
129
+ meta_def(name) do |*args|
130
+ resulting_scope = scope
131
+ resulting_scope = block.call(*args) if block
132
+
133
+ target = self
134
+ Proxy.new(target, resulting_scope, self.current_scoped_methods, opts)
135
+ end
136
+ end
137
+ end
138
+ end
139
+
140
+ class ActiveRecord::Base
141
+ include ScopedProxy
142
+ end
@@ -0,0 +1,164 @@
1
+ require 'spec'
2
+
3
+ require File.join(File.dirname(__FILE__), 'spec_helper')
4
+ require 'scoped_proxy'
5
+
6
+ def silence
7
+ old_verbose, $VERBOSE = $VERBOSE, nil
8
+ yield
9
+ ensure
10
+ $VERBOSE = old_verbose
11
+ end
12
+
13
+ # NOTE: Running these tests WILL destroy a database called test (overwriting tables and such). You have been warned.
14
+
15
+ describe ScopedProxy, " availability" do
16
+ it "should be a module" do
17
+ ScopedProxy.should be_kind_of(Module)
18
+ end
19
+ end
20
+
21
+ silence do
22
+ ActiveRecord::Schema.define do
23
+ create_table :users, :force => true do |t|
24
+ t.column :name, :string
25
+ t.column :role, :string
26
+ end
27
+ end
28
+ end
29
+ class User < ActiveRecord::Base
30
+ scoped_proxy :johns, :find => {
31
+ :conditions => ['name like ?', 'john%']
32
+ }
33
+ scoped_proxy :janes, :find => {
34
+ :conditions => ['name like ?', 'jane%']
35
+ }
36
+ end
37
+ john1 = User.create!(:name => 'john1', :role => 'admin')
38
+ john2 = User.create!(:name => 'john2', :role => 'user')
39
+ jane1 = User.create!(:name => 'jane1', :role => 'user')
40
+
41
+ describe User, " and users that are john" do
42
+ it "should have johns" do
43
+ User.should respond_to(:johns)
44
+ end
45
+ it "johns should return a proxy" do
46
+ User.johns.should be_kind_of(ScopedProxy::Proxy)
47
+ end
48
+ it "should allow us to find all johns" do
49
+ johns = User.find(:all, :conditions => 'name like \'john%\'')
50
+
51
+ User.johns.find(:all).should eql(johns)
52
+ end
53
+ it "should have count and other methods" do
54
+ User.johns.find(:all)
55
+ User.johns.count
56
+ end
57
+ it "should respond to count and other methods" do
58
+ User.johns.should respond_to(:count)
59
+ User.johns.should respond_to(:find)
60
+ User.johns.should respond_to(:destroy_all)
61
+ end
62
+ end
63
+
64
+ class User
65
+ scoped_proxy :role do |role|
66
+ {
67
+ :find => { :conditions => ['role = ?', role] }
68
+ }
69
+ end
70
+ end
71
+ describe User, "extended scoping" do
72
+ it "should have role proxy" do
73
+ User.should respond_to(:role)
74
+ end
75
+ it "should select roles as a filter" do
76
+ ['admin', 'user'].each do |role|
77
+ expectation = User.find(:all, :conditions => {:role => role} )
78
+ User.role(role).find(:all).should eql(expectation)
79
+ end
80
+ end
81
+ it "should allow combining roles into a bigger filter" do
82
+ # try both orders
83
+ User.janes.role('user').find(:first).should eql(jane1)
84
+ User.role('user').janes.find(:first).should eql(jane1)
85
+ end
86
+ it "should allow storing proxy and later on using it" do
87
+ janes = User.janes
88
+
89
+ janes.find(:first).should eql(jane1)
90
+ end
91
+ it "should capture its environment" do
92
+ johns = nil
93
+ User.send(:with_scope, :find => {:conditions => { :role => 'user'} } ) do
94
+ johns = User.johns
95
+ end
96
+
97
+ johns.find(:all).should eql([john2])
98
+ end
99
+ it "should capture a complex scoped environment" do
100
+ johns = nil
101
+ User.send(:with_scope, :find => {:conditions => { :role => 'user'} } ) do
102
+ johns = User.johns.role('admin')
103
+ end
104
+
105
+ johns.find(:all).should be_empty
106
+ end
107
+ end
108
+
109
+ silence do
110
+ ActiveRecord::Schema.define do
111
+ create_table :zebras, :force => true do |t|
112
+ t.column :stripes, :boolean
113
+ end
114
+ end
115
+ end
116
+ class Zebra < ActiveRecord::Base
117
+ default_proxy :find => {
118
+ :conditions => ['stripes = ?', true]
119
+ }
120
+
121
+ # Any further proxying will go back to unproxied default
122
+ scoped_proxy :bad
123
+ end
124
+
125
+ # All good zebras have stripes
126
+ 10.times do Zebra.create!(:stripes => true) end
127
+ # Only some don't
128
+ Zebra.create!(:stripes => false)
129
+
130
+ describe Zebra, "default proxies" do
131
+ it "should not have johns or janes - that's User" do
132
+ # NOTE to the confused reader: For johns to be a method on User, it can either be a method of User's class (== Class)
133
+ # or a method of the singleton metaclass of User (== an anonymous class that inherits from Class). Now, we don't want
134
+ # stuff we define for the user (like eat_at_a_table) to be defined for all Zebras too - this is why we need #meta_def and
135
+ # not class_def. The two solutions are otherwise identical - this expectation forces the decision.
136
+ #
137
+ Zebra.should_not respond_to(:johns)
138
+ Zebra.should_not respond_to(:janes)
139
+ end
140
+
141
+ it "should select only striped zebras by default" do
142
+ Zebra.count.should == 10
143
+
144
+ Zebra.find(:all).each do |zebra|
145
+ zebra.stripes.should eql(true)
146
+ end
147
+ end
148
+ it "should select all zebras with any other proxy" do
149
+ Zebra.send(:with_scope, :find => { :conditions => ['id>0'] }) do
150
+ Zebra.bad.count.should == 11
151
+ end
152
+ Zebra.bad.count.should == 11
153
+ Zebra.bad.bad.count.should == 11
154
+
155
+ Zebra.bad.find(:all, :conditions => { :stripes => false }).each do |zebra|
156
+ zebra.stripes.should == false
157
+ end
158
+ end
159
+ it "should allow accessing the default proxy trough #default_proxy" do
160
+ default = Zebra.default_proxy
161
+
162
+ default.count.should == 10
163
+ end
164
+ end
@@ -0,0 +1,12 @@
1
+
2
+ $:.unshift File.join(File.dirname(__FILE__), '../lib')
3
+
4
+ require 'active_record'
5
+
6
+ ActiveRecord::Base.connection = {
7
+ :user => 'developer',
8
+ :adapter => 'mysql',
9
+ :database => 'test',
10
+ :host => '127.0.0.1'
11
+ }
12
+
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ScopedProxy
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ""
6
+ authors:
7
+ - Kaspar Schiess
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2007-12-21 00:00:00 +01:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: hoe
17
+ version_requirement:
18
+ version_requirements: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 1.3.0
23
+ version:
24
+ description: "== SYNOPSIS: Allows storing scopes as names; that way you can address subsets of your model space by meaningful names. require 'scoped_proxy' # Railsers: You might want to call this in environment.rb class User < ActiveRecord::Base scoped_proxy :role do |role| { :find => { :conditions => ['role = ?', role] } } end scoped_proxy :deleted, :find => { :conditions => 'deleted_at is not null' } end admins = User.role('admin') admins.count # => 12 admins.find(:all) # => [ ... ] User.deleted.count # => a number This implementation also brings (NEW, SHINY) default proxies. That means you can have a proxy in effect when no other proxy is in effect. class User < ActiveRecord::Base default_proxy :find => { :conditions => 'deleted_at is null' } end User.find(:all) # only finds users that aren't deleted"
25
+ email: eule@space.ch
26
+ executables: []
27
+
28
+ extensions: []
29
+
30
+ extra_rdoc_files:
31
+ - History.txt
32
+ - Manifest.txt
33
+ - README.txt
34
+ files:
35
+ - History.txt
36
+ - Manifest.txt
37
+ - README.txt
38
+ - Rakefile
39
+ - lib/scoped_proxy.rb
40
+ - spec/scoped_proxy_spec.rb
41
+ - spec/spec_helper.rb
42
+ has_rdoc: true
43
+ homepage: http://rubyforge.org/projects/swissrb
44
+ post_install_message:
45
+ rdoc_options:
46
+ - --main
47
+ - README.txt
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: "0"
55
+ version:
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: "0"
61
+ version:
62
+ requirements: []
63
+
64
+ rubyforge_project: swissrb
65
+ rubygems_version: 0.9.5
66
+ signing_key:
67
+ specification_version: 2
68
+ summary: == SUMMARY Finally, long awaited, scoped proxies as a release. Which you say. Admitted, there are about 15 plugins that are called scoped_proxy, but only one comes with a full suite of tests that is full 40 lines longer than the actual code. That is the scoped proxy you are looking at. It is y2k compliant and works as a gem. Magnificent, isn't it? This gem is all about simplicity; you won't find a whole toolbox here, just another valued screwdriver. But we happen to encounter a lot of screws - hopefully you do too - and find this very useful. If you want to skip all this summary that is really longer than all the other texts in this release, have a look at the synopsis below.
69
+ test_files: []
70
+