blockscore-happymapper 0.6.0

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