ruby_odata 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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")
|