friendly 0.3.5 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +1 -1
- data/.gitignore +3 -0
- data/CHANGELOG.md +11 -0
- data/CONTRIBUTORS.md +6 -0
- data/README.md +93 -1
- data/Rakefile +1 -0
- data/VERSION +1 -1
- data/friendly.gemspec +32 -2
- data/lib/friendly.rb +4 -0
- data/lib/friendly/associations.rb +7 -0
- data/lib/friendly/associations/association.rb +34 -0
- data/lib/friendly/associations/set.rb +37 -0
- data/lib/friendly/attribute.rb +37 -11
- data/lib/friendly/boolean.rb +6 -2
- data/lib/friendly/document.rb +93 -1
- data/lib/friendly/named_scope.rb +17 -0
- data/lib/friendly/scope.rb +100 -0
- data/lib/friendly/scope_proxy.rb +45 -0
- data/lib/friendly/sequel_monkey_patches.rb +0 -1
- data/lib/friendly/table_creator.rb +11 -6
- data/lib/friendly/uuid.rb +5 -0
- data/spec/config.yml.example +7 -0
- data/spec/integration/ad_hoc_scopes_spec.rb +42 -0
- data/spec/integration/has_many_spec.rb +18 -0
- data/spec/integration/named_scope_spec.rb +34 -0
- data/spec/integration/scope_chaining_spec.rb +22 -0
- data/spec/integration/table_creator_spec.rb +17 -5
- data/spec/spec_helper.rb +19 -6
- data/spec/unit/associations/association_spec.rb +57 -0
- data/spec/unit/associations/set_spec.rb +43 -0
- data/spec/unit/attribute_spec.rb +41 -0
- data/spec/unit/document_spec.rb +47 -0
- data/spec/unit/named_scope_spec.rb +16 -0
- data/spec/unit/scope_proxy_spec.rb +44 -0
- data/spec/unit/scope_spec.rb +113 -0
- metadata +39 -2
data/lib/friendly/boolean.rb
CHANGED
@@ -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 }
|
data/lib/friendly/document.rb
CHANGED
@@ -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,
|
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
|
@@ -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
|
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
|
34
|
-
table.fields.flatten.each do |f|
|
35
|
-
|
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,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
|