seedable 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +10 -0
- data/.rspec +1 -0
- data/.rvmrc +1 -0
- data/.travis.yml +7 -0
- data/Appraisals +9 -0
- data/Gemfile +4 -0
- data/Guardfile +9 -0
- data/LICENSE +21 -0
- data/README.rdoc +95 -0
- data/Rakefile +28 -0
- data/db/schema.rb +34 -0
- data/gemfiles/rails-3.1.0.gemfile +7 -0
- data/gemfiles/rails-3.1.0.gemfile.lock +149 -0
- data/gemfiles/rails-master.gemfile +7 -0
- data/gemfiles/rails-master.gemfile.lock +153 -0
- data/lib/seedable.rb +19 -0
- data/lib/seedable/core_ext.rb +66 -0
- data/lib/seedable/exporter.rb +163 -0
- data/lib/seedable/helpers.rb +144 -0
- data/lib/seedable/importer.rb +55 -0
- data/lib/seedable/object_tracker.rb +45 -0
- data/lib/seedable/version.rb +5 -0
- data/seedable.gemspec +44 -0
- data/spec/database.yml +7 -0
- data/spec/models/filtered_garage_spec.rb +22 -0
- data/spec/models/garage_spec.rb +197 -0
- data/spec/spec_helper.rb +41 -0
- data/spec/support/factories.rb +23 -0
- data/spec/support/models.rb +43 -0
- metadata +271 -0
data/lib/seedable.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require "seedable/version"
|
4
|
+
require "seedable/importer"
|
5
|
+
require "seedable/exporter"
|
6
|
+
require "seedable/helpers"
|
7
|
+
require "seedable/core_ext"
|
8
|
+
require "seedable/object_tracker"
|
9
|
+
|
10
|
+
module Seedable # :nodoc:
|
11
|
+
include Seedable::Importer
|
12
|
+
|
13
|
+
extend ActiveSupport::Concern
|
14
|
+
|
15
|
+
included do
|
16
|
+
include Seedable::Importer
|
17
|
+
include Seedable::Exporter
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Seedable # :nodoc:
|
4
|
+
module CoreExt # :nodoc:
|
5
|
+
module Array # :nodoc:
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
module InstanceMethods # :nodoc:
|
9
|
+
|
10
|
+
# Serialize an array of heterogeneous objects.
|
11
|
+
#
|
12
|
+
def as_seedable
|
13
|
+
map { |o| o.as_seedable }
|
14
|
+
end
|
15
|
+
|
16
|
+
# Serialize an array of heterogeneous objects and output as
|
17
|
+
# JSON.
|
18
|
+
#
|
19
|
+
def to_seedable
|
20
|
+
as_seedable.to_json
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
module Serialization # :nodoc:
|
27
|
+
extend ActiveSupport::Concern
|
28
|
+
|
29
|
+
included do
|
30
|
+
alias_method_chain :serializable_hash, :object_tracker
|
31
|
+
end
|
32
|
+
|
33
|
+
# Use thread-local storage to carry the object tracker through the
|
34
|
+
# association traversal.
|
35
|
+
#
|
36
|
+
module InstanceMethods # :nodoc:
|
37
|
+
|
38
|
+
# Extend serializable_hash functionality by calling out to the
|
39
|
+
# object tracker.
|
40
|
+
#
|
41
|
+
def serializable_hash_with_object_tracker(options = {})
|
42
|
+
unless object_tracker = Thread.current[:object_tracker]
|
43
|
+
object_tracker = ObjectTracker.new
|
44
|
+
clean_up = true
|
45
|
+
Thread.current[:object_tracker] = object_tracker
|
46
|
+
end
|
47
|
+
|
48
|
+
if object_tracker.contains?(self)
|
49
|
+
return_value = {}
|
50
|
+
else
|
51
|
+
object_tracker.add(self)
|
52
|
+
return_value = serializable_hash_without_object_tracker(options)
|
53
|
+
end
|
54
|
+
|
55
|
+
Thread.current[:object_tracker] = nil if clean_up
|
56
|
+
|
57
|
+
return_value
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
Array.send :include, Seedable::CoreExt::Array
|
65
|
+
|
66
|
+
ActiveRecord::Base.send :include, Seedable::CoreExt::Serialization
|
@@ -0,0 +1,163 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Seedable # :nodoc:
|
4
|
+
module Exporter # :nodoc:
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
# Extensions to all ActiveRecord classes to enable this class for
|
8
|
+
# import and export of data.
|
9
|
+
#
|
10
|
+
# Use +acts_as_seedable+ here to enable your classes to use this
|
11
|
+
# functionality.
|
12
|
+
#
|
13
|
+
# Use +from_seedable+ and +to_seedable+ here to perform the
|
14
|
+
# serialization and deserialization.
|
15
|
+
#
|
16
|
+
module ClassMethods
|
17
|
+
|
18
|
+
# Enable seedable export and import for the including class.
|
19
|
+
#
|
20
|
+
# ==== Options
|
21
|
+
#
|
22
|
+
# * +filter_attributes+ - Attributes to filter from the export.
|
23
|
+
# * +include_associations+ - Associations that should be traversed during export.
|
24
|
+
#
|
25
|
+
# ==== Examples
|
26
|
+
#
|
27
|
+
# class Garage < ActiveRecord::Base
|
28
|
+
# acts_as_seedable :include_associations => [:cars]
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
def seedable(options = {})
|
32
|
+
cattr_accessor :filterable_attributes
|
33
|
+
cattr_accessor :includable_associations
|
34
|
+
|
35
|
+
if options[:filter_attributes]
|
36
|
+
self.send(:filter_attributes, options.delete(:filter_attributes))
|
37
|
+
end
|
38
|
+
|
39
|
+
if options[:include_associations]
|
40
|
+
self.send(:include_associations, options.delete(:include_associations))
|
41
|
+
else
|
42
|
+
self.send(:include_associations, Helpers.associations_for(self))
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Return seedable status
|
47
|
+
#
|
48
|
+
def seedable?
|
49
|
+
self.respond_to?(:filterable_attributes) && self.respond_to?(:includable_associations)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Sets which attributes should be fitlered from the
|
53
|
+
# serialization of this object.
|
54
|
+
#
|
55
|
+
# ==== Parameters
|
56
|
+
#
|
57
|
+
# * +attributes+ - Array of symbols representing attributes.
|
58
|
+
#
|
59
|
+
# ==== Examples
|
60
|
+
#
|
61
|
+
# Garage.filter_attributes([:id, :name])
|
62
|
+
#
|
63
|
+
def filter_attributes(attributes)
|
64
|
+
self.send(:filterable_attributes=, attributes)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Sets which associations should be traversed when performing
|
68
|
+
# serialization.
|
69
|
+
#
|
70
|
+
# ==== Parameters
|
71
|
+
#
|
72
|
+
# * +associations+ - Array of symbols representing associations.
|
73
|
+
#
|
74
|
+
# ==== Examples
|
75
|
+
#
|
76
|
+
# Garage.include_associations([:cars])
|
77
|
+
#
|
78
|
+
# Car.include_associations([:garage, :drivers])
|
79
|
+
#
|
80
|
+
def include_associations(associations)
|
81
|
+
self.send(:includable_associations=, associations)
|
82
|
+
|
83
|
+
unless Rails.env.production?
|
84
|
+
associations.each do |association|
|
85
|
+
self.accepts_nested_attributes_for association
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Create object from attributes without a root node, since it's
|
91
|
+
# assumed to be the type this method is being called on.
|
92
|
+
#
|
93
|
+
# ==== Parameters
|
94
|
+
#
|
95
|
+
# * +attributes+ - Hash of attributes, without a root node.
|
96
|
+
#
|
97
|
+
# ==== Examples
|
98
|
+
#
|
99
|
+
# Garage.from_seedable_attributes({ :id => '1', :name => 'Name' })
|
100
|
+
#
|
101
|
+
def from_seedable_attributes(attributes) # :nodoc:
|
102
|
+
object = self.new
|
103
|
+
|
104
|
+
# Handling for rails-3.2.x vs. rails-3.0.x changes.
|
105
|
+
#
|
106
|
+
if object.respond_to?(:assign_attributes)
|
107
|
+
object.assign_attributes(
|
108
|
+
Helpers.convert_to_nested_attributes(self, attributes),
|
109
|
+
:without_protection => true
|
110
|
+
)
|
111
|
+
else
|
112
|
+
object.send(
|
113
|
+
:attributes=,
|
114
|
+
Helpers.convert_to_nested_attributes(self, attributes),
|
115
|
+
false
|
116
|
+
)
|
117
|
+
end
|
118
|
+
|
119
|
+
object.save!(:validate => false)
|
120
|
+
object
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
|
125
|
+
# Extensions to all ActiveRecord classes to enable this class for
|
126
|
+
# import and export of data.
|
127
|
+
#
|
128
|
+
# Use +acts_as_seedable+ here to enable your classes to use this
|
129
|
+
# functionality.
|
130
|
+
#
|
131
|
+
# Use +from_seedable+ and +to_seedable+ here to perform the
|
132
|
+
# serialization and deserialization.
|
133
|
+
#
|
134
|
+
module InstanceMethods
|
135
|
+
|
136
|
+
# Returns hash of objects attributes and all included associations
|
137
|
+
# attributes.
|
138
|
+
#
|
139
|
+
# ==== Examples
|
140
|
+
#
|
141
|
+
# json = @garage.as_seedable.to_json
|
142
|
+
#
|
143
|
+
def as_seedable
|
144
|
+
includable = Helpers.traverse_includable_associations_from_instance(self)
|
145
|
+
exceptions = Helpers.filterable_attributes(self)
|
146
|
+
|
147
|
+
self.as_json(:include => includable, :except => exceptions)
|
148
|
+
end
|
149
|
+
|
150
|
+
# Performs render of as_seedable content into properly formatted
|
151
|
+
# JSON.
|
152
|
+
#
|
153
|
+
# ==== Examples
|
154
|
+
#
|
155
|
+
# json = @garage.to_seedable
|
156
|
+
#
|
157
|
+
def to_seedable
|
158
|
+
as_seedable.to_json
|
159
|
+
end
|
160
|
+
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Seedable # :nodoc:
|
4
|
+
module Helpers # :nodoc:
|
5
|
+
|
6
|
+
# Convert a string or symbol to a class in the object space.
|
7
|
+
#
|
8
|
+
def self.to_class(string)
|
9
|
+
string.to_s.classify.constantize
|
10
|
+
end
|
11
|
+
|
12
|
+
# Parse a seedable object returning a hash.
|
13
|
+
#
|
14
|
+
def self.parse_seedable(json)
|
15
|
+
JSON.parse(json)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Convert hash root node to class, and return attributes.
|
19
|
+
#
|
20
|
+
def self.to_class_and_attributes(hash)
|
21
|
+
node = hash.keys.first
|
22
|
+
attributes = hash.delete(node)
|
23
|
+
klass = Helpers.to_class(node)
|
24
|
+
|
25
|
+
[klass, attributes]
|
26
|
+
end
|
27
|
+
|
28
|
+
# Traverse all associations starting from an instance of a
|
29
|
+
# particular class.
|
30
|
+
#
|
31
|
+
def self.traverse_includable_associations_from_instance(klass)
|
32
|
+
traverse_includable_associations(klass.class, klass.class)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Traverse all associations starting from a particular class.
|
36
|
+
#
|
37
|
+
def self.traverse_includable_associations(klass, ancestor_klass)
|
38
|
+
if klass.respond_to?(:includable_associations)
|
39
|
+
klass.includable_associations.inject({}) do |associations, association|
|
40
|
+
descendent_klass = Helpers.to_class(association)
|
41
|
+
unless descendent_klass == ancestor_klass || !descendent_klass.seedable?
|
42
|
+
associations[association] = {
|
43
|
+
:include => traverse_includable_associations(Helpers.to_class(association), klass),
|
44
|
+
:except => filterable_attributes(Helpers.to_class(association))
|
45
|
+
}
|
46
|
+
end
|
47
|
+
associations
|
48
|
+
end
|
49
|
+
else
|
50
|
+
{}
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Filterable attributes for a particular class.
|
55
|
+
#
|
56
|
+
def self.filterable_attributes(klass)
|
57
|
+
if klass.respond_to?(:filterable_attributes)
|
58
|
+
klass.filterable_attributes
|
59
|
+
else
|
60
|
+
[]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Valid traversable association types.
|
65
|
+
#
|
66
|
+
def self.valid_association_types
|
67
|
+
[:has_many, :has_one, :belongs_to]
|
68
|
+
end
|
69
|
+
|
70
|
+
# Determine if a particular association macro is valid.
|
71
|
+
#
|
72
|
+
def self.valid_associations_include?(macro)
|
73
|
+
valid_association_types.include?(macro)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Return all associations for a particular class.
|
77
|
+
#
|
78
|
+
def self.associations_for(klass)
|
79
|
+
klass.reflections.map do |reflection|
|
80
|
+
valid_associations_include?(reflection.last.macro) ? reflection.first : nil
|
81
|
+
end.compact
|
82
|
+
end
|
83
|
+
|
84
|
+
# Returns true of a particular association is valid for a particular
|
85
|
+
# class.
|
86
|
+
#
|
87
|
+
def self.is_key_association_for_class?(klass, key)
|
88
|
+
klass.reflections.any? do |reflection|
|
89
|
+
valid_associations_include?(reflection.last.macro) && reflection.first == key.to_sym
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Return all attributes in a hash, without the root node, if
|
94
|
+
# present.
|
95
|
+
#
|
96
|
+
def self.attributes_without_root(attributes, root)
|
97
|
+
attributes.delete(root)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Return the name of the nested association accessor for a
|
101
|
+
# particular association.
|
102
|
+
#
|
103
|
+
def self.nested_attributes_key_for(key)
|
104
|
+
"#{key}_attributes"
|
105
|
+
end
|
106
|
+
|
107
|
+
# Return a particular reflection on an object or class.
|
108
|
+
#
|
109
|
+
def self.return_reflection(klass, association)
|
110
|
+
if klass.respond_to?(:reflect_on_association)
|
111
|
+
klass.reflect_on_association(association)
|
112
|
+
else
|
113
|
+
klass.class.reflect_on_association(association)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Return the proper active_record reflection type for a nested
|
118
|
+
# association.
|
119
|
+
#
|
120
|
+
def self.reflection_type(klass, association)
|
121
|
+
return_reflection(klass, association).collection? ? :collection : :one_to_one
|
122
|
+
end
|
123
|
+
|
124
|
+
# Convert a hash of nested model attributes to a valid hash to be
|
125
|
+
# passed to assign_attributes by substituting association name for
|
126
|
+
# the appropriate association accessors.
|
127
|
+
#
|
128
|
+
def self.convert_to_nested_attributes(klass, node_attributes)
|
129
|
+
node_attributes.keys.inject(node_attributes) do |attributes, key|
|
130
|
+
if is_key_association_for_class?(klass, key)
|
131
|
+
sub_attributes = attributes_without_root(attributes, key)
|
132
|
+
|
133
|
+
attributes[nested_attributes_key_for(key)] = sub_attributes.is_a?(Enumerable) ?
|
134
|
+
sub_attributes.map do |sub_attribute|
|
135
|
+
convert_to_nested_attributes(to_class(key), sub_attribute)
|
136
|
+
end :
|
137
|
+
convert_to_nested_attributes(to_class(key), sub_attributes)
|
138
|
+
end
|
139
|
+
attributes
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Seedable # :nodoc:
|
4
|
+
module Importer # :nodoc:
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
# Seedable methods for loading serialized objects of unknown or
|
8
|
+
# disperse types into objects.
|
9
|
+
#
|
10
|
+
# Use +from_seedable+ here to load arrays of one or multiple types,
|
11
|
+
# or when you do not know what type of object the JSON represents to
|
12
|
+
# have it return the appropriate object.
|
13
|
+
#
|
14
|
+
module ClassMethods
|
15
|
+
|
16
|
+
# Takes JSON and builds objects from it. Returns either the
|
17
|
+
# object, or array of objects depending on input.
|
18
|
+
#
|
19
|
+
# ==== Parameters
|
20
|
+
#
|
21
|
+
# * +json+ - A block of JSON.
|
22
|
+
#
|
23
|
+
# ==== Examples
|
24
|
+
#
|
25
|
+
# array_of_objects = Seedable.from_seedable(json_containing_array_of_hashes)
|
26
|
+
# object = Seedable.from_seedable(json_for_one_object)
|
27
|
+
#
|
28
|
+
def from_seedable(json)
|
29
|
+
objects = Helpers.parse_seedable(json)
|
30
|
+
|
31
|
+
if Array === objects
|
32
|
+
objects.map do |object|
|
33
|
+
objects_from_serialized_hash(object)
|
34
|
+
end
|
35
|
+
else
|
36
|
+
objects_from_serialized_hash(objects)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Convert a hash's root node to a class, and return the remainder
|
41
|
+
# of the hash as attributes.
|
42
|
+
#
|
43
|
+
# ==== Parameters
|
44
|
+
#
|
45
|
+
# * +hash+ - Hash with one root note reflecting a class.
|
46
|
+
#
|
47
|
+
def objects_from_serialized_hash(hash) # :nodoc:
|
48
|
+
klass, attributes = Helpers.to_class_and_attributes(hash)
|
49
|
+
klass.from_seedable_attributes(attributes)
|
50
|
+
end
|
51
|
+
private :objects_from_serialized_hash
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|