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