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 CHANGED
@@ -1,2 +1,2 @@
1
- README.rdoc
1
+ README.md
2
2
  lib/**/*.rb
data/.gitignore CHANGED
@@ -21,3 +21,6 @@ pkg
21
21
  ## PROJECT::SPECIFIC
22
22
  test.rb
23
23
  test_active_record.rb
24
+ spec/config.yml
25
+ .yardoc
26
+ doc
data/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ Changelog
2
+ =========
3
+
4
+ ### 0.4.0 (master)
5
+
6
+ * (jamesgolick) Add scope chaining. See the README and the docs for Friendly::Scope.
7
+ * (jamesgolick) Add has_many association.
8
+ * (jamesgolick) Add ad-hoc scopes. See the docs for Document.scope.
9
+ * (jamesgolick) Add named_scope functionality. See the docs for Document.named_scope.
10
+ * (jeffrafter + jamesgolick) DDL generation now supports custom types. Previously, it would only work with types that Sequel already supported. Now, if you want to register a custom type, use Friendly::Attribute.register_type(klass, sql_type, &conversion_method).
11
+
data/CONTRIBUTORS.md ADDED
@@ -0,0 +1,6 @@
1
+ Contributors to Friendly
2
+ ========================
3
+
4
+ * James Golick - http://jamesgolick.com (github: jamesgolick)
5
+ * Jonathan Palardy - http://technotales.wordpress.com (github: jpalardy)
6
+ * Jeff Rafter - http://socialorange.com (github: jeffrafter)
data/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  Friendly
2
- =======
2
+ ========
3
3
 
4
4
  ### Short Version
5
5
 
@@ -112,6 +112,98 @@ Currently, only caching by id is supported, but caching of arbitrary indexes is
112
112
 
113
113
  __We're seeing a 99.8% cache hit rate in production with this code.__
114
114
 
115
+ Scopes
116
+ ======
117
+
118
+ ### Named Scopes
119
+
120
+ It's possible to create a scope that you can refer to by name:
121
+
122
+ class Post
123
+ attribute :author, String
124
+
125
+ named_scope :by_james, :author => "James"
126
+ end
127
+
128
+ Calling the scope will provide you with a Friendly:Scope object:
129
+
130
+ Post.by_james #=> #<Friendly::Scope>
131
+
132
+ That scope object supports a variety of methods. Calling any of the methods is the equivalent of calling those methods directly on Document with the scope's parameters.
133
+
134
+ e.g.
135
+
136
+ Post.by_james.all == Post.all(:author => "James")
137
+ Post.by_james.first == Post.first(:author => "James")
138
+ Post.by_james.paginate # => #<WillPaginate::Collection>
139
+ Post.by_james.build.name == "James"
140
+ @post = Post.by_james.create
141
+ @post.new_record? # => false
142
+ @post.name # => "James"
143
+
144
+ Each of the methods also accepts override parameters. The APIs are the same as on Document.
145
+
146
+ Post.by_james.all(:author => "Steve") == Post.all(:author => "Steve")
147
+
148
+ You can also compose arbitrary combinations of scopes with simple chaining.
149
+
150
+ class Post
151
+ # ... snip ...
152
+ named_scope :recent, :order! => :created_at.desc, :limit! => 4
153
+ indexes :name, :created_at
154
+ end
155
+
156
+ Post.by_james.recent == Post.all(:name => "James",
157
+ :order! => :created_at.desc,
158
+ :limit! => 4)
159
+
160
+ If two parameters conflict, the right-most scope takes precedence.
161
+
162
+ ### Ad-hoc Scopes
163
+
164
+ You can also create a scope object on the fly:
165
+
166
+ Post.scope(:author => "Steve")
167
+
168
+ The object you get is identical to the one you get from a named_scope. So, see above for the API.
169
+
170
+ Associations
171
+ ============
172
+
173
+ Friendly currently only supports has\_many associations.
174
+
175
+ Creating a has\_many is as simple as setting up the necessary association and foreign key.
176
+
177
+ _Note: Make sure that the target model is indexed on the foreign key. If it isn't, querying the association will raise Friendly::MissingIndex._
178
+
179
+ e.g.
180
+
181
+ class Post
182
+ attribute :user_id, Friendly::UUID
183
+ indexes :user_id
184
+ end
185
+
186
+ class User
187
+ has_many :posts
188
+ end
189
+
190
+ @user = User.create
191
+ @post = @user.posts.create
192
+ @user.posts.all == [@post] # => true
193
+
194
+ Friendly defaults the foreign key to class_name_id just like ActiveRecord. It also converts the name of the association to the name of the target class just like ActiveRecord does.
195
+
196
+ 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.
197
+
198
+ You can also use any other Friendly::Scope method like scope chaining.
199
+
200
+ # note: the Post.recent scope is defined in the above section
201
+ @user.posts.recent == Post.all(:user_id => @user.id,
202
+ :order! => :created_at.desc,
203
+ :limit! => 4)
204
+
205
+ See the section above or the Friendly::Scope docs for more details.
206
+
115
207
  Installation
116
208
  ============
117
209
 
data/Rakefile CHANGED
@@ -13,6 +13,7 @@ begin
13
13
  gem.add_development_dependency "rspec", ">= 1.2.9"
14
14
  gem.add_development_dependency "cucumber", ">= 0"
15
15
  gem.add_development_dependency "jferris-mocha"
16
+ gem.add_development_dependency "memcached"
16
17
  gem.add_dependency "sequel", ">= 3.7.0"
17
18
  gem.add_dependency "json"
18
19
  gem.add_dependency "activesupport"
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.5
1
+ 0.4.0
data/friendly.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{friendly}
8
- s.version = "0.3.5"
8
+ s.version = "0.4.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["James Golick"]
12
- s.date = %q{2009-12-17}
12
+ s.date = %q{2009-12-22}
13
13
  s.description = %q{}
14
14
  s.email = %q{jamesgolick@gmail.com}
15
15
  s.extra_rdoc_files = [
@@ -20,6 +20,8 @@ Gem::Specification.new do |s|
20
20
  ".document",
21
21
  ".gitignore",
22
22
  "APACHE-LICENSE",
23
+ "CHANGELOG.md",
24
+ "CONTRIBUTORS.md",
23
25
  "LICENSE",
24
26
  "README.md",
25
27
  "Rakefile",
@@ -27,6 +29,9 @@ Gem::Specification.new do |s|
27
29
  "examples/friendly.yml",
28
30
  "friendly.gemspec",
29
31
  "lib/friendly.rb",
32
+ "lib/friendly/associations.rb",
33
+ "lib/friendly/associations/association.rb",
34
+ "lib/friendly/associations/set.rb",
30
35
  "lib/friendly/attribute.rb",
31
36
  "lib/friendly/boolean.rb",
32
37
  "lib/friendly/cache.rb",
@@ -37,8 +42,11 @@ Gem::Specification.new do |s|
37
42
  "lib/friendly/document_table.rb",
38
43
  "lib/friendly/index.rb",
39
44
  "lib/friendly/memcached.rb",
45
+ "lib/friendly/named_scope.rb",
40
46
  "lib/friendly/newrelic.rb",
41
47
  "lib/friendly/query.rb",
48
+ "lib/friendly/scope.rb",
49
+ "lib/friendly/scope_proxy.rb",
42
50
  "lib/friendly/sequel_monkey_patches.rb",
43
51
  "lib/friendly/storage.rb",
44
52
  "lib/friendly/storage_factory.rb",
@@ -49,12 +57,14 @@ Gem::Specification.new do |s|
49
57
  "lib/friendly/translator.rb",
50
58
  "lib/friendly/uuid.rb",
51
59
  "rails/init.rb",
60
+ "spec/config.yml.example",
52
61
  "spec/fakes/data_store_fake.rb",
53
62
  "spec/fakes/database_fake.rb",
54
63
  "spec/fakes/dataset_fake.rb",
55
64
  "spec/fakes/document.rb",
56
65
  "spec/fakes/serializer_fake.rb",
57
66
  "spec/fakes/time_fake.rb",
67
+ "spec/integration/ad_hoc_scopes_spec.rb",
58
68
  "spec/integration/basic_object_lifecycle_spec.rb",
59
69
  "spec/integration/batch_insertion_spec.rb",
60
70
  "spec/integration/convenience_api_spec.rb",
@@ -62,12 +72,17 @@ Gem::Specification.new do |s|
62
72
  "spec/integration/default_value_spec.rb",
63
73
  "spec/integration/find_via_cache_spec.rb",
64
74
  "spec/integration/finder_spec.rb",
75
+ "spec/integration/has_many_spec.rb",
65
76
  "spec/integration/index_spec.rb",
77
+ "spec/integration/named_scope_spec.rb",
66
78
  "spec/integration/pagination_spec.rb",
79
+ "spec/integration/scope_chaining_spec.rb",
67
80
  "spec/integration/table_creator_spec.rb",
68
81
  "spec/integration/write_through_cache_spec.rb",
69
82
  "spec/spec.opts",
70
83
  "spec/spec_helper.rb",
84
+ "spec/unit/associations/association_spec.rb",
85
+ "spec/unit/associations/set_spec.rb",
71
86
  "spec/unit/attribute_spec.rb",
72
87
  "spec/unit/cache_by_id_spec.rb",
73
88
  "spec/unit/cache_spec.rb",
@@ -78,7 +93,10 @@ Gem::Specification.new do |s|
78
93
  "spec/unit/friendly_spec.rb",
79
94
  "spec/unit/index_spec.rb",
80
95
  "spec/unit/memcached_spec.rb",
96
+ "spec/unit/named_scope_spec.rb",
81
97
  "spec/unit/query_spec.rb",
98
+ "spec/unit/scope_proxy_spec.rb",
99
+ "spec/unit/scope_spec.rb",
82
100
  "spec/unit/storage_factory_spec.rb",
83
101
  "spec/unit/storage_proxy_spec.rb",
84
102
  "spec/unit/translator_spec.rb",
@@ -139,6 +157,7 @@ Gem::Specification.new do |s|
139
157
  "spec/fakes/document.rb",
140
158
  "spec/fakes/serializer_fake.rb",
141
159
  "spec/fakes/time_fake.rb",
160
+ "spec/integration/ad_hoc_scopes_spec.rb",
142
161
  "spec/integration/basic_object_lifecycle_spec.rb",
143
162
  "spec/integration/batch_insertion_spec.rb",
144
163
  "spec/integration/convenience_api_spec.rb",
@@ -146,11 +165,16 @@ Gem::Specification.new do |s|
146
165
  "spec/integration/default_value_spec.rb",
147
166
  "spec/integration/find_via_cache_spec.rb",
148
167
  "spec/integration/finder_spec.rb",
168
+ "spec/integration/has_many_spec.rb",
149
169
  "spec/integration/index_spec.rb",
170
+ "spec/integration/named_scope_spec.rb",
150
171
  "spec/integration/pagination_spec.rb",
172
+ "spec/integration/scope_chaining_spec.rb",
151
173
  "spec/integration/table_creator_spec.rb",
152
174
  "spec/integration/write_through_cache_spec.rb",
153
175
  "spec/spec_helper.rb",
176
+ "spec/unit/associations/association_spec.rb",
177
+ "spec/unit/associations/set_spec.rb",
154
178
  "spec/unit/attribute_spec.rb",
155
179
  "spec/unit/cache_by_id_spec.rb",
156
180
  "spec/unit/cache_spec.rb",
@@ -161,7 +185,10 @@ Gem::Specification.new do |s|
161
185
  "spec/unit/friendly_spec.rb",
162
186
  "spec/unit/index_spec.rb",
163
187
  "spec/unit/memcached_spec.rb",
188
+ "spec/unit/named_scope_spec.rb",
164
189
  "spec/unit/query_spec.rb",
190
+ "spec/unit/scope_proxy_spec.rb",
191
+ "spec/unit/scope_spec.rb",
165
192
  "spec/unit/storage_factory_spec.rb",
166
193
  "spec/unit/storage_proxy_spec.rb",
167
194
  "spec/unit/translator_spec.rb"
@@ -175,6 +202,7 @@ Gem::Specification.new do |s|
175
202
  s.add_development_dependency(%q<rspec>, [">= 1.2.9"])
176
203
  s.add_development_dependency(%q<cucumber>, [">= 0"])
177
204
  s.add_development_dependency(%q<jferris-mocha>, [">= 0"])
205
+ s.add_development_dependency(%q<memcached>, [">= 0"])
178
206
  s.add_runtime_dependency(%q<sequel>, [">= 3.7.0"])
179
207
  s.add_runtime_dependency(%q<json>, [">= 0"])
180
208
  s.add_runtime_dependency(%q<activesupport>, [">= 0"])
@@ -183,6 +211,7 @@ Gem::Specification.new do |s|
183
211
  s.add_dependency(%q<rspec>, [">= 1.2.9"])
184
212
  s.add_dependency(%q<cucumber>, [">= 0"])
185
213
  s.add_dependency(%q<jferris-mocha>, [">= 0"])
214
+ s.add_dependency(%q<memcached>, [">= 0"])
186
215
  s.add_dependency(%q<sequel>, [">= 3.7.0"])
187
216
  s.add_dependency(%q<json>, [">= 0"])
188
217
  s.add_dependency(%q<activesupport>, [">= 0"])
@@ -192,6 +221,7 @@ Gem::Specification.new do |s|
192
221
  s.add_dependency(%q<rspec>, [">= 1.2.9"])
193
222
  s.add_dependency(%q<cucumber>, [">= 0"])
194
223
  s.add_dependency(%q<jferris-mocha>, [">= 0"])
224
+ s.add_dependency(%q<memcached>, [">= 0"])
195
225
  s.add_dependency(%q<sequel>, [">= 3.7.0"])
196
226
  s.add_dependency(%q<json>, [">= 0"])
197
227
  s.add_dependency(%q<activesupport>, [">= 0"])
data/lib/friendly.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require 'friendly/associations'
1
2
  require 'friendly/attribute'
2
3
  require 'friendly/boolean'
3
4
  require 'friendly/cache'
@@ -8,8 +9,11 @@ require 'friendly/document'
8
9
  require 'friendly/document_table'
9
10
  require 'friendly/index'
10
11
  require 'friendly/memcached'
12
+ require 'friendly/named_scope'
11
13
  require 'friendly/query'
12
14
  require 'friendly/sequel_monkey_patches'
15
+ require 'friendly/scope'
16
+ require 'friendly/scope_proxy'
13
17
  require 'friendly/storage_factory'
14
18
  require 'friendly/storage_proxy'
15
19
  require 'friendly/translator'
@@ -0,0 +1,7 @@
1
+ require 'friendly/associations/association'
2
+ require 'friendly/associations/set'
3
+
4
+ module Friendly
5
+ module Associations
6
+ end
7
+ end
@@ -0,0 +1,34 @@
1
+ module Friendly
2
+ module Associations
3
+ class Association
4
+ attr_reader :owner_klass, :name
5
+
6
+ def initialize(owner_klass, name, options = {})
7
+ @owner_klass = owner_klass
8
+ @name = name
9
+ @class_name = options[:class_name]
10
+ @foreign_key = options[:foreign_key]
11
+ end
12
+
13
+ def klass
14
+ @klass ||= class_name.constantize
15
+ end
16
+
17
+ def foreign_key
18
+ @foreign_key ||= [owner_klass_name, :id].join("_").to_sym
19
+ end
20
+
21
+ def class_name
22
+ @class_name ||= name.to_s.classify
23
+ end
24
+
25
+ def owner_klass_name
26
+ owner_klass.name.to_s.underscore.singularize
27
+ end
28
+
29
+ def scope(document)
30
+ klass.scope(foreign_key => document.id)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,37 @@
1
+ module Friendly
2
+ module Associations
3
+ class Set
4
+ attr_reader :klass, :association_klass, :associations
5
+
6
+ def initialize(klass, association_klass = Association)
7
+ @klass = klass
8
+ @association_klass = association_klass
9
+ @associations = {}
10
+ end
11
+
12
+ def add(name, options = {})
13
+ associations[name] = association_klass.new(klass, name, options)
14
+ add_association_accessor(name)
15
+ end
16
+
17
+ def get_scope(name, document)
18
+ get(name).scope(document)
19
+ end
20
+
21
+ def get(name)
22
+ associations[name]
23
+ end
24
+
25
+ protected
26
+ def add_association_accessor(name)
27
+ klass.class_eval do
28
+ eval <<-__END__
29
+ def #{name}
30
+ self.class.association_set.get_scope(:#{name}, self)
31
+ end
32
+ __END__
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -1,14 +1,36 @@
1
- require 'friendly/boolean'
2
- require 'friendly/uuid'
3
-
4
1
  module Friendly
5
2
  class Attribute
6
- CONVERTERS = {}
7
- CONVERTERS[UUID] = lambda { |s| UUID.new(s) }
8
- CONVERTERS[Integer] = lambda { |s| s.to_i }
9
- CONVERTERS[String] = lambda { |s| s.to_s }
10
- CONVERTERS[Boolean] = lambda { |s| s }
3
+ class << self
4
+ def register_type(type, sql_type, &block)
5
+ sql_types[type.name] = sql_type
6
+ converters[type] = block
7
+ end
8
+
9
+ def deregister_type(type)
10
+ sql_types.delete(type.name)
11
+ converters.delete(type)
12
+ end
13
+
14
+ def sql_type(type)
15
+ sql_types[type.name]
16
+ end
17
+
18
+ def sql_types
19
+ @sql_types ||= {}
20
+ end
11
21
 
22
+ def converters
23
+ @converters ||= {}
24
+ end
25
+
26
+ def custom_type?(klass)
27
+ !sql_type(klass).nil?
28
+ end
29
+ end
30
+
31
+ converters[Integer] = lambda { |s| s.to_i }
32
+ converters[String] = lambda { |s| s.to_s }
33
+
12
34
  attr_reader :klass, :name, :type, :default_value
13
35
 
14
36
  def initialize(klass, name, type, options = {})
@@ -25,7 +47,7 @@ module Friendly
25
47
 
26
48
  def convert(value)
27
49
  assert_converter_exists(value)
28
- CONVERTERS[type].call(value)
50
+ converters[type].call(value)
29
51
  end
30
52
 
31
53
  def default
@@ -37,7 +59,7 @@ module Friendly
37
59
  nil
38
60
  end
39
61
  end
40
-
62
+
41
63
  protected
42
64
  def build_accessors
43
65
  n = name
@@ -55,11 +77,15 @@ module Friendly
55
77
  end
56
78
 
57
79
  def assert_converter_exists(value)
58
- unless CONVERTERS.has_key?(type)
80
+ unless converters.has_key?(type)
59
81
  msg = "Can't convert #{value} to #{type}.
60
82
  Add a custom converter to Friendly::Attribute::CONVERTERS."
61
83
  raise NoConverterExists, msg
62
84
  end
63
85
  end
86
+
87
+ def converters
88
+ self.class.converters
89
+ end
64
90
  end
65
91
  end