nokogiri-happymapper 0.6.0 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/CHANGELOG.md +26 -1
- data/README.md +204 -117
- data/lib/happymapper.rb +318 -343
- data/lib/happymapper/anonymous_mapper.rb +27 -29
- data/lib/happymapper/attribute.rb +7 -5
- data/lib/happymapper/element.rb +19 -24
- data/lib/happymapper/item.rb +18 -20
- data/lib/happymapper/supported_types.rb +20 -19
- data/lib/happymapper/text_node.rb +4 -3
- data/lib/happymapper/version.rb +3 -1
- data/spec/attribute_default_value_spec.rb +14 -15
- data/spec/attributes_spec.rb +14 -15
- data/spec/happymapper/attribute_spec.rb +4 -4
- data/spec/happymapper/element_spec.rb +3 -1
- data/spec/happymapper/item_spec.rb +49 -41
- data/spec/happymapper/text_node_spec.rb +3 -1
- data/spec/happymapper_parse_spec.rb +62 -44
- data/spec/happymapper_spec.rb +270 -263
- data/spec/has_many_empty_array_spec.rb +8 -7
- data/spec/ignay_spec.rb +27 -31
- data/spec/inheritance_spec.rb +30 -24
- data/spec/mixed_namespaces_spec.rb +14 -15
- data/spec/parse_with_object_to_update_spec.rb +37 -38
- data/spec/spec_helper.rb +18 -0
- data/spec/to_xml_spec.rb +64 -63
- data/spec/to_xml_with_namespaces_spec.rb +66 -64
- data/spec/wilcard_tag_name_spec.rb +25 -21
- data/spec/wrap_spec.rb +11 -11
- data/spec/xpath_spec.rb +33 -32
- metadata +33 -5
@@ -1,8 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module HappyMapper
|
2
4
|
module AnonymousMapper
|
3
|
-
|
4
5
|
def parse(xml_content)
|
5
|
-
|
6
6
|
# TODO: this should be able to handle all the types of functionality that parse is able
|
7
7
|
# to handle which includes the text, xml document, node, fragment, etc.
|
8
8
|
xml = Nokogiri::XML(xml_content)
|
@@ -13,8 +13,7 @@ module HappyMapper
|
|
13
13
|
# for the class to actually use the normal HappyMapper powers to parse
|
14
14
|
# the content. At this point this code is utilizing all of the existing
|
15
15
|
# code implemented for parsing.
|
16
|
-
happymapper_class.parse(xml_content, :
|
17
|
-
|
16
|
+
happymapper_class.parse(xml_content, single: true)
|
18
17
|
end
|
19
18
|
|
20
19
|
private
|
@@ -25,9 +24,9 @@ module HappyMapper
|
|
25
24
|
#
|
26
25
|
def underscore(camel_cased_word)
|
27
26
|
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!(
|
27
|
+
word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
|
28
|
+
word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
|
29
|
+
word.tr!('-', '_')
|
31
30
|
word.downcase!
|
32
31
|
word
|
33
32
|
end
|
@@ -56,59 +55,58 @@ module HappyMapper
|
|
56
55
|
|
57
56
|
happymapper_class.namespace element.namespace.prefix if element.namespace
|
58
57
|
|
59
|
-
element.namespaces.each do |prefix,namespace|
|
58
|
+
element.namespaces.each do |prefix, namespace|
|
60
59
|
happymapper_class.register_namespace prefix, namespace
|
61
60
|
end
|
62
61
|
|
63
|
-
element.attributes.
|
64
|
-
define_attribute_on_class(happymapper_class,attribute)
|
62
|
+
element.attributes.each_value do |attribute|
|
63
|
+
define_attribute_on_class(happymapper_class, attribute)
|
65
64
|
end
|
66
65
|
|
67
66
|
element.children.each do |child|
|
68
|
-
define_element_on_class(happymapper_class,child)
|
67
|
+
define_element_on_class(happymapper_class, child)
|
69
68
|
end
|
70
69
|
|
71
70
|
happymapper_class
|
72
71
|
end
|
73
72
|
|
74
|
-
|
75
73
|
#
|
76
74
|
# Define a HappyMapper element on the provided class based on
|
77
75
|
# the element provided.
|
78
76
|
#
|
79
|
-
def define_element_on_class(class_instance,element)
|
80
|
-
|
77
|
+
def define_element_on_class(class_instance, element)
|
81
78
|
# When a text element has been provided create the necessary
|
82
|
-
# HappyMapper content attribute if the text happens to
|
79
|
+
# HappyMapper content attribute if the text happens to contain
|
83
80
|
# some content.
|
84
81
|
|
85
|
-
if element.text?
|
86
|
-
class_instance.content :content, String
|
87
|
-
end
|
82
|
+
class_instance.content :content, String if element.text? && (element.content.strip != '')
|
88
83
|
|
89
84
|
# When the element has children elements, that are not text
|
90
85
|
# elements, then we want to recursively define a new HappyMapper
|
91
86
|
# class that will have elements and attributes.
|
92
87
|
|
93
|
-
element_type = if !element.elements.reject
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
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
|
98
95
|
|
99
|
-
|
96
|
+
options = {}
|
97
|
+
options[:tag] = element.name
|
98
|
+
namespace = element.namespace
|
99
|
+
options[:namespace] = namespace.prefix if namespace
|
100
100
|
|
101
|
-
class_instance.send(method,underscore(element.name),element_type)
|
101
|
+
class_instance.send(method, underscore(element.name), element_type, options)
|
102
102
|
end
|
103
103
|
|
104
104
|
#
|
105
105
|
# Define a HappyMapper attribute on the provided class based on
|
106
106
|
# the attribute provided.
|
107
107
|
#
|
108
|
-
def define_attribute_on_class(class_instance,attribute)
|
109
|
-
class_instance.attribute underscore(attribute.name), String
|
108
|
+
def define_attribute_on_class(class_instance, attribute)
|
109
|
+
class_instance.attribute underscore(attribute.name), String, tag: attribute.name
|
110
110
|
end
|
111
|
-
|
112
111
|
end
|
113
|
-
|
114
112
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module HappyMapper
|
2
4
|
class Attribute < Item
|
3
5
|
attr_accessor :default
|
@@ -5,16 +7,16 @@ module HappyMapper
|
|
5
7
|
# @see Item#initialize
|
6
8
|
# Additional options:
|
7
9
|
# :default => Object The default value for this
|
8
|
-
def initialize(name, type,
|
10
|
+
def initialize(name, type, options = {})
|
9
11
|
super
|
10
|
-
self.default =
|
12
|
+
self.default = options[:default]
|
11
13
|
end
|
12
14
|
|
13
|
-
def find(node,
|
15
|
+
def find(node, _namespace, xpath_options)
|
14
16
|
if options[:xpath]
|
15
|
-
yield(node.xpath(options[:xpath],xpath_options))
|
17
|
+
yield(node.xpath(options[:xpath], xpath_options))
|
16
18
|
else
|
17
|
-
yield(node[tag])
|
19
|
+
yield(node.attributes[tag])
|
18
20
|
end
|
19
21
|
end
|
20
22
|
end
|
data/lib/happymapper/element.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module HappyMapper
|
2
4
|
class Element < Item
|
3
|
-
|
4
5
|
def find(node, namespace, xpath_options)
|
5
6
|
if self.namespace
|
6
7
|
# from the class definition
|
@@ -10,11 +11,11 @@ module HappyMapper
|
|
10
11
|
end
|
11
12
|
|
12
13
|
if options[:single]
|
13
|
-
if options[:xpath]
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
14
|
+
result = if options[:xpath]
|
15
|
+
node.xpath(options[:xpath], xpath_options)
|
16
|
+
else
|
17
|
+
node.xpath(xpath(namespace), xpath_options)
|
18
|
+
end
|
18
19
|
|
19
20
|
if result
|
20
21
|
value = yield(result.first)
|
@@ -22,7 +23,7 @@ module HappyMapper
|
|
22
23
|
value
|
23
24
|
end
|
24
25
|
else
|
25
|
-
target_path = options[:xpath]
|
26
|
+
target_path = options[:xpath] || xpath(namespace)
|
26
27
|
node.xpath(target_path, xpath_options).collect do |item|
|
27
28
|
value = yield(item)
|
28
29
|
handle_attributes_option(item, value, xpath_options)
|
@@ -32,24 +33,18 @@ module HappyMapper
|
|
32
33
|
end
|
33
34
|
|
34
35
|
def handle_attributes_option(result, value, xpath_options)
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
return unless result.respond_to?(:attribute_nodes)
|
36
|
+
return unless options[:attributes].is_a?(Hash)
|
37
|
+
result = result.first unless result.respond_to?(:attribute_nodes)
|
38
|
+
return unless result.respond_to?(:attribute_nodes)
|
39
39
|
|
40
|
-
|
41
|
-
|
42
|
-
|
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...
|
40
|
+
result.attribute_nodes.each do |xml_attribute|
|
41
|
+
next unless (attribute_options = options[:attributes][xml_attribute.name.to_sym])
|
42
|
+
attribute_value = Attribute.new(xml_attribute.name.to_sym, *attribute_options).
|
43
|
+
from_xml_node(result, namespace, xpath_options)
|
53
44
|
|
45
|
+
method_name = xml_attribute.name.tr('-', '_')
|
46
|
+
value.define_singleton_method(method_name) { attribute_value }
|
47
|
+
end
|
48
|
+
end
|
54
49
|
end
|
55
50
|
end
|
data/lib/happymapper/item.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module HappyMapper
|
2
4
|
class Item
|
3
5
|
attr_accessor :name, :type, :tag, :options, :namespace
|
@@ -11,12 +13,12 @@ module HappyMapper
|
|
11
13
|
# :raw => Boolean Use raw node value (inc. tags) when parsing.
|
12
14
|
# :single => Boolean False if object should be collection, True for single object
|
13
15
|
# :tag => String Element name if it doesn't match the specified name.
|
14
|
-
def initialize(name, type,
|
16
|
+
def initialize(name, type, options = {})
|
15
17
|
self.name = name.to_s
|
16
18
|
self.type = type
|
17
|
-
#self.tag =
|
18
|
-
self.tag =
|
19
|
-
self.options = { :
|
19
|
+
# self.tag = options.delete(:tag) || name.to_s
|
20
|
+
self.tag = options[:tag] || name.to_s
|
21
|
+
self.options = { single: true }.merge(options.merge(name: self.name))
|
20
22
|
|
21
23
|
@xml_type = self.class.to_s.split('::').last.downcase
|
22
24
|
end
|
@@ -31,7 +33,6 @@ module HappyMapper
|
|
31
33
|
# @param [Hash] xpath_options additional xpath options
|
32
34
|
#
|
33
35
|
def from_xml_node(node, namespace, xpath_options)
|
34
|
-
|
35
36
|
namespace = options[:namespace] if options.key?(:namespace)
|
36
37
|
|
37
38
|
if suported_type_registered?
|
@@ -41,9 +42,8 @@ module HappyMapper
|
|
41
42
|
elsif custom_parser_defined?
|
42
43
|
find(node, namespace, xpath_options) { |n| process_node_with_custom_parser(n) }
|
43
44
|
else
|
44
|
-
process_node_with_default_parser(node
|
45
|
+
process_node_with_default_parser(node, namespaces: xpath_options)
|
45
46
|
end
|
46
|
-
|
47
47
|
end
|
48
48
|
|
49
49
|
def xpath(namespace = self.namespace)
|
@@ -51,7 +51,7 @@ module HappyMapper
|
|
51
51
|
xpath += './/' if options[:deep]
|
52
52
|
xpath += "#{namespace}:" if namespace
|
53
53
|
xpath += tag
|
54
|
-
#puts "xpath: #{xpath}"
|
54
|
+
# puts "xpath: #{xpath}"
|
55
55
|
xpath
|
56
56
|
end
|
57
57
|
|
@@ -72,19 +72,18 @@ module HappyMapper
|
|
72
72
|
typecaster(value).apply(value)
|
73
73
|
end
|
74
74
|
|
75
|
-
|
76
75
|
private
|
77
76
|
|
78
77
|
# @return [Boolean] true if the type defined for the item is defined in the
|
79
78
|
# list of support types.
|
80
79
|
def suported_type_registered?
|
81
|
-
SupportedTypes.types.map
|
80
|
+
SupportedTypes.types.map(&:type).include?(constant)
|
82
81
|
end
|
83
82
|
|
84
83
|
# @return [#apply] the typecaster object that will be able to convert
|
85
84
|
# the value into a value with the correct type.
|
86
85
|
def typecaster(value)
|
87
|
-
SupportedTypes.types.find { |caster| caster.apply?(value,constant) }
|
86
|
+
SupportedTypes.types.find { |caster| caster.apply?(value, constant) }
|
88
87
|
end
|
89
88
|
|
90
89
|
#
|
@@ -113,21 +112,21 @@ module HappyMapper
|
|
113
112
|
end
|
114
113
|
|
115
114
|
def process_node_with_custom_parser(node)
|
116
|
-
if node.respond_to?(:content) && !options[:raw]
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
115
|
+
value = if node.respond_to?(:content) && !options[:raw]
|
116
|
+
node.content
|
117
|
+
else
|
118
|
+
node.to_s
|
119
|
+
end
|
121
120
|
|
122
121
|
begin
|
123
122
|
constant.send(options[:parser].to_sym, value)
|
124
|
-
rescue
|
123
|
+
rescue StandardError
|
125
124
|
nil
|
126
125
|
end
|
127
126
|
end
|
128
127
|
|
129
|
-
def process_node_with_default_parser(node,parse_options)
|
130
|
-
constant.parse(node,options.merge(parse_options))
|
128
|
+
def process_node_with_default_parser(node, parse_options)
|
129
|
+
constant.parse(node, options.merge(parse_options))
|
131
130
|
end
|
132
131
|
|
133
132
|
#
|
@@ -155,6 +154,5 @@ module HappyMapper
|
|
155
154
|
end
|
156
155
|
constant
|
157
156
|
end
|
158
|
-
|
159
157
|
end
|
160
158
|
end
|
@@ -1,6 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module HappyMapper
|
2
4
|
module SupportedTypes
|
3
|
-
|
5
|
+
module_function
|
4
6
|
|
5
7
|
#
|
6
8
|
# All of the registerd supported types that can be parsed.
|
@@ -45,8 +47,8 @@ module HappyMapper
|
|
45
47
|
# DateTime.parse(value,to_s)
|
46
48
|
# end
|
47
49
|
#
|
48
|
-
def register_type(type
|
49
|
-
register CastWhenType.new(type
|
50
|
+
def register_type(type, &block)
|
51
|
+
register CastWhenType.new(type, &block)
|
50
52
|
end
|
51
53
|
|
52
54
|
#
|
@@ -57,16 +59,16 @@ module HappyMapper
|
|
57
59
|
class CastWhenType
|
58
60
|
attr_reader :type
|
59
61
|
|
60
|
-
def initialize(type
|
62
|
+
def initialize(type, &block)
|
61
63
|
@type = type
|
62
64
|
@apply_block = block || no_operation
|
63
65
|
end
|
64
66
|
|
65
67
|
def no_operation
|
66
|
-
|
68
|
+
->(value) { value }
|
67
69
|
end
|
68
70
|
|
69
|
-
def apply?(
|
71
|
+
def apply?(_value, convert_to_type)
|
70
72
|
convert_to_type == type
|
71
73
|
end
|
72
74
|
|
@@ -81,13 +83,12 @@ module HappyMapper
|
|
81
83
|
# value simply can be returned.
|
82
84
|
#
|
83
85
|
class NilOrAlreadyConverted
|
84
|
-
|
85
86
|
def type
|
86
87
|
NilClass
|
87
88
|
end
|
88
89
|
|
89
|
-
def apply?(value,convert_to_type)
|
90
|
-
value.
|
90
|
+
def apply?(value, convert_to_type)
|
91
|
+
value.is_a?(convert_to_type) || value.nil?
|
91
92
|
end
|
92
93
|
|
93
94
|
def apply(value)
|
@@ -97,28 +98,30 @@ module HappyMapper
|
|
97
98
|
|
98
99
|
register NilOrAlreadyConverted.new
|
99
100
|
|
100
|
-
register_type String
|
101
|
-
value.to_s
|
102
|
-
end
|
101
|
+
register_type String, &:to_s
|
103
102
|
|
104
|
-
register_type Float
|
105
|
-
value.to_f
|
106
|
-
end
|
103
|
+
register_type Float, &:to_f
|
107
104
|
|
108
105
|
register_type Time do |value|
|
109
|
-
|
106
|
+
begin
|
107
|
+
Time.parse(value.to_s)
|
108
|
+
rescue StandardError
|
109
|
+
Time.at(value.to_i)
|
110
|
+
end
|
110
111
|
end
|
111
112
|
|
113
|
+
# rubocop:disable Style/DateTime
|
112
114
|
register_type DateTime do |value|
|
113
115
|
DateTime.parse(value.to_s) if value && !value.empty?
|
114
116
|
end
|
117
|
+
# rubocop:enable Style/DateTime
|
115
118
|
|
116
119
|
register_type Date do |value|
|
117
120
|
Date.parse(value.to_s) if value && !value.empty?
|
118
121
|
end
|
119
122
|
|
120
123
|
register_type Boolean do |value|
|
121
|
-
|
124
|
+
%w(true t 1).include?(value.to_s.downcase)
|
122
125
|
end
|
123
126
|
|
124
127
|
register_type Integer do |value|
|
@@ -134,7 +137,5 @@ module HappyMapper
|
|
134
137
|
value_to_i
|
135
138
|
end
|
136
139
|
end
|
137
|
-
|
138
140
|
end
|
139
|
-
|
140
141
|
end
|
@@ -1,8 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module HappyMapper
|
2
4
|
class TextNode < Item
|
3
|
-
|
4
|
-
|
5
|
-
yield(node.children.detect{|c| c.text?})
|
5
|
+
def find(node, _namespace, _xpath_options)
|
6
|
+
yield(node.children.detect(&:text?))
|
6
7
|
end
|
7
8
|
end
|
8
9
|
end
|
data/lib/happymapper/version.rb
CHANGED
@@ -1,46 +1,45 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
describe "Attribute Default Value" do
|
1
|
+
# frozen_string_literal: true
|
4
2
|
|
5
|
-
|
3
|
+
require 'spec_helper'
|
6
4
|
|
5
|
+
describe 'Attribute Default Value' do
|
6
|
+
context 'when given a default value' do
|
7
7
|
class Meal
|
8
8
|
include HappyMapper
|
9
9
|
tag 'meal'
|
10
|
-
attribute :type, String, :
|
10
|
+
attribute :type, String, default: 'omnivore'
|
11
11
|
end
|
12
12
|
|
13
13
|
let(:subject) { Meal }
|
14
14
|
let(:default_meal_type) { 'omnivore' }
|
15
15
|
|
16
|
-
context
|
17
|
-
it
|
16
|
+
context 'when no value has been specified' do
|
17
|
+
it 'returns the default value' do
|
18
18
|
meal = subject.parse('<meal />')
|
19
19
|
expect(meal.type).to eq default_meal_type
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
23
|
-
context
|
24
|
-
|
25
|
-
let(:expected_xml) { %{<?xml version="1.0"?>\n<meal/>\n} }
|
23
|
+
context 'when saving to xml' do
|
24
|
+
let(:expected_xml) { %(<?xml version="1.0"?>\n<meal/>\n) }
|
26
25
|
|
27
|
-
it
|
26
|
+
it 'the default value is not included' do
|
28
27
|
meal = subject.new
|
29
28
|
expect(meal.to_xml).to eq expected_xml
|
30
29
|
end
|
31
30
|
end
|
32
31
|
|
33
|
-
context
|
34
|
-
it
|
32
|
+
context 'when a new, non-nil value has been set' do
|
33
|
+
it 'returns the new value' do
|
35
34
|
meal = subject.parse('<meal />')
|
36
35
|
meal.type = 'vegan'
|
37
36
|
|
38
37
|
expect(meal.type).to_not eq default_meal_type
|
39
38
|
end
|
40
39
|
|
41
|
-
let(:expected_xml) { %
|
40
|
+
let(:expected_xml) { %(<?xml version="1.0"?>\n<meal type="kosher"/>\n) }
|
42
41
|
|
43
|
-
it
|
42
|
+
it 'saves the new value to the xml' do
|
44
43
|
meal = subject.new
|
45
44
|
meal.type = 'kosher'
|
46
45
|
expect(meal.to_xml).to eq expected_xml
|