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 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
+