gcnovus-arns 0.0.1

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.
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
+