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.
@@ -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