portable_model 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in portable_model.gemspec
4
+ gemspec
data/README.rdoc ADDED
@@ -0,0 +1,7 @@
1
+ = PortableModel
2
+
3
+ Include +PortableModel+ in any +ActiveRecord+ model to enable exporting and
4
+ importing the model's records.
5
+
6
+ Author:: Clyde Law (mailto:claw@alum.mit.edu)
7
+ License:: Released under the MIT license
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,3 @@
1
+ module PortableModel
2
+ VERSION = "0.1.0"
3
+ 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
+