happymapper-swanandp 0.4.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.
Files changed (41) hide show
  1. data/License +20 -0
  2. data/README.rdoc +55 -0
  3. data/Rakefile +35 -0
  4. data/examples/amazon.rb +34 -0
  5. data/examples/current_weather.rb +21 -0
  6. data/examples/dashed_elements.rb +20 -0
  7. data/examples/multi_street_address.rb +30 -0
  8. data/examples/post.rb +19 -0
  9. data/examples/twitter.rb +37 -0
  10. data/lib/happymapper.rb +321 -0
  11. data/lib/happymapper/attribute.rb +3 -0
  12. data/lib/happymapper/element.rb +3 -0
  13. data/lib/happymapper/item.rb +179 -0
  14. data/lib/happymapper/version.rb +3 -0
  15. data/spec/fixtures/address.xml +8 -0
  16. data/spec/fixtures/analytics.xml +61 -0
  17. data/spec/fixtures/commit.xml +52 -0
  18. data/spec/fixtures/current_weather.xml +89 -0
  19. data/spec/fixtures/family_tree.xml +7 -0
  20. data/spec/fixtures/multi_street_address.xml +9 -0
  21. data/spec/fixtures/multiple_namespaces.xml +170 -0
  22. data/spec/fixtures/nested_namespaces.xml +17 -0
  23. data/spec/fixtures/notes.xml +9 -0
  24. data/spec/fixtures/pita.xml +133 -0
  25. data/spec/fixtures/posts.xml +23 -0
  26. data/spec/fixtures/product_default_namespace.xml +10 -0
  27. data/spec/fixtures/product_no_namespace.xml +10 -0
  28. data/spec/fixtures/product_single_namespace.xml +10 -0
  29. data/spec/fixtures/radar.xml +21 -0
  30. data/spec/fixtures/raw.xml +9 -0
  31. data/spec/fixtures/statuses.xml +422 -0
  32. data/spec/happymapper_attribute_spec.rb +17 -0
  33. data/spec/happymapper_element_spec.rb +17 -0
  34. data/spec/happymapper_item_spec.rb +115 -0
  35. data/spec/happymapper_spec.rb +404 -0
  36. data/spec/happymapper_to_xml_namespaces_spec.rb +149 -0
  37. data/spec/happymapper_to_xml_spec.rb +138 -0
  38. data/spec/spec.opts +1 -0
  39. data/spec/spec_helper.rb +9 -0
  40. data/spec/support/models.rb +323 -0
  41. metadata +121 -0
data/License ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 John Nunemaker
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,55 @@
1
+ = happymapper
2
+
3
+ == DESCRIPTION:
4
+
5
+ XML to object mapping library. I have included examples to help get you going. The specs should also point you in the right direction.
6
+
7
+ == FEATURES:
8
+
9
+ * Easy to define xml attributes and elements for an object
10
+ * Fast because it uses libxml-ruby under the hood
11
+ * Automatic conversion of xml to defined objects
12
+
13
+ == EXAMPLES:
14
+
15
+ Here is a simple example that maps Twitter statuses and users.
16
+
17
+ class User
18
+ include HappyMapper
19
+
20
+ element :id, Integer
21
+ element :name, String
22
+ element :screen_name, String
23
+ element :location, String
24
+ element :description, String
25
+ element :profile_image_url, String
26
+ element :url, String
27
+ element :protected, Boolean
28
+ element :followers_count, Integer
29
+ end
30
+
31
+ class Status
32
+ include HappyMapper
33
+
34
+ element :id, Integer
35
+ element :text, String
36
+ element :created_at, Time
37
+ element :source, String
38
+ element :truncated, Boolean
39
+ element :in_reply_to_status_id, Integer
40
+ element :in_reply_to_user_id, Integer
41
+ element :favorited, Boolean
42
+ has_one :user, User
43
+ end
44
+
45
+ See examples directory in the gem for more examples.
46
+
47
+ http://github.com/jnunemaker/happymapper/tree/master/examples/
48
+
49
+ == INSTALL:
50
+
51
+ * gem install happymapper
52
+
53
+ == DOCS:
54
+
55
+ http://rdoc.info/projects/jnunemaker/happymapper
@@ -0,0 +1,35 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ require 'rake'
5
+ require 'spec/rake/spectask'
6
+ require File.expand_path('../lib/happymapper/version', __FILE__)
7
+
8
+ Spec::Rake::SpecTask.new do |t|
9
+ t.ruby_opts << '-rubygems'
10
+ t.verbose = true
11
+ end
12
+ task :default => :spec
13
+
14
+ desc 'Builds the gem'
15
+ task :build do
16
+ sh "gem build happymapper.gemspec"
17
+ end
18
+
19
+ desc 'Builds and installs the gem'
20
+ task :install => :build do
21
+ sh "gem install happymapper-#{HappyMapper::Version}"
22
+ end
23
+
24
+ desc 'Tags version, pushes to remote, and pushes gem'
25
+ task :release => :build do
26
+ sh "git tag v#{HappyMapper::Version}"
27
+ sh "git push origin master"
28
+ sh "git push origin v#{HappyMapper::Version}"
29
+ sh "gem push happymapper-#{HappyMapper::Version}.gem"
30
+ end
31
+
32
+ desc 'Upload website files to rubyforge'
33
+ task :website do
34
+ sh %{rsync -av website/ jnunemaker@rubyforge.org:/var/www/gforge-projects/happymapper}
35
+ end
@@ -0,0 +1,34 @@
1
+ dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ require File.join(dir, 'happymapper')
3
+
4
+ file_contents = File.read(dir + '/../spec/fixtures/pita.xml')
5
+
6
+ # The document `pita.xml` contains both a default namespace and the 'georss'
7
+ # namespace (for the 'point' element).
8
+ module PITA
9
+ class Item
10
+ include HappyMapper
11
+
12
+ tag 'Item' # if you put class in module you need tag
13
+ element :asin, String, :tag => 'ASIN'
14
+ element :detail_page_url, String, :tag => 'DetailPageURL'
15
+ element :manufacturer, String, :tag => 'Manufacturer', :deep => true
16
+ # this is the only element that exists in a different namespace, so it
17
+ # must be explicitly specified
18
+ element :point, String, :tag => 'point', :namespace => 'georss'
19
+ end
20
+
21
+ class Items
22
+ include HappyMapper
23
+
24
+ tag 'Items' # if you put class in module you need tag
25
+ element :total_results, Integer, :tag => 'TotalResults'
26
+ element :total_pages, Integer, :tag => 'TotalPages'
27
+ has_many :items, Item
28
+ end
29
+ end
30
+
31
+ item = PITA::Items.parse(file_contents, :single => true)
32
+ item.items.each do |i|
33
+ puts i.asin, i.detail_page_url, i.manufacturer, ''
34
+ end
@@ -0,0 +1,21 @@
1
+ dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ require File.join(dir, 'happymapper')
3
+
4
+ file_contents = File.read(dir + '/../spec/fixtures/current_weather.xml')
5
+
6
+ class CurrentWeather
7
+ include HappyMapper
8
+
9
+ tag 'ob'
10
+ namespace 'http://www.aws.com/aws'
11
+ element :temperature, Integer, :tag => 'temp'
12
+ element :feels_like, Integer, :tag => 'feels-like'
13
+ element :current_condition, String, :tag => 'current-condition', :attributes => {:icon => String}
14
+ end
15
+
16
+ CurrentWeather.parse(file_contents).each do |current_weather|
17
+ puts "temperature: #{current_weather.temperature}"
18
+ puts "feels_like: #{current_weather.feels_like}"
19
+ puts "current_condition: #{current_weather.current_condition}"
20
+ puts "current_condition.icon: #{current_weather.current_condition.icon}"
21
+ end
@@ -0,0 +1,20 @@
1
+ dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ require File.join(dir, 'happymapper')
3
+
4
+ file_contents = File.read(dir + '/../spec/fixtures/commit.xml')
5
+
6
+ module GitHub
7
+ class Commit
8
+ include HappyMapper
9
+
10
+ tag "commit"
11
+ element :url, String
12
+ element :tree, String
13
+ element :message, String
14
+ element :id, String
15
+ element :'committed-date', Date
16
+ end
17
+ end
18
+
19
+ commit = GitHub::Commit.parse(file_contents)
20
+ puts commit.committed_date, commit.url, commit.id
@@ -0,0 +1,30 @@
1
+ dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ require File.join(dir, 'happymapper')
3
+
4
+ file_contents = File.read(dir + '/../spec/fixtures/multi_street_address.xml')
5
+
6
+ class MultiStreetAddress
7
+ include HappyMapper
8
+
9
+ tag 'address'
10
+
11
+ # allow primitive type to be collection
12
+ has_many :street_address, String, :tag => "streetaddress"
13
+ element :city, String
14
+ element :state_or_province, String, :tag => "stateOrProvince"
15
+ element :zip, String
16
+ element :country, String
17
+ end
18
+
19
+ multi = MultiStreetAddress.parse(file_contents)
20
+
21
+ puts "Street Address:"
22
+
23
+ multi.street_address.each do |street|
24
+ puts street
25
+ end
26
+
27
+ puts "City: #{multi.city}"
28
+ puts "State/Province: #{multi.state_or_province}"
29
+ puts "Zip: #{multi.zip}"
30
+ puts "Country: #{multi.country}"
@@ -0,0 +1,19 @@
1
+ dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ require File.join(dir, 'happymapper')
3
+
4
+ file_contents = File.read(dir + '/../spec/fixtures/posts.xml')
5
+
6
+ class Post
7
+ include HappyMapper
8
+
9
+ attribute :href, String
10
+ attribute :hash, String
11
+ attribute :description, String
12
+ attribute :tag, String
13
+ attribute :time, DateTime
14
+ attribute :others, Integer
15
+ attribute :extended, String
16
+ end
17
+
18
+ posts = Post.parse(file_contents)
19
+ posts.each { |post| puts post.description, post.href, post.extended, '' }
@@ -0,0 +1,37 @@
1
+ dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ require File.join(dir, 'happymapper')
3
+
4
+ file_contents = File.read(dir + '/../spec/fixtures/statuses.xml')
5
+
6
+ class User
7
+ include HappyMapper
8
+
9
+ element :id, Integer
10
+ element :name, String
11
+ element :screen_name, String
12
+ element :location, String
13
+ element :description, String
14
+ element :profile_image_url, String
15
+ element :url, String
16
+ element :protected, Boolean
17
+ element :followers_count, Integer
18
+ end
19
+
20
+ class Status
21
+ include HappyMapper
22
+
23
+ element :id, Integer
24
+ element :text, String
25
+ element :created_at, Time
26
+ element :source, String
27
+ element :truncated, Boolean
28
+ element :in_reply_to_status_id, Integer
29
+ element :in_reply_to_user_id, Integer
30
+ element :favorited, Boolean
31
+ has_one :user, User
32
+ end
33
+
34
+ statuses = Status.parse(file_contents)
35
+ statuses.each do |status|
36
+ puts status.user.name, status.user.screen_name, status.text, status.source, ''
37
+ end
@@ -0,0 +1,321 @@
1
+ require 'rubygems'
2
+ require 'date'
3
+ require 'time'
4
+ require 'xml'
5
+
6
+ class Boolean; end
7
+
8
+ module HappyMapper
9
+
10
+ DEFAULT_NS = "happymapper"
11
+
12
+ def self.included(base)
13
+ base.instance_variable_set("@attributes", {})
14
+ base.instance_variable_set("@elements", {})
15
+ base.instance_variable_set("@registered_namespaces", {})
16
+
17
+ base.extend ClassMethods
18
+ end
19
+
20
+ module ClassMethods
21
+ def attribute(name, type, options={})
22
+ attribute = Attribute.new(name, type, options)
23
+ @attributes[to_s] ||= []
24
+ @attributes[to_s] << attribute
25
+ attr_accessor attribute.method_name.intern
26
+ end
27
+
28
+ def attributes
29
+ @attributes[to_s] || []
30
+ end
31
+
32
+ def element(name, type, options={})
33
+ element = Element.new(name, type, options)
34
+ @elements[to_s] ||= []
35
+ @elements[to_s] << element
36
+ attr_accessor element.method_name.intern
37
+ end
38
+
39
+ def content(name)
40
+ @content = name
41
+ attr_accessor name
42
+ end
43
+
44
+ def raw_content(name)
45
+ @raw_content = name
46
+ attr_accessor name
47
+ end
48
+
49
+ def after_parse_callbacks
50
+ @after_parse_callbacks ||= []
51
+ end
52
+
53
+ def after_parse(&block)
54
+ after_parse_callbacks.push(block)
55
+ end
56
+
57
+ def elements
58
+ @elements[to_s] || []
59
+ end
60
+
61
+ def has_one(name, type, options={})
62
+ element name, type, {:single => true}.merge(options)
63
+ end
64
+
65
+ def has_many(name, type, options={})
66
+ element name, type, {:single => false}.merge(options)
67
+ end
68
+
69
+ # Specify a namespace if a node and all its children are all namespaced
70
+ # elements. This is simpler than passing the :namespace option to each
71
+ # defined element.
72
+ def namespace(namespace = nil)
73
+ @namespace = namespace if namespace
74
+ @namespace
75
+ end
76
+
77
+ def register_namespace(namespace, ns)
78
+ @registered_namespaces.merge!(namespace => ns)
79
+ end
80
+
81
+ def tag(new_tag_name)
82
+ @tag_name = new_tag_name.to_s
83
+ end
84
+
85
+ def tag_name
86
+ @tag_name ||= to_s.split('::')[-1].downcase
87
+ end
88
+
89
+ def parse(xml, options = {})
90
+ if xml.is_a?(XML::Node)
91
+ node = xml
92
+ else
93
+ if xml.is_a?(XML::Document)
94
+ node = xml.root
95
+ else
96
+ node = XML::Parser.string(xml).parse.root
97
+ end
98
+
99
+ root = node.name == tag_name
100
+ end
101
+
102
+ namespace = @namespace || (node.namespaces && node.namespaces.default)
103
+ namespace = "#{DEFAULT_NS}:#{namespace}" if namespace
104
+
105
+ xpath = root ? '/' : './/'
106
+ xpath += "#{DEFAULT_NS}:" if namespace
107
+ xpath += tag_name
108
+
109
+ nodes = node.find(xpath, Array(namespace))
110
+ collection = nodes.collect do |n|
111
+ obj = new
112
+
113
+ attributes.each do |attr|
114
+ obj.send("#{attr.method_name}=",
115
+ attr.from_xml_node(n, namespace))
116
+ end
117
+
118
+ elements.each do |elem|
119
+ obj.send("#{elem.method_name}=",
120
+ elem.from_xml_node(n, namespace))
121
+ end
122
+
123
+ obj.send("#{@content}=", n.content) if @content
124
+ obj.send("#{@raw_content}=", n.inner_xml) if @raw_content
125
+
126
+ obj.class.after_parse_callbacks.each { |callback| callback.call(obj) }
127
+
128
+ obj
129
+ end
130
+
131
+ # per http://libxml.rubyforge.org/rdoc/classes/LibXML/XML/Document.html#M000354
132
+ nodes = nil
133
+
134
+ if options[:single] || root
135
+ collection.first
136
+ else
137
+ collection
138
+ end
139
+ end
140
+
141
+ end
142
+
143
+ #
144
+ # Create an xml representation of the specified class based on defined
145
+ # HappyMapper elements and attributes. The method is defined in a way
146
+ # that it can be called recursively by classes that are also HappyMapper
147
+ # classes, allowg for the composition of classes.
148
+ #
149
+ def to_xml(parent_node = nil, default_namespace = nil)
150
+
151
+ #
152
+ # Create a tag that uses the tag name of the class that has no contents
153
+ # but has the specified namespace or uses the default namespace
154
+ #
155
+ current_node = XML::Node.new(self.class.tag_name)
156
+
157
+
158
+ if parent_node
159
+ #
160
+ # if #to_xml has been called with a parent_node that means this method
161
+ # is being called recursively (or a special case) and we want to return
162
+ # the parent_node with the new node as a child
163
+ #
164
+ parent_node << current_node
165
+ else
166
+ #
167
+ # If #to_xml has been called without a Node (and namespace) that
168
+ # means we want to return an xml document
169
+ #
170
+ write_out_to_xml = true
171
+ end
172
+
173
+ #
174
+ # Add all the registered namespaces to the current node and the current node's
175
+ # root element. Without adding it to the root element it is not possible to
176
+ # parse or use xpath to find elements.
177
+ #
178
+ if self.class.instance_variable_get('@registered_namespaces')
179
+
180
+ # Given a node, continue moving up to parents until there are no more parents
181
+ find_root_node = lambda {|node| while node.parent? ; node = node.parent ; end ; node }
182
+ root_node = find_root_node.call(current_node)
183
+
184
+ # Add the registered namespace to the found root node only if it does not already have one defined
185
+ self.class.instance_variable_get('@registered_namespaces').each_pair do |prefix,href|
186
+ XML::Namespace.new(current_node,prefix,href)
187
+ XML::Namespace.new(root_node,prefix,href) unless root_node.namespaces.find_by_prefix(prefix)
188
+ end
189
+ end
190
+
191
+ #
192
+ # Determine the tag namespace if one has been specified. This value takes
193
+ # precendence over one that is handed down to composed sub-classes.
194
+ #
195
+ tag_namespace = current_node.namespaces.find_by_prefix(self.class.namespace) || default_namespace
196
+
197
+ # Set the namespace of the current node to the specified namespace
198
+ current_node.namespaces.namespace = tag_namespace if tag_namespace
199
+
200
+ #
201
+ # Add all the attribute tags to the current node with their namespace, if one
202
+ # is defined, or the namespace handed down to the node.
203
+ #
204
+ self.class.attributes.each do |attribute|
205
+ attribute_namespace = current_node.namespaces.find_by_prefix(attribute.options[:namespace]) || default_namespace
206
+
207
+ value = send(attribute.method_name)
208
+
209
+ #
210
+ # If the attribute has a :on_save attribute defined that is a proc or
211
+ # a defined method, then call those with the current value.
212
+ #
213
+ if on_save_operation = attribute.options[:on_save]
214
+ if on_save_operation.is_a?(Proc)
215
+ value = on_save_operation.call(value)
216
+ elsif respond_to?(on_save_operation)
217
+ value = send(on_save_operation,value)
218
+ end
219
+ end
220
+
221
+ current_node[ "#{attribute_namespace ? "#{attribute_namespace.prefix}:" : ""}#{attribute.tag}" ] = value
222
+ end
223
+
224
+ #
225
+ # All all the elements defined (e.g. has_one, has_many, element) ...
226
+ #
227
+ self.class.elements.each do |element|
228
+
229
+ tag = element.tag || element.name
230
+
231
+ element_namespace = current_node.namespaces.find_by_prefix(element.options[:namespace]) || tag_namespace
232
+
233
+ value = send(element.name)
234
+
235
+ #
236
+ # If the element defines an :on_save lambda/proc then we will call that
237
+ # operation on the specified value. This allows for operations to be
238
+ # performed to convert the value to a specific value to be saved to the xml.
239
+ #
240
+ if on_save_operation = element.options[:on_save]
241
+ if on_save_operation.is_a?(Proc)
242
+ value = on_save_operation.call(value)
243
+ elsif respond_to?(on_save_operation)
244
+ value = send(on_save_operation,value)
245
+ end
246
+ end
247
+
248
+ #
249
+ # Normally a nil value would be ignored, however if specified then
250
+ # an empty element will be written to the xml
251
+ #
252
+ if value.nil? && element.options[:state_when_nil]
253
+ current_node << XML::Node.new(tag,nil,element_namespace)
254
+ end
255
+
256
+ #
257
+ # To allow for us to treat both groups of items and singular items
258
+ # equally we wrap the value and treat it as an array.
259
+ #
260
+ if value.nil?
261
+ values = []
262
+ elsif value.respond_to?(:to_ary) && !element.options[:single]
263
+ values = value.to_ary
264
+ else
265
+ values = [value]
266
+ end
267
+
268
+
269
+ values.each do |item|
270
+
271
+ if item.is_a?(HappyMapper)
272
+
273
+ #
274
+ # Other HappyMapper items that are convertable should not be called
275
+ # with the current node and the namespace defined for the element.
276
+ #
277
+ item.to_xml(current_node,element_namespace)
278
+
279
+ elsif item
280
+
281
+ #
282
+ # When a value exists we should append the value for the tag
283
+ #
284
+ current_node << XML::Node.new(tag,item.to_s,element_namespace)
285
+
286
+ else
287
+
288
+ #
289
+ # Normally a nil value would be ignored, however if specified then
290
+ # an empty element will be written to the xml
291
+ #
292
+ current_node << XML.Node.new(tag,nil,element_namespace) if element.options[:state_when_nil]
293
+
294
+ end
295
+
296
+ end
297
+
298
+ end
299
+
300
+
301
+ #
302
+ # Generate xml from a document if no node was passed as a parameter. Otherwise
303
+ # this method is being called recursively (or special case) and we should
304
+ # return the node with this node attached as a child.
305
+ #
306
+ if write_out_to_xml
307
+ document = XML::Document.new
308
+ document.root = current_node
309
+ document.to_s
310
+ else
311
+ parent_node
312
+ end
313
+
314
+ end
315
+
316
+
317
+ end
318
+
319
+ require File.dirname(__FILE__) + '/happymapper/item'
320
+ require File.dirname(__FILE__) + '/happymapper/attribute'
321
+ require File.dirname(__FILE__) + '/happymapper/element'