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