frodata 0.9.1

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 (128) hide show
  1. checksums.yaml +7 -0
  2. data/.autotest +2 -0
  3. data/.gitignore +24 -0
  4. data/.rspec +2 -0
  5. data/.ruby-gemset +1 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +75 -0
  8. data/CHANGELOG.md +150 -0
  9. data/Gemfile +4 -0
  10. data/LICENSE.txt +23 -0
  11. data/README.md +427 -0
  12. data/Rakefile +7 -0
  13. data/TODO.md +55 -0
  14. data/frodata.gemspec +34 -0
  15. data/lib/frodata.rb +36 -0
  16. data/lib/frodata/entity.rb +332 -0
  17. data/lib/frodata/entity_container.rb +75 -0
  18. data/lib/frodata/entity_set.rb +161 -0
  19. data/lib/frodata/errors.rb +68 -0
  20. data/lib/frodata/navigation_property.rb +29 -0
  21. data/lib/frodata/navigation_property/proxy.rb +80 -0
  22. data/lib/frodata/properties.rb +32 -0
  23. data/lib/frodata/properties/binary.rb +50 -0
  24. data/lib/frodata/properties/boolean.rb +37 -0
  25. data/lib/frodata/properties/collection.rb +50 -0
  26. data/lib/frodata/properties/complex.rb +114 -0
  27. data/lib/frodata/properties/date.rb +27 -0
  28. data/lib/frodata/properties/date_time.rb +83 -0
  29. data/lib/frodata/properties/date_time_offset.rb +17 -0
  30. data/lib/frodata/properties/decimal.rb +50 -0
  31. data/lib/frodata/properties/enum.rb +62 -0
  32. data/lib/frodata/properties/float.rb +67 -0
  33. data/lib/frodata/properties/geography.rb +13 -0
  34. data/lib/frodata/properties/geography/base.rb +162 -0
  35. data/lib/frodata/properties/geography/line_string.rb +33 -0
  36. data/lib/frodata/properties/geography/point.rb +31 -0
  37. data/lib/frodata/properties/geography/polygon.rb +38 -0
  38. data/lib/frodata/properties/guid.rb +17 -0
  39. data/lib/frodata/properties/integer.rb +107 -0
  40. data/lib/frodata/properties/number.rb +14 -0
  41. data/lib/frodata/properties/string.rb +72 -0
  42. data/lib/frodata/properties/time.rb +40 -0
  43. data/lib/frodata/properties/time_of_day.rb +27 -0
  44. data/lib/frodata/property.rb +139 -0
  45. data/lib/frodata/property_registry.rb +41 -0
  46. data/lib/frodata/query.rb +233 -0
  47. data/lib/frodata/query/criteria.rb +92 -0
  48. data/lib/frodata/query/criteria/comparison_operators.rb +49 -0
  49. data/lib/frodata/query/criteria/date_functions.rb +61 -0
  50. data/lib/frodata/query/criteria/geography_functions.rb +21 -0
  51. data/lib/frodata/query/criteria/lambda_operators.rb +27 -0
  52. data/lib/frodata/query/criteria/string_functions.rb +40 -0
  53. data/lib/frodata/query/in_batches.rb +58 -0
  54. data/lib/frodata/railtie.rb +19 -0
  55. data/lib/frodata/schema.rb +155 -0
  56. data/lib/frodata/schema/complex_type.rb +79 -0
  57. data/lib/frodata/schema/enum_type.rb +95 -0
  58. data/lib/frodata/service.rb +254 -0
  59. data/lib/frodata/service/request.rb +85 -0
  60. data/lib/frodata/service/response.rb +162 -0
  61. data/lib/frodata/service/response/atom.rb +40 -0
  62. data/lib/frodata/service/response/json.rb +41 -0
  63. data/lib/frodata/service/response/plain.rb +36 -0
  64. data/lib/frodata/service/response/xml.rb +40 -0
  65. data/lib/frodata/service_registry.rb +52 -0
  66. data/lib/frodata/version.rb +3 -0
  67. data/spec/fixtures/files/entity_to_xml.xml +17 -0
  68. data/spec/fixtures/files/error.xml +5 -0
  69. data/spec/fixtures/files/metadata.xml +150 -0
  70. data/spec/fixtures/files/product_0.json +10 -0
  71. data/spec/fixtures/files/product_0.xml +28 -0
  72. data/spec/fixtures/files/products.json +106 -0
  73. data/spec/fixtures/files/products.xml +308 -0
  74. data/spec/fixtures/files/supplier_0.json +26 -0
  75. data/spec/fixtures/files/supplier_0.xml +32 -0
  76. data/spec/fixtures/vcr_cassettes/entity_set_specs.yml +1635 -0
  77. data/spec/fixtures/vcr_cassettes/entity_set_specs/bad_entry.yml +183 -0
  78. data/spec/fixtures/vcr_cassettes/entity_set_specs/existing_entry.yml +256 -0
  79. data/spec/fixtures/vcr_cassettes/entity_set_specs/new_entry.yml +185 -0
  80. data/spec/fixtures/vcr_cassettes/entity_specs.yml +285 -0
  81. data/spec/fixtures/vcr_cassettes/navigation_property_proxy_specs.yml +346 -0
  82. data/spec/fixtures/vcr_cassettes/query/result_specs.yml +189 -0
  83. data/spec/fixtures/vcr_cassettes/query_specs.yml +1060 -0
  84. data/spec/fixtures/vcr_cassettes/schema/complex_type_specs.yml +127 -0
  85. data/spec/fixtures/vcr_cassettes/service/request_specs.yml +193 -0
  86. data/spec/fixtures/vcr_cassettes/service_registry_specs.yml +129 -0
  87. data/spec/fixtures/vcr_cassettes/service_specs.yml +127 -0
  88. data/spec/fixtures/vcr_cassettes/usage_example_specs.yml +1330 -0
  89. data/spec/frodata/entity/shared_examples.rb +82 -0
  90. data/spec/frodata/entity_container_spec.rb +38 -0
  91. data/spec/frodata/entity_set_spec.rb +168 -0
  92. data/spec/frodata/entity_spec.rb +151 -0
  93. data/spec/frodata/errors_spec.rb +48 -0
  94. data/spec/frodata/navigation_property/proxy_spec.rb +44 -0
  95. data/spec/frodata/navigation_property_spec.rb +55 -0
  96. data/spec/frodata/properties/binary_spec.rb +50 -0
  97. data/spec/frodata/properties/boolean_spec.rb +72 -0
  98. data/spec/frodata/properties/collection_spec.rb +44 -0
  99. data/spec/frodata/properties/date_spec.rb +23 -0
  100. data/spec/frodata/properties/date_time_offset_spec.rb +30 -0
  101. data/spec/frodata/properties/date_time_spec.rb +23 -0
  102. data/spec/frodata/properties/decimal_spec.rb +51 -0
  103. data/spec/frodata/properties/float_spec.rb +45 -0
  104. data/spec/frodata/properties/geography/line_string_spec.rb +33 -0
  105. data/spec/frodata/properties/geography/point_spec.rb +29 -0
  106. data/spec/frodata/properties/geography/polygon_spec.rb +55 -0
  107. data/spec/frodata/properties/geography/shared_examples.rb +72 -0
  108. data/spec/frodata/properties/guid_spec.rb +17 -0
  109. data/spec/frodata/properties/integer_spec.rb +58 -0
  110. data/spec/frodata/properties/string_spec.rb +46 -0
  111. data/spec/frodata/properties/time_of_day_spec.rb +23 -0
  112. data/spec/frodata/properties/time_spec.rb +15 -0
  113. data/spec/frodata/property_registry_spec.rb +16 -0
  114. data/spec/frodata/property_spec.rb +71 -0
  115. data/spec/frodata/query/criteria_spec.rb +229 -0
  116. data/spec/frodata/query_spec.rb +199 -0
  117. data/spec/frodata/schema/complex_type_spec.rb +96 -0
  118. data/spec/frodata/schema/enum_type_spec.rb +112 -0
  119. data/spec/frodata/schema_spec.rb +97 -0
  120. data/spec/frodata/service/request_spec.rb +49 -0
  121. data/spec/frodata/service/response_spec.rb +85 -0
  122. data/spec/frodata/service_registry_spec.rb +18 -0
  123. data/spec/frodata/service_spec.rb +191 -0
  124. data/spec/frodata/usage_example_spec.rb +188 -0
  125. data/spec/spec_helper.rb +32 -0
  126. data/spec/support/coverage.rb +2 -0
  127. data/spec/support/vcr.rb +9 -0
  128. metadata +401 -0
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
7
+
data/TODO.md ADDED
@@ -0,0 +1,55 @@
1
+ # OData V4 To-Do
2
+
3
+ This is a non-complete list of things that need to be done in order to achieve OData V4 compatibility. It will be updated regularly to keep track with current development.
4
+
5
+ ## Tasks
6
+
7
+ [x] `DataServiceVersion` headers changes to `OData-Version`
8
+ [x] Atom: update namespace URIs
9
+ [x] Implement JSON data format
10
+ [x] with batch processing
11
+ [ ] Implement missing/new OData V4 types
12
+ [x] `Edm.Date` (V4/RESO)
13
+ [ ] `Edm.Duration` (V4)
14
+ [x] `Edm.TimeOfDay` (V4/RESO)
15
+ [x] `Edm.EnumType` (V4/RESO)
16
+ [ ] `Edm.Geography` subtypes (RESO)
17
+ [x] `Edm.GeographyPoint`
18
+ [ ] `Edm.GeographyMultiPoint`
19
+ [x] `Edm.GeographyLineString`
20
+ [ ] `Edm.GeographyMultiLineString`
21
+ [x] `Edm.GeographyPolygon` (see note below)
22
+ [ ] Support for holes
23
+ [ ] Support for other serialization formats
24
+ [ ] `Edm.GeopgrahyMultiPolygon`
25
+
26
+ ##### NOTE
27
+
28
+ Due to the lack of library support for GeoXML/GML in Ruby, Geography support is somewhat limited. For instance, [there are more than 3 different ways to represent a polygon in GML][gml-madness], all of which are equivalent and interchangeable. However, due to the lack of GML libraries, we currently only support a single serialization format (`<gml:LinearRing>` with `<gml:pos>` elements, see [polygon_spec.rb][polygon_spec]).
29
+
30
+ [gml-madness]: http://erouault.blogspot.com/2014/04/gml-madness.html
31
+ [polygon_spec]: spec/odata/v4/properties/geography/polygon_spec.rb
32
+
33
+ [x] Changes to `NavigationProperty`
34
+ [x] No more associations (but we probably still need a proxy class)
35
+ [x] New `Type` property
36
+ [x] New `Nullable` property
37
+ [x] New `Partner` property
38
+ [ ] New `ContainsTarget` property
39
+
40
+ [ ] Changes to querying
41
+ [x] `$count=true` replaces `$inlinecount=allpages`
42
+ [x] New `$search` param for fulltext search
43
+ [x] String functions
44
+ [x] Date/time functions
45
+ [x] Geospatial functions
46
+ [x] [Lambda operators][1]
47
+
48
+ [ ] Logging
49
+
50
+ [1]: http://docs.oasis-open.org/odata/odata/v4.0/errata02/os/complete/part2-url-conventions/odata-v4.0-errata02-os-part2-url-conventions-complete.html#_Toc406398149
51
+
52
+ ## Questions / Thoughts
53
+
54
+ [ ] Use standard JSON parser or OJ (or offer choice?)
55
+ [x] Continue to support XML data format (JSON is recommended for V4)? -> We'll support both, ATOM first, JSON to be added later.
data/frodata.gemspec ADDED
@@ -0,0 +1,34 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'frodata/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'frodata'
8
+ spec.version = FrOData::VERSION
9
+ spec.authors = ['Christoph Wagner', 'James Thompson']
10
+ spec.email = %w{christoph@wrstudios.com james@plainprograms.com}
11
+ spec.summary = %q{Simple OData library}
12
+ spec.description = %q{Provides a simple interface for working with OData V4 APIs.}
13
+ spec.homepage = 'https://github.com/wrstudios/frodata'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = %w{lib}
20
+
21
+ spec.add_development_dependency 'bundler', '~> 1.6'
22
+ spec.add_development_dependency 'rake', '~> 0'
23
+ spec.add_development_dependency 'simplecov', '~> 0.15'
24
+ spec.add_development_dependency 'rspec', '~> 3.7'
25
+ spec.add_development_dependency 'rspec-autotest', '~> 1.0'
26
+ spec.add_development_dependency 'autotest', '~> 4.4'
27
+ spec.add_development_dependency 'vcr', '~> 4.0'
28
+ spec.add_development_dependency 'timecop', '~> 0.9'
29
+ spec.add_development_dependency 'equivalent-xml', '~> 0.6'
30
+
31
+ spec.add_dependency 'nokogiri', '~> 1.8'
32
+ spec.add_dependency 'faraday', '~> 0.15'
33
+ spec.add_dependency 'andand', '~> 1.3'
34
+ end
data/lib/frodata.rb ADDED
@@ -0,0 +1,36 @@
1
+ require 'uri'
2
+ require 'date'
3
+ require 'time'
4
+ require 'bigdecimal'
5
+ require 'nokogiri'
6
+ require 'faraday'
7
+ require 'logger'
8
+ require 'andand'
9
+ require 'json'
10
+
11
+ # require 'active_support'
12
+ # require 'active_support/core_ext'
13
+ # require 'active_support/concern'
14
+
15
+ require 'frodata/version'
16
+ require 'frodata/errors'
17
+ require 'frodata/property_registry'
18
+ require 'frodata/property'
19
+ require 'frodata/properties'
20
+ require 'frodata/navigation_property'
21
+ require 'frodata/entity'
22
+ require 'frodata/entity_container'
23
+ require 'frodata/entity_set'
24
+ require 'frodata/query'
25
+ require 'frodata/schema'
26
+ require 'frodata/service'
27
+ require 'frodata/service_registry'
28
+
29
+ require 'frodata/railtie' if defined?(::Rails)
30
+
31
+ # The FrOData gem provides a convenient way to interact with OData V4 services from
32
+ # Ruby. Please look to the {file:README.md README} for how to get started using
33
+ # the FrOData gem.
34
+ module FrOData
35
+ # Your code goes here...
36
+ end
@@ -0,0 +1,332 @@
1
+ module FrOData
2
+ # An FrOData::Entity represents a single record returned by the service. All
3
+ # Entities have a type and belong to a specific namespace. They are written
4
+ # back to the service via the EntitySet they came from. FrOData::Entity
5
+ # instances should not be instantiated directly; instead, they should either
6
+ # be read or instantiated from their respective FrOData::EntitySet.
7
+ class Entity
8
+ # The Entity type name
9
+ attr_reader :type
10
+ # The FrOData::Service's identifying name
11
+ attr_reader :service_name
12
+ # The entity set this entity belongs to
13
+ attr_reader :entity_set
14
+ # List of errors on entity
15
+ attr_reader :errors
16
+
17
+ PROPERTY_NOT_LOADED = :not_loaded
18
+
19
+ XML_NAMESPACES = {
20
+ 'xmlns' => 'http://www.w3.org/2005/Atom',
21
+ 'xmlns:data' => 'http://docs.oasis-open.org/odata/ns/data',
22
+ 'xmlns:metadata' => 'http://docs.oasis-open.org/odata/ns/metadata',
23
+ 'xmlns:georss' => 'http://www.georss.org/georss',
24
+ 'xmlns:gml' => 'http://www.opengis.net/gml',
25
+ }.freeze
26
+
27
+ # Initializes a bare Entity
28
+ # @param options [Hash]
29
+ def initialize(options = {})
30
+ @id = options[:id]
31
+ @type = options[:type]
32
+ @service_name = options[:service_name]
33
+ @entity_set = options[:entity_set]
34
+ @context = options[:context]
35
+ @links = options[:links]
36
+ @errors = []
37
+ end
38
+
39
+ def namespace
40
+ @namespace ||= type.rpartition('.').first
41
+ end
42
+
43
+ # Returns name of Entity from Service specified type.
44
+ # @return [String]
45
+ def name
46
+ @name ||= type.split('.').last
47
+ end
48
+
49
+ # Returns context URL for this entity
50
+ # @return [String]
51
+ def context
52
+ @context ||= context_url
53
+ end
54
+
55
+ # Get property value
56
+ # @param property_name [to_s]
57
+ # @return [*]
58
+ def [](property_name)
59
+ if get_property(property_name).is_a?(::FrOData::Properties::Complex)
60
+ get_property(property_name)
61
+ else
62
+ get_property(property_name).value
63
+ end
64
+ end
65
+
66
+ # Set property value
67
+ # @param property_name [to_s]
68
+ # @param value [*]
69
+ def []=(property_name, value)
70
+ get_property(property_name).value = value
71
+ end
72
+
73
+ def get_property(property_name)
74
+ prop_name = property_name.to_s
75
+ # Property is lazy loaded
76
+ if properties_xml_value.has_key?(prop_name)
77
+ property = instantiate_property(prop_name, properties_xml_value[prop_name])
78
+ set_property(prop_name, property.dup)
79
+ properties_xml_value.delete(prop_name)
80
+ end
81
+
82
+ if properties.has_key? prop_name
83
+ properties[prop_name]
84
+ elsif navigation_properties.has_key? prop_name
85
+ navigation_properties[prop_name]
86
+ else
87
+ raise ArgumentError, "Unknown property: #{property_name}"
88
+ end
89
+ end
90
+
91
+ def property_names
92
+ [
93
+ @properties_xml_value.andand.keys,
94
+ @properties.andand.keys
95
+ ].compact.flatten
96
+ end
97
+
98
+ def navigation_property_names
99
+ navigation_properties.keys
100
+ end
101
+
102
+ def navigation_properties
103
+ @navigation_properties ||= links.keys.map do |nav_name|
104
+ [
105
+ nav_name,
106
+ FrOData::NavigationProperty::Proxy.new(self, nav_name)
107
+ ]
108
+ end.to_h
109
+ end
110
+
111
+ # Links to other FrOData entitites
112
+ # @return [Hash]
113
+ def links
114
+ @links ||= schema.navigation_properties[name].map do |nav_name, details|
115
+ [
116
+ nav_name,
117
+ { type: details.nav_type, href: "#{id}/#{nav_name}" }
118
+ ]
119
+ end.to_h
120
+ end
121
+
122
+ # Create Entity with provided properties and options.
123
+ # @param new_properties [Hash]
124
+ # @param options [Hash]
125
+ # @param [FrOData::Entity]
126
+ def self.with_properties(new_properties = {}, options = {})
127
+ entity = FrOData::Entity.new(options)
128
+ entity.instance_eval do
129
+ service.properties_for_entity(type).each do |property_name, instance|
130
+ set_property(property_name, instance)
131
+ end
132
+
133
+ new_properties.each do |property_name, property_value|
134
+ self[property_name] = property_value
135
+ end
136
+ end
137
+ entity
138
+ end
139
+
140
+ # Create Entity from JSON document with provided options.
141
+ # @param json [Hash|to_s]
142
+ # @param options [Hash]
143
+ # @return [FrOData::Entity]
144
+ def self.from_json(json, options = {})
145
+ return nil if json.nil?
146
+ json = JSON.parse(json.to_s) unless json.is_a?(Hash)
147
+ metadata = extract_metadata(json)
148
+ options.merge!(context: metadata['@odata.context'])
149
+ entity = with_properties(json, options)
150
+ process_metadata(entity, metadata)
151
+ entity
152
+ end
153
+
154
+ # Create Entity from XML document with provided options.
155
+ # @param xml_doc [Nokogiri::XML]
156
+ # @param options [Hash]
157
+ # @return [FrOData::Entity]
158
+ def self.from_xml(xml_doc, options = {})
159
+ return nil if xml_doc.nil?
160
+ entity = FrOData::Entity.new(options)
161
+ process_properties(entity, xml_doc)
162
+ process_links(entity, xml_doc)
163
+ entity
164
+ end
165
+
166
+ # Converts Entity to its XML representation.
167
+ # @return [String]
168
+ def to_xml
169
+ namespaces = XML_NAMESPACES.merge('xml:base' => service.service_url)
170
+ builder = Nokogiri::XML::Builder.new do |xml|
171
+ xml.entry(namespaces) do
172
+ xml.category(term: type,
173
+ scheme: 'http://docs.oasis-open.org/odata/ns/scheme')
174
+ xml.author { xml.name }
175
+
176
+ xml.content(type: 'application/xml') do
177
+ xml['metadata'].properties do
178
+ property_names.each do |name|
179
+ next if name == primary_key
180
+ get_property(name).to_xml(xml)
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end
186
+ builder.to_xml
187
+ end
188
+
189
+ # Converts Entity to its JSON representation.
190
+ # @return [String]
191
+ def to_json
192
+ # TODO: add @odata.context
193
+ to_hash.to_json
194
+ end
195
+
196
+ # Converts Entity to a hash.
197
+ # @return [Hash]
198
+ def to_hash
199
+ property_names.map do |name|
200
+ [name, get_property(name).json_value]
201
+ end.to_h
202
+ end
203
+
204
+ # Returns the canonical URL for this entity
205
+ # @return [String]
206
+ def id
207
+ @id ||= lambda {
208
+ entity_set = self.entity_set.andand.name
209
+ entity_set ||= context.split('#').last.split('/').first
210
+ "#{entity_set}(#{self[primary_key]})"
211
+ }.call
212
+ end
213
+
214
+ # Returns the primary key for the Entity.
215
+ # @return [String]
216
+ def primary_key
217
+ schema.primary_key_for(name)
218
+ end
219
+
220
+ def is_new?
221
+ self[primary_key].nil?
222
+ end
223
+
224
+ def any_errors?
225
+ !errors.empty?
226
+ end
227
+
228
+ def service
229
+ @service ||= FrOData::ServiceRegistry[service_name]
230
+ end
231
+
232
+ def schema
233
+ @schema ||= service.schemas[namespace]
234
+ end
235
+
236
+ private
237
+
238
+ def instantiate_property(property_name, value_xml)
239
+ prop_type = schema.get_property_type(name, property_name)
240
+ prop_type, value_type = prop_type.split(/\(|\)/)
241
+
242
+ if prop_type == 'Collection'
243
+ klass = ::FrOData::Properties::Collection
244
+ options = { value_type: value_type }
245
+ else
246
+ klass = ::FrOData::PropertyRegistry[prop_type]
247
+ options = {}
248
+ end
249
+
250
+ if klass.nil?
251
+ raise RuntimeError, "Unknown property type: #{prop_type}"
252
+ else
253
+ klass.from_xml(value_xml, options.merge(service: service))
254
+ end
255
+ end
256
+
257
+ def properties
258
+ @properties ||= {}
259
+ end
260
+
261
+ def properties_xml_value
262
+ @properties_xml_value ||= {}
263
+ end
264
+
265
+ # Computes the entity's canonical context URL
266
+ def context_url
267
+ "#{service.service_url}/$metadata##{entity_set.name}/$entity"
268
+ end
269
+
270
+ def set_property(name, property)
271
+ properties[name.to_s] = property
272
+ end
273
+
274
+ # Instantiating properties takes time, so we can lazy load properties by passing xml_value and lookup when needed
275
+ def set_property_lazy_load(name, xml_value )
276
+ properties_xml_value[name.to_s] = xml_value
277
+ end
278
+
279
+ def self.process_properties(entity, xml_doc)
280
+ entity.instance_eval do
281
+ unless instance_variable_get(:@context)
282
+ context = xml_doc.xpath('/entry').first.andand['context']
283
+ instance_variable_set(:@context, context)
284
+ end
285
+
286
+ xml_doc.xpath('./content/properties/*').each do |property_xml|
287
+ # Doing lazy loading here because instantiating each object takes a long time
288
+ set_property_lazy_load(property_xml.name, property_xml)
289
+ end
290
+ end
291
+ end
292
+
293
+ def self.process_links(entity, xml_doc)
294
+ entity.instance_eval do
295
+ new_links = instance_variable_get(:@links) || {}
296
+ schema.navigation_properties[name].each do |nav_name, details|
297
+ xml_doc.xpath("./link[@title='#{nav_name}']").each do |node|
298
+ next if node.attributes['type'].nil?
299
+ next unless node.attributes['type'].value =~ /^application\/atom\+xml;type=(feed|entry)$/i
300
+ link_type = node.attributes['type'].value =~ /type=entry$/i ? :entity : :collection
301
+ new_links[nav_name] = {
302
+ type: link_type,
303
+ href: node.attributes['href'].value
304
+ }
305
+ end
306
+ end
307
+ instance_variable_set(:@links, new_links)
308
+ end
309
+ end
310
+
311
+ def self.extract_metadata(json)
312
+ metadata = json.select { |key, val| key =~ /@odata/ }
313
+ json.delete_if { |key, val| key =~ /@odata/ }
314
+ metadata
315
+ end
316
+
317
+ def self.process_metadata(entity, metadata)
318
+ entity.instance_eval do
319
+ new_links = instance_variable_get(:@links) || {}
320
+ schema.navigation_properties[name].each do |nav_name, details|
321
+ href = metadata["#{nav_name}@odata.navigationLink"]
322
+ next if href.nil?
323
+ new_links[nav_name] = {
324
+ type: details.nav_type,
325
+ href: href
326
+ }
327
+ end
328
+ instance_variable_set(:@links, new_links) unless new_links.empty?
329
+ end
330
+ end
331
+ end
332
+ end