friendly 0.3.5 → 0.4.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.
@@ -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