gcnovus-arns 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +48 -0
- data/Rakefile +64 -0
- data/lib/active_resource/named_scope.rb +167 -0
- data/test/named_scope_test.rb +147 -0
- data/test/test_helper.rb +7 -0
- metadata +60 -0
data/README.rdoc
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
= ARNS adds named_scopes to ActiveResource.
|
2
|
+
|
3
|
+
== Usage
|
4
|
+
|
5
|
+
class Book < ActiveResource::Base
|
6
|
+
self.site = 'http://library.alexandria.org/'
|
7
|
+
named_scope :limit, lambda { |n| { :params => { :limit => n } } }
|
8
|
+
named_scope :by_author, lambda { |author| { :params => { :author => author } } }
|
9
|
+
named_scope :out_of_print, lambda { { :from => :out_of_print } }
|
10
|
+
end
|
11
|
+
|
12
|
+
# same as Book.find(:all, :params => { :author => 'Zinn' }):
|
13
|
+
Book.by_author('Zinn')
|
14
|
+
# => GET /books.xml?author=Zinn
|
15
|
+
|
16
|
+
# same as Book.find(:all, :params => { :author => 'Scarry', :limit => 2 }):
|
17
|
+
Book.by_author('Scarry').limit(2)
|
18
|
+
# => GET /books.xml?author=Scarry&limit=2
|
19
|
+
|
20
|
+
# same as Book.find(:all, :from => :out_of_print, :params => { :limit => 10 }):
|
21
|
+
Book.out_of_print.limit(10)
|
22
|
+
# => GET /books/out_of_print.xml?limit=10
|
23
|
+
|
24
|
+
== Installation
|
25
|
+
|
26
|
+
=== As a gem, from the command line:
|
27
|
+
|
28
|
+
gem sources -a http://gems.github.com (you only have to do this once)
|
29
|
+
sudo gem install gcnovus-arns
|
30
|
+
|
31
|
+
=== In your Rails app's <tt>config/environment.rb</tt>:
|
32
|
+
|
33
|
+
gem 'gcnovus-arns', :source => 'http://gems.github.com', :lib => 'arns'
|
34
|
+
|
35
|
+
Then, from the command line in RAILS_APP_ROOT:
|
36
|
+
|
37
|
+
sudo rake gems:install
|
38
|
+
|
39
|
+
== Duplication w.r.t. <tt>ActiveRecord::NamedScope</tt>
|
40
|
+
|
41
|
+
Much of the code in this project could be replaced with that in <tt>ActiveRecord::Base</tt> and <tt>ActiveRecord::NamedScope</tt>.
|
42
|
+
Unfortunately, that code is very tightly tied to +ActiveRecord+. If the Rails team does want to move this into
|
43
|
+
core, I would encourage them to try to merge those two implementation. The nastiest bit is mostly that <tt>ActiveRecord</tt>'s
|
44
|
+
version hardcodes the list of things that get merged instead of replaced when scopes are combined:
|
45
|
+
<tt>[:conditions, :include, :joins, :find]</tt>. This version needs to only do <tt>merge</tt>s (or <tt>reverse_merge</tt>s)
|
46
|
+
on <tt>:params</tt>. The only other significant bit is that the support for scoping (the <tt>#with_scope</tt>, <tt>#default_scope</tt>,
|
47
|
+
<tt>#scoped?</tt>, <tt>#scope</tt>, <tt>#scoped_methods</tt>, and <tt>#current_scoped_methods</tt> methods) for +ActiveRecord+ is in
|
48
|
+
<tt>ActiveRecord::Base</tt>, not in the included <tt>ActiveRecord::NamedScope</tt> module and thus can't be re-used here.
|
data/Rakefile
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
require 'rcov/rcovtask'
|
5
|
+
require 'rake/gempackagetask'
|
6
|
+
|
7
|
+
$LOAD_PATH.unshift("lib")
|
8
|
+
require 'active_resource/named_scope'
|
9
|
+
|
10
|
+
desc 'Run unit tests'
|
11
|
+
task :default => :test
|
12
|
+
|
13
|
+
test_files_pattern = 'test/**/*_test.rb'
|
14
|
+
|
15
|
+
Rake::TestTask.new do |t|
|
16
|
+
t.libs << 'lib'
|
17
|
+
t.test_files = Dir.glob(test_files_pattern).sort
|
18
|
+
t.verbose = true
|
19
|
+
end
|
20
|
+
|
21
|
+
Rake::RDocTask.new { |rdoc|
|
22
|
+
rdoc.rdoc_dir = 'doc'
|
23
|
+
rdoc.title = "ARNS -- named_scope for ActiveResource"
|
24
|
+
rdoc.main = "README.rdoc"
|
25
|
+
rdoc.options << '--line-numbers'
|
26
|
+
rdoc.template = "#{ENV['template']}.rb" if ENV['template']
|
27
|
+
rdoc.rdoc_files.include('README.rdoc', 'lib/**/*.rb')
|
28
|
+
}
|
29
|
+
|
30
|
+
desc 'Calculate test coverage of plugin.'
|
31
|
+
Rcov::RcovTask.new(:coverage) do |rcov|
|
32
|
+
rcov.pattern = test_files_pattern
|
33
|
+
rcov.output_dir = 'coverage'
|
34
|
+
rcov.verbose = true
|
35
|
+
rcov.rcov_opts = ['--sort coverage', '-x "(^/)|(/Gems/)"', '-Ilib']
|
36
|
+
end
|
37
|
+
|
38
|
+
spec = Gem::Specification.new do |s|
|
39
|
+
s.name = "arns"
|
40
|
+
s.version = ActiveResource::NamedScope::VERSION
|
41
|
+
s.summary = "named_scope for ActiveResource"
|
42
|
+
s.homepage = "http://github.com/gcnovus/arns"
|
43
|
+
|
44
|
+
s.files = FileList['[A-Z]*', '{lib,test}/**/*']
|
45
|
+
|
46
|
+
s.has_rdoc = true
|
47
|
+
s.extra_rdoc_files = ["README.rdoc"]
|
48
|
+
s.rdoc_options = ["--line-numbers", "--main", "README.rdoc"]
|
49
|
+
|
50
|
+
s.authors = ["Gaius Novus"]
|
51
|
+
s.email = "gaius.c.novus@gmail.com"
|
52
|
+
end
|
53
|
+
|
54
|
+
Rake::GemPackageTask.new spec do |pkg|
|
55
|
+
pkg.need_tar = true
|
56
|
+
pkg.need_zip = true
|
57
|
+
end
|
58
|
+
|
59
|
+
desc "Generate a gemspec file for GitHub"
|
60
|
+
task :gemspec do
|
61
|
+
File.open("#{spec.name}.gemspec", 'w') do |f|
|
62
|
+
f.write spec.to_ruby
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
module ActiveResource
|
2
|
+
|
3
|
+
# To be included by ActiveResource::Base. Provides +named_scope+ functionality
|
4
|
+
# similar to (but not quite as rich as) ActiveRecord's.
|
5
|
+
#
|
6
|
+
# The options supported in a scope are <tt>:from</tt> and <tt>:params</tt>.
|
7
|
+
# The <tt>:from</tt> value should be a +Symbol+ or +String+ and represents
|
8
|
+
# a subdirectory in the resource. Later <tt>:from</tt> values will replace
|
9
|
+
# earlier ones. The <tt>:params</tt> value should be a +Hash+ and represents
|
10
|
+
# additional query parameters. Later <tt>:params</tt> values will be merged
|
11
|
+
# with earlier ones.
|
12
|
+
#
|
13
|
+
# == Example Usage:
|
14
|
+
#
|
15
|
+
# class Book < ActiveResource::Base
|
16
|
+
# self.site = 'http://library.alexandria.org/'
|
17
|
+
# named_scope :limit, lambda { |n| { :params => { :limit => n } } }
|
18
|
+
# named_scope :by_author, lambda { |author| { :params => { :author => author } } }
|
19
|
+
# named_scope :out_of_print, lambda { { :from => :out_of_print } }
|
20
|
+
# named_scope :rare, lambda { { :from => :rare } }
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# # same as Book.find(:all, :params => { :author => 'Zinn' }):
|
24
|
+
# Book.by_author('Zinn')
|
25
|
+
# # => GET /books.xml?author=Zinn
|
26
|
+
#
|
27
|
+
# # same as Book.find(:all, :params => { :author => 'Scarry', :limit => 2 }):
|
28
|
+
# Book.by_author('Scarry').limit(2)
|
29
|
+
# # => GET /books.xml?author=Scarry&limit=2
|
30
|
+
#
|
31
|
+
# # same as Book.find(:all, :from => :out_of_print, :params => { :limit => 10 }):
|
32
|
+
# Book.out_of_print.limit(10)
|
33
|
+
# # => GET /books/out_of_print.xml?limit=10
|
34
|
+
#
|
35
|
+
# # later :from values overwrite earlier ones:
|
36
|
+
# Book.out_of_print.rare
|
37
|
+
# # => GET /books/rare.xml
|
38
|
+
#
|
39
|
+
# # later :params values are merged with earlier ones:
|
40
|
+
# Book.author('Douglas Adams').limit(10).limit(2)
|
41
|
+
# # => GET /books.xml?author=Douglas+Adams&limit=2
|
42
|
+
module NamedScope
|
43
|
+
|
44
|
+
VERSION = "0.0.1"
|
45
|
+
|
46
|
+
def self.included(base)
|
47
|
+
base.extend ClassMethods
|
48
|
+
base.send :include, InstanceMethods
|
49
|
+
end
|
50
|
+
|
51
|
+
module InstanceMethods
|
52
|
+
end
|
53
|
+
|
54
|
+
module ClassMethods
|
55
|
+
|
56
|
+
# Define a new named scope.
|
57
|
+
#
|
58
|
+
# == Parameters
|
59
|
+
#
|
60
|
+
# +name+: the name of the Scope. A Symbol. Required.
|
61
|
+
#
|
62
|
+
# +options+: a Hash or Proc (lambda) to evaluate for the scope. Required.
|
63
|
+
def named_scope(name, options)
|
64
|
+
name = name.to_sym
|
65
|
+
named_scopes[name] = ActiveResource::NamedScope::ScopePrototype.new(name, options)
|
66
|
+
(class << self; self end).instance_eval do
|
67
|
+
define_method name do |*args|
|
68
|
+
named_scopes[name].call(self, *args)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def named_scopes
|
74
|
+
read_inheritable_attribute(:scopes) || write_inheritable_attribute(:scopes, {})
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
class ScopePrototype
|
80
|
+
|
81
|
+
attr_reader :name, :content
|
82
|
+
|
83
|
+
def initialize(name, content)
|
84
|
+
@name, @content = name, content
|
85
|
+
end
|
86
|
+
|
87
|
+
def call(base_scope, *args)
|
88
|
+
options = case @content
|
89
|
+
when Hash
|
90
|
+
@content.dup
|
91
|
+
when Proc
|
92
|
+
@content.call(*args)
|
93
|
+
end
|
94
|
+
Scope.new(base_scope, options)
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
class Scope
|
100
|
+
|
101
|
+
def initialize(base_scope, options)
|
102
|
+
@base_scope = base_scope
|
103
|
+
@options = deep_merge(base_options, options)
|
104
|
+
end
|
105
|
+
|
106
|
+
def proxy_options
|
107
|
+
@options
|
108
|
+
end
|
109
|
+
|
110
|
+
# Object defines a default +to_a+ in Ruby 1.8, but it is deprecated.
|
111
|
+
def to_a
|
112
|
+
found
|
113
|
+
end
|
114
|
+
|
115
|
+
def method_missing(method, *args, &block)
|
116
|
+
if (scope_prototype = resource_class.named_scopes[method])
|
117
|
+
return scope_prototype.call(self, *args)
|
118
|
+
elsif found.respond_to?(method)
|
119
|
+
return found.send(method, *args, &block)
|
120
|
+
end
|
121
|
+
super(method, *args)
|
122
|
+
end
|
123
|
+
|
124
|
+
def respond_to?(method)
|
125
|
+
super(method) || resource_class.named_scopes[method] || found.respond_to?(method)
|
126
|
+
end
|
127
|
+
|
128
|
+
protected
|
129
|
+
|
130
|
+
def base_scope
|
131
|
+
@base_scope
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
|
136
|
+
# Merges Hash +b+ into Hash +a+, marging instead of replacing any included Hash stored under :params.
|
137
|
+
#
|
138
|
+
# Returns a new Hash.
|
139
|
+
def deep_merge(a, b)
|
140
|
+
params = {}.merge(a[:params] || {}).merge(b[:params] || {})
|
141
|
+
result = a.merge(b)
|
142
|
+
result.merge!({ :params => params }) unless params.empty?
|
143
|
+
result
|
144
|
+
end
|
145
|
+
|
146
|
+
def base_options
|
147
|
+
@base_scope.respond_to?(:proxy_options) ? @base_scope.proxy_options : {}
|
148
|
+
end
|
149
|
+
|
150
|
+
def found
|
151
|
+
@found ||= resource_class.find(:all, proxy_options).tap(&:freeze)
|
152
|
+
end
|
153
|
+
|
154
|
+
def resource_class
|
155
|
+
@resource_class ||= begin
|
156
|
+
result = @base_scope
|
157
|
+
result = result.base_scope while result.kind_of?(Scope)
|
158
|
+
result
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
end
|
163
|
+
|
164
|
+
|
165
|
+
end
|
166
|
+
|
167
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'test_helper')
|
2
|
+
require 'active_resource/named_scope'
|
3
|
+
|
4
|
+
class NamedScopeTest < Test::Unit::TestCase
|
5
|
+
|
6
|
+
context 'an ActiveResource class' do
|
7
|
+
|
8
|
+
setup do
|
9
|
+
@class = Class.new do
|
10
|
+
def self.name; 'Book'; end
|
11
|
+
include ActiveResource::NamedScope
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
context 'defining a new named scope' do
|
16
|
+
|
17
|
+
context 'with a name and Hash' do
|
18
|
+
|
19
|
+
setup do
|
20
|
+
@class.named_scope :rare, { :from => :rare }
|
21
|
+
end
|
22
|
+
|
23
|
+
should "add the named scope to the class's list of named scopes" do
|
24
|
+
assert_not_nil @class.named_scopes[:rare]
|
25
|
+
assert @class.named_scopes[:rare].kind_of?(ActiveResource::NamedScope::ScopePrototype)
|
26
|
+
end
|
27
|
+
|
28
|
+
should 'use the given Hash for the proxy options' do
|
29
|
+
assert_equal({ :from => :rare}, @class.rare.proxy_options)
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
context 'with a name and Proc' do
|
35
|
+
|
36
|
+
setup do
|
37
|
+
@class.named_scope :with_author, lambda { |author| { :params => { :author => author } } }
|
38
|
+
end
|
39
|
+
|
40
|
+
should "add the named scope to the class's list of named scopes" do
|
41
|
+
assert_not_nil @class.named_scopes[:with_author]
|
42
|
+
end
|
43
|
+
|
44
|
+
should 'evaluate the Proc for the proxy options' do
|
45
|
+
assert_equal({ :params => { :author => 'Pirsig' } }, @class.with_author('Pirsig').proxy_options)
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
context 'with some named scopes' do
|
53
|
+
|
54
|
+
setup do
|
55
|
+
@class.named_scope :rare, { :from => :rare }
|
56
|
+
@class.named_scope :out_of_print, { :from => :out_of_print }
|
57
|
+
@class.named_scope :with_author, lambda { |author| { :params => { :author => author } } }
|
58
|
+
@class.named_scope :limit, lambda { |limit| { :params => { :limit => limit } } }
|
59
|
+
end
|
60
|
+
|
61
|
+
should 'return the results of a find(:all) when a chain of named scopes is evaluated' do
|
62
|
+
result = []
|
63
|
+
@class.expects(:find).returns(result)
|
64
|
+
assert_equal result, @class.limit(20).to_a
|
65
|
+
end
|
66
|
+
|
67
|
+
should 'not make a find call in the middle of chained named scopes' do
|
68
|
+
@class.expects(:find).once
|
69
|
+
@class.rare.out_of_print.with_author('Silverstein').limit(10).to_a
|
70
|
+
end
|
71
|
+
|
72
|
+
should 'overwrite earlier :from values with later ones' do
|
73
|
+
@class.expects(:find).with(:all, { :from => :out_of_print })
|
74
|
+
@class.rare.out_of_print.to_a
|
75
|
+
end
|
76
|
+
|
77
|
+
should 'merge :params values' do
|
78
|
+
@class.expects(:find).with(:all, { :params => { :author => 'Plato', :limit => 5 } })
|
79
|
+
@class.with_author('Plato').limit(5).to_a
|
80
|
+
end
|
81
|
+
|
82
|
+
should 'overwrite earlier values in the :params hash with later ones' do
|
83
|
+
@class.expects(:find).with(:all, { :params => { :limit => 2 } })
|
84
|
+
@class.limit(4).limit(2).to_a
|
85
|
+
end
|
86
|
+
|
87
|
+
should 'accept long chains of named scopes with all sorts of overwriting' do
|
88
|
+
@class.expects(:find).with(:all, { :from => :rare, :params => { :author => 'Wilkie Collins', :limit => 22 } })
|
89
|
+
@class.limit(9).rare.out_of_print.with_author('Wilkie Collins').limit(22).rare.to_a
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
|
94
|
+
context 'the result of a named scope evaluation' do
|
95
|
+
|
96
|
+
setup do
|
97
|
+
@class.named_scope(:foo, {})
|
98
|
+
@x = Object.new
|
99
|
+
@y = Object.new
|
100
|
+
@class.stubs(:find).returns([@x, @y])
|
101
|
+
end
|
102
|
+
|
103
|
+
should 'support :to_a' do
|
104
|
+
assert_equal [@x, @y], @class.foo.to_a
|
105
|
+
end
|
106
|
+
|
107
|
+
should 'support :each with a block' do
|
108
|
+
@x.expects(:to_s)
|
109
|
+
@y.expects(:to_s)
|
110
|
+
@class.foo.each(&:to_s)
|
111
|
+
end
|
112
|
+
|
113
|
+
should 'support :first' do
|
114
|
+
assert_equal @x, @class.foo.first
|
115
|
+
end
|
116
|
+
|
117
|
+
should 'support :last' do
|
118
|
+
assert_equal @x, @class.foo.first
|
119
|
+
end
|
120
|
+
|
121
|
+
should 'support :any? without a block' do
|
122
|
+
assert @class.foo.any?
|
123
|
+
end
|
124
|
+
|
125
|
+
should 'support :any? with a block' do
|
126
|
+
assert @class.foo.any? { |z| z == @y }
|
127
|
+
assert !@class.foo.any? { |z| z == 7 }
|
128
|
+
end
|
129
|
+
|
130
|
+
should 'support :all? with a block' do
|
131
|
+
assert @class.foo.all? { |z| !z.nil? }
|
132
|
+
assert !@class.foo.all? { |z| z == @y }
|
133
|
+
end
|
134
|
+
|
135
|
+
should 'support :length' do
|
136
|
+
assert_equal 2, @class.foo.length
|
137
|
+
end
|
138
|
+
|
139
|
+
should 'raise a NoMethodError for other methods' do
|
140
|
+
assert_raises(NoMethodError) { @class.foo.bar }
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: gcnovus-arns
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Gaius Novus
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-05-17 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description:
|
17
|
+
email: gaius.c.novus@gmail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README.rdoc
|
24
|
+
files:
|
25
|
+
- Rakefile
|
26
|
+
- README.rdoc
|
27
|
+
- lib/active_resource
|
28
|
+
- lib/active_resource/named_scope.rb
|
29
|
+
- test/named_scope_test.rb
|
30
|
+
- test/test_helper.rb
|
31
|
+
has_rdoc: false
|
32
|
+
homepage: http://github.com/gcnovus/arns
|
33
|
+
post_install_message:
|
34
|
+
rdoc_options:
|
35
|
+
- --line-numbers
|
36
|
+
- --main
|
37
|
+
- README.rdoc
|
38
|
+
require_paths:
|
39
|
+
- lib
|
40
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: "0"
|
45
|
+
version:
|
46
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
47
|
+
requirements:
|
48
|
+
- - ">="
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: "0"
|
51
|
+
version:
|
52
|
+
requirements: []
|
53
|
+
|
54
|
+
rubyforge_project:
|
55
|
+
rubygems_version: 1.2.0
|
56
|
+
signing_key:
|
57
|
+
specification_version: 3
|
58
|
+
summary: named_scope for ActiveResource
|
59
|
+
test_files: []
|
60
|
+
|