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.
@@ -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
+
@@ -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.5
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
- 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
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
- @service_result.each do |result|
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)
@@ -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
- @save_operation = Operation.new("Add", $1, args[0])
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
- @save_operation = Operation.new("Delete", type, obj)
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
- @save_operation = Operation.new("Update", type, obj)
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 @save_operation.nil?
72
+ return nil if @save_operations.empty?
70
73
 
71
74
  result = nil
72
-
73
- if @save_operation.kind == "Add"
74
- save_uri = "#{@uri}/#{@save_operation.klass_name}"
75
- json_klass = @save_operation.klass.to_json(:type => :add)
76
- post_result = RestClient.post save_uri, json_klass, :content_type => :json
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
- @save_operation = nil # Clear out the last operation
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
@@ -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.5"
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-13}
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: 21
4
+ hash: 19
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
8
  - 0
9
- - 5
10
- version: 0.0.5
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-13 00:00:00 -04:00
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