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