friendly 0.3.5 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,10 @@
1
+ require 'friendly/attribute'
2
+
1
3
  module Friendly
4
+ # placeholder that represents a boolean
5
+ # since ruby has no boolean superclass
2
6
  module Boolean
3
- # placeholder that represents a boolean
4
- # since ruby has no boolean superclass
5
7
  end
6
8
  end
9
+
10
+ Friendly::Attribute.register_type(Friendly::Boolean, 'boolean') { |s| s }
@@ -1,4 +1,5 @@
1
1
  require 'active_support/inflector'
2
+ require 'friendly/associations'
2
3
 
3
4
  module Friendly
4
5
  module Document
@@ -25,7 +26,9 @@ module Friendly
25
26
  end
26
27
 
27
28
  module ClassMethods
28
- attr_writer :storage_proxy, :query_klass, :table_name, :collection_klass
29
+ attr_writer :storage_proxy, :query_klass,
30
+ :table_name, :collection_klass,
31
+ :scope_proxy, :association_set
29
32
 
30
33
  def create_tables!
31
34
  storage_proxy.create_tables!
@@ -95,6 +98,95 @@ module Friendly
95
98
  @table_name ||= name.pluralize.underscore
96
99
  end
97
100
 
101
+ def scope_proxy
102
+ @scope_proxy ||= ScopeProxy.new(self)
103
+ end
104
+
105
+ # Add a named scope to this Document.
106
+ #
107
+ # e.g.
108
+ #
109
+ # class Post
110
+ # indexes :created_at
111
+ # named_scope :recent, :order! => :created_at.desc
112
+ # end
113
+ #
114
+ # Then, you can access the recent posts with:
115
+ #
116
+ # Post.recent.all
117
+ # ...or...
118
+ # Post.recent.first
119
+ #
120
+ # Both #all and #first also take additional parameters:
121
+ #
122
+ # Post.recent.all(:author_id => @author.id)
123
+ #
124
+ # Scopes are also chainable. See the README or Friendly::Scope docs for details.
125
+ #
126
+ # @param [Symbol] name the name of the scope.
127
+ # @param [Hash] parameters the query that this named scope will perform.
128
+ #
129
+ def named_scope(name, parameters)
130
+ scope_proxy.add_named(name, parameters)
131
+ end
132
+
133
+ # Returns boolean based on whether the Document has a scope by a particular name.
134
+ #
135
+ # @param [Symbol] name The name of the scope in question.
136
+ #
137
+ def has_named_scope?(name)
138
+ scope_proxy.has_named_scope?(name)
139
+ end
140
+
141
+ # Create an ad hoc scope on this Document.
142
+ #
143
+ # e.g.
144
+ #
145
+ # scope = Post.scope(:order! => :created_at)
146
+ # scope.all # => [#<Post>, #<Post>]
147
+ #
148
+ # @param [Hash] parameters the query parameters to create the scope with.
149
+ #
150
+ def scope(parameters)
151
+ scope_proxy.ad_hoc(parameters)
152
+ end
153
+
154
+ def association_set
155
+ @association_set ||= Associations::Set.new(self)
156
+ end
157
+
158
+ # Add a has_many association.
159
+ #
160
+ # e.g.
161
+ #
162
+ # class Post
163
+ # attribute :user_id, Friendly::UUID
164
+ # indexes :user_id
165
+ # end
166
+ #
167
+ # class User
168
+ # has_many :posts
169
+ # end
170
+ #
171
+ # @user = User.create
172
+ # @post = @user.posts.create
173
+ # @user.posts.all == [@post] # => true
174
+ #
175
+ # _Note: Make sure that the target model is indexed on the foreign key. If it isn't, querying the association will raise Friendly::MissingIndex._
176
+ #
177
+ # Friendly defaults the foreign key to class_name_id just like ActiveRecord.
178
+ # It also converts the name of the association to the name of the target class just like ActiveRecord does.
179
+ #
180
+ # The biggest difference in semantics between Friendly's has_many and active_record's is that Friendly's just returns a Friendly::Scope object. If you want all the associated objects, you have to call #all to get them. You can also use any other Friendly::Scope method.
181
+ #
182
+ # @param [Symbol] name The name of the association and plural name of the target class.
183
+ # @option options [String] :class_name The name of the target class of this association if it is different than the name would imply.
184
+ # @option options [Symbol] :foreign_key Override the foreign key.
185
+ #
186
+ def has_many(name, options = {})
187
+ association_set.add(name, options)
188
+ end
189
+
98
190
  protected
99
191
  def query(conditions)
100
192
  conditions.is_a?(Query) ? conditions : query_klass.new(conditions)
@@ -0,0 +1,17 @@
1
+ require 'friendly/scope'
2
+
3
+ module Friendly
4
+ class NamedScope
5
+ attr_reader :klass, :parameters, :scope_klass
6
+
7
+ def initialize(klass, parameters, scope_klass = Scope)
8
+ @klass = klass
9
+ @parameters = parameters
10
+ @scope_klass = scope_klass
11
+ end
12
+
13
+ def scope
14
+ @scope_klass.new(@klass, @parameters)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,100 @@
1
+ module Friendly
2
+ class Scope
3
+ attr_reader :klass, :parameters
4
+
5
+ def initialize(klass, parameters)
6
+ @klass = klass
7
+ @parameters = parameters
8
+ end
9
+
10
+ # Fetch all documents at this scope.
11
+ #
12
+ # @param [Hash] extra_parameters add extra parameters to this query.
13
+ #
14
+ def all(extra_parameters = {})
15
+ klass.all(params(extra_parameters))
16
+ end
17
+
18
+ # Fetch the first document at this scope.
19
+ #
20
+ # @param [Hash] extra_parameters add extra parameters to this query.
21
+ #
22
+ def first(extra_parameters = {})
23
+ klass.first(params(extra_parameters))
24
+ end
25
+
26
+ # Paginate the documents at this scope.
27
+ #
28
+ # @param [Hash] extra_parameters add extra parameters to this query.
29
+ # @return WillPaginate::Collection
30
+ #
31
+ def paginate(extra_parameters = {})
32
+ klass.paginate(params(extra_parameters))
33
+ end
34
+
35
+ # Build an object at this scope.
36
+ #
37
+ # e.g.
38
+ # Post.scope(:name => "James").build.name # => "James"
39
+ #
40
+ # @param [Hash] extra_parameters add extra parameters to this query.
41
+ #
42
+ def build(extra_parameters = {})
43
+ klass.new(params_without_modifiers(extra_parameters))
44
+ end
45
+
46
+ # Create an object at this scope.
47
+ #
48
+ # e.g.
49
+ # @post = Post.scope(:name => "James").create
50
+ # @post.new_record? # => false
51
+ # @post.name # => "James"
52
+ #
53
+ # @param [Hash] extra_parameters add extra parameters to this query.
54
+ #
55
+ def create(extra_parameters = {})
56
+ klass.create(params_without_modifiers(extra_parameters))
57
+ end
58
+
59
+ # Override #respond_to? so that we can return true when it's another named_scope.
60
+ #
61
+ # @override
62
+ #
63
+ def respond_to?(method_name, include_private = false)
64
+ klass.has_named_scope?(method_name) || super
65
+ end
66
+
67
+ # Use method_missing to respond to other named scopes on klass.
68
+ #
69
+ # @override
70
+ #
71
+ def method_missing(method_name, *args, &block)
72
+ respond_to?(method_name) ? chain_with(method_name) : super
73
+ end
74
+
75
+ # Chain with another one of klass's named_scopes.
76
+ #
77
+ # @param [Symbol] scope_name The name of the scope to chain with.
78
+ #
79
+ def chain_with(scope_name)
80
+ self + klass.send(scope_name)
81
+ end
82
+
83
+ # Create a new Scope that is the combination of self and other, where other takes priority
84
+ #
85
+ # @param [Friendly::Scope] other The scope to merge with.
86
+ #
87
+ def +(other_scope)
88
+ self.class.new(klass, parameters.merge(other_scope.parameters))
89
+ end
90
+
91
+ protected
92
+ def params(extra)
93
+ parameters.merge(extra)
94
+ end
95
+
96
+ def params_without_modifiers(extra)
97
+ params(extra).reject { |k,v| k.to_s =~ /!$/ }
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,45 @@
1
+ require 'friendly/named_scope'
2
+
3
+ module Friendly
4
+ class ScopeProxy
5
+ attr_reader :klass, :scope_klass, :scopes
6
+
7
+ def initialize(klass, scope_klass = Scope)
8
+ @klass = klass
9
+ @scope_klass = scope_klass
10
+ @scopes = {}
11
+ end
12
+
13
+ def add_named(name, parameters)
14
+ scopes[name] = parameters
15
+ add_scope_method_to_klass(name)
16
+ end
17
+
18
+ def get(name)
19
+ scopes[name]
20
+ end
21
+
22
+ def get_instance(name)
23
+ scope_klass.new(klass, get(name))
24
+ end
25
+
26
+ def ad_hoc(parameters)
27
+ scope_klass.new(klass, parameters)
28
+ end
29
+
30
+ def has_named_scope?(name)
31
+ scopes.has_key?(name)
32
+ end
33
+
34
+ protected
35
+ def add_scope_method_to_klass(scope_name)
36
+ klass.class_eval do
37
+ eval <<-__END__
38
+ def self.#{scope_name}
39
+ scope_proxy.get_instance(:#{scope_name})
40
+ end
41
+ __END__
42
+ end
43
+ end
44
+ end
45
+ end
@@ -32,4 +32,3 @@ module Sequel
32
32
  end
33
33
  end
34
34
  end
35
-
@@ -1,9 +1,10 @@
1
1
  module Friendly
2
2
  class TableCreator
3
- attr_reader :db
3
+ attr_reader :db, :attr_klass
4
4
 
5
- def initialize(db = Friendly.db)
6
- @db = db
5
+ def initialize(db = Friendly.db, attr_klass = Friendly::Attribute)
6
+ @db = db
7
+ @attr_klass = attr_klass
7
8
  end
8
9
 
9
10
  def create(table)
@@ -29,10 +30,14 @@ module Friendly
29
30
  end
30
31
 
31
32
  def create_index_table(table)
33
+ attr = attr_klass # close around this please
34
+
32
35
  db.create_table(table.table_name) do
33
- binary :id, :size => 16
34
- table.fields.flatten.each do |f|
35
- method(table.klass.attributes[f].type.name.to_sym).call(f)
36
+ binary :id, :size => 16
37
+ table.fields.flatten.each do |f|
38
+ klass = table.klass.attributes[f].type
39
+ type = attr.custom_type?(klass) ? attr.sql_type(klass) : klass
40
+ column(f, type)
36
41
  end
37
42
  primary_key table.fields.flatten + [:id]
38
43
  unique :id
data/lib/friendly/uuid.rb CHANGED
@@ -1,4 +1,6 @@
1
1
  require 'friendly/time'
2
+ require 'friendly/attribute'
3
+
2
4
  # This class was extracted from the cassandra gem by Evan Weaver
3
5
  # As such, it is distributed under the terms of the apache license.
4
6
  # See the APACHE-LICENSE file in the root of this project for more information.
@@ -141,3 +143,6 @@ module Friendly
141
143
  end
142
144
  end
143
145
 
146
+ Friendly::Attribute.register_type(Friendly::UUID, 'binary(16)') do |s|
147
+ Friendly::UUID.new(s)
148
+ end
@@ -0,0 +1,7 @@
1
+ test:
2
+ :adapter: "mysql"
3
+ :host: "localhost"
4
+ :user: "root"
5
+ :password: "swordfish"
6
+ :database: "friendly_test"
7
+
@@ -0,0 +1,42 @@
1
+ require File.expand_path("../../spec_helper", __FILE__)
2
+
3
+ describe "Querying with an ad-hoc scope" do
4
+ before do
5
+ User.all(:name => "Fred").each { |u| u.destroy }
6
+ @users = (1..10).map { User.create(:name => "Fred") }
7
+ end
8
+
9
+ it "can return all the objects matching the scope" do
10
+ User.scope(:name => "Fred").all.should == @users
11
+ end
12
+
13
+ it "can return the first object matching the scope" do
14
+ User.scope(:name => "Fred").first.should == User.first(:name => "Fred")
15
+ end
16
+
17
+ it "can paginate over the matching objects" do
18
+ found = User.scope(:name => "Fred").paginate(:per_page! => 5)
19
+ found.should == User.paginate(:name => "Fred", :per_page! => 5)
20
+ end
21
+
22
+ it "can build an object at scope" do
23
+ User.scope(:name => "Fred", :limit! => 5).build.name.should == "Fred"
24
+ end
25
+
26
+ it "supports overriding parameters when building" do
27
+ scope = User.scope(:name => "Fred", :limit! => 5)
28
+ scope.build(:name => "Joe").name.should == "Joe"
29
+ end
30
+
31
+ it "can create an object at scope" do
32
+ user = User.scope(:name => "Joe").create
33
+ user.should_not be_new_record
34
+ user.name.should == "Joe"
35
+ end
36
+
37
+ it "supports overriding parameters when creating" do
38
+ user = User.scope(:name => "Joe").create(:name => "Fred")
39
+ user.should_not be_new_record
40
+ user.name.should == "Fred"
41
+ end
42
+ end
@@ -0,0 +1,18 @@
1
+ require File.expand_path("../../spec_helper", __FILE__)
2
+
3
+ describe "Has many associations" do
4
+ before do
5
+ @user = User.create :name => "Fred"
6
+ @addresses = (0..2).map { Address.create :user_id => @user.id }
7
+ end
8
+
9
+ it "returns the objects whose foreign keys match the object's id" do
10
+ found = @user.addresses.all.sort { |a, b| a.id <=> b.id }
11
+ found.should == @addresses.sort { |a, b| a.id <=> b.id }
12
+ end
13
+
14
+ it "accepts class_name and foreign_key overrides" do
15
+ found = @user.addresses_override.all.sort { |a, b| a.id <=> b.id }
16
+ found.should == @addresses.sort { |a, b| a.id <=> b.id }
17
+ end
18
+ end
@@ -0,0 +1,34 @@
1
+ require File.expand_path("../../spec_helper", __FILE__)
2
+
3
+ describe "named_scope" do
4
+ describe "calling a single named_scope" do
5
+ before do
6
+ User.all(:name => "Quagmire").each { |q| q.destroy }
7
+ 5.times { User.create(:name => "Quagmire") }
8
+ end
9
+
10
+ describe "all" do
11
+ it "returns all objects matching the conditions" do
12
+ User.named_quagmire.all.should == User.all(:name => "Quagmire")
13
+ end
14
+
15
+ it "accepts extra conditions" do
16
+ User.create(:name => "Fred")
17
+ found = User.named_quagmire.all(:name => "Fred")
18
+ found.should == User.all(:name => "Fred")
19
+ end
20
+ end
21
+
22
+ describe "first" do
23
+ it "returns the first object matching the conditions" do
24
+ User.named_quagmire.first.should == User.first(:name => "Quagmire")
25
+ end
26
+
27
+ it "accepts extra conditions" do
28
+ User.create(:name => "Fred")
29
+ found = User.named_quagmire.first(:name => "Fred")
30
+ found.should == User.first(:name => "Fred")
31
+ end
32
+ end
33
+ end
34
+ end