ruby_odata 0.0.9 → 0.0.10

Sign up to get free protection for your applications and to get access to all the features.
@@ -54,4 +54,18 @@
54
54
  * Bug Fixes
55
55
  * Fixed issue with passing a service URL with a trailing slash
56
56
  * Other
57
- * Cleaned up testing by adding a default task to the Rakefile that runs RSpec and Cucumber
57
+ * Cleaned up testing by adding a default task to the Rakefile that runs RSpec and Cucumber
58
+
59
+ === 0.0.10
60
+ * New Features
61
+ * Added the ability to pass additional parameters that are appended to the query string for requests
62
+ * Added initial support for feed customizations (SyndicationTitle and SyndicationSummary)
63
+ * Enhanced ruby_odata's awareness of classes based on the metadata instead of relying on results that are returned
64
+ * Bug Fixes
65
+ * Fixed issues with nested collections (eager loading)
66
+ * Handled ArgumentError on the Time.parse for older versions of Ruby; used DateTime.parse instead if Time.parse fails
67
+ * Removed the camelize method call when building the root URL for collections (Reported by mkoegel, issue #3 on github)
68
+ * Handled building results (classes) where the category element is missing but there is a title element instead. (Reported by mkoegel, issue #3 on github in the comments)
69
+ * Other
70
+ * Change HTTP port to 8989 since 8888 conflicts with the Intel AppStore
71
+ * Refactored service step for HTTP calls where the service address is defined within the step making it easier to make changes in the future.
@@ -6,10 +6,13 @@ The <b>Open Data Protocol</b> (OData) is a fantastic way to query and update dat
6
6
 
7
7
  * Source Code (hosted on GitHub): http://github.com/visoft/ruby_odata
8
8
  * Documentation (hosted on rdoc.info): http://rdoc.info/projects/visoft/ruby_odata
9
- * Issue tracking (hosted on Lighthouse): http://visoft.lighthouseapp.com/projects/54309/home
9
+ * Issue tracking (hosted on GitHub): https://github.com/visoft/ruby_odata/issues
10
10
  * Wiki (hosted on GitHub): http://wiki.github.com/visoft/ruby_odata/
11
11
  * Gem (hosted on Gemcutter): http://gemcutter.org/gems/ruby_odata
12
12
  * Blog articles: {Introducing a Ruby OData Client Library}[http://bit.ly/IntroRubyOData]
13
+ {Ruby OData Update v0.0.7}[http://bit.ly/ruby_odata007]
14
+ {Ruby OData Update v0.0.8}[http://bit.ly/ruby_odata008]
15
+ {Ruby OData Update v0.0.10}[http://bit.ly/ruby_odata0010]
13
16
 
14
17
  == Installation
15
18
  You can install ruby_odata as a gem using:
@@ -17,12 +20,19 @@ You can install ruby_odata as a gem using:
17
20
 
18
21
  == Usage
19
22
 
23
+ === Instantiating the Service
24
+ There are various options that you can pass when creating an instance of the service, these include:
25
+ * username: username for http basic auth
26
+ * password: password for http basic auth
27
+ * verify_ssl: false if no verification, otherwise mode (OpenSSL::SSL::VERIFY_PEER is default)
28
+ * additional_params: a hash of query string params that will be passed on all calls (query, new, update, delete, batch)
29
+
20
30
  === Adding
21
31
  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:
22
32
 
23
33
  require 'ruby_odata'
24
34
 
25
- svc = OData::Service.new "http://127.0.0.1:8888/SampleService/Entities.svc"
35
+ svc = OData::Service.new "http://127.0.0.1:8989/SampleService/Entities.svc"
26
36
  new_category = Category.new
27
37
  new_category.Name = "Sample Category"
28
38
  svc.AddToCategories(new_category)
@@ -34,7 +44,7 @@ To update an object, simply pass the modified object to the update_object method
34
44
 
35
45
  require 'ruby_odata'
36
46
 
37
- svc = OData::Service.new "http://127.0.0.1:8888/SampleService/Entities.svc"
47
+ svc = OData::Service.new "http://127.0.0.1:8989/SampleService/Entities.svc"
38
48
  new_category = Category.new
39
49
  new_category.Name = "Sample Category"
40
50
  svc.AddToCategories(new_category)
@@ -51,7 +61,7 @@ Deleting an object involves passing the tracked object to the delete_object meth
51
61
 
52
62
  require 'ruby_odata'
53
63
 
54
- svc = OData::Service.new "http://127.0.0.1:8888/SampleService/Entities.svc"
64
+ svc = OData::Service.new "http://127.0.0.1:8989/SampleService/Entities.svc"
55
65
  new_category = Category.new
56
66
  new_category.Name = "Sample Category"
57
67
  svc.AddToCategories(new_category)
@@ -67,7 +77,7 @@ Querying is easy, for example to pull all the categories from the SampleService,
67
77
 
68
78
  require 'ruby_odata'
69
79
 
70
- svc = OData::Service.new "http://127.0.0.1:8888/SampleService/Entities.svc"
80
+ svc = OData::Service.new "http://127.0.0.1:8989/SampleService/Entities.svc"
71
81
  svc.Categories
72
82
  categories = svc.execute
73
83
  puts categories.to_json
@@ -159,7 +169,7 @@ Basic HTTP Authentication is supported via sending a username and password as se
159
169
 
160
170
  require 'ruby_odata'
161
171
 
162
- svc = OData::Service.new "http://127.0.0.1:8888/SampleService/Entities.svc", { :username => "bob", :password=> "12345" }
172
+ svc = OData::Service.new "http://127.0.0.1:8989/SampleService/Entities.svc", { :username => "bob", :password=> "12345" }
163
173
 
164
174
  === SSL/https Certificate Verification
165
175
  The certificate verification mode can be passed in the options hash via the :verify_ssl key. For example, to ignore verification in order to use a self-signed certificate:
@@ -177,7 +187,7 @@ Or an OpenSSL integer constant can be passed as well:
177
187
  Default verification is OpenSSL::SSL::VERIFY_PEER. Note due to the way Ruby's Request object implements certificate checking, you CAN NOT pass OpenSSL::SSL::VERIFY_NONE, you must instead pass a boolean false.
178
188
 
179
189
  == Tests
180
- All of the tests are written using Cucumber going against a sample service (Found in /test/SampleService/*). The SampleService is an ASP.NET Web Site running a SQLEXPRESS 2008 R2 Database (TestDB), as well as the ADO.NET Entity Framework and a WCF Data Service. In order to run the tests, you need to spin up the SampleService and have it running on port 8888 (http://localhost:8888/SampleService).
190
+ All of the tests are written using Cucumber going against a sample service (Found in /test/SampleService/*). The SampleService is an ASP.NET Web Site running a SQLEXPRESS 2008 R2 Database (TestDB), as well as the ADO.NET Entity Framework and a WCF Data Service. In order to run the tests, you need to spin up the SampleService and have it running on port 8989 (http://localhost:8989/SampleService).
181
191
 
182
192
  One way to do this is to open the SampleService within Visual Studio 2010 and run it from there (must use IIS instead of development web server in order for basic auth tests to pass, however). Another option is to use IIS Express. It is a free download from Microsoft. Once installed, there is a batch file found in /test called "iisExpress x64.bat", you can run the batch file and just close the command window. There is a also an "iisExpress x86.bat" file for those of you running a 32-bit machine. The only difference is the path to the Program Files directory. Once you run the batch file, the web server will spin up and you can find the instance in your systray just like if Visual Studio ran it for you. To stop the server, right click on the icon in your systray and tell it to stop.
183
193
 
@@ -1,7 +1,7 @@
1
1
  Feature: Service Should Access Basic Auth Protected Resources
2
2
 
3
3
  Background:
4
- Given an ODataService exists with uri: "http://localhost:8888/SampleService/BasicAuth/Entities.svc" using username "admin" and password "passwd"
4
+ Given an ODataService exists with uri: "http://localhost:8989/SampleService/BasicAuth/Entities.svc" using username "admin" and password "passwd"
5
5
  And blueprints exist for the service
6
6
 
7
7
  Scenario: Service should respond to valid collections
@@ -16,9 +16,9 @@ Scenario: Entity should fill values on protected resource
16
16
  And the method "Name" on the result should equal: "Auth Test Category"
17
17
 
18
18
  Scenario: Should get 401 if invalid credentials provided to protected URL
19
- Given an ODataService exists with uri: "http://localhost:8888/SampleService/BasicAuth/Entities.svc" using username "admin" and password "bad_pwd" it should throw an exception with message "401 Unauthorized"
19
+ Given an ODataService exists with uri: "http://localhost:8989/SampleService/BasicAuth/Entities.svc" using username "admin" and password "bad_pwd" it should throw an exception with message "401 Unauthorized"
20
20
 
21
21
  Scenario: Should get 401 if no credentials provided to protected URL
22
- Given an ODataService exists with uri: "http://localhost:8888/SampleService/BasicAuth/Entities.svc" it should throw an exception with message "401 Unauthorized"
22
+ Given an ODataService exists with uri: "http://localhost:8989/SampleService/BasicAuth/Entities.svc" it should throw an exception with message "401 Unauthorized"
23
23
 
24
24
 
@@ -4,7 +4,7 @@ Feature: Batch request
4
4
  I want to be able to batch changes (Add/Update/Delete) and persist the batch instead of one at a time
5
5
 
6
6
  Background:
7
- Given an ODataService exists with uri: "http://localhost:8888/SampleService/Entities.svc"
7
+ Given a sample HTTP ODataService exists
8
8
  And blueprints exist for the service
9
9
 
10
10
  Scenario: Save Changes should allow for batch additions
@@ -4,7 +4,7 @@ Feature: Complex types
4
4
  I want to be able to manage objects with complex types
5
5
 
6
6
  Background:
7
- Given an ODataService exists with uri: "http://localhost:8888/SampleService/Entities.svc"
7
+ Given a sample HTTP ODataService exists
8
8
  And blueprints exist for the service
9
9
 
10
10
  Scenario: The proxy must generate classes for complex types if they exist
@@ -4,7 +4,7 @@ Feature: Query Builder
4
4
  I want to be able to perform valid OData protocol operations
5
5
 
6
6
  Background:
7
- Given an ODataService exists with uri: "http://localhost:8888/SampleService/Entities.svc"
7
+ Given a sample HTTP ODataService exists
8
8
  And blueprints exist for the service
9
9
 
10
10
  # Expand
@@ -4,7 +4,7 @@ Feature: Service Should Generate a Proxy
4
4
  I want to be able to access data
5
5
 
6
6
  Background:
7
- Given an ODataService exists with uri: "http://localhost:8888/SampleService/Entities.svc"
7
+ Given a sample HTTP ODataService exists
8
8
  And blueprints exist for the service
9
9
 
10
10
  Scenario: Service should respond to valid collections
@@ -4,7 +4,7 @@ Feature: Service management
4
4
  I want to be able to add, edit, and delete entities
5
5
 
6
6
  Background:
7
- Given an ODataService exists with uri: "http://localhost:8888/SampleService/Entities.svc"
7
+ Given an ODataService exists with uri: "http://localhost:8989/SampleService/Entities.svc"
8
8
  And blueprints exist for the service
9
9
 
10
10
  Scenario: Service should respond to AddToEntityName for adding objects
@@ -2,6 +2,10 @@ Given /^an ODataService exists with uri: "([^\"]*)"$/ do |uri|
2
2
  @service = OData::Service.new(uri)
3
3
  end
4
4
 
5
+ Given /^a sample HTTP ODataService exists$/ do
6
+ @service = OData::Service.new("http://localhost:#{HTTP_PORT_NUMBER}/SampleService/Entities.svc")
7
+ end
8
+
5
9
  Given /^an ODataService exists with uri: "([^\"]*)" using username "([^\"]*)" and password "([^\"]*)"$/ do |uri, username, password|
6
10
  @service = OData::Service.new(uri, { :username => username, :password => password })
7
11
  end
@@ -11,4 +11,6 @@ root_dir = File.expand_path(File.join(File.dirname(__FILE__), "../..", "test/Sam
11
11
  if !File.exists?("#{root_dir}/TestDB.mdf")
12
12
  FileUtils.copy("#{root_dir}/_TestDB.mdf", "#{root_dir}/TestDB.mdf")
13
13
  FileUtils.copy("#{root_dir}/_TestDB_Log.ldf", "#{root_dir}/TestDB_Log.ldf")
14
- end
14
+ end
15
+
16
+ HTTP_PORT_NUMBER = 8989
@@ -1,4 +1,4 @@
1
1
  Before do
2
2
  Sham.reset
3
- RestClient.post "http://localhost:8888/SampleService/Entities.svc/CleanDatabaseForTesting", {}
3
+ RestClient.post "http://localhost:#{HTTP_PORT_NUMBER}/SampleService/Entities.svc/CleanDatabaseForTesting", {}
4
4
  end
@@ -4,7 +4,7 @@ Feature: Type conversion
4
4
  I want types returned to be accurately represented
5
5
 
6
6
  Background:
7
- Given an ODataService exists with uri: "http://localhost:8888/SampleService/Entities.svc"
7
+ Given a sample HTTP ODataService exists
8
8
  And blueprints exist for the service
9
9
 
10
10
  Scenario: Integers should be Fixnums
@@ -11,6 +11,7 @@ require 'nokogiri'
11
11
  require 'bigdecimal'
12
12
  require 'bigdecimal/util'
13
13
 
14
+ require lib + '/ruby_odata/property_metadata'
14
15
  require lib + '/ruby_odata/query_builder'
15
16
  require lib + '/ruby_odata/class_builder'
16
17
  require lib + '/ruby_odata/operation'
@@ -0,0 +1,28 @@
1
+ module OData
2
+ # Internally used helper class for storing an entity property's metadata. This class shouldn't be used directly.
3
+ class PropertyMetadata
4
+ # The property name
5
+ attr_accessor :name
6
+ # The property EDM type
7
+ attr_accessor :type
8
+ # Is the property nullable?
9
+ attr_accessor :nullable
10
+ # Feed customization target path
11
+ attr_accessor :fc_target_path
12
+ # Should the property appear in both the mapped schema path and the properties collection
13
+ attr_accessor :fc_keep_in_content
14
+
15
+ # Creates a new instance of the Class Property class
16
+ #
17
+ # ==== Required Attributes
18
+ # property_element: The property element from the EDMX
19
+
20
+ def initialize(property_element)
21
+ @name = property_element['Name']
22
+ @type = property_element['Type']
23
+ @nullable = (property_element['Nullable'] && property_element['Nullable'] == "true") || false
24
+ @fc_target_path = property_element['FC_TargetPath']
25
+ @fc_keep_in_content = (property_element['FC_KeepInContent']) ? (property_element['FC_KeepInContent'] == "true") : nil
26
+ end
27
+ end
28
+ end
@@ -2,7 +2,7 @@ module OData
2
2
  # The query builder is used to call query operations against the service. This shouldn't be called directly, but rather it is returned from the dynamic methods created for the specific service that you are calling.
3
3
  #
4
4
  # For example, given the following code snippet:
5
- # svc = OData::Service.new "http://127.0.0.1:8888/SampleService/Entities.svc"
5
+ # svc = OData::Service.new "http://127.0.0.1:8989/SampleService/Entities.svc"
6
6
  # svc.Categories
7
7
  # The *Categories* method would return a QueryBuilder
8
8
  class QueryBuilder
@@ -10,13 +10,16 @@ class QueryBuilder
10
10
  #
11
11
  # ==== Required Attributes
12
12
  # - root: The root entity collection to query against
13
- def initialize(root)
13
+ # ==== Optional
14
+ # Hash of additional parameters to use for a query
15
+ def initialize(root, additional_params = {})
14
16
  @root = root.to_s
15
17
  @expands = []
16
18
  @filters = []
17
19
  @order_bys = []
18
20
  @skip = nil
19
21
  @top = nil
22
+ @additional_params = additional_params
20
23
  end
21
24
 
22
25
  # Used to eagerly-load data for nested objects, for example, obtaining a Category for a Product within one call to the server
@@ -96,11 +99,12 @@ class QueryBuilder
96
99
  query_options << "$orderby=#{@order_bys.join(',')}" unless @order_bys.empty?
97
100
  query_options << "$skip=#{@skip}" unless @skip.nil?
98
101
  query_options << "$top=#{@top}" unless @top.nil?
102
+ query_options << @additional_params.to_query unless @additional_params.empty?
99
103
  if !query_options.empty?
100
104
  q << "?"
101
105
  q << query_options.join('&')
102
106
  end
103
- return q
107
+ return q
104
108
  end
105
109
  end
106
110
 
@@ -1,9 +1,7 @@
1
- require 'logger'
2
-
3
1
  module OData
4
2
 
5
3
  class Service
6
- attr_reader :classes
4
+ attr_reader :classes, :class_metadata, :options
7
5
  # Creates a new instance of the Service class
8
6
  #
9
7
  # ==== Required Attributes
@@ -12,12 +10,14 @@ class Service
12
10
  # - username: username for http basic auth
13
11
  # - password: password for http basic auth
14
12
  # - verify_ssl: false if no verification, otherwise mode (OpenSSL::SSL::VERIFY_PEER is default)
13
+ # - additional_params: a hash of query string params that will be passed on all calls
15
14
  def initialize(service_uri, options = {})
16
15
  @uri = service_uri.gsub!(/\/?$/, '')
17
16
  @options = options
18
17
  @rest_options = { :verify_ssl => get_verify_mode, :user => @options[:username], :password => @options[:password] }
19
18
  @collections = []
20
19
  @save_operations = []
20
+ @additional_params = options[:additional_params] || {}
21
21
  build_collections_and_classes
22
22
  end
23
23
 
@@ -25,9 +25,9 @@ class Service
25
25
  def method_missing(name, *args)
26
26
  # Queries
27
27
  if @collections.include?(name.to_s)
28
- root = "/#{name.to_s.camelize}"
28
+ root = "/#{name.to_s}"
29
29
  root << "(#{args.join(',')})" unless args.empty?
30
- @query = QueryBuilder.new(root)
30
+ @query = QueryBuilder.new(root, @additional_params)
31
31
  return @query
32
32
  # Adds
33
33
  elsif name.to_s =~ /^AddTo(.*)/
@@ -129,7 +129,9 @@ class Service
129
129
  # Build the classes required by the metadata
130
130
  def build_collections_and_classes
131
131
  @classes = Hash.new
132
- doc = Nokogiri::XML(RestClient::Resource.new("#{@uri}/$metadata", @rest_options).get)
132
+ @class_metadata = Hash.new # This is used to store property information about a class
133
+
134
+ doc = Nokogiri::XML(RestClient::Resource.new(build_metadata_uri, @rest_options).get)
133
135
 
134
136
  # Get the edm namespace
135
137
  edm_ns = doc.xpath("edmx:Edmx/edmx:DataServices/*", "edmx" => "http://schemas.microsoft.com/ado/2007/06/edmx").first.namespaces['xmlns'].to_s
@@ -151,12 +153,22 @@ class Service
151
153
  entity_types.each do |e|
152
154
  name = e['Name']
153
155
  props = e.xpath(".//edm:Property", "edm" => edm_ns)
156
+ @class_metadata[name] = build_property_metadata(props)
154
157
  methods = props.collect { |p| p['Name'] } # Standard Properties
155
158
  nprops = e.xpath(".//edm:NavigationProperty", "edm" => edm_ns)
156
- nav_props = nprops.collect { |p| p['Name'] } # Standard Properties
159
+ nav_props = nprops.collect { |p| p['Name'] } # Navigation Properties
157
160
  @classes[name] = ClassBuilder.new(name, methods, nav_props).build unless @classes.keys.include?(name)
158
161
  end
159
162
  end
163
+
164
+ def build_property_metadata(props)
165
+ metadata = {}
166
+ props.each do |property_element|
167
+ prop_meta = PropertyMetadata.new(property_element)
168
+ metadata[prop_meta.name] = prop_meta
169
+ end
170
+ metadata
171
+ end
160
172
 
161
173
  # Helper to loop through a result and create an instance for each entity in the results
162
174
  def build_classes_from_result(result)
@@ -175,10 +187,22 @@ class Service
175
187
  def entry_to_class(entry)
176
188
  # Retrieve the class name from the fully qualified name (the last string after the last dot)
177
189
  klass_name = entry.xpath("./atom:category/@term", "atom" => "http://www.w3.org/2005/Atom").to_s.split('.')[-1]
178
- return nil if klass_name.empty?
179
-
180
- properties = entry.xpath(".//m:properties/*", { "m" => "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" })
190
+
191
+ # Is the category missing? See if there is a title that we can use to build the class
192
+ if klass_name.nil?
193
+ title = entry.xpath("./atom:title", "atom" => "http://www.w3.org/2005/Atom").first
194
+ return nil if title.nil?
195
+ klass_name = title.content.to_s
196
+ end
181
197
 
198
+ return nil if klass_name.nil?
199
+
200
+ # If we are working against a child (inline) entry, we need to use the more generic xpath because a child entry WILL
201
+ # have properties that are ancestors of m:inline. Check if there is an m:inline child to determine the xpath query to use
202
+ has_inline = entry.xpath(".//m:inline", { "m" => "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" }).any?
203
+ properties_xpath = has_inline ? ".//m:properties[not(ancestor::m:inline)]/*" : ".//m:properties/*"
204
+ properties = entry.xpath(properties_xpath, { "m" => "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" })
205
+
182
206
  klass = @classes[klass_name].new
183
207
 
184
208
  # Fill metadata
@@ -191,35 +215,71 @@ class Service
191
215
  klass.send "#{prop_name}=", parse_value(prop)
192
216
  end
193
217
 
218
+ # Fill properties represented outside of the properties collection
219
+ @class_metadata[klass_name].select { |k,v| v.fc_keep_in_content == false }.each do |k, meta|
220
+ if meta.fc_target_path == "SyndicationTitle"
221
+ title = entry.xpath("./atom:title", "atom" => "http://www.w3.org/2005/Atom").first
222
+ klass.send "#{meta.name}=", title.content
223
+ elsif meta.fc_target_path == "SyndicationSummary"
224
+ summary = entry.xpath("./atom:summary", "atom" => "http://www.w3.org/2005/Atom").first
225
+ klass.send "#{meta.name}=", summary.content
226
+ end
227
+ end
228
+
194
229
  inline_links = entry.xpath("./atom:link[m:inline]", { "m" => "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata", "atom" => "http://www.w3.org/2005/Atom" })
195
230
 
196
231
  for link in inline_links
197
232
  inline_entries = link.xpath(".//atom:entry", "atom" => "http://www.w3.org/2005/Atom")
198
-
199
- if inline_entries.length == 1
200
- property_name = link.attributes['title'].to_s
201
-
202
- build_inline_class(klass, inline_entries[0], property_name)
233
+
234
+ # TODO: Use the metadata's associations to determine the multiplicity instead of this "hack"
235
+ property_name = link.attributes['title'].to_s
236
+ if inline_entries.length == 1 && singular?(property_name)
237
+ inline_klass = build_inline_class(klass, inline_entries[0], property_name)
238
+ klass.send "#{property_name}=", inline_klass
203
239
  else
204
- # TODO: Test handling multiple children
240
+ inline_classes = []
205
241
  for inline_entry in inline_entries
206
- property_name = link.xpath("atom:link[@rel='edit']/@title", "atom" => "http://www.w3.org/2005/Atom")
207
-
208
242
  # Build the class
209
243
  inline_klass = entry_to_class(inline_entry)
210
-
211
- # Add the property
212
- klass.send "#{property_name}=", inline_klass
244
+
245
+ # Add the property to the temp collection
246
+ inline_classes << inline_klass
213
247
  end
248
+
249
+ # Assign the array of classes to the property
250
+ property_name = link.xpath("@title", "atom" => "http://www.w3.org/2005/Atom")
251
+ klass.send "#{property_name}=", inline_classes
214
252
  end
215
253
  end
216
254
 
217
- return klass
255
+ klass
218
256
  end
219
257
 
258
+ # Build URIs
259
+ def build_metadata_uri
260
+ uri = "#{@uri}/$metadata"
261
+ uri << "?#{@additional_params.to_query}" unless @additional_params.empty?
262
+ uri
263
+ end
220
264
  def build_query_uri
221
265
  "#{@uri}#{@query.query}"
222
266
  end
267
+ def build_save_uri(operation)
268
+ uri = "#{@uri}/#{operation.klass_name}"
269
+ uri << "?#{@additional_params.to_query}" unless @additional_params.empty?
270
+ uri
271
+ end
272
+ def build_resource_uri(operation)
273
+ uri = operation.klass.send(:__metadata)[:uri]
274
+ uri << "?#{@additional_params.to_query}" unless @additional_params.empty?
275
+ uri
276
+ end
277
+ def build_batch_uri
278
+ uri = "#{@uri}/$batch"
279
+ uri << "?#{@additional_params.to_query}" unless @additional_params.empty?
280
+ uri
281
+ end
282
+
223
283
  def build_inline_class(klass, entry, property_name)
224
284
  # Build the class
225
285
  inline_klass = entry_to_class(entry)
@@ -229,17 +289,17 @@ class Service
229
289
  end
230
290
  def single_save(operation)
231
291
  if operation.kind == "Add"
232
- save_uri = "#{@uri}/#{operation.klass_name}"
292
+ save_uri = build_save_uri(operation)
233
293
  json_klass = operation.klass.to_json(:type => :add)
234
294
  post_result = RestClient::Resource.new(save_uri, @rest_options).post json_klass, {:content_type => :json}
235
295
  return build_classes_from_result(post_result)
236
296
  elsif operation.kind == "Update"
237
- update_uri = operation.klass.send(:__metadata)[:uri]
297
+ update_uri = build_resource_uri(operation)
238
298
  json_klass = operation.klass.to_json
239
299
  update_result = RestClient::Resource.new(update_uri, @rest_options).put json_klass, {:content_type => :json}
240
300
  return (update_result.code == 204)
241
301
  elsif operation.kind == "Delete"
242
- delete_uri = operation.klass.send(:__metadata)[:uri]
302
+ delete_uri = build_resource_uri(operation)
243
303
  delete_result = RestClient::Resource.new(delete_uri, @rest_options).delete
244
304
  return (delete_result.code == 204)
245
305
  end
@@ -252,7 +312,7 @@ class Service
252
312
  def batch_save(operations)
253
313
  batch_num = generate_guid
254
314
  changeset_num = generate_guid
255
- batch_uri = "#{@uri}/$batch"
315
+ batch_uri = build_batch_uri
256
316
 
257
317
  body = build_batch_body(operations, batch_num, changeset_num)
258
318
 
@@ -353,14 +413,27 @@ class Service
353
413
 
354
414
  # Assume this is UTC if no timezone is specified
355
415
  sdate = sdate + "Z" unless sdate.match(/Z|([+|-]\d{2}:\d{2})$/)
356
-
357
- return Time.parse(sdate)
416
+
417
+ # This is to handle older versions of Ruby (e.g. ruby 1.8.7 (2010-12-23 patchlevel 330) [i386-mingw32])
418
+ # See http://makandra.com/notes/1017-maximum-representable-value-for-a-ruby-time-object
419
+ # In recent versions of Ruby, Time has a much larger range
420
+ begin
421
+ result = Time.parse(sdate)
422
+ rescue ArgumentError
423
+ result = DateTime.parse(sdate)
424
+ end
425
+
426
+ return result
358
427
  end
359
428
 
360
429
  # If we can't parse the value, just return the element's content
361
430
  property_xml.content
362
431
  end
363
432
 
433
+ # Helpers
434
+ def singular?(value)
435
+ value.singularize == value
436
+ end
364
437
  end
365
438
 
366
439
  end # module OData