surus 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --colour
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in surus.gemspec
4
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,9 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard 'rspec', :version => 2 do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/surus/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+ end
9
+
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License
2
+
3
+ Copyright (c) Jack Christensen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,35 @@
1
+ Surus
2
+ =====
3
+
4
+ # Description
5
+
6
+ Surus extends ActiveRecord with PostgreSQL specific functionality. At the
7
+ moment this is limited to hstore.
8
+
9
+ # Installation
10
+
11
+ gem install surus
12
+
13
+ # Hstore
14
+
15
+ Hashes can be serialized to an hstore column.
16
+
17
+ class User < ActiveRecord::Base
18
+ serialize :properties, Hstore::Serializer.new
19
+ end
20
+
21
+ Even though the underlying hstore can only use strings for keys and values
22
+ (and NULL for values) Surus can successfully maintain type for integers,
23
+ floats, bigdecimals, and dates. It does this by storing an extra key
24
+ value pair (or two) to maintain type information.
25
+
26
+ Hstores can be searched with helper scopes.
27
+
28
+ User.hstore_has_pairs(:properties, "favorite_color" => "green")
29
+ User.hstore_has_key(:properties, "favorite_color")
30
+ User.hstore_has_all_keys(:properties, "favorite_color", "gender")
31
+ User.hstore_has_any_keys(:properties, "favorite_color", "favorite_artist")
32
+
33
+ # License
34
+
35
+ MIT
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,21 @@
1
+ module Hstore
2
+ module Scope
3
+ def hstore_has_pairs(column, hash)
4
+ where("#{connection.quote_column_name(column)} @> ?", Serializer.new.dump(hash))
5
+ end
6
+
7
+ def hstore_has_key(column, key)
8
+ where("#{connection.quote_column_name(column)} ? :key", :key => key)
9
+ end
10
+
11
+ def hstore_has_all_keys(column, *keys)
12
+ where("#{connection.quote_column_name(column)} ?& ARRAY[:keys]", :keys => keys.flatten)
13
+ end
14
+
15
+ def hstore_has_any_keys(column, *keys)
16
+ where("#{connection.quote_column_name(column)} ?| ARRAY[:keys]", :keys => keys.flatten)
17
+ end
18
+ end
19
+ end
20
+
21
+ ActiveRecord::Base.extend Hstore::Scope
@@ -0,0 +1,140 @@
1
+ module Hstore
2
+ class Serializer
3
+ KEY_VALUE_REGEX = %r{
4
+ "
5
+ ((?:[^"\\]|\\.)*)
6
+ "
7
+ =>
8
+ (
9
+ "
10
+ (?:[^"\\]|\\.)*
11
+ "
12
+ |(NULL)
13
+ )
14
+ }x
15
+
16
+ def load(string)
17
+ return unless string
18
+ stringified_hash = string.scan(KEY_VALUE_REGEX).each_with_object({}) do |key_value, hash|
19
+ key, value = key_value
20
+ key = unescape(key)
21
+ value = if value == "NULL"
22
+ nil
23
+ else
24
+ unescape(value[1..-2])
25
+ end
26
+
27
+ hash[key] = value
28
+ end
29
+
30
+ key_types = stringified_hash.delete "__key_types"
31
+ key_types = YAML.load key_types if key_types
32
+ value_types = stringified_hash.delete "__value_types"
33
+ value_types = YAML.load value_types if value_types
34
+
35
+ return stringified_hash unless key_types || value_types
36
+
37
+ stringified_hash.each_with_object({}) do |key_value, hash|
38
+ string_key, string_value = key_value
39
+
40
+ key = if key_types && key_types.key?(string_key)
41
+ typecast(string_key, key_types[string_key])
42
+ else
43
+ string_key
44
+ end
45
+
46
+ value = if value_types && value_types.key?(string_key)
47
+ typecast(string_value, value_types[string_key])
48
+ else
49
+ string_value
50
+ end
51
+
52
+ hash[key] = value
53
+ end
54
+ end
55
+
56
+ def dump(hash)
57
+ return unless hash
58
+
59
+ key_types = {}
60
+ value_types = {}
61
+
62
+ stringified_hash = hash.each_with_object({}) do |key_value, stringified_hash|
63
+ key_string, key_type = stringify(key_value[0])
64
+ value_string, value_type = stringify(key_value[1])
65
+
66
+ stringified_hash[key_string] = value_string
67
+
68
+ key_types[key_string] = key_type unless key_type == "String"
69
+ value_types[key_string] = value_type unless value_type == "String"
70
+ end
71
+
72
+ # Use YAML for recording types as it is much simpler than trying to
73
+ # handle all the special characters that could be in a key or value
74
+ # and encoding them again to fit into one string. Let YAML handle all
75
+ # the mess for us.
76
+ stringified_hash["__key_types"] = YAML.dump(key_types) if key_types.present?
77
+ stringified_hash["__value_types"] = YAML.dump(value_types) if value_types.present?
78
+
79
+ stringified_hash.map do |key, value|
80
+ "#{format_key(key)}=>#{format_value(value)}"
81
+ end.join(", ")
82
+ end
83
+
84
+ def format_key(key)
85
+ %Q("#{escape(key)}")
86
+ end
87
+
88
+ def format_value(value)
89
+ value ? %Q("#{escape(value)}") : "NULL"
90
+ end
91
+
92
+ # Escape a value for use as a key or value in an hstore
93
+ def escape(value)
94
+ value
95
+ .gsub('\\', '\\\\\\')
96
+ .gsub('"', '\\"')
97
+ end
98
+
99
+ # Unescape a value from a key or value in an hstore
100
+ def unescape(value)
101
+ value
102
+ .gsub('\\\\', '\\')
103
+ .gsub('\\"', '"')
104
+ end
105
+
106
+ # Returns an array of value as a string and value type
107
+ def stringify(value)
108
+ if value.kind_of?(String)
109
+ [value, "String"]
110
+ elsif value.kind_of?(Integer)
111
+ [value.to_s, "Integer"]
112
+ elsif value.kind_of?(Float)
113
+ [value.to_s, "Float"]
114
+ elsif value.kind_of?(BigDecimal)
115
+ [value.to_s, "BigDecimal"]
116
+ elsif value.kind_of?(Date)
117
+ [value.to_s(:db), "Date"]
118
+ elsif value == nil
119
+ [nil, "String"] # we don't actually stringify nil because format_value special cases nil
120
+ else
121
+ [value.to_s, "String"] # coerce to string as we don't know how to reconstitue an unknown class
122
+ end
123
+ end
124
+
125
+ def typecast(value, type)
126
+ case type
127
+ when "Integer"
128
+ Integer(value)
129
+ when "Float"
130
+ Float(value)
131
+ when "BigDecimal"
132
+ BigDecimal(value)
133
+ when "Date"
134
+ Date.parse(value)
135
+ else
136
+ raise ArgumentError, "Can't typecast: #{type}"
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,3 @@
1
+ module Surus
2
+ VERSION = "0.0.1"
3
+ end
data/lib/surus.rb ADDED
@@ -0,0 +1,4 @@
1
+ require 'active_record'
2
+ require 'surus/version'
3
+ require 'surus/hstore/serializer'
4
+ require 'surus/hstore/scope'
data/spec/database.yml ADDED
@@ -0,0 +1,4 @@
1
+ test:
2
+ adapter: postgresql
3
+ encoding: unicode
4
+ database: ar_pg_test
@@ -0,0 +1,9 @@
1
+ CREATE EXTENSION IF NOT EXISTS hstore;
2
+
3
+ DROP TABLE IF EXISTS hstore_records;
4
+
5
+ CREATE TABLE hstore_records(
6
+ id serial PRIMARY KEY,
7
+ properties hstore
8
+ );
9
+
@@ -0,0 +1,76 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hstore::Scope do
4
+ let!(:empty) { HstoreRecord.create! :properties => {} }
5
+
6
+ context "hstore_has_pairs" do
7
+ let!(:match) { HstoreRecord.create! :properties => { "a" => "1", "b" => "2", "c" => "3" } }
8
+ let!(:missing_key) { HstoreRecord.create! :properties => { "a" => "1", "c" => "3" } }
9
+ let!(:wrong_value) { HstoreRecord.create! :properties => { "a" => "1", "b" => "5" } }
10
+
11
+ subject { HstoreRecord.hstore_has_pairs(:properties, "a" => "1", "b" => "2").all }
12
+
13
+ it { should include(match) }
14
+ it { should_not include(missing_key) }
15
+ it { should_not include(wrong_value) }
16
+ it { should_not include(empty) }
17
+ end
18
+
19
+ context "hstore_has_key" do
20
+ let!(:match) { HstoreRecord.create! :properties => { "a" => "1", "b" => "2" } }
21
+ let!(:missing_key) { HstoreRecord.create! :properties => { "a" => "1", "c" => "3" } }
22
+
23
+ subject { HstoreRecord.hstore_has_key(:properties, "b").all }
24
+
25
+ it { should include(match) }
26
+ it { should_not include(missing_key) }
27
+ it { should_not include(empty) }
28
+ end
29
+
30
+ context "hstore_has_all_keys" do
31
+ let!(:match) { HstoreRecord.create! :properties => { "a" => "1", "b" => "2", "c" => "3" } }
32
+ let!(:missing_one_key) { HstoreRecord.create! :properties => { "b" => "2", "c" => "3" } }
33
+ let!(:missing_all_keys) { HstoreRecord.create! :properties => { "f" => "1", "g" => "2" } }
34
+
35
+ def self.shared_examples
36
+ it { should include(match) }
37
+ it { should_not include(missing_one_key) }
38
+ it { should_not include(missing_all_keys) }
39
+ it { should_not include(empty) }
40
+ end
41
+
42
+ context "with array of keys" do
43
+ subject { HstoreRecord.hstore_has_all_keys(:properties, ["a", "b"]).all }
44
+ shared_examples
45
+ end
46
+
47
+ context "with multiple key arguments" do
48
+ subject { HstoreRecord.hstore_has_all_keys(:properties, "a", "b").all }
49
+ shared_examples
50
+ end
51
+ end
52
+
53
+ context "hstore_has_any_key" do
54
+ let!(:match_1) { HstoreRecord.create! :properties => { "a" => "1", "c" => "3" } }
55
+ let!(:match_2) { HstoreRecord.create! :properties => { "b" => "2", "d" => "4" } }
56
+ let!(:missing_all_keys) { HstoreRecord.create! :properties => { "c" => "3", "d" => "4" } }
57
+
58
+ def self.shared_examples
59
+ it { should include(match_1) }
60
+ it { should include(match_2) }
61
+ it { should_not include(missing_all_keys) }
62
+ it { should_not include(empty) }
63
+ it { should_not include(empty) }
64
+ end
65
+
66
+ context "with array of keys" do
67
+ subject { HstoreRecord.hstore_has_any_keys(:properties, ["a", "b"]).all }
68
+ shared_examples
69
+ end
70
+
71
+ context "with multiple key arguments" do
72
+ subject { HstoreRecord.hstore_has_any_keys(:properties, "a", "b").all }
73
+ shared_examples
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,61 @@
1
+ require 'spec_helper'
2
+
3
+ describe Hstore::Serializer do
4
+ round_trip_examples = [
5
+ [nil, "nil"],
6
+ [{}, "empty hash"],
7
+ [{"foo" => "bar"}, "single key/value pair"],
8
+ [{"foo" => "bar", "baz" => "quz"}, "multiple key/value pairs"],
9
+ [{"foo" => nil}, "value is nil"],
10
+ ]
11
+
12
+ [
13
+ ['"', 'double quote (")'],
14
+ ["'", "single quote (')"],
15
+ ["\\", "backslash (\\)"],
16
+ ["\\", "multiple backslashes (\\)"],
17
+ ["=>", "separator (=>)"],
18
+ [" ", "space"],
19
+ [%q~\ / / \\ => " ' " '~, "multiple special characters"]
20
+ ].each do |value, description|
21
+ round_trip_examples << [{"#{value}foo" => "bar"}, "key with #{description} at beginning"]
22
+ round_trip_examples << [{"foo#{value}foo" => "bar"}, "key with #{description} in middle"]
23
+ round_trip_examples << [{"foo#{value}" => "bar"}, "key with #{description} at end"]
24
+ round_trip_examples << [{value => "bar"}, "key is #{description}"]
25
+
26
+ round_trip_examples << [{"foo" => "#{value}bar"}, "value with #{description} at beginning"]
27
+ round_trip_examples << [{"foo" => "bar#{value}bar"}, "value with #{description} in middle"]
28
+ round_trip_examples << [{"foo" => "bar#{value}"}, "value with #{description} at end"]
29
+ round_trip_examples << [{"foo" => value}, "value is #{description}"]
30
+ end
31
+
32
+ [
33
+ [0, "integer 0"],
34
+ [1, "positive integer"],
35
+ [-1, "negative integer"],
36
+ [1_000_000_000_000_000_000_000, "huge positive integer"],
37
+ [-1_000_000_000_000_000_000_000, "huge negative integer"],
38
+ [0.0, "float 0.0"],
39
+ [-0.0, "float -0.0"],
40
+ [1.5, "positive float"],
41
+ [-1.5, "negative float"],
42
+ [Float::MAX, "maximum float"],
43
+ [Float::MIN, "minimum float"],
44
+ [BigDecimal("0"), "BigDecimal 0"],
45
+ [BigDecimal("1"), "positive BigDecimal"],
46
+ [BigDecimal("-1"), "negative BigDecimal"],
47
+ [Date.today, "date"]
48
+ ].each do |value, description|
49
+ round_trip_examples << [{"foo" => value}, "value is #{description}"]
50
+ round_trip_examples << [{value => "bar"}, "key is #{description}"]
51
+ round_trip_examples << [{value => value}, "key and value are each #{description}"]
52
+ end
53
+
54
+ round_trip_examples.each do |value, description|
55
+ it "round trips when #{description}" do
56
+ r = HstoreRecord.create! :properties => value
57
+ r.reload
58
+ r.properties.should == value
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,24 @@
1
+ require 'surus'
2
+ require 'yaml'
3
+
4
+ require 'rspec'
5
+
6
+ database_config = YAML.load_file(File.expand_path("../database.yml", __FILE__))
7
+ ActiveRecord::Base.establish_connection database_config["test"]
8
+
9
+
10
+ class HstoreRecord < ActiveRecord::Base
11
+ serialize :properties, Hstore::Serializer.new
12
+ end
13
+
14
+
15
+
16
+ RSpec.configure do |config|
17
+ config.around do |example|
18
+ ActiveRecord::Base.transaction do
19
+ example.call
20
+ raise ActiveRecord::Rollback
21
+ end
22
+ end
23
+ end
24
+
data/surus.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "surus/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "surus"
7
+ s.version = Surus::VERSION
8
+ s.authors = ["Jack Christensen"]
9
+ s.email = ["jack@jackchristensen.com"]
10
+ s.homepage = "https://github.com/JackC/surus"
11
+ s.summary = %q{PostgreSQL extensions for ActiveRecord}
12
+ s.description = %q{PostgreSQL extensions for ActiveRecord}
13
+
14
+ s.rubyforge_project = ""
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ # specify any dependencies here; for example:
22
+ s.add_dependency 'pg'
23
+ s.add_dependency 'activerecord', ">= 3.1.0"
24
+
25
+ s.add_development_dependency 'rspec', "~> 2.8.0"
26
+ s.add_development_dependency 'guard', ">= 0.10.0"
27
+ s.add_development_dependency 'guard-rspec', ">= 0.6.0"
28
+ end
metadata ADDED
@@ -0,0 +1,117 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: surus
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jack Christensen
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-02-01 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: pg
16
+ requirement: &10920640 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *10920640
25
+ - !ruby/object:Gem::Dependency
26
+ name: activerecord
27
+ requirement: &10916840 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: 3.1.0
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *10916840
36
+ - !ruby/object:Gem::Dependency
37
+ name: rspec
38
+ requirement: &10930900 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ version: 2.8.0
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *10930900
47
+ - !ruby/object:Gem::Dependency
48
+ name: guard
49
+ requirement: &10927660 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: 0.10.0
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *10927660
58
+ - !ruby/object:Gem::Dependency
59
+ name: guard-rspec
60
+ requirement: &10943960 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: 0.6.0
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *10943960
69
+ description: PostgreSQL extensions for ActiveRecord
70
+ email:
71
+ - jack@jackchristensen.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - .gitignore
77
+ - .rspec
78
+ - Gemfile
79
+ - Guardfile
80
+ - LICENSE
81
+ - README.md
82
+ - Rakefile
83
+ - lib/surus.rb
84
+ - lib/surus/hstore/scope.rb
85
+ - lib/surus/hstore/serializer.rb
86
+ - lib/surus/version.rb
87
+ - spec/database.yml
88
+ - spec/database_structure.sql
89
+ - spec/hstore/scope_spec.rb
90
+ - spec/hstore/serializer_spec.rb
91
+ - spec/spec_helper.rb
92
+ - surus.gemspec
93
+ homepage: https://github.com/JackC/surus
94
+ licenses: []
95
+ post_install_message:
96
+ rdoc_options: []
97
+ require_paths:
98
+ - lib
99
+ required_ruby_version: !ruby/object:Gem::Requirement
100
+ none: false
101
+ requirements:
102
+ - - ! '>='
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ required_rubygems_version: !ruby/object:Gem::Requirement
106
+ none: false
107
+ requirements:
108
+ - - ! '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ requirements: []
112
+ rubyforge_project: ''
113
+ rubygems_version: 1.8.11
114
+ signing_key:
115
+ specification_version: 3
116
+ summary: PostgreSQL extensions for ActiveRecord
117
+ test_files: []