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