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.
- data/CHANGELOG.rdoc +15 -1
- data/README.rdoc +17 -7
- data/features/basic_auth.feature +3 -3
- data/features/batch_request.feature +1 -1
- data/features/complex_types.feature +1 -1
- data/features/query_builder.feature +1 -1
- data/features/service.feature +1 -1
- data/features/service_manage.feature +1 -1
- data/features/step_definitions/service_steps.rb +4 -0
- data/features/support/env.rb +3 -1
- data/features/support/hooks.rb +1 -1
- data/features/type_conversion.feature +1 -1
- data/lib/ruby_odata.rb +1 -0
- data/lib/ruby_odata/property_metadata.rb +28 -0
- data/lib/ruby_odata/query_builder.rb +7 -3
- data/lib/ruby_odata/service.rb +101 -28
- data/lib/ruby_odata/version.rb +1 -1
- data/ruby_odata.gemspec +1 -0
- data/spec/fixtures/feed_customization/edmx_feed_customization.xml +70 -0
- data/spec/fixtures/feed_customization/result_feed_customization_categories_expand.xml +213 -0
- data/spec/fixtures/feed_customization/result_feed_customization_products_expand.xml +217 -0
- data/spec/fixtures/sap/edmx_sap_demo_flight.xml +61 -0
- data/spec/fixtures/sap/result_sap_demo_flight_missing_category.xml +81 -0
- data/spec/property_metadata_spec.rb +42 -0
- data/spec/query_builder_spec.rb +23 -0
- data/spec/service_spec.rb +224 -0
- data/spec/spec_helper.rb +3 -1
- data/spec/support/describe_private.rb +13 -0
- data/spec/support/element_helpers.rb +7 -0
- data/test/Cassini x64.bat +1 -1
- data/test/Cassini x86.bat +1 -1
- data/test/applicationhost.config.template +1 -1
- metadata +93 -131
data/CHANGELOG.rdoc
CHANGED
@@ -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.
|
data/README.rdoc
CHANGED
@@ -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
|
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:
|
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:
|
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:
|
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:
|
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:
|
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
|
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
|
|
data/features/basic_auth.feature
CHANGED
@@ -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:
|
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:
|
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:
|
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
|
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
|
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
|
7
|
+
Given a sample HTTP ODataService exists
|
8
8
|
And blueprints exist for the service
|
9
9
|
|
10
10
|
# Expand
|
data/features/service.feature
CHANGED
@@ -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
|
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:
|
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
|
data/features/support/env.rb
CHANGED
@@ -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
|
data/features/support/hooks.rb
CHANGED
@@ -4,7 +4,7 @@ Feature: Type conversion
|
|
4
4
|
I want types returned to be accurately represented
|
5
5
|
|
6
6
|
Background:
|
7
|
-
Given
|
7
|
+
Given a sample HTTP ODataService exists
|
8
8
|
And blueprints exist for the service
|
9
9
|
|
10
10
|
Scenario: Integers should be Fixnums
|
data/lib/ruby_odata.rb
CHANGED
@@ -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:
|
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
|
-
|
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
|
|
data/lib/ruby_odata/service.rb
CHANGED
@@ -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
|
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
|
-
|
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'] } #
|
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
|
-
|
179
|
-
|
180
|
-
|
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
|
-
|
200
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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 =
|
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
|
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
|
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 =
|
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
|
-
|
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
|