activerecord-embedding 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: