ruby_odata 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -2
- data/.travis.yml +2 -1
- data/.yardopts +6 -0
- data/CHANGELOG.md +102 -0
- data/Guardfile +14 -0
- data/README.md +285 -0
- data/Rakefile +0 -7
- data/features/basic_auth.feature +3 -2
- data/features/batch_request.feature +7 -6
- data/features/cassettes/basic_auth_protected_resource.yml +57 -0
- data/features/cassettes/batch_request_additions.yml +69 -0
- data/features/cassettes/batch_request_deletes.yml +69 -0
- data/features/cassettes/batch_request_updates.yml +69 -0
- data/features/cassettes/clean_database_for_testing.yml +46 -0
- data/features/cassettes/cucumber_tags/basic_auth.yml +297 -0
- data/features/cassettes/cucumber_tags/batch_request.yml +1459 -0
- data/features/cassettes/cucumber_tags/complex_types.yml +326 -0
- data/features/cassettes/cucumber_tags/error_handling.yml +64 -0
- data/features/cassettes/cucumber_tags/query_builder.yml +2025 -0
- data/features/cassettes/cucumber_tags/service.yml +234 -0
- data/features/cassettes/cucumber_tags/service_manage.yml +937 -0
- data/features/cassettes/cucumber_tags/service_methods.yml +647 -0
- data/features/cassettes/cucumber_tags/ssl.yml +203 -0
- data/features/cassettes/cucumber_tags/type_conversion.yml +337 -0
- data/features/cassettes/service_manage_additions.yml +65 -0
- data/features/cassettes/service_manage_deletions.yml +58 -0
- data/features/cassettes/service_manage_deletions_2.yml +58 -0
- data/features/cassettes/unsecured_metadata.yml +89 -0
- data/features/complex_types.feature +4 -3
- data/features/error_handling.feature +14 -0
- data/features/query_builder.feature +30 -9
- data/features/service.feature +4 -3
- data/features/service_manage.feature +6 -5
- data/features/service_methods.feature +3 -2
- data/features/ssl.feature +8 -8
- data/features/step_definitions/service_steps.rb +38 -24
- data/features/support/env.rb +1 -3
- data/features/support/hooks.rb +3 -2
- data/features/support/pickle.rb +29 -18
- data/features/support/vcr.rb +24 -0
- data/features/type_conversion.feature +16 -17
- data/lib/ruby_odata/association.rb +7 -6
- data/lib/ruby_odata/class_builder.rb +6 -7
- data/lib/ruby_odata/exceptions.rb +4 -0
- data/lib/ruby_odata/helpers.rb +11 -0
- data/lib/ruby_odata/operation.rb +5 -6
- data/lib/ruby_odata/property_metadata.rb +4 -5
- data/lib/ruby_odata/query_builder.rb +98 -63
- data/lib/ruby_odata/service.rb +118 -103
- data/lib/ruby_odata/version.rb +3 -1
- data/lib/ruby_odata.rb +20 -18
- data/ruby_odata.gemspec +16 -12
- data/spec/query_builder_spec.rb +78 -14
- data/spec/service_spec.rb +83 -83
- data/spec/support/sample_service_matcher.rb +15 -0
- data/test/RubyODataService/RubyODataService/App_Data/.gitkeep +0 -0
- data/test/blueprints.rb +15 -9
- data/test/usage_samples/querying.rb +5 -1
- data/test/usage_samples/sample_data.rb +1 -3
- metadata +213 -39
- data/CHANGELOG.rdoc +0 -88
- data/README.rdoc +0 -259
data/lib/ruby_odata/service.rb
CHANGED
@@ -1,17 +1,16 @@
|
|
1
1
|
module OData
|
2
|
-
|
2
|
+
# The main service class, also known as a *Context*
|
3
3
|
class Service
|
4
4
|
attr_reader :classes, :class_metadata, :options, :collections, :edmx, :function_imports
|
5
5
|
# Creates a new instance of the Service class
|
6
6
|
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
# - eager_partial: true (default) if queries should consume partial feeds until the feed is complete, false if explicit calls to next must be performed
|
7
|
+
# @param [String] service_uri the root URI of the OData service
|
8
|
+
# @param [Hash] options the options to pass to the service
|
9
|
+
# @option options [String] :username for http basic auth
|
10
|
+
# @option options [String] :password for http basic auth
|
11
|
+
# @option options [Object] :verify_ssl false if no verification, otherwise mode (OpenSSL::SSL::VERIFY_PEER is default)
|
12
|
+
# @option options [Hash] :additional_params a hash of query string params that will be passed on all calls
|
13
|
+
# @option options [Boolean, true] :eager_partial true if queries should consume partial feeds until the feed is complete, false if explicit calls to next must be performed
|
15
14
|
def initialize(service_uri, options = {})
|
16
15
|
@uri = service_uri.gsub!(/\/?$/, '')
|
17
16
|
set_options! options
|
@@ -20,7 +19,7 @@ class Service
|
|
20
19
|
build_collections_and_classes
|
21
20
|
end
|
22
21
|
|
23
|
-
# Handles the dynamic AddTo<EntityName
|
22
|
+
# Handles the dynamic `AddTo<EntityName>` methods as well as the collections on the service
|
24
23
|
def method_missing(name, *args)
|
25
24
|
# Queries
|
26
25
|
if @collections.include?(name.to_s)
|
@@ -45,31 +44,29 @@ class Service
|
|
45
44
|
|
46
45
|
# Queues an object for deletion. To actually remove it from the server, you must call save_changes as well.
|
47
46
|
#
|
48
|
-
#
|
49
|
-
# - obj: The object to mark for deletion
|
47
|
+
# @param [Object] obj the object to mark for deletion
|
50
48
|
#
|
51
|
-
#
|
49
|
+
# @raise [NotSupportedError] if the `obj` isn't a tracked entity
|
52
50
|
def delete_object(obj)
|
53
51
|
type = obj.class.to_s
|
54
52
|
if obj.respond_to?(:__metadata) && !obj.send(:__metadata).nil?
|
55
53
|
@save_operations << Operation.new("Delete", type, obj)
|
56
54
|
else
|
57
|
-
raise "You cannot delete a non-tracked entity"
|
55
|
+
raise OData::NotSupportedError.new "You cannot delete a non-tracked entity"
|
58
56
|
end
|
59
57
|
end
|
60
58
|
|
61
59
|
# Queues an object for update. To actually update it on the server, you must call save_changes as well.
|
62
60
|
#
|
63
|
-
#
|
64
|
-
# - obj: The object to queue for update
|
61
|
+
# @param [Object] obj the object to queue for update
|
65
62
|
#
|
66
|
-
#
|
63
|
+
# @raise [NotSupportedError] if the `obj` isn't a tracked entity
|
67
64
|
def update_object(obj)
|
68
65
|
type = obj.class.to_s
|
69
66
|
if obj.respond_to?(:__metadata) && !obj.send(:__metadata).nil?
|
70
67
|
@save_operations << Operation.new("Update", type, obj)
|
71
68
|
else
|
72
|
-
raise "You cannot update a non-tracked entity"
|
69
|
+
raise OData::NotSupportedError.new "You cannot update a non-tracked entity"
|
73
70
|
end
|
74
71
|
end
|
75
72
|
|
@@ -79,22 +76,28 @@ class Service
|
|
79
76
|
|
80
77
|
result = nil
|
81
78
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
79
|
+
begin
|
80
|
+
if @save_operations.length == 1
|
81
|
+
result = single_save(@save_operations[0])
|
82
|
+
else
|
83
|
+
result = batch_save(@save_operations)
|
84
|
+
end
|
87
85
|
|
88
|
-
|
89
|
-
|
90
|
-
|
86
|
+
# TODO: We should probably perform a check here
|
87
|
+
# to make sure everything worked before clearing it out
|
88
|
+
@save_operations.clear
|
91
89
|
|
92
|
-
|
90
|
+
return result
|
91
|
+
rescue Exception => e
|
92
|
+
handle_exception(e)
|
93
|
+
end
|
93
94
|
end
|
94
95
|
|
95
|
-
# Performs query operations (Read) against the server
|
96
|
+
# Performs query operations (Read) against the server.
|
97
|
+
# Typically this returns an array of record instances, except in the case of count queries
|
96
98
|
def execute
|
97
99
|
result = RestClient::Resource.new(build_query_uri, @rest_options).get
|
100
|
+
return Integer(result) if result =~ /^\d+$/
|
98
101
|
handle_collection_result(result)
|
99
102
|
end
|
100
103
|
|
@@ -118,7 +121,7 @@ class Service
|
|
118
121
|
end
|
119
122
|
end
|
120
123
|
|
121
|
-
# Retrieves the next resultset of a partial result (if any). Does not honor the
|
124
|
+
# Retrieves the next resultset of a partial result (if any). Does not honor the `:eager_partial` option.
|
122
125
|
def next
|
123
126
|
return if not partial?
|
124
127
|
handle_partial
|
@@ -131,14 +134,13 @@ class Service
|
|
131
134
|
|
132
135
|
# Lazy loads a navigation property on a model
|
133
136
|
#
|
134
|
-
#
|
135
|
-
#
|
136
|
-
# - nav_prop: The navigation property to fill
|
137
|
+
# @param [Object] obj the object to fill
|
138
|
+
# @param [String] nav_prop the navigation property to fill
|
137
139
|
#
|
138
|
-
#
|
139
|
-
#
|
140
|
+
# @raise [NotSupportedError] if the `obj` isn't a tracked entity
|
141
|
+
# @raise [ArgumentError] if the `nav_prop` isn't a valid navigation property
|
140
142
|
def load_property(obj, nav_prop)
|
141
|
-
raise
|
143
|
+
raise NotSupportedError, "You cannot load a property on an entity that isn't tracked" if obj.send(:__metadata).nil?
|
142
144
|
raise ArgumentError, "'#{nav_prop}' is not a valid navigation property" unless obj.respond_to?(nav_prop.to_sym)
|
143
145
|
raise ArgumentError, "'#{nav_prop}' is not a valid navigation property" unless @class_metadata[obj.class.to_s][nav_prop].nav_prop
|
144
146
|
results = RestClient::Resource.new(build_load_property_uri(obj, nav_prop), @rest_options).get
|
@@ -148,15 +150,17 @@ class Service
|
|
148
150
|
|
149
151
|
# Adds a child object to a parent object's collection
|
150
152
|
#
|
151
|
-
#
|
152
|
-
#
|
153
|
-
#
|
154
|
-
#
|
153
|
+
# @param [Object] parent the parent object
|
154
|
+
# @param [String] nav_prop the name of the navigation property to add the child to
|
155
|
+
# @param [Object] child the child object
|
156
|
+
# @raise [NotSupportedError] if the `parent` isn't a tracked entity
|
157
|
+
# @raise [ArgumentError] if the `nav_prop` isn't a valid navigation property
|
158
|
+
# @raise [NotSupportedError] if the `child` isn't a tracked entity
|
155
159
|
def add_link(parent, nav_prop, child)
|
156
|
-
raise
|
160
|
+
raise NotSupportedError, "You cannot add a link on an entity that isn't tracked (#{parent.class})" if parent.send(:__metadata).nil?
|
157
161
|
raise ArgumentError, "'#{nav_prop}' is not a valid navigation property for #{parent.class}" unless parent.respond_to?(nav_prop.to_sym)
|
158
162
|
raise ArgumentError, "'#{nav_prop}' is not a valid navigation property for #{parent.class}" unless @class_metadata[parent.class.to_s][nav_prop].nav_prop
|
159
|
-
raise
|
163
|
+
raise NotSupportedError, "You cannot add a link on a child entity that isn't tracked (#{child.class})" if child.send(:__metadata).nil?
|
160
164
|
@save_operations << Operation.new("AddLink", nav_prop, parent, child)
|
161
165
|
end
|
162
166
|
|
@@ -179,16 +183,16 @@ class Service
|
|
179
183
|
@has_partial = false
|
180
184
|
@next_uri = nil
|
181
185
|
end
|
182
|
-
|
186
|
+
|
183
187
|
def set_namespaces
|
184
188
|
@edmx = Nokogiri::XML(RestClient::Resource.new(build_metadata_uri, @rest_options).get)
|
185
|
-
@ds_namespaces = {
|
189
|
+
@ds_namespaces = {
|
186
190
|
"m" => "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata",
|
187
191
|
"edmx" => "http://schemas.microsoft.com/ado/2007/06/edmx",
|
188
192
|
"ds" => "http://schemas.microsoft.com/ado/2007/08/dataservices",
|
189
193
|
"atom" => "http://www.w3.org/2005/Atom"
|
190
194
|
}
|
191
|
-
|
195
|
+
|
192
196
|
# Get the edm namespace from the edmx
|
193
197
|
edm_ns = @edmx.xpath("edmx:Edmx/edmx:DataServices/*", @namespaces).first.namespaces['xmlns'].to_s
|
194
198
|
@ds_namespaces.merge! "edm" => edm_ns
|
@@ -232,7 +236,7 @@ class Service
|
|
232
236
|
entity_type = c["EntityType"]
|
233
237
|
@collections[c["Name"]] = { :edmx_type => entity_type, :type => convert_to_local_type(entity_type) }
|
234
238
|
end
|
235
|
-
|
239
|
+
|
236
240
|
build_function_imports
|
237
241
|
end
|
238
242
|
|
@@ -258,9 +262,9 @@ class Service
|
|
258
262
|
parameters[p["Name"]] = p["Type"]
|
259
263
|
end
|
260
264
|
end
|
261
|
-
@function_imports[f["Name"]] = {
|
262
|
-
:http_method => http_method,
|
263
|
-
:return_type => return_type,
|
265
|
+
@function_imports[f["Name"]] = {
|
266
|
+
:http_method => http_method,
|
267
|
+
:return_type => return_type,
|
264
268
|
:inner_return_type => inner_return_type,
|
265
269
|
:parameters => parameters }
|
266
270
|
end
|
@@ -294,7 +298,7 @@ class Service
|
|
294
298
|
end
|
295
299
|
metadata
|
296
300
|
end
|
297
|
-
|
301
|
+
|
298
302
|
# Handle parsing of OData Atom result and return an array of Entry classes
|
299
303
|
def handle_collection_result(result)
|
300
304
|
results = build_classes_from_result(result)
|
@@ -303,7 +307,18 @@ class Service
|
|
303
307
|
end
|
304
308
|
results
|
305
309
|
end
|
306
|
-
|
310
|
+
|
311
|
+
# Handles errors from the OData service
|
312
|
+
def handle_exception(e)
|
313
|
+
raise e unless e.response
|
314
|
+
|
315
|
+
code = e.http_code
|
316
|
+
error = Nokogiri::XML(e.response)
|
317
|
+
|
318
|
+
message = error.xpath("m:error/m:message", @ds_namespaces).first.content
|
319
|
+
raise "HTTP Error #{code}: #{message}"
|
320
|
+
end
|
321
|
+
|
307
322
|
# Loops through the standard properties (non-navigation) for a given class and returns the appropriate list of methods
|
308
323
|
def collect_properties(klass_name, element, doc)
|
309
324
|
props = element.xpath(".//edm:Property", @ds_namespaces)
|
@@ -318,7 +333,7 @@ class Service
|
|
318
333
|
end
|
319
334
|
methods
|
320
335
|
end
|
321
|
-
|
336
|
+
|
322
337
|
# Similar to +collect_properties+, but handles the navigation properties
|
323
338
|
def collect_navigation_properties(klass_name, element, doc)
|
324
339
|
nav_props = element.xpath(".//edm:NavigationProperty", @ds_namespaces)
|
@@ -329,14 +344,14 @@ class Service
|
|
329
344
|
# Helper to loop through a result and create an instance for each entity in the results
|
330
345
|
def build_classes_from_result(result)
|
331
346
|
doc = Nokogiri::XML(result)
|
332
|
-
|
347
|
+
|
333
348
|
is_links = doc.at_xpath("/ds:links", @ds_namespaces)
|
334
349
|
return parse_link_results(doc) if is_links
|
335
|
-
|
350
|
+
|
336
351
|
entries = doc.xpath("//atom:entry[not(ancestor::atom:entry)]", @ds_namespaces)
|
337
|
-
|
352
|
+
|
338
353
|
extract_partial(doc)
|
339
|
-
|
354
|
+
|
340
355
|
results = []
|
341
356
|
entries.each do |entry|
|
342
357
|
results << entry_to_class(entry)
|
@@ -348,24 +363,24 @@ class Service
|
|
348
363
|
def entry_to_class(entry)
|
349
364
|
# Retrieve the class name from the fully qualified name (the last string after the last dot)
|
350
365
|
klass_name = entry.xpath("./atom:category/@term", @ds_namespaces).to_s.split('.')[-1]
|
351
|
-
|
366
|
+
|
352
367
|
# Is the category missing? See if there is a title that we can use to build the class
|
353
368
|
if klass_name.nil?
|
354
369
|
title = entry.xpath("./atom:title", @ds_namespaces).first
|
355
370
|
return nil if title.nil?
|
356
371
|
klass_name = title.content.to_s
|
357
372
|
end
|
358
|
-
|
373
|
+
|
359
374
|
return nil if klass_name.nil?
|
360
375
|
|
361
|
-
# If we are working against a child (inline) entry, we need to use the more generic xpath because a child entry WILL
|
376
|
+
# If we are working against a child (inline) entry, we need to use the more generic xpath because a child entry WILL
|
362
377
|
# have properties that are ancestors of m:inline. Check if there is an m:inline child to determine the xpath query to use
|
363
378
|
has_inline = entry.xpath(".//m:inline", @ds_namespaces).any?
|
364
379
|
properties_xpath = has_inline ? ".//m:properties[not(ancestor::m:inline)]/*" : ".//m:properties/*"
|
365
380
|
properties = entry.xpath(properties_xpath, @ds_namespaces)
|
366
381
|
|
367
382
|
klass = @classes[qualify_class_name(klass_name)].new
|
368
|
-
|
383
|
+
|
369
384
|
# Fill metadata
|
370
385
|
meta_id = entry.xpath("./atom:id", @ds_namespaces)[0].content
|
371
386
|
klass.send :__metadata=, { :uri => meta_id }
|
@@ -375,7 +390,7 @@ class Service
|
|
375
390
|
prop_name = prop.name
|
376
391
|
klass.send "#{prop_name}=", parse_value(prop)
|
377
392
|
end
|
378
|
-
|
393
|
+
|
379
394
|
# Fill properties represented outside of the properties collection
|
380
395
|
@class_metadata[qualify_class_name(klass_name)].select { |k,v| v.fc_keep_in_content == false }.each do |k, meta|
|
381
396
|
if meta.fc_target_path == "SyndicationTitle"
|
@@ -386,13 +401,13 @@ class Service
|
|
386
401
|
klass.send "#{meta.name}=", summary.content
|
387
402
|
end
|
388
403
|
end
|
389
|
-
|
404
|
+
|
390
405
|
inline_links = entry.xpath("./atom:link[m:inline]", @ds_namespaces)
|
391
|
-
|
406
|
+
|
392
407
|
for link in inline_links
|
393
408
|
inline_entries = link.xpath(".//atom:entry", @ds_namespaces)
|
394
409
|
|
395
|
-
# TODO: Use the metadata's associations to determine the multiplicity instead of this "hack"
|
410
|
+
# TODO: Use the metadata's associations to determine the multiplicity instead of this "hack"
|
396
411
|
property_name = link.attributes['title'].to_s
|
397
412
|
if inline_entries.length == 1 && singular?(property_name)
|
398
413
|
inline_klass = build_inline_class(klass, inline_entries[0], property_name)
|
@@ -412,17 +427,17 @@ class Service
|
|
412
427
|
klass.send "#{property_name}=", inline_classes
|
413
428
|
end
|
414
429
|
end
|
415
|
-
|
430
|
+
|
416
431
|
klass
|
417
432
|
end
|
418
|
-
|
433
|
+
|
419
434
|
# Tests for and extracts the next href of a partial
|
420
435
|
def extract_partial(doc)
|
421
436
|
next_links = doc.xpath('//atom:link[@rel="next"]', @ds_namespaces)
|
422
437
|
@has_partial = next_links.any?
|
423
438
|
@next_uri = next_links[0]['href'] if @has_partial
|
424
439
|
end
|
425
|
-
|
440
|
+
|
426
441
|
def handle_partial
|
427
442
|
if @next_uri
|
428
443
|
result = RestClient::Resource.new(@next_uri, @rest_options).get
|
@@ -430,7 +445,7 @@ class Service
|
|
430
445
|
end
|
431
446
|
results
|
432
447
|
end
|
433
|
-
|
448
|
+
|
434
449
|
# Handle link results
|
435
450
|
def parse_link_results(doc)
|
436
451
|
uris = doc.xpath("/ds:links/ds:uri", @ds_namespaces)
|
@@ -440,7 +455,7 @@ class Service
|
|
440
455
|
results << URI.parse(link)
|
441
456
|
end
|
442
457
|
results
|
443
|
-
end
|
458
|
+
end
|
444
459
|
|
445
460
|
# Build URIs
|
446
461
|
def build_metadata_uri
|
@@ -470,7 +485,7 @@ class Service
|
|
470
485
|
def build_batch_uri
|
471
486
|
uri = "#{@uri}/$batch"
|
472
487
|
uri << "?#{@additional_params.to_query}" unless @additional_params.empty?
|
473
|
-
uri
|
488
|
+
uri
|
474
489
|
end
|
475
490
|
def build_load_property_uri(obj, property)
|
476
491
|
uri = obj.__metadata[:uri]
|
@@ -483,11 +498,11 @@ class Service
|
|
483
498
|
uri << "?#{params.to_query}" unless params.empty?
|
484
499
|
uri
|
485
500
|
end
|
486
|
-
|
501
|
+
|
487
502
|
def build_inline_class(klass, entry, property_name)
|
488
503
|
# Build the class
|
489
504
|
inline_klass = entry_to_class(entry)
|
490
|
-
|
505
|
+
|
491
506
|
# Add the property
|
492
507
|
klass.send "#{property_name}=", inline_klass
|
493
508
|
end
|
@@ -543,20 +558,20 @@ class Service
|
|
543
558
|
def generate_guid
|
544
559
|
rand(36**12).to_s(36).insert(4, "-").insert(9, "-")
|
545
560
|
end
|
546
|
-
def batch_save(operations)
|
561
|
+
def batch_save(operations)
|
547
562
|
batch_num = generate_guid
|
548
563
|
changeset_num = generate_guid
|
549
564
|
batch_uri = build_batch_uri
|
550
|
-
|
565
|
+
|
551
566
|
body = build_batch_body(operations, batch_num, changeset_num)
|
552
567
|
result = RestClient::Resource.new( batch_uri, @rest_options).post body, {:content_type => "multipart/mixed; boundary=batch_#{batch_num}"}
|
553
568
|
|
554
|
-
# TODO: More result validation needs to be done.
|
569
|
+
# TODO: More result validation needs to be done.
|
555
570
|
# The result returns HTTP 202 even if there is an error in the batch
|
556
571
|
return (result.code == 202)
|
557
572
|
end
|
558
573
|
def build_batch_body(operations, batch_num, changeset_num)
|
559
|
-
# Header
|
574
|
+
# Header
|
560
575
|
body = "--batch_#{batch_num}\n"
|
561
576
|
body << "Content-Type: multipart/mixed;boundary=changeset_#{changeset_num}\n\n"
|
562
577
|
|
@@ -565,51 +580,51 @@ class Service
|
|
565
580
|
body << build_batch_operation(operation, changeset_num)
|
566
581
|
body << "\n"
|
567
582
|
end
|
568
|
-
|
569
|
-
# Footer
|
583
|
+
|
584
|
+
# Footer
|
570
585
|
body << "\n\n--changeset_#{changeset_num}--\n"
|
571
586
|
body << "--batch_#{batch_num}--"
|
572
|
-
|
587
|
+
|
573
588
|
return body
|
574
589
|
end
|
575
590
|
def build_batch_operation(operation, changeset_num)
|
576
591
|
accept_headers = "Accept-Charset: utf-8\n"
|
577
592
|
accept_headers << "Content-Type: application/json;charset=utf-8\n" unless operation.kind == "Delete"
|
578
593
|
accept_headers << "\n"
|
579
|
-
|
594
|
+
|
580
595
|
content = "--changeset_#{changeset_num}\n"
|
581
596
|
content << "Content-Type: application/http\n"
|
582
597
|
content << "Content-Transfer-Encoding: binary\n\n"
|
583
|
-
|
584
|
-
if operation.kind == "Add"
|
598
|
+
|
599
|
+
if operation.kind == "Add"
|
585
600
|
save_uri = "#{@uri}/#{operation.klass_name}"
|
586
601
|
json_klass = operation.klass.to_json(:type => :add)
|
587
|
-
|
602
|
+
|
588
603
|
content << "POST #{save_uri} HTTP/1.1\n"
|
589
604
|
content << accept_headers
|
590
605
|
content << json_klass
|
591
606
|
elsif operation.kind == "Update"
|
592
607
|
update_uri = operation.klass.send(:__metadata)[:uri]
|
593
608
|
json_klass = operation.klass.to_json
|
594
|
-
|
609
|
+
|
595
610
|
content << "PUT #{update_uri} HTTP/1.1\n"
|
596
611
|
content << accept_headers
|
597
612
|
content << json_klass
|
598
613
|
elsif operation.kind == "Delete"
|
599
614
|
delete_uri = operation.klass.send(:__metadata)[:uri]
|
600
|
-
|
615
|
+
|
601
616
|
content << "DELETE #{delete_uri} HTTP/1.1\n"
|
602
617
|
content << accept_headers
|
603
618
|
elsif
|
604
619
|
save_uri = build_add_link_uri(operation)
|
605
620
|
json_klass = operation.child_klass.to_json(:type => :link)
|
606
|
-
|
621
|
+
|
607
622
|
content << "POST #{save_uri} HTTP/1.1\n"
|
608
623
|
content << accept_headers
|
609
624
|
content << json_klass
|
610
625
|
link_child_to_parent(operation)
|
611
626
|
end
|
612
|
-
|
627
|
+
|
613
628
|
return content
|
614
629
|
end
|
615
630
|
|
@@ -628,24 +643,24 @@ class Service
|
|
628
643
|
end
|
629
644
|
|
630
645
|
# Field Converters
|
631
|
-
|
646
|
+
|
632
647
|
# Handles parsing datetimes from a string
|
633
648
|
def parse_date(sdate)
|
634
649
|
# Assume this is UTC if no timezone is specified
|
635
650
|
sdate = sdate + "Z" unless sdate.match(/Z|([+|-]\d{2}:\d{2})$/)
|
636
|
-
|
651
|
+
|
637
652
|
# This is to handle older versions of Ruby (e.g. ruby 1.8.7 (2010-12-23 patchlevel 330) [i386-mingw32])
|
638
653
|
# See http://makandra.com/notes/1017-maximum-representable-value-for-a-ruby-time-object
|
639
654
|
# In recent versions of Ruby, Time has a much larger range
|
640
655
|
begin
|
641
|
-
result = Time.parse(sdate)
|
656
|
+
result = Time.parse(sdate)
|
642
657
|
rescue ArgumentError
|
643
658
|
result = DateTime.parse(sdate)
|
644
659
|
end
|
645
|
-
|
660
|
+
|
646
661
|
return result
|
647
662
|
end
|
648
|
-
|
663
|
+
|
649
664
|
# Parses a value into the proper type based on an xml property element
|
650
665
|
def parse_value(property_xml)
|
651
666
|
property_type = property_xml.attr('type')
|
@@ -673,7 +688,7 @@ class Service
|
|
673
688
|
# If we can't parse the value, just return the element's content
|
674
689
|
property_xml.content
|
675
690
|
end
|
676
|
-
|
691
|
+
|
677
692
|
# Parses a value into the proper type based on a specified return type
|
678
693
|
def parse_primative_type(value, return_type)
|
679
694
|
return value.to_i if return_type == Fixnum
|
@@ -681,7 +696,7 @@ class Service
|
|
681
696
|
return parse_date(value.to_s) if return_type == Time
|
682
697
|
return value.to_s
|
683
698
|
end
|
684
|
-
|
699
|
+
|
685
700
|
# Converts an edm type (string) to a ruby type
|
686
701
|
def edm_to_ruby_type(edm_type)
|
687
702
|
return String if edm_type =~ /Edm.String/
|
@@ -690,30 +705,30 @@ class Service
|
|
690
705
|
return Time if edm_type =~ /Edm.DateTime/
|
691
706
|
return String
|
692
707
|
end
|
693
|
-
|
708
|
+
|
694
709
|
# Method Missing Handlers
|
695
|
-
|
710
|
+
|
696
711
|
# Executes an import function
|
697
712
|
def execute_import_function(name, *args)
|
698
713
|
func = @function_imports[name]
|
699
|
-
|
714
|
+
|
700
715
|
# Check the args making sure that more weren't passed in than the function needs
|
701
716
|
param_count = func[:parameters].nil? ? 0 : func[:parameters].count
|
702
717
|
arg_count = args.nil? ? 0 : args[0].count
|
703
718
|
if arg_count > param_count
|
704
719
|
raise ArgumentError, "wrong number of arguments (#{arg_count} for #{param_count})"
|
705
720
|
end
|
706
|
-
|
721
|
+
|
707
722
|
# Convert the parameters to a hash
|
708
723
|
params = {}
|
709
724
|
func[:parameters].keys.each_with_index { |key, i| params[key] = args[0][i] } unless func[:parameters].nil?
|
710
|
-
|
725
|
+
|
711
726
|
function_uri = build_function_import_uri(name, params)
|
712
727
|
result = RestClient::Resource.new(function_uri, @rest_options).send(func[:http_method].downcase, {})
|
713
|
-
|
728
|
+
|
714
729
|
# Is this a 204 (No content) result?
|
715
730
|
return true if result.code == 204
|
716
|
-
|
731
|
+
|
717
732
|
# No? Then we need to parse the results. There are 4 kinds...
|
718
733
|
if func[:return_type] == Array
|
719
734
|
# a collection of entites
|
@@ -726,19 +741,19 @@ class Service
|
|
726
741
|
end
|
727
742
|
return results
|
728
743
|
end
|
729
|
-
|
744
|
+
|
730
745
|
# a single entity
|
731
746
|
if @classes.include?(func[:return_type].to_s)
|
732
747
|
entry = Nokogiri::XML(result).xpath("atom:entry[not(ancestor::atom:entry)]", @ds_namespaces)
|
733
748
|
return entry_to_class(entry)
|
734
749
|
end
|
735
|
-
|
750
|
+
|
736
751
|
# or a single native type
|
737
752
|
unless func[:return_type].nil?
|
738
753
|
e = Nokogiri::XML(result).xpath("/*").first
|
739
754
|
return parse_primative_type(e.content, func[:return_type])
|
740
755
|
end
|
741
|
-
|
756
|
+
|
742
757
|
# Nothing could be parsed, so just return if we got a 200 or not
|
743
758
|
return (result.code == 200)
|
744
759
|
end
|
data/lib/ruby_odata/version.rb
CHANGED
data/lib/ruby_odata.rb
CHANGED
@@ -1,21 +1,23 @@
|
|
1
1
|
lib = File.dirname(__FILE__)
|
2
2
|
|
3
|
-
$: << lib +
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
8
|
-
require
|
9
|
-
require
|
10
|
-
require
|
11
|
-
require
|
12
|
-
require
|
13
|
-
require
|
3
|
+
$: << lib + "/ruby_odata/"
|
4
|
+
require "rubygems"
|
5
|
+
require "i18n"
|
6
|
+
require "active_support" # Used for serializtion to JSON
|
7
|
+
require "active_support/inflector"
|
8
|
+
require "active_support/core_ext"
|
9
|
+
require "cgi"
|
10
|
+
require "rest_client"
|
11
|
+
require "nokogiri"
|
12
|
+
require "bigdecimal"
|
13
|
+
require "bigdecimal/util"
|
14
|
+
require "backports"
|
14
15
|
|
15
|
-
require lib +
|
16
|
-
require lib +
|
17
|
-
require lib +
|
18
|
-
require lib +
|
19
|
-
require lib +
|
20
|
-
require lib +
|
21
|
-
require lib +
|
16
|
+
require lib + "/ruby_odata/exceptions"
|
17
|
+
require lib + "/ruby_odata/association"
|
18
|
+
require lib + "/ruby_odata/property_metadata"
|
19
|
+
require lib + "/ruby_odata/query_builder"
|
20
|
+
require lib + "/ruby_odata/class_builder"
|
21
|
+
require lib + "/ruby_odata/operation"
|
22
|
+
require lib + "/ruby_odata/service"
|
23
|
+
require lib + "/ruby_odata/helpers"
|
data/ruby_odata.gemspec
CHANGED
@@ -14,18 +14,22 @@ Gem::Specification.new do |s|
|
|
14
14
|
|
15
15
|
s.rubyforge_project = "ruby-odata"
|
16
16
|
|
17
|
-
s.add_dependency(
|
18
|
-
s.add_dependency(
|
19
|
-
s.add_dependency(
|
20
|
-
s.add_dependency(
|
21
|
-
|
22
|
-
|
23
|
-
s.add_development_dependency(
|
24
|
-
s.add_development_dependency(
|
25
|
-
s.add_development_dependency(
|
26
|
-
s.add_development_dependency(
|
27
|
-
s.add_development_dependency(
|
28
|
-
s.add_development_dependency(
|
17
|
+
s.add_dependency("i18n", "~> 0.6.0")
|
18
|
+
s.add_dependency("activesupport", ">= 3.0.0")
|
19
|
+
s.add_dependency("rest-client", ">= 1.5.1")
|
20
|
+
s.add_dependency("nokogiri", ">= 1.4.2")
|
21
|
+
s.add_dependency("backports", "~> 2.3.0")
|
22
|
+
|
23
|
+
s.add_development_dependency("rake", "0.9.2")
|
24
|
+
s.add_development_dependency("rspec", "~> 2.11.0")
|
25
|
+
s.add_development_dependency("cucumber", "~> 1.2.1")
|
26
|
+
s.add_development_dependency("pickle", "~> 0.4.11")
|
27
|
+
s.add_development_dependency("machinist", "~> 2.0")
|
28
|
+
s.add_development_dependency("webmock", "~> 1.8.8")
|
29
|
+
s.add_development_dependency("guard", "~> 1.3.0")
|
30
|
+
s.add_development_dependency("guard-rspec", "~> 1.2.1")
|
31
|
+
s.add_development_dependency("guard-cucumber", "~> 1.2.0")
|
32
|
+
s.add_development_dependency("vcr", "~> 2.2.4")
|
29
33
|
|
30
34
|
s.files = `git ls-files`.split("\n")
|
31
35
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|