nokogiri-happymapper 0.6.0 → 0.7.0
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 +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
|