Browse files

initial commit

  • Loading branch information...
0 parents commit 0ce298e20e9571d5d65a1f87f71942e66957e053 Robert Sosinski committed Feb 24, 2012
3 .gitignore
@@ -0,0 +1,3 @@
+.DS_Store
+test/tmp
+*.gem
22 LICENSE
@@ -0,0 +1,22 @@
+(The MIT License)
+
+Copyright (c) 2012 Robert Sosinski
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+'Software'), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.[
43 README.md
@@ -0,0 +1,43 @@
+Simple library that handles the conversion of Postgres HStores to and from Ruby Hashes
+
+Independent Usage
+-----------------
+
+ # Hash to HStore
+ {:a => 'apple', :b => 'banana bread', :fruit => true}.to_hstore
+ => 'a=>apple,fruit=>true,b=>"banana bread"'
+
+ # HStore to Hash
+ 'a=>apple,fruit=>true,b=>"banana bread"'.from_hstore
+ => {"a"=>"apple", "b"=>"banana bread", "fruit"=>"true"}
+
+NOTE: Keep in mind that HStore keys and values are always typecasted as text in Postgres, and can only be one-dimensional.
+
+Using With ActiveRecord
+-----------------------
+
+ class Basket < ActiveRecord::Base
+ serialize :fruits, HstoreSerializer
+ end
+
+ # Create a new record
+ basket = Basket.create(:fruits => {:a => "apple", :b => "banana bread"})
+
+ # Find records with the key "a"
+ Basket.where("fruits ? 'a' = ?")
+
+ # Find records where the key "a" is equal to "apple"
+ Basket.where("fruits -> 'a' = ?", "apple")
+
+ # Updating HStore values
+ basket.fruits["a"] = "apricot"
+ basket.save
+
+NOTE: You can find more HStore query operators at http://www.postgresql.org/docs/9.1/static/hstore.html#HSTORE-OP-TABLE
+
+Credits
+-------
+
+The important parts of this library were pulled from http://github.com/softa/activerecord-postgres-hstore, and the idea to use a serializer came from http://github.com/ruckus/active_record_hstore_serializer.
+
+The main driver of this library was to make a light weight and well tested library without any dependencies on ActiveRecord or ActiveSupport.
13 hstore_serializer.gemspec
@@ -0,0 +1,13 @@
+Gem::Specification.new do |s|
+ s.name = 'hstore_serializer'
+ s.version = '0.1.0'
+ s.date = '2012-02-24'
+ s.summary = "Convert Postgres HStores to and from Ruby Hashes."
+ s.description = s.summary
+ s.authors = ["Robert Sosinski", "JT Archie", "Cody Caughlan", "Juan Maiz", "Diogo Biazus"]
+ s.email = 'email@robertsosinski.com'
+ s.homepage = 'https://github.com/robertsosinski/hstore_serializer'
+ s.files = Dir['hstore_serializer.gemspec', 'LICENSE', 'README.md', '**/*.rb']
+ s.require_paths = ['lib']
+ s.has_rdoc = false
+end
3 lib/hstore_serializer.rb
@@ -0,0 +1,3 @@
+require 'hstore_serializer/hash'
+require 'hstore_serializer/string'
+require 'hstore_serializer/hstore_serializer'
27 lib/hstore_serializer/hash.rb
@@ -0,0 +1,27 @@
+class Hash
+ # Generates an hstore string format. This is the format used
+ # to insert or update stuff in the database.
+ def to_hstore
+ return "" if empty?
+
+ map { |idx, val|
+ iv = [idx,val].map { |_|
+ e = _.to_s.gsub(/"/, '\"')
+ if _.nil?
+ 'NULL'
+ elsif e =~ /[,\s=>]/ || e == '' || e.nil?
+ '"%s"' % e
+ else
+ e
+ end
+ }
+
+ "%s=>%s" % iv
+ } * ","
+ end
+
+ # If the method from_hstore is called on a Hash, it just returns self.
+ def from_hstore
+ self
+ end
+end
10 lib/hstore_serializer/hstore_serializer.rb
@@ -0,0 +1,10 @@
+class HstoreSerializer
+ def self.load(text)
+ return unless text
+ text.from_hstore
+ end
+
+ def self.dump(hash)
+ hash.to_hstore
+ end
+end
41 lib/hstore_serializer/string.rb
@@ -0,0 +1,41 @@
+class String
+ # If the value of a column is already a String and it calls to_hstore, it
+ # just returns self. Validation occurs afterwards.
+ def to_hstore
+ self
+ end
+
+ # Validates the hstore format. Valid formats are:
+ # * An empty string
+ # * A string like %("foo"=>"bar"). I'll call it a "double quoted hstore format".
+ # * A string like %(foo=>bar). Postgres doesn't emit this but it does accept it as input, we should accept any input Postgres does
+ def valid_hstore?
+ pair = hstore_pair
+ !!match(/^\s*(#{pair}\s*(,\s*#{pair})*)?\s*$/)
+ end
+
+ # Creates a hash from a valid double quoted hstore format, because this is the format
+ # that postgresql returns.
+ def from_hstore
+ token_pairs = (scan(hstore_pair)).map { |k,v| [k,v =~ /^NULL$/i ? nil : v] }
+ token_pairs = token_pairs.map { |k,v|
+ [k,v].map { |t|
+ case t
+ when nil then t
+ when /^"(.*)"$/ then $1.gsub(/\\(.)/, '\1')
+ else t.gsub(/\\(.)/, '\1')
+ end
+ }
+ }
+ Hash[ token_pairs ]
+ end
+
+ private
+
+ def hstore_pair
+ quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/
+ unquoted_string = /[^\s=,][^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/
+ string = /(#{quoted_string}|#{unquoted_string})/
+ /#{string}\s*=>\s*#{string}/
+ end
+end
81 spec/hstore_serializer_spec.rb
@@ -0,0 +1,81 @@
+require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
+
+describe HstoreSerializer do
+ it "should recognize a valid hstore string" do
+ "".valid_hstore?.should be_true
+ "a=>b".valid_hstore?.should be_true
+ '"a"=>"b"'.valid_hstore?.should be_true
+ '"a" => "b"'.valid_hstore?.should be_true
+ '"a"=>"b","c"=>"d"'.valid_hstore?.should be_true
+ '"a"=>"b", "c"=>"d"'.valid_hstore?.should be_true
+ '"a" => "b", "c"=>"d"'.valid_hstore?.should be_true
+ '"a"=>"b","c" => "d"'.valid_hstore?.should be_true
+ 'k => v'.valid_hstore?.should be_true
+ 'foo => bar, baz => whatever'.valid_hstore?.should be_true
+ '"1-a" => "anything at all"'.valid_hstore?.should be_true
+ 'key => NULL'.valid_hstore?.should be_true
+ %q(c=>"}", "\"a\""=>"b \"a b").valid_hstore?.should be_true
+ end
+
+ it "should not recognize an invalid hstore string" do
+ '"a"=>"b",Hello?'.valid_hstore?.should be_false
+ end
+
+ it "should convert hash to hstore string and back (sort of)" do
+ {:a => 1, :b => 2}.to_hstore.from_hstore.should eq({"a" => "1", "b" => "2"})
+ end
+
+ it "should convert hstore string to hash" do
+ '"a"=>"1", "b"=>"2"'.from_hstore.should eq({'a' => '1', 'b' => '2'})
+ end
+
+ it "should quote correctly" do
+ {:a => "'a'"}.to_hstore.should eq(%q(a=>'a'))
+ end
+
+ it "should quote keys correctly" do
+ {"'a'" => "a"}.to_hstore.should eq(%q('a'=>a))
+ end
+
+ it "should preserve null values on store" do
+ # NULL=>b will be interpreted as the string pair "NULL"=>"b"
+
+ {'a' => nil,nil=>'b'}.to_hstore.should eq(%q(a=>NULL,NULL=>b))
+ end
+
+ it "should preserve null values on load" do
+ 'a=>null,b=>NuLl,c=>"NuLl",null=>c'.from_hstore.should eq({'a'=>nil,'b'=>nil,'c'=>'NuLl','null'=>'c'})
+ end
+
+ it "should quote tokens with nothing space comma equals or greaterthan" do
+ {' '=>''}.to_hstore.should eq(%q(" "=>""))
+ {','=>''}.to_hstore.should eq(%q(","=>""))
+ {'='=>''}.to_hstore.should eq(%q("="=>""))
+ {'>'=>''}.to_hstore.should eq(%q(">"=>""))
+ end
+
+ it "should unquote keys correctly with single quotes" do
+ "\"'a'\"=>\"a\"". from_hstore.should eq({"'a'" => "a"})
+ '\=a=>q=w'. from_hstore.should eq({"=a"=>"q=w"})
+ '"=a"=>q\=w'. from_hstore.should eq({"=a"=>"q=w"});
+ '"\"a"=>q>w'. from_hstore.should eq({"\"a"=>"q>w"});
+ '\"a=>q"w'. from_hstore.should eq({"\"a"=>"q\"w"})
+ end
+
+ it "should quote keys and values correctly with combinations of single and double quotes" do
+ { %q("a') => %q(b "a' b) }.to_hstore.should eq(%q(\"a'=>"b \"a' b"))
+ end
+
+ it "should unquote keys and values correctly with combinations of single and double quotes" do
+ %q("\"a'"=>"b \"a' b").from_hstore.should eq({%q("a') => %q(b "a' b)})
+ end
+
+ it "should convert empty hash" do
+ {}.to_hstore.should eq("")
+ end
+
+ it "should convert empty string" do
+ ''.from_hstore.should eq({})
+ ' '.from_hstore.should eq({})
+ end
+end
9 spec/spec_helper.rb
@@ -0,0 +1,9 @@
+$LOAD_PATH.unshift(File.dirname(__FILE__))
+$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
+
+require 'active_record_hstore_serializer'
+require 'rspec'
+require 'rspec/autorun'
+
+RSpec.configure do |config|
+end

0 comments on commit 0ce298e

Please sign in to comment.