frodata 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
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