seedable 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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