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