Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Allow alternate adapters to be used for serializing/deserializing #145

Open
wants to merge 7 commits into from

9 participants

@mhuggins

I'm working on an app that is intended to work with session between Rails and Node.js. The problem is, Node.js can't read a Ruby marshaled object from the session with redis-store's current implementation.

Looking through old issues, I see I'm not the only one with this problem, as issues #77 and #79 were both opened with a similar purpose. The problem is that both of those implementations could not properly unmarshal Ruby objects.

This pull request does two things. First, it creates the concept of an "adapter". An adapter is anything that can respond to load and dump, and it defaults to a new wrapper class that just uses Marshal. (This is to allow for backward compatibility without existing users having to worry about changing their app code.)

Second, it introduces a new JSON adapter. This adapter serializes Hash, Array, String, Numeric, TrueClass, FalseClass, and NilClass objects into their appropriate JSON representations. Any other non-native JSON class is converted to a String via Marshal.dump. This allows other languages that are sharing the session to parse the JSON session data itself, simply ignoring the native Ruby objects that aren't needed.

Example usage:

# each of the following will create a new redis store object that uses the new Marshal adapter
Redis::Store.new
Redis::Store.new(:adapter => :marshal)  # symbol shorthand
Redis::Store.new(:adapter => Redis::Store::Adapters::Marshal)  # actual class
Redis::Store.new(:adapter => "Redis::Store::Adapters::Marshal")  # string name representing class

# each of the following will create a new redis store object that uses the new Json adapter
Redis::Store.new(:adapter => :json)   # symbol shorthand
Redis::Store.new(:adapter => Redis::Store::Adapters::Json)  # actual class
Redis::Store.new(:adapter => "Redis::Store::Adapters::Json")  # string name representing class

Here is an example of how easy it is to create a custom adapter:

module RedisYamlAdapter
  def self.dump(object)
    ::YAML.dump(object)
  end

  def self.load(string)
    ::YAML.load(string)
  end
end

Redis::Store.new(:adapter => RedisYamlAdapter)

Of course, we didn't really even need to create an adapter for this very simple scenario. We could use the YAML class as is since it already responds to load/dump:

Redis::Store.new(:adapter => YAML)

And it's just as easy to integrate into your Rails app:

# Rails 3 example
MyApp::Application.config.session_store :redis_store, :servers => { :adapter => :json }

I would love to hear feedback on the approach. Some new unit tests could also be used (along with updated documentation), but I wanted to ensure you're open to the idea before moving forward with coding anything more into this branch. Thanks!

@jodosha
Owner

Thanks for opening the discussion, this is an intersting topic. Here my considerations: first of all I don't like the idea of having ActiveSupport as dependency, just for #constantize. This will force developers to install this gem for a secondary feature, and also could prevent them to decide the Rails version the want to use (in the patch you're automatically excluding all the < 3.2.2 apps). A solution can be borrowing that code and consider it as a Ruby ext.

Second, does this JSON serializer guarantees ActiveRecord or Ruby objects to be transparently serialized/unserialized, including their internal state? Could be convenient to implement something similar to MongoId, where there is a more structured representations of the documents? I'm afraid that just invoking #to_json, isn't enough.

Third, by implementing this feature, I would like to see a little refactoring. Marshalling shound became an internal adapter (strartegy, if you will) of a more generic Serializer class. The _extend_marshalling should disappear, all the data will pass thru the Serializer, that by default will have a NoOp strategy, or, according to the conf, will change in marshalling/json/yaml/whatever.

Fourth, how to specify the adapter in the case of the configuration URI? Maybe redis+json:// ?

@mhuggins

@jodosha - Thanks for the quick feedback, I really appreciate it.

After submitting this pull request and thinking about it in bed last night, I had some of the same thoughts about requiring ActiveSupport, as you mentioned. I starting thinking that it would make more sense to take that part out, not worry about constantizing strings, and only allow actual classes/objects that support dump/load to be supplied as values for the adapter param. This would allow the ActiveSupport dependency to be removed altogether.

There is a concern with this JSON serializer that if you have a Hash where the keys are strings, then you serialize/deserialize, you'll end up with a Hash where the keys are symbols. Strings will not have internal state information such as @html_safe instance variables being saved. Other objects that are extensions of Hash or Array will also lose any internal state they have prior to internal serialization with the current implementation. I''m not sure if there's really a way around this, unless a fake value is added to the serialized Hash/Array that can be interpreted back. I feel like that's kind of a hack though, personally.

I was wondering while coding this if the _extend_marshalling call should disappear, so I'm glad you mentioned that. Could you supply a little info on what you have in mind for the Serializer? Will it just be a placeholder class/module with dump/load methods that the other classes will extend or include? Also, why include a NoOp strategy as default when users currently expect Marshaling by default?

With regards to the configuration URI, I'm not familiar with this at all. Is this something Redis supports by default, or is it exposed by the gem? I haven't worked with Redis in enough detail to know more about it, unfortunately.

Thanks! :)

@mhuggins

Alright, I went ahead and made some additional changes to this from what you suggested. It's using the verbiage "strategy" instead of "adapter" now. There are four possible options built in:

  • :marshal (default): uses Ruby's built-in Marshal dump/load methods
  • :json: builds and parses JSON in the manner I described in my original comment
  • :yaml. builds and parses YAML
  • false: acts the same as :marshalling => false previously did. (I removed the :marshalling option in the process.)

I removed the ActiveSupport dependency, and I also added tests for the new strategies.

Let me know what you think so far, and also if you could provide more insight about configuration URI's, that would be great so I could maybe take a look at that as well.

Lastly, if you decide you want to pull this in after all is said and done, I can squash the commits first if needed. Just let me know! Thanks again :)

@mhuggins

Hmm, I seem to be getting a "Session collision" error quite a bit now, even though I didn't change any of the underlying code. I'll make sure it's nothing I've changed, but if you see anything that might have caused an issue in my changes, please let me know!

@travisbot

This pull request fails (merged ca5079a into 6644a83).

@mhuggins

Well, I fixed the errors I had previously, which related to the session collisions. However, I now have an issue with 'json' only being available in Ruby 1.9+, but not Ruby 1.8 or non-MRI Rubies it seems. I suppose I could do the following, though I could use some input from you on how you might want this to go.

In the redis-store.gemspec file:

s.add_dependency 'json',  '>= 1.7.0' if RUBY_VERSION < '1.9'

In the json.rb strategy module:

require 'rubygems'
require 'json'

Thoughts?

@mhuggins

@jodosha - Just wanted to follow up and see if you have any input regarding my above questions. I would love to follow through on this and ideally have it pulled into the redis-store gem if that's an option (after ironing out the questions and json gem issue).

@mhoran

@mhuggins, the problem with the RUBY_VERSION requirement in the gemspec is that the requirement gets compiled when the gem is pushed, and we'll only push the gem once so it will get compiled with whatever Ruby we're running at build time. Perhaps you could look into how Rails works around this?

I'm not sure we need to be too concerned about the loss of internal state resulting from serialization. This should be left as a caveat to those choosing an alternative marshaling strategy. The default state, however, should maintain internal state to preserve backwards compatibility.

Regarding URI configuration, perhaps we could pass the strategy (and possibly future options) as "query parameters" in the URI. For example, "redis://localhost/namespace?strategy=json".

@jodosha
Owner

Sorry guys, just back from holidays.
I think that specifying the strategy as query string is an elegant solution, but I'm still not convinced about the loss of internal the state. For instance flash messages which are containing some markup declared secure with html_safe or memoized vars for object cached with Rails.cache, will be lost after the JSON is being loaded from the store.

@mhuggins

@jodosha - Given that feedback, I wonder then if a simpler approach could be implemented compared to what I have here. Consider this:

  • remove all strategy classes from my current pull request
  • keep the idea of a strategy, but instead of passing in a symbol that represents the strategy to use, pass in an object reference that responds to dump/load (e.g.: pass Marshal instead of :marshal).

That way, redis-store does not have to be concerned with issues such as the html_safe issue when it comes to serializing JSON, as you pointed out. I can optionally create my own JSON encoder and pass that as the strategy option if I prefer.

@mhuggins

Then again, that would make it difficult to handle the "query parameter" idea in the URI configuration.

@hayesgm

Just FYI, I created a NodeJS module a while ago to unmarshall Ruby serialized data. This could be a quick-fix alternative to the problem presented in this thread: https://gist.github.com/4417808

@alainbloch

im surprised this wasn't accepted.

@jodosha - im wondering why you care so much if JSON doesn't properly de/serialize objects in session. People shouldn't be saving objects into session anyways. The REAL problem is that redis-store only uses ruby Marshal which is a non-standard way of serializing data. You shouldn't care what strategy/adapter people use to load/dump into redis. If a developer creates a crazy adapter and it breaks their app, then it breaks their app.

@alainbloch alainbloch commented on the diff
redis-store/lib/redis/store/strategy/json.rb
((26 lines not shown))
+ object.each_with_index { |v, i| object[i] = _marshal(v) }
+ when *SERIALIZABLE
+ object
+ else
+ ::Marshal.dump(object)
+ end
+ end
+
+ def _unmarshal(object)
+ case object
+ when Hash
+ object.each { |k,v| object[k] = _unmarshal(v) }
+ when Array
+ object.each_with_index { |v, i| object[i] = _unmarshal(v) }
+ when String
+ object.start_with?(MARSHAL_INDICATORS) ? ::Marshal.load(object) : object

This won't work. You need to add an asterisk so the MARSHAL_INDICATORS Array is used as multiple arguments for the star_with? method :

object.start_with?(*MARSHAL_INDICATORS)

I suggest adding a test for it. I noticed that there wasn't a test covering marshalling Ruby Marshalled strings from the store.

You're right, good catch. It could use some more tests, but it doesn't seem like this pull request is going anywhere for me to commit the time.

Well, if it means anything to you, Im using your code on a fairly large website. ;) Thanks for taking the time those months ago to do this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@steventen

+1

This is a good feature, JSON is a standard way when the session is not used only in rails/ruby

@radar
Owner

Hi @mhuggins.

There seems to be some interest in getting this PR merged still. If so, can you please update it to make it mergeable with the current master?

Anyone else is welcome also to take this PR and do the same and just open a new PR.

Thanks!

@radar radar closed this
@radar radar reopened this
@mhuggins

Sure thing, I'll try to take a look this weekend. :)

@mhuggins mhuggins referenced this pull request in kapost/circuitry
Closed

Prevent duplicate message processing #12

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
1  redis-store/lib/redis-store.rb
@@ -3,7 +3,6 @@
require 'redis/factory'
require 'redis/distributed_store'
require 'redis/store/namespace'
-require 'redis/store/marshalling'
require 'redis/store/version'
class Redis
View
21 redis-store/lib/redis/store.rb
@@ -1,14 +1,21 @@
require 'redis/store/ttl'
require 'redis/store/interface'
+require 'redis/store/strategy'
class Redis
class Store < self
include Ttl, Interface
+ STRATEGIES = {
+ :marshal => Strategy::Marshal,
+ :json => Strategy::Json,
+ :yaml => Strategy::Yaml,
+ }.freeze
+
def initialize(options = { })
super
- _extend_marshalling options
- _extend_namespace options
+ _extend_strategy options
+ _extend_namespace options
end
def reconnect
@@ -20,9 +27,13 @@ def to_s
end
private
- def _extend_marshalling(options)
- @marshalling = !(options[:marshalling] === false) # HACK - TODO delegate to Factory
- extend Marshalling if @marshalling
+ def _extend_strategy(options)
+ strategy = options[:strategy]
+
+ unless strategy === false
+ strategy_class = STRATEGIES[strategy] || STRATEGIES[:marshal]
+ extend Strategy, strategy_class
+ end
end
def _extend_namespace(options)
View
30 redis-store/lib/redis/store/marshalling.rb → redis-store/lib/redis/store/strategy.rb
@@ -1,44 +1,48 @@
+require 'redis/store/strategy/json'
+require 'redis/store/strategy/marshal'
+require 'redis/store/strategy/yaml'
+
class Redis
class Store < self
- module Marshalling
+ module Strategy
def set(key, value, options = nil)
- _marshal(value, options) { |value| super encode(key), encode(value), options }
+ dump(value, options) { |value| super encode(key), encode(value), options }
end
def setnx(key, value, options = nil)
- _marshal(value, options) { |value| super encode(key), encode(value), options }
+ dump(value, options) { |value| super encode(key), encode(value), options }
end
def setex(key, expiry, value, options = nil)
- _marshal(value, options) { |value| super encode(key), expiry, encode(value), options }
+ dump(value, options) { |value| super encode(key), expiry, encode(value), options }
end
def get(key, options = nil)
- _unmarshal super(key), options
+ load super(key), options
end
def mget(*keys)
options = keys.flatten.pop if keys.flatten.last.is_a?(Hash)
super(*keys).map do |result|
- _unmarshal result, options
+ load result, options
end
end
private
- def _marshal(val, options)
- yield marshal?(options) ? Marshal.dump(val) : val
+ def dump(val, options)
+ yield dump?(options) ? _dump(val) : val
end
- def _unmarshal(val, options)
- unmarshal?(val, options) ? Marshal.load(val) : val
+ def load(val, options)
+ load?(val, options) ? _load(val) : val
end
- def marshal?(options)
+ def dump?(options)
!(options && options[:raw])
end
- def unmarshal?(result, options)
- result && result.size > 0 && marshal?(options)
+ def load?(result, options)
+ result && result.size > 0 && dump?(options)
end
if defined?(Encoding)
View
49 redis-store/lib/redis/store/strategy/json.rb
@@ -0,0 +1,49 @@
+require 'json'
+
+class Redis
+ class Store < self
+ module Strategy
+ module Json
+ private
+ SERIALIZABLE = [String, TrueClass, FalseClass, NilClass, Numeric, Date, Time].freeze
+ MARSHAL_INDICATORS = ["\x04", "\004", "\u0004"].freeze
+
+ def _dump(object)
+ object = _marshal(object)
+ object.to_json
+ end
+
+ def _load(string)
+ object = JSON.parse(string, :symbolize_names => true)
+ _unmarshal(object)
+ end
+
+ def _marshal(object)
+ case object
+ when Hash
+ object.each { |k,v| object[k] = _marshal(v) }
+ when Array
+ object.each_with_index { |v, i| object[i] = _marshal(v) }
+ when *SERIALIZABLE
+ object
+ else
+ ::Marshal.dump(object)
+ end
+ end
+
+ def _unmarshal(object)
+ case object
+ when Hash
+ object.each { |k,v| object[k] = _unmarshal(v) }
+ when Array
+ object.each_with_index { |v, i| object[i] = _unmarshal(v) }
+ when String
+ object.start_with?(MARSHAL_INDICATORS) ? ::Marshal.load(object) : object

This won't work. You need to add an asterisk so the MARSHAL_INDICATORS Array is used as multiple arguments for the star_with? method :

object.start_with?(*MARSHAL_INDICATORS)

I suggest adding a test for it. I noticed that there wasn't a test covering marshalling Ruby Marshalled strings from the store.

You're right, good catch. It could use some more tests, but it doesn't seem like this pull request is going anywhere for me to commit the time.

Well, if it means anything to you, Im using your code on a fairly large website. ;) Thanks for taking the time those months ago to do this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ else
+ object
+ end
+ end
+ end
+ end
+ end
+end
View
16 redis-store/lib/redis/store/strategy/marshal.rb
@@ -0,0 +1,16 @@
+class Redis
+ class Store < self
+ module Strategy
+ module Marshal
+ private
+ def _dump(object)
+ ::Marshal.dump(object)
+ end
+
+ def _load(string)
+ ::Marshal.load(string)
+ end
+ end
+ end
+ end
+end
View
16 redis-store/lib/redis/store/strategy/yaml.rb
@@ -0,0 +1,16 @@
+class Redis
+ class Store < self
+ module Strategy
+ module Yaml
+ private
+ def _dump(object)
+ YAML.dump(object)
+ end
+
+ def _load(string)
+ YAML.load(string)
+ end
+ end
+ end
+ end
+end
View
28 redis-store/test/redis/factory_test.rb
@@ -41,9 +41,31 @@
store.instance_variable_get(:@client).password.must_equal("secret")
end
- it "allows/disable marshalling" do
- store = Redis::Factory.create :marshalling => false
- store.instance_variable_get(:@marshalling).must_equal(false)
+ it "allows json strategy option" do
+ store = Redis::Factory.create :strategy => :json
+ store.must_be_kind_of(Redis::Store::Strategy::Json)
+ end
+
+ it "allows marshal strategy option" do
+ store = Redis::Factory.create :strategy => :marshal
+ store.must_be_kind_of(Redis::Store::Strategy::Marshal)
+ end
+
+ it "allows yaml strategy option" do
+ store = Redis::Factory.create :strategy => :yaml
+ store.must_be_kind_of(Redis::Store::Strategy::Yaml)
+ end
+
+ it "allows false strategy option" do
+ store = Redis::Factory.create :strategy => false
+ store.wont_be_kind_of(Redis::Store::Strategy::Json)
+ store.wont_be_kind_of(Redis::Store::Strategy::Marshal)
+ store.wont_be_kind_of(Redis::Store::Strategy::Yaml)
+ end
+
+ it "defaults to marshal strategy" do
+ store = Redis::Factory.create
+ store.must_be_kind_of(Redis::Store::Strategy::Marshal)
end
it "should instantiate a Redis::DistributedStore store" do
View
2  redis-store/test/redis/store/namespace_test.rb
@@ -3,7 +3,7 @@
describe "Redis::Store::Namespace" do
def setup
@namespace = "theplaylist"
- @store = Redis::Store.new :namespace => @namespace, :marshalling => false # TODO remove mashalling option
+ @store = Redis::Store.new :namespace => @namespace, :strategy => false
@client = @store.instance_variable_get(:@client)
@rabbit = "bunny"
end
View
108 redis-store/test/redis/store/strategy/json_test.rb
@@ -0,0 +1,108 @@
+require 'test_helper'
+
+describe "Redis::Store::Strategy::Json" do
+ def setup
+ @store = Redis::Store.new :strategy => :json
+ @rabbit = OpenStruct.new :name => 'rabbit', :legs => 4
+ @peter = { :name => "Peter Cottontail",
+ :race => @rabbit }
+ @bunnicula = { :name => "Bunnicula",
+ :race => @rabbit,
+ :friends => [@peter],
+ :age => 3.1,
+ :alive => true }
+ @store.set "rabbit", @bunnicula
+ @store.del "rabbit2"
+ end
+
+ def teardown
+ @store.quit
+ end
+
+ it "unmarshals on get" do
+ @store.get("rabbit").must_equal(@bunnicula)
+ end
+
+ it "marshals on set" do
+ @store.set "rabbit", @peter
+ @store.get("rabbit").must_equal(@peter)
+ end
+
+ it "doesn't unmarshal on get if raw option is true" do
+ race = Marshal.dump(@rabbit).to_json
+ @store.get("rabbit", :raw => true).must_equal(%({"name":"Bunnicula","race":#{race},"friends":[{"name":"Peter Cottontail","race":#{race}}],"age":3.1,"alive":true}))
+ end
+
+ it "doesn't marshal on set if raw option is true" do
+ race = Marshal.dump(@rabbit)
+ @store.set "rabbit", @peter, :raw => true
+ @store.get("rabbit", :raw => true).must_equal(%({:name=>"Peter Cottontail", :race=>#{race.inspect}}))
+ end
+
+ it "doesn't set an object if already exist" do
+ @store.setnx "rabbit", @peter
+ @store.get("rabbit").must_equal(@bunnicula)
+ end
+
+ it "marshals on set unless exists" do
+ @store.setnx "rabbit2", @peter
+ @store.get("rabbit2").must_equal(@peter)
+ end
+
+ it "doesn't marshal on set unless exists if raw option is true" do
+ @store.setnx "rabbit2", @peter, :raw => true
+ race = Marshal.dump(@rabbit)
+ @store.get("rabbit2", :raw => true).must_equal(%({:name=>"Peter Cottontail", :race=>#{race.inspect}}))
+ end
+
+ it "marshals on set expire" do
+ @store.setex "rabbit2", 1, @peter
+ @store.get("rabbit2").must_equal(@peter)
+ sleep 2
+ @store.get("rabbit2").must_be_nil
+ end
+
+ it "doesn't unmarshal on multi get" do
+ @store.set "rabbit2", @peter
+ rabbit, rabbit2 = @store.mget "rabbit", "rabbit2"
+ rabbit.must_equal(@bunnicula)
+ rabbit2.must_equal(@peter)
+ end
+
+ it "doesn't unmarshal on multi get if raw option is true" do
+ @store.set "rabbit", @bunnicula
+ @store.set "rabbit2", @peter
+ rabbit, rabbit2 = @store.mget "rabbit", "rabbit2", :raw => true
+ race = Marshal.dump(@rabbit).to_json
+ rabbit.must_equal(%({"name":"Bunnicula","race":#{race},"friends":[{"name":"Peter Cottontail","race":#{race}}],"age":3.1,"alive":true}))
+ rabbit2.must_equal(%({"name":"Peter Cottontail","race":#{race}}))
+ end
+
+ describe "binary safety" do
+ before do
+ @utf8_key = [51339].pack("U*")
+ @ascii_string = [128].pack("C*")
+ @ascii_rabbit = OpenStruct.new(:name => @ascii_string)
+ end
+
+ it "marshals objects"
+ # @store.set(@utf8_key, @ascii_rabbit)
+ # @store.get(@utf8_key).must_equal(@ascii_rabbit)
+
+ it "gets and sets raw values" do
+ @store.set(@utf8_key, @ascii_string, :raw => true)
+ @store.get(@utf8_key, :raw => true).bytes.to_a.must_equal(@ascii_string.bytes.to_a)
+ end
+
+ it "marshals objects on setnx"
+ # @store.del(@utf8_key)
+ # @store.setnx(@utf8_key, @ascii_rabbit)
+ # @store.get(@utf8_key).must_equal(@ascii_rabbit)
+
+ it "gets and sets raw values on setnx" do
+ @store.del(@utf8_key)
+ @store.setnx(@utf8_key, @ascii_string, :raw => true)
+ @store.get(@utf8_key, :raw => true).bytes.to_a.must_equal(@ascii_string.bytes.to_a)
+ end
+ end if defined?(Encoding)
+end
View
44 ...tore/test/redis/store/marshalling_test.rb → ...test/redis/store/strategy/marshal_test.rb
@@ -1,8 +1,8 @@
require 'test_helper'
-describe "Redis::Marshalling" do
+describe "Redis::Store::Strategy::Marshal" do
def setup
- @store = Redis::Store.new :marshalling => true
+ @store = Redis::Store.new :strategy => :marshal
@rabbit = OpenStruct.new :name => "bunny"
@white_rabbit = OpenStruct.new :color => "white"
@store.set "rabbit", @rabbit
@@ -32,7 +32,7 @@ def teardown
end
end
- it "doesn't marshal set if raw option is true" do
+ it "doesn't marshal on set if raw option is true" do
@store.set "rabbit", @white_rabbit, :raw => true
@store.get("rabbit", :raw => true).must_equal(%(#<OpenStruct color="white">))
end
@@ -90,38 +90,32 @@ def teardown
end
describe "binary safety" do
- it "marshals objects" do
- utf8_key = [51339].pack("U*")
- ascii_rabbit = OpenStruct.new(:name => [128].pack("C*"))
+ before do
+ @utf8_key = [51339].pack("U*")
+ @ascii_string = [128].pack("C*")
+ @ascii_rabbit = OpenStruct.new(:name => @ascii_string)
+ end
- @store.set(utf8_key, ascii_rabbit)
- @store.get(utf8_key).must_equal(ascii_rabbit)
+ it "marshals objects" do
+ @store.set(@utf8_key, @ascii_rabbit)
+ @store.get(@utf8_key).must_equal(@ascii_rabbit)
end
it "gets and sets raw values" do
- utf8_key = [51339].pack("U*")
- ascii_string = [128].pack("C*")
-
- @store.set(utf8_key, ascii_string, :raw => true)
- @store.get(utf8_key, :raw => true).bytes.to_a.must_equal(ascii_string.bytes.to_a)
+ @store.set(@utf8_key, @ascii_string, :raw => true)
+ @store.get(@utf8_key, :raw => true).bytes.to_a.must_equal(@ascii_string.bytes.to_a)
end
it "marshals objects on setnx" do
- utf8_key = [51339].pack("U*")
- ascii_rabbit = OpenStruct.new(:name => [128].pack("C*"))
-
- @store.del(utf8_key)
- @store.setnx(utf8_key, ascii_rabbit)
- @store.get(utf8_key).must_equal(ascii_rabbit)
+ @store.del(@utf8_key)
+ @store.setnx(@utf8_key, @ascii_rabbit)
+ @store.get(@utf8_key).must_equal(@ascii_rabbit)
end
it "gets and sets raw values on setnx" do
- utf8_key = [51339].pack("U*")
- ascii_string = [128].pack("C*")
-
- @store.del(utf8_key)
- @store.setnx(utf8_key, ascii_string, :raw => true)
- @store.get(utf8_key, :raw => true).bytes.to_a.must_equal(ascii_string.bytes.to_a)
+ @store.del(@utf8_key)
+ @store.setnx(@utf8_key, @ascii_string, :raw => true)
+ @store.get(@utf8_key, :raw => true).bytes.to_a.must_equal(@ascii_string.bytes.to_a)
end
end if defined?(Encoding)
end
View
105 redis-store/test/redis/store/strategy/yaml_test.rb
@@ -0,0 +1,105 @@
+require 'test_helper'
+
+describe "Redis::Store::Strategy::Yaml" do
+ def setup
+ @store = Redis::Store.new :strategy => :yaml
+ @rabbit = OpenStruct.new :name => "bunny"
+ @white_rabbit = OpenStruct.new :color => "white"
+ @store.set "rabbit", @rabbit
+ @store.del "rabbit2"
+ end
+
+ def teardown
+ @store.quit
+ end
+
+ # Psych::YAML had a bug in which it could not properly serialize binary Strings
+ # in Ruby 1.9.3. The issue was addressed in 1.9.3-p125.
+ def self.binary_encodable_yaml?
+ RUBY_VERSION != '1.9.3' or RUBY_PATCHLEVEL >= 125
+ end
+
+ it "unmarshals on get" do
+ @store.get("rabbit").must_equal(@rabbit)
+ end
+
+ it "marshals on set" do
+ @store.set "rabbit", @white_rabbit
+ @store.get("rabbit").must_equal(@white_rabbit)
+ end
+
+ it "doesn't unmarshal on get if raw option is true" do
+ @store.get("rabbit", :raw => true).must_equal("--- !ruby/object:OpenStruct\ntable:\n :name: bunny\n")
+ end
+
+ it "doesn't marshal on set if raw option is true" do
+ @store.set "rabbit", @white_rabbit, :raw => true
+ @store.get("rabbit", :raw => true).must_equal(%(#<OpenStruct color=\"white\">))
+ end
+
+ it "doesn't set an object if already exist" do
+ @store.setnx "rabbit", @white_rabbit
+ @store.get("rabbit").must_equal(@rabbit)
+ end
+
+ it "marshals on set unless exists" do
+ @store.setnx "rabbit2", @white_rabbit
+ @store.get("rabbit2").must_equal(@white_rabbit)
+ end
+
+ it "doesn't marshal on set unless exists if raw option is true" do
+ @store.setnx "rabbit2", @white_rabbit, :raw => true
+ @store.get("rabbit2", :raw => true).must_equal(%(#<OpenStruct color=\"white\">))
+ end
+
+ it "marshals on set expire" do
+ @store.setex "rabbit2", 1, @white_rabbit
+ @store.get("rabbit2").must_equal(@white_rabbit)
+ sleep 2
+ @store.get("rabbit2").must_be_nil
+ end
+
+ it "doesn't unmarshal on multi get" do
+ @store.set "rabbit2", @white_rabbit
+ rabbit, rabbit2 = @store.mget "rabbit", "rabbit2"
+ rabbit.must_equal(@rabbit)
+ rabbit2.must_equal(@white_rabbit)
+ end
+
+ it "doesn't unmarshal on multi get if raw option is true" do
+ @store.set "rabbit2", @white_rabbit
+ rabbit, rabbit2 = @store.mget "rabbit", "rabbit2", :raw => true
+ rabbit.must_equal("--- !ruby/object:OpenStruct\ntable:\n :name: bunny\n")
+ rabbit2.must_equal("--- !ruby/object:OpenStruct\ntable:\n :color: white\n")
+ end
+
+ describe "binary safety" do
+ before do
+ @utf8_key = [51339].pack("U*")
+ @ascii_string = [128].pack("C*")
+ @ascii_rabbit = OpenStruct.new(:name => @ascii_string)
+ end
+
+ it "marshals objects" do
+ @store.set(@utf8_key, @ascii_rabbit)
+ @store.get(@utf8_key).must_equal(@ascii_rabbit)
+ end
+
+ it "gets and sets raw values" do
+ @store.set(@utf8_key, @ascii_string, :raw => true)
+ @store.get(@utf8_key, :raw => true).bytes.to_a.must_equal(@ascii_string.bytes.to_a)
+ end
+
+ it "marshals objects on setnx" do
+ @store.del(@utf8_key)
+ @store.setnx(@utf8_key, @ascii_rabbit)
+ @store.get(@utf8_key).must_equal(@ascii_rabbit)
+ end
+
+ it "gets and sets raw values on setnx" do
+ @store.del(@utf8_key)
+ @store.setnx(@utf8_key, @ascii_string, :raw => true)
+ @store.get(@utf8_key, :raw => true).bytes.to_a.must_equal(@ascii_string.bytes.to_a)
+ end
+ end if defined?(Encoding) && binary_encodable_yaml?
+end
Something went wrong with that request. Please try again.