ruby_odata 0.0.5 → 0.0.6
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +6 -1
- data/README.rdoc +1 -2
- data/VERSION +1 -1
- data/features/batch_request.feature +94 -0
- data/features/step_definitions/service_steps.rb +29 -8
- data/lib/ruby_odata/service.rb +101 -21
- data/ruby_odata.gemspec +3 -2
- data/test/SampleService/App_Code/Entities.cs +3 -0
- metadata +5 -4
data/CHANGELOG.rdoc
CHANGED
@@ -24,4 +24,9 @@
|
|
24
24
|
=== 0.0.5
|
25
25
|
* Bug Fixes
|
26
26
|
* Works with Ruby 1.9.1
|
27
|
-
* Works with ActiveSupport 3.0.0.beta4
|
27
|
+
* Works with ActiveSupport 3.0.0.beta4
|
28
|
+
|
29
|
+
=== 0.0.6
|
30
|
+
* New Features
|
31
|
+
* Ability to batch saves (Adds, Updates, Deletes); this will help save on network chatter
|
32
|
+
|
data/README.rdoc
CHANGED
@@ -16,9 +16,8 @@ You can install ruby_odata as a gem using:
|
|
16
16
|
gem install ruby_odata
|
17
17
|
|
18
18
|
== Usage
|
19
|
-
The API is a work in progress. Notably, changes can't be bundled (through save_changes, only the last operation before save_changes is persisted).
|
20
|
-
|
21
19
|
As of version 0.0.5, support has been added for ActiveSupport 3.0.0 beta 4 and Ruby 1.9.1
|
20
|
+
As of version 0.0.6, support has been added for batch saves
|
22
21
|
|
23
22
|
=== Adding
|
24
23
|
When you point at a service, an AddTo<EntityName> method is created for you. This method takes in the new entity to create. To commit the change, you need to call the save_changes method on the service. To add a new category for example, you would simply do the following:
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
1
|
+
0.0.6
|
@@ -0,0 +1,94 @@
|
|
1
|
+
Feature: Batch request
|
2
|
+
In order to minimize network traffic
|
3
|
+
As a user of the library
|
4
|
+
I want to be able to batch changes (Add/Update/Delete) and persist the batch instead of one at a time
|
5
|
+
|
6
|
+
Background:
|
7
|
+
Given an ODataService exists with uri: "http://localhost:8888/SampleService/Entities.svc"
|
8
|
+
And blueprints exist for the service
|
9
|
+
|
10
|
+
Scenario: Save Changes should allow for batch additions
|
11
|
+
Given I call "AddToProducts" on the service with a new "Product" object with Name: "Product 1"
|
12
|
+
And I call "AddToProducts" on the service with a new "Product" object with Name: "Product 2"
|
13
|
+
When I save changes
|
14
|
+
Then the save result should equal: "true"
|
15
|
+
When I call "Products" on the service
|
16
|
+
And I order by: "Name"
|
17
|
+
And I run the query
|
18
|
+
Then the result should be:
|
19
|
+
| Name |
|
20
|
+
| Product 1 |
|
21
|
+
| Product 2 |
|
22
|
+
|
23
|
+
Scenario: Save Changes should allow for batch updates
|
24
|
+
Given I call "AddToProducts" on the service with a new "Product" object with Name: "Product 1"
|
25
|
+
And I call "AddToProducts" on the service with a new "Product" object with Name: "Product 2"
|
26
|
+
When I save changes
|
27
|
+
When I call "Products" on the service
|
28
|
+
And I filter the query with: "Name eq 'Product 1'"
|
29
|
+
And I run the query
|
30
|
+
And I set "Name" on the result to "Product 1 - Updated"
|
31
|
+
And I call "update_object" on the service with the last query result
|
32
|
+
When I call "Products" on the service
|
33
|
+
And I filter the query with: "Name eq 'Product 2'"
|
34
|
+
And I run the query
|
35
|
+
And I set "Name" on the result to "Product 2 - Updated"
|
36
|
+
And I call "update_object" on the service with the last query result
|
37
|
+
When I save changes
|
38
|
+
When I call "Products" on the service
|
39
|
+
And I order by: "Name"
|
40
|
+
And I run the query
|
41
|
+
Then the result should be:
|
42
|
+
| Name |
|
43
|
+
| Product 1 - Updated |
|
44
|
+
| Product 2 - Updated |
|
45
|
+
|
46
|
+
Scenario: Save Changes should allow for batch deletes
|
47
|
+
Given I call "AddToProducts" on the service with a new "Product" object with Name: "Product 1"
|
48
|
+
And I call "AddToProducts" on the service with a new "Product" object with Name: "Product 2"
|
49
|
+
And I call "AddToProducts" on the service with a new "Product" object with Name: "Product 3"
|
50
|
+
And I call "AddToProducts" on the service with a new "Product" object with Name: "Product 4"
|
51
|
+
When I save changes
|
52
|
+
When I call "Products" on the service
|
53
|
+
And I filter the query with: "Name eq 'Product 2'"
|
54
|
+
And I run the query
|
55
|
+
And I call "delete_object" on the service with the last query result
|
56
|
+
When I call "Products" on the service
|
57
|
+
And I filter the query with: "Name eq 'Product 3'"
|
58
|
+
And I run the query
|
59
|
+
And I call "delete_object" on the service with the last query result
|
60
|
+
When I save changes
|
61
|
+
When I call "Products" on the service
|
62
|
+
And I order by: "Name"
|
63
|
+
And I run the query
|
64
|
+
Then the result should be:
|
65
|
+
| Name |
|
66
|
+
| Product 1 |
|
67
|
+
| Product 4 |
|
68
|
+
|
69
|
+
Scenario: Save Changes should allow for a mix of adds, updates, and deletes to be batched
|
70
|
+
Given the following Products exist:
|
71
|
+
| Name |
|
72
|
+
| Product 1 |
|
73
|
+
| Product 2 |
|
74
|
+
And I call "AddToProducts" on the service with a new "Product" object with Name: "Product 3"
|
75
|
+
And I call "AddToProducts" on the service with a new "Product" object with Name: "Product 4"
|
76
|
+
When I call "Products" on the service
|
77
|
+
And I filter the query with: "Name eq 'Product 1'"
|
78
|
+
And I run the query
|
79
|
+
And I set "Name" on the result to "Product 1 - Updated"
|
80
|
+
And I call "update_object" on the service with the last query result
|
81
|
+
When I call "Products" on the service
|
82
|
+
And I filter the query with: "Name eq 'Product 2'"
|
83
|
+
And I run the query
|
84
|
+
And I call "delete_object" on the service with the last query result
|
85
|
+
When I save changes
|
86
|
+
When I call "Products" on the service
|
87
|
+
And I order by: "Name"
|
88
|
+
And I run the query
|
89
|
+
Then the result should be:
|
90
|
+
| Name |
|
91
|
+
| Product 1 - Updated |
|
92
|
+
| Product 3 |
|
93
|
+
| Product 4 |
|
94
|
+
|
@@ -137,17 +137,38 @@ Then /^no "([^\"]*)" should exist$/ do |collection|
|
|
137
137
|
results.should == []
|
138
138
|
end
|
139
139
|
|
140
|
-
Given /^the following (.*) exist:$/ do |plural_factory, table|
|
140
|
+
Given /^the following (.*) exist:$/ do |plural_factory, table|
|
141
|
+
# table is a Cucumber::Ast::Table
|
142
|
+
factory = plural_factory.singularize
|
143
|
+
table.hashes.map do |hash|
|
144
|
+
obj = factory.constantize.send(:make, hash)
|
145
|
+
@service.send("AddTo#{plural_factory}", obj)
|
146
|
+
@service.save_changes
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
Then /^the result should be:$/ do |table|
|
141
151
|
# table is a Cucumber::Ast::Table
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
152
|
+
|
153
|
+
fields = table.hashes[0].keys
|
154
|
+
|
155
|
+
# Build an array of hashes so that we can compare tables
|
156
|
+
results = []
|
157
|
+
|
158
|
+
@service_result.each do |result|
|
159
|
+
obj_hash = Hash.new
|
160
|
+
fields.each do |field|
|
161
|
+
obj_hash[field] = result.send(field)
|
162
|
+
end
|
163
|
+
results << obj_hash
|
147
164
|
end
|
165
|
+
|
166
|
+
result_table = Cucumber::Ast::Table.new(results)
|
167
|
+
|
168
|
+
table.diff!(result_table)
|
148
169
|
end
|
149
170
|
|
150
|
-
Then /^the result should be:$/ do |table|
|
171
|
+
Then /^the save result should be:$/ do |table|
|
151
172
|
# table is a Cucumber::Ast::Table
|
152
173
|
|
153
174
|
fields = table.hashes[0].keys
|
@@ -155,7 +176,7 @@ Then /^the result should be:$/ do |table|
|
|
155
176
|
# Build an array of hashes so that we can compare tables
|
156
177
|
results = []
|
157
178
|
|
158
|
-
@
|
179
|
+
@saved_result.each do |result|
|
159
180
|
obj_hash = Hash.new
|
160
181
|
fields.each do |field|
|
161
182
|
obj_hash[field] = result.send(field)
|
data/lib/ruby_odata/service.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
1
3
|
module OData
|
2
4
|
|
3
5
|
class Service
|
@@ -9,6 +11,7 @@ class Service
|
|
9
11
|
def initialize(service_uri)
|
10
12
|
@uri = service_uri
|
11
13
|
@collections = get_collections
|
14
|
+
@save_operations = []
|
12
15
|
build_classes
|
13
16
|
end
|
14
17
|
|
@@ -24,7 +27,7 @@ class Service
|
|
24
27
|
elsif name.to_s =~ /^AddTo(.*)/
|
25
28
|
type = $1
|
26
29
|
if @collections.include?(type)
|
27
|
-
@
|
30
|
+
@save_operations << Operation.new("Add", $1, args[0])
|
28
31
|
else
|
29
32
|
super
|
30
33
|
end
|
@@ -43,7 +46,7 @@ class Service
|
|
43
46
|
def delete_object(obj)
|
44
47
|
type = obj.class.to_s
|
45
48
|
if obj.respond_to?(:__metadata) && !obj.send(:__metadata).nil?
|
46
|
-
@
|
49
|
+
@save_operations << Operation.new("Delete", type, obj)
|
47
50
|
else
|
48
51
|
raise "You cannot delete a non-tracked entity"
|
49
52
|
end
|
@@ -58,7 +61,7 @@ class Service
|
|
58
61
|
def update_object(obj)
|
59
62
|
type = obj.class.to_s
|
60
63
|
if obj.respond_to?(:__metadata) && !obj.send(:__metadata).nil?
|
61
|
-
@
|
64
|
+
@save_operations << Operation.new("Update", type, obj)
|
62
65
|
else
|
63
66
|
raise "You cannot update a non-tracked entity"
|
64
67
|
end
|
@@ -66,27 +69,20 @@ class Service
|
|
66
69
|
|
67
70
|
# Performs save operations (Create/Update/Delete) against the server
|
68
71
|
def save_changes
|
69
|
-
return nil if @
|
72
|
+
return nil if @save_operations.empty?
|
70
73
|
|
71
74
|
result = nil
|
72
|
-
|
73
|
-
if @
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
result = build_classes_from_result(post_result)
|
78
|
-
elsif @save_operation.kind == "Update"
|
79
|
-
update_uri = @save_operation.klass.send(:__metadata)[:uri]
|
80
|
-
json_klass = @save_operation.klass.to_json
|
81
|
-
update_result = RestClient.put update_uri, json_klass, :content_type => :json
|
82
|
-
return (update_result.code == 204)
|
83
|
-
elsif @save_operation.kind == "Delete"
|
84
|
-
delete_uri = @save_operation.klass.send(:__metadata)[:uri]
|
85
|
-
delete_result = RestClient.delete delete_uri
|
86
|
-
return (delete_result.code == 204)
|
75
|
+
|
76
|
+
if @save_operations.length == 1
|
77
|
+
result = single_save(@save_operations[0])
|
78
|
+
else
|
79
|
+
result = batch_save(@save_operations)
|
87
80
|
end
|
88
|
-
|
89
|
-
|
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
|
+
|
90
86
|
return result
|
91
87
|
end
|
92
88
|
|
@@ -202,6 +198,90 @@ class Service
|
|
202
198
|
# Add the property
|
203
199
|
klass.send "#{property_name}=", inline_klass
|
204
200
|
end
|
201
|
+
def single_save(operation)
|
202
|
+
if operation.kind == "Add"
|
203
|
+
save_uri = "#{@uri}/#{operation.klass_name}"
|
204
|
+
json_klass = operation.klass.to_json(:type => :add)
|
205
|
+
post_result = RestClient.post save_uri, json_klass, :content_type => :json
|
206
|
+
return build_classes_from_result(post_result)
|
207
|
+
elsif operation.kind == "Update"
|
208
|
+
update_uri = operation.klass.send(:__metadata)[:uri]
|
209
|
+
json_klass = operation.klass.to_json
|
210
|
+
update_result = RestClient.put update_uri, json_klass, :content_type => :json
|
211
|
+
return (update_result.code == 204)
|
212
|
+
elsif operation.kind == "Delete"
|
213
|
+
delete_uri = operation.klass.send(:__metadata)[:uri]
|
214
|
+
delete_result = RestClient.delete delete_uri
|
215
|
+
return (delete_result.code == 204)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
def batch_save(operations)
|
219
|
+
batch_num = generate_guid
|
220
|
+
changeset_num = generate_guid
|
221
|
+
batch_uri = "#{@uri}/$batch"
|
222
|
+
|
223
|
+
body = build_batch_body(operations, batch_num, changeset_num)
|
224
|
+
|
225
|
+
result = RestClient.post batch_uri, body, :content_type => "multipart/mixed; boundary=batch_#{batch_num}"
|
226
|
+
|
227
|
+
# TODO: More result validation needs to be done.
|
228
|
+
# The result returns HTTP 202 even if there is an error in the batch
|
229
|
+
return (result.code == 202)
|
230
|
+
end
|
231
|
+
def build_batch_body(operations, batch_num, changeset_num)
|
232
|
+
# Header
|
233
|
+
body = "--batch_#{batch_num}\n"
|
234
|
+
body << "Content-Type: multipart/mixed;boundary=changeset_#{changeset_num}\n\n"
|
235
|
+
|
236
|
+
# Operations
|
237
|
+
operations.each do |operation|
|
238
|
+
body << build_batch_operation(operation, changeset_num)
|
239
|
+
body << "\n"
|
240
|
+
end
|
241
|
+
|
242
|
+
# Footer
|
243
|
+
body << "\n\n--changeset_#{changeset_num}--\n"
|
244
|
+
body << "--batch_#{batch_num}--"
|
245
|
+
|
246
|
+
return body
|
247
|
+
end
|
248
|
+
|
249
|
+
def build_batch_operation(operation, changeset_num)
|
250
|
+
accept_headers = "Accept-Charset: utf-8\n"
|
251
|
+
accept_headers << "Content-Type: application/json;charset=utf-8\n" unless operation.kind == "Delete"
|
252
|
+
accept_headers << "\n"
|
253
|
+
|
254
|
+
content = "--changeset_#{changeset_num}\n"
|
255
|
+
content << "Content-Type: application/http\n"
|
256
|
+
content << "Content-Transfer-Encoding: binary\n\n"
|
257
|
+
|
258
|
+
if operation.kind == "Add"
|
259
|
+
save_uri = "#{@uri}/#{operation.klass_name}"
|
260
|
+
json_klass = operation.klass.to_json(:type => :add)
|
261
|
+
|
262
|
+
content << "POST #{save_uri} HTTP/1.1\n"
|
263
|
+
content << accept_headers
|
264
|
+
content << json_klass
|
265
|
+
elsif operation.kind == "Update"
|
266
|
+
update_uri = operation.klass.send(:__metadata)[:uri]
|
267
|
+
json_klass = operation.klass.to_json
|
268
|
+
|
269
|
+
content << "PUT #{update_uri} HTTP/1.1\n"
|
270
|
+
content << accept_headers
|
271
|
+
content << json_klass
|
272
|
+
elsif operation.kind == "Delete"
|
273
|
+
delete_uri = operation.klass.send(:__metadata)[:uri]
|
274
|
+
|
275
|
+
content << "DELETE #{delete_uri} HTTP/1.1\n"
|
276
|
+
content << accept_headers
|
277
|
+
end
|
278
|
+
|
279
|
+
return content
|
280
|
+
end
|
281
|
+
|
282
|
+
def generate_guid
|
283
|
+
rand(36**12).to_s(36).insert(4, "-").insert(9, "-")
|
284
|
+
end
|
205
285
|
end
|
206
286
|
|
207
287
|
end # module OData
|
data/ruby_odata.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{ruby_odata}
|
8
|
-
s.version = "0.0.
|
8
|
+
s.version = "0.0.6"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Damien White"]
|
12
|
-
s.date = %q{2010-06-
|
12
|
+
s.date = %q{2010-06-17}
|
13
13
|
s.description = %q{An OData Client Library for Ruby. Use this to interact with OData services}
|
14
14
|
s.email = %q{damien.white@visoftinc.com}
|
15
15
|
s.extra_rdoc_files = [
|
@@ -25,6 +25,7 @@ Gem::Specification.new do |s|
|
|
25
25
|
"Rakefile",
|
26
26
|
"VERSION",
|
27
27
|
"config/cucumber.yml",
|
28
|
+
"features/batch_request.feature",
|
28
29
|
"features/query_builder.feature",
|
29
30
|
"features/service.feature",
|
30
31
|
"features/service_manage.feature",
|
@@ -1,9 +1,11 @@
|
|
1
1
|
using System.Data.Services;
|
2
2
|
using System.Data.Services.Common;
|
3
|
+
using System.ServiceModel;
|
3
4
|
using System.ServiceModel.Web;
|
4
5
|
using System.Web;
|
5
6
|
using Model;
|
6
7
|
|
8
|
+
[ServiceBehavior(IncludeExceptionDetailInFaults = true)]
|
7
9
|
public class Entities : DataService< ModelContainer >
|
8
10
|
{
|
9
11
|
// This method is called only once to initialize service-wide policies.
|
@@ -12,6 +14,7 @@ public class Entities : DataService< ModelContainer >
|
|
12
14
|
config.SetEntitySetAccessRule("*", EntitySetRights.All);
|
13
15
|
config.SetServiceOperationAccessRule("*", ServiceOperationRights.All);
|
14
16
|
config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
|
17
|
+
config.UseVerboseErrors = true;
|
15
18
|
}
|
16
19
|
|
17
20
|
/// <summary>
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby_odata
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 19
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 0
|
9
|
-
-
|
10
|
-
version: 0.0.
|
9
|
+
- 6
|
10
|
+
version: 0.0.6
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Damien White
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2010-06-
|
18
|
+
date: 2010-06-17 00:00:00 -04:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
@@ -84,6 +84,7 @@ files:
|
|
84
84
|
- Rakefile
|
85
85
|
- VERSION
|
86
86
|
- config/cucumber.yml
|
87
|
+
- features/batch_request.feature
|
87
88
|
- features/query_builder.feature
|
88
89
|
- features/service.feature
|
89
90
|
- features/service_manage.feature
|