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,42 @@
1
+ require 'spec_helper'
2
+
3
+ module Sheep
4
+ class Item
5
+ include HappyMapper
6
+ end
7
+
8
+ class Navigator
9
+ include HappyMapper
10
+ tag 'navigator'
11
+
12
+ # This is purposefully set to have the name 'items' with the tag 'item'.
13
+ # The idea is that it should not find the empty items contained within the
14
+ # xml and return an empty array. This exercises the order of how nodes
15
+ # are searched for within an XML document.
16
+ has_many :items, Item, tag: 'item'
17
+
18
+ has_many :items_with_a_different_name, Item, tag: 'item'
19
+ end
20
+ end
21
+
22
+ describe 'emptyness' do
23
+ let(:xml) do
24
+ <<-EOF
25
+ <navigator>
26
+ <items/>
27
+ </navigator>
28
+ EOF
29
+ end
30
+
31
+ let(:navigator) do
32
+ Sheep::Navigator.parse(xml)
33
+ end
34
+
35
+ it 'returns an empty array' do
36
+ navigator.items_with_a_different_name.should be_empty
37
+ end
38
+
39
+ it 'returns an empty array' do
40
+ navigator.items.should be_empty
41
+ end
42
+ end
@@ -0,0 +1,87 @@
1
+ require 'spec_helper'
2
+
3
+ class CatalogTree
4
+ include HappyMapper
5
+
6
+ tag 'CatalogTree'
7
+ register_namespace 'xmlns', 'urn:eventis:prodis:onlineapi:1.0'
8
+ register_namespace 'xsi', 'http://www.w3.org/2001/XMLSchema-instance'
9
+ register_namespace 'xsd', 'http://www.w3.org/2001/XMLSchema'
10
+
11
+ attribute :code, String
12
+
13
+ has_many :nodes, 'CatalogNode', tag: 'Node', xpath: '.'
14
+ end
15
+
16
+ class CatalogNode
17
+ include HappyMapper
18
+
19
+ tag 'Node'
20
+
21
+ attribute :back_office_id, String, tag: 'vodBackOfficeId'
22
+
23
+ has_one :name, String, tag: 'Name'
24
+ # other important fields
25
+
26
+ has_many :translations, 'CatalogNode::Translations', tag: 'Translation', xpath: 'child::*'
27
+
28
+ class Translations
29
+ include HappyMapper
30
+ tag 'Translation'
31
+
32
+ attribute :language, String, tag: 'Language'
33
+ has_one :name, String, tag: 'Name'
34
+ end
35
+
36
+ has_many :nodes, CatalogNode, tag: 'Node', xpath: 'child::*'
37
+ end
38
+
39
+ describe HappyMapper do
40
+ it 'should not be nil' do
41
+ catalog_tree.should_not be_nil
42
+ end
43
+
44
+ it 'should have the attribute code' do
45
+ catalog_tree.code.should == 'NLD'
46
+ end
47
+
48
+ it 'should have many nodes' do
49
+ catalog_tree.nodes.should_not be_empty
50
+ catalog_tree.nodes.length.should == 2
51
+ end
52
+
53
+ context 'first node' do
54
+ it 'should have a name' do
55
+ first_node.name.should == 'Parent 1'
56
+ end
57
+
58
+ it 'should have translations' do
59
+ first_node.translations.length.should == 2
60
+
61
+ first_node.translations.first.language.should == 'en-GB'
62
+
63
+ first_node.translations.last.name.should == 'Parent 1 de'
64
+ end
65
+
66
+ it 'should have subnodes' do
67
+ first_node.nodes.should be_kind_of(Enumerable)
68
+ first_node.nodes.should_not be_empty
69
+ first_node.nodes.length.should == 1
70
+ end
71
+
72
+ it 'first node - first node name' do
73
+ first_node.nodes.first.name.should == 'First'
74
+ end
75
+
76
+ def first_node
77
+ @first_node = catalog_tree.nodes.first
78
+ end
79
+ end
80
+
81
+ attr_reader :catalog_tree
82
+
83
+ before(:all) do
84
+ xml_reference = "#{File.dirname(__FILE__)}/fixtures/inagy.xml"
85
+ @catalog_tree = CatalogTree.parse(File.read(xml_reference), single: true)
86
+ end
87
+ end
@@ -0,0 +1,105 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Using inheritance to share elements and attributes' do
4
+ class Genetics
5
+ include HappyMapper
6
+ content :dna, String
7
+ end
8
+
9
+ class Parent
10
+ include HappyMapper
11
+ attribute :love, Integer
12
+ element :genetics, Genetics
13
+ end
14
+
15
+ class Child < Parent
16
+ include HappyMapper
17
+ attribute :naivety, String
18
+ has_many :immunities, String
19
+ end
20
+
21
+ class Overwrite < Parent
22
+ include HappyMapper
23
+
24
+ attribute :love, String
25
+ element :genetics, Integer
26
+ end
27
+
28
+ describe 'Overwrite' do
29
+ let(:subject) do
30
+ xml = '<overwrite love="love" naivety="trusting"><genetics>1001</genetics><immunities>Chicken Pox</immunities></overwrite>'
31
+ Overwrite.parse(xml, single: true)
32
+ end
33
+
34
+ it 'overrides the parent elements and attributes' do
35
+ expect(Overwrite.attributes.count).to be == Parent.attributes.count
36
+ expect(Overwrite.elements.count).to be == Parent.elements.count
37
+ end
38
+
39
+ context 'when parsing xml' do
40
+ it 'parses the new overwritten attribut' do
41
+ expect(subject.love).to be == 'love'
42
+ end
43
+
44
+ it 'parses the new overwritten element' do
45
+ expect(subject.genetics).to be == 1001
46
+ end
47
+ end
48
+
49
+ context 'when saving to xml' do
50
+ subject do
51
+ overwrite = Overwrite.new
52
+ overwrite.genetics = 1
53
+ overwrite.love = 'love'
54
+ Nokogiri::XML(overwrite.to_xml).root
55
+ end
56
+
57
+ it 'has only 1 genetics element' do
58
+ expect(subject.xpath('//genetics').count).to be == 1
59
+ end
60
+
61
+ it 'has only 1 love attribute' do
62
+ expect(subject.xpath('@love').text).to be == 'love'
63
+ end
64
+ end
65
+ end
66
+
67
+ describe 'Child', 'a subclass of the Parent' do
68
+ let(:subject) do
69
+ xml = '<child love="99" naivety="trusting"><genetics>ABBA</genetics><immunities>Chicken Pox</immunities></child>'
70
+ Child.parse(xml)
71
+ end
72
+
73
+ context 'when parsing xml' do
74
+ it 'should be possible to deserialize XML into a Child class instance' do
75
+ expect(subject.love).to eq 99
76
+ expect(subject.genetics.dna).to eq 'ABBA'
77
+ expect(subject.naivety).to eq 'trusting'
78
+ expect(subject.immunities).to have(1).item
79
+ end
80
+ end
81
+
82
+ context 'when saving to xml' do
83
+ let(:subject) do
84
+ child = Child.new
85
+ child.love = 100
86
+ child.naivety = 'Bright Eyed'
87
+ child.immunities = ['Small Pox', 'Chicken Pox', 'Mumps']
88
+ genetics = Genetics.new
89
+ genetics.dna = 'GATTACA'
90
+ child.genetics = genetics
91
+ Nokogiri::XML(child.to_xml).root
92
+ end
93
+
94
+ it 'saves both the Child and Parent attributes' do
95
+ expect(subject.xpath('@naivety').text).to eq 'Bright Eyed'
96
+ expect(subject.xpath('@love').text).to eq '100'
97
+ end
98
+
99
+ it 'saves both the Child and Parent elements' do
100
+ expect(subject.xpath('genetics').text).to eq 'GATTACA'
101
+ expect(subject.xpath('immunities')).to have(3).items
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,58 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'A document with mixed namespaces' do
4
+ #
5
+ # Note that the parent element of the xml has the namespacing. The elements
6
+ # contained within the xml do not share the parent element namespace so a
7
+ # user of the library would likely need to clear the namespace on each of
8
+ # these child elements.
9
+ #
10
+ let(:xml_document) do
11
+ %(<prefix:address location='home' xmlns:prefix="http://www.unicornland.com/prefix"
12
+ xmlns:different="http://www.trollcountry.com/different">
13
+ <street>Milchstrasse</street>
14
+ <street>Another Street</street>
15
+ <housenumber>23</housenumber>
16
+ <different:postcode>26131</different:postcode>
17
+ <city>Oldenburg</city>
18
+ </prefix:address>)
19
+ end
20
+
21
+ module MixedNamespaces
22
+ class Address
23
+ include HappyMapper
24
+
25
+ namespace :prefix
26
+ tag :address
27
+
28
+ # Here each of the elements have their namespace set to nil to reset their
29
+ # namespace so that it is not the same as the prefix namespace
30
+
31
+ has_many :streets, String, tag: 'street', namespace: nil
32
+
33
+ has_one :house_number, String, tag: 'housenumber', namespace: nil
34
+ has_one :postcode, String, namespace: 'different'
35
+ has_one :city, String, namespace: nil
36
+ end
37
+ end
38
+
39
+ let(:address) do
40
+ MixedNamespaces::Address.parse(xml_document, single: true)
41
+ end
42
+
43
+ it 'has the correct streets' do
44
+ expect(address.streets).to eq ['Milchstrasse', 'Another Street']
45
+ end
46
+
47
+ it 'house number' do
48
+ expect(address.house_number).to eq '23'
49
+ end
50
+
51
+ it 'postcode' do
52
+ expect(address.postcode).to eq '26131'
53
+ end
54
+
55
+ it 'city' do
56
+ expect(address.city).to eq 'Oldenburg'
57
+ end
58
+ end
@@ -0,0 +1,108 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Updating existing objects with .parse and #parse' do
4
+ let(:subject) { ParseInstanceSpec::Root.parse(parse_instance_initial_xml) }
5
+
6
+ let(:parse_instance_initial_xml) do
7
+ %(<root attr1="initial">
8
+ <item attr1="initial">
9
+ <description>initial</description>
10
+ <subitem attr1="initial">
11
+ <name>initial</name>
12
+ </subitem>
13
+ <subitem attr1="initial">
14
+ <name>initial</name>
15
+ </subitem>
16
+ </item>
17
+ <item attr1="initial">
18
+ <description>initial</description>
19
+ <subitem attr1="initial">
20
+ <name>initial</name>
21
+ </subitem>
22
+ <subitem attr1="initial">
23
+ <name>initial</name>
24
+ </subitem>
25
+ </item>
26
+ </root>)
27
+ end
28
+
29
+ let(:parse_instance_updated_xml) do
30
+ %(<root attr1="updated">
31
+ <item attr1="updated">
32
+ <description>updated</description>
33
+ <subitem attr1="updated">
34
+ <name>updated</name>
35
+ </subitem>
36
+ <subitem attr1="updated">
37
+ <name>updated</name>
38
+ </subitem>
39
+ </item>
40
+ <item attr1="updated">
41
+ <description>updated</description>
42
+ <subitem attr1="updated">
43
+ <name>updated</name>
44
+ </subitem>
45
+ <subitem attr1="updated">
46
+ <name>updated</name>
47
+ </subitem>
48
+ </item>
49
+ </root>)
50
+ end
51
+
52
+ module ParseInstanceSpec
53
+ class SubItem
54
+ include HappyMapper
55
+ tag 'subitem'
56
+ attribute :attr1, String
57
+ element :name, String
58
+ end
59
+ class Item
60
+ include HappyMapper
61
+ tag 'item'
62
+ attribute :attr1, String
63
+ element :description, String
64
+ has_many :sub_items, SubItem
65
+ end
66
+ class Root
67
+ include HappyMapper
68
+ tag 'root'
69
+ attribute :attr1, String
70
+ has_many :items, Item
71
+ end
72
+ end
73
+
74
+ def item_is_correctly_defined(item, value = 'initial')
75
+ expect(item.attr1).to eq value
76
+ expect(item.description).to eq value
77
+ expect(item.sub_items[0].attr1).to eq value
78
+ expect(item.sub_items[0].name).to eq value
79
+ expect(item.sub_items[1].attr1).to eq value
80
+ expect(item.sub_items[1].name).to eq value
81
+ end
82
+
83
+ it 'initial values are correct' do
84
+ subject.attr1.should == 'initial'
85
+ item_is_correctly_defined(subject.items[0])
86
+ item_is_correctly_defined(subject.items[1])
87
+ end
88
+
89
+ describe '.parse', 'specifying an existing object to update' do
90
+ it 'all fields are correct' do
91
+ ParseInstanceSpec::Root.parse(parse_instance_updated_xml, update: subject)
92
+ expect(subject.attr1).to eq 'updated'
93
+
94
+ item_is_correctly_defined(subject.items[0], 'updated')
95
+ item_is_correctly_defined(subject.items[1], 'updated')
96
+ end
97
+ end
98
+
99
+ describe '#parse' do
100
+ it 'all fields are correct' do
101
+ subject.parse(parse_instance_updated_xml)
102
+ expect(subject.attr1).to eq 'updated'
103
+
104
+ item_is_correctly_defined(subject.items[0], 'updated')
105
+ item_is_correctly_defined(subject.items[1], 'updated')
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,8 @@
1
+ require 'rspec'
2
+ require 'rspec/collection_matchers'
3
+
4
+ require 'happymapper'
5
+
6
+ def fixture_file(filename)
7
+ File.read(File.dirname(__FILE__) + "/fixtures/#{filename}")
8
+ end
@@ -0,0 +1,197 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Saving #to_xml' do
4
+ module ToXML
5
+ class Address
6
+ include HappyMapper
7
+
8
+ tag 'address'
9
+
10
+ attribute :location, String, on_save: :when_saving_location
11
+
12
+ element :street, String
13
+ element :postcode, String
14
+ element :city, String
15
+
16
+ element :housenumber, String
17
+
18
+ attribute :modified, Boolean, read_only: true
19
+ element :temporary, Boolean, read_only: true
20
+ #
21
+ # to_xml will default to the attr_accessor method and not the attribute,
22
+ # allowing for that to be overwritten
23
+ #
24
+ def housenumber
25
+ "[#{@housenumber}]"
26
+ end
27
+
28
+ def when_saving_location(loc)
29
+ loc + '-live'
30
+ end
31
+
32
+ #
33
+ # Write a empty element even if this is not specified
34
+ #
35
+ element :description, String, state_when_nil: true
36
+
37
+ #
38
+ # Perform the on_save operation when saving
39
+ #
40
+ has_one :date_created, Time, on_save: ->(time) { DateTime.parse(time).strftime('%T %D') if time }
41
+
42
+ #
43
+ # Execute the method with the same name
44
+
45
+ #
46
+ # Write multiple elements and call on_save when saving
47
+ #
48
+ has_many :dates_updated, Time, on_save: lambda {|times|
49
+ times.compact.map { |time| DateTime.parse(time).strftime('%T %D') } if times
50
+ }
51
+
52
+ #
53
+ # Class composition
54
+ #
55
+ element :country, 'Country', tag: 'country'
56
+
57
+ attribute :occupied, Boolean
58
+
59
+ def initialize(parameters)
60
+ parameters.each_pair do |property, value|
61
+ send("#{property}=", value) if respond_to?("#{property}=")
62
+ end
63
+ @modified = @temporary = true
64
+ end
65
+ end
66
+
67
+ #
68
+ # Country is composed above the in Address class. Here is a demonstration
69
+ # of how to_xml will handle class composition as well as utilizing the tag
70
+ # value.
71
+ #
72
+ class Country
73
+ include HappyMapper
74
+
75
+ attribute :code, String, tag: 'countryCode'
76
+ has_one :name, String, tag: 'countryName'
77
+ has_one :description, 'Description', tag: 'description'
78
+
79
+ #
80
+ # This inner-class here is to demonstrate saving a text node
81
+ # and optional attributes
82
+ #
83
+ class Description
84
+ include HappyMapper
85
+ content :description, String
86
+ attribute :category, String, tag: 'category'
87
+ attribute :rating, String, tag: 'rating', state_when_nil: true
88
+
89
+ def initialize(desc)
90
+ @description = desc
91
+ end
92
+ end
93
+
94
+ def initialize(parameters)
95
+ parameters.each_pair do |property, value|
96
+ send("#{property}=", value) if respond_to?("#{property}=")
97
+ end
98
+ end
99
+ end
100
+ end
101
+
102
+ let(:subject) do
103
+ address = ToXML::Address.new 'street' => 'Mockingbird Lane',
104
+ 'location' => 'Home',
105
+ 'housenumber' => '1313',
106
+ 'postcode' => '98103',
107
+ 'city' => 'Seattle',
108
+ 'country' => ToXML::Country.new(name: 'USA', code: 'us', empty_code: nil,
109
+ description: ToXML::Country::Description.new('A lovely country')),
110
+ 'date_created' => '2011-01-01 15:00:00',
111
+ 'occupied' => false
112
+
113
+ address.dates_updated = ['2011-01-01 16:01:00', '2011-01-02 11:30:01']
114
+
115
+ Nokogiri::XML(address.to_xml).root
116
+ end
117
+
118
+ it 'saves elements' do
119
+ elements = { 'street' => 'Mockingbird Lane', 'postcode' => '98103', 'city' => 'Seattle' }
120
+ elements.each_pair do |property, value|
121
+ expect(subject.xpath("#{property}").text).to eq value
122
+ end
123
+ end
124
+
125
+ it 'saves attributes' do
126
+ expect(subject.xpath('@location').text).to eq 'Home-live'
127
+ end
128
+
129
+ it 'saves attributes that are Boolean and have a value of false' do
130
+ expect(subject.xpath('@occupied').text).to eq 'false'
131
+ end
132
+
133
+ context "when an element has a 'read_only' parameter" do
134
+ it 'does not save elements' do
135
+ expect(subject.xpath('temporary')).to be_empty
136
+ end
137
+ end
138
+
139
+ context "when an attribute has a 'read_only' parameter" do
140
+ it 'does not save attributes' do
141
+ expect(subject.xpath('@modified')).to be_empty
142
+ end
143
+ end
144
+
145
+ context "when an element has a 'state_when_nil' parameter" do
146
+ it 'saves an empty element' do
147
+ expect(subject.xpath('description').text).to eq ''
148
+ end
149
+ end
150
+
151
+ context "when an element has a 'on_save' parameter" do
152
+ context 'with a symbol which represents a function' do
153
+ it 'saves the element with the result of the function' do
154
+ expect(subject.xpath('housenumber').text).to eq '[1313]'
155
+ end
156
+ end
157
+
158
+ context 'with a lambda' do
159
+ it 'saves the result of the lambda' do
160
+ expect(subject.xpath('date_created').text).to eq '15:00:00 01/01/11'
161
+ end
162
+ end
163
+ end
164
+
165
+ context "when a has_many has a 'on_save' parameter" do
166
+ context 'with a lambda' do
167
+ it 'saves the results' do
168
+ dates_updated = subject.xpath('dates_updated')
169
+ expect(dates_updated.length).to eq 2
170
+ expect(dates_updated.first.text).to eq '16:01:00 01/01/11'
171
+ expect(dates_updated.last.text).to eq '11:30:01 01/02/11'
172
+ end
173
+ end
174
+ end
175
+
176
+ context "when an attribute has a 'on_save' parameter" do
177
+ context 'with a symbol which represents a function' do
178
+ it 'saves the result' do
179
+ expect(subject.xpath('@location').text).to eq 'Home-live'
180
+ end
181
+ end
182
+ end
183
+
184
+ context 'when an element type is a HappyMapper subclass' do
185
+ it 'saves attributes' do
186
+ expect(subject.xpath('country/@countryCode').text).to eq 'us'
187
+ end
188
+
189
+ it 'saves elements' do
190
+ expect(subject.xpath('country/countryName').text).to eq 'USA'
191
+ end
192
+
193
+ it 'saves elements' do
194
+ expect(subject.xpath('country/description').text).to eq 'A lovely country'
195
+ end
196
+ end
197
+ end