blockscore-happymapper 0.6.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.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +39 -0
  3. data/README.md +605 -0
  4. data/lib/happymapper.rb +754 -0
  5. data/lib/happymapper/anonymous_mapper.rb +107 -0
  6. data/lib/happymapper/attribute.rb +21 -0
  7. data/lib/happymapper/element.rb +53 -0
  8. data/lib/happymapper/item.rb +156 -0
  9. data/lib/happymapper/supported_types.rb +137 -0
  10. data/lib/happymapper/text_node.rb +7 -0
  11. data/lib/happymapper/version.rb +3 -0
  12. data/spec/attribute_default_value_spec.rb +47 -0
  13. data/spec/attributes_spec.rb +33 -0
  14. data/spec/fixtures/address.xml +9 -0
  15. data/spec/fixtures/ambigous_items.xml +22 -0
  16. data/spec/fixtures/analytics.xml +61 -0
  17. data/spec/fixtures/analytics_profile.xml +127 -0
  18. data/spec/fixtures/atom.xml +19 -0
  19. data/spec/fixtures/commit.xml +52 -0
  20. data/spec/fixtures/current_weather.xml +89 -0
  21. data/spec/fixtures/current_weather_missing_elements.xml +18 -0
  22. data/spec/fixtures/default_namespace_combi.xml +6 -0
  23. data/spec/fixtures/dictionary.xml +20 -0
  24. data/spec/fixtures/family_tree.xml +21 -0
  25. data/spec/fixtures/inagy.xml +85 -0
  26. data/spec/fixtures/lastfm.xml +355 -0
  27. data/spec/fixtures/multiple_namespaces.xml +170 -0
  28. data/spec/fixtures/multiple_primitives.xml +5 -0
  29. data/spec/fixtures/optional_attributes.xml +6 -0
  30. data/spec/fixtures/pita.xml +133 -0
  31. data/spec/fixtures/posts.xml +23 -0
  32. data/spec/fixtures/product_default_namespace.xml +18 -0
  33. data/spec/fixtures/product_no_namespace.xml +10 -0
  34. data/spec/fixtures/product_single_namespace.xml +10 -0
  35. data/spec/fixtures/quarters.xml +19 -0
  36. data/spec/fixtures/radar.xml +21 -0
  37. data/spec/fixtures/set_config_options.xml +3 -0
  38. data/spec/fixtures/statuses.xml +422 -0
  39. data/spec/fixtures/subclass_namespace.xml +50 -0
  40. data/spec/fixtures/wrapper.xml +11 -0
  41. data/spec/happymapper/attribute_spec.rb +10 -0
  42. data/spec/happymapper/element_spec.rb +9 -0
  43. data/spec/happymapper/item_spec.rb +114 -0
  44. data/spec/happymapper/text_node_spec.rb +9 -0
  45. data/spec/happymapper_parse_spec.rb +99 -0
  46. data/spec/happymapper_spec.rb +1096 -0
  47. data/spec/has_many_empty_array_spec.rb +42 -0
  48. data/spec/ignay_spec.rb +87 -0
  49. data/spec/inheritance_spec.rb +105 -0
  50. data/spec/mixed_namespaces_spec.rb +58 -0
  51. data/spec/parse_with_object_to_update_spec.rb +108 -0
  52. data/spec/spec_helper.rb +8 -0
  53. data/spec/to_xml_spec.rb +197 -0
  54. data/spec/to_xml_with_namespaces_spec.rb +223 -0
  55. data/spec/wilcard_tag_name_spec.rb +94 -0
  56. data/spec/wrap_spec.rb +80 -0
  57. data/spec/xpath_spec.rb +86 -0
  58. 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