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,223 @@
1
+ require 'spec_helper'
2
+
3
+ module ToXMLWithNamespaces
4
+ #
5
+ # Similar example as the to_xml but this time with namespacing
6
+ #
7
+ class Address
8
+ include HappyMapper
9
+
10
+ register_namespace 'address', 'http://www.company.com/address'
11
+ register_namespace 'country', 'http://www.company.com/country'
12
+
13
+ tag 'Address'
14
+ namespace 'address'
15
+
16
+ element :country, 'Country', tag: 'country', namespace: 'country'
17
+
18
+ attribute :location, String, on_save: :when_saving_location
19
+
20
+ element :street, String
21
+ element :postcode, String
22
+ element :city, String
23
+
24
+ element :housenumber, String
25
+
26
+ #
27
+ # to_xml will default to the attr_accessor method and not the attribute,
28
+ # allowing for that to be overwritten
29
+ #
30
+ def housenumber
31
+ "[#{@housenumber}]"
32
+ end
33
+
34
+ def when_saving_location(loc)
35
+ loc + '-live'
36
+ end
37
+
38
+ #
39
+ # Write a empty element even if this is not specified
40
+ #
41
+ element :description, String, state_when_nil: true
42
+
43
+ #
44
+ # Perform the on_save operation when saving
45
+ #
46
+ has_one :date_created, Time, on_save: ->(time) { DateTime.parse(time).strftime('%T %D') if time }
47
+
48
+ #
49
+ # Write multiple elements and call on_save when saving
50
+ #
51
+ has_many :dates_updated, Time, on_save: lambda {|times|
52
+ times.compact.map { |time| DateTime.parse(time).strftime('%T %D') } if times
53
+ }
54
+
55
+ #
56
+ # Class composition
57
+ #
58
+
59
+ def initialize(parameters)
60
+ parameters.each_pair do |property, value|
61
+ send("#{property}=", value) if respond_to?("#{property}=")
62
+ end
63
+ end
64
+ end
65
+
66
+ #
67
+ # Country is composed above the in Address class. Here is a demonstration
68
+ # of how to_xml will handle class composition as well as utilizing the tag
69
+ # value.
70
+ #
71
+ class Country
72
+ include HappyMapper
73
+
74
+ register_namespace 'countryName', 'http://www.company.com/countryName'
75
+
76
+ attribute :code, String, tag: 'countryCode'
77
+ has_one :name, String, tag: 'countryName', namespace: 'countryName'
78
+
79
+ def initialize(parameters)
80
+ parameters.each_pair do |property, value|
81
+ send("#{property}=", value) if respond_to?("#{property}=")
82
+ end
83
+ end
84
+ end
85
+
86
+ #
87
+ # This class is an example of a class that has a default namespace
88
+ # xmlns="urn:eventis:prodis:onlineapi:1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
89
+ #
90
+ class Recipe
91
+ include HappyMapper
92
+
93
+ # this is the default namespace of the document
94
+ register_namespace 'xmlns', 'urn:eventis:prodis:onlineapi:1.0'
95
+ register_namespace 'xsi', 'http://www.w3.org/2001/XMLSchema-instance'
96
+ register_namespace 'xsd', 'http://www.w3.org/2001/XMLSchema'
97
+
98
+ has_many :ingredients, String
99
+
100
+ def initialize(parameters)
101
+ parameters.each_pair { |property, value| send("#{property}=", value) if respond_to?("#{property}=") }
102
+ end
103
+ end
104
+ end
105
+
106
+ describe 'Saving #to_xml', 'with xml namespaces' do
107
+ context '#to_xml', 'with namespaces' do
108
+ let(:subject) do
109
+ address = ToXMLWithNamespaces::Address.new('street' => 'Mockingbird Lane',
110
+ 'location' => 'Home',
111
+ 'housenumber' => '1313',
112
+ 'postcode' => '98103',
113
+ 'city' => 'Seattle',
114
+ 'country' => ToXMLWithNamespaces::Country.new(name: 'USA', code: 'us'),
115
+ 'date_created' => '2011-01-01 15:00:00')
116
+
117
+ address.dates_updated = ['2011-01-01 16:01:00', '2011-01-02 11:30:01']
118
+
119
+ Nokogiri::XML(address.to_xml).root
120
+ end
121
+
122
+ it 'saves elements' do
123
+ elements = { 'street' => 'Mockingbird Lane', 'postcode' => '98103', 'city' => 'Seattle' }
124
+
125
+ elements.each_pair do |property, value|
126
+ expect(subject.xpath("address:#{property}").text).to eq value
127
+ end
128
+ end
129
+
130
+ it 'saves attributes' do
131
+ expect(subject.xpath('@location').text).to eq 'Home-live'
132
+ end
133
+
134
+ context "when an element has a 'state_when_nil' parameter" do
135
+ it 'saves an empty element' do
136
+ expect(subject.xpath('address:description').text).to eq ''
137
+ end
138
+ end
139
+
140
+ context "when an element has a 'on_save' parameter" do
141
+ context 'with a symbol which represents a function' do
142
+ it 'saves the element with the result of a function call and not the value of the instance variable' do
143
+ expect(subject.xpath('address:housenumber').text).to eq '[1313]'
144
+ end
145
+ end
146
+
147
+ context 'with a lambda' do
148
+ it 'saves the results' do
149
+ expect(subject.xpath('address:date_created').text).to eq '15:00:00 01/01/11'
150
+ end
151
+ end
152
+ end
153
+
154
+ context "when an attribute has a 'on_save' parameter" do
155
+ context 'with a lambda' do
156
+ it 'saves the result' do
157
+ expect(subject.xpath('@location').text).to eq 'Home-live'
158
+ end
159
+ end
160
+ end
161
+
162
+ context "when a has_many has a 'on_save' parameter" do
163
+ context 'with a lambda' do
164
+ it 'saves the result' do
165
+ dates_updated = subject.xpath('address:dates_updated')
166
+ expect(dates_updated.length).to eq 2
167
+ expect(dates_updated.first.text).to eq '16:01:00 01/01/11'
168
+ expect(dates_updated.last.text).to eq '11:30:01 01/02/11'
169
+ end
170
+ end
171
+ end
172
+
173
+ context 'when an element type is a HappyMapper subclass' do
174
+ it 'saves attributes' do
175
+ expect(subject.xpath('country:country/@countryCode').text).to eq 'us'
176
+ end
177
+
178
+ it 'saves elements' do
179
+ expect(subject.xpath('country:country/countryName:countryName').text).to eq 'USA'
180
+ end
181
+ end
182
+ end
183
+
184
+ context 'with a default namespace' do
185
+ it 'writes the default namespace to xml without repeating xmlns' do
186
+ recipe = ToXMLWithNamespaces::Recipe.new(ingredients: ['One Cup Flour', 'Two Scoops of Lovin'])
187
+ expect(recipe.to_xml).to match /xmlns=\"urn:eventis:prodis:onlineapi:1\.0\"/
188
+ end
189
+ end
190
+
191
+ context 'namespace supplied by element declaration trumps namespace ' \
192
+ 'specified by element class' do
193
+ let(:expected_xml) do
194
+ <<-XML.gsub(/^\s*\|/, '')
195
+ |<?xml version="1.0"?>
196
+ |<coffeemachine xmlns:coffee="http://coffee.org/Coffee/0.1" xmlns:beverage="http://beverages.org/Beverage/0.1">
197
+ | <beverage:beverage name="coffee"/>
198
+ |</coffeemachine>
199
+ XML
200
+ end
201
+
202
+ class Beverage
203
+ include HappyMapper
204
+ namespace 'coffee'
205
+
206
+ attribute :name, String
207
+ end
208
+
209
+ class CoffeeMachine
210
+ include HappyMapper
211
+ register_namespace 'coffee', 'http://coffee.org/Coffee/0.1'
212
+ register_namespace 'beverage', 'http://beverages.org/Beverage/0.1'
213
+
214
+ element :beverage, 'beverage', namespace: 'beverage'
215
+ end
216
+
217
+ it 'uses the element declaration namespace on the element' do
218
+ machine = CoffeeMachine.new
219
+ machine.beverage = Beverage.new.tap { |obj| obj.name = 'coffee' }
220
+ machine.to_xml.should be == expected_xml
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,94 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Wildcard Root Tag' do
4
+ generic_class_xml = %(
5
+ <root>
6
+ <description>some description</description>
7
+ <blarg name='blargname1' href='http://blarg.com'/>
8
+ <blarg name='blargname2' href='http://blarg.com'/>
9
+ <jello name='jelloname' href='http://jello.com'/>
10
+ <subelement>
11
+ <jello name='subjelloname' href='http://ohnojello.com' other='othertext'/>
12
+ </subelement>
13
+ </root>)
14
+
15
+ module GenericBase
16
+ class Base
17
+ include Comparable
18
+ include HappyMapper
19
+
20
+ def initialize(params = {})
21
+ @name = params[:name]
22
+ @href = params[:href]
23
+ @other = params[:other]
24
+ end
25
+
26
+ tag '*'
27
+ attribute :name, String
28
+ attribute :href, String
29
+ attribute :other, String
30
+
31
+ def <=>(compared)
32
+ name <=> compared.name && href <=> compared.href && other <=> compared.other
33
+ end
34
+ end
35
+ class Sub
36
+ include HappyMapper
37
+ tag 'subelement'
38
+ has_one :jello, Base, tag: 'jello'
39
+ end
40
+ class Root
41
+ include HappyMapper
42
+ tag 'root'
43
+ element :description, String
44
+ has_many :blargs, Base, tag: 'blarg', xpath: '.'
45
+ has_many :jellos, Base, tag: 'jello', xpath: '.'
46
+ has_many :subjellos, Base, tag: 'jello', xpath: 'subelement/.', read_only: true
47
+ has_one :sub_element, Sub
48
+ end
49
+ end
50
+
51
+ describe "can have generic classes using tag '*'" do
52
+ let(:subject) { GenericBase::Root.parse(generic_class_xml) }
53
+ let(:xml) { Nokogiri::XML(subject.to_xml) }
54
+
55
+ it 'should map different elements to same class' do
56
+ subject.blargs.should_not be_nil
57
+ subject.jellos.should_not be_nil
58
+ end
59
+
60
+ it 'should filter on xpath appropriately' do
61
+ subject.blargs.should have(2).items
62
+ subject.jellos.should have(1).items
63
+ subject.subjellos.should have(1).items
64
+ end
65
+
66
+ def base_with(name, href, other)
67
+ GenericBase::Base.new(name: name, href: href, other: other)
68
+ end
69
+
70
+ it 'should parse correct values onto generic class' do
71
+ expect(subject.blargs[0]).to eq base_with('blargname1', 'http://blarg.com', nil)
72
+ expect(subject.blargs[1]).to eq base_with('blargname2', 'http://blarg.com', nil)
73
+ expect(subject.jellos[0]).to eq base_with('jelloname', 'http://jello.com', nil)
74
+ expect(subject.subjellos[0]).to eq base_with('subjelloname', 'http://ohnojello.com', 'othertext')
75
+ end
76
+
77
+ def validate_xpath(xpath, name, href, other)
78
+ expect(xml.xpath("#{xpath}/@name").text).to eq name
79
+ expect(xml.xpath("#{xpath}/@href").text).to eq href
80
+ expect(xml.xpath("#{xpath}/@other").text).to eq other
81
+ end
82
+
83
+ it 'should #to_xml using parent element tag name' do
84
+ xml.xpath('/root/description').text.should == 'some description'
85
+ validate_xpath('/root/blarg[1]', 'blargname1', 'http://blarg.com', '')
86
+ validate_xpath('/root/blarg[2]', 'blargname2', 'http://blarg.com', '')
87
+ validate_xpath('/root/jello[1]', 'jelloname', 'http://jello.com', '')
88
+ end
89
+
90
+ it "should properly respect child HappyMapper tags if tag isn't provided on the element defintion" do
91
+ xml.xpath('root/subelement').should have(1).item
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,80 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'wrap which allows you to specify a wrapper element' do
4
+ module Wrap
5
+ class SubClass
6
+ include HappyMapper
7
+ tag 'subclass'
8
+ attribute :myattr, String
9
+ has_many :items, String, tag: 'item'
10
+ end
11
+ class Root
12
+ include HappyMapper
13
+ tag 'root'
14
+ attribute :attr1, String
15
+ element :name, String
16
+ wrap 'mywraptag' do
17
+ element :description, String
18
+ has_one :subclass, SubClass
19
+ end
20
+ element :number, Integer
21
+ end
22
+ end
23
+
24
+ describe '.parse' do
25
+ context 'when given valid XML' do
26
+ let(:subject) { Wrap::Root.parse fixture_file('wrapper.xml') }
27
+
28
+ it 'sets the values correctly' do
29
+ expect(subject.attr1).to eq 'somevalue'
30
+ expect(subject.name).to eq 'myname'
31
+ expect(subject.description).to eq 'some description'
32
+ expect(subject.subclass.myattr).to eq 'attrvalue'
33
+ expect(subject.subclass.items).to have(2).items
34
+ expect(subject.subclass.items[0]).to eq 'item1'
35
+ expect(subject.subclass.items[1]).to eq 'item2'
36
+ expect(subject.number).to eq 12_345
37
+ end
38
+ end
39
+
40
+ context 'when initialized without XML' do
41
+ let(:subject) { Wrap::Root.new }
42
+
43
+ it 'anonymous classes are created so nil class values does not occur' do
44
+ expect { subject.description = 'anything' }.to_not raise_error
45
+ end
46
+ end
47
+ end
48
+
49
+ describe '.to_xml' do
50
+ let(:subject) do
51
+ root = Wrap::Root.new
52
+ root.attr1 = 'somevalue'
53
+ root.name = 'myname'
54
+ root.description = 'some description'
55
+ root.number = 12_345
56
+
57
+ subclass = Wrap::SubClass.new
58
+ subclass.myattr = 'attrvalue'
59
+ subclass.items = []
60
+ subclass.items << 'item1'
61
+ subclass.items << 'item2'
62
+
63
+ root.subclass = subclass
64
+
65
+ root
66
+ end
67
+
68
+ it 'generates the correct xml' do
69
+ xml = Nokogiri::XML(subject.to_xml)
70
+ expect(xml.xpath('/root/@attr1').text).to eq 'somevalue'
71
+ expect(xml.xpath('/root/name').text).to eq 'myname'
72
+ expect(xml.xpath('/root/mywraptag/description').text).to eq 'some description'
73
+ expect(xml.xpath('/root/mywraptag/subclass/@myattr').text).to eq 'attrvalue'
74
+ expect(xml.xpath('/root/mywraptag/subclass/item')).to have(2).items
75
+ expect(xml.xpath('/root/mywraptag/subclass/item[1]').text).to eq 'item1'
76
+ expect(xml.xpath('/root/mywraptag/subclass/item[2]').text).to eq 'item2'
77
+ expect(xml.xpath('/root/number').text).to eq '12345'
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,86 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Specifying elements and attributes with an xpath' do
4
+ class Item
5
+ include HappyMapper
6
+
7
+ tag 'item'
8
+ namespace 'amazing'
9
+
10
+ element :title, String
11
+ attribute :link, String, xpath: 'amazing:link/@href'
12
+ has_one :different_link, String, xpath: 'different:link/@href'
13
+ element :detail, String, xpath: 'amazing:subitem/amazing:detail'
14
+ has_many :more_details_text, String, xpath: 'amazing:subitem/amazing:more'
15
+ has_many :more_details, String, xpath: 'amazing:subitem/amazing:more/@first|amazing:subitem/amazing:more/@alternative'
16
+ has_many :more_details_alternative, String, xpath: 'amazing:subitem/amazing:more/@*'
17
+
18
+ has_one :baby, 'Baby', name: 'baby', namespace: 'amazing'
19
+ end
20
+
21
+ class Baby
22
+ include HappyMapper
23
+
24
+ has_one :name, String
25
+ end
26
+
27
+ let(:subject) { Item.parse(xml_string, single: true) }
28
+
29
+ let(:xml_string) do
30
+ %(
31
+ <rss>
32
+ <amazing:item xmlns:amazing="http://www.amazing.com/amazing" xmlns:different="http://www.different.com/different">
33
+ <amazing:title>Test XML</amazing:title>
34
+ <different:link href="different_link" />
35
+ <amazing:link href="link_to_resources" />
36
+ <amazing:subitem>
37
+ <amazing:detail>I want to parse this</amazing:detail>
38
+ <amazing:more first="this one">more 1</amazing:more>
39
+ <amazing:more alternative="another one">more 2</amazing:more>
40
+ </amazing:subitem>
41
+ <amazing:baby>
42
+ <amazing:name>Jumbo</amazing:name>
43
+ </amazing:baby>
44
+ </amazing:item>
45
+ </rss>
46
+ )
47
+ end
48
+
49
+ it 'should have a title' do
50
+ expect(subject.title).to eq 'Test XML'
51
+ end
52
+
53
+ it 'should find the link href value' do
54
+ expect(subject.link).to eq 'link_to_resources'
55
+ end
56
+
57
+ it 'should find the link href value' do
58
+ expect(subject.different_link).to eq 'different_link'
59
+ end
60
+
61
+ it 'should find this subitem based on the xpath' do
62
+ expect(subject.detail).to eq 'I want to parse this'
63
+ end
64
+
65
+ it 'should find the subitems based on the xpath' do
66
+ expect(subject.more_details_text).to have(2).items
67
+ expect(subject.more_details_text.first).to eq 'more 1'
68
+ expect(subject.more_details_text.last).to eq 'more 2'
69
+ end
70
+
71
+ it 'should find the subitems based on the xpath' do
72
+ expect(subject.more_details).to have(2).items
73
+ expect(subject.more_details.first).to eq 'this one'
74
+ expect(subject.more_details.last).to eq 'another one'
75
+ end
76
+
77
+ it 'should find the subitems based on the xpath' do
78
+ expect(subject.more_details_alternative).to have(2).items
79
+ expect(subject.more_details_alternative.first).to eq 'this one'
80
+ expect(subject.more_details_alternative.last).to eq 'another one'
81
+ end
82
+
83
+ it 'should have a baby name' do
84
+ expect(subject.baby.name).to eq 'Jumbo'
85
+ end
86
+ end