ruby_odata 0.0.7 → 0.0.8

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.
@@ -1,348 +1,368 @@
1
1
  require 'logger'
2
+ require 'base64'
2
3
 
3
4
  module OData
4
-
5
+
5
6
  class Service
6
- attr_reader :classes
7
- # Creates a new instance of the Service class
8
- #
9
- # ==== Required Attributes
10
- # - service_uri: The root URI of the OData service
11
- def initialize(service_uri)
12
- @uri = service_uri
13
- @collections = get_collections
14
- @save_operations = []
15
- build_classes
16
- end
17
-
18
- # Handles the dynamic AddTo<EntityName> methods as well as the collections on the service
19
- def method_missing(name, *args)
20
- # Queries
21
- if @collections.include?(name.to_s)
22
- root = "/#{name.to_s.camelize}"
23
- root << "(#{args.join(',')})" unless args.empty?
24
- @query = QueryBuilder.new(root)
25
- return @query
26
- # Adds
27
- elsif name.to_s =~ /^AddTo(.*)/
28
- type = $1
29
- if @collections.include?(type)
30
- @save_operations << Operation.new("Add", $1, args[0])
31
- else
32
- super
33
- end
34
- else
35
- super
36
- end
37
-
38
- end
39
-
40
- # Queues an object for deletion. To actually remove it from the server, you must call save_changes as well.
41
- #
42
- # ==== Required Attributes
43
- # - obj: The object to mark for deletion
44
- #
45
- # Note: This method will throw an exception if the +obj+ isn't a tracked entity
46
- def delete_object(obj)
47
- type = obj.class.to_s
48
- if obj.respond_to?(:__metadata) && !obj.send(:__metadata).nil?
49
- @save_operations << Operation.new("Delete", type, obj)
50
- else
51
- raise "You cannot delete a non-tracked entity"
52
- end
53
- end
54
-
55
- # Queues an object for update. To actually update it on the server, you must call save_changes as well.
56
- #
57
- # ==== Required Attributes
58
- # - obj: The object to queue for update
59
- #
60
- # Note: This method will throw an exception if the +obj+ isn't a tracked entity
61
- def update_object(obj)
62
- type = obj.class.to_s
63
- if obj.respond_to?(:__metadata) && !obj.send(:__metadata).nil?
64
- @save_operations << Operation.new("Update", type, obj)
65
- else
66
- raise "You cannot update a non-tracked entity"
67
- end
68
- end
69
-
70
- # Performs save operations (Create/Update/Delete) against the server
71
- def save_changes
72
- return nil if @save_operations.empty?
73
-
74
- result = nil
75
-
76
- if @save_operations.length == 1
77
- result = single_save(@save_operations[0])
78
- else
79
- result = batch_save(@save_operations)
80
- end
81
-
82
- # TODO: We should probably perform a check here
83
- # to make sure everything worked before clearing it out
84
- @save_operations.clear
85
-
86
- return result
87
- end
88
-
89
- # Performs query operations (Read) against the server
90
- def execute
91
- result = RestClient.get build_query_uri
92
- build_classes_from_result(result)
93
- end
94
-
95
- # Overridden to identify methods handled by method_missing
96
- def respond_to?(method)
97
- if @collections.include?(method.to_s)
98
- return true
99
- # Adds
100
- elsif method.to_s =~ /^AddTo(.*)/
101
- type = $1
102
- if @collections.include?(type)
103
- return true
104
- else
105
- super
106
- end
107
- else
108
- super
109
- end
110
- end
111
-
112
- private
113
- # Retrieves collections from the main service page
114
- def get_collections
115
- doc = Nokogiri::XML(open(@uri))
116
- collections = doc.xpath("//app:collection", "app" => "http://www.w3.org/2007/app")
117
- collections.collect { |c| c["href"] }
118
- end
119
-
120
- # Build the classes required by the metadata
121
- def build_classes
122
- @classes = Hash.new
123
- doc = Nokogiri::XML(open("#{@uri}/$metadata"))
124
-
125
- # Get the edm namespace
126
- edm_ns = doc.xpath("edmx:Edmx/edmx:DataServices/*", "edmx" => "http://schemas.microsoft.com/ado/2007/06/edmx").first.namespaces['xmlns'].to_s
127
-
128
- # Build complex types first, these will be used for entities
129
- complex_types = doc.xpath("//edm:ComplexType", "edm" => edm_ns) || []
130
- complex_types.each do |c|
131
- name = c['Name']
132
- props = c.xpath(".//edm:Property", "edm" => edm_ns)
133
- methods = props.collect { |p| p['Name'] } # Standard Properties
134
- @classes[name] = ClassBuilder.new(name, methods, []).build unless @classes.keys.include?(name)
135
- end
136
-
137
- entity_types = doc.xpath("//edm:EntityType", "edm" => edm_ns)
138
- entity_types.each do |e|
139
- name = e['Name']
140
- props = e.xpath(".//edm:Property", "edm" => edm_ns)
141
- methods = props.collect { |p| p['Name'] } # Standard Properties
142
- nprops = e.xpath(".//edm:NavigationProperty", "edm" => edm_ns)
143
- nav_props = nprops.collect { |p| p['Name'] } # Standard Properties
144
- @classes[name] = ClassBuilder.new(name, methods, nav_props).build unless @classes.keys.include?(name)
145
- end
146
- end
147
-
148
- # Helper to loop through a result and create an instance for each entity in the results
149
- def build_classes_from_result(result)
150
- doc = Nokogiri::XML(result)
151
- entries = doc.xpath("//atom:entry[not(ancestor::atom:entry)]", "atom" => "http://www.w3.org/2005/Atom")
152
- return entry_to_class(entries[0]) if entries.length == 1
153
-
154
- results = []
155
- entries.each do |entry|
156
- results << entry_to_class(entry)
157
- end
158
- return results
159
- end
160
-
161
- # Converts an XML Entry into a class
162
- def entry_to_class(entry)
163
- # Retrieve the class name from the fully qualified name (the last string after the last dot)
164
- klass_name = entry.xpath("./atom:category/@term", "atom" => "http://www.w3.org/2005/Atom").to_s.split('.')[-1]
165
- return nil if klass_name.empty?
166
-
167
- properties = entry.xpath(".//m:properties/*", { "m" => "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" })
168
-
169
- klass = @classes[klass_name].new
170
-
171
- # Fill metadata
172
- meta_id = entry.xpath("./atom:id", "atom" => "http://www.w3.org/2005/Atom")[0].content
173
- klass.send :__metadata=, { :uri => meta_id }
174
-
175
- # Fill properties
176
- for prop in properties
177
- prop_name = prop.name
178
- klass.send "#{prop_name}=", parse_value(prop)
179
- end
180
-
181
- inline_links = entry.xpath("./atom:link[m:inline]", { "m" => "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata", "atom" => "http://www.w3.org/2005/Atom" })
182
-
183
- for link in inline_links
184
- inline_entries = link.xpath(".//atom:entry", "atom" => "http://www.w3.org/2005/Atom")
185
-
186
- if inline_entries.length == 1
187
- property_name = link.attributes['title'].to_s
188
-
189
- build_inline_class(klass, inline_entries[0], property_name)
7
+ attr_reader :classes
8
+ # Creates a new instance of the Service class
9
+ #
10
+ # ==== Required Attributes
11
+ # - service_uri: The root URI of the OData service
12
+ def initialize(service_uri, options = {})
13
+ @uri = service_uri
14
+ if not options[:username].nil?
15
+ @auth_header = "Basic " + Base64.encode64( options[:username] + ":" + (options[:password] || "") )
16
+ @http_headers = {:Authorization => @auth_header}
17
+ else
18
+ @http_headers = {}
19
+ end
20
+ @collections = get_collections
21
+ @save_operations = []
22
+ build_classes
23
+ end
24
+
25
+ # Handles the dynamic AddTo<EntityName> methods as well as the collections on the service
26
+ def method_missing(name, *args)
27
+ # Queries
28
+ if @collections.include?(name.to_s)
29
+ root = "/#{name.to_s.camelize}"
30
+ root << "(#{args.join(',')})" unless args.empty?
31
+ @query = QueryBuilder.new(root)
32
+ return @query
33
+ # Adds
34
+ elsif name.to_s =~ /^AddTo(.*)/
35
+ type = $1
36
+ if @collections.include?(type)
37
+ @save_operations << Operation.new("Add", $1, args[0])
38
+ else
39
+ super
40
+ end
41
+ else
42
+ super
43
+ end
44
+
45
+ end
46
+
47
+ # Queues an object for deletion. To actually remove it from the server, you must call save_changes as well.
48
+ #
49
+ # ==== Required Attributes
50
+ # - obj: The object to mark for deletion
51
+ #
52
+ # Note: This method will throw an exception if the +obj+ isn't a tracked entity
53
+ def delete_object(obj)
54
+ type = obj.class.to_s
55
+ if obj.respond_to?(:__metadata) && !obj.send(:__metadata).nil?
56
+ @save_operations << Operation.new("Delete", type, obj)
57
+ else
58
+ raise "You cannot delete a non-tracked entity"
59
+ end
60
+ end
61
+
62
+ # Queues an object for update. To actually update it on the server, you must call save_changes as well.
63
+ #
64
+ # ==== Required Attributes
65
+ # - obj: The object to queue for update
66
+ #
67
+ # Note: This method will throw an exception if the +obj+ isn't a tracked entity
68
+ def update_object(obj)
69
+ type = obj.class.to_s
70
+ if obj.respond_to?(:__metadata) && !obj.send(:__metadata).nil?
71
+ @save_operations << Operation.new("Update", type, obj)
72
+ else
73
+ raise "You cannot update a non-tracked entity"
74
+ end
75
+ end
76
+
77
+ # Performs save operations (Create/Update/Delete) against the server
78
+ def save_changes
79
+ return nil if @save_operations.empty?
80
+
81
+ result = nil
82
+
83
+ if @save_operations.length == 1
84
+ result = single_save(@save_operations[0])
85
+ else
86
+ result = batch_save(@save_operations)
87
+ end
88
+
89
+ # TODO: We should probably perform a check here
90
+ # to make sure everything worked before clearing it out
91
+ @save_operations.clear
92
+
93
+ return result
94
+ end
95
+
96
+ # Performs query operations (Read) against the server
97
+ def execute
98
+ result = RestClient.get build_query_uri, @http_headers
99
+ build_classes_from_result(result)
100
+ end
101
+
102
+ # Overridden to identify methods handled by method_missing
103
+ def respond_to?(method)
104
+ if @collections.include?(method.to_s)
105
+ return true
106
+ # Adds
107
+ elsif method.to_s =~ /^AddTo(.*)/
108
+ type = $1
109
+ if @collections.include?(type)
110
+ return true
111
+ else
112
+ super
113
+ end
114
+ else
115
+ super
116
+ end
117
+ end
118
+
119
+ private
120
+ # Wrapper around open call to make http request
121
+ def http_open( address )
122
+ if not @auth_header.nil?
123
+ return open(address, "Authorization" => @auth_header)
124
+ else
125
+ return open(address)
126
+ end
127
+ end
128
+
129
+ # Retrieves collections from the main service page
130
+ def get_collections
131
+ doc = Nokogiri::XML(http_open(@uri))
132
+ collections = doc.xpath("//app:collection", "app" => "http://www.w3.org/2007/app")
133
+ collections.collect { |c| c["href"] }
134
+ end
135
+
136
+ # Build the classes required by the metadata
137
+ def build_classes
138
+ @classes = Hash.new
139
+ doc = Nokogiri::XML(http_open("#{@uri}/$metadata"))
140
+
141
+ # Get the edm namespace
142
+ edm_ns = doc.xpath("edmx:Edmx/edmx:DataServices/*", "edmx" => "http://schemas.microsoft.com/ado/2007/06/edmx").first.namespaces['xmlns'].to_s
143
+
144
+ # Build complex types first, these will be used for entities
145
+ complex_types = doc.xpath("//edm:ComplexType", "edm" => edm_ns) || []
146
+ complex_types.each do |c|
147
+ name = c['Name']
148
+ props = c.xpath(".//edm:Property", "edm" => edm_ns)
149
+ methods = props.collect { |p| p['Name'] } # Standard Properties
150
+ @classes[name] = ClassBuilder.new(name, methods, []).build unless @classes.keys.include?(name)
151
+ end
152
+
153
+ entity_types = doc.xpath("//edm:EntityType", "edm" => edm_ns)
154
+ entity_types.each do |e|
155
+ name = e['Name']
156
+ props = e.xpath(".//edm:Property", "edm" => edm_ns)
157
+ methods = props.collect { |p| p['Name'] } # Standard Properties
158
+ nprops = e.xpath(".//edm:NavigationProperty", "edm" => edm_ns)
159
+ nav_props = nprops.collect { |p| p['Name'] } # Standard Properties
160
+ @classes[name] = ClassBuilder.new(name, methods, nav_props).build unless @classes.keys.include?(name)
161
+ end
162
+ end
163
+
164
+ # Helper to loop through a result and create an instance for each entity in the results
165
+ def build_classes_from_result(result)
166
+ doc = Nokogiri::XML(result)
167
+ entries = doc.xpath("//atom:entry[not(ancestor::atom:entry)]", "atom" => "http://www.w3.org/2005/Atom")
168
+ return entry_to_class(entries[0]) if entries.length == 1
169
+
170
+ results = []
171
+ entries.each do |entry|
172
+ results << entry_to_class(entry)
173
+ end
174
+ return results
175
+ end
176
+
177
+ # Converts an XML Entry into a class
178
+ def entry_to_class(entry)
179
+ # Retrieve the class name from the fully qualified name (the last string after the last dot)
180
+ klass_name = entry.xpath("./atom:category/@term", "atom" => "http://www.w3.org/2005/Atom").to_s.split('.')[-1]
181
+ return nil if klass_name.empty?
182
+
183
+ properties = entry.xpath(".//m:properties/*", { "m" => "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" })
184
+
185
+ klass = @classes[klass_name].new
186
+
187
+ # Fill metadata
188
+ meta_id = entry.xpath("./atom:id", "atom" => "http://www.w3.org/2005/Atom")[0].content
189
+ klass.send :__metadata=, { :uri => meta_id }
190
+
191
+ # Fill properties
192
+ for prop in properties
193
+ prop_name = prop.name
194
+ klass.send "#{prop_name}=", parse_value(prop)
195
+ end
196
+
197
+ inline_links = entry.xpath("./atom:link[m:inline]", { "m" => "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata", "atom" => "http://www.w3.org/2005/Atom" })
198
+
199
+ for link in inline_links
200
+ inline_entries = link.xpath(".//atom:entry", "atom" => "http://www.w3.org/2005/Atom")
201
+
202
+ if inline_entries.length == 1
203
+ property_name = link.attributes['title'].to_s
204
+
205
+ build_inline_class(klass, inline_entries[0], property_name)
190
206
  else
191
207
  # TODO: Test handling multiple children
192
- for inline_entry in inline_entries
193
- property_name = link.xpath("atom:link[@rel='edit']/@title", "atom" => "http://www.w3.org/2005/Atom")
194
-
195
- # Build the class
196
- inline_klass = entry_to_class(inline_entry)
197
-
198
- # Add the property
199
- klass.send "#{property_name}=", inline_klass
200
- end
201
- end
202
- end
203
-
204
- return klass
205
- end
206
-
207
- def build_query_uri
208
- "#{@uri}#{@query.query}"
209
- end
210
- def build_inline_class(klass, entry, property_name)
211
- # Build the class
212
- inline_klass = entry_to_class(entry)
213
-
214
- # Add the property
215
- klass.send "#{property_name}=", inline_klass
216
- end
217
- def single_save(operation)
218
- if operation.kind == "Add"
219
- save_uri = "#{@uri}/#{operation.klass_name}"
220
- json_klass = operation.klass.to_json(:type => :add)
221
- post_result = RestClient.post save_uri, json_klass, :content_type => :json
222
- return build_classes_from_result(post_result)
223
- elsif operation.kind == "Update"
224
- update_uri = operation.klass.send(:__metadata)[:uri]
225
- json_klass = operation.klass.to_json
226
- update_result = RestClient.put update_uri, json_klass, :content_type => :json
227
- return (update_result.code == 204)
228
- elsif operation.kind == "Delete"
229
- delete_uri = operation.klass.send(:__metadata)[:uri]
230
- delete_result = RestClient.delete delete_uri
231
- return (delete_result.code == 204)
232
- end
233
- end
234
-
235
- # Batch Saves
236
- def generate_guid
237
- rand(36**12).to_s(36).insert(4, "-").insert(9, "-")
238
- end
239
- def batch_save(operations)
240
- batch_num = generate_guid
241
- changeset_num = generate_guid
242
- batch_uri = "#{@uri}/$batch"
243
-
244
- body = build_batch_body(operations, batch_num, changeset_num)
245
-
246
- result = RestClient.post batch_uri, body, :content_type => "multipart/mixed; boundary=batch_#{batch_num}"
247
-
248
- # TODO: More result validation needs to be done.
249
- # The result returns HTTP 202 even if there is an error in the batch
250
- return (result.code == 202)
251
- end
252
- def build_batch_body(operations, batch_num, changeset_num)
253
- # Header
254
- body = "--batch_#{batch_num}\n"
255
- body << "Content-Type: multipart/mixed;boundary=changeset_#{changeset_num}\n\n"
256
-
257
- # Operations
258
- operations.each do |operation|
259
- body << build_batch_operation(operation, changeset_num)
260
- body << "\n"
261
- end
262
-
263
- # Footer
264
- body << "\n\n--changeset_#{changeset_num}--\n"
265
- body << "--batch_#{batch_num}--"
266
-
267
- return body
268
- end
269
- def build_batch_operation(operation, changeset_num)
270
- accept_headers = "Accept-Charset: utf-8\n"
271
- accept_headers << "Content-Type: application/json;charset=utf-8\n" unless operation.kind == "Delete"
272
- accept_headers << "\n"
273
-
274
- content = "--changeset_#{changeset_num}\n"
275
- content << "Content-Type: application/http\n"
276
- content << "Content-Transfer-Encoding: binary\n\n"
277
-
278
- if operation.kind == "Add"
279
- save_uri = "#{@uri}/#{operation.klass_name}"
280
- json_klass = operation.klass.to_json(:type => :add)
281
-
282
- content << "POST #{save_uri} HTTP/1.1\n"
283
- content << accept_headers
284
- content << json_klass
285
- elsif operation.kind == "Update"
286
- update_uri = operation.klass.send(:__metadata)[:uri]
287
- json_klass = operation.klass.to_json
288
-
289
- content << "PUT #{update_uri} HTTP/1.1\n"
290
- content << accept_headers
291
- content << json_klass
292
- elsif operation.kind == "Delete"
293
- delete_uri = operation.klass.send(:__metadata)[:uri]
294
-
295
- content << "DELETE #{delete_uri} HTTP/1.1\n"
296
- content << accept_headers
297
- end
298
-
299
- return content
300
- end
301
-
302
- # Complex Types
303
- def complex_type_to_class(complex_type_xml)
304
- klass_name = complex_type_xml.attr('type').split('.')[-1]
305
- klass = @classes[klass_name].new
306
-
307
- # Fill in the properties
308
- properties = complex_type_xml.xpath(".//*")
309
- properties.each do |prop|
310
- klass.send "#{prop.name}=", parse_value(prop)
311
- end
312
-
313
- return klass
314
- end
315
-
316
- # Field Converters
317
- def parse_value(property_xml)
318
- property_type = property_xml.attr('type')
319
-
320
- # Handle a nil property type, this is a string
321
- return property_xml.content if property_type.nil?
322
-
323
- # Handle complex types
324
- return complex_type_to_class(property_xml) if !property_type.match(/^Edm/)
325
-
326
- # Handle integers
327
- return property_xml.content.to_i if property_type.match(/^Edm.Int/)
328
-
329
- # Handle decimals
330
- return property_xml.content.to_d if property_type.match(/Edm.Decimal/)
331
-
332
- # Handle DateTimes
333
- # return Time.parse(property_xml.content) if property_type.match(/Edm.DateTime/)
334
- if property_type.match(/Edm.DateTime/)
335
- sdate = property_xml.content
336
-
337
- # Assume this is UTC if no timezone is specified
338
- sdate = sdate + "Z" unless sdate.match(/Z|([+|-]\d{2}:\d{2})$/)
339
-
340
- return Time.parse(sdate)
341
- end
342
-
343
- # If we can't parse the value, just return the element's content
344
- property_xml.content
345
- end
208
+ for inline_entry in inline_entries
209
+ property_name = link.xpath("atom:link[@rel='edit']/@title", "atom" => "http://www.w3.org/2005/Atom")
210
+
211
+ # Build the class
212
+ inline_klass = entry_to_class(inline_entry)
213
+
214
+ # Add the property
215
+ klass.send "#{property_name}=", inline_klass
216
+ end
217
+ end
218
+ end
219
+
220
+ return klass
221
+ end
222
+
223
+ def build_query_uri
224
+ "#{@uri}#{@query.query}"
225
+ end
226
+ def build_inline_class(klass, entry, property_name)
227
+ # Build the class
228
+ inline_klass = entry_to_class(entry)
229
+
230
+ # Add the property
231
+ klass.send "#{property_name}=", inline_klass
232
+ end
233
+ def single_save(operation)
234
+ if operation.kind == "Add"
235
+ save_uri = "#{@uri}/#{operation.klass_name}"
236
+ json_klass = operation.klass.to_json(:type => :add)
237
+ post_result = RestClient.post save_uri, json_klass, {:content_type => :json}.merge(@http_headers)
238
+ return build_classes_from_result(post_result)
239
+ elsif operation.kind == "Update"
240
+ update_uri = operation.klass.send(:__metadata)[:uri]
241
+ json_klass = operation.klass.to_json
242
+ update_result = RestClient.put update_uri, json_klass, {:content_type => :json}.merge(@http_headers)
243
+ return (update_result.code == 204)
244
+ elsif operation.kind == "Delete"
245
+ delete_uri = operation.klass.send(:__metadata)[:uri]
246
+ delete_result = RestClient.delete delete_uri, @http_headers
247
+ return (delete_result.code == 204)
248
+ end
249
+ end
250
+
251
+ # Batch Saves
252
+ def generate_guid
253
+ rand(36**12).to_s(36).insert(4, "-").insert(9, "-")
254
+ end
255
+ def batch_save(operations)
256
+ batch_num = generate_guid
257
+ changeset_num = generate_guid
258
+ batch_uri = "#{@uri}/$batch"
259
+
260
+ body = build_batch_body(operations, batch_num, changeset_num)
261
+
262
+ result = RestClient.post batch_uri, body, {:content_type => "multipart/mixed; boundary=batch_#{batch_num}"}.merge(@http_headers)
263
+
264
+ # TODO: More result validation needs to be done.
265
+ # The result returns HTTP 202 even if there is an error in the batch
266
+ return (result.code == 202)
267
+ end
268
+ def build_batch_body(operations, batch_num, changeset_num)
269
+ # Header
270
+ body = "--batch_#{batch_num}\n"
271
+ body << "Content-Type: multipart/mixed;boundary=changeset_#{changeset_num}\n\n"
272
+
273
+ # Operations
274
+ operations.each do |operation|
275
+ body << build_batch_operation(operation, changeset_num)
276
+ body << "\n"
277
+ end
278
+
279
+ # Footer
280
+ body << "\n\n--changeset_#{changeset_num}--\n"
281
+ body << "--batch_#{batch_num}--"
282
+
283
+ return body
284
+ end
285
+ def build_batch_operation(operation, changeset_num)
286
+ accept_headers = "Accept-Charset: utf-8\n"
287
+ accept_headers << "Content-Type: application/json;charset=utf-8\n" unless operation.kind == "Delete"
288
+ accept_headers << "\n"
289
+
290
+ content = "--changeset_#{changeset_num}\n"
291
+ content << "Content-Type: application/http\n"
292
+ content << "Content-Transfer-Encoding: binary\n\n"
293
+
294
+ if operation.kind == "Add"
295
+ save_uri = "#{@uri}/#{operation.klass_name}"
296
+ json_klass = operation.klass.to_json(:type => :add)
297
+
298
+ content << "POST #{save_uri} HTTP/1.1\n"
299
+ content << accept_headers
300
+ content << json_klass
301
+ elsif operation.kind == "Update"
302
+ update_uri = operation.klass.send(:__metadata)[:uri]
303
+ json_klass = operation.klass.to_json
304
+
305
+ content << "PUT #{update_uri} HTTP/1.1\n"
306
+ content << accept_headers
307
+ content << json_klass
308
+ elsif operation.kind == "Delete"
309
+ delete_uri = operation.klass.send(:__metadata)[:uri]
310
+
311
+ content << "DELETE #{delete_uri} HTTP/1.1\n"
312
+ content << accept_headers
313
+ end
314
+
315
+ return content
316
+ end
317
+
318
+ # Complex Types
319
+ def complex_type_to_class(complex_type_xml)
320
+ klass_name = complex_type_xml.attr('type').split('.')[-1]
321
+ klass = @classes[klass_name].new
322
+
323
+ # Fill in the properties
324
+ properties = complex_type_xml.xpath(".//*")
325
+ properties.each do |prop|
326
+ klass.send "#{prop.name}=", parse_value(prop)
327
+ end
328
+
329
+ return klass
330
+ end
331
+
332
+ # Field Converters
333
+ def parse_value(property_xml)
334
+ property_type = property_xml.attr('type')
335
+ property_null = property_xml.attr('null')
336
+
337
+ # Handle a nil property type, this is a string
338
+ return property_xml.content if property_type.nil?
339
+
340
+ # Handle anything marked as null
341
+ return nil if !property_null.nil? && property_null == "true"
342
+
343
+ # Handle complex types
344
+ return complex_type_to_class(property_xml) if !property_type.match(/^Edm/)
345
+
346
+ # Handle integers
347
+ return property_xml.content.to_i if property_type.match(/^Edm.Int/)
348
+
349
+ # Handle decimals
350
+ return property_xml.content.to_d if property_type.match(/Edm.Decimal/)
351
+
352
+ # Handle DateTimes
353
+ # return Time.parse(property_xml.content) if property_type.match(/Edm.DateTime/)
354
+ if property_type.match(/Edm.DateTime/)
355
+ sdate = property_xml.content
356
+
357
+ # Assume this is UTC if no timezone is specified
358
+ sdate = sdate + "Z" unless sdate.match(/Z|([+|-]\d{2}:\d{2})$/)
359
+
360
+ return Time.parse(sdate)
361
+ end
362
+
363
+ # If we can't parse the value, just return the element's content
364
+ property_xml.content
365
+ end
346
366
 
347
367
  end
348
368