xmlmapper 0.5.9

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 (59) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +35 -0
  3. data/README.md +605 -0
  4. data/lib/happymapper.rb +776 -0
  5. data/lib/happymapper/anonymous_mapper.rb +114 -0
  6. data/lib/happymapper/attribute.rb +21 -0
  7. data/lib/happymapper/element.rb +55 -0
  8. data/lib/happymapper/item.rb +160 -0
  9. data/lib/happymapper/supported_types.rb +140 -0
  10. data/lib/happymapper/text_node.rb +8 -0
  11. data/lib/happymapper/version.rb +3 -0
  12. data/lib/xmlmapper.rb +1 -0
  13. data/spec/attribute_default_value_spec.rb +50 -0
  14. data/spec/attributes_spec.rb +36 -0
  15. data/spec/fixtures/address.xml +9 -0
  16. data/spec/fixtures/ambigous_items.xml +22 -0
  17. data/spec/fixtures/analytics.xml +61 -0
  18. data/spec/fixtures/analytics_profile.xml +127 -0
  19. data/spec/fixtures/atom.xml +19 -0
  20. data/spec/fixtures/commit.xml +52 -0
  21. data/spec/fixtures/current_weather.xml +89 -0
  22. data/spec/fixtures/current_weather_missing_elements.xml +18 -0
  23. data/spec/fixtures/default_namespace_combi.xml +6 -0
  24. data/spec/fixtures/dictionary.xml +20 -0
  25. data/spec/fixtures/family_tree.xml +21 -0
  26. data/spec/fixtures/inagy.xml +85 -0
  27. data/spec/fixtures/lastfm.xml +355 -0
  28. data/spec/fixtures/multiple_namespaces.xml +170 -0
  29. data/spec/fixtures/multiple_primitives.xml +5 -0
  30. data/spec/fixtures/optional_attributes.xml +6 -0
  31. data/spec/fixtures/pita.xml +133 -0
  32. data/spec/fixtures/posts.xml +23 -0
  33. data/spec/fixtures/product_default_namespace.xml +18 -0
  34. data/spec/fixtures/product_no_namespace.xml +10 -0
  35. data/spec/fixtures/product_single_namespace.xml +10 -0
  36. data/spec/fixtures/quarters.xml +19 -0
  37. data/spec/fixtures/radar.xml +21 -0
  38. data/spec/fixtures/set_config_options.xml +3 -0
  39. data/spec/fixtures/statuses.xml +422 -0
  40. data/spec/fixtures/subclass_namespace.xml +50 -0
  41. data/spec/fixtures/wrapper.xml +11 -0
  42. data/spec/happymapper/attribute_spec.rb +12 -0
  43. data/spec/happymapper/element_spec.rb +9 -0
  44. data/spec/happymapper/item_spec.rb +115 -0
  45. data/spec/happymapper/text_node_spec.rb +9 -0
  46. data/spec/happymapper_parse_spec.rb +113 -0
  47. data/spec/happymapper_spec.rb +1116 -0
  48. data/spec/has_many_empty_array_spec.rb +43 -0
  49. data/spec/ignay_spec.rb +95 -0
  50. data/spec/inheritance_spec.rb +107 -0
  51. data/spec/mixed_namespaces_spec.rb +61 -0
  52. data/spec/parse_with_object_to_update_spec.rb +111 -0
  53. data/spec/spec_helper.rb +7 -0
  54. data/spec/to_xml_spec.rb +200 -0
  55. data/spec/to_xml_with_namespaces_spec.rb +231 -0
  56. data/spec/wilcard_tag_name_spec.rb +96 -0
  57. data/spec/wrap_spec.rb +82 -0
  58. data/spec/xpath_spec.rb +89 -0
  59. metadata +182 -0
@@ -0,0 +1,114 @@
1
+ module HappyMapper
2
+ module AnonymousMapper
3
+
4
+ def parse(xml_content)
5
+
6
+ # TODO: this should be able to handle all the types of functionality that parse is able
7
+ # to handle which includes the text, xml document, node, fragment, etc.
8
+ xml = Nokogiri::XML(xml_content)
9
+
10
+ happymapper_class = create_happymapper_class_with_element(xml.root)
11
+
12
+ # With all the elements and attributes defined on the class it is time
13
+ # for the class to actually use the normal HappyMapper powers to parse
14
+ # the content. At this point this code is utilizing all of the existing
15
+ # code implemented for parsing.
16
+ happymapper_class.parse(xml_content, :single => true)
17
+
18
+ end
19
+
20
+ private
21
+
22
+ #
23
+ # Borrowed from Active Support to convert unruly element names into a format
24
+ # known and loved by Rubyists.
25
+ #
26
+ def underscore(camel_cased_word)
27
+ word = camel_cased_word.to_s.dup
28
+ word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2')
29
+ word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
30
+ word.tr!("-", "_")
31
+ word.downcase!
32
+ word
33
+ end
34
+
35
+ #
36
+ # Used internally when parsing to create a class that is capable of
37
+ # parsing the content. The name of the class is of course not likely
38
+ # going to match the content it will be able to parse so the tag
39
+ # value is set to the one provided.
40
+ #
41
+ def create_happymapper_class_with_tag(tag_name)
42
+ happymapper_class = Class.new
43
+ happymapper_class.class_eval do
44
+ include HappyMapper
45
+ tag tag_name
46
+ end
47
+ happymapper_class
48
+ end
49
+
50
+ #
51
+ # Used internally to create and define the necessary happymapper
52
+ # elements.
53
+ #
54
+ def create_happymapper_class_with_element(element)
55
+ happymapper_class = create_happymapper_class_with_tag(element.name)
56
+
57
+ happymapper_class.namespace element.namespace.prefix if element.namespace
58
+
59
+ element.namespaces.each do |prefix,namespace|
60
+ happymapper_class.register_namespace prefix, namespace
61
+ end
62
+
63
+ element.attributes.each do |name,attribute|
64
+ define_attribute_on_class(happymapper_class,attribute)
65
+ end
66
+
67
+ element.children.each do |element|
68
+ define_element_on_class(happymapper_class,element)
69
+ end
70
+
71
+ happymapper_class
72
+ end
73
+
74
+
75
+ #
76
+ # Define a HappyMapper element on the provided class based on
77
+ # the element provided.
78
+ #
79
+ def define_element_on_class(class_instance,element)
80
+
81
+ # When a text element has been provided create the necessary
82
+ # HappyMapper content attribute if the text happens to content
83
+ # some content.
84
+
85
+ if element.text? and element.content.strip != ""
86
+ class_instance.content :content, String
87
+ end
88
+
89
+ # When the element has children elements, that are not text
90
+ # elements, then we want to recursively define a new HappyMapper
91
+ # class that will have elements and attributes.
92
+
93
+ element_type = if !element.elements.reject {|e| e.text? }.empty? or !element.attributes.empty?
94
+ create_happymapper_class_with_element(element)
95
+ else
96
+ String
97
+ end
98
+
99
+ method = class_instance.elements.find {|e| e.name == element.name } ? :has_many : :has_one
100
+
101
+ class_instance.send(method,underscore(element.name),element_type)
102
+ end
103
+
104
+ #
105
+ # Define a HappyMapper attribute on the provided class based on
106
+ # the attribute provided.
107
+ #
108
+ def define_attribute_on_class(class_instance,attribute)
109
+ class_instance.attribute underscore(attribute.name), String
110
+ end
111
+
112
+ end
113
+
114
+ end
@@ -0,0 +1,21 @@
1
+ module HappyMapper
2
+ class Attribute < Item
3
+ attr_accessor :default
4
+
5
+ # @see Item#initialize
6
+ # Additional options:
7
+ # :default => Object The default value for this
8
+ def initialize(name, type, o={})
9
+ super
10
+ self.default = o[:default]
11
+ end
12
+
13
+ def find(node, namespace, xpath_options)
14
+ if options[:xpath]
15
+ yield(node.xpath(options[:xpath],xpath_options))
16
+ else
17
+ yield(node[tag])
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,55 @@
1
+ module HappyMapper
2
+ class Element < Item
3
+
4
+ def find(node, namespace, xpath_options)
5
+ if self.namespace
6
+ # from the class definition
7
+ namespace = self.namespace
8
+ elsif options[:namespace]
9
+ namespace = options[:namespace]
10
+ end
11
+
12
+ if options[:single]
13
+ if options[:xpath]
14
+ result = node.xpath(options[:xpath], xpath_options)
15
+ else
16
+ result = node.xpath(xpath(namespace), xpath_options)
17
+ end
18
+
19
+ if result
20
+ value = yield(result.first)
21
+ handle_attributes_option(result, value, xpath_options)
22
+ value
23
+ end
24
+ else
25
+ target_path = options[:xpath] ? options[:xpath] : xpath(namespace)
26
+ node.xpath(target_path, xpath_options).collect do |result|
27
+ value = yield(result)
28
+ handle_attributes_option(result, value, xpath_options)
29
+ value
30
+ end
31
+ end
32
+ end
33
+
34
+ def handle_attributes_option(result, value, xpath_options)
35
+ if options[:attributes].is_a?(Hash)
36
+ result = result.first unless result.respond_to?(:attribute_nodes)
37
+
38
+ return unless result.respond_to?(:attribute_nodes)
39
+
40
+ result.attribute_nodes.each do |xml_attribute|
41
+ if attribute_options = options[:attributes][xml_attribute.name.to_sym]
42
+ attribute_value = Attribute.new(xml_attribute.name.to_sym, *attribute_options).from_xml_node(result, namespace, xpath_options)
43
+
44
+ result.instance_eval <<-EOV
45
+ def value.#{xml_attribute.name.gsub(/\-/, '_')}
46
+ #{attribute_value.inspect}
47
+ end
48
+ EOV
49
+ end # if attributes_options
50
+ end # attribute_nodes.each
51
+ end # if options[:attributes]
52
+ end # def handle...
53
+
54
+ end
55
+ end
@@ -0,0 +1,160 @@
1
+ module HappyMapper
2
+ class Item
3
+ attr_accessor :name, :type, :tag, :options, :namespace
4
+
5
+ # options:
6
+ # :deep => Boolean False to only parse element's children, True to include
7
+ # grandchildren and all others down the chain (// in xpath)
8
+ # :namespace => String Element's namespace if it's not the global or inherited
9
+ # default
10
+ # :parser => Symbol Class method to use for type coercion.
11
+ # :raw => Boolean Use raw node value (inc. tags) when parsing.
12
+ # :single => Boolean False if object should be collection, True for single object
13
+ # :tag => String Element name if it doesn't match the specified name.
14
+ def initialize(name, type, o={})
15
+ self.name = name.to_s
16
+ self.type = type
17
+ #self.tag = o.delete(:tag) || name.to_s
18
+ self.tag = o[:tag] || name.to_s
19
+ self.options = { :single => true }.merge(o.merge(:name => self.name))
20
+
21
+ @xml_type = self.class.to_s.split('::').last.downcase
22
+ end
23
+
24
+ def constant
25
+ @constant ||= constantize(type)
26
+ end
27
+
28
+ #
29
+ # @param [XMLNode] node the xml node that is being parsed
30
+ # @param [String] namespace the name of the namespace
31
+ # @param [Hash] xpath_options additional xpath options
32
+ #
33
+ def from_xml_node(node, namespace, xpath_options)
34
+
35
+ namespace = options[:namespace] if options.key?(:namespace)
36
+
37
+ if suported_type_registered?
38
+ find(node, namespace, xpath_options) { |n| process_node_as_supported_type(n) }
39
+ elsif constant == XmlContent
40
+ find(node, namespace, xpath_options) { |n| process_node_as_xml_content(n) }
41
+ elsif custom_parser_defined?
42
+ find(node, namespace, xpath_options) { |n| process_node_with_custom_parser(n) }
43
+ else
44
+ process_node_with_default_parser(node,:namespaces => xpath_options)
45
+ end
46
+
47
+ end
48
+
49
+ def xpath(namespace = self.namespace)
50
+ xpath = ''
51
+ xpath += './/' if options[:deep]
52
+ xpath += "#{namespace}:" if namespace
53
+ xpath += tag
54
+ #puts "xpath: #{xpath}"
55
+ xpath
56
+ end
57
+
58
+ def method_name
59
+ @method_name ||= name.tr('-', '_')
60
+ end
61
+
62
+ #
63
+ # Convert the value into the correct type.
64
+ #
65
+ # @param [String] value the string value parsed from the XML value that will
66
+ # be converted to the particular primitive type.
67
+ #
68
+ # @return [String,Float,Time,Date,DateTime,Boolean,Integer] the converted value
69
+ # to the new type.
70
+ #
71
+ def typecast(value)
72
+ typecaster(value).apply(value)
73
+ end
74
+
75
+
76
+ private
77
+
78
+ # @return [Boolean] true if the type defined for the item is defined in the
79
+ # list of support types.
80
+ def suported_type_registered?
81
+ SupportedTypes.types.map {|caster| caster.type }.include?(constant)
82
+ end
83
+
84
+ # @return [#apply] the typecaster object that will be able to convert
85
+ # the value into a value with the correct type.
86
+ def typecaster(value)
87
+ SupportedTypes.types.find { |caster| caster.apply?(value,constant) }
88
+ end
89
+
90
+ #
91
+ # Processes a Nokogiri::XML::Node as a supported type
92
+ #
93
+ def process_node_as_supported_type(node)
94
+ content = node.respond_to?(:content) ? node.content : node
95
+ typecast(content)
96
+ end
97
+
98
+ #
99
+ # Process a Nokogiri::XML::Node as XML Content
100
+ #
101
+ def process_node_as_xml_content(node)
102
+ node = node.children if node.respond_to?(:children)
103
+ node.respond_to?(:to_xml) ? node.to_xml : node.to_s
104
+ end
105
+
106
+ #
107
+ # A custom parser is a custom parse method on the class. When the parser
108
+ # option has been set this value is the name of the method which will be
109
+ # used to parse the node content.
110
+ #
111
+ def custom_parser_defined?
112
+ options[:parser]
113
+ end
114
+
115
+ def process_node_with_custom_parser(node)
116
+ if node.respond_to?(:content) && !options[:raw]
117
+ value = node.content
118
+ else
119
+ value = node.to_s
120
+ end
121
+
122
+ begin
123
+ constant.send(options[:parser].to_sym, value)
124
+ rescue
125
+ nil
126
+ end
127
+ end
128
+
129
+ def process_node_with_default_parser(node,parse_options)
130
+ constant.parse(node,options.merge(parse_options))
131
+ end
132
+
133
+ #
134
+ # Convert any String defined types into their constant version so that
135
+ # the method #parse or the custom defined parser method would be used.
136
+ #
137
+ # @param [String,Constant] type is the name of the class or the constant
138
+ # for the class.
139
+ # @return [Constant] the constant of the type
140
+ #
141
+ def constantize(type)
142
+ type.is_a?(String) ? convert_string_to_constant(type) : type
143
+ end
144
+
145
+ def convert_string_to_constant(type)
146
+ names = type.split('::')
147
+ constant = Object
148
+ names.each do |name|
149
+ constant =
150
+ if constant.const_defined?(name)
151
+ constant.const_get(name)
152
+ else
153
+ constant.const_missing(name)
154
+ end
155
+ end
156
+ constant
157
+ end
158
+
159
+ end
160
+ end
@@ -0,0 +1,140 @@
1
+ module HappyMapper
2
+ module SupportedTypes
3
+ extend self
4
+
5
+ #
6
+ # All of the registerd supported types that can be parsed.
7
+ #
8
+ # All types defined here are set through #register.
9
+ #
10
+ def types
11
+ @types ||= []
12
+ end
13
+
14
+ #
15
+ # Add a new converter to the list of supported types. A converter
16
+ # is an object that adheres to the protocol which is defined with two
17
+ # methods #apply?(value,convert_to_type) and #apply(value).
18
+ #
19
+ # @example Defining a class that would process `nil` or values that have
20
+ # already been converted.
21
+ #
22
+ # class NilOrAlreadyConverted
23
+ # def apply?(value,convert_to_type)
24
+ # value.kind_of?(convert_to_type) || value.nil?
25
+ # end
26
+ #
27
+ # def apply(value)
28
+ # value
29
+ # end
30
+ # end
31
+ #
32
+ #
33
+ def register(type_converter)
34
+ types.push type_converter
35
+ end
36
+
37
+ #
38
+ # An additional shortcut registration method that assumes that you want
39
+ # to perform a conversion on a specific type. A block is provided which
40
+ # is the operation to perform when #apply(value) has been called.
41
+ #
42
+ # @example Registering a DateTime parser
43
+ #
44
+ # HappyMapper::SupportedTypes.register_type DateTime do |value|
45
+ # DateTime.parse(value,to_s)
46
+ # end
47
+ #
48
+ def register_type(type,&block)
49
+ register CastWhenType.new(type,&block)
50
+ end
51
+
52
+ #
53
+ # Many of the conversions are based on type. When the type specified
54
+ # matches then perform the action specified in the specified block.
55
+ # If no block is provided the value is simply returned.
56
+ #
57
+ class CastWhenType
58
+ attr_reader :type
59
+
60
+ def initialize(type,&block)
61
+ @type = type
62
+ @apply_block = block || no_operation
63
+ end
64
+
65
+ def no_operation
66
+ lambda {|value| value }
67
+ end
68
+
69
+ def apply?(value,convert_to_type)
70
+ convert_to_type == type
71
+ end
72
+
73
+ def apply(value)
74
+ @apply_block.call(value)
75
+ end
76
+ end
77
+
78
+ #
79
+ # For the cases when the value is nil or is already the
80
+ # intended type then no work needs to be done and the
81
+ # value simply can be returned.
82
+ #
83
+ class NilOrAlreadyConverted
84
+
85
+ def type
86
+ NilClass
87
+ end
88
+
89
+ def apply?(value,convert_to_type)
90
+ value.kind_of?(convert_to_type) || value.nil?
91
+ end
92
+
93
+ def apply(value)
94
+ value
95
+ end
96
+ end
97
+
98
+ register NilOrAlreadyConverted.new
99
+
100
+ register_type String do |value|
101
+ value.to_s
102
+ end
103
+
104
+ register_type Float do |value|
105
+ value.to_f
106
+ end
107
+
108
+ register_type Time do |value|
109
+ Time.parse(value.to_s) rescue Time.at(value.to_i)
110
+ end
111
+
112
+ register_type DateTime do |value|
113
+ DateTime.parse(value.to_s)
114
+ end
115
+
116
+ register_type Date do |value|
117
+ Date.parse(value.to_s)
118
+ end
119
+
120
+ register_type Boolean do |value|
121
+ ['true', 't', '1'].include?(value.to_s.downcase)
122
+ end
123
+
124
+ register_type Integer do |value|
125
+ value_to_i = value.to_i
126
+ if value_to_i == 0 && value != '0'
127
+ value_to_s = value.to_s
128
+ begin
129
+ Integer(value_to_s =~ /^(\d+)/ ? $1 : value_to_s)
130
+ rescue ArgumentError
131
+ nil
132
+ end
133
+ else
134
+ value_to_i
135
+ end
136
+ end
137
+
138
+ end
139
+
140
+ end