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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +35 -0
- data/README.md +605 -0
- data/lib/happymapper.rb +776 -0
- data/lib/happymapper/anonymous_mapper.rb +114 -0
- data/lib/happymapper/attribute.rb +21 -0
- data/lib/happymapper/element.rb +55 -0
- data/lib/happymapper/item.rb +160 -0
- data/lib/happymapper/supported_types.rb +140 -0
- data/lib/happymapper/text_node.rb +8 -0
- data/lib/happymapper/version.rb +3 -0
- data/lib/xmlmapper.rb +1 -0
- data/spec/attribute_default_value_spec.rb +50 -0
- data/spec/attributes_spec.rb +36 -0
- data/spec/fixtures/address.xml +9 -0
- data/spec/fixtures/ambigous_items.xml +22 -0
- data/spec/fixtures/analytics.xml +61 -0
- data/spec/fixtures/analytics_profile.xml +127 -0
- data/spec/fixtures/atom.xml +19 -0
- data/spec/fixtures/commit.xml +52 -0
- data/spec/fixtures/current_weather.xml +89 -0
- data/spec/fixtures/current_weather_missing_elements.xml +18 -0
- data/spec/fixtures/default_namespace_combi.xml +6 -0
- data/spec/fixtures/dictionary.xml +20 -0
- data/spec/fixtures/family_tree.xml +21 -0
- data/spec/fixtures/inagy.xml +85 -0
- data/spec/fixtures/lastfm.xml +355 -0
- data/spec/fixtures/multiple_namespaces.xml +170 -0
- data/spec/fixtures/multiple_primitives.xml +5 -0
- data/spec/fixtures/optional_attributes.xml +6 -0
- data/spec/fixtures/pita.xml +133 -0
- data/spec/fixtures/posts.xml +23 -0
- data/spec/fixtures/product_default_namespace.xml +18 -0
- data/spec/fixtures/product_no_namespace.xml +10 -0
- data/spec/fixtures/product_single_namespace.xml +10 -0
- data/spec/fixtures/quarters.xml +19 -0
- data/spec/fixtures/radar.xml +21 -0
- data/spec/fixtures/set_config_options.xml +3 -0
- data/spec/fixtures/statuses.xml +422 -0
- data/spec/fixtures/subclass_namespace.xml +50 -0
- data/spec/fixtures/wrapper.xml +11 -0
- data/spec/happymapper/attribute_spec.rb +12 -0
- data/spec/happymapper/element_spec.rb +9 -0
- data/spec/happymapper/item_spec.rb +115 -0
- data/spec/happymapper/text_node_spec.rb +9 -0
- data/spec/happymapper_parse_spec.rb +113 -0
- data/spec/happymapper_spec.rb +1116 -0
- data/spec/has_many_empty_array_spec.rb +43 -0
- data/spec/ignay_spec.rb +95 -0
- data/spec/inheritance_spec.rb +107 -0
- data/spec/mixed_namespaces_spec.rb +61 -0
- data/spec/parse_with_object_to_update_spec.rb +111 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/to_xml_spec.rb +200 -0
- data/spec/to_xml_with_namespaces_spec.rb +231 -0
- data/spec/wilcard_tag_name_spec.rb +96 -0
- data/spec/wrap_spec.rb +82 -0
- data/spec/xpath_spec.rb +89 -0
- 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
|