This is an example of a Helper? that was written to support HowToRenderProxiedPages. It is intended as a ProgrammingOutLoud example to demonstrate 1) the construction of a Helper? and the use of Ruby, as well as to provide a specific solution to a specific problem.
Rendering proxied pages may require that state for the remote session (in the form of HTTP Cookies) be kept by the rails application. Due to a number of complications, mostly arising from the ad-hoc (“it just growed like that”) design of the cookie system, and the relatively modular nature of the task I concluded that this was an ideal canidate for becoming a Helper.
The helper’s sole purpose is to define a class of objects that
Basically, something similar to this java class, but a little more light weight.
The first requirement can be psudo-coded as follows:
class A_cookie_jar
def parse_cookie_from(uri,s)
# parse the string s for a cookie, and store it
# as coming from from uri
end
def cookies_for(uri)
# search through the stored cookies, and return
# any that apply to the specified URI.
end
end
This is easy to flesh out a bit.
A quick peek at the specification led to the following additional points:
name=value pair followed by some optionsSo now we have:
class A_cookie_jar < Array
def initialize
#define the @jar data structure
end
def parse_cookie_from(uri,s)
# check s for reasonableness; ignore bad input
# find a name/value pair and options in s
# parse (& provide defaults) for options such as:
# expires=Friday, 01-Apr-2005 00:00:03 GMT
# path=/joke_ideas/office/index.html
# domain=www.jokers.edu
# secure
# put the cookie in the jar
end
def cookies_for(uri)
# construct an empty result set
# check all the cookies
# add the name/value pair to the result, iff
# * it applies to this uri
# * the cookie is valid
# return the result
end
end
Actually reading the documentaion lets us refine this further. For instance,
secure
If a cookie is marked secure, it will only be transmitted if the communications channel with the host is a secure one. Currently this means that secure cookies will only be sent to HTTPS (HTTP over SSL) servers.
If secure is not specified, a cookie is considered safe to be sent in the clear over unsecured channels.
class A_cookie_jar < Array
def initialize
#define the @jar data structure
end
def parse_cookie_from(uri,s)
# check s for reasonableness; ignore bad input
# split s into a ';'-delimited list consisting of
# a name/value pair and
# a (possibly empty) list of options
# split the name and the value at the '='
# determine default values for all the options
# parse each option, overriding defaults
# as appropriate. Supported options will
# match one of the following patterns
# /expires=.+?, (..-...-.... ..:..:.. GMT)/
# /path=(.*)/
# /domain=(.*)/
# /secure/
# put the cookie in the jar
end
def cookies_for(uri)
# construct an empty result set
# check each domain suffix (longest first)
# check for path prefix (longest first)
# for each cookie in domain-suffix:path-prefix
# add the name/value pair to the result if
# * the cookie hasn't expired and
# * the protocol is acceptable
# return the result
end
end
This is fairly easy to translate into “hopeful Ruby”—by which I mean Ruby with the assumption that various objects support the methods I need and that they are called what I expect and work the way I think they should.
For the data structure, I chose a hash (indexed by domain suffix) of hashes (indexed by path prefix) of arrays of cookies. This looked like a good compromise between the demands of the two functions, and is about as space efficient as you’re likely to get.
To enforce the structure, I create the outermost Hash with a “default proc” to create a new nested Hash whenever a previously unseen key was accessed (e.g., when we get out first cookie for a domain). To keep this from creating an empty structure when we check for cookies from a domain that hasn’t given us any, I put a guard (if @jar.has_key? domain) on the access in cookies_for(uri).
For finding cookies I decided to pretend there was a way to generate all the non-empty prefixes or sufixes of a string (using a specified delimiter) longest first. In other words, I decided to assume that "The price of freedom".prefixes(' ') would give me ["The price of freedom","The price of","The price","The"]. With that it’s easy to implement the search for applicable cookies.
The result is pretty much a line-by-line translation of the PseudoCode? into HopefulRuby:
alpha version
class A_cookie_jar
def initialize
@jar = Hash.new { Hash.new { [] } }
end
def parse_cookie_from(uri,s)
return unless s.is_a? String and s.length > 2
name_value,*options = cookie.split(';')
name,value = name_value.split('=')
acceptable_protocols,domain,path,expires = ['http','https'],uri.host,uri.path,nil
options.each { |option| case option
when /expires=.+?, (..-...-.... ..:..:.. GMT)/
expires = DateTime.parse($1)
when /path=(.*)/
path = $1
when /domain=(.*)/
domain = $1
when /secure/
acceptable_protocols = ['https']
end}
@jar[domain][path] << [name,value,expires, acceptable_protocols]
end
def cookies_for(uri)
result = Hash.new { [] }
uri.host.suffixes('.').each { |domain|
uri.path.prefixes.each { |path|
@jar[domain][path].each { |name,value,expires,acceptable_protocols|
result[name] << value if
DateTime.now < expires and
acceptable_protocols.include? uri.scheme
} if @jar[domain].has_key? path
} if @jar.has_key? domain
}
def result.to_s
keys.collect { |name|
self[name].collect { |value| "#{name}=#{value}" }
}.flatten.join(';')
end
result
end
end
More writeup to follow…
Problems found in testing:
prefixes and suffixes", "
class String
def prefixes(delimiter='/')
segments = split(delimiter)
(1..segments.length).collect { |i| segments[0..-i].join(delimiter) }
end
def suffixes(delimiter='/')
segments = split(delimiter)
(1..segments.length).collect { |i| segments[(i-1)..-1].join(delimiter) }
end
end
class A_cookie_jar
def initialize
@jar = {}
end
def parse_cookie_from(uri,s)
return unless s.is_a? String and s.length > 2
cookies = s.gsub(/, ([^\d])/,';;\1').split(';;')
cookies.each { |cookie|
name_value,*options = cookie.split(';')
name,value = name_value.split('=')
acceptable_protocols,domain,path,expires = ['http','https'],uri.host,uri.path,nil
options.each { |option| case option
when /expires=.+?, (..-...-.. ..:..:..(..)? GMT)/
expires = DateTime.parse($1,:guess_year)
when /path=(.*)/
path = $1
when /domain=(.*)/
domain = $1
when /secure/
acceptable_protocols = ['https']
end}
((@jar[domain] ||= {})[path] ||= []) << [name,value,expires, acceptable_protocols]
}
end
def cookies_for(uri)
result = {}
uri.host.suffixes('.').each { |domain|
uri.path.prefixes.each { |path|
@jar[domain][path].each { |name,value,expires,acceptable_protocols|
(result[name] ||= []) << value if DateTime.now < expires and acceptable_protocols.include? uri.scheme
} if @jar[domain].has_key? path
} if @jar.has_key? domain
}
def result.to_s
keys.collect { |name|
self[name].collect { |value| "#{name}=#{value}" }
}.flatten.join(';')
end
result
end
end
Regular expression for parsing date have error: it should be /expires=.+?, (..-...-..(..)? ..:..:.. GMT)/
In cookies_for method, path prefixes does not include trailing slash so cookie is never received for ‘/’. Quick solution is to add path += '/' for every path. bmihelac
This is an example of a Helper? that was written to support HowToRenderProxiedPages. It is intended as a ProgrammingOutLoud example to demonstrate 1) the construction of a Helper? and the use of Ruby, as well as to provide a specific solution to a specific problem.
Rendering proxied pages may require that state for the remote session (in the form of HTTP Cookies) be kept by the rails application. Due to a number of complications, mostly arising from the ad-hoc (“it just growed like that”) design of the cookie system, and the relatively modular nature of the task I concluded that this was an ideal canidate for becoming a Helper.
The helper’s sole purpose is to define a class of objects that
Basically, something similar to this java class, but a little more light weight.
The first requirement can be psudo-coded as follows:
class A_cookie_jar
def parse_cookie_from(uri,s)
# parse the string s for a cookie, and store it
# as coming from from uri
end
def cookies_for(uri)
# search through the stored cookies, and return
# any that apply to the specified URI.
end
end
This is easy to flesh out a bit.
A quick peek at the specification led to the following additional points:
name=value pair followed by some optionsSo now we have:
class A_cookie_jar < Array
def initialize
#define the @jar data structure
end
def parse_cookie_from(uri,s)
# check s for reasonableness; ignore bad input
# find a name/value pair and options in s
# parse (& provide defaults) for options such as:
# expires=Friday, 01-Apr-2005 00:00:03 GMT
# path=/joke_ideas/office/index.html
# domain=www.jokers.edu
# secure
# put the cookie in the jar
end
def cookies_for(uri)
# construct an empty result set
# check all the cookies
# add the name/value pair to the result, iff
# * it applies to this uri
# * the cookie is valid
# return the result
end
end
Actually reading the documentaion lets us refine this further. For instance,
secure
If a cookie is marked secure, it will only be transmitted if the communications channel with the host is a secure one. Currently this means that secure cookies will only be sent to HTTPS (HTTP over SSL) servers.
If secure is not specified, a cookie is considered safe to be sent in the clear over unsecured channels.
class A_cookie_jar < Array
def initialize
#define the @jar data structure
end
def parse_cookie_from(uri,s)
# check s for reasonableness; ignore bad input
# split s into a ';'-delimited list consisting of
# a name/value pair and
# a (possibly empty) list of options
# split the name and the value at the '='
# determine default values for all the options
# parse each option, overriding defaults
# as appropriate. Supported options will
# match one of the following patterns
# /expires=.+?, (..-...-.... ..:..:.. GMT)/
# /path=(.*)/
# /domain=(.*)/
# /secure/
# put the cookie in the jar
end
def cookies_for(uri)
# construct an empty result set
# check each domain suffix (longest first)
# check for path prefix (longest first)
# for each cookie in domain-suffix:path-prefix
# add the name/value pair to the result if
# * the cookie hasn't expired and
# * the protocol is acceptable
# return the result
end
end
This is fairly easy to translate into “hopeful Ruby”—by which I mean Ruby with the assumption that various objects support the methods I need and that they are called what I expect and work the way I think they should.
For the data structure, I chose a hash (indexed by domain suffix) of hashes (indexed by path prefix) of arrays of cookies. This looked like a good compromise between the demands of the two functions, and is about as space efficient as you’re likely to get.
To enforce the structure, I create the outermost Hash with a “default proc” to create a new nested Hash whenever a previously unseen key was accessed (e.g., when we get out first cookie for a domain). To keep this from creating an empty structure when we check for cookies from a domain that hasn’t given us any, I put a guard (if @jar.has_key? domain) on the access in cookies_for(uri).
For finding cookies I decided to pretend there was a way to generate all the non-empty prefixes or sufixes of a string (using a specified delimiter) longest first. In other words, I decided to assume that "The price of freedom".prefixes(' ') would give me ["The price of freedom","The price of","The price","The"]. With that it’s easy to implement the search for applicable cookies.
The result is pretty much a line-by-line translation of the PseudoCode? into HopefulRuby:
alpha version
class A_cookie_jar
def initialize
@jar = Hash.new { Hash.new { [] } }
end
def parse_cookie_from(uri,s)
return unless s.is_a? String and s.length > 2
name_value,*options = cookie.split(';')
name,value = name_value.split('=')
acceptable_protocols,domain,path,expires = ['http','https'],uri.host,uri.path,nil
options.each { |option| case option
when /expires=.+?, (..-...-.... ..:..:.. GMT)/
expires = DateTime.parse($1)
when /path=(.*)/
path = $1
when /domain=(.*)/
domain = $1
when /secure/
acceptable_protocols = ['https']
end}
@jar[domain][path] << [name,value,expires, acceptable_protocols]
end
def cookies_for(uri)
result = Hash.new { [] }
uri.host.suffixes('.').each { |domain|
uri.path.prefixes.each { |path|
@jar[domain][path].each { |name,value,expires,acceptable_protocols|
result[name] << value if
DateTime.now < expires and
acceptable_protocols.include? uri.scheme
} if @jar[domain].has_key? path
} if @jar.has_key? domain
}
def result.to_s
keys.collect { |name|
self[name].collect { |value| "#{name}=#{value}" }
}.flatten.join(';')
end
result
end
end
More writeup to follow…
Problems found in testing:
prefixes and suffixes", "
class String
def prefixes(delimiter='/')
segments = split(delimiter)
(1..segments.length).collect { |i| segments[0..-i].join(delimiter) }
end
def suffixes(delimiter='/')
segments = split(delimiter)
(1..segments.length).collect { |i| segments[(i-1)..-1].join(delimiter) }
end
end
class A_cookie_jar
def initialize
@jar = {}
end
def parse_cookie_from(uri,s)
return unless s.is_a? String and s.length > 2
cookies = s.gsub(/, ([^\d])/,';;\1').split(';;')
cookies.each { |cookie|
name_value,*options = cookie.split(';')
name,value = name_value.split('=')
acceptable_protocols,domain,path,expires = ['http','https'],uri.host,uri.path,nil
options.each { |option| case option
when /expires=.+?, (..-...-.. ..:..:..(..)? GMT)/
expires = DateTime.parse($1,:guess_year)
when /path=(.*)/
path = $1
when /domain=(.*)/
domain = $1
when /secure/
acceptable_protocols = ['https']
end}
((@jar[domain] ||= {})[path] ||= []) << [name,value,expires, acceptable_protocols]
}
end
def cookies_for(uri)
result = {}
uri.host.suffixes('.').each { |domain|
uri.path.prefixes.each { |path|
@jar[domain][path].each { |name,value,expires,acceptable_protocols|
(result[name] ||= []) << value if DateTime.now < expires and acceptable_protocols.include? uri.scheme
} if @jar[domain].has_key? path
} if @jar.has_key? domain
}
def result.to_s
keys.collect { |name|
self[name].collect { |value| "#{name}=#{value}" }
}.flatten.join(';')
end
result
end
end
Regular expression for parsing date have error: it should be /expires=.+?, (..-...-..(..)? ..:..:.. GMT)/
In cookies_for method, path prefixes does not include trailing slash so cookie is never received for ‘/’. Quick solution is to add path += '/' for every path. bmihelac