diff --git a/README.md b/README.md
index aec8d99..cc08b44 100644
--- a/README.md
+++ b/README.md
@@ -63,8 +63,6 @@ Or install it yourself as:
- Print kind error messages to make migration from Mechanize::Cookie easier
-- Make serializers pluggable/autoloadable and prepare a binary friendly API
-
## Contributing
1. Fork it
diff --git a/lib/http/cookie.rb b/lib/http/cookie.rb
index 8a388c2..2c6ac2b 100644
--- a/lib/http/cookie.rb
+++ b/lib/http/cookie.rb
@@ -20,8 +20,6 @@ class HTTP::Cookie
secure httponly
expires created_at accessed_at
]
- True = "TRUE"
- False = "FALSE"
# In Ruby < 1.9.3 URI() does not accept an URI object.
if RUBY_VERSION < "1.9.3"
@@ -247,35 +245,6 @@ class HTTP::Cookie
}
}
end
-
- # Parses a line from cookies.txt and returns a cookie object if
- # the line represents a cookie record or returns nil otherwise.
- def parse_cookiestxt_line(line)
- return nil if line.match(/^#/)
-
- domain,
- s_for_domain, # Whether this cookie is for domain
- path, # Path for which the cookie is relevant
- s_secure, # Requires a secure connection
- s_expires, # Time the cookie expires (Unix epoch time)
- name, value = line.split("\t", 7)
- return nil if value.nil?
-
- value.chomp!
-
- if (expires_seconds = s_expires.to_i).nonzero?
- expires = Time.at(expires_seconds)
- return nil if expires < Time.now
- end
-
- HTTP::Cookie.new(name, value,
- :domain => domain,
- :for_domain => s_for_domain == True,
- :path => path,
- :secure => s_secure == True,
- :expires => expires,
- :version => 0)
- end
end
def name=(name)
@@ -392,19 +361,6 @@ class HTTP::Cookie
end
alias to_s cookie_value
- # Serializes the cookie into a cookies.txt line.
- def to_cookiestxt_line(linefeed = "\n")
- [
- @domain,
- @for_domain ? True : False,
- @path,
- @secure ? True : False,
- @expires.to_i,
- @name,
- @value
- ].join("\t") << linefeed
- end
-
# Returns a string for use in a Set-Cookie header value. If the
# cookie does not have an origin set, one must be given from the
# argument.
diff --git a/lib/http/cookie_jar.rb b/lib/http/cookie_jar.rb
index e476dd3..8b21214 100644
--- a/lib/http/cookie_jar.rb
+++ b/lib/http/cookie_jar.rb
@@ -7,7 +7,7 @@ end
# any particular website.
class HTTP::CookieJar
- include Enumerable
+ autoload :AbstractSaver, 'http/cookie_jar/abstract_saver'
attr_reader :jar
@@ -75,13 +75,15 @@ class HTTP::CookieJar
}
self
end
+ include Enumerable
# call-seq:
- # jar.save_as(file, format = :yaml)
- # jar.save_as(file, options)
+ # jar.save(filename_or_io, **options)
+ # jar.save(filename_or_io, format = :yaml, **options)
#
- # Save the cookie jar to a file in the format specified and return
- # self.
+ # Save the cookie jar into a file or an IO in the format specified
+ # and return self. If the given object responds to #write it is
+ # taken as an IO, or taken as a filename otherwise.
#
# Available option keywords are below:
#
@@ -95,65 +97,108 @@ class HTTP::CookieJar
# Save session cookies as well.
# [+false+]
# Do not save session cookies. (default)
- def save_as(file, options = nil)
- if Symbol === options
- format = options
- session = false
+ #
+ # All options given are passed through to the underlying cookie
+ # saver module.
+ def save(writable, *options)
+ opthash = {
+ :format => :yaml,
+ :session => false,
+ }
+ case options.size
+ when 0
+ when 1
+ case options = options.first
+ when Symbol
+ opthash[:format] = options
+ else
+ opthash.update(options) if options
+ end
+ when 2
+ opthash[:format], options = options
+ opthash.update(options) if options
else
- options ||= {}
- format = options[:format] || :yaml
- session = !!options[:session]
+ raise ArgumentError, 'wrong number of arguments (%d for 1-3)' % (1 + options.size)
end
- jar = dup
- jar.cleanup !session
+ begin
+ saver = AbstractSaver.implementation(opthash[:format]).new(opthash)
+ rescue KeyError => e
+ raise ArgumentError, e.message
+ end
- open(file, 'w') { |f|
- case format
- when :yaml then
- require_yaml
-
- YAML.dump(jar.jar, f)
- when :cookiestxt then
- jar.dump_cookiestxt(f)
- else
- raise ArgumentError, "Unknown cookie jar file format"
- end
- }
+ if writable.respond_to?(:write)
+ saver.save(writable, self)
+ else
+ File.open(writable, 'w') { |io|
+ saver.save(io, self)
+ }
+ end
self
end
- # Load cookie jar from a file in the format specified.
- #
- # Available formats:
- # :yaml <- YAML structure.
- # :cookiestxt <- Mozilla's cookies.txt format
- def load(file, format = :yaml)
- File.open(file) { |f|
- case format
- when :yaml then
- require_yaml
- @jar = YAML.load(f)
- when :cookiestxt then
- load_cookiestxt(f)
- else
- raise ArgumentError, "Unknown cookie jar file format"
- end
- }
-
- cleanup
+ # An obsolete name for save().
+ def save_as(*args)
+ warn "%s() is obsolete; use save()." % __method__
+ save(*args)
end
- def require_yaml # :nodoc:
- begin
- require 'psych'
- rescue LoadError
+ # call-seq:
+ # jar.load(filename_or_io, **options)
+ # jar.load(filename_or_io, format = :yaml, **options)
+ #
+ # Load cookies recorded in a file or an IO in the format specified
+ # into the jar and return self. If the given object responds to
+ # #read it is taken as an IO, or taken as a filename otherwise.
+ #
+ # Available option keywords are below:
+ #
+ # * +format+
+ # [:yaml]
+ # YAML structure (default)
+ # [:cookiestxt]
+ # Mozilla's cookies.txt format
+ #
+ # All options given are passed through to the underlying cookie
+ # saver module.
+ def load(readable, *options)
+ opthash = {
+ :format => :yaml,
+ :session => false,
+ }
+ case options.size
+ when 0
+ when 1
+ case options = options.first
+ when Symbol
+ opthash[:format] = options
+ else
+ opthash.update(options) if options
+ end
+ when 2
+ opthash[:format], options = options
+ opthash.update(options) if options
+ else
+ raise ArgumentError, 'wrong number of arguments (%d for 1-3)' % (1 + options.size)
end
- require 'yaml'
+ begin
+ saver = AbstractSaver.implementation(opthash[:format]).new(opthash)
+ rescue KeyError => e
+ raise ArgumentError, e.message
+ end
+
+ if readable.respond_to?(:write)
+ saver.load(readable, self)
+ else
+ File.open(readable, 'r') { |io|
+ saver.load(io, self)
+ }
+ end
+
+ self
end
- private :require_yaml
# Clear the cookie jar and return self.
def clear
@@ -161,26 +206,6 @@ class HTTP::CookieJar
self
end
- # Read cookies from Mozilla cookies.txt-style IO stream and return
- # self.
- def load_cookiestxt(io)
- io.each_line do |line|
- c = HTTP::Cookie.parse_cookiestxt_line(line) and add(c)
- end
-
- self
- end
-
- # Write cookies to Mozilla cookies.txt-style IO stream and return
- # self.
- def dump_cookiestxt(io)
- io.puts "# HTTP Cookie File"
- to_a.each do |cookie|
- io.print cookie.to_cookiestxt_line
- end
- self
- end
-
protected
# Remove expired cookies and return self.
diff --git a/lib/http/cookie_jar/abstract_saver.rb b/lib/http/cookie_jar/abstract_saver.rb
new file mode 100644
index 0000000..af5a706
--- /dev/null
+++ b/lib/http/cookie_jar/abstract_saver.rb
@@ -0,0 +1,53 @@
+require 'http/cookie_jar'
+
+class HTTP::CookieJar::AbstractSaver
+ class << self
+ @@class_map = {}
+
+ # Gets an implementation class by the name, optionally trying to
+ # load "http/cookie_jar/*_saver" if not found. If loading fails,
+ # KeyError is raised.
+ def implementation(symbol)
+ @@class_map.fetch(symbol)
+ rescue KeyError
+ begin
+ require 'http/cookie_jar/%s_saver' % symbol
+ @@class_map.fetch(symbol)
+ rescue LoadError, KeyError => e
+ raise KeyError, 'cookie saver unavailable: %s' % symbol.inspect
+ end
+ end
+
+ def inherited(subclass)
+ @@class_map[class_to_symbol(subclass)] = subclass
+ end
+
+ def class_to_symbol(klass)
+ klass.name[/[^:]+?(?=Saver$|$)/].downcase.to_sym
+ end
+ end
+
+ def default_options
+ {}
+ end
+ private :default_options
+
+ def initialize(options = nil)
+ options ||= {}
+ @logger = options[:logger]
+ @session = options[:session]
+ # Initializes each instance variable of the same name as option
+ # keyword.
+ default_options.each_pair { |key, default|
+ instance_variable_set("@#{key}", options.key?(key) ? options[key] : default)
+ }
+ end
+
+ def save(io, jar)
+ raise
+ end
+
+ def load(io, jar)
+ raise
+ end
+end
diff --git a/lib/http/cookie_jar/cookiestxt_saver.rb b/lib/http/cookie_jar/cookiestxt_saver.rb
new file mode 100644
index 0000000..442af14
--- /dev/null
+++ b/lib/http/cookie_jar/cookiestxt_saver.rb
@@ -0,0 +1,74 @@
+require 'http/cookie_jar'
+
+# CookiestxtSaver saves and loads cookies in the cookies.txt format.
+class HTTP::CookieJar::CookiestxtSaver < HTTP::CookieJar::AbstractSaver
+ True = "TRUE"
+ False = "FALSE"
+
+ def save(io, jar)
+ io.puts @header if @header
+ jar.each { |cookie|
+ next if !@session && cookie.session?
+ io.print cookie_to_record(cookie)
+ }
+ end
+
+ def load(io, jar)
+ io.each_line { |line|
+ cookie = parse_record(line) and jar.add(cookie)
+ }
+ end
+
+ private
+
+ def default_options
+ {
+ header: "# HTTP Cookie File",
+ linefeed: "\n",
+ }
+ end
+
+ # Serializes the cookie into a cookies.txt line.
+ def cookie_to_record(cookie)
+ cookie.instance_eval {
+ [
+ @domain,
+ @for_domain ? True : False,
+ @path,
+ @secure ? True : False,
+ @expires.to_i,
+ @name,
+ @value
+ ]
+ }.join("\t") << @linefeed
+ end
+
+ # Parses a line from cookies.txt and returns a cookie object if the
+ # line represents a cookie record or returns nil otherwise.
+ def parse_record(line)
+ return nil if line.match(/^#/)
+
+ domain,
+ s_for_domain, # Whether this cookie is for domain
+ path, # Path for which the cookie is relevant
+ s_secure, # Requires a secure connection
+ s_expires, # Time the cookie expires (Unix epoch time)
+ name, value = line.split("\t", 7)
+ return nil if value.nil?
+
+ value.chomp!
+
+ if (expires_seconds = s_expires.to_i).nonzero?
+ expires = Time.at(expires_seconds)
+ return nil if expires < Time.now
+ end
+
+ HTTP::Cookie.new(name, value,
+ :domain => domain,
+ :for_domain => s_for_domain == True,
+ :path => path,
+ :secure => s_secure == True,
+ :expires => expires,
+ :version => 0)
+ end
+end
diff --git a/lib/http/cookie_jar/yaml_saver.rb b/lib/http/cookie_jar/yaml_saver.rb
new file mode 100644
index 0000000..a9f66b5
--- /dev/null
+++ b/lib/http/cookie_jar/yaml_saver.rb
@@ -0,0 +1,30 @@
+require 'http/cookie_jar'
+begin
+ require 'psych'
+rescue LoadError
+end
+require 'yaml'
+
+# YAMLSaver saves and loads cookies in the YAML format.
+class HTTP::CookieJar::YAMLSaver < HTTP::CookieJar::AbstractSaver
+ def save(io, jar)
+ YAML.dump(@session ? jar.to_a : jar.reject(&:session?), io)
+ end
+
+ def load(io, jar)
+ begin
+ YAML.load(io)
+ rescue ArgumentError
+ @logger.warn "incompatible YAML cookie data discarded" if @logger
+ return
+ end.each { |cookie|
+ jar.add(cookie)
+ }
+ end
+
+ private
+
+ def default_options
+ {}
+ end
+end
diff --git a/test/test_http_cookie_jar.rb b/test/test_http_cookie_jar.rb
index 75cd2a8..467276d 100644
--- a/test/test_http_cookie_jar.rb
+++ b/test/test_http_cookie_jar.rb
@@ -301,7 +301,7 @@ class TestHTTPCookieJar < Test::Unit::TestCase
assert_equal(3, @jar.cookies(url).length)
in_tmpdir do
- value = @jar.save_as("cookies.yml")
+ value = @jar.save("cookies.yml")
assert_same @jar, value
jar = HTTP::CookieJar.new
@@ -333,7 +333,7 @@ class TestHTTPCookieJar < Test::Unit::TestCase
assert_equal(3, @jar.cookies(url).length)
in_tmpdir do
- @jar.save_as("cookies.yml", :format => :yaml, :session => true)
+ @jar.save("cookies.yml", :format => :yaml, :session => true)
jar = HTTP::CookieJar.new
jar.load("cookies.yml")
@@ -365,7 +365,7 @@ class TestHTTPCookieJar < Test::Unit::TestCase
assert_equal(3, @jar.cookies(url).length)
in_tmpdir do
- @jar.save_as("cookies.txt", :cookiestxt)
+ @jar.save("cookies.txt", :cookiestxt)
jar = HTTP::CookieJar.new
jar.load("cookies.txt", :cookiestxt) # HACK test the format