unhappymapper 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/README.md +479 -0
  2. data/TODO +0 -0
  3. data/lib/happymapper/attribute.rb +3 -0
  4. data/lib/happymapper/element.rb +3 -0
  5. data/lib/happymapper/item.rb +250 -0
  6. data/lib/happymapper/text_node.rb +3 -0
  7. data/lib/happymapper.rb +574 -0
  8. data/spec/fixtures/address.xml +8 -0
  9. data/spec/fixtures/ambigous_items.xml +22 -0
  10. data/spec/fixtures/analytics.xml +61 -0
  11. data/spec/fixtures/analytics_profile.xml +127 -0
  12. data/spec/fixtures/commit.xml +52 -0
  13. data/spec/fixtures/current_weather.xml +89 -0
  14. data/spec/fixtures/dictionary.xml +20 -0
  15. data/spec/fixtures/family_tree.xml +21 -0
  16. data/spec/fixtures/inagy.xml +86 -0
  17. data/spec/fixtures/lastfm.xml +355 -0
  18. data/spec/fixtures/multiple_namespaces.xml +170 -0
  19. data/spec/fixtures/multiple_primitives.xml +5 -0
  20. data/spec/fixtures/pita.xml +133 -0
  21. data/spec/fixtures/posts.xml +23 -0
  22. data/spec/fixtures/product_default_namespace.xml +17 -0
  23. data/spec/fixtures/product_no_namespace.xml +10 -0
  24. data/spec/fixtures/product_single_namespace.xml +10 -0
  25. data/spec/fixtures/quarters.xml +19 -0
  26. data/spec/fixtures/radar.xml +21 -0
  27. data/spec/fixtures/statuses.xml +422 -0
  28. data/spec/fixtures/subclass_namespace.xml +50 -0
  29. data/spec/happymapper_attribute_spec.rb +21 -0
  30. data/spec/happymapper_element_spec.rb +21 -0
  31. data/spec/happymapper_item_spec.rb +115 -0
  32. data/spec/happymapper_spec.rb +941 -0
  33. data/spec/happymapper_text_node_spec.rb +21 -0
  34. data/spec/happymapper_to_xml_namespaces_spec.rb +196 -0
  35. data/spec/happymapper_to_xml_spec.rb +196 -0
  36. data/spec/ignay_spec.rb +95 -0
  37. data/spec/spec_helper.rb +7 -0
  38. data/spec/xpath_spec.rb +88 -0
  39. metadata +150 -0
@@ -0,0 +1,250 @@
1
+ module HappyMapper
2
+ class Item
3
+ attr_accessor :name, :type, :tag, :options, :namespace
4
+
5
+ Types = [String, Float, Time, Date, DateTime, Integer, Boolean]
6
+
7
+ # options:
8
+ # :deep => Boolean False to only parse element's children, True to include
9
+ # grandchildren and all others down the chain (// in xpath)
10
+ # :namespace => String Element's namespace if it's not the global or inherited
11
+ # default
12
+ # :parser => Symbol Class method to use for type coercion.
13
+ # :raw => Boolean Use raw node value (inc. tags) when parsing.
14
+ # :single => Boolean False if object should be collection, True for single object
15
+ # :tag => String Element name if it doesn't match the specified name.
16
+ def initialize(name, type, o={})
17
+ self.name = name.to_s
18
+ self.type = type
19
+ #self.tag = o.delete(:tag) || name.to_s
20
+ self.tag = o[:tag] || name.to_s
21
+ self.options = { :single => true }.merge(o.merge(:name => self.name))
22
+
23
+ @xml_type = self.class.to_s.split('::').last.downcase
24
+ end
25
+
26
+ def constant
27
+ @constant ||= constantize(type)
28
+ end
29
+
30
+ #
31
+ # @param [XMLNode] node the xml node that is being parsed
32
+ # @param [String] namespace the name of the namespace
33
+ # @param [Hash] xpath_options additional xpath options
34
+ #
35
+ def from_xml_node(node, namespace, xpath_options)
36
+
37
+ # If the item is defined as a primitive type then cast the value to that type
38
+ # else if the type is XMLContent then store the xml value
39
+ # else the type, specified, needs to handle the parsing.
40
+
41
+ if primitive?
42
+ find(node, namespace, xpath_options) do |n|
43
+ if n.respond_to?(:content)
44
+ typecast(n.content)
45
+ else
46
+ typecast(n.to_s)
47
+ end
48
+ end
49
+ elsif constant == XmlContent
50
+ find(node, namespace, xpath_options) do |n|
51
+ n = n.children if n.respond_to?(:children)
52
+ n.respond_to?(:to_xml) ? n.to_xml : n.to_s
53
+ end
54
+ else
55
+
56
+ # When not a primitive type or XMLContent then default to using the
57
+ # class method #parse of the type class. If the option 'parser' has been
58
+ # defined then call that method on the type class instead of #parse
59
+
60
+ if options[:parser]
61
+ find(node, namespace, xpath_options) do |n|
62
+ if n.respond_to?(:content) && !options[:raw]
63
+ value = n.content
64
+ else
65
+ value = n.to_s
66
+ end
67
+
68
+ begin
69
+ constant.send(options[:parser].to_sym, value)
70
+ rescue
71
+ nil
72
+ end
73
+ end
74
+ else
75
+ constant.parse(node, options.merge(:namespaces => xpath_options))
76
+ end
77
+ end
78
+ end
79
+
80
+ def xpath(namespace = self.namespace)
81
+ xpath = ''
82
+ xpath += './/' if options[:deep]
83
+ xpath += "#{namespace}:" if namespace
84
+ xpath += tag
85
+ #puts "xpath: #{xpath}"
86
+ xpath
87
+ end
88
+
89
+ # @return [Boolean] true if the type defined for the item is defined in the
90
+ # list of primite types {Types}.
91
+ def primitive?
92
+ Types.include?(constant)
93
+ end
94
+
95
+ def element?
96
+ @xml_type == 'element'
97
+ end
98
+
99
+ def attribute?
100
+ @xml_type == 'attribute'
101
+ end
102
+
103
+ def text_node?
104
+ @xml_type == 'textnode'
105
+ end
106
+
107
+ def method_name
108
+ @method_name ||= name.tr('-', '_')
109
+ end
110
+
111
+ #
112
+ # When the type of the item is a primitive type, this will convert value specifed
113
+ # to the particular primitive type. If it fails during this process it will
114
+ # return the original String value.
115
+ #
116
+ # @param [String] value the string value parsed from the XML value that will
117
+ # be converted to the particular primitive type.
118
+ #
119
+ # @return [String,Float,Time,Date,DateTime,Boolean,Integer] the converted value
120
+ # to the new type.
121
+ #
122
+ def typecast(value)
123
+ return value if value.kind_of?(constant) || value.nil?
124
+ begin
125
+ if constant == String then value.to_s
126
+ elsif constant == Float then value.to_f
127
+ elsif constant == Time then Time.parse(value.to_s) rescue Time.at(value.to_i)
128
+ elsif constant == Date then Date.parse(value.to_s)
129
+ elsif constant == DateTime then DateTime.parse(value.to_s)
130
+ elsif constant == Boolean then ['true', 't', '1'].include?(value.to_s.downcase)
131
+ elsif constant == Integer
132
+ # ganked from datamapper
133
+ value_to_i = value.to_i
134
+ if value_to_i == 0 && value != '0'
135
+ value_to_s = value.to_s
136
+ begin
137
+ Integer(value_to_s =~ /^(\d+)/ ? $1 : value_to_s)
138
+ rescue ArgumentError
139
+ nil
140
+ end
141
+ else
142
+ value_to_i
143
+ end
144
+ else
145
+ value
146
+ end
147
+ rescue
148
+ value
149
+ end
150
+ end
151
+
152
+ private
153
+
154
+ #
155
+ # Convert any String defined types into their constant version so that
156
+ # the method #parse or the custom defined parser method would be used.
157
+ #
158
+ # @param [String,Constant] type is the name of the class or the constant
159
+ # for the class.
160
+ # @return [Constant] the constant of the type
161
+ #
162
+ def constantize(type)
163
+ if type.is_a?(String)
164
+ names = type.split('::')
165
+ constant = Object
166
+ names.each do |name|
167
+ constant =
168
+ if constant.const_defined?(name)
169
+ constant.const_get(name)
170
+ else
171
+ constant.const_missing(name)
172
+ end
173
+ end
174
+ constant
175
+ else
176
+ type
177
+ end
178
+ end
179
+
180
+
181
+ def find(node, namespace, xpath_options, &block)
182
+ if self.namespace && xpath_options["xmlns:#{self.namespace}"]
183
+ # from the class definition
184
+ namespace = self.namespace
185
+ elsif options[:namespace] && xpath_options["xmlns:#{options[:namespace]}"]
186
+ namespace = options[:namespace]
187
+ end
188
+
189
+ if element?
190
+ if options[:single]
191
+
192
+ result = nil
193
+
194
+ if options[:xpath]
195
+ result = node.xpath(options[:xpath], xpath_options)
196
+ else
197
+ result = node.xpath(xpath(namespace), xpath_options)
198
+ end
199
+
200
+ if result
201
+ value = options[:single] ? yield(result.first) : result.map {|r| yield r }
202
+ handle_attributes_option(result, value, xpath_options)
203
+
204
+ value
205
+ end
206
+ else
207
+
208
+ target_path = options[:xpath] ? options[:xpath] : xpath(namespace)
209
+
210
+ results = node.xpath(target_path, xpath_options).collect do |result|
211
+ value = yield(result)
212
+ handle_attributes_option(result, value, xpath_options)
213
+ value
214
+ end
215
+ results
216
+ end
217
+ elsif attribute?
218
+
219
+ if options[:xpath]
220
+ yield(node.xpath(options[:xpath],xpath_options))
221
+ else
222
+ yield(node[tag])
223
+ end
224
+
225
+ else # text node
226
+ yield(node.children.detect{|c| c.text?})
227
+ end
228
+ end
229
+
230
+ def handle_attributes_option(result, value, xpath_options)
231
+ if options[:attributes].is_a?(Hash)
232
+ result = result.first if result.respond_to?(:first)
233
+
234
+ result.attribute_nodes.each do |xml_attribute|
235
+ if attribute_options = options[:attributes][xml_attribute.name.to_sym]
236
+ attribute_value = Attribute.new(xml_attribute.name.to_sym, *attribute_options).from_xml_node(result, namespace, xpath_options)
237
+
238
+ result.instance_eval <<-EOV
239
+ def value.#{xml_attribute.name}
240
+ #{attribute_value.inspect}
241
+ end
242
+ EOV
243
+ end # if attributes_options
244
+ end # attribute_nodes.each
245
+ end # if options[:attributes]
246
+ end # def handle...
247
+
248
+ # end private methods
249
+ end
250
+ end
@@ -0,0 +1,3 @@
1
+ module HappyMapper
2
+ class TextNode < Item; end
3
+ end