activerecord-embedding 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ pkg/*
2
+ *.gem
3
+ .bundle
4
+ Gemfile.lock
5
+
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source :rubygems
2
+
3
+ gemspec
4
+
@@ -0,0 +1,84 @@
1
+ # activerecord-embedding
2
+
3
+ Adds MongoDB style `embeds_many` to `ActiveRecord`.
4
+
5
+ ## Example
6
+
7
+ ```
8
+ class Invoice < ActiveRecord::Base
9
+ include ActiveRecord::Embedding
10
+
11
+ embeds_many :items
12
+ end
13
+ ```
14
+
15
+ Now you can do all the magic `ActiveRecord` does not support natively.
16
+
17
+ ```
18
+ @invoice = Invoice.new
19
+ @invoice.attributes = {items: [{description: "Some fancy ORM", value: 10.00},
20
+ {description: "When ActiveRecord does what you want", value: "priceless"}]}
21
+ ```
22
+
23
+ You can also change your items and ActiveRecord will mark them for destruction and destroy them later.
24
+
25
+ ```
26
+ # Imagine an Invoice with two items...
27
+ @invoice.find(1)
28
+ Items.count # => 2
29
+ @invoice.items.length # => 2
30
+
31
+ # When we change the items to zero...
32
+ @invoice.attributes = {items: []}
33
+ @invoice.items.length # => 0
34
+ Items.count # => 2 (because not saved yet)
35
+
36
+ # It will write the changes after we save them
37
+ @invoice.save!
38
+ @invoice.items.length # => 0
39
+ Items.count # => 0
40
+ ```
41
+
42
+ Hopefully someday there will be native support for this in ActiveRecord.
43
+
44
+ ## Usage
45
+
46
+ Add the gem to your `Gemfile`
47
+
48
+ ```
49
+ gem "activerecord-embedding"
50
+ ```
51
+
52
+ Then use in your models.
53
+ ```
54
+ class Invoice < ActiveRecord::Base
55
+ include ActiveRecord::Embedding
56
+
57
+ embeds_many :items
58
+ end
59
+ ```
60
+
61
+ Remember to `include ActiveRecord::Embedding`!
62
+
63
+ ## Development
64
+
65
+ There are some pretty evil hacks in the source. Feel free to fix them and send
66
+ me a pull request. Please comment your code and write tests. You can run the
67
+ test suite with `rake`. Please do not modify the `version.rb` file.
68
+
69
+ ## Release
70
+
71
+ (Because my short time memory lasts for less than 23 ignoseconds, I need to write this down)
72
+
73
+ ```
74
+ # Remember to run the tests and to bump the version
75
+ gem build activerecord-embedding.gemspec
76
+ gem push activerecord-embedding-$version.gem
77
+ ```
78
+
79
+ ## Credits
80
+
81
+ I must admit that this code is mostly based on a gist of netzpirat. Michael,
82
+ you did a great job! I just improved your code, added a few really really ugly
83
+ hacks to work around some strange ActiveRecord behavior and wrote some tests.
84
+
@@ -0,0 +1,9 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new do |t|
4
+ t.libs << 'test'
5
+ end
6
+
7
+ desc "Run tests"
8
+ task :default => :test
9
+
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path("../lib/active_record/embedding/version", __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "activerecord-embedding"
6
+ s.version = ActiveRecord::Embedding::VERSION
7
+ s.platform = Gem::Platform::RUBY
8
+ s.authors = ["Markus Fenske"]
9
+ s.email = ["iblue@gmx.net"]
10
+ s.homepage = "http://github.com/iblue/activerecord-embedding"
11
+ s.summary = "Adds MongoMapper embeds_many style behavior to ActiveRecord"
12
+ s.description = "Adds MongoMapper embeds_many style behavior to ActiveRecord"
13
+
14
+ s.required_rubygems_version = ">= 1.3.6"
15
+
16
+ s.add_development_dependency "bundler", ">= 1.0.0"
17
+ s.add_development_dependency "debugger"
18
+ s.add_development_dependency "sqlite3"
19
+
20
+ s.add_dependency "activerecord", "~> 3.0"
21
+
22
+ s.files = `git ls-files`.split("\n")
23
+ s.executables = `git ls-files`.split("\n").map{|f| f =~ /^bin\/(.*)/ ? $1 : nil}.compact
24
+ s.require_path = 'lib'
25
+ end
26
+
@@ -0,0 +1,155 @@
1
+ # The code is based on this original source by Michael Kessler (netzpirat):
2
+ # Original source: https://gist.github.com/892426
3
+ module ActiveRecord
4
+ # Allows embedding of ActiveRecord models.
5
+ #
6
+ # Embedding other ActiveRecord models is a composition of the two
7
+ # and leads to the following behaviour:
8
+ #
9
+ # - Nested attributes are accepted on the parent without the _attributes suffix
10
+ # - Mass assignment security allows the embedded attributes
11
+ # - Embedded models are destroyed with the parent when not appearing in an update again
12
+ # - Embedded documents appears in the JSON output
13
+ # - Embedded documents that are deleted are not visible to the parent anymore, but
14
+ # will be deleted *after* save has been caled
15
+ #
16
+ # You have to manually include this module
17
+ #
18
+ # @example
19
+ # class Invoice
20
+ # include ActiveRecord::Embedding
21
+ #
22
+ # embeds_many :items
23
+ # end
24
+ #
25
+ # @author Michael Kessler
26
+ # modified by Markus Fenske <iblue@gmx.net>
27
+ #
28
+ module Embedding
29
+ extend ActiveSupport::Concern
30
+
31
+ module ClassMethods
32
+ mattr_accessor :embeddings
33
+ self.embeddings = []
34
+
35
+ # Embeds many ActiveRecord model
36
+ #
37
+ # @param models [Symbol] the name of the embedded models
38
+ # @param options [Hash] the embedding options
39
+ #
40
+ def embeds_many(models, options = { })
41
+ has_many models, options.merge(:dependent => :destroy, :autosave => true)
42
+ embed_attribute(models)
43
+ attr_accessible "#{models}_attributes".to_sym
44
+
45
+ # What is marked for destruction does not evist anymore from
46
+ # our point of view. FIXME: Really evil hack.
47
+ alias_method "_super_#{models}".to_sym, models
48
+ define_method models do
49
+ # This is an evil hack. Because activerecord uses the items method itself to
50
+ # find out which items are deleted, we need to act differently if called by
51
+ # ActiveRecord. So we look at the paths in the Backtrace. If there is
52
+ # activerecord-3 anywhere there, this is called by AR. This will work until
53
+ # AR 4.0...
54
+ if caller(0).select{|x| x =~ /activerecord-3/}.any?
55
+ return send("_super_#{models}".to_sym)
56
+ end
57
+
58
+ # Otherwise, when we are called by someone else, we will not return the items
59
+ # marked for destruction.
60
+ send("_super_#{models}".to_sym).reject(&:marked_for_destruction?)
61
+ end
62
+ end
63
+
64
+ # Embeds many ActiveRecord models which have been referenced
65
+ # with has_many.
66
+ #
67
+ # @param models [Symbol] the name of the embedded models
68
+ #
69
+ def embeds(models)
70
+ embed_attribute(models)
71
+ end
72
+
73
+ private
74
+
75
+ # Makes the child model accessible by accepting nested attributes and
76
+ # makes the attributes accessible when mass assignment security is enabled.
77
+ #
78
+ # @param name [Symbol] the name of the embedded model
79
+ #
80
+ def embed_attribute(name)
81
+ accepts_nested_attributes_for name, :allow_destroy => true
82
+ attr_accessible "#{ name }_attributes".to_sym if _accessible_attributes?
83
+ self.embeddings << name
84
+ end
85
+ end
86
+
87
+ # Sets the attributes
88
+ #
89
+ # @param new_attributes [Hash] the new attributes
90
+ #
91
+ def attributes=(attrs)
92
+ return unless attrs.is_a?(Hash)
93
+
94
+ # Create a copy early so we do not overwrite the argument
95
+ new_attributes = attrs.dup
96
+
97
+ mark_for_destruction(new_attributes)
98
+
99
+ self.class.embeddings.each do |embed|
100
+ if new_attributes[embed]
101
+ new_attributes["#{embed}_attributes"] = new_attributes[embed]
102
+ new_attributes.delete(embed)
103
+ end
104
+ end
105
+
106
+ super(new_attributes)
107
+ end
108
+
109
+ # Update attributes and destroys missing embeds
110
+ # from the database.
111
+ #
112
+ # @params attributes [Hash] the attributes to update
113
+ #
114
+ def update_attributes(attributes)
115
+ super(mark_for_destruction(attributes))
116
+ end
117
+
118
+ # Update attributes and destroys missing embeds
119
+ # from the database.
120
+ #
121
+ # @params attributes [Hash] the attributes to update
122
+ #
123
+ def update_attributes!(attributes)
124
+ super(mark_for_destruction(attributes))
125
+ end
126
+
127
+ # Add the embedded document in JSON serialization
128
+ #
129
+ # @param options [Hash] the rendering options
130
+ #
131
+ def as_json(options = { })
132
+ super({ :include => self.class.embeddings }.merge(options || { }))
133
+ end
134
+
135
+ private
136
+
137
+ # Marks missing models as deleted. Writes the changes to the database,
138
+ # after save has been called.
139
+ #
140
+ # @param attributes [Hash] the attributes
141
+ #
142
+ def mark_for_destruction(attributes)
143
+ self.class.embeddings.each do |embed|
144
+ if attributes[embed]
145
+ updates = attributes[embed].map { |model| model[:id] }.compact
146
+ destroy = updates.empty? ? send("_super_#{embed}".to_sym).select(:id) : send("_super_#{embed}".to_sym).select(:id).where('id NOT IN (?)', updates)
147
+ destroy.each { |model| attributes[embed] << { :id => model.id, :_destroy => '1' } }
148
+ end
149
+ end
150
+
151
+ attributes
152
+ end
153
+ end
154
+ end
155
+
@@ -0,0 +1,6 @@
1
+ module ActiveRecord
2
+ module Embedding
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
6
+
@@ -0,0 +1,4 @@
1
+ require 'active_record'
2
+ require 'active_record/embedding.rb'
3
+ require 'active_record/embedding/version.rb'
4
+
@@ -0,0 +1,10 @@
1
+ class Invoice < ActiveRecord::Base
2
+ include ActiveRecord::Embedding
3
+
4
+ embeds_many :items
5
+
6
+ def total
7
+ items.map{|i| i.amount * i.value}.compact.reduce(:+) || 0.0
8
+ end
9
+ end
10
+
@@ -0,0 +1,6 @@
1
+ class Item < ActiveRecord::Base
2
+ belongs_to :invoice
3
+
4
+ attr_accessible :amount, :description, :value
5
+ end
6
+
@@ -0,0 +1,14 @@
1
+ # encoding: UTF-8
2
+ ActiveRecord::Schema.define do
3
+ create_table "invoices", :force => true do |t|
4
+ t.string "recipient_email"
5
+ end
6
+
7
+ create_table "items", :force => true do |t|
8
+ t.integer "invoice_id"
9
+ t.integer "amount"
10
+ t.string "description"
11
+ t.float "value"
12
+ end
13
+ end
14
+
@@ -0,0 +1,73 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ Bundler.setup
4
+
5
+ require 'debugger'
6
+
7
+ require 'test/unit'
8
+ require 'active_record'
9
+ require 'active_record/base'
10
+ require 'logger'
11
+
12
+ require 'activerecord-embedding'
13
+
14
+ require 'models/invoice'
15
+ require 'models/item'
16
+
17
+ ActiveRecord::Base.establish_connection(
18
+ :adapter => "sqlite3",
19
+ :database => "/tmp/test.db"
20
+ )
21
+ load "schema/schema.rb"
22
+
23
+
24
+ # Make sure we see whats happening
25
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
26
+
27
+ class MainTest < Test::Unit::TestCase
28
+ def test_embedding
29
+ # Build a new invoice, do not save anything, exists in memory only
30
+ puts "--- Should not issue a query"
31
+ @invoice = Invoice.new
32
+ @invoice.attributes = {items: [{amount: 1, description: "Item 1", value: 10.00}, {amount: 2, description: "Item 2", value: 8.00}]}
33
+ assert_equal 0, Invoice.count
34
+ assert_equal 0, Item.count
35
+ assert_equal 26.00, @invoice.total
36
+ puts " end"
37
+
38
+ # Create everything, make sure it gets saved to the database
39
+ puts "--- Should INSERT 1 invoice and 2 items"
40
+ @invoice.save!
41
+ assert_equal 1, Invoice.count
42
+ assert_equal 2, Item.count
43
+ assert_equal ["Item 1", "Item 2"], Item.all.map(&:description)
44
+ assert_equal 26.00, @invoice.total
45
+ puts " end"
46
+
47
+ # Change attributes, make sure it exists in memory only and does not get
48
+ # saved to the database
49
+ puts "--- Should not issue a query"
50
+ @invoice.attributes = {items: [{amount: 1, description: "Item 3", value: 10.00}]}
51
+ assert_equal 1, Invoice.count
52
+ assert_equal 2, Item.count
53
+ assert_equal ["Item 1", "Item 2"], Item.all.map(&:description)
54
+ assert_equal 10.00, @invoice.total # But the total value in memory should change
55
+ puts " end"
56
+
57
+ # Now save changes to database.
58
+ puts "--- Should DELETE 2 items, INSERT 1 item"
59
+ @invoice.save!
60
+ assert_equal 1, Invoice.count
61
+ assert_equal 1, Item.count
62
+ assert_equal ["Item 3"], Item.all.map(&:description)
63
+ assert_equal 10.00, @invoice.total # Total value in memory should stay the same
64
+ puts " end"
65
+
66
+ # Delete everything, make sure it's gone.
67
+ puts "--- Should DELETE 1 item, DELETE 1 invoice"
68
+ @invoice.destroy
69
+ assert_equal 0, Invoice.count
70
+ assert_equal 0, Item.count
71
+ puts " end"
72
+ end
73
+ end
metadata ADDED
@@ -0,0 +1,122 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: activerecord-embedding
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Markus Fenske
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-07-12 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 1.0.0
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: 1.0.0
30
+ - !ruby/object:Gem::Dependency
31
+ name: debugger
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: sqlite3
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: activerecord
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: '3.0'
70
+ type: :runtime
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ version: '3.0'
78
+ description: Adds MongoMapper embeds_many style behavior to ActiveRecord
79
+ email:
80
+ - iblue@gmx.net
81
+ executables: []
82
+ extensions: []
83
+ extra_rdoc_files: []
84
+ files:
85
+ - .gitignore
86
+ - Gemfile
87
+ - README.md
88
+ - Rakefile
89
+ - activerecord-embedding.gemspec
90
+ - lib/active_record/embedding.rb
91
+ - lib/active_record/embedding/version.rb
92
+ - lib/activerecord-embedding.rb
93
+ - test/models/invoice.rb
94
+ - test/models/item.rb
95
+ - test/schema/schema.rb
96
+ - test/test_main.rb
97
+ homepage: http://github.com/iblue/activerecord-embedding
98
+ licenses: []
99
+ post_install_message:
100
+ rdoc_options: []
101
+ require_paths:
102
+ - lib
103
+ required_ruby_version: !ruby/object:Gem::Requirement
104
+ none: false
105
+ requirements:
106
+ - - ! '>='
107
+ - !ruby/object:Gem::Version
108
+ version: '0'
109
+ required_rubygems_version: !ruby/object:Gem::Requirement
110
+ none: false
111
+ requirements:
112
+ - - ! '>='
113
+ - !ruby/object:Gem::Version
114
+ version: 1.3.6
115
+ requirements: []
116
+ rubyforge_project:
117
+ rubygems_version: 1.8.24
118
+ signing_key:
119
+ specification_version: 3
120
+ summary: Adds MongoMapper embeds_many style behavior to ActiveRecord
121
+ test_files: []
122
+ has_rdoc: