Chapter 13. Active Support
Active Support is a grab bag of utility methods and extensions to the Ruby standard library that accelerate development by providing syntactic sugar and easy ways to write expressive and DRY code. It makes time-related calculations easy, provides memoization & caching, handles multibyte/unicode operations, and offers all sorts of other convenience methods and nifty tricks. This chapter documents some of the most useful bits and pieces.
Note that while Rails makes all these class extensions and methods available to you everywhere within your application, you can use Active Support outside of a Rails application by just requiring it in:
require "active_support/all" # Note the /all part
%w(1 2 3 4 5 6 7).in_groups_of(3) # => [["1", "2", "3"], ["4", "5", "6"], ["7", nil, nil]]
If you can get away with it, it’s possible to just require the parts of Active Support you use, so take a look at the source for Active Support should you choose to go down that route.
Note
When requiring-in Rails-related libraries, be sure to use an underscore in separating their names:
require "active_support/all" # Do this require "activesupport/all" # Don't do this.
The version without an underscore will work, but brings in the older versions of the library (Rails 2 and previous), which might lead to frustrating mysteries down the road.
Object
The Object class gets several
upgrades from Active Support:
Object#blank?Returns
trueif the receiver isfalse, empty, or a string of whitespace:"Keep calm and carry on".blank? # => false " ".blank? # => true "".blank? # => true [].blank? # => true ["apple"].blank? # => false 42.blank? # => false true.blank? # => false nil.blank? # => true
This behaviour means you can replace code like this
if !title.nil? && !title.empty? # do something end
with this instead:
if !title.blank? # do something end
Object#present?Returns the negation of
Object#blank?– let’s use that to reduce the previous example to justif title.present? # do something end
Object#to_query(key)Returns a URL query string using the key as the parameter name and the receiver as the value:
"Snow".to_query("white") # => "white=Snow" 7.to_query("stinky dwarves") # => "stinky+dwarves=7"Object#try(method, *args, &block)Attempts to call a
methodwith whatever arguments and optional block. In the event themethoddoesn’t exist, it returnsnilinstead of raising aNoMethodexception.What this means is that you can turn lines like
@video && @person.title
into
@video.try(:title)
Class
Active Support extends the Class
class to provide Class#class_attribute, which functions just like the
attr_reader, attr_writer, and attr_accessor methods for instances in standard Ruby, but at a class
level:
class Video < ActiveRecord::Base class_attribute :commentable end class CellphoneVideo < Video end Video.commentable = true CellphoneVideo.commentable # => true CellphoneVideo.commentable = false CellphoneVideo.commentable # => false Video.commentable # => true # For convenience, a query method is also supplied Video.commentable? # => true
Class attributes are inherited and overwritable by subclasses.
Class#class_attribute also produces instance attribute accessors
by default:
Video.commentable = true v = Video.new v.commentable? # => true v.commentable = false v.commentable? # => false Video.commentable # => true
To disable the creation of instance writers (to just
have instance readers), pass in :instance_writer =>
false
class Video < ActiveRecord::Base class_attribute :commentable, :instance_writer => false end Video.commentable = true v = Video.new v.commentable # => true v.commentable = false # => raises NoMethodError: undefined method `commentable='
Module
Active Support extends the Module
class to provide several convenience methods for aliasing:
Module#alias_attribute(new_name, old_name)Provides a more complete interface compared to just using
aliasoralias_methodto give another name to an Active Record model’s attribute:class Video < ActiveRecord::Base # Adds #name-version aliases for: # #title # #title? # #title= alias_attribute :name, :title end v = Video.first v.name # => "John Candy" v.name? # => true v.name = "Canadian Bacon" # => "Canadian Bacon" v.title # => "Canadian Bacon"
Module#alias_method_chain(target, feature)Makes writing method wrappers using aliases easy.
Let’s say that you want to wrap each call to a method
Video#calculate_ratingwith a logging statement indicating that a rating has been calculated.One way to do it would be the following:
class Video < ActiveRecord::Base def calculate_rating # does some kind of rating calculation end def calculate_rating_with_logging(*args) logger.info "Calulating a rating!" calculate_rating_without_logging(*args) end alias_method :calculate_rating_without_logging, :calculate_rating alias_method :calculate_rating, :calculate_rating_with_logging endBy using aliases to wrap the
#calculate_ratingmethod we’ve unobtrusively added logging. This pattern is popular enough that Active Support extendedModuleto have a methodalias_method_chain(target, feature)which does some meta-programming to specifically look for a method named target_with_feature and constructs the corresponding aliases for you to use. Here’s how to rewrite the above using it:class Video < ActiveRecord::Base def calculate_rating # does some kind of rating calculation end def calculate_rating_with_logging(*args) logger.info "Calulating a rating!" calculate_rating_without_logging(*args) end alias_method_chain :calculate_rating, :logging endThe key to
alias_method_chainis that we’ve written a method according to the expected pattern named correctly:calculate_rating_with_logging– again, going by the method definintion, that’starget_with_featureand making use oftarget_without_featureinside of it.Warning
This pattern used to be popular and we’re covering it here so you know how it works and to use it when you come across it. However, by extending or overriding the functionality of a method using
alias_method_chain, it becomes more difficult down the road to extend.Whenever you see this method being used to override a method, there’s usually a more maintainable way to do the same using inheritance and the
superkeyword. For more on what that’s about, see Yehuda Katz’s post onalias_method_chain.
NilClass
Curious about the following exception?
RuntimeError: Called id for nil, which would mistakenly be 4 -- if you really wanted the id of nil, use object_id
It’s Active Support’s extension of NilClass at
work.
When you see that exception raised, it’s probably
because an object you’re calling #id on is nil,
but you expected it to be an ActiveRecord::Base object. Code like
@video = Video.find_by_title(params[:title]) puts @video.id
could cause this exception to be raised if params[:title] is nil.
These kinds of exceptions are a sign that your code
needs more validation to check if values like params[:title] or the
resulting @video in this case aren’t nil.
This feature is active by default when running in development and test environments, and disabled in production.
Disable it manually by setting
config.whiny_nils = false
in config/application.rb
Integer
Integer#multiple_of?(number)Returns
trueorfalsewhether the integer is evenly divisible bynumber:8.multiple_of?(2) # => true 9.multiple_of?(3) # => true 10.multiple_of?(3) # => false
Float
Float#round(precision = nil)Rounds the
Floatby the given precision:x = 1.337 x.round # => 1 x.round(1) # => 1.3 x.round(2) # => 1.34
Numeric
Active Support augments the Numeric class to naturally have an interface for working with byte-sized
numbers:
1.byte # => 1 2.bytes # => 2 1.kilobyte # => 1024 1.megabyte # => 1048576 1.gigabyte # => 1073741824 1.terabyte # => 1099511627776 1.petabyte # => 1125899906842624 1.exabyte # => 1152921504606846976 1.kilobyte + 2.megabytes + 42.terabytes # => 46179490464768
Hash
Hash#diff(other_hash)Returns a
Hashconsisting of the strict difference between theHashand theother_hash:{1 => 2}.diff(1 => 2) # => {} {1 => 2}.diff(1 => 3) # => {1 => 2} {}.diff(1 => 2) # => {1 => 2} {1 => 2, 3 => 4}.diff(1 => 2) # => {3 => 4}Hash#slice(*keys)Returns a copy of the
Hashwith only the givenkeys. Useful for limiting an options hash to a set of valid keys prior to processing.{:mass => 2, :speed => 4, :deliciosity => 6}.slice(:mass, :speed) # => {:mass=>2, :speed=>4}When limiting based on an
Arrayof keys, splat theArraywhen passing it to#slice:valid_keys = [:mass, :speed] {:mass => 2, :speed => 4, :deliciosity => 6}.slice(*valid_keys) # => {:mass=>2, :speed=>4}Hash#slice!is also available, replacing theHashin-place.Hash#except(*keys)Returns a copy of the
Hashwithout the givenkeys:{:a => 1, :b => 2}.except(:a) # => {:b => 2}If the
HashorHashclass-descendent responds to#convert_key, that method is called on each key prior to determining if the key/value pair should be included in the returned results. (This is how Rails’ hashes with indifferent access work:{:a => 1}.with_indifferent_access.except("a") # => {}Hash#except!also exists, destructively modifying theHashin-place.Hash#extract!(*keys)Returns a
Hashusing the givenkeys, removing those key/value pairs extracted.h = {"rock" => "lobster", "king" => "crimson"} h.extract!("king") # => {"king" => "crimson"} h # => {"rock" => "lobster"}Hash#assert_valid_keys(*valid_keys)Asserts that the
Hash’s keys arevalid_keys, raisingArgumentErrorupon a mismatch or absence.h = {:name => "Edward", :age => 26} h.assert_valid_keys(:name, :age) # => no exception raised h.assert_valid_keys(:name, :hair_colour) # => mismatch; exception raised h.assert_valid_keys(:name) # => absence; exception raisedHash#reverse_merge(other_hash)Merges an
other_hashwith theHash, giving precedent to theHashthis method is called on.{:red => "Swingline"}.reverse_merge(:blue => "Whale") # => {:blue=>"Whale", :red=>"Swingline"} {:red => "Swingline"}.reverse_merge(:red => "Ninja Turtle") # => {:red=>"Swingline"}Hash#reverse_merge!, the in-place, destructive version, is particularly useful for initializing an option hash with default values in a way that reads nicely:def setup(options = {}) options.reverse_merge! :size => 25, :velocity => 10 endThis example
setupmethod sets a default:sizeand:velocityif theoptionshash passed in didn’t set them.Hash#deep_merge(other_hash)Returns a new
Hashproduced from merging theHashthis method is called on, and theother_hash, merged recursively with precedence given toother_hash.hash_1 = { :a => "a", :b => "b", :c => { :c1 => "c1", :c2 => "c2", :c3 => { :d1 => "d1" } } } hash_2 = { :a => 1, :c => { :c1 => 2, :c3 => { :d2 => "d2" } } } hash_1.deep_merge!(hash_2) # => { :a => 1, # :b => "b", # :c => { :c1 => 2, # :c2 => "c2", # :c3 => { :d1 => "d1", # :d2 => "d2" } } }A
Hash#deep_merge!variant also exists, destructively altering theHashin-place.Hash#to_param(namespace = nil)Returns a URL query string using the keys and values of the
Hashthis method is called on:{ :name => 'Edward', :nationality => 'Canadian' }.to_param # => "name=Edward&nationality=Canadian"When an optional namespace is provided, it’s used accordingly:
{ :name => 'Edward', :nationality => 'Canadian' }.to_param('author') # => "author[name]=Edward&author[nationality]=Canadian"Hash#to_queryAn alias for
#to_param.
Array
The standard Enumerable
module in Ruby is extended by Active Support in order to provide instance methods like
the following:
Array#in_groups_of(number, fill_with = nil)Splits up an
Arrayinto groups containingnumberelements, optionally filling in remainder slots withfill_withunless it’sfalse:[1, 2, 3, 4, 5, 6, 7].in_groups_of(3) # => [[1, 2, 3], [4, 5, 6], [7, nil, nil]] [1, 2, 3, 4, 5, 6, 7].in_groups_of(3, ' ') # => [[1, 2, 3], [4, 5, 6], [7, " ", " "]] [1, 2, 3, 4, 5, 6, 7].in_groups_of(3, false) # => [[1, 2, 3], [4, 5, 6], [7]]
Array#in_groups_ofalso conveniently yields each group when passed a block:[1, 2, 3, 4, 5, 6, 7].in_groups_of(3, false) {|group| puts group.size } # => prints "3\n3\n1"Array#in_groups(number, fill_with = nil)Splits up an
Arrayintonumbergroups, optionally filling in remainder slots withfill_withunless it’sfalse:[1, 2, 3, 4, 5, 6, 7].in_groups(3) # => [[1, 2, 3], [4, 5, nil], [6, 7, nil]] [1, 2, 3, 4, 5, 6, 7].in_groups(3, ' ') # => [[1, 2, 3], [4, 5, " "], [6, 7, " "]] [1, 2, 3, 4, 5, 6, 7].in_groups(3, false) # => [[1, 2, 3], [4, 5], [6, 7]]
Array#in_groupsalso conveniently yields each group when passed a block:[1, 2, 3, 4, 5, 6, 7].in_groups(3, false) {|group| puts group.size } # => prints "3\n2\n2"Array#split(value = nil)Splits up an Array into subarrays based on a delimiting
valueor the result of an optional block.[1, 2, 3, 4, 5].split(3) # => [[1, 2], [4, 5]] (1..10).to_a.split { |i| i % 3 == 0 } # => [[1, 2], [4, 5], [7, 8], [10]]Array#sampleReturns a random element from the
Array:[1, 2, 3, 4, 5].sample # => 5
This method is backported from Ruby 1.9
Array#to_paramReturns a
Stringto be used as part of a URL. Produced by calling#to_paramon each element and joining the results with forward-slashes. Used by Action Pack’surl_for:["brave", "new", "world"].to_param # => "brave/new/world"
Array#to_query(key)Returns a URL query string using the given key as the parameter name and the
Arraythis method is called on as the values:['Documenting', 'makes me thirsty'].to_query('thoughts') # => "thoughts%5B%5D=Documenting&thoughts%5B%5D=makes+me+thirsty"Array#uniq_byReturns an
Arrayof unique elements based on criteria in a supplied block.[1, 2, 3, 4].uniq_by { |i| i.odd? } # => [1, 2]Array#uniq_by!is also available, doing the same thing but modifies theArrayin-place.Array#extract_options!Extracts options from a set of arguments, removing and returning the last element if it's a
Hashor an emptyHashif not. Particularly useful when writing a method that takes*argsas arguments, yet still needs to take aHashof options:def awesome(*args) options = args.extract_options! puts "The options are #{options.inspect}" puts "The arguments are #{args.inspect}" end awesome("dogs", "cats", "bears", :ruby_slippers => true) # => prints "The options are {:ruby_slippers=>true}\nThe arguments are [\"dogs\", \"cats\", \"bears\"]"Array.wrap(object)Wraps the
objectin anArray, converting it using#to_aryif the object implements it. Does nothing on an existingArray.Array(:chunky => :bacon) # => [[:chunky, :bacon]] Array.wrap(:chunky => :bacon) # => [{:chunky=>:bacon}]Use this instead of
Array()when you don’t want the implicit#to_acall on the object:Array("lol\ncats") # => ["lol\n", "cats"], in Ruby 1.8 Array.wrap("lol\ncats") # => ["lol\ncats"]Note
Ruby 1.8’s
String#to_auses\nas a default delimiter when transforming itself into anArray.
Range
Range#overlaps?(other)Returns
trueorfalsedepending on if aRangeoverlaps anotherRange.(1..5).overlaps?(4..6) # => true (1..5).overlaps?(7..9) # => false
Range#step(n = 1)Same as Ruby’s own
Range#stepwhich iterates over theRangeinstance, passing each nth element to a passed block, but extended by Active Support to return anArrayof values when invoked without a block:(1..10).step(2) { |x| puts x } # => prints "1\n3\n5\n7\n9"(1..10).step(2) # => [1, 3, 5, 7, 9]
Range#include?Extended to return whether the enumeration of a
Rangeincludes a given element or otherRange:(1..5).include?(1..5) # => true (1..5).include?(2..3) # => true (1..5).include?(2..6) # => false ("a".."f").include?("c") # => true (5..9).include?(11) # => false
Enumerable
The standard Enumerable module in
Ruby is extended by Active Support in order to provide instance methods like the
following:
Enumerable#sum(identity = 0, &block)Sums up a total from the enumerable object’s elements. Optionally takes a block to augment summation.
Usage without a block looks like what you would expect:
[5, 5, 5].sum # => 15
Calling
sumwith a block lets you specify the addition, much like aninjectcall:products.sum { |p| p.price * tax }You can also use a shortcut syntax to have the summation add up the results of a call on each element:
products.sum(&:price) # Adds up all product prices # This is the same as writing: # # products.inject(0) {|sum, p| sum + p.price }You can also specify the value of an empty enumerable collection:
# By default, the sum of an empty list is 0 [].sum # => 0
# Specify the identity value by passing in a value for it: [].sum("Nothing") # => "Nothing"Warning
Whenever the
Enumerable#summethod call behaves oddly or there’s an error associated with it, check to see if the object you’re callingsumon really is anEnumerable, and not aActiveRecord::Baseinstance. The Active Record instance also implementssum, but does so slightly differently, and also calls out to MySQL to perform the summation.Enumerable#index_byConverts an
Enumerableto aHashby iterating over the collection and building key/value pairs using what the block returns as the key, and the current element as the value:["dogs", "cats", "turtles"].index_by {|e| e[0...1] } # => {"c"=>"cats", "d"=>"dogs", "t"=>"turtles"}Author.all.index_by(&:first_name) # => {"Cody"=> <Author ... >, "Edward" => <Author ... >, # "James" => <Author ... >, "John" => <Author ... >}Enumerable#many?(&block)Returns
trueif the collection has more than 1 element:["The Art of Conversation", "On Simplicity"].many? # => true
[1].many? # => false
{}.many? # => falseWhen provided an optional block,
#many?passes iterates through the collection, passing each element to the block and counts how many times the block returnstrue. If it’s more than once,#many?returnstrue:[2, 4, 6].many? {|n| n.even? } # => true[2, 4, 6].many? {|n| n.odd? } # => falseEnumerable#exclude?(object)The negative of
Enumerable#include?– returnstrueif the collection does not includeobject[2, 4, 6].exclude?(1) # => true
[2, 4, 6].exclude?(2) # => false
Enumerable#each_with_object(memo, &block)Iterates over a collection, passing the current element and
memoobject to the block. Handy for reducing a collection of objects down to a single one, like a numerical value. Also useful for transforming a collection into another, like from anArrayto aHash.(1..10).each_with_object([1, 1]) do |i, memo| memo[0] = i + (memo[0] || 1) memo[1] = i * (memo[1] || 1) end # => [56, 3628800]
["Some", "words"].each_with_object({}) do |word, word_counts| word_counts[word] = word.length end # => {"Some"=>4, "words"=>5}Curious about what the difference between this and
Enumerable#injectis? With#each_with_object, you don’t need to worry about what the block returns. With#inject, the block must return the accumulated object, ormemo. Here’s what the last example would look like using#inject:["Some", "words"].inject({}) do |word_counts, word| word_counts[word] = word.length word_counts end # => {"Some"=>4, "words"=>5}By using
#each_with_objectyou save a line of code as well as tend not to fall into head-scratching debugging situations where you see the following because you forgot to return the memo at the end of the block:["Some", "words"].inject({}) do |word_counts, word| word_counts[word] = word.length end # ~> -:3: syntax error, unexpected kEND # ~> ...mp_1277015147_730_23377 = (end);$stderr.puts("!XMP1277015147... # ~> ^ # ~> -:3: syntax error, unexpected $end, expecting ')'Note
The block arguments passed to
#each_with_objectand#injectare swapped.A final note: the memo must be mutable, so you can’t use immutable objects like numbers, true, or false. Your code will not raise any exceptions, but your results will be surprising:
(1..5).each_with_object(1) { |value, memo| memo *= value } # => 1Enumerable#each_with_objectis backported from Ruby 1.9.Enumerable#group_byGroups an enumerable into sets using the resulting value from the provided block. Useful for grouping records by date or another attribute. Here’s an example where we want to group a list of names by their first character:
people = ["Harry", "Tom", "Dick", "Darcy", "Dwemthy", "Harold", "Tim"] people.group_by {|name| name[0...1] } # => #<OrderedHash {"D"=>["Dick", "Darcy", "Dwemthy"], "H"=>["Harry", "Harold"], "T"=>["Tom", "Tim"]}>Note
ActiveSupport::OrderedHashis an implementation of Ruby 1.9’s orderedHashes, backported for Ruby 1.8 users.
Time, Date, and DateTime
The Time, Date, and DateTime classes are heavily extended by
Active Support to provide a better language for describing moments in time, asking if a
moment is in the future or past, doing arithmetic with time, as well as much richer
support for time zones in comparison to what comes out of the box with Ruby.
Time, Date, and DateTime are fairly consistent in terms of their classes & instances’
public methods, so if you see something described here for a Time
object, it likely also works for a Date or DateTime object.
Describing moments in time
Need to express a moment in time 10 minutes ago or 2 days from now? Take a look at this list of examples to get a feel of what’s possible:
# Reads best without an argument 10.minutes.ago # => Thu, 15 Sep 2011 06:14:18 UTC +00:00 # Same, but reads best with an argument 10.minutes.until(Time.now) # => Thu Sep 15 02:14:18 -0400 2011 # Reads best with an argument 2.days.since(Date.yesterday) # => Fri, 16 Sep 2011 # Reads best without an argument 2.days.from_now # => Sat, 17 Sep 2011 06:24:18 UTC +00:00 # Same as Time.now.advance(:months => 4, :years => 5) (4.months + 5.years).from_now # => Sun, 15 Jan 2017 06:24:18 UTC +00:00 # Be careful to only call #from_now on Time-like objects # to ensure calculations keep their precision, so don't do this: 1.month.to_i.from_now # => Sat, 15 Oct 2011 06:24:18 UTC +00:00 t = Time.now # => Thu Sep 15 02:24:18 -0400 2011 # Time#since(seconds) so the equivalent arithmetic # would be Time.now + 10.seconds t.since(10) # => Thu Sep 15 02:24:28 -0400 2011 # Same t.in(10) # => Thu Sep 15 02:24:28 -0400 2011 # Time.now - 9.months t.months_ago(9) # => Wed Dec 15 02:24:18 -0500 2010 # Time.now + 9.months t.months_since(9) # => Fri Jun 15 02:24:18 -0400 2012 # Time.now - 18.years t.years_ago(18) # => Wed Sep 15 02:24:18 -0400 1993 # Time.now + 18.years t.years_since(18) # => Sat Sep 15 02:24:18 -0400 2029 # Same as Time#years_ago(1) t.prev_year # => Wed Sep 15 02:24:18 -0400 2010 t.next_year # => Sat Sep 15 02:24:18 -0400 2012 t.prev_month # => Mon Aug 15 02:24:18 -0400 2011 t.next_month # => Sat Oct 15 02:24:18 -0400 2011 # Refers to the start of this week, Monday at 0:00 t.beginning_of_week # => Mon Sep 12 00:00:00 -0400 2011 t.at_beginning_of_week # => Mon Sep 12 00:00:00 -0400 2011 t.monday # => Mon Sep 12 00:00:00 -0400 2011 t.end_of_week # => Sun Sep 18 23:59:59 -0400 2011 t.at_end_of_week # => Sun Sep 18 23:59:59 -0400 2011 # Returns a new Time object in the next week. t.next_week # => Mon Sep 19 00:00:00 -0400 2011 # Great for referring to moments in time like "next Tuesday" t.next_week(:tuesday) # => Tue Sep 20 00:00:00 -0400 2011 t.beginning_of_day # => Thu Sep 15 00:00:00 -0400 2011 t.midnight # => Thu Sep 15 00:00:00 -0400 2011 t.at_midnight # => Thu Sep 15 00:00:00 -0400 2011 t.at_beginning_of_day # => Thu Sep 15 00:00:00 -0400 2011 # Last moment of the day - 23:59:59.999999 t.end_of_day # => Thu Sep 15 23:59:59 -0400 2011 # First moment of the month t.beginning_of_month # => Thu Sep 01 00:00:00 -0400 2011 # Last moment of the month t.end_of_month # => Fri Sep 30 23:59:59 -0400 2011 # Start of the quarter (1st of January, April, July, October at 0:00) t.beginning_of_quarter # => Fri Jul 01 00:00:00 -0400 2011 t.at_beginning_of_quarter # => Fri Jul 01 00:00:00 -0400 2011 t.beginning_of_year # => Sat Jan 01 00:00:00 -0500 2011 t.end_of_year # => Sat Dec 31 23:59:59 -0500 2011 t.yesterday # => Wed Sep 14 02:24:18 -0400 2011 t.tomorrow # => Fri Sep 16 02:24:18 -0400 2011 Time.now.seconds_since_midnight # => 8658.4142 DateTime.now.seconds_since_midnight # => 8658
Direct time duration manipulation
Need to set a specific date or time or advance an existing one? There’s more than one way to do it. Here’s a few examples:
t = Time.now # => Thu Sep 15 02:24:18 -0400 2011
t.change(:utc => true, :year => 1, :month => 2, :day => 3, :hour => 4,
:min => 5, :sec => 6, :usec => 7)
# => Thu, 03 Feb 0001 04:05:06 -0500
d = Date.today # => Thu, 15 Sep 2011
d.change(:year => 1, :month => 2, :day => 3) # => Thu, 03 Feb 0001
d = DateTime.now # => Thu, 15 Sep 2011 02:24:18 -0400
d.change(:year => 1, :month => 2, :day => 3, :hour => 4,
:min => 5, :sec => 6, :offset => 7, :start => DateTime.yesterday)
# => Sat, 03 Feb 0001 04:05:06 +16800
# Also works on Date and DateTime
t = Time.now # => Thu Sep 15 02:24:18 -0400 2011
t.advance(:years => 1, :months => 2, :weeks => 3, :days => 4, :hours => 5,
:minutes => 6, :seconds => 7)
# => Mon Dec 10 07:30:25 -0500 2012Note
#advance has differently named options than
#change
Is it in the future? Is it today? Is it in the past?
Determining if a date is in the past, present, or future is easy:
1.day.ago.past? # => true 1.day.from_now.past? # => false Date.today.today? # => true Date.yesterday.today? # => false Date.tomorrow.future? # => true Date.yesterday.future? # => false
Time Arithmetic
As you’ve already seen, it’s easy to perform arithmetic on time-like objects. Here are some more examples of what you can do:
t = Time.now # => Wed Jul 14 00:03:22 -0400 2010
t + 1.day + 2.weeks + 3.months + 4.years
# => Wed Oct 29 00:03:22 -0400 2014
t + 1.seconds + 2.minutes + 3.hours # => Wed Jul 14 03:05:23 -0400 2010
t + 1.fortnight # => Wed Jul 28 00:03:22 -0400 2010
# Timezone arithmetic is internally handled; just pick a starting timezone...
Time.zone = "Hawaii"
t1 = Time.zone.now # => Tue, 13 Jul 2010 18:03:22 HST -10:00
# ... and parse the incoming string in whatever zone it's in
t2 = Time.zone.parse('2010-05-05 15:30:45 -0300')
# => Wed, 05 May 2010 08:30:45 HST -10:00
# Addition and subtraction work, even with different timezones
# (Subtraction gives you how many seconds elapsed between the two moments)
t2 - t1 # => -5995957.11177897
# Comparisons work as expected between the various time-related classes
t1 = Time.now # => Wed Jul 14 00:03:22 -0400 2010
t2 = Date.tomorrow # => Thu, 15 Jul 2010
t3 = DateTime.now - 1.day # => Tue, 13 Jul 2010 00:03:22 -0400
t1 < t2 # => true
t2 > t3 # => true
t3 > t1 # => falseTime Zone Conversions
Active Support adds a TimeZone class that wraps around
TZInfo::Timezone instances that makes it easy to set a
current time zone, retrieve the current timezone, and have it all transparently work
with existing and newly created time objects.
Set a time zone in config/application.rb like
so:
Rails::Initializer.run do |config| config.time_zone = "Eastern Time (US & Canada)" end
This default time zone is accessible from within your application
via Time.zone. Likewise, to set a different time zone later on in
your application, do
Time.zone = "Eastern Time (US & Canada)"
For a full set of available time zones, check ActiveSupport::TimeZone::MAPPING.keys in a console.
In order to create Time objects that use a set time zone,
you need to use the following methods:
Time.zone.local(year, month=1, day=1, hour=0, min=0, sec=0, usec=0)Returns a
TimeWithZoneobject with the time zone fromTime.zoneset.Note that like other methods with similar arguments, if the requested time is not between 1970 and 2038 (or 1902 and 2038, depending on system architecture), a
DateTimeinstance will be returned instead.Time.zone.parse(time, now = Time.now)Parses time string using
Time.parseand returns aTimeWithZoneobject, setting the time zone usingTime.zone.Time.zone.at(seconds_since_unix_epoch)Returns a
TimeWithZoneobject representing a moment in time a given amount of seconds since the Unix epoch (the year 1970). Time zone set usingTime.zone.
The reason you need to use these methods is because they return
TimeWithZone objects; a new Time-like class that Active Support provides that represents a time in
any time zone. Since standard Ruby Time instances can only
handle UTC and the system’s local time zone, it’s important to
use the above methods when setting your own time zone within the application.
Here are some more examples of it in action:
Time.zone = 'Eastern Time (US & Canada)'
Time.zone.local(2007, 2, 10, 15, 30, 45) # => Sat, 10 Feb 2007 15:30:45 EST -05:00
Time.zone.parse('2007-02-01 15:30:45') # => Thu, 01 Feb 2007 15:30:45 EST -05:00
Time.zone.at(1170361845) # => Thu, 01 Feb 2007 15:30:45 EST -05:00
Time.zone.now # => Tue, 13 Jul 2010 01:20:55 EDT -04:00
# Representing the time in another time zone is easy
t = Time.utc(2007, 2, 10, 20, 30, 45) # => Sat Feb 10 20:30:45 UTC 2007
t.in_time_zone # => Sat, 10 Feb 2007 15:30:45 EST -05:00
# Return the simultaneous time in Time.zone or the specified zone
Time.now.in_time_zone # => Tue, 13 Jul 2010 01:20:55 EDT -04:00
Time.now.in_time_zone(Time.zone) # => Tue, 13 Jul 2010 01:20:55 EDT -04:00
Time.now.in_time_zone("Asia/Vladivostok") # => Tue, 13 Jul 2010 16:20:55 VLAST +11:00
Time.now.in_time_zone(-3.hours) # => Tue, 13 Jul 2010 02:20:55 BRT -03:00
# Switch to a given timezone within a block
Time.use_zone "Asia/Vladivostok" do
Time.zone.now # => Tue, 13 Jul 2010 16:20:55 VLAST +11:00
endTime and TimeZone
objects also come with methods for determining the answers to common
time-zone-related questions:
Time.zone = "Eastern Time (US & Canada)" t = Time.zone.now # Is it daylight savings time? t.dst? # => true # UTC offset in seconds t.utc_offset # => -14400 t.zone # => "EDT" # Format representation in RFC-822 standard t.to_s(:rfc822) # => "Thu, 15 Sep 2011 02:24:18 -0400"
Formatting Time-like objects
Time, Date, and DateTime instances all implement a
method #to_formatted_s, aliased to #to_s which
converts the date into a string of a given format using a given #strtime string or Proc. Table 13.1, “Built-in Time, Date,
DateTime#to_s formats” lists the built-in conversion formats.
Table 13.1. Built-in Time, Date,
DateTime#to_s formats
| Built-in Conversion Formats | Internal Format/Implementation | Example Result |
|---|---|---|
to_s | "Thu Jan 18 06:10:17 CST 2007" | |
to_s(:time) | "%H:%M" | "06:10:17" |
to_s(:db) | "%Y-%m-%d %H:%M:%S" | "2007-01-18 06:10:17" |
to_s(:number) | "%Y%m%d%H%M%S" | "20070118061017" |
to_s(:short) | "%d %b %H:%M" | "18 Jan 06:10" |
to_s(:long) | "%B %d, %Y %H:%M" | "January 18, 2007 06:10" |
to_s(:long_ordinal) | lambda { |time| time.strftime("%B
#{ActiveSupport::Inflector.ordinalize(time.day)}, %Y %H:%M")
} | "January 18th, 2007 06:10" |
to_s(:rfc822) | lambda { |time| time.strftime("%a, %d %b %Y %H:%M:%S
#{time.formatted_offset(false)}") } | "Thu, 18 Jan 2007 06:10:17 -0600" |
The list of formats themselves are held in a Hash in
Time::DATE_FORMATS. Note that this Hash for formats is used for instances of Date
and DateTime classes as well.
To add a format, the cleanest way is to create a
file config/initializers/time_formats.rb in which formats are
added to Time::DATE_FORMATS like so:
Time::DATE_FORMATS[:month_and_year] = "%B %Y"
Time::DATE_FORMATS[:short_ordinal] = lambda { |time| time.strftime("%B #{time.day.ordinalize}") }Days in a month
It’s easy to find out how many days are in the current month:
# Time.days_in_month(month, year = now.year) Time.days_in_month(1) # => 31
MessageVerifier
ActiveSupport::MessageVerifier is
used to sign and verify messages to prevent tampering. It’s great for situations where
you need to confirm that a request really did originate from your own site, and that the
attributes in the URI haven’t been messed with. Here’s an example of
MessageVerifier in action:
secret = "a secret, complex and long string" message = 'Some important message' verifier = ActiveSupport::MessageVerifier.new(secret) signature = verifier.generate(message) verifier.verify(signature) # => "Some important message" verifier.verify(signature + 'tampered') # => "raises ActiveSupport::MessageVerifier::InvalidSignature"
A usecase for MessageVerifier is
this common situation: you’re writing an auto-unsubscribe feature and need to include a
link in an email that unsubscribes the user from a given newsletter. The link must
verifiably originate from the app itself, so we use MessageVerifier:
# Defined somewhere in scope @verifier ||= ActiveSupport::MessageVerifier.new(Rails.application.config.secret_token) # Elsewhere in a mailer helper def unsubscribe_link signature = @verifier.generate(@user.id, @newsletter.id) url_for(:controller => 'newsletters', :action => 'unsubscribe', :signature => signature) end # In NewslettersController def unsubscribe user_id, newsletter_id = @verifier.verify(params[:signature]) # [...] rescue ActiveSupport::MessageVerifier::InvalidSignature # Respond appropriately # [...] end
MessageVerifier signs and verifies messages by
using a Hash-based Message Authentication Code (HMAC) which works by hashing together the data and your Rails application’s
secret token using the SHA1 digest algorithm by default. MessageVerifier also supports other digest algorithms; see Action
Controller’s Cookie Store’s options for :digest for what’s
possible.
MessageVerifier is a good
fit for any similar situation where the session store isn’t suitable or available, like
Rails’ own cookie signing and store, which is where it’s used internally.
Warning
Just because the message’s content has been verified to originate from where you think it does, doesn’t mean that the message is encrypted.
To prevent others from being able to
read its contents, see MessageEncryptor in the next
section.
One final note about MessageVerifier: the signed
message’s data is a base-64-encoded Marshal dump. Because Ruby’s
marshalling library can only reconstitute data that’s been dumped by a version of Ruby
with the same major and minor version numbers, be careful when upgrading your Ruby
interpreter as it could break all existing messages signed with MessageVerifier.
MessageEncryptor
ActiveSupport::MessageEncryptor
does what you expect: it makes it easy to encrypt content so you can store sensitive
data in untrusted places. Here’s how to use it in its most basic form:
secret = "a secret, complex and long string"
encryptor = ActiveSupport::MessageEncryptor.new(secret)
ciphertext = encryptor.encrypt("some message")
encryptor.decrypt(ciphertext) # => "some message"
Here’s how to use it to store a phone number in a cookie, a place that’s easily read by other users on a shared machine:
encryptor = ActiveSupport::MessageEncryptor.new(Rails.application.config.secret_token) cookie[:phone_number] = encryptor.encrypt(@user.phone_number) # (later) encryptor = ActiveSupport::MessageEncryptor.new(Rails.application.config.secret_token) phone_number = encryptor.decrypt(cookie[:phone_number])
MessageEncryptor also features MessageEncryptor#encrypt_and_sign and MessageEncryptor#decrypt_and_verify which do exactly what you expect,
using MessageVerifier in tandem and in that order.
MessageEncryptor uses the
AES-256-CBC cipher by default, but other ciphers are available, like BF, DES, RC2, RC4,
and RC5. Longer AES keylengths are also available:
ActiveSupport::MessageEncryptor.new('sekrit', 'aes-192-cbc')
ActiveSupport::MessageEncryptor.new('sekrit', 'aes-128-cbc')To list all available ciphers, see
OpenSSL::Cipher.ciphers
SecureRandom
ActiveSupport::SecureRandom
generates secure random numbers for things like session keys in HTTP
cookies which Rails does internally when automatically handling the session for you.
Uses OpenSSL’s random number generator by default. If not available, /dev/urandom or a CryptGenRandom Win32 API call is
used, depending on the platform.
SecureRandom.hex(n = nil)Generates a random hex
Stringof length 2n:SecureRandom.hex(10) # => "c0a0e3b1c8f70c9fb2e9"
If
nisnil, 16 is assumed.Raises
NotImplementedErrorwhen a secure random number generator is not available.SecureRandom.base64(n = nil)Genrates a random base64
Stringof length ≈ 4/3n:# Random 10-character base64 string SecureRandom.base64(10) # => "vA1N9RKHBD5n8Q=="
If
nisnil, 16 is assumed.Raises
NotImplementedErrorwhen a secure random number generator is not available.SecureRandom.random_number(n = 0)Generates a random integer between 0 (inclusive) and
nwhenn> 0:SecureRandom.random_number(20) # => 13
Generates a random float between 0.0 (inclusive) and 1.0 when
nis 0 ornil:SecureRandom.random_number # => 0.405922930890556
SecureRandom.random_bytes(n = nil)Generates a random binary
Stringof lengthn:SecureRandom.random_bytes(10) # => "\f\205\206M@\245\362\217\027\212"
Inflections
Active Support extends the String
class to provide some of the functionality Active Record needs when figuring out which
database tables map to which models by singularizing or pluralizing words and names.
Here’s an example of some of the methods at work:
"person".pluralize # => "people" "octopus".pluralize # => "octopi" "animals".singularize # => "animal" "videos of things".titleize # => "Videos Of Things" "lol_cat".camelize # => "LolCat" "LolCat".underscore # => "lol_cat" "lol_cat".dasherize # => "lol-cat" # Capitalizes the first word, turns underscores into spaces, and strips '_id' "video_id".humanize # => "Video"
To add an inflection the built-in rules don’t cover, take a look at
config/initializers/inflections.rb and follow the comments
there, which look like this:
# Add new inflection rules using the following format # (all these examples are active by default): ActiveSupport::Inflector.inflections do |inflect| inflect.plural /^(ox)$/i, '\1en' inflect.singular /^(ox)en/i, '\1' inflect.irregular 'person', 'people' inflect.uncountable %w( fish sheep ) end
The Array and Integer classes are also extended to provide methods like Array#to_sentence and Integer#ordinalize which you can
use to ease making pretty output:
["dogs", "cats"].to_sentence # => "dogs and cats" ["dogs", "cats"].to_sentence(:two_words_connector => ' & ') # => "dogs & cats" ["planes", "trains", "automobiles"].to_sentence(:words_connector => '; ') # => "planes; trains, and automobiles" ["planes", "trains", "automobiles", "John Candy"].to_sentence(:last_word_connector => ", and the inimitable ") # => "planes, trains, automobiles, and the inimitable John Candy" 1002.ordinalize # => "1002nd"
There are even more methods like these available under
ActiveSupport::Inflector – see the official documentation for
more.
StringInquirer
Provides a nicer way to test for string-equality.
To determine if you’re running in the production environment, one way to do it is:
Rails.env == "production"
However, you can also be a little more elegant and write:
Rails.env.production?
The above comes to you via StringInquirer – it
wraps any string, does some method_missing magic and lets you
write the above. Here’s an implementation example:
class Video
def initialize(title)
@title = title
end
def title
ActiveSupport::StringInquirer.new(@title)
end
end
v = Video.new("awesome")
v.title.radical? # => false
v.title.awesome? # => trueMultibyte
Multibyte backports multibyte-aware String operations for Ruby
1.8. Let’s take a look at what that means:
# Tell Ruby that we want to work with UTF8-encoded strings $KCODE = 'UTF8' s = 'Québec' # Grabbing the first 3 chars ends up grabbing the first 2 and half puts s[0..2] # => "Qu�" # This is one character too long puts s.length # => 7 # Nice work multibyte Strings. Nice work. puts s.reverse # => ceb��uQ
What’s happening here is that Ruby is getting really confused trying
to deal with the “é” being represented with two bytes internally. In order to work with
these sorts of String operations (#slice, #reverse, #length, and friends), Active Support provides a proxy accessible through
#mb_chars that figures it out for you:
s = 'Québec' mb_s = s.mb_chars puts mb_s[0..2] # => Qué puts mb_s.length # => 6 puts mb_s.reverse # => cebéuQ
This proxy object is interchangeable for String except when performing
explicit class-comparison – in that case, just call #to_s to
get back a String:
s = '' s.mb_chars.class == String # => false s.mb_chars.to_s.class == String # => true
Usage of ActiveSupport::Multibyte is
unnecessary when working with Ruby 1.9 as its String class is
already encoding-aware.
Base64
Encodes and decodes Strings of data in base-64 representation.
ActiveSupport::Base64.encode64(data)Encodes a
Stringdatato its base-64 representation. Each 60 characters of output is separated by a newline (\n) character.ActiveSupport::Base64.encode64("Original unencoded string") # => "T3JpZ2luYWwgdW5lbmNvZGVkIHN0cmluZw==\n"ActiveSupport::Base64.decode64(data)Decodes a base-64 encoded
Stringdatato its original representation.ActiveSupport::Base64.decode64("T3JpZ2luYWwgdW5lbmNvZGVkIHN0cmluZw==") # => "Original unencoded string"ActiveSupport::Base64.encode64s(data)Encodes the
Stringdatato its base-64 representation. Does not separate output with newlines. Useful for encoding URI parameters or memcache keys.ActiveSupport::Base64.encode64s("Original unencoded string") # => "T3JpZ2luYWwgdW5lbmNvZGVkIHN0cmluZw=="
Gzip compression & decompression
Compress and decompress Gzip-ed Strings using these convenience methods. Internally mplemented using
the Ruby standard library’s Zlib class.
ActiveSupport::Gzip.compress(source)Compresses a Gzip-ed
Stringsource.ActiveSupport::Gzip.compress("Nutshell") # => "\037\213\b\000\315\330\002L\000\003\363+-)\316H\315\311\001\000\r\372f\364\b\000\000\000"ActiveSupport::Gzip.decompress(source)Decompresses a Gzip-ed
Stringsource.ActiveSupport::Gzip.decompress("\037\213\b\000\315\330\002L\000\003\363+-)\316H\315\311\001\000\r\372f\364\b\000\000\000") # => "Nutshell"
Callbacks
The callback behaviour present in Active Record objects is available to be mixed in to your own non-Active Record classes. If you wanted to trigger some side-effect code upon a method firing, callbacks are a convenient way of accomplishing that.
Here’s an example of using callbacks:
class Vehicle
include ActiveSupport::Callbacks
define_callbacks :ride
set_callback :ride, :before, :check_the_weather
def check_the_weather
puts "Ok! Looks like it's not going to rain."
end
end
class Bicycle < Vehicle
set_callback :ride, :before, :put_on_helmet
def put_on_helmet
puts "I'm putting on my helmet. Safety first!"
end
set_callback :ride, :after do |object|
puts "Locking up my bike."
end
def ride
run_callbacks :ride do
puts "Whooo! Riding bikes is awesome!"
end
end
end
bike = Bicycle.new
bike.ride
# Outputs:
# Ok! Looks like it's not going to rain.
# I'm putting on my helmet. Safety first!
# Whooo! Riding bikes is awesome!
# Locking up my bike.Callbacks are inherited – note how the check_for_weather callback is called as a side-effect of bike.ride.
The most commonly used methods for callbacks are as follows:
ActiveSupport::Callbacks::Callback.define_callbacks(*callbacks)Defines callback types. Example usage:
define_callbacks :ride, :terminator => "result == false" define_callbacks :glide, :rescuable => false
The
:terminatoroption specifies when a “before filter” should halt the callback chain. Here, we specify that if anyridecallback returnsfalse, halt the chain. By default, this is what it does.The
:rescuableoption specifies whether or not “after filters” execute if the given block or a “before filter” raises an exception. By default, this is set tofalse.ActiveSupport::Callbacks::Callback.set_callback(name, *filter_list, &block)Sets callbacks. See the previous example for usage, as well as the following on how to use the various filter list options:
set_callback :ride, :before, :check_the_weather set_callback :ride, :after, :check_the_weather, :if => weather_is_a_big_deal? set_callback :ride, :after, :check_the_weather, :unless => "weather == 'nice'" set_callback :save, :around, lambda { |r| code_before; yield; code_after }The conditions provided to the
:ifand:unlesskeys work the same way as they do for Active Record callback conditions: anArrayof multiple conditions, the name of a method as aSymbol, aStringwhich is eval-ed, aProcto call with the object having its callback executed, or anObjectthat defines theafter_ridemethod (for example).ActiveSupport::Callbacks::Callback.skip_callback(name, *filter_list, &block)Callbacks may be skipped in child classes by using this method. Example usage:
class Unicycle < Vehicle # [...] skip_callback :ride # [...] end
ActiveSupport::Callbacks.run_callbacks(kind, *args, &block)Runs a given callback. See the
Bicycle#ridemethod in the example at the start of this Callbacks section.
Cache
ActiveSupport::Cache is an
abstract cache store; essentially a big Hash used to stash
already-computed results in the form of serialized Ruby objects.
All cache keys are stored as case-sensitive strings, derived by calling #cache_key on the key object. If #cache_key
is not defined, the result of #to_param is used instead. If an
Array is used as a key, its elements are joined by slashes to
form a String. If a Hash is used as a
cache key, the Hash’s keys will be sorted, then joined by slashes
to form a String.
Example usage of the cache store looks like this:
cache = Rails.cache # initialized with a cache store instance during Rails boot
cache.read("author") # => nil
cache.write("author", "Edward")
cache.read("author") # => "Edward"If your cache occupies shared infrastructure, you can set a namespace to prefix all cache keys:
cache.namespace = 'some-prefix'
cache.read("author") # Reads value for key "some-prefix:author"Prefixes can also take the shape of a Proc,
evaluated each time a key is determined so that application logic can be used to
invalidate keys:
# Cache is invalidated each time @last_mod_time is updated
@last_mod_time = Time.now
cache.namespace = lambda { @last_mod_time }Namespaces may also be specified on a per-call basis.
Auto-expiring content after a given amount of time in seconds is also supported both at a global and per-call level:
cache = ActiveSupport::Cache::MemoryStore.new(:expires_in => 5.minutes) cache.write(key, value, :expires_in => 1.minute) cache.fetch(key, :expires_in => 1.minute) do "replacement value" end
MemCacheStorage supports Gzip compression to
reduce data transmission time and storage space. Activate it at initialization time or
on #write and #fetch
calls:
cache = ActiveSupport::Cache::MemCacheStorage.new(:compress => true) cache.write(key, value, :compress => true) cache.fetch(key, :compress => true) do "replacement value" end
Because compression produces negligible or detrimental results with
small amounts of data, a default threshold of 16KB is set by default. To change it, set
it in the initializer or on #write or #fetch calls:
cache = ActiveSupport::Cache::MemCacheStorage.new(:compress => true, :compress_threshold => 64.kilobytes) cache.write(key, value, :compress_threshold => 32.kilobytes) cache.fetch(key, :compress_threshold => 32.kilobytes) do "replacement value" end
Commonly Used Methods
ActiveSupport::Cache
offers an extensive and flexible API that goes beyond what’s
covered here. Here’s a listing of the methods you will most likely
need:
Cache#fetch(key, options = {}, &block)Returns data from the cache using
key.cache = ActiveSupport::Cache::MemoryStore.new cache.write("city", "Ottawa") cache.fetch("city") # => "Ottawa"In the event of a cache-miss (i.e. nothing found for that key), does one of two things:
When called without a block, returns
nil.cache.fetch("food") # => nilWhen called with a block, the block is executed. Its return value is written to the cache using the request key and is also returned by
fetch.cache.fetch("food") do "Gazpacho" end # => "Gazpacho" cache.fetch("food") # => "Gazpacho"
Passing
:force => trueas an option will force a cache miss.Supports
:compress,:compress_thresholdoptions when using a cache store implementation likeMemCacheStorethat supports compression.Supports
:expires_infor per-call expiration date setting.Note that when using
:expires_in, it’s a good idea to take a look at the:race_condition_ttloption, as it prevents race conditions that may occur during heavy load. Think about this situation:require "active_support/cache" cache = ActiveSupport::Cache::MemoryStore.new(:expires_in => 2.seconds) cache.write("foo", "original value") val_1 = nil val_2 = nil # Wait till the cache value has expired sleep 2 threads = [] # Start a thread that will read the cache first, but take # a little while to generate the replacement value threads << Thread.new do val_1 = cache.fetch("foo") do sleep 1 "new value 1" end end # Start a second thread that will read the cache second, # but finish first threads << Thread.new do val_2 = cache.fetch("foo") do "new value 2" end end threads.each(&:join) val_1 # => "new value 1" val_2 # => "new value 2" cache.fetch("foo") # => "new value 1"This example demonstrates a situation where multiple threads are competing to fetch and write to a single cache entry may reveal surprising and inconsistent results from
fetchcalls. In a production environment with greater concurrency, this problem only gets worse.A solution to this race condition situation is to introduce a time-to-live (TTL) timespan by which a
fetchcall can immediately advance the entry’s expiration date:cache = ActiveSupport::Cache::MemoryStore.new(:expires_in => 2.seconds) cache.write("foo", "original value") val_1 = nil val_2 = nil sleep 2 threads = [] threads << Thread.new do # If a cache entry has been expired for less than 10 seconds, # reset the entry to expire 10 seconds in the future val_1 = cache.fetch("foo", :race_condition_ttl => 10.seconds) do sleep 1 "new value 1" end end threads << Thread.new do val_2 = cache.fetch("foo", :race_condition_ttl => 10.seconds) do "new value 2" end end threads.each(&:join) val_1 # => "new value 1" val_2 # => "original value" cache.fetch("foo") # => "new value 1"By having the first thread to fetch an expired key advance its expiration timestamp, other threads asking for the entry will predictably get back the now-no-longer-expired value while the first thread generates the replacement value.
Note that if the TTL values are too short, and the first thread exceeds twice the TTL timespan while generating the replacement value, your inconsistency problems will return. For this reason, it’s important you measure carefully.
When using
MemCacheStore, supports:raw => true.Cache#read(key, options = nil)Returns data for
keyin the cache.Returns
nilin the event of a cache-miss.Cache#read_multi(*names)Returns a
Hashof multiple values from the cache in one call. Options may be passed in as the last argumentCache#write(key, value, options = nil)Writes data to the cache for the given
key.Cache#delete(key, options = nil)Deletes data in the cache for the given
key.Cache#exist?(key, options = nil)Returns
trueorfalseifkeyexists in the cache or not.
The following methods are only available on some cache storage implementations:
Cache#delete_matched(matcher, options = nil)Deletes all data in the cache with keys matching the regular expression
matcher. Implemented byFileStore, andMemoryStore.Cache#increment(key, amount = 1)Adds
amountto value forkeyin the cache. Implemented byFileStore,MemCacheStore, andMemoryStore. (Note: can only be used on values written with the:rawoption – calling it on others will set the value to 0.)Cache#decrement(key, amount = 1)Subtracts
amountfrom value forkeyin the cache. Implemented byFileStore,MemCacheStore, andMemoryStore. (Note: can only be used on values written with the:rawoption – calling it on others will set the value to 0.)Cache#cleanup(options = nil)Removes expired entries. Implemented by
FileStore, andMemoryStore.Cache#clear(options = nil)Clears the entire cache store. Implemented by
FileStore,MemCacheStore, andMemoryStore.
Storage Implementations
ActiveSupport::Cache
uses of one of the following storage implementations as its backend. See the the section called “Active Support Cache Store” for more on configuring your Rails
application to use one of the following backends.
Cache::MemCacheStorageStores content in Memcached. This is the recommended choice for production environments.
Supports clustering and load balancing across multiple memcached servers. See the section called “Memcached” for more on setup.
Supports the
:expires_inoption on#writewhich returnsnilfor cached content older than the expiry period.Also supports an option for automatic Gzip-compression of values:
ActiveSupport::Cache::MemCacheStore(:compress => true)
Also supports the
:rawoption to store values as strings instead of serialized objects. This option must be active for entries you want to use#incrementand#decrementon.Cache::FileStoreStores content on the filesystem.
Supports the
:expires_inoption on#readwhich returnsnilfor cached content older than the expiry period.Cache::MemoryStoreStores content in memory in the same process as the executing Rails application, meaning that if you’re running a cluster of application servers (which you probably will), you won’t be able to share a cache between them. If you’re using generational cache keys and never manually expiring your cache, this might work for you. Consider another option otherwise.
Able to store arbitrary Ruby objects as well as strings (other stores can’t do this).
Note that
MemoryStorein Rails 3 is thread-safe, unlike earlier versions.
Note that like MessageVerifier, the data
Cache stores is marshal-dumped, and sensitive to the Ruby
implementation used to do the marshalling. Be careful of data portability issues
when planning migrations that involve changing your version of Ruby.
Memoizable
Memoization is the practice of caching the results of a calculation or method call to avoid re-computing the calculation/method call. You’ve probably done this already without knowing what the pattern is called when writing code like
def some_long_running_computation @computation_result ||= factorial(1000) end
whereupon calling some_long_running_computation
will result in only one actual calculation internally, and subsequent calls will use the
cached value. (Note however that if your internal computation returns nil or false, the above will compute every time.)
ActiveSupport::Memoizable
provides this kind of functionality in a declarative fashion, taking into account the
nil issue:
class MathematicalThing
extend ActiveSupport::Memoizable # Mix-in memoization with this extend
def some_long_running_computation(n)
factorial(n)
end
memoize :some_long_running_computation # Declare this method to be memoized
end
m = MathematicalThing.new
m.some_long_running_computation(100) # First call takes a long time
m.some_long_running_computation(100) # Second call takes no time; uses memoized resultTo clear the memoized cache for a single method, pass true or :reload as the last argument when calling that
method:
m = MathematicalThing.new m.some_long_running_computation(100) # Nothing cached; computation occurs m.some_long_running_computation(100) # Cache hit; computation skipped m.some_long_running_computation(100, :reload) # Cache cleared; computation occurs m.some_long_running_computation(100) # Cache hit; computation skipped
When memoizing methods that take no arguments, pass in true or :reload as an argument to clear the cache and
force computation:
class SomeThing
extend ActiveSupport::Memoizable
def long_running_computation
factorial(1000)
end
memoize :long_running_computation
end
s = SomeThing.new
s.long_running_computation # Nothing cached; computation occurs
s.long_running_computation # Cache hit; computation skipped
s.long_running_computation(:reload) # Cache cleared; computation forcedActiveSupport::Memoizable handles
instance-wide cache invalidation via the following methods:
Memoizable#flush_cache(:some_memoized_method_name)Expire just one memoized method’s cache:
thing = SomeOtherThing.new thing.some_long_running_computation # Nothing cached; computation occurs thing.some_long_running_computation # Cache hit; computation skipped thing.flush_cache(:some_long_running_computation) # Flush just this method's cache thing.some_long_running_computation # Nothing cached; computation occursMemoizable#unmemoize_allExpires the cache for all memoized methods in the instance:
class SomeOtherThing extend ActiveSupport::Memoizable def long_running_computation factorial(1000) end memoize :long_running_computation def other_long_running_computation sleep(42) factorial(100) end memoize :other_long_running_computation end thing = SomeOtherThing.new thing.some_long_running_computation # Nothing cached; computation occurs thing.other_long_running_computation # Nothing cached; computation occurs thing.some_long_running_computation # Cache hit; computation skipped thing.other_long_running_computation # Cache hit; computation skipped thing.unmemoize_all # Clear all memoized methods' cached results thing.some_long_running_computation # Nothing cached; computation occurs thing.other_long_running_computation # Nothing cached; computation occurs
JSON serialization
JSON encoding and decoding is provided as part of Active Support:
require "active_support/json"
{1 => {"Fruit" => ["Bananas", "Strawberries"]}}.to_json # => "{\"1\":{\"Fruit\":[\"Bananas\",\"Strawberries\"]}}"
Date.today.to_json # => "\"2010-05-01\""
JSON.parse("{\"1\":{\"Fruit\":[\"Bananas\",\"Strawberries\"]}}") # => {"1"=>{"Fruit"=>["Bananas", "Strawberries"]}}XML serialization
XML encoding and decoding is provided as part of Active Support:
Video.first.to_xml
# <?xml version="1.0" encoding="UTF-8"?>
# <video>
# <created-at type="datetime">2010-02-16T04:46:28Z</created-at>
# <embed-code><object width="425" [...] </embed-code>
# <id type="integer">1</id>
# <title>First thing I could think of</title>
# <updated-at type="datetime">2010-02-16T04:46:28Z</updated-at>
# </video>
Hash.from_xml(Video.first.to_xml)
# => {"video"=>{"created_at"=>Tue Feb 16 04:46:28 UTC 2010,
# "embed_code"=>"<object width=\"425\" [...] </embed></object>",
# "title"=>"First thing I could think of",
# "updated_at"=>Tue Feb 16 04:46:28 UTC 2010,
# "id"=>1}}These methods play a big part of Active Resource, which uses #to_xml and #from_xml on various classes like
Array, Hash, and ActiveRecord::Base in order to do its magic.
Note
If your application is doing a lot of XML consumption, install the much faster libxml parser by adding:
gem 'libxml-ruby', '>=0.9.7'
in your Gemfile, and
ActiveSupport::XmlMini.backend = 'LibXML'
at the end of config/application.rb
While it is possible to use Hash#to_xml and Array#to_xml to manually form XML, in most cases, it’s more work than it’s worth.
When dealing with another Rails application you need to produce and consume XML for, it’s best to just let Active Resource do the work and stay at a high level.
When dealing with another application having a RESTful API, it’s easiest to use a library/gem like HTTParty to consume XML, and to write a Builder template for producing it.
When dealing with another application that’s not
RESTful, use a library like Nokogiri to parse and consume
XML and write a Builder template for producing it. The built-in
#from_xml methods will cause you headaches if you try using them
in situations like these because they expect nodes to come with metadata like type='integer', but that sort of information usually lives in a schema
which Rails doesn’t use. Fortunately, Nokogiri and Builder are easy to use and make
short work of these scenarios.





View 1 comment




Regarding Multibyte/Unicode operations being missing from this chapter: I haven’t written it yet :P – I’ll update this comment thread when they are.
Add a comment