instructure-happymapper 0.5.10

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