gravatar-ultimate 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
@@ -0,0 +1,24 @@
1
+ .idea
2
+ tmp
3
+
4
+ ## MAC OS
5
+ .DS_Store
6
+
7
+ ## TEXTMATE
8
+ *.tmproj
9
+ tmtags
10
+
11
+ ## EMACS
12
+ *~
13
+ \#*
14
+ .\#*
15
+
16
+ ## VIM
17
+ *.swp
18
+
19
+ ## PROJECT::GENERAL
20
+ coverage
21
+ rdoc
22
+ pkg
23
+
24
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Colin MacKenzie IV
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,100 @@
1
+ = gravatar-ultimate
2
+
3
+ The Ultimate Gravatar Gem!
4
+
5
+ This gem is used to interface with the entire Gravatar API: it's not just for generating image URLs, but for connecting
6
+ to and communicating with the XML-RPC API too! Additionally, it can be used to download the Gravatar image data itself,
7
+ rather than just a URL to that data. This saves you the extra step of having to do so.
8
+
9
+ == Installation
10
+
11
+ gem install gravitar-ultimate
12
+
13
+ == Activate the gem...
14
+
15
+ As with any gem, you have to type a few lines to tell Ruby to actually *use* it. Here's how to do that...
16
+
17
+ ==== ...in Ruby on Rails (v3.x)
18
+
19
+ * This isn't ready yet, but it's in the works.
20
+
21
+ ==== ...in Ruby on Rails (v2.x)
22
+
23
+ * Edit your config/environment.rb file
24
+ * Add this line beneath "Rails::Initializer.run do |config|":
25
+ config.gem 'gravitar-ultimate'
26
+
27
+ ==== ...in vanilla Ruby
28
+
29
+ require 'rubygems'
30
+ gem 'gravitar-ultimate'
31
+ require 'gravitar-ultimate'
32
+
33
+ == Usage
34
+ Using the gem is actually pretty simple. Let's say you want the Gravatar image URL for "generic@example.com":
35
+ url = Gravatar.new("generic@example.com").image_url
36
+
37
+ Cool, huh? Let's take it a step further and grab the actual image *data* so that we can render it on the screen:
38
+ data = Gravatar.new("generic@example.com").image_data
39
+
40
+ Fine, but how about the rest of the API as advertised at http://en.gravatar.com/site/implement/xmlrpc? Well, for
41
+ that you need either the user's Gravatar password, or their API key:
42
+
43
+ api = Gravatar.new("generic@example.com", :password => "helloworld")
44
+ api = Gravatar.new("generic@example.com", :api_key => "AbCdEfG1234")
45
+
46
+ After you have that, things get a lot easier:
47
+
48
+ api.exists? #=> true or false, depending on whether this user has an account.
49
+ api.addresses #=> a list of email addresses and their corresponding images
50
+ api.save_data!(rating, image_data) #=> saves an image to this user's account and returns a handle to it
51
+ api.use_user_image!(handle, an_email_address) #=> uses the specified image handle for the specified email address(es)
52
+ api.exists?("another@example.com") #=> true or false, depending on whether the specified email exists.
53
+
54
+
55
+ == Caching
56
+
57
+ As you can see this is quite powerful. But it gets better. Gravatar Ultimate even manages caching of API responses for
58
+ you! That way, if an error occurs, (such as the Gravatar site being offline), your code won't break. It'll instead
59
+ gracefully fall back to the cached copy! By default, if you are using Rails, it'll use the Rails cache. Otherwise, it'll
60
+ use whatever cache you're using with Gravatar (by default an instance of ActiveSupport::Cache::FileStore).
61
+
62
+ This has obvious benefits when used for the API calls that do not result in changing the user's profile, but what you
63
+ might not have thought of yet is that it also caches #image_data, so you can hook your application up to that method
64
+ without fear of what might happen to all those Gravatar images if the Gravatar server should be unavailable!
65
+
66
+ To customize exactly which cache is used, see the next section...
67
+
68
+ === Configuration
69
+
70
+ To see settings and options you can give for a particular Gravatar instance, check out the Gravatar class documentation.
71
+ There are a few things you can set for Gravatar on a system-wide basis, and that's what we'll go over next.
72
+
73
+ For a non-Rails project, simply set these options before you start using Gravatar. For a Rails project, you should set
74
+ them within an Initializer in config/initializers/any_filename.rb in order to ensure that the settings are applied
75
+ (A) after Gravatar has been included into the project, and (B) before it is actually used by Rails.
76
+
77
+ # You can set the default cache for Gravatar to use:
78
+ Gravatar.cache = ActiveSupport::Cache::SynchronizedMemoryStore.new
79
+
80
+ # You can also set the length of time an item in the Gravatar cache is valid. Default is 24.hours
81
+ Gravatar.duration = 20.minutes
82
+
83
+ # You can also change the logger used by default. It's worth mentioning that, once again, Gravatar will use
84
+ # the Rails logger if it's available. Otherwise, the default is $stdout.
85
+ grav_log = ""
86
+ Gravatar.logger = StringIO.new(grav_log) # logs Gravatar output to a String
87
+
88
+ == Note on Patches/Pull Requests
89
+
90
+ * Fork the project.
91
+ * Make your feature addition or bug fix.
92
+ * Add tests for it. This is important so I don't break it in a
93
+ future version unintentionally.
94
+ * Commit, do not mess with rakefile, version, or history.
95
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
96
+ * Send me a pull request. Bonus points for topic branches.
97
+
98
+ == Copyright
99
+
100
+ Copyright (c) 2010 Colin MacKenzie IV. See LICENSE for details.
@@ -0,0 +1,64 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "gravatar-ultimate"
8
+ gem.summary = %Q{A gem for interfacing with the entire Gravatar API: not just images, but the XML-RPC API too!}
9
+ gem.description = %Q{The Ultimate Gravatar Gem!
10
+
11
+ This gem is used to interface with the entire Gravatar API: it's not just for generating image URLs, but for connecting
12
+ to and communicating with the XML-RPC API too! Additionally, it can be used to download the Gravatar image data itself,
13
+ rather than just a URL to that data. This saves you the extra step of having to do so.}
14
+ gem.email = "sinisterchipmunk@gmail.com"
15
+ gem.homepage = "http://github.com/sinisterchipmunk/gravatar"
16
+ gem.authors = ["Colin MacKenzie IV"]
17
+ gem.add_dependency "sc-core-ext", ">= 1.2.0"
18
+ gem.add_development_dependency "rspec", ">= 1.3.0"
19
+ gem.add_development_dependency "fakeweb", ">= 1.2.8"
20
+ end
21
+ Jeweler::GemcutterTasks.new
22
+ rescue LoadError
23
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
24
+ end
25
+
26
+ require 'spec/rake/spectask'
27
+ Spec::Rake::SpecTask.new(:test) do |test|
28
+ test.libs << 'lib' << 'spec'
29
+ test.pattern = 'spec/**/*_spec.rb'
30
+ test.verbose = true
31
+ end
32
+
33
+ desc "Run all specs"
34
+ task :spec => :test
35
+
36
+ begin
37
+ gem 'rcov'
38
+ require 'rcov/rcovtask'
39
+ Spec::Rake::SpecTask.new(:coverage) do |test|
40
+ test.libs << 'lib' << 'spec'
41
+ test.pattern = "spec/**/*_spec.rb"
42
+ test.verbose = true
43
+ test.rcov = true
44
+ test.rcov_opts = ['--html', '--exclude spec']
45
+ end
46
+ rescue LoadError
47
+ task :coverage do
48
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
49
+ end
50
+ end
51
+
52
+ task :test => :check_dependencies
53
+
54
+ task :default => :test
55
+
56
+ require 'rake/rdoctask'
57
+ Rake::RDocTask.new do |rdoc|
58
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
59
+
60
+ rdoc.rdoc_dir = 'rdoc'
61
+ rdoc.title = "gravatar #{version}"
62
+ rdoc.rdoc_files.include('README*')
63
+ rdoc.rdoc_files.include('lib/**/*.rb')
64
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
@@ -0,0 +1,77 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{gravatar-ultimate}
8
+ s.version = "1.0.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Colin MacKenzie IV"]
12
+ s.date = %q{2010-06-16}
13
+ s.description = %q{The Ultimate Gravatar Gem!
14
+
15
+ This gem is used to interface with the entire Gravatar API: it's not just for generating image URLs, but for connecting
16
+ to and communicating with the XML-RPC API too! Additionally, it can be used to download the Gravatar image data itself,
17
+ rather than just a URL to that data. This saves you the extra step of having to do so.}
18
+ s.email = %q{sinisterchipmunk@gmail.com}
19
+ s.extra_rdoc_files = [
20
+ "LICENSE",
21
+ "README.rdoc"
22
+ ]
23
+ s.files = [
24
+ ".document",
25
+ ".gitignore",
26
+ "LICENSE",
27
+ "README.rdoc",
28
+ "Rakefile",
29
+ "VERSION",
30
+ "gravatar-ultimate.gemspec",
31
+ "lib/gravatar-ultimate.rb",
32
+ "lib/gravatar.rb",
33
+ "lib/gravatar/cache.rb",
34
+ "lib/gravatar/dependencies.rb",
35
+ "lib/gravatar_ultimate.rb",
36
+ "spec/credentials.yml.example",
37
+ "spec/fixtures/image.jpg",
38
+ "spec/lib/gravatar/cache_and_logger_spec.rb",
39
+ "spec/lib/gravatar/cache_setup_spec.rb",
40
+ "spec/lib/gravatar/dependencies_spec.rb",
41
+ "spec/lib/gravatar_spec.rb",
42
+ "spec/spec.opts",
43
+ "spec/spec_helper.rb"
44
+ ]
45
+ s.homepage = %q{http://github.com/sinisterchipmunk/gravatar}
46
+ s.rdoc_options = ["--charset=UTF-8"]
47
+ s.require_paths = ["lib"]
48
+ s.rubygems_version = %q{1.3.6}
49
+ s.summary = %q{A gem for interfacing with the entire Gravatar API: not just images, but the XML-RPC API too!}
50
+ s.test_files = [
51
+ "spec/spec_helper.rb",
52
+ "spec/lib/gravatar_spec.rb",
53
+ "spec/lib/gravatar/dependencies_spec.rb",
54
+ "spec/lib/gravatar/cache_and_logger_spec.rb",
55
+ "spec/lib/gravatar/cache_setup_spec.rb"
56
+ ]
57
+
58
+ if s.respond_to? :specification_version then
59
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
60
+ s.specification_version = 3
61
+
62
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
63
+ s.add_runtime_dependency(%q<sc-core-ext>, [">= 1.2.0"])
64
+ s.add_development_dependency(%q<rspec>, [">= 1.3.0"])
65
+ s.add_development_dependency(%q<fakeweb>, [">= 1.2.8"])
66
+ else
67
+ s.add_dependency(%q<sc-core-ext>, [">= 1.2.0"])
68
+ s.add_dependency(%q<rspec>, [">= 1.3.0"])
69
+ s.add_dependency(%q<fakeweb>, [">= 1.2.8"])
70
+ end
71
+ else
72
+ s.add_dependency(%q<sc-core-ext>, [">= 1.2.0"])
73
+ s.add_dependency(%q<rspec>, [">= 1.3.0"])
74
+ s.add_dependency(%q<fakeweb>, [">= 1.2.8"])
75
+ end
76
+ end
77
+
@@ -0,0 +1 @@
1
+ require File.expand_path("../gravatar", __FILE__)
@@ -0,0 +1,282 @@
1
+ require File.expand_path('../gravatar/dependencies', __FILE__)
2
+ require File.expand_path("../gravatar/cache", __FILE__)
3
+
4
+ # ==== Errors ====
5
+ #
6
+ # Errors usually come with a number and human readable text. Generally the text should be followed whenever possible,
7
+ # but a brief description of the numeric error codes are as follows:
8
+ #
9
+ # -7 Use secure.gravatar.com
10
+ # -8 Internal error
11
+ # -9 Authentication error
12
+ # -10 Method parameter missing
13
+ # -11 Method parameter incorrect
14
+ # -100 Misc error (see text)
15
+ #
16
+ class Gravatar
17
+ attr_reader :email
18
+
19
+ # Creates a new instance of Gravatar. Valid options include:
20
+ # :password => the password for this account, to be used instead of :api_key (don't supply both)
21
+ # :api_key or :apikey or :key => the API key for this account, to be used instead of :password (don't supply both)
22
+ # :duration => the cache duration to use for this instance
23
+ # :logger => the logger to use for this instance
24
+ #
25
+ # Note that :password and :api_key are both optional. If omitted, no web services will be available but this
26
+ # user's Gravatar image can still be constructed using #image_uri or #image_data.
27
+ #
28
+ def initialize(email, options = {})
29
+ raise ArgumentError, "Expected :email" unless email
30
+ @options = options || {}
31
+ @email = email
32
+
33
+ pw_or_key = auth.keys.first || :none
34
+ @cache = Gravatar::Cache.new(self.class.cache, options[:duration] || self.class.duration,
35
+ "gravatar-#{email_hash}-#{pw_or_key}", options[:logger] || self.class.logger)
36
+
37
+ if !auth.empty?
38
+ @api = XMLRPC::Client.new("secure.gravatar.com", "/xmlrpc?user=#{email_hash}", 443, nil, nil, nil, nil, true)
39
+ end
40
+ end
41
+
42
+ # The duration of the cache for this instance of Gravatar, independent of any other instance
43
+ def cache_duration
44
+ @cache.duration
45
+ end
46
+
47
+ # Sets the duration of the cache for this instance of Gravatar, independent of any other instance
48
+ def cache_duration=(time)
49
+ @cache.duration = time
50
+ end
51
+
52
+ # Check whether one or more email addresses have corresponding avatars. If no email addresses are
53
+ # specified, the one associated with this object is used.
54
+ #
55
+ # Returns: Boolean for a single email address; a hash of emails => booleans for multiple addresses.
56
+ #
57
+ # This method is cached for up to the value of @duration or Gravatar.duration.
58
+ def exists?(*emails)
59
+ hashed_emails = normalize_email_addresses(emails)
60
+ cache('exists', hashed_emails) do
61
+ hash = call('grav.exists', :hashes => hashed_emails)
62
+ if hash.length == 1
63
+ boolean(hash.values.first)
64
+ else
65
+ dehashify_emails(hash, emails) { |value| boolean(value) }
66
+ end
67
+ end
68
+ end
69
+
70
+ # Gets a list of addresses for this account, returning a hash following this format:
71
+ # {
72
+ # address => {
73
+ # :rating => rating,
74
+ # :userimage => userimage,
75
+ # :userimage_url => userimage_url
76
+ # }
77
+ # }
78
+ #
79
+ # This method is cached for up to the value of @duration or Gravatar.duration.
80
+ def addresses
81
+ cache('addresses') do
82
+ call('grav.addresses').inject({}) do |hash, (address, info)|
83
+ hash[address] = info.merge(:rating => rating(info[:rating]))
84
+ hash
85
+ end
86
+ end
87
+ end
88
+
89
+ # Returns a hash of user images for this account in the following format:
90
+ # { user_img_hash => [rating, url] }
91
+ #
92
+ # This method is cached for up to the value of @duration or Gravatar.duration.
93
+ def user_images
94
+ cache('user_images') do
95
+ call('grav.userimages').inject({}) do |hash, (key, array)|
96
+ hash[key] = [rating(array.first), array.last]
97
+ hash
98
+ end
99
+ end
100
+ end
101
+
102
+ # Saves binary image data as a userimage for this account and returns the ID of the image.
103
+ #
104
+ # This method is not cached.
105
+ def save_data!(rating, data)
106
+ call('grav.saveData', :data => Base64.encode64(data), :rating => _rating(rating))
107
+ end
108
+ alias save_image! save_data!
109
+
110
+ # Read an image via its URL and save that as a userimage for this account, returning true or false
111
+ #
112
+ # This method is not cached.
113
+ def save_url!(rating, url)
114
+ call('grav.saveUrl', :url => url, :rating => _rating(rating))
115
+ end
116
+
117
+ # Use a userimage as a gravatar for one or more addresses on this account. Returns a hash:
118
+ # { email_address => true/false }
119
+ #
120
+ # This method is not cached.
121
+ #
122
+ # This method will clear out the cache, since it may have an effect on what the API methods respond with.
123
+ def use_user_image!(image_hash, *email_addresses)
124
+ hashed_email_addresses = normalize_email_addresses(email_addresses)
125
+ hash = call('grav.useUserimage', :userimage => image_hash, :addresses => hashed_email_addresses)
126
+ returning dehashify_emails(hash, email_addresses) { |value| boolean(value) } do
127
+ expire_cache!
128
+ end
129
+ end
130
+ alias use_image! use_user_image!
131
+
132
+ # Remove the userimage associated with one or more email addresses. Returns a hash of booleans.
133
+ # NOTE: This appears to always return false, even when it is really removing an image. If you
134
+ # know what the deal with that is, drop me a line so I can update this documentation!
135
+ #
136
+ # This method is not cached.
137
+ #
138
+ # This method will clear out the cache, since it may have an effect on what the API methods respond with.
139
+ def remove_image!(*emails)
140
+ hashed_email_addresses = normalize_email_addresses(emails)
141
+ hash = call('grav.removeImage', :addresses => hashed_email_addresses)
142
+ returning dehashify_emails(hash, emails) { |value| boolean(value) } do
143
+ expire_cache!
144
+ end
145
+ end
146
+
147
+ # Remove a userimage from the account and any email addresses with which it is associated. Returns
148
+ # true or false.
149
+ #
150
+ # This method is not cached.
151
+ #
152
+ # This method will clear out the cache, since it may have an effect on what the API methods respond with.
153
+ def delete_user_image!(userimage)
154
+ returning boolean(call('grav.deleteUserimage', :userimage => userimage)) do
155
+ expire_cache!
156
+ end
157
+ end
158
+
159
+ # Runs a simple Gravatar test. Useful for debugging. Gravatar will echo back any arguments you pass.
160
+ # This method is not cached.
161
+ def test(hash)
162
+ call('grav.test', hash)
163
+ end
164
+
165
+ # Returns the MD5 hash for the specified email address, or the one associated with this object.
166
+ def email_hash(email = self.email)
167
+ Digest::MD5.hexdigest(email.downcase.strip)
168
+ end
169
+
170
+ # Returns the URL for this user's gravatar image. Options include:
171
+ #
172
+ # :ssl or :secure if true, HTTPS will be used instead of HTTP. Default is false.
173
+ # :rating or :r a rating threshold for this image. Can be one of [ :g, :pg, :r, :x ]. Default is :g.
174
+ # :size or :s a size for this image. Can be anywhere between 1 and 512. Default is 80.
175
+ # :default or :d a default URL for this image to display if the specified user has no image;
176
+ # or this can be one of [ :identicon, :monsterid, :wavatar, 404 ]. By default a generic
177
+ # Gravatar image URL will be returned.
178
+ # :filetype an extension such as :jpg or :png. Default is omitted.
179
+ #
180
+ # See http://en.gravatar.com/site/implement/url for much more detailed information.
181
+ def image_url(options = {})
182
+ secure = options[:ssl] || options[:secure]
183
+ proto = "http#{secure ? 's' : ''}"
184
+ sub = secure ? "secure" : "www"
185
+
186
+ "#{proto}://#{sub}.gravatar.com/avatar/#{email_hash}#{extension_for_image(options)}#{query_for_image(options)}"
187
+ end
188
+
189
+ # Returns the image data for this user's gravatar image. This is the same as reading the data at #image_url. See
190
+ # that method for more information.
191
+ #
192
+ # This method is cached for up to the value of @duration or Gravatar.duration.
193
+ def image_data(options = {})
194
+ url = image_url(options)
195
+ cache(url) { OpenURI.open_uri(URI.parse(url)).read }
196
+ end
197
+
198
+ def self.version
199
+ @version ||= File.read(File.join(File.dirname(__FILE__), "../VERSION")).chomp
200
+ end
201
+
202
+ private
203
+ def cache(*key, &block)
204
+ @cache.call(*key, &block)
205
+ end
206
+
207
+ def expire_cache!
208
+ @cache.clear!
209
+ end
210
+
211
+ def dehashify_emails(response, emails)
212
+ hashed_emails = emails.collect { |email| email_hash(email) }
213
+ response.inject({}) do |hash, (hashed_email, value)|
214
+ value = yield(value) if value
215
+ email = emails[hashed_emails.index(hashed_email)]
216
+ hash[email] = value
217
+ hash
218
+ end
219
+ end
220
+
221
+ def normalize_email_addresses(addresses)
222
+ addresses.flatten!
223
+ addresses << @email if addresses.empty?
224
+ addresses.map { |email| email_hash(email) }
225
+ end
226
+
227
+ def rating(i)
228
+ case i
229
+ when 0, '0' then :g
230
+ when 1, '1' then :pg
231
+ when 2, '2' then :r
232
+ when 3, '3' then :x
233
+ when :g then 0
234
+ when :pg then 1
235
+ when :r then 2
236
+ when :x then 3
237
+ else raise ArgumentError, "Unexpected rating index: #{i} (expected between 0..3)"
238
+ end
239
+ end
240
+ alias _rating rating
241
+
242
+ def boolean(i)
243
+ i.kind_of?(Numeric) ? i != 0 : i
244
+ end
245
+
246
+ def call(name, args_hash = {})
247
+ r = @api.call(name, auth.merge(args_hash))
248
+ r = r.with_indifferent_access if r.kind_of?(Hash)
249
+ r
250
+ end
251
+
252
+ def auth
253
+ api_key ? {:apikey => api_key} : (password ? {:password => password} : {})
254
+ end
255
+
256
+ def api_key
257
+ options[:apikey] || options[:api_key] || options[:key]
258
+ end
259
+
260
+ def password
261
+ options[:password]
262
+ end
263
+
264
+ def options
265
+ @options
266
+ end
267
+
268
+ def query_for_image(options)
269
+ query = ''
270
+ [:rating, :size, :default, :r, :s, :d].each do |key|
271
+ if options.key?(key)
272
+ query.blank? ? query.concat("?") : query.concat("&")
273
+ query.concat("#{key}=#{CGI::escape options[key].to_s}")
274
+ end
275
+ end
276
+ query
277
+ end
278
+
279
+ def extension_for_image(options)
280
+ options.key?(:filetype) ? "." + (options[:filetype] || "jpg").to_s : ""
281
+ end
282
+ end
@@ -0,0 +1,133 @@
1
+ class Gravatar
2
+ # A wrapper around any given Cache object which provides Gravatar-specific helpers. Used internally.
3
+ class Cache
4
+ attr_reader :real_cache, :namespace
5
+ attr_accessor :duration, :logger
6
+
7
+ def initialize(real_cache, duration, namespace = nil, logger = Gravatar.logger)
8
+ @duration = duration
9
+ @real_cache = real_cache
10
+ @namespace = namespace
11
+ @logger = logger
12
+ end
13
+
14
+ # Provide a series of arguments to be used as a cache key, and a block to be executed when the cache
15
+ # is expired or needs to be populated.
16
+ #
17
+ # Example:
18
+ # cache = Gravatar::Cache.new(Rails.cache, 30.minutes)
19
+ # cache.call(:first_name => "Colin", :last_name => "MacKenzie") { call_webservice(with_some_args) }
20
+ #
21
+ def call(*key, &block)
22
+ cached_copy = read_cache(*key)
23
+ if expired?(*key) && block_given?
24
+ begin
25
+ returning(yield) do |object|
26
+ write_cache(object, *key)
27
+ end
28
+ rescue
29
+ log_error($!)
30
+ cached_copy.nil? ? nil : cached_copy[:object]
31
+ end
32
+ else
33
+ cached_copy.nil? ? nil : cached_copy[:object]
34
+ end
35
+ end
36
+
37
+ # Clears out the entire cache for this object's namespace. This actually removes the objects,
38
+ # instead of simply marking them as expired, so it will be as if the object never existed.
39
+ def clear!
40
+ @real_cache.delete_matched(/^#{Regexp::escape @namespace}/)
41
+ end
42
+
43
+ # forces the specified key to become expired
44
+ def expire!(*key)
45
+ unless expired?(*key)
46
+ @real_cache.write(cache_key(*key), { :expires_at => 1.minute.ago, :object => read_cache(*key)[:object] })
47
+ end
48
+ end
49
+
50
+ # Returns true if the cached copy is nil or expired based on @duration.
51
+ def expired?(*key)
52
+ cached_copy = read_cache(*key)
53
+ cached_copy.nil? || cached_copy[:expires_at] < Time.now
54
+ end
55
+
56
+ # Reads an object from the cache based on the cache key constructed from *key.
57
+ def read_cache(*key)
58
+ @real_cache.read(cache_key(*key))
59
+ end
60
+
61
+ # Writes an object to the cache based on th cache key constructed from *key.
62
+ def write_cache(object, *key)
63
+ @real_cache.write(cache_key(*key), { :expires_at => Time.now + duration, :object => object })
64
+ end
65
+
66
+ # Constructs a cache key from the specified *args and @namespace.
67
+ def cache_key(*args)
68
+ ActiveSupport::Cache.expand_cache_key(args, @namespace)
69
+ end
70
+
71
+ # Logs an error message, as long as self.logger responds to :error or :write.
72
+ # Otherwise, re-raises the error.
73
+ def log_error(error)
74
+ if logger.respond_to?(:error)
75
+ logger.error error.message
76
+ error.backtrace.each { |line| logger.error " #{line}" }
77
+ elsif logger.respond_to?(:write)
78
+ logger.write(([error.message]+error.backtrace).join("\n "))
79
+ logger.write("\n")
80
+ else raise error
81
+ end
82
+ end
83
+ end
84
+
85
+ class << self
86
+ def default_cache_instance
87
+ if defined?(Rails)
88
+ Rails.cache
89
+ else
90
+ ActiveSupport::Cache::FileStore.new("tmp/cache")
91
+ end
92
+ end
93
+
94
+ def default_logger_instance
95
+ if defined?(Rails)
96
+ Rails.logger
97
+ else
98
+ $stdout
99
+ end
100
+ end
101
+
102
+ def cache
103
+ @cache ||= default_cache_instance
104
+ end
105
+
106
+ def cache=(instance)
107
+ @cache = instance
108
+ end
109
+
110
+ def logger
111
+ @logger ||= default_logger_instance
112
+ end
113
+
114
+ def logger=(logger)
115
+ @logger = logger
116
+ end
117
+
118
+ # How long is a cached object good for? Default is 30 minutes.
119
+ def duration
120
+ @duration ||= 24.hours
121
+ end
122
+
123
+ def duration=(duration)
124
+ @duration = duration
125
+ end
126
+
127
+ # Resets any changes to the cache and initializes a new cache. If using Rails, the
128
+ # new cache will be the Rails cache.
129
+ def reset_cache!
130
+ @cache = nil
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,16 @@
1
+ if defined?(Rails)
2
+ Rails.configuration.gem "sc-core-ext", ">= 1.2.0"
3
+ elsif !defined?(Gem)
4
+ require 'rubygems'
5
+ gem 'sc-core-ext', '>= 1.2.0'
6
+ end
7
+
8
+ unless defined?(ScCoreExt) || defined?(Rails) # because Rails will load it later and we don't really need it quite yet.
9
+ require 'sc-core-ext'
10
+ end
11
+
12
+ # The rest of this is core Ruby stuff so it's safe to load immediately, even if Rails is running the show.
13
+ require 'open-uri'
14
+ require "digest/md5"
15
+ require 'xmlrpc/client'
16
+ require 'base64'
@@ -0,0 +1 @@
1
+ require File.expand_path("../gravatar", __FILE__)
@@ -0,0 +1,11 @@
1
+ # To run this test suite, we need to set up some credentials because we're running real HTTP requests.
2
+ # Make sure you have a working Gravatar account, and enter the following options:
3
+ primary_email: email@example.com
4
+ email: email@example.com
5
+ api_key: YOUR_API_KEY
6
+
7
+ # The primary email is the one that's marked 'primary' on your Gravatar account page. Both emails can
8
+ # be the same. The API Key is your Gravatar API key, which you can optionally replace with:
9
+ # password: YOUR_GRAVATAR_PASSWORD
10
+ #
11
+ # Take care not to commit your credentials!
Binary file
@@ -0,0 +1,94 @@
1
+ require 'spec_helper'
2
+
3
+ describe Gravatar::Cache do
4
+ subject { Gravatar::Cache.new(new_cache, 30.minutes, "gravatar-specs") }
5
+
6
+ context "with a nonexistent cache item" do
7
+ it "should be expired" do
8
+ subject.expired?(:nothing).should be_true
9
+ end
10
+
11
+ it "should fire the block" do
12
+ subject.call(:nothing) { @fired = 1 }
13
+ @fired.should == 1
14
+ end
15
+
16
+ it "should return nil if no block given" do
17
+ subject.call(:nothing).should be_nil
18
+ end
19
+
20
+ it "should return the block value" do
21
+ subject.call(:nothing) { 1 }.should == 1
22
+ end
23
+ end
24
+
25
+ context "with a pre-existing cache item" do
26
+ before(:each) { subject.call(:nothing) { 1 } }
27
+
28
+ it "should not be expired" do
29
+ subject.expired?(:nothing).should be_false
30
+ end
31
+
32
+ it "should not fire the block" do
33
+ subject.call(:nothing) { @fired = 1 }
34
+ @fired.should_not == 1
35
+ end
36
+
37
+ it "should return the cached value" do
38
+ subject.call(:nothing) { raise "Block expected not to fire" }.should == 1
39
+ end
40
+
41
+ context "that is expired" do
42
+ before(:each) { subject.expire!(:nothing) }
43
+
44
+ it "should be expired" do
45
+ subject.expired?(:nothing).should be_true
46
+ end
47
+
48
+ it "should fire the block" do
49
+ subject.call(:nothing) { @fired = 1 }
50
+ @fired.should == 1
51
+ end
52
+
53
+ context "in the event of an error while refreshing" do
54
+ before(:each) { subject.logger = StringIO.new("") }
55
+
56
+ it "should recover" do
57
+ proc { subject.call(:nothing) { raise "something bad happened" } }.should_not raise_error(RuntimeError)
58
+ end
59
+
60
+ it "should return the cached copy" do
61
+ subject.call(:nothing) { raise "something bad happened" }.should == 1
62
+ end
63
+
64
+ context "its logger" do
65
+ before(:each) { subject.logger = Object.new }
66
+
67
+ it "should log it if #error is available" do
68
+ subject.logger.stub!(:error => nil)
69
+ subject.logger.should_receive(:error).and_return(nil)
70
+ subject.call(:nothing) { raise "something bad happened" }
71
+ end
72
+
73
+ it "should log it if #write is available" do
74
+ subject.logger.stub!(:write => nil)
75
+ subject.logger.should_receive(:write).and_return(nil)
76
+ subject.call(:nothing) { raise "something bad happened" }
77
+ end
78
+
79
+ it "should re-raise the error if no other methods are available" do
80
+ proc { subject.call(:nothing) { raise "something bad happened" } }.should raise_error(RuntimeError)
81
+ end
82
+ end
83
+ end
84
+
85
+ it "should return the block value" do
86
+ subject.call(:nothing) { 2 }.should == 2
87
+ end
88
+
89
+ it "should return the cached value if there is no block value" do
90
+ subject.call(:nothing).should == 1
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,42 @@
1
+ describe "gravatar cache setup" do
2
+ context "within Rails" do
3
+ before(:each) do
4
+ # We reset it here because we've already set it to MemoryStore for the sake of the majority.
5
+ Gravatar.reset_cache!
6
+ module Rails
7
+ end
8
+ end
9
+
10
+ it "should get the default cache instance from Rails" do
11
+ Rails.should_receive(:cache).and_return("a cache object")
12
+ Gravatar.cache
13
+ end
14
+
15
+ it "should get the current cache from cache assignment, if any" do
16
+ Gravatar.cache = "a cache object"
17
+ Gravatar.cache.should == "a cache object"
18
+ end
19
+ end
20
+
21
+ context "out of Rails" do
22
+ it "should get the default cache from ActiveSupport" do
23
+ Gravatar.cache.should be_kind_of(ActiveSupport::Cache::FileStore)
24
+ end
25
+
26
+ it "should get the current cache from cache assignment, if any" do
27
+ Gravatar.cache = "a cache object"
28
+ Gravatar.cache.should == "a cache object"
29
+ end
30
+ end
31
+
32
+ after(:each) do
33
+ # We reset it here because we've already fubarred it with the Rails tests.
34
+ Gravatar.reset_cache!
35
+
36
+ silence_warnings do
37
+ if defined?(Rails)
38
+ Object.send :remove_const, :Rails
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,29 @@
1
+ require "spec_helper"
2
+
3
+ describe "Dependencies" do
4
+ context "within Rails" do
5
+ before(:each) do
6
+ module ::Rails
7
+ def self.configuration
8
+ return @config if @config
9
+ @config = Object.new
10
+ klass = class << @config; self; end
11
+ klass.instance_eval do
12
+ def gem(*a, &b); end
13
+ public :gem
14
+ end
15
+ @config
16
+ end
17
+ end
18
+ end
19
+
20
+ it "should set a Rails gem dependency" do
21
+ Rails.configuration.should_receive(:gem, :with => ["sc-core-ext", ">= 1.2.0"])
22
+ load File.expand_path("../../../../lib/gravatar/dependencies.rb", __FILE__)
23
+ end
24
+
25
+ after(:each) { silence_warnings { Object.send(:remove_const, :Rails) } }
26
+ end
27
+
28
+ # Don't know how to test the inclusion of sc-core-ext via rubygems, but I suppose there's no reason that should fail.
29
+ end
@@ -0,0 +1,141 @@
1
+ require 'spec_helper'
2
+
3
+ describe Gravatar do
4
+ it "should have a valid version number" do
5
+ Gravatar.version.should =~ /^\d+\.\d+\.\d+$/
6
+ end
7
+
8
+ it "should allow setting cache duration by instance" do
9
+ grav = Gravatar.new($credentials[:primary_email])
10
+ grav.cache_duration = 10.minutes
11
+ grav.cache_duration.should == 10.minutes
12
+ end
13
+
14
+ it "should allow setting cache duration globally" do
15
+ Gravatar.duration = 10.minutes
16
+ Gravatar.new($credentials[:primary_email]).cache_duration.should == 10.minutes
17
+ Gravatar.duration = 30.minutes
18
+ end
19
+
20
+ it "should require :email" do
21
+ proc { subject }.should raise_error(ArgumentError)
22
+ end
23
+
24
+ context "given :email and :key" do
25
+ subject { Gravatar.new($credentials[:primary_email], $credentials)}
26
+
27
+ context "varying image ratings" do
28
+ [:g, :pg, :r, :x].each do |rating|
29
+ it "should save #{rating}-rated URLs and delete them" do
30
+ subject.save_url!(rating, "http://jigsaw.w3.org/css-validator/images/vcss").should ==
31
+ "2df7db511c46303983f0092556a1e47c"
32
+ subject.delete_user_image!("2df7db511c46303983f0092556a1e47c").should == true
33
+ end
34
+ end
35
+
36
+ it "should raise an ArgumentError given an invalid rating" do
37
+ proc { subject.save_url!(:invalid_rating, "http://jigsaw.w3.org/css-validator/images/vcss") }.should \
38
+ raise_error(ArgumentError)
39
+ end
40
+ end
41
+
42
+ it "should return addresses" do
43
+ subject.addresses.should_not be_empty
44
+ end
45
+
46
+ it "should test successfully" do
47
+ subject.test(:greeting => 'hello').should have_key(:response)
48
+ end
49
+
50
+ it "should save URLs and delete them" do
51
+ subject.save_url!(:g, "http://jigsaw.w3.org/css-validator/images/vcss").should == "2df7db511c46303983f0092556a1e47c"
52
+ subject.delete_user_image!("2df7db511c46303983f0092556a1e47c").should == true
53
+ end
54
+
55
+ # Not really the ideal approach but it's a valid test, at least
56
+ it "should save and delete images and associate/unassociate them with accounts" do
57
+ begin
58
+ subject.save_data!(:g, image_data).should == "23f086a793459fa25aab280054fec1b2"
59
+ subject.use_user_image!("23f086a793459fa25aab280054fec1b2", $credentials[:email]).should ==
60
+ { $credentials[:email] => false }
61
+ # See rdoc for #remove_image! for why we're not checking this.
62
+ subject.remove_image!($credentials[:email])#.should == { $credentials[:email] => true }
63
+ subject.delete_user_image!("23f086a793459fa25aab280054fec1b2").should == true
64
+ ensure
65
+ subject.remove_image!($credentials[:email])
66
+ begin
67
+ subject.delete_user_image!("23f086a793459fa25aab280054fec1b2")
68
+ rescue XMLRPC::FaultException
69
+ end
70
+ end
71
+ end
72
+
73
+ it "should return user images" do
74
+ subject.user_images.should == {"fe9dee44a1df19967db30a04083722d5"=>
75
+ [:g, "http://en.gravatar.com/userimage/14612723/fe9dee44a1df19967db30a04083722d5.jpg"]}
76
+ end
77
+
78
+ it "should determine that the user exists" do
79
+ subject.exists?.should be_true
80
+ end
81
+
82
+ it "should determine that a fake user does not exist" do
83
+ subject.exists?("not-even-a-valid-email").should be_false
84
+ end
85
+
86
+ it "should determine that multiple fake users do not exist" do
87
+ subject.exists?("invalid-1", "invalid-2").should == { "invalid-1" => false, "invalid-2" => false }
88
+ end
89
+ end
90
+
91
+ context "given :email" do
92
+ subject { Gravatar.new("sinisterchipmunk@gmail.com") }
93
+
94
+ it "should not raise an error" do
95
+ proc { subject }.should_not raise_error(ArgumentError)
96
+ end
97
+
98
+ it "should return email_hash" do
99
+ subject.email_hash.should == "5d8c7a8d951a28e10bd7407f33df6d63"
100
+ end
101
+
102
+ it "should return gravatar image_url" do
103
+ subject.image_url.should == "http://www.gravatar.com/avatar/5d8c7a8d951a28e10bd7407f33df6d63"
104
+ end
105
+
106
+ it "should return gravitar image data" do
107
+ subject.image_data.should == image_data
108
+ end
109
+
110
+ it "should return gravatar image_url with SSL" do
111
+ subject.image_url(:ssl => true).should == "https://secure.gravatar.com/avatar/5d8c7a8d951a28e10bd7407f33df6d63"
112
+ end
113
+
114
+ it "should return gravatar image_url with size" do
115
+ subject.image_url(:size => 512).should == "http://www.gravatar.com/avatar/5d8c7a8d951a28e10bd7407f33df6d63?size=512"
116
+ end
117
+
118
+ it "should return gravatar image_url with rating" do
119
+ subject.image_url(:rating => 'pg').should == "http://www.gravatar.com/avatar/5d8c7a8d951a28e10bd7407f33df6d63?rating=pg"
120
+ end
121
+
122
+ it "should return gravatar image_url with file type" do
123
+ subject.image_url(:filetype => 'png').should == "http://www.gravatar.com/avatar/5d8c7a8d951a28e10bd7407f33df6d63.png"
124
+ end
125
+
126
+ it "should return gravatar image_url with default image" do
127
+ subject.image_url(:default => "http://example.com/images/example.jpg").should ==
128
+ "http://www.gravatar.com/avatar/5d8c7a8d951a28e10bd7407f33df6d63?default=http%3A%2F%2Fexample.com%2Fimages%2Fexample.jpg"
129
+ end
130
+
131
+ it "should return gravatar image_url with SSL and default and size and rating" do
132
+ combinations = %w(
133
+ https://secure.gravatar.com/avatar/5d8c7a8d951a28e10bd7407f33df6d63?default=identicon&size=80&rating=g
134
+ https://secure.gravatar.com/avatar/5d8c7a8d951a28e10bd7407f33df6d63?size=80&rating=g&default=identicon
135
+ https://secure.gravatar.com/avatar/5d8c7a8d951a28e10bd7407f33df6d63?size=80&default=identicon&rating=g
136
+ https://secure.gravatar.com/avatar/5d8c7a8d951a28e10bd7407f33df6d63?rating=g&size=80&default=identicon
137
+ )
138
+ combinations.should include(subject.image_url(:ssl => true, :default => "identicon", :size => 80, :rating => :g))
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,2 @@
1
+ -c
2
+ -b
@@ -0,0 +1,30 @@
1
+ require File.expand_path("../../lib/gravatar", __FILE__)
2
+ unless defined?(Spec)
3
+ gem 'rspec'
4
+ require 'spec'
5
+ end
6
+
7
+ def image_data
8
+ File.read(File.expand_path("../fixtures/image.jpg", __FILE__))
9
+ end
10
+
11
+ require 'fakeweb'
12
+ FakeWeb.register_uri(:get, "http://www.gravatar.com/avatar/5d8c7a8d951a28e10bd7407f33df6d63", :response =>
13
+ "HTTP/1.1 200 OK\nContent-Type: image/jpg\n\n" +image_data)
14
+
15
+ def new_cache
16
+ ActiveSupport::Cache::MemoryStore.new
17
+ end
18
+
19
+ Gravatar.cache = new_cache
20
+
21
+ class Net::HTTP
22
+ alias_method :original_initialize, :initialize
23
+ def initialize(*args, &block)
24
+ original_initialize(*args, &block)
25
+ @ssl_context = OpenSSL::SSL::SSLContext.new
26
+ @ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
27
+ end
28
+ end
29
+
30
+ $credentials = YAML::load(File.read(File.expand_path("../credentials.yml", __FILE__))).with_indifferent_access
metadata ADDED
@@ -0,0 +1,132 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gravatar-ultimate
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 1
7
+ - 0
8
+ - 0
9
+ version: 1.0.0
10
+ platform: ruby
11
+ authors:
12
+ - Colin MacKenzie IV
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-06-16 00:00:00 -04:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: sc-core-ext
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 1
29
+ - 2
30
+ - 0
31
+ version: 1.2.0
32
+ type: :runtime
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: rspec
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ segments:
42
+ - 1
43
+ - 3
44
+ - 0
45
+ version: 1.3.0
46
+ type: :development
47
+ version_requirements: *id002
48
+ - !ruby/object:Gem::Dependency
49
+ name: fakeweb
50
+ prerelease: false
51
+ requirement: &id003 !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ segments:
56
+ - 1
57
+ - 2
58
+ - 8
59
+ version: 1.2.8
60
+ type: :development
61
+ version_requirements: *id003
62
+ description: |-
63
+ The Ultimate Gravatar Gem!
64
+
65
+ This gem is used to interface with the entire Gravatar API: it's not just for generating image URLs, but for connecting
66
+ to and communicating with the XML-RPC API too! Additionally, it can be used to download the Gravatar image data itself,
67
+ rather than just a URL to that data. This saves you the extra step of having to do so.
68
+ email: sinisterchipmunk@gmail.com
69
+ executables: []
70
+
71
+ extensions: []
72
+
73
+ extra_rdoc_files:
74
+ - LICENSE
75
+ - README.rdoc
76
+ files:
77
+ - .document
78
+ - .gitignore
79
+ - LICENSE
80
+ - README.rdoc
81
+ - Rakefile
82
+ - VERSION
83
+ - gravatar-ultimate.gemspec
84
+ - lib/gravatar-ultimate.rb
85
+ - lib/gravatar.rb
86
+ - lib/gravatar/cache.rb
87
+ - lib/gravatar/dependencies.rb
88
+ - lib/gravatar_ultimate.rb
89
+ - spec/credentials.yml.example
90
+ - spec/fixtures/image.jpg
91
+ - spec/lib/gravatar/cache_and_logger_spec.rb
92
+ - spec/lib/gravatar/cache_setup_spec.rb
93
+ - spec/lib/gravatar/dependencies_spec.rb
94
+ - spec/lib/gravatar_spec.rb
95
+ - spec/spec.opts
96
+ - spec/spec_helper.rb
97
+ has_rdoc: true
98
+ homepage: http://github.com/sinisterchipmunk/gravatar
99
+ licenses: []
100
+
101
+ post_install_message:
102
+ rdoc_options:
103
+ - --charset=UTF-8
104
+ require_paths:
105
+ - lib
106
+ required_ruby_version: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ segments:
111
+ - 0
112
+ version: "0"
113
+ required_rubygems_version: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ segments:
118
+ - 0
119
+ version: "0"
120
+ requirements: []
121
+
122
+ rubyforge_project:
123
+ rubygems_version: 1.3.6
124
+ signing_key:
125
+ specification_version: 3
126
+ summary: "A gem for interfacing with the entire Gravatar API: not just images, but the XML-RPC API too!"
127
+ test_files:
128
+ - spec/spec_helper.rb
129
+ - spec/lib/gravatar_spec.rb
130
+ - spec/lib/gravatar/dependencies_spec.rb
131
+ - spec/lib/gravatar/cache_and_logger_spec.rb
132
+ - spec/lib/gravatar/cache_setup_spec.rb