Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100644 318 lines (249 sloc) 9.291 kB
5007845 FIX: Requires, XML, and Spec typo
Frank Webber authored
1 require 'rubygems'
9d8c631 @jnunemaker initial commit, attributes and elements with primitive types work
authored
2 require 'date'
3 require 'time'
4 require 'xml'
5
6 class Boolean; end
7
8 module HappyMapper
11e4264 @mojodna Improved namespace support
mojodna authored
9
10 DEFAULT_NS = "happymapper"
11
9d8c631 @jnunemaker initial commit, attributes and elements with primitive types work
authored
12 def self.included(base)
13 base.instance_variable_set("@attributes", {})
14 base.instance_variable_set("@elements", {})
a25d080 @burtlo Feature: #to_xml
burtlo authored
15 base.instance_variable_set("@registered_namespaces", {})
16
9d8c631 @jnunemaker initial commit, attributes and elements with primitive types work
authored
17 base.extend ClassMethods
18 end
97693df @jnunemaker Added support for after parse callbacks.
authored
19
9d8c631 @jnunemaker initial commit, attributes and elements with primitive types work
authored
20 module ClassMethods
46dec6f @jnunemaker Added some crappy amazon xml to the specs. Added some examples.
authored
21 def attribute(name, type, options={})
22 attribute = Attribute.new(name, type, options)
9d8c631 @jnunemaker initial commit, attributes and elements with primitive types work
authored
23 @attributes[to_s] ||= []
24 @attributes[to_s] << attribute
a88ab23 @jnunemaker Automatic handling of namespaces. Much thanks to Robert Lowrey for st…
authored
25 attr_accessor attribute.method_name.intern
9d8c631 @jnunemaker initial commit, attributes and elements with primitive types work
authored
26 end
97693df @jnunemaker Added support for after parse callbacks.
authored
27
9d8c631 @jnunemaker initial commit, attributes and elements with primitive types work
authored
28 def attributes
29 @attributes[to_s] || []
30 end
97693df @jnunemaker Added support for after parse callbacks.
authored
31
46dec6f @jnunemaker Added some crappy amazon xml to the specs. Added some examples.
authored
32 def element(name, type, options={})
33 element = Element.new(name, type, options)
9d8c631 @jnunemaker initial commit, attributes and elements with primitive types work
authored
34 @elements[to_s] ||= []
35 @elements[to_s] << element
a88ab23 @jnunemaker Automatic handling of namespaces. Much thanks to Robert Lowrey for st…
authored
36 attr_accessor element.method_name.intern
9d8c631 @jnunemaker initial commit, attributes and elements with primitive types work
authored
37 end
88891f6 @technicalpickles Support for assigning a node's content to an attribute.
technicalpickles authored
38
39 def content(name)
40 @content = name
41 attr_accessor name
42 end
97693df @jnunemaker Added support for after parse callbacks.
authored
43
44 def after_parse_callbacks
45 @after_parse_callbacks ||= []
46 end
47
48 def after_parse(&block)
49 after_parse_callbacks.push(block)
50 end
51
9d8c631 @jnunemaker initial commit, attributes and elements with primitive types work
authored
52 def elements
53 @elements[to_s] || []
54 end
97693df @jnunemaker Added support for after parse callbacks.
authored
55
501f761 @jnunemaker Fixed default namespace issue. Added has_one and has_many.
authored
56 def has_one(name, type, options={})
57 element name, type, {:single => true}.merge(options)
58 end
97693df @jnunemaker Added support for after parse callbacks.
authored
59
501f761 @jnunemaker Fixed default namespace issue. Added has_one and has_many.
authored
60 def has_many(name, type, options={})
61 element name, type, {:single => false}.merge(options)
62 end
11e4264 @mojodna Improved namespace support
mojodna authored
63
64 # Specify a namespace if a node and all its children are all namespaced
65 # elements. This is simpler than passing the :namespace option to each
66 # defined element.
67 def namespace(namespace = nil)
68 @namespace = namespace if namespace
69 @namespace
70 end
a25d080 @burtlo Feature: #to_xml
burtlo authored
71
72 def register_namespace(namespace, ns)
73 @registered_namespaces.merge!(namespace => ns)
74 end
11e4264 @mojodna Improved namespace support
mojodna authored
75
6f4ef79 @mojodna auto-detect root nodes
mojodna authored
76 def tag(new_tag_name)
9d8c631 @jnunemaker initial commit, attributes and elements with primitive types work
authored
77 @tag_name = new_tag_name.to_s
78 end
97693df @jnunemaker Added support for after parse callbacks.
authored
79
4cf558a @bkeepers Removed get_ prefix, it reminds me too much of Java
bkeepers authored
80 def tag_name
81 @tag_name ||= to_s.split('::')[-1].downcase
9d8c631 @jnunemaker initial commit, attributes and elements with primitive types work
authored
82 end
97693df @jnunemaker Added support for after parse callbacks.
authored
83
9b58408 @mojodna Pass namespaces around to avoid storing state
mojodna authored
84 def parse(xml, options = {})
11e4264 @mojodna Improved namespace support
mojodna authored
85 if xml.is_a?(XML::Node)
86 node = xml
87 else
9b58408 @mojodna Pass namespaces around to avoid storing state
mojodna authored
88 if xml.is_a?(XML::Document)
89 node = xml.root
90 else
545ebcd @jnunemaker Removed no longer needed libxml helpers.
authored
91 node = XML::Parser.string(xml).parse.root
9b58408 @mojodna Pass namespaces around to avoid storing state
mojodna authored
92 end
93
4cf558a @bkeepers Removed get_ prefix, it reminds me too much of Java
bkeepers authored
94 root = node.name == tag_name
a434992 @jnunemaker Implemented first pass of automatic namespace support.
authored
95 end
11e4264 @mojodna Improved namespace support
mojodna authored
96
374850a @bkeepers Rework namespaces to work for namespaces declared inline.
bkeepers authored
97 namespace = @namespace || (node.namespaces && node.namespaces.default)
98 namespace = "#{DEFAULT_NS}:#{namespace}" if namespace
11e4264 @mojodna Improved namespace support
mojodna authored
99
dbaaebc @teleological Support for explicit xpath option
teleological authored
100 unless xpath = options[:xpath]
101 xpath = root ? '/' : './/'
102 xpath += "#{DEFAULT_NS}:" if namespace
103 xpath += tag_name
104 end
97693df @jnunemaker Added support for after parse callbacks.
authored
105
374850a @bkeepers Rework namespaces to work for namespaces declared inline.
bkeepers authored
106 nodes = node.find(xpath, Array(namespace))
9b58408 @mojodna Pass namespaces around to avoid storing state
mojodna authored
107 collection = nodes.collect do |n|
a88ab23 @jnunemaker Automatic handling of namespaces. Much thanks to Robert Lowrey for st…
authored
108 obj = new
97693df @jnunemaker Added support for after parse callbacks.
authored
109
110 attributes.each do |attr|
111 obj.send("#{attr.method_name}=",
a25d080 @burtlo Feature: #to_xml
burtlo authored
112 attr.from_xml_node(n, namespace))
a88ab23 @jnunemaker Automatic handling of namespaces. Much thanks to Robert Lowrey for st…
authored
113 end
97693df @jnunemaker Added support for after parse callbacks.
authored
114
a88ab23 @jnunemaker Automatic handling of namespaces. Much thanks to Robert Lowrey for st…
authored
115 elements.each do |elem|
97693df @jnunemaker Added support for after parse callbacks.
authored
116 obj.send("#{elem.method_name}=",
a25d080 @burtlo Feature: #to_xml
burtlo authored
117 elem.from_xml_node(n, namespace))
a88ab23 @jnunemaker Automatic handling of namespaces. Much thanks to Robert Lowrey for st…
authored
118 end
88891f6 @technicalpickles Support for assigning a node's content to an attribute.
technicalpickles authored
119
120 obj.send("#{@content}=", n.content) if @content
97693df @jnunemaker Added support for after parse callbacks.
authored
121
122 obj.class.after_parse_callbacks.each { |callback| callback.call(obj) }
123
9b58408 @mojodna Pass namespaces around to avoid storing state
mojodna authored
124 obj
3ecf732 added support for nested collection elements
Justin Marney authored
125 end
126
278c968 @jnunemaker Added libxml helper and refactored a few of the methods
authored
127 # per http://libxml.rubyforge.org/rdoc/classes/LibXML/XML/Document.html#M000354
128 nodes = nil
a434992 @jnunemaker Implemented first pass of automatic namespace support.
authored
129
9b58408 @mojodna Pass namespaces around to avoid storing state
mojodna authored
130 if options[:single] || root
131 collection.first
132 else
133 collection
134 end
9d8c631 @jnunemaker initial commit, attributes and elements with primitive types work
authored
135 end
a25d080 @burtlo Feature: #to_xml
burtlo authored
136
9d8c631 @jnunemaker initial commit, attributes and elements with primitive types work
authored
137 end
a25d080 @burtlo Feature: #to_xml
burtlo authored
138
139 #
140 # Create an xml representation of the specified class based on defined
141 # HappyMapper elements and attributes. The method is defined in a way
142 # that it can be called recursively by classes that are also HappyMapper
6d730a9 @nikkypx Fix typo in comment
nikkypx authored
143 # classes, allowing for the composition of classes.
a25d080 @burtlo Feature: #to_xml
burtlo authored
144 #
47b9565 @burtlo Comments and Test Cleanup
burtlo authored
145 def to_xml(parent_node = nil, default_namespace = nil)
a25d080 @burtlo Feature: #to_xml
burtlo authored
146
147 #
148 # Create a tag that uses the tag name of the class that has no contents
149 # but has the specified namespace or uses the default namespace
150 #
47b9565 @burtlo Comments and Test Cleanup
burtlo authored
151 current_node = XML::Node.new(self.class.tag_name)
152
153
154 if parent_node
155 #
156 # if #to_xml has been called with a parent_node that means this method
157 # is being called recursively (or a special case) and we want to return
158 # the parent_node with the new node as a child
159 #
160 parent_node << current_node
161 else
162 #
163 # If #to_xml has been called without a Node (and namespace) that
164 # means we want to return an xml document
165 #
166 write_out_to_xml = true
167 end
a25d080 @burtlo Feature: #to_xml
burtlo authored
168
169 #
47b9565 @burtlo Comments and Test Cleanup
burtlo authored
170 # Add all the registered namespaces to the current node and the current node's
171 # root element. Without adding it to the root element it is not possible to
172 # parse or use xpath to find elements.
a25d080 @burtlo Feature: #to_xml
burtlo authored
173 #
174 if self.class.instance_variable_get('@registered_namespaces')
175
47b9565 @burtlo Comments and Test Cleanup
burtlo authored
176 # Given a node, continue moving up to parents until there are no more parents
177 find_root_node = lambda {|node| while node.parent? ; node = node.parent ; end ; node }
178 root_node = find_root_node.call(current_node)
179
180 # Add the registered namespace to the found root node only if it does not already have one defined
a25d080 @burtlo Feature: #to_xml
burtlo authored
181 self.class.instance_variable_get('@registered_namespaces').each_pair do |prefix,href|
47b9565 @burtlo Comments and Test Cleanup
burtlo authored
182 XML::Namespace.new(current_node,prefix,href)
a25d080 @burtlo Feature: #to_xml
burtlo authored
183 XML::Namespace.new(root_node,prefix,href) unless root_node.namespaces.find_by_prefix(prefix)
184 end
185 end
186
187 #
47b9565 @burtlo Comments and Test Cleanup
burtlo authored
188 # Determine the tag namespace if one has been specified. This value takes
189 # precendence over one that is handed down to composed sub-classes.
a25d080 @burtlo Feature: #to_xml
burtlo authored
190 #
47b9565 @burtlo Comments and Test Cleanup
burtlo authored
191 tag_namespace = current_node.namespaces.find_by_prefix(self.class.namespace) || default_namespace
a25d080 @burtlo Feature: #to_xml
burtlo authored
192
47b9565 @burtlo Comments and Test Cleanup
burtlo authored
193 # Set the namespace of the current node to the specified namespace
194 current_node.namespaces.namespace = tag_namespace if tag_namespace
a25d080 @burtlo Feature: #to_xml
burtlo authored
195
196 #
47b9565 @burtlo Comments and Test Cleanup
burtlo authored
197 # Add all the attribute tags to the current node with their namespace, if one
198 # is defined, or the namespace handed down to the node.
a25d080 @burtlo Feature: #to_xml
burtlo authored
199 #
200 self.class.attributes.each do |attribute|
47b9565 @burtlo Comments and Test Cleanup
burtlo authored
201 attribute_namespace = current_node.namespaces.find_by_prefix(attribute.options[:namespace]) || default_namespace
202
203 value = send(attribute.method_name)
204
205 #
206 # If the attribute has a :on_save attribute defined that is a proc or
207 # a defined method, then call those with the current value.
208 #
209 if on_save_operation = attribute.options[:on_save]
210 if on_save_operation.is_a?(Proc)
211 value = on_save_operation.call(value)
212 elsif respond_to?(on_save_operation)
213 value = send(on_save_operation,value)
214 end
215 end
216
e6c2c6b @nerab Allow optional attributes with types other than String
nerab authored
217 current_node[ "#{attribute_namespace ? "#{attribute_namespace.prefix}:" : ""}#{attribute.tag}" ] = value.to_s
a25d080 @burtlo Feature: #to_xml
burtlo authored
218 end
219
47b9565 @burtlo Comments and Test Cleanup
burtlo authored
220 #
221 # All all the elements defined (e.g. has_one, has_many, element) ...
222 #
a25d080 @burtlo Feature: #to_xml
burtlo authored
223 self.class.elements.each do |element|
224
225 tag = element.tag || element.name
47b9565 @burtlo Comments and Test Cleanup
burtlo authored
226
227 element_namespace = current_node.namespaces.find_by_prefix(element.options[:namespace]) || tag_namespace
228
a25d080 @burtlo Feature: #to_xml
burtlo authored
229 value = send(element.name)
230
231 #
47b9565 @burtlo Comments and Test Cleanup
burtlo authored
232 # If the element defines an :on_save lambda/proc then we will call that
a25d080 @burtlo Feature: #to_xml
burtlo authored
233 # operation on the specified value. This allows for operations to be
234 # performed to convert the value to a specific value to be saved to the xml.
235 #
47b9565 @burtlo Comments and Test Cleanup
burtlo authored
236 if on_save_operation = element.options[:on_save]
237 if on_save_operation.is_a?(Proc)
238 value = on_save_operation.call(value)
239 elsif respond_to?(on_save_operation)
240 value = send(on_save_operation,value)
241 end
a25d080 @burtlo Feature: #to_xml
burtlo authored
242 end
243
244 #
245 # Normally a nil value would be ignored, however if specified then
246 # an empty element will be written to the xml
247 #
248 if value.nil? && element.options[:state_when_nil]
47b9565 @burtlo Comments and Test Cleanup
burtlo authored
249 current_node << XML::Node.new(tag,nil,element_namespace)
a25d080 @burtlo Feature: #to_xml
burtlo authored
250 end
251
252 #
253 # To allow for us to treat both groups of items and singular items
254 # equally we wrap the value and treat it as an array.
255 #
256 if value.nil?
257 values = []
258 elsif value.respond_to?(:to_ary) && !element.options[:single]
259 values = value.to_ary
260 else
261 values = [value]
262 end
263
264
265 values.each do |item|
266
267 if item.is_a?(HappyMapper)
268
269 #
47b9565 @burtlo Comments and Test Cleanup
burtlo authored
270 # Other HappyMapper items that are convertable should not be called
271 # with the current node and the namespace defined for the element.
a25d080 @burtlo Feature: #to_xml
burtlo authored
272 #
47b9565 @burtlo Comments and Test Cleanup
burtlo authored
273 item.to_xml(current_node,element_namespace)
a25d080 @burtlo Feature: #to_xml
burtlo authored
274
275 elsif item
276
277 #
278 # When a value exists we should append the value for the tag
279 #
47b9565 @burtlo Comments and Test Cleanup
burtlo authored
280 current_node << XML::Node.new(tag,item.to_s,element_namespace)
a25d080 @burtlo Feature: #to_xml
burtlo authored
281
282 else
283
284 #
285 # Normally a nil value would be ignored, however if specified then
286 # an empty element will be written to the xml
287 #
47b9565 @burtlo Comments and Test Cleanup
burtlo authored
288 current_node << XML.Node.new(tag,nil,element_namespace) if element.options[:state_when_nil]
a25d080 @burtlo Feature: #to_xml
burtlo authored
289
290 end
291
292 end
293
294 end
295
296
47b9565 @burtlo Comments and Test Cleanup
burtlo authored
297 #
298 # Generate xml from a document if no node was passed as a parameter. Otherwise
299 # this method is being called recursively (or special case) and we should
300 # return the node with this node attached as a child.
301 #
a25d080 @burtlo Feature: #to_xml
burtlo authored
302 if write_out_to_xml
303 document = XML::Document.new
47b9565 @burtlo Comments and Test Cleanup
burtlo authored
304 document.root = current_node
a25d080 @burtlo Feature: #to_xml
burtlo authored
305 document.to_s
306 else
47b9565 @burtlo Comments and Test Cleanup
burtlo authored
307 parent_node
a25d080 @burtlo Feature: #to_xml
burtlo authored
308 end
309
310 end
311
312
9d8708d @jnunemaker Moved item, attribute and element into their own files.
authored
313 end
314
5007845 FIX: Requires, XML, and Spec typo
Frank Webber authored
315 require File.dirname(__FILE__) + '/happymapper/item'
316 require File.dirname(__FILE__) + '/happymapper/attribute'
317 require File.dirname(__FILE__) + '/happymapper/element'
Something went wrong with that request. Please try again.