ruby_odata 0.0.5 → 0.0.6
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/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
|