happymapper-swanandp 0.4.0

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