hstore-document 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a6b822aa3298952dc3444dc2c5198d92c7901aae
4
+ data.tar.gz: 2e08a5d7627f638ffd4c68d9618075172832f312
5
+ SHA512:
6
+ metadata.gz: eba7709d09faff0caf967936a1be26ed47c6e5a2e937d1d500b79144555f7f58926cb3f4711dbc33e476cbe399ea273e813c756c9c7aa1990f23554d8ed35290
7
+ data.tar.gz: 3206bd0eaaa2fbc5d957191247fef144e4e5f8d5078e1c73d3256214ec21beb97130a28964b93eaedb4e10418c23927e2fa5168748aa695d8282a61290f2a712
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,21 @@
1
+ source "http://rubygems.org"
2
+ # Add dependencies required to use your gem here.
3
+ # Example:
4
+ # gem "activesupport", ">= 2.3.5"
5
+
6
+ # Add dependencies to develop your gem here.
7
+ # Include everything needed to run rake, tests, features, etc.
8
+ group :development do
9
+ gem "rspec", "~> 2.14"
10
+ gem "rdoc", "~> 3.12"
11
+ gem "bundler", "~> 1.0"
12
+ gem "jeweler", "~> 1.8.7"
13
+ gem "pry"
14
+ gem "pry-debugger"
15
+ gem "guard"
16
+ gem "guard-rspec"
17
+ end
18
+
19
+ gem 'pg'
20
+ gem 'pg-hstore'
21
+ gem 'activerecord', '>=3.2', '<4.1'
data/Gemfile.lock ADDED
@@ -0,0 +1,137 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ activemodel (4.0.4)
5
+ activesupport (= 4.0.4)
6
+ builder (~> 3.1.0)
7
+ activerecord (4.0.4)
8
+ activemodel (= 4.0.4)
9
+ activerecord-deprecated_finders (~> 1.0.2)
10
+ activesupport (= 4.0.4)
11
+ arel (~> 4.0.0)
12
+ activerecord-deprecated_finders (1.0.3)
13
+ activesupport (4.0.4)
14
+ i18n (~> 0.6, >= 0.6.9)
15
+ minitest (~> 4.2)
16
+ multi_json (~> 1.3)
17
+ thread_safe (~> 0.1)
18
+ tzinfo (~> 0.3.37)
19
+ addressable (2.3.6)
20
+ arel (4.0.2)
21
+ atomic (1.1.16)
22
+ builder (3.1.4)
23
+ celluloid (0.15.2)
24
+ timers (~> 1.1.0)
25
+ celluloid-io (0.15.0)
26
+ celluloid (>= 0.15.0)
27
+ nio4r (>= 0.5.0)
28
+ coderay (1.1.0)
29
+ columnize (0.3.6)
30
+ debugger (1.6.6)
31
+ columnize (>= 0.3.1)
32
+ debugger-linecache (~> 1.2.0)
33
+ debugger-ruby_core_source (~> 1.3.2)
34
+ debugger-linecache (1.2.0)
35
+ debugger-ruby_core_source (1.3.2)
36
+ diff-lcs (1.2.5)
37
+ faraday (0.8.9)
38
+ multipart-post (~> 1.2.0)
39
+ ffi (1.9.3)
40
+ formatador (0.2.4)
41
+ git (1.2.6)
42
+ github_api (0.10.1)
43
+ addressable
44
+ faraday (~> 0.8.1)
45
+ hashie (>= 1.2)
46
+ multi_json (~> 1.4)
47
+ nokogiri (~> 1.5.2)
48
+ oauth2
49
+ guard (2.6.0)
50
+ formatador (>= 0.2.4)
51
+ listen (~> 2.7)
52
+ lumberjack (~> 1.0)
53
+ pry (>= 0.9.12)
54
+ thor (>= 0.18.1)
55
+ guard-rspec (4.2.8)
56
+ guard (~> 2.1)
57
+ rspec (>= 2.14, < 4.0)
58
+ hashie (2.0.5)
59
+ highline (1.6.21)
60
+ i18n (0.6.9)
61
+ jeweler (1.8.8)
62
+ builder
63
+ bundler (~> 1.0)
64
+ git (>= 1.2.5)
65
+ github_api (= 0.10.1)
66
+ highline (>= 1.6.15)
67
+ nokogiri (= 1.5.10)
68
+ rake
69
+ rdoc
70
+ json (1.8.1)
71
+ jwt (0.1.11)
72
+ multi_json (>= 1.5)
73
+ listen (2.7.1)
74
+ celluloid (>= 0.15.2)
75
+ celluloid-io (>= 0.15.0)
76
+ rb-fsevent (>= 0.9.3)
77
+ rb-inotify (>= 0.9)
78
+ lumberjack (1.0.5)
79
+ method_source (0.8.2)
80
+ minitest (4.7.5)
81
+ multi_json (1.9.2)
82
+ multi_xml (0.5.5)
83
+ multipart-post (1.2.0)
84
+ nio4r (1.0.0)
85
+ nokogiri (1.5.10)
86
+ oauth2 (0.9.3)
87
+ faraday (>= 0.8, < 0.10)
88
+ jwt (~> 0.1.8)
89
+ multi_json (~> 1.3)
90
+ multi_xml (~> 0.5)
91
+ rack (~> 1.2)
92
+ pg (0.17.1)
93
+ pg-hstore (1.2.0)
94
+ pry (0.9.12.6)
95
+ coderay (~> 1.0)
96
+ method_source (~> 0.8)
97
+ slop (~> 3.4)
98
+ pry-debugger (0.2.2)
99
+ debugger (~> 1.3)
100
+ pry (~> 0.9.10)
101
+ rack (1.5.2)
102
+ rake (10.2.2)
103
+ rb-fsevent (0.9.4)
104
+ rb-inotify (0.9.3)
105
+ ffi (>= 0.5.0)
106
+ rdoc (3.12.2)
107
+ json (~> 1.4)
108
+ rspec (2.14.1)
109
+ rspec-core (~> 2.14.0)
110
+ rspec-expectations (~> 2.14.0)
111
+ rspec-mocks (~> 2.14.0)
112
+ rspec-core (2.14.8)
113
+ rspec-expectations (2.14.5)
114
+ diff-lcs (>= 1.1.3, < 2.0)
115
+ rspec-mocks (2.14.6)
116
+ slop (3.5.0)
117
+ thor (0.19.1)
118
+ thread_safe (0.3.1)
119
+ atomic (>= 1.1.7, < 2)
120
+ timers (1.1.0)
121
+ tzinfo (0.3.39)
122
+
123
+ PLATFORMS
124
+ ruby
125
+
126
+ DEPENDENCIES
127
+ activerecord (>= 3.2, < 4.1)
128
+ bundler (~> 1.0)
129
+ guard
130
+ guard-rspec
131
+ jeweler (~> 1.8.7)
132
+ pg
133
+ pg-hstore
134
+ pry
135
+ pry-debugger
136
+ rdoc (~> 3.12)
137
+ rspec (~> 2.14)
data/Guardfile ADDED
@@ -0,0 +1,24 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard :rspec do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+
9
+ # Rails example
10
+ watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
11
+ watch(%r{^app/(.*)(\.erb|\.haml|\.slim)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
12
+ watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] }
13
+ watch(%r{^spec/support/(.+)\.rb$}) { "spec" }
14
+ watch('config/routes.rb') { "spec/routing" }
15
+ watch('app/controllers/application_controller.rb') { "spec/controllers" }
16
+
17
+ # Capybara features specs
18
+ watch(%r{^app/views/(.+)/.*\.(erb|haml|slim)$}) { |m| "spec/features/#{m[1]}_spec.rb" }
19
+
20
+ # Turnip features and steps
21
+ watch(%r{^spec/acceptance/(.+)\.feature$})
22
+ watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance' }
23
+ end
24
+
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2013 Roman Shterenzon
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # hstore-document
2
+
3
+ This gem allows embedding of an auxillary object in an ActiveRecord::Base model using Hstore.
4
+ This brings the niceties of Mongo's embedded documents to PostgreSQL.
5
+ It's modelled after Mongoid's embedded documents.
6
+
7
+ An example is worth a thousand words:
8
+
9
+ ```ruby
10
+
11
+ class Person < ActiveRecord::Base
12
+ embeds :address
13
+
14
+ # you can even do that:
15
+ accepts_nested_attributes_for :address
16
+ end
17
+
18
+ class Address
19
+ include Hstore::Document
20
+
21
+ field :street
22
+ field :number, type: Fixnum
23
+ field :zip, type: Fixnum
24
+ field :business, type: Boolean
25
+ # though it's probably a very bad idea to store large arrays
26
+ field :phones, type: Array
27
+
28
+ validates_presence_of :street, :number
29
+ validates_numericality_of :number, :zip
30
+
31
+ before_validation :check_zip_code
32
+
33
+ # etc.
34
+ end
35
+
36
+ person = Person.new
37
+ person.address = Address.new(street: 'Elm', number: 'invalid')
38
+ person.save # => false (validations failed)
39
+
40
+ person.address.number = 13
41
+ person.save
42
+
43
+ person.address.destroy
44
+ ```
45
+
46
+ ## TODO
47
+
48
+ * Rails 4.1 support
49
+ * Query support, e.g. Person.where('address.street' => 'Elm')
50
+
51
+ ## Contributing to hstore-document
52
+
53
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
54
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
55
+ * Fork the project.
56
+ * Start a feature/bugfix branch.
57
+ * Commit and push until you are happy with your contribution.
58
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
59
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
60
+
61
+ ## Copyright
62
+
63
+ Copyright (c) 2013 Roman Shterenzon. See LICENSE.txt for further details.
data/Rakefile ADDED
@@ -0,0 +1,49 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
+ gem.name = "hstore-document"
18
+ gem.homepage = "http://github.com/romanbsd/hstore-document"
19
+ gem.license = "MIT"
20
+ gem.summary = %Q{Embedded documents using HStore}
21
+ gem.description = %Q{Allows embedding documents in ActiveRecord models using PostgreSQL HStore}
22
+ gem.email = "roman.shterenzon@gmail.com"
23
+ gem.authors = ["Roman Shterenzon"]
24
+ # dependencies defined in Gemfile
25
+ end
26
+ Jeweler::RubygemsDotOrgTasks.new
27
+
28
+ require 'rspec/core'
29
+ require 'rspec/core/rake_task'
30
+ RSpec::Core::RakeTask.new(:spec) do |spec|
31
+ spec.pattern = FileList['spec/**/*_spec.rb']
32
+ end
33
+
34
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
35
+ spec.pattern = 'spec/**/*_spec.rb'
36
+ spec.rcov = true
37
+ end
38
+
39
+ task :default => :spec
40
+
41
+ require 'rdoc/task'
42
+ Rake::RDocTask.new do |rdoc|
43
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
44
+
45
+ rdoc.rdoc_dir = 'rdoc'
46
+ rdoc.title = "hstore-document #{version}"
47
+ rdoc.rdoc_files.include('README*')
48
+ rdoc.rdoc_files.include('lib/**/*.rb')
49
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,91 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+ # stub: hstore-document 0.1.0 ruby lib
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = "hstore-document"
9
+ s.version = "0.1.0"
10
+
11
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
12
+ s.require_paths = ["lib"]
13
+ s.authors = ["Roman Shterenzon"]
14
+ s.date = "2014-04-22"
15
+ s.description = "Allows embedding documents in ActiveRecord models using PostgreSQL HStore"
16
+ s.email = "roman.shterenzon@gmail.com"
17
+ s.extra_rdoc_files = [
18
+ "LICENSE.txt",
19
+ "README.md"
20
+ ]
21
+ s.files = [
22
+ ".document",
23
+ ".rspec",
24
+ "Gemfile",
25
+ "Gemfile.lock",
26
+ "Guardfile",
27
+ "LICENSE.txt",
28
+ "README.md",
29
+ "Rakefile",
30
+ "VERSION",
31
+ "hstore-document.gemspec",
32
+ "lib/active_record/associations/builder/embeds_one.rb",
33
+ "lib/active_record/associations/embeds_one_association.rb",
34
+ "lib/active_record/embeds_reflection.rb",
35
+ "lib/hstore-document.rb",
36
+ "lib/hstore/document.rb",
37
+ "lib/hstore/fields.rb",
38
+ "lib/hstore/railtie.rb",
39
+ "spec/document_spec.rb",
40
+ "spec/integration_spec.rb",
41
+ "spec/spec_helper.rb",
42
+ "spec/support/models.rb"
43
+ ]
44
+ s.homepage = "http://github.com/romanbsd/hstore-document"
45
+ s.licenses = ["MIT"]
46
+ s.rubygems_version = "2.2.2"
47
+ s.summary = "Embedded documents using HStore"
48
+
49
+ if s.respond_to? :specification_version then
50
+ s.specification_version = 4
51
+
52
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
53
+ s.add_runtime_dependency(%q<pg>, [">= 0"])
54
+ s.add_runtime_dependency(%q<pg-hstore>, [">= 0"])
55
+ s.add_runtime_dependency(%q<activerecord>, ["< 4.1", ">= 3.2"])
56
+ s.add_development_dependency(%q<rspec>, ["~> 2.14"])
57
+ s.add_development_dependency(%q<rdoc>, ["~> 3.12"])
58
+ s.add_development_dependency(%q<bundler>, ["~> 1.0"])
59
+ s.add_development_dependency(%q<jeweler>, ["~> 1.8.7"])
60
+ s.add_development_dependency(%q<pry>, [">= 0"])
61
+ s.add_development_dependency(%q<pry-debugger>, [">= 0"])
62
+ s.add_development_dependency(%q<guard>, [">= 0"])
63
+ s.add_development_dependency(%q<guard-rspec>, [">= 0"])
64
+ else
65
+ s.add_dependency(%q<pg>, [">= 0"])
66
+ s.add_dependency(%q<pg-hstore>, [">= 0"])
67
+ s.add_dependency(%q<activerecord>, ["< 4.1", ">= 3.2"])
68
+ s.add_dependency(%q<rspec>, ["~> 2.14"])
69
+ s.add_dependency(%q<rdoc>, ["~> 3.12"])
70
+ s.add_dependency(%q<bundler>, ["~> 1.0"])
71
+ s.add_dependency(%q<jeweler>, ["~> 1.8.7"])
72
+ s.add_dependency(%q<pry>, [">= 0"])
73
+ s.add_dependency(%q<pry-debugger>, [">= 0"])
74
+ s.add_dependency(%q<guard>, [">= 0"])
75
+ s.add_dependency(%q<guard-rspec>, [">= 0"])
76
+ end
77
+ else
78
+ s.add_dependency(%q<pg>, [">= 0"])
79
+ s.add_dependency(%q<pg-hstore>, [">= 0"])
80
+ s.add_dependency(%q<activerecord>, ["< 4.1", ">= 3.2"])
81
+ s.add_dependency(%q<rspec>, ["~> 2.14"])
82
+ s.add_dependency(%q<rdoc>, ["~> 3.12"])
83
+ s.add_dependency(%q<bundler>, ["~> 1.0"])
84
+ s.add_dependency(%q<jeweler>, ["~> 1.8.7"])
85
+ s.add_dependency(%q<pry>, [">= 0"])
86
+ s.add_dependency(%q<pry-debugger>, [">= 0"])
87
+ s.add_dependency(%q<guard>, [">= 0"])
88
+ s.add_dependency(%q<guard-rspec>, [">= 0"])
89
+ end
90
+ end
91
+
@@ -0,0 +1,18 @@
1
+ require 'active_record/associations/builder/association'
2
+ require 'active_record/associations/builder/singular_association'
3
+
4
+ module ActiveRecord
5
+ module Associations
6
+ module Builder
7
+ class EmbedsOne < SingularAssociation # :nodoc:
8
+ self.macro = :embeds_one if respond_to?(:macro=)
9
+ self.valid_options = [:class_name]
10
+
11
+ # For AR 4.0
12
+ def macro
13
+ :embeds_one
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,77 @@
1
+ require 'active_record/associations/association'
2
+
3
+ module ActiveRecord
4
+ module Associations
5
+ module ClassMethods
6
+ # @param [Symbol, String] name of association
7
+ # @param [Hash] options
8
+ # @option options [Boolean] :validate Validate associaited object (default: true)
9
+ # @option options [String] :class_name Name of the associated class
10
+ def embeds(name, options = {})
11
+ validates_associated(name) unless options.delete(:validate) == false
12
+ if ActiveRecord::VERSION::MAJOR < 4
13
+ Builder::EmbedsOne.build(self, name, options)
14
+ else
15
+ Builder::EmbedsOne.build(self, name, options, nil)
16
+ end
17
+ end
18
+ end
19
+
20
+ class EmbedsOneAssociation < Association # :nodoc:
21
+ def reader(force_reload = false)
22
+ return @reader if @reader
23
+ data = owner.read_attribute(reflection.name)
24
+ if data.present?
25
+ reflection.klass.from_hstore(data).tap do |inst|
26
+ inst.send(:_owner=, owner)
27
+ @reader = inst
28
+ end
29
+ end
30
+ end
31
+
32
+ def writer(record)
33
+ replace(record, false)
34
+ end
35
+
36
+ def replace(record, save = true)
37
+ @reader = record
38
+ if record
39
+ record.send(:_owner=, owner)
40
+ end
41
+ owner.send(:write_attribute, reflection.name, record ? record.to_hstore : nil)
42
+ owner.save if save
43
+ end
44
+
45
+ def create(attributes = {}, options = {}, &block)
46
+ create_record(attributes, options, &block)
47
+ end
48
+
49
+ def create!(attributes = {}, options = {}, &block)
50
+ create_record(attributes, options, true, &block)
51
+ end
52
+
53
+ def build(attributes = {}, options = {})
54
+ build_record(attributes, options).tap do |record|
55
+ yield(record) if block_given?
56
+ replace(record, false)
57
+ end
58
+ end
59
+
60
+ private
61
+ def build_record(attributes, options = nil)
62
+ reflection.build_association(attributes) do |record|
63
+ record.assign_attributes(attributes, without_protection: true)
64
+ end
65
+ end
66
+
67
+ def create_record(attributes, options, raise_error = false)
68
+ build_record(attributes, options).tap do |record|
69
+ yield record if block_given?
70
+ saved = replace(record)
71
+ raise RecordInvalid.new(record) if !saved && raise_error
72
+ end
73
+ end
74
+
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,40 @@
1
+ require 'active_support/core_ext/module/delegation'
2
+ require 'active_record/reflection'
3
+
4
+ module ActiveRecord
5
+ module Reflection
6
+ class AssociationReflection # :nodoc:
7
+ def association_class_with_embeds_one
8
+ if macro == :embeds_one
9
+ Associations::EmbedsOneAssociation
10
+ else
11
+ association_class_without_embeds_one
12
+ end
13
+ end
14
+ alias_method_chain :association_class, :embeds_one
15
+ end
16
+
17
+ module ClassMethods # :nodoc:
18
+ if ActiveRecord::VERSION::MAJOR < 4
19
+ def create_reflection_with_embeds(macro, name, options, active_record)
20
+ unless macro == :embeds_one
21
+ return create_reflection_without_embeds(macro, name, options, active_record)
22
+ end
23
+ AssociationReflection.new(macro, name, options, active_record).tap do |reflection|
24
+ reflections.merge!(name => reflection)
25
+ end
26
+ end
27
+ else
28
+ def create_reflection_with_embeds(macro, name, scope, options, active_record)
29
+ unless macro == :embeds_one
30
+ return create_reflection_without_embeds(macro, name, scope, options, active_record)
31
+ end
32
+ AssociationReflection.new(macro, name, scope, options, active_record).tap do |reflection|
33
+ reflections.merge!(name => reflection)
34
+ end
35
+ end
36
+ end
37
+ alias_method_chain :create_reflection, :embeds
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,9 @@
1
+ if defined?(Rails)
2
+ require 'hstore/railtie'
3
+ else
4
+ require 'active_record'
5
+ require 'active_record/embeds_reflection'
6
+ require 'active_record/associations/builder/embeds_one'
7
+ require 'active_record/associations/embeds_one_association'
8
+ require 'hstore/document'
9
+ end
@@ -0,0 +1,217 @@
1
+ require 'active_support/concern'
2
+ require 'active_model/naming'
3
+ require 'active_model/conversion'
4
+ require 'active_model/translation'
5
+ require 'active_model/validations'
6
+ require 'active_record/readonly_attributes'
7
+ require 'pg_hstore'
8
+ require 'hstore/fields'
9
+
10
+ module Hstore
11
+ module Document
12
+ extend ActiveSupport::Concern
13
+
14
+ included do
15
+ include Hstore::Fields
16
+ include ActiveModel::Conversion
17
+ include ActiveModel::Validations
18
+ include ActiveModel::Serialization
19
+ include ActiveModel::Dirty
20
+ include ActiveModel::AttributeMethods
21
+ include ActiveRecord::AttributeAssignment
22
+ include ActiveRecord::ReadonlyAttributes
23
+ include ActiveRecord::AttributeMethods::BeforeTypeCast
24
+ include ActiveRecord::AttributeMethods::Serialization
25
+ extend ActiveModel::Naming
26
+
27
+ class << self
28
+ def attributes_protected_by_default
29
+ []
30
+ end
31
+ end
32
+
33
+ class_attribute :_attr_defaults, instance_writer: false
34
+ self._attr_defaults = {}
35
+ alias_method :as_json, :serializable_hash
36
+ end
37
+
38
+ Attribute = ActiveRecord::AttributeMethods::Serialization::Attribute
39
+ class OwnerMissingError < StandardError; end
40
+
41
+ def initialize(attributes = nil, options = {})
42
+ @attributes = {}
43
+ assign_attributes(self.class._attr_defaults)
44
+
45
+ if options[:serialized] and attributes.present?
46
+ attributes = attributes.stringify_keys
47
+ self.class.serialized_attributes.each do |key, coder|
48
+ if attributes.key?(key)
49
+ attributes[key] = Attribute.new(coder, attributes[key], :serialized)
50
+ end
51
+ end
52
+ assign_attributes(attributes.except(self.class.serialized_attributes.keys))
53
+ else
54
+ assign_attributes(attributes)
55
+ end
56
+ changed_attributes.clear
57
+ end
58
+
59
+ def attributes
60
+ attrs = {}
61
+ @attributes.each_key { |name| attrs[name] = read_attribute(name) }
62
+ attrs
63
+ end
64
+
65
+ def eql?(other)
66
+ other.attributes == attributes
67
+ end
68
+ alias :== :eql?
69
+
70
+ def update_attribute(name, value)
71
+ name = name.to_s
72
+ raise ActiveRecordError, "#{name} is marked as readonly" if self.class.readonly_attributes.include?(name)
73
+ send("#{name}=", value)
74
+ save(validate: false)
75
+ end
76
+
77
+ def read_attribute(name)
78
+ name = name.to_s
79
+ if @attributes.key?(name)
80
+ val = @attributes[name]
81
+ if self.class.serialized_attributes.key?(name)
82
+ val = val.unserialized_value
83
+ end
84
+ val
85
+ else
86
+ _attr_defaults[name]
87
+ end
88
+ end
89
+
90
+ def write_attribute(name, value)
91
+ name = name.to_s
92
+ if coder = self.class.serialized_attributes[name] and ! value.respond_to?(:serialized_value)
93
+ value = Attribute.new(coder, value, :unserialized)
94
+ end
95
+ attribute_will_change!(name)
96
+ @attributes[name] = value
97
+ end
98
+
99
+ def update_attributes(attributes, options)
100
+ assign_attributes(attributes, options)
101
+ save
102
+ end
103
+
104
+ def update_attributes!(attributes, options)
105
+ assign_attributes(attributes, options)
106
+ save!
107
+ end
108
+
109
+ def persisted?
110
+ ! changed? && _owner && _owner.persisted?
111
+ end
112
+
113
+ def destroyed?
114
+ @_destroyed || (_owner && _owner.destroyed?)
115
+ end
116
+
117
+ def marked_for_destruction?
118
+ _owner && _owner.marked_for_destruction?
119
+ end
120
+
121
+ def save(options = {})
122
+ perform_validations(options) ? create_or_update : false
123
+ end
124
+
125
+ # XXX the proper way would have been to introduce an embedded_in macro, which would
126
+ # even support polymorphic associations, but it's a lot of work, overhead, and I
127
+ # think that accesing the parent from embedded object should be discouraged as a matter
128
+ # of principle.
129
+ def destroy
130
+ @_destroyed = true
131
+ _owner.update_attribute(embedded_as, nil) if _owner
132
+ end
133
+
134
+ def to_hstore
135
+ PgHstore.dump(attributes_after_type_cast, true)
136
+ end
137
+
138
+ private
139
+
140
+ attr_accessor :_owner
141
+
142
+ def embedded_as
143
+ self.class.name.demodulize.underscore
144
+ end
145
+
146
+ def perform_validations(options = {})
147
+ perform_validation = options[:validate] != false
148
+ perform_validation ? valid?(options[:context]) : true
149
+ end
150
+
151
+ def create_or_update
152
+ raise OwnerMissingError unless _owner
153
+ changed_attributes.clear if _owner.update_attribute(embedded_as, self)
154
+ end
155
+
156
+ def attributes_after_type_cast
157
+ @attributes.dup.tap do |attributes|
158
+ self.class.serialized_attributes.each_key do |key|
159
+ if attributes.key?(key)
160
+ attributes[key] = attributes[key].serialized_value
161
+ end
162
+ end
163
+ end
164
+ end
165
+
166
+ module ClassMethods
167
+
168
+ def from_hstore(data)
169
+ if ActiveRecord::VERSION::MAJOR < 4
170
+ data = PgHstore.load(data)
171
+ end
172
+ new(data, serialized: true)
173
+ end
174
+
175
+ def field(name, options = {})
176
+ klass = options[:type]
177
+
178
+ default = options[:default]
179
+ _attr_defaults[name.to_s] = default unless default.nil?
180
+
181
+ if klass.to_s.start_with?('Hstore::Fields::')
182
+ serialize(name, klass)
183
+ end
184
+ define_accessors(name)
185
+ end
186
+
187
+ private
188
+ def define_accessors(name)
189
+ define_reader name
190
+ define_writer name
191
+ end
192
+
193
+ def define_reader(name)
194
+ define_method(name) do
195
+ read_attribute(name)
196
+ end
197
+ end
198
+
199
+ def define_writer(name)
200
+ define_method("#{name}=".to_sym) do |value|
201
+ write_attribute(name, value)
202
+ end
203
+ end
204
+ end
205
+
206
+ end
207
+ end
208
+
209
+ =begin
210
+ $: << Dir.pwd + '/lib'; true
211
+ require 'hstore-document'
212
+ ActiveRecord::Base.establish_connection(adapter: 'postgresql', database: 'hstore_document', username: 'postgres', password: 'postgres')
213
+ class Address; include Hstore::Document; field :street; end
214
+ class Person < ActiveRecord::Base; embeds :address; end
215
+ a = Address.new(bar: true)
216
+ p = Person.new(address: a)
217
+ =end
@@ -0,0 +1,60 @@
1
+ module Hstore
2
+ module Fields # :nodoc:
3
+
4
+ class Boolean
5
+ def self.load(str)
6
+ case str
7
+ when 't'
8
+ true
9
+ when 'f'
10
+ false
11
+ else
12
+ nil
13
+ end
14
+ end
15
+
16
+ def self.dump(obj)
17
+ case obj
18
+ when true
19
+ 't'
20
+ when false
21
+ 'f'
22
+ else
23
+ nil
24
+ end
25
+ end
26
+ end
27
+
28
+ class Fixnum
29
+ def self.load(str)
30
+ str.to_i if str.present?
31
+ end
32
+
33
+ def self.dump(obj)
34
+ obj.to_s
35
+ end
36
+ end
37
+ Integer = Fixnum
38
+
39
+ class Array
40
+ def self.load(str)
41
+ MultiJson.load(str) if str.present?
42
+ end
43
+
44
+ def self.dump(obj)
45
+ MultiJson.dump(obj)
46
+ end
47
+ end
48
+
49
+ class Time
50
+ def self.load(str)
51
+ Time.parse(str) if str.present?
52
+ end
53
+
54
+ def self.dump(obj)
55
+ obj.strftime("%Y-%m-%d %H:%M:%S") if obj.present?
56
+ end
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1,14 @@
1
+ module Hstore
2
+ class Railtie < Rails::Railtie
3
+
4
+ initializer 'hstore_document' do
5
+ ActiveSupport.on_load :active_record do
6
+ require 'active_record/embeds_reflection'
7
+ require 'active_record/associations/builder/embeds_one'
8
+ require 'active_record/associations/embeds_one_association'
9
+ require 'hstore/document'
10
+ end
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,87 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe Hstore::Document do
4
+
5
+ subject { Address.new(street: 'Elm', number: 13) }
6
+
7
+ describe "Instantiation" do
8
+ it "builds" do
9
+ subject.street.should eq('Elm')
10
+ subject.number.should eq(13)
11
+ end
12
+
13
+ it "has defaults" do
14
+ address = Address.new
15
+ address.street.should eq('unknown')
16
+ address.street = nil
17
+ address.street.should be_nil
18
+ end
19
+ end
20
+
21
+ describe "Validations" do
22
+ subject { StrictAddress.new }
23
+
24
+ it "validates" do
25
+ subject.street = nil
26
+ subject.should_not be_valid
27
+ subject.errors[:street].should_not be_empty
28
+ subject.errors[:number].should_not be_empty
29
+
30
+ subject.street = 'Oak'
31
+ subject.number = 7
32
+ subject.should be_valid
33
+ end
34
+ end
35
+
36
+ describe "Serialization" do
37
+ before { subject.business = true }
38
+ let(:data) { %{"street"=>"Elm","number"=>"13","business"=>"t"} }
39
+
40
+ it "serializes to json" do
41
+ subject.as_json.should eq({
42
+ 'street' => 'Elm',
43
+ 'number' => 13,
44
+ 'business' => true
45
+ })
46
+ end
47
+
48
+ it "serializes to hstore" do
49
+ subject.to_hstore.should eq(data)
50
+ subject.business = false
51
+ subject.to_hstore.should eq(%{"street"=>"Elm","number"=>"13","business"=>"f"})
52
+ end
53
+
54
+ it "unserializes from hstore" do
55
+ serialized = if ActiveRecord::VERSION::MAJOR < 4
56
+ data
57
+ else
58
+ {"street"=>"Elm","number"=>"13","business"=>"t"}
59
+ end
60
+ obj = Address.from_hstore(serialized)
61
+ obj.should be_a_kind_of(Address)
62
+ obj.street.should eq('Elm')
63
+ obj.business.should eq(true)
64
+ obj.number.should eq(13)
65
+ end
66
+
67
+ end
68
+
69
+ describe "State change tracking" do
70
+ subject { Address.new }
71
+
72
+ it { should_not be_changed }
73
+
74
+ it "is changed when attribute is changed" do
75
+ subject.street = 'Elm'
76
+ subject.should be_changed
77
+ end
78
+
79
+ it "destroys" do
80
+ expect { subject.destroy }.to change { subject.destroyed? }.to(true)
81
+ end
82
+ end
83
+
84
+ it "raises error when save is attempted" do
85
+ expect { subject.save }.to raise_error(Hstore::Document::OwnerMissingError)
86
+ end
87
+ end
@@ -0,0 +1,117 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "Embedded" do
4
+
5
+ subject { Person.new }
6
+ let(:address) { Address.new(street: 'Elm', number: 13) }
7
+
8
+ let(:valid_attributes) do
9
+ {street: 'Birch', number: 1, business: false}
10
+ end
11
+
12
+ describe "Assignment" do
13
+ it "assigns" do
14
+ subject.address = address
15
+ subject.address.should eq(address)
16
+ end
17
+ end
18
+
19
+ describe "Persistence" do
20
+ subject { Person.create!(address: address) }
21
+
22
+ it "saves" do
23
+ subject.reload.address.should eq(address)
24
+ end
25
+ end
26
+
27
+ context "embedded" do
28
+ subject { Person.create!(address: address) }
29
+
30
+ it "tracks persisted state" do
31
+ subject.reload.address.should be_persisted
32
+ end
33
+
34
+ it "tracks destroyed state" do
35
+ subject.destroy
36
+ subject.address.should be_destroyed
37
+ end
38
+
39
+ it "destroys" do
40
+ subject.address.destroy
41
+ subject.should_not be_destroyed
42
+ subject.reload.address.should be_nil
43
+ end
44
+
45
+ it "updates" do
46
+ subject.address.number = 17
47
+ subject.address.save
48
+ subject.reload.address.number.should eq(17)
49
+ end
50
+
51
+ it "creates" do
52
+ person = Person.new
53
+ address = Address.new(valid_attributes)
54
+ person.address = address
55
+ person.address.save
56
+ person.reload.address.should eq(address)
57
+ end
58
+
59
+ end
60
+
61
+ describe "Nested attributes" do
62
+ subject { Person.new(address_attributes: valid_attributes) }
63
+
64
+ it "assigns" do
65
+ address = subject.address
66
+ address.street.should eq('Birch')
67
+ address.number.should eq(1)
68
+ address.business.should eq(false)
69
+ end
70
+
71
+ it "saves" do
72
+ subject.save!
73
+ subject.reload
74
+ address = subject.address
75
+ address.street.should eq('Birch')
76
+ address.number.should eq(1)
77
+ address.business.should eq(false)
78
+ end
79
+ end
80
+
81
+ describe "Building" do
82
+
83
+ it "builds" do
84
+ address = subject.build_address(valid_attributes)
85
+ address.should be_a_kind_of(Address)
86
+ address.number.should eq(1)
87
+ end
88
+
89
+ it "creates" do
90
+ address = subject.create_address(valid_attributes)
91
+ address.should be_a_kind_of(Address)
92
+ address.number.should eq(1)
93
+ end
94
+
95
+ it "creates!" do
96
+ address = subject.create_address!(valid_attributes)
97
+ address.should be_a_kind_of(Address)
98
+ address.number.should eq(1)
99
+ end
100
+ end
101
+
102
+ describe "Validation" do
103
+ before { subject.address = Address.new }
104
+
105
+ context "Invalid" do
106
+ it "validates associated" do
107
+ subject.address.should_not be_valid
108
+ subject.should_not be_valid
109
+ subject.errors['address'].should_not be_empty
110
+ end
111
+
112
+ it "doesn't save parent when validation fails" do
113
+ subject.save.should be_false
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,26 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ require 'rspec'
4
+ require 'active_record'
5
+ require 'hstore-document'
6
+ require 'pry'
7
+
8
+ # Requires supporting files with custom matchers and macros, etc,
9
+ # in ./support/ and its subdirectories.
10
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
11
+
12
+ RSpec.configure do |config|
13
+ config.before(:suite) do
14
+ ActiveRecord::Base.establish_connection({
15
+ adapter: 'postgresql',
16
+ encoding: 'unicode',
17
+ database: 'hstore_document',
18
+ username: 'postgres',
19
+ password: 'postgres'
20
+ })
21
+ # ActiveRecord::Base.logger = Logger.new(STDERR)
22
+ end
23
+
24
+ config.after(:suite) do
25
+ end
26
+ end
@@ -0,0 +1,20 @@
1
+ class Address
2
+ include Hstore::Document
3
+
4
+ field :street, type: String, default: 'unknown'
5
+ field :number, type: Fixnum
6
+ field :business, type: Boolean
7
+
8
+ validates_presence_of :number
9
+ end
10
+
11
+ class StrictAddress < Address
12
+ validates_presence_of :street
13
+ validates_numericality_of :number
14
+ end
15
+
16
+ class Person < ActiveRecord::Base
17
+ embeds :address
18
+
19
+ accepts_nested_attributes_for :address
20
+ end
metadata ADDED
@@ -0,0 +1,226 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hstore-document
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Roman Shterenzon
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-04-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: pg
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: pg-hstore
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: activerecord
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "<"
46
+ - !ruby/object:Gem::Version
47
+ version: '4.1'
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: '3.2'
51
+ type: :runtime
52
+ prerelease: false
53
+ version_requirements: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - "<"
56
+ - !ruby/object:Gem::Version
57
+ version: '4.1'
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '3.2'
61
+ - !ruby/object:Gem::Dependency
62
+ name: rspec
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '2.14'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '2.14'
75
+ - !ruby/object:Gem::Dependency
76
+ name: rdoc
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '3.12'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '3.12'
89
+ - !ruby/object:Gem::Dependency
90
+ name: bundler
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '1.0'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '1.0'
103
+ - !ruby/object:Gem::Dependency
104
+ name: jeweler
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: 1.8.7
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: 1.8.7
117
+ - !ruby/object:Gem::Dependency
118
+ name: pry
119
+ requirement: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ type: :development
125
+ prerelease: false
126
+ version_requirements: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ - !ruby/object:Gem::Dependency
132
+ name: pry-debugger
133
+ requirement: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ type: :development
139
+ prerelease: false
140
+ version_requirements: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ - !ruby/object:Gem::Dependency
146
+ name: guard
147
+ requirement: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: '0'
152
+ type: :development
153
+ prerelease: false
154
+ version_requirements: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - ">="
157
+ - !ruby/object:Gem::Version
158
+ version: '0'
159
+ - !ruby/object:Gem::Dependency
160
+ name: guard-rspec
161
+ requirement: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - ">="
164
+ - !ruby/object:Gem::Version
165
+ version: '0'
166
+ type: :development
167
+ prerelease: false
168
+ version_requirements: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - ">="
171
+ - !ruby/object:Gem::Version
172
+ version: '0'
173
+ description: Allows embedding documents in ActiveRecord models using PostgreSQL HStore
174
+ email: roman.shterenzon@gmail.com
175
+ executables: []
176
+ extensions: []
177
+ extra_rdoc_files:
178
+ - LICENSE.txt
179
+ - README.md
180
+ files:
181
+ - ".document"
182
+ - ".rspec"
183
+ - Gemfile
184
+ - Gemfile.lock
185
+ - Guardfile
186
+ - LICENSE.txt
187
+ - README.md
188
+ - Rakefile
189
+ - VERSION
190
+ - hstore-document.gemspec
191
+ - lib/active_record/associations/builder/embeds_one.rb
192
+ - lib/active_record/associations/embeds_one_association.rb
193
+ - lib/active_record/embeds_reflection.rb
194
+ - lib/hstore-document.rb
195
+ - lib/hstore/document.rb
196
+ - lib/hstore/fields.rb
197
+ - lib/hstore/railtie.rb
198
+ - spec/document_spec.rb
199
+ - spec/integration_spec.rb
200
+ - spec/spec_helper.rb
201
+ - spec/support/models.rb
202
+ homepage: http://github.com/romanbsd/hstore-document
203
+ licenses:
204
+ - MIT
205
+ metadata: {}
206
+ post_install_message:
207
+ rdoc_options: []
208
+ require_paths:
209
+ - lib
210
+ required_ruby_version: !ruby/object:Gem::Requirement
211
+ requirements:
212
+ - - ">="
213
+ - !ruby/object:Gem::Version
214
+ version: '0'
215
+ required_rubygems_version: !ruby/object:Gem::Requirement
216
+ requirements:
217
+ - - ">="
218
+ - !ruby/object:Gem::Version
219
+ version: '0'
220
+ requirements: []
221
+ rubyforge_project:
222
+ rubygems_version: 2.2.2
223
+ signing_key:
224
+ specification_version: 4
225
+ summary: Embedded documents using HStore
226
+ test_files: []