ruby_odata 0.0.7 → 0.0.8

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