blockscore-happymapper 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +39 -0
- data/README.md +605 -0
- data/lib/happymapper.rb +754 -0
- data/lib/happymapper/anonymous_mapper.rb +107 -0
- data/lib/happymapper/attribute.rb +21 -0
- data/lib/happymapper/element.rb +53 -0
- data/lib/happymapper/item.rb +156 -0
- data/lib/happymapper/supported_types.rb +137 -0
- data/lib/happymapper/text_node.rb +7 -0
- data/lib/happymapper/version.rb +3 -0
- data/spec/attribute_default_value_spec.rb +47 -0
- data/spec/attributes_spec.rb +33 -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 +10 -0
- data/spec/happymapper/element_spec.rb +9 -0
- data/spec/happymapper/item_spec.rb +114 -0
- data/spec/happymapper/text_node_spec.rb +9 -0
- data/spec/happymapper_parse_spec.rb +99 -0
- data/spec/happymapper_spec.rb +1096 -0
- data/spec/has_many_empty_array_spec.rb +42 -0
- data/spec/ignay_spec.rb +87 -0
- data/spec/inheritance_spec.rb +105 -0
- data/spec/mixed_namespaces_spec.rb +58 -0
- data/spec/parse_with_object_to_update_spec.rb +108 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/to_xml_spec.rb +197 -0
- data/spec/to_xml_with_namespaces_spec.rb +223 -0
- data/spec/wilcard_tag_name_spec.rb +94 -0
- data/spec/wrap_spec.rb +80 -0
- data/spec/xpath_spec.rb +86 -0
- metadata +183 -0
@@ -0,0 +1,107 @@
|
|
1
|
+
module HappyMapper
|
2
|
+
module AnonymousMapper
|
3
|
+
def parse(xml_content)
|
4
|
+
# TODO: this should be able to handle all the types of functionality that parse is able
|
5
|
+
# to handle which includes the text, xml document, node, fragment, etc.
|
6
|
+
xml = Nokogiri::XML(xml_content)
|
7
|
+
|
8
|
+
happymapper_class = create_happymapper_class_with_element(xml.root)
|
9
|
+
|
10
|
+
# With all the elements and attributes defined on the class it is time
|
11
|
+
# for the class to actually use the normal HappyMapper powers to parse
|
12
|
+
# the content. At this point this code is utilizing all of the existing
|
13
|
+
# code implemented for parsing.
|
14
|
+
happymapper_class.parse(xml_content, single: true)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
#
|
20
|
+
# Borrowed from Active Support to convert unruly element names into a format
|
21
|
+
# known and loved by Rubyists.
|
22
|
+
#
|
23
|
+
def underscore(camel_cased_word)
|
24
|
+
word = camel_cased_word.to_s.dup
|
25
|
+
word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
|
26
|
+
word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
|
27
|
+
word.tr!('-', '_')
|
28
|
+
word.downcase!
|
29
|
+
word
|
30
|
+
end
|
31
|
+
|
32
|
+
#
|
33
|
+
# Used internally when parsing to create a class that is capable of
|
34
|
+
# parsing the content. The name of the class is of course not likely
|
35
|
+
# going to match the content it will be able to parse so the tag
|
36
|
+
# value is set to the one provided.
|
37
|
+
#
|
38
|
+
def create_happymapper_class_with_tag(tag_name)
|
39
|
+
happymapper_class = Class.new
|
40
|
+
happymapper_class.class_eval do
|
41
|
+
include HappyMapper
|
42
|
+
tag tag_name
|
43
|
+
end
|
44
|
+
happymapper_class
|
45
|
+
end
|
46
|
+
|
47
|
+
#
|
48
|
+
# Used internally to create and define the necessary happymapper
|
49
|
+
# elements.
|
50
|
+
#
|
51
|
+
def create_happymapper_class_with_element(element)
|
52
|
+
happymapper_class = create_happymapper_class_with_tag(element.name)
|
53
|
+
|
54
|
+
happymapper_class.namespace element.namespace.prefix if element.namespace
|
55
|
+
|
56
|
+
element.namespaces.each do |prefix, namespace|
|
57
|
+
happymapper_class.register_namespace prefix, namespace
|
58
|
+
end
|
59
|
+
|
60
|
+
element.attributes.each do |_name, attribute|
|
61
|
+
define_attribute_on_class(happymapper_class, attribute)
|
62
|
+
end
|
63
|
+
|
64
|
+
element.children.each do |element|
|
65
|
+
define_element_on_class(happymapper_class, element)
|
66
|
+
end
|
67
|
+
|
68
|
+
happymapper_class
|
69
|
+
end
|
70
|
+
|
71
|
+
#
|
72
|
+
# Define a HappyMapper element on the provided class based on
|
73
|
+
# the element provided.
|
74
|
+
#
|
75
|
+
def define_element_on_class(class_instance, element)
|
76
|
+
# When a text element has been provided create the necessary
|
77
|
+
# HappyMapper content attribute if the text happens to content
|
78
|
+
# some content.
|
79
|
+
|
80
|
+
if element.text? && element.content.strip != ''
|
81
|
+
class_instance.content :content, String
|
82
|
+
end
|
83
|
+
|
84
|
+
# When the element has children elements, that are not text
|
85
|
+
# elements, then we want to recursively define a new HappyMapper
|
86
|
+
# class that will have elements and attributes.
|
87
|
+
|
88
|
+
element_type = if !element.elements.reject(&:text?).empty? || !element.attributes.empty?
|
89
|
+
create_happymapper_class_with_element(element)
|
90
|
+
else
|
91
|
+
String
|
92
|
+
end
|
93
|
+
|
94
|
+
method = class_instance.elements.find { |e| e.name == element.name } ? :has_many : :has_one
|
95
|
+
|
96
|
+
class_instance.send(method, underscore(element.name), element_type)
|
97
|
+
end
|
98
|
+
|
99
|
+
#
|
100
|
+
# Define a HappyMapper attribute on the provided class based on
|
101
|
+
# the attribute provided.
|
102
|
+
#
|
103
|
+
def define_attribute_on_class(class_instance, attribute)
|
104
|
+
class_instance.attribute underscore(attribute.name), String
|
105
|
+
end
|
106
|
+
end
|
107
|
+
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,53 @@
|
|
1
|
+
module HappyMapper
|
2
|
+
class Element < Item
|
3
|
+
def find(node, namespace, xpath_options)
|
4
|
+
if self.namespace
|
5
|
+
# from the class definition
|
6
|
+
namespace = self.namespace
|
7
|
+
elsif options[:namespace]
|
8
|
+
namespace = options[:namespace]
|
9
|
+
end
|
10
|
+
|
11
|
+
if options[:single]
|
12
|
+
if options[:xpath]
|
13
|
+
result = node.xpath(options[:xpath], xpath_options)
|
14
|
+
else
|
15
|
+
result = node.xpath(xpath(namespace), xpath_options)
|
16
|
+
end
|
17
|
+
|
18
|
+
if result
|
19
|
+
value = yield(result.first)
|
20
|
+
handle_attributes_option(result, value, xpath_options)
|
21
|
+
value
|
22
|
+
end
|
23
|
+
else
|
24
|
+
target_path = options[:xpath] ? options[:xpath] : xpath(namespace)
|
25
|
+
node.xpath(target_path, xpath_options).collect do |result|
|
26
|
+
value = yield(result)
|
27
|
+
handle_attributes_option(result, value, xpath_options)
|
28
|
+
value
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def handle_attributes_option(result, value, xpath_options)
|
34
|
+
if options[:attributes].is_a?(Hash)
|
35
|
+
result = result.first unless result.respond_to?(:attribute_nodes)
|
36
|
+
|
37
|
+
return unless result.respond_to?(:attribute_nodes)
|
38
|
+
|
39
|
+
result.attribute_nodes.each do |xml_attribute|
|
40
|
+
if attribute_options = options[:attributes][xml_attribute.name.to_sym]
|
41
|
+
attribute_value = Attribute.new(xml_attribute.name.to_sym, *attribute_options).from_xml_node(result, namespace, xpath_options)
|
42
|
+
|
43
|
+
result.instance_eval <<-EOV
|
44
|
+
def value.#{xml_attribute.name.gsub(/\-/, '_')}
|
45
|
+
#{attribute_value.inspect}
|
46
|
+
end
|
47
|
+
EOV
|
48
|
+
end # if attributes_options
|
49
|
+
end # attribute_nodes.each
|
50
|
+
end # if options[:attributes]
|
51
|
+
end # def handle...
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,156 @@
|
|
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
|
+
namespace = options[:namespace] if options.key?(:namespace)
|
35
|
+
|
36
|
+
if suported_type_registered?
|
37
|
+
find(node, namespace, xpath_options) { |n| process_node_as_supported_type(n) }
|
38
|
+
elsif constant == XmlContent
|
39
|
+
find(node, namespace, xpath_options) { |n| process_node_as_xml_content(n) }
|
40
|
+
elsif custom_parser_defined?
|
41
|
+
find(node, namespace, xpath_options) { |n| process_node_with_custom_parser(n) }
|
42
|
+
else
|
43
|
+
process_node_with_default_parser(node, namespaces: xpath_options)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def xpath(namespace = self.namespace)
|
48
|
+
xpath = ''
|
49
|
+
xpath += './/' if options[:deep]
|
50
|
+
xpath += "#{namespace}:" if namespace
|
51
|
+
xpath += tag
|
52
|
+
# puts "xpath: #{xpath}"
|
53
|
+
xpath
|
54
|
+
end
|
55
|
+
|
56
|
+
def method_name
|
57
|
+
@method_name ||= name.tr('-', '_')
|
58
|
+
end
|
59
|
+
|
60
|
+
#
|
61
|
+
# Convert the value into the correct type.
|
62
|
+
#
|
63
|
+
# @param [String] value the string value parsed from the XML value that will
|
64
|
+
# be converted to the particular primitive type.
|
65
|
+
#
|
66
|
+
# @return [String,Float,Time,Date,DateTime,Boolean,Integer] the converted value
|
67
|
+
# to the new type.
|
68
|
+
#
|
69
|
+
def typecast(value)
|
70
|
+
typecaster(value).apply(value)
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
# @return [Boolean] true if the type defined for the item is defined in the
|
76
|
+
# list of support types.
|
77
|
+
def suported_type_registered?
|
78
|
+
SupportedTypes.types.map(&:type).include?(constant)
|
79
|
+
end
|
80
|
+
|
81
|
+
# @return [#apply] the typecaster object that will be able to convert
|
82
|
+
# the value into a value with the correct type.
|
83
|
+
def typecaster(value)
|
84
|
+
SupportedTypes.types.find { |caster| caster.apply?(value, constant) }
|
85
|
+
end
|
86
|
+
|
87
|
+
#
|
88
|
+
# Processes a Nokogiri::XML::Node as a supported type
|
89
|
+
#
|
90
|
+
def process_node_as_supported_type(node)
|
91
|
+
content = node.respond_to?(:content) ? node.content : node
|
92
|
+
typecast(content)
|
93
|
+
end
|
94
|
+
|
95
|
+
#
|
96
|
+
# Process a Nokogiri::XML::Node as XML Content
|
97
|
+
#
|
98
|
+
def process_node_as_xml_content(node)
|
99
|
+
node = node.children if node.respond_to?(:children)
|
100
|
+
node.respond_to?(:to_xml) ? node.to_xml : node.to_s
|
101
|
+
end
|
102
|
+
|
103
|
+
#
|
104
|
+
# A custom parser is a custom parse method on the class. When the parser
|
105
|
+
# option has been set this value is the name of the method which will be
|
106
|
+
# used to parse the node content.
|
107
|
+
#
|
108
|
+
def custom_parser_defined?
|
109
|
+
options[:parser]
|
110
|
+
end
|
111
|
+
|
112
|
+
def process_node_with_custom_parser(node)
|
113
|
+
if node.respond_to?(:content) && !options[:raw]
|
114
|
+
value = node.content
|
115
|
+
else
|
116
|
+
value = node.to_s
|
117
|
+
end
|
118
|
+
|
119
|
+
begin
|
120
|
+
constant.send(options[:parser].to_sym, value)
|
121
|
+
rescue
|
122
|
+
nil
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def process_node_with_default_parser(node, parse_options)
|
127
|
+
constant.parse(node, options.merge(parse_options))
|
128
|
+
end
|
129
|
+
|
130
|
+
#
|
131
|
+
# Convert any String defined types into their constant version so that
|
132
|
+
# the method #parse or the custom defined parser method would be used.
|
133
|
+
#
|
134
|
+
# @param [String,Constant] type is the name of the class or the constant
|
135
|
+
# for the class.
|
136
|
+
# @return [Constant] the constant of the type
|
137
|
+
#
|
138
|
+
def constantize(type)
|
139
|
+
type.is_a?(String) ? convert_string_to_constant(type) : type
|
140
|
+
end
|
141
|
+
|
142
|
+
def convert_string_to_constant(type)
|
143
|
+
names = type.split('::')
|
144
|
+
constant = Object
|
145
|
+
names.each do |name|
|
146
|
+
constant =
|
147
|
+
if constant.const_defined?(name)
|
148
|
+
constant.const_get(name)
|
149
|
+
else
|
150
|
+
constant.const_missing(name)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
constant
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,137 @@
|
|
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
|
+
->(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
|
+
def type
|
85
|
+
NilClass
|
86
|
+
end
|
87
|
+
|
88
|
+
def apply?(value, convert_to_type)
|
89
|
+
value.is_a?(convert_to_type) || value.nil?
|
90
|
+
end
|
91
|
+
|
92
|
+
def apply(value)
|
93
|
+
value
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
register NilOrAlreadyConverted.new
|
98
|
+
|
99
|
+
register_type String, &:to_s
|
100
|
+
|
101
|
+
register_type Float, &:to_f
|
102
|
+
|
103
|
+
register_type Time do |value|
|
104
|
+
begin
|
105
|
+
Time.parse(value.to_s)
|
106
|
+
rescue
|
107
|
+
Time.at(value.to_i)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
register_type DateTime do |value|
|
112
|
+
DateTime.parse(value.to_s)
|
113
|
+
end
|
114
|
+
|
115
|
+
register_type Date do |value|
|
116
|
+
Date.parse(value.to_s)
|
117
|
+
end
|
118
|
+
|
119
|
+
register_type Boolean do |value|
|
120
|
+
%w(true t 1).include?(value.to_s.downcase)
|
121
|
+
end
|
122
|
+
|
123
|
+
register_type Integer do |value|
|
124
|
+
value_to_i = value.to_i
|
125
|
+
if value_to_i == 0 && value != '0'
|
126
|
+
value_to_s = value.to_s
|
127
|
+
begin
|
128
|
+
Integer(value_to_s =~ /^(\d+)/ ? Regexp.last_match(1) : value_to_s)
|
129
|
+
rescue ArgumentError
|
130
|
+
nil
|
131
|
+
end
|
132
|
+
else
|
133
|
+
value_to_i
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|