portable_model 0.1.0
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.
- 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
|
+
|