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 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
@@ -0,0 +1,7 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+ require 'mocha'
5
+ require 'active_support'
6
+
7
+ $: << File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
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
+