portable_model 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/README.rdoc +7 -0
- data/Rakefile +1 -0
- data/lib/portable_model/active_record.rb +102 -0
- data/lib/portable_model/version.rb +3 -0
- data/lib/portable_model.rb +151 -0
- data/portable_model.gemspec +22 -0
- metadata +90 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.rdoc
ADDED
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# Enables exporting and importing ActiveRecord associations of ActiveRecord
|
2
|
+
# models that include PortableModel.
|
3
|
+
#
|
4
|
+
module ActiveRecord::Associations
|
5
|
+
|
6
|
+
class NotPortableError < StandardError
|
7
|
+
|
8
|
+
def initialize(assoc)
|
9
|
+
super("#{assoc.proxy_reflection.name}:#{assoc.proxy_reflection.klass.name} is not portable")
|
10
|
+
end
|
11
|
+
|
12
|
+
class << self
|
13
|
+
|
14
|
+
def raise_on_not_portable(assoc)
|
15
|
+
raise NotPortableError.new(assoc) unless assoc.proxy_reflection.klass.include?(PortableModel)
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
class AssociationProxy
|
23
|
+
|
24
|
+
# Export the association to a YAML file.
|
25
|
+
#
|
26
|
+
def export_to_yml(filename)
|
27
|
+
NotPortableError.raise_on_not_portable(self)
|
28
|
+
|
29
|
+
Pathname.new(filename).open('w') do |out|
|
30
|
+
YAML::dump(export_portable_association, out)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Import the association from a YAML file.
|
35
|
+
#
|
36
|
+
def import_from_yml(filename)
|
37
|
+
NotPortableError.raise_on_not_portable(self)
|
38
|
+
import_portable_association(YAML::load_file(filename))
|
39
|
+
end
|
40
|
+
|
41
|
+
protected
|
42
|
+
|
43
|
+
# Used to make sure that imported records are associated with the
|
44
|
+
# association owner.
|
45
|
+
#
|
46
|
+
def primary_key_hash
|
47
|
+
{ proxy_reflection.primary_key_name.to_s => proxy_owner.id }
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
class HasOneAssociation
|
53
|
+
|
54
|
+
# Export the association to a hash.
|
55
|
+
#
|
56
|
+
def export_portable_association
|
57
|
+
NotPortableError.raise_on_not_portable(self)
|
58
|
+
export_to_hash
|
59
|
+
end
|
60
|
+
|
61
|
+
# Import the association from a hash.
|
62
|
+
#
|
63
|
+
def import_portable_association(record_hash)
|
64
|
+
NotPortableError.raise_on_not_portable(self)
|
65
|
+
raise ArgumentError.new('specified argument is not a hash') unless record_hash.is_a?(Hash)
|
66
|
+
|
67
|
+
proxy_owner.transaction do
|
68
|
+
if target.nil?
|
69
|
+
assoc_record = proxy_reflection.klass.import_from_hash(record_hash.merge(primary_key_hash))
|
70
|
+
replace(assoc_record)
|
71
|
+
else
|
72
|
+
raise 'cannot replace existing association record'
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
class HasManyAssociation
|
80
|
+
|
81
|
+
# Export the association to an array of hashes.
|
82
|
+
#
|
83
|
+
def export_portable_association
|
84
|
+
NotPortableError.raise_on_not_portable(self)
|
85
|
+
map(&:export_to_hash)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Import the association from an array of hashes.
|
89
|
+
#
|
90
|
+
def import_portable_association(record_hashes)
|
91
|
+
NotPortableError.raise_on_not_portable(self)
|
92
|
+
raise ArgumentError.new('specified argument is not an array of hashes') unless record_hashes.is_a?(Array) && record_hashes.all? { |record_hash| record_hash.is_a?(Hash) }
|
93
|
+
|
94
|
+
proxy_owner.transaction do
|
95
|
+
assoc_records = record_hashes.map { |record_hash| proxy_reflection.klass.import_from_hash(record_hash.merge(primary_key_hash)) }
|
96
|
+
concat(*assoc_records)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
require 'portable_model/version'
|
2
|
+
require 'portable_model/active_record'
|
3
|
+
|
4
|
+
# Include PortableModel in any ActiveRecord model to enable exporting and
|
5
|
+
# importing the model's records.
|
6
|
+
#
|
7
|
+
module PortableModel
|
8
|
+
|
9
|
+
def self.included(base)
|
10
|
+
base.extend(ClassMethods)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Export the record to a hash.
|
14
|
+
#
|
15
|
+
def export_to_hash
|
16
|
+
# Export portable attributes.
|
17
|
+
record_hash = self.class.portable_attributes.inject({}) do |hash, attr_name|
|
18
|
+
hash[attr_name] = attributes[attr_name]
|
19
|
+
hash
|
20
|
+
end
|
21
|
+
|
22
|
+
# Include the exported attributes of portable associations.
|
23
|
+
self.class.portable_associations.inject(record_hash) do |hash, assoc_name|
|
24
|
+
assoc = self.__send__(assoc_name)
|
25
|
+
hash[assoc_name] = assoc.export_portable_association if assoc
|
26
|
+
hash
|
27
|
+
end
|
28
|
+
|
29
|
+
record_hash
|
30
|
+
end
|
31
|
+
|
32
|
+
# Export the record to a YAML file.
|
33
|
+
#
|
34
|
+
def export_to_yml(filename)
|
35
|
+
Pathname.new(filename).open('w') do |out|
|
36
|
+
YAML::dump(export_to_hash, out)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Export values from the record's association.
|
41
|
+
#
|
42
|
+
def export_from_association(assoc_name)
|
43
|
+
self.__send__(assoc_name).export_portable_association
|
44
|
+
end
|
45
|
+
|
46
|
+
# Import values into the record's association.
|
47
|
+
#
|
48
|
+
def import_into_association(assoc_name, assoc_value)
|
49
|
+
assoc = self.__send__(assoc_name)
|
50
|
+
if assoc
|
51
|
+
assoc.import_portable_association(assoc_value)
|
52
|
+
else
|
53
|
+
assoc_reflection = self.class.reflect_on_association(assoc_name.to_sym)
|
54
|
+
raise 'nil can only be handled for direct has_one associations' unless assoc_reflection.macro == :has_one && !assoc_reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
|
55
|
+
assoc = ActiveRecord::Associations::HasOneAssociation.new(self, assoc_reflection)
|
56
|
+
assoc.import_portable_association(assoc_value)
|
57
|
+
association_instance_set(assoc_reflection.name, assoc.target.nil? ? nil : assoc)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
module ClassMethods
|
62
|
+
|
63
|
+
# Import a record from a hash.
|
64
|
+
#
|
65
|
+
def import_from_hash(record_hash)
|
66
|
+
raise ArgumentError.new('specified argument is not a hash') unless record_hash.is_a?(Hash)
|
67
|
+
|
68
|
+
# Override any necessary attributes before importing.
|
69
|
+
record_hash = record_hash.merge(overridden_imported_attrs)
|
70
|
+
|
71
|
+
transaction do
|
72
|
+
if (columns_hash.include?(inheritance_column) &&
|
73
|
+
(record_type_name = record_hash[inheritance_column.to_s]) &&
|
74
|
+
!record_type_name.blank? &&
|
75
|
+
record_type_name != sti_name)
|
76
|
+
# The model implements STI and the record type points to a different
|
77
|
+
# class; call the method in that class instead.
|
78
|
+
return compute_type(record_type_name).import_from_hash(record_hash)
|
79
|
+
end
|
80
|
+
|
81
|
+
# First split out the attributes that correspond to portable
|
82
|
+
# associations.
|
83
|
+
assoc_attrs = portable_associations.inject({}) do |hash, assoc_name|
|
84
|
+
hash[assoc_name] = record_hash.delete(assoc_name) if record_hash.has_key?(assoc_name)
|
85
|
+
hash
|
86
|
+
end
|
87
|
+
|
88
|
+
# Create a new record.
|
89
|
+
record = create!(record_hash)
|
90
|
+
|
91
|
+
# Import each of the record's associations into the record.
|
92
|
+
assoc_attrs.each do |assoc_name, assoc_value|
|
93
|
+
record.import_into_association(assoc_name, assoc_value)
|
94
|
+
end
|
95
|
+
|
96
|
+
record
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Export a record from a YAML file.
|
101
|
+
#
|
102
|
+
def import_from_yml(filename, additional_attrs = {})
|
103
|
+
record_hash = YAML::load_file(filename)
|
104
|
+
import_from_hash(record_hash.merge(additional_attrs))
|
105
|
+
end
|
106
|
+
|
107
|
+
# Returns the names of portable attributes, which are any attributes that
|
108
|
+
# are not primary or foreign keys.
|
109
|
+
#
|
110
|
+
def portable_attributes
|
111
|
+
columns.reject do |column|
|
112
|
+
# TODO: Consider rejecting counter_cache columns as well; this will involve retrieving a has_many association's corresponding belongs_to association to retrieve its counter_cache_column.
|
113
|
+
column.primary || column.name.in?(reflect_on_all_associations(:belongs_to).map(&:association_foreign_key))
|
114
|
+
end.map(&:name).map(&:to_s)
|
115
|
+
end
|
116
|
+
|
117
|
+
# Returns names of portable associations, which are has_one and has_many
|
118
|
+
# associations that do not go through other associations and that also
|
119
|
+
# include PortableModel.
|
120
|
+
#
|
121
|
+
# Because has_and_belongs_to_many associations are bi-directional, they are
|
122
|
+
# not portable.
|
123
|
+
#
|
124
|
+
def portable_associations
|
125
|
+
reflect_on_all_associations.select do |assoc_reflection|
|
126
|
+
assoc_reflection.macro.in?([:has_one, :has_many]) &&
|
127
|
+
!assoc_reflection.is_a?(ActiveRecord::Reflection::ThroughReflection) &&
|
128
|
+
assoc_reflection.klass.include?(PortableModel)
|
129
|
+
end.map(&:name).map(&:to_s)
|
130
|
+
end
|
131
|
+
|
132
|
+
protected
|
133
|
+
|
134
|
+
# Overrides the specified attributes whenever a record is imported.
|
135
|
+
#
|
136
|
+
def override_attributes_on_import(attrs)
|
137
|
+
attrs.inject(overridden_imported_attrs) do |overridden_attrs, (attr_name, attr_value)|
|
138
|
+
overridden_attrs[attr_name.to_s] = attr_value
|
139
|
+
overridden_attrs
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
private
|
144
|
+
|
145
|
+
def overridden_imported_attrs
|
146
|
+
@overridden_imported_attrs ||= {}
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "portable_model/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "portable_model"
|
7
|
+
s.version = PortableModel::VERSION
|
8
|
+
s.authors = ["Clyde Law"]
|
9
|
+
s.email = ["claw@alum.mit.edu"]
|
10
|
+
s.homepage = %q{http://github.com/Umofomia/portable_model}
|
11
|
+
s.summary = %q{Enables exporting and importing an ActiveRecord model's records.}
|
12
|
+
s.description = %q{Enables exporting and importing an ActiveRecord model's records.}
|
13
|
+
|
14
|
+
s.add_dependency('activerecord', '>= 2.3.8')
|
15
|
+
|
16
|
+
s.rubyforge_project = "portable_model"
|
17
|
+
|
18
|
+
s.files = `git ls-files`.split("\n")
|
19
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
20
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
21
|
+
s.require_paths = ["lib"]
|
22
|
+
end
|
metadata
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: portable_model
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Clyde Law
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2012-04-12 00:00:00 -07:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: activerecord
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 19
|
30
|
+
segments:
|
31
|
+
- 2
|
32
|
+
- 3
|
33
|
+
- 8
|
34
|
+
version: 2.3.8
|
35
|
+
type: :runtime
|
36
|
+
version_requirements: *id001
|
37
|
+
description: Enables exporting and importing an ActiveRecord model's records.
|
38
|
+
email:
|
39
|
+
- claw@alum.mit.edu
|
40
|
+
executables: []
|
41
|
+
|
42
|
+
extensions: []
|
43
|
+
|
44
|
+
extra_rdoc_files: []
|
45
|
+
|
46
|
+
files:
|
47
|
+
- .gitignore
|
48
|
+
- Gemfile
|
49
|
+
- README.rdoc
|
50
|
+
- Rakefile
|
51
|
+
- lib/portable_model.rb
|
52
|
+
- lib/portable_model/active_record.rb
|
53
|
+
- lib/portable_model/version.rb
|
54
|
+
- portable_model.gemspec
|
55
|
+
has_rdoc: true
|
56
|
+
homepage: http://github.com/Umofomia/portable_model
|
57
|
+
licenses: []
|
58
|
+
|
59
|
+
post_install_message:
|
60
|
+
rdoc_options: []
|
61
|
+
|
62
|
+
require_paths:
|
63
|
+
- lib
|
64
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
hash: 3
|
70
|
+
segments:
|
71
|
+
- 0
|
72
|
+
version: "0"
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
74
|
+
none: false
|
75
|
+
requirements:
|
76
|
+
- - ">="
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
hash: 3
|
79
|
+
segments:
|
80
|
+
- 0
|
81
|
+
version: "0"
|
82
|
+
requirements: []
|
83
|
+
|
84
|
+
rubyforge_project: portable_model
|
85
|
+
rubygems_version: 1.6.2
|
86
|
+
signing_key:
|
87
|
+
specification_version: 3
|
88
|
+
summary: Enables exporting and importing an ActiveRecord model's records.
|
89
|
+
test_files: []
|
90
|
+
|