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,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