ruby_odata 0.0.10 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +9 -3
- data/.travis.yml +4 -0
- data/CHANGELOG.rdoc +18 -1
- data/README.rdoc +77 -19
- data/config/cucumber.yml +4 -3
- data/features/basic_auth.feature +5 -7
- data/features/batch_request.feature +11 -11
- data/features/complex_types.feature +11 -11
- data/features/query_builder.feature +27 -18
- data/features/service.feature +8 -8
- data/features/service_manage.feature +27 -12
- data/features/service_methods.feature +37 -0
- data/features/ssl.feature +4 -4
- data/features/step_definitions/pickle_steps.rb +100 -0
- data/features/step_definitions/service_steps.rb +99 -65
- data/features/support/constants.rb +3 -0
- data/features/support/custom_helpers.rb +54 -0
- data/features/support/env.rb +0 -10
- data/features/support/hooks.rb +1 -1
- data/features/support/pickle.rb +82 -0
- data/features/type_conversion.feature +10 -10
- data/lib/ruby_odata.rb +4 -1
- data/lib/ruby_odata/association.rb +36 -0
- data/lib/ruby_odata/class_builder.rb +96 -16
- data/lib/ruby_odata/helpers.rb +10 -0
- data/lib/ruby_odata/operation.rb +7 -5
- data/lib/ruby_odata/property_metadata.rb +11 -6
- data/lib/ruby_odata/query_builder.rb +20 -1
- data/lib/ruby_odata/service.rb +407 -94
- data/lib/ruby_odata/version.rb +1 -1
- data/ruby_odata.gemspec +2 -0
- data/spec/association_spec.rb +48 -0
- data/spec/class_builder_spec.rb +11 -2
- data/spec/fixtures/inheritance/edmx_pluralsight.xml +111 -0
- data/spec/fixtures/inheritance/result_pluralsight_courses.xml +229 -0
- data/spec/fixtures/links/result_links_query.xml +6 -0
- data/spec/fixtures/partial/partial_feed_metadata.xml +25 -0
- data/spec/fixtures/partial/partial_feed_part_1.xml +42 -0
- data/spec/fixtures/partial/partial_feed_part_2.xml +42 -0
- data/spec/fixtures/partial/partial_feed_part_3.xml +40 -0
- data/spec/fixtures/sample_service/edmx_categories_products.xml +1 -0
- data/spec/fixtures/sample_service/result_category_names.xml +5 -0
- data/spec/fixtures/sample_service/result_entity_category_web_get.xml +29 -0
- data/spec/fixtures/sample_service/result_entity_single_category_web_get.xml +23 -0
- data/spec/fixtures/sample_service/result_first_category_id.xml +2 -0
- data/spec/fixtures/sample_service/result_multiple_category_products.xml +57 -0
- data/spec/fixtures/sample_service/result_single_category.xml +18 -0
- data/spec/fixtures/sample_service/result_single_product.xml +26 -0
- data/spec/fixtures/sample_service/result_single_product_not_found.xml +4 -0
- data/spec/fixtures/sap/edmx_sap_demo_flight.xml +62 -58
- data/spec/property_metadata_spec.rb +9 -2
- data/spec/query_builder_spec.rb +11 -0
- data/spec/revised_service_spec.rb +197 -0
- data/spec/service_spec.rb +463 -4
- data/test/RubyODataService/RubyODataService.sln +20 -0
- data/test/RubyODataService/RubyODataService/App_Start/EntityFramework.SqlServerCompact.cs +12 -0
- data/test/RubyODataService/RubyODataService/BasicAuth/RubyOData.svc +3 -0
- data/test/RubyODataService/RubyODataService/BasicAuth/RubyOData.svc.cs +107 -0
- data/test/RubyODataService/RubyODataService/Global.asax +1 -0
- data/test/RubyODataService/RubyODataService/Global.asax.cs +49 -0
- data/test/RubyODataService/RubyODataService/Models/AuditFields.cs +16 -0
- data/test/RubyODataService/RubyODataService/Models/Category.cs +16 -0
- data/test/RubyODataService/RubyODataService/Models/Product.cs +20 -0
- data/test/RubyODataService/RubyODataService/Properties/AssemblyInfo.cs +35 -0
- data/test/RubyODataService/RubyODataService/RubyOData.svc +3 -0
- data/test/RubyODataService/RubyODataService/RubyOData.svc.cs +57 -0
- data/test/RubyODataService/RubyODataService/RubyODataContext.cs +16 -0
- data/test/RubyODataService/RubyODataService/RubyODataService.csproj +159 -0
- data/test/RubyODataService/RubyODataService/RubyODataService.csproj.user +31 -0
- data/test/RubyODataService/RubyODataService/Web.Debug.config +30 -0
- data/test/RubyODataService/RubyODataService/Web.Release.config +31 -0
- data/test/RubyODataService/RubyODataService/Web.config +27 -0
- data/test/RubyODataService/RubyODataService/bin/EntityFramework.dll +0 -0
- data/test/RubyODataService/RubyODataService/bin/Microsoft.Data.Edm.dll +0 -0
- data/test/RubyODataService/RubyODataService/bin/Microsoft.Data.OData.dll +0 -0
- data/test/RubyODataService/RubyODataService/bin/Microsoft.Data.Services.Client.dll +0 -0
- data/test/RubyODataService/RubyODataService/bin/Microsoft.Data.Services.dll +0 -0
- data/test/RubyODataService/RubyODataService/bin/Microsoft.Data.Spatial.dll +0 -0
- data/test/RubyODataService/RubyODataService/bin/RubyODataService.dll +0 -0
- data/test/RubyODataService/RubyODataService/bin/System.Data.SqlServerCe.Entity.dll +0 -0
- data/test/RubyODataService/RubyODataService/bin/System.Data.SqlServerCe.dll +0 -0
- data/test/RubyODataService/RubyODataService/bin/System.Spatial.dll +0 -0
- data/test/RubyODataService/RubyODataService/bin/WebActivator.dll +0 -0
- data/test/RubyODataService/RubyODataService/bin/amd64/Microsoft.VC90.CRT/Microsoft.VC90.CRT.manifest +6 -0
- data/test/RubyODataService/RubyODataService/bin/amd64/Microsoft.VC90.CRT/README_ENU.txt +0 -0
- data/test/RubyODataService/RubyODataService/bin/amd64/Microsoft.VC90.CRT/msvcr90.dll +0 -0
- data/test/RubyODataService/RubyODataService/bin/amd64/sqlcecompact40.dll +0 -0
- data/test/RubyODataService/RubyODataService/bin/amd64/sqlceer40EN.dll +0 -0
- data/test/RubyODataService/RubyODataService/bin/amd64/sqlceme40.dll +0 -0
- data/test/RubyODataService/RubyODataService/bin/amd64/sqlceqp40.dll +0 -0
- data/test/RubyODataService/RubyODataService/bin/amd64/sqlcese40.dll +0 -0
- data/test/RubyODataService/RubyODataService/bin/x86/Microsoft.VC90.CRT/Microsoft.VC90.CRT.manifest +6 -0
- data/test/RubyODataService/RubyODataService/bin/x86/Microsoft.VC90.CRT/README_ENU.txt +0 -0
- data/test/RubyODataService/RubyODataService/bin/x86/Microsoft.VC90.CRT/msvcr90.dll +0 -0
- data/test/RubyODataService/RubyODataService/bin/x86/sqlcecompact40.dll +0 -0
- data/test/RubyODataService/RubyODataService/bin/x86/sqlceer40EN.dll +0 -0
- data/test/RubyODataService/RubyODataService/bin/x86/sqlceme40.dll +0 -0
- data/test/RubyODataService/RubyODataService/bin/x86/sqlceqp40.dll +0 -0
- data/test/RubyODataService/RubyODataService/bin/x86/sqlcese40.dll +0 -0
- data/test/RubyODataService/RubyODataService/packages.config +7 -0
- data/test/RubyODataService/packages/EntityFramework.4.2.0.0/EntityFramework.4.2.0.0.nupkg +0 -0
- data/test/RubyODataService/packages/EntityFramework.4.2.0.0/lib/net40/EntityFramework.dll +0 -0
- data/test/RubyODataService/packages/EntityFramework.4.2.0.0/lib/net40/EntityFramework.xml +13488 -0
- data/test/RubyODataService/packages/EntityFramework.SqlServerCompact.4.1.8482.2/Content/App_Start/EntityFramework.SqlServerCompact.cs.pp +12 -0
- data/test/RubyODataService/packages/EntityFramework.SqlServerCompact.4.1.8482.2/EULA_ENU.rtf +969 -0
- data/test/RubyODataService/packages/EntityFramework.SqlServerCompact.4.1.8482.2/EntityFramework.SqlServerCompact.4.1.8482.2.nupkg +0 -0
- data/test/RubyODataService/packages/EntityFramework.SqlServerCompact.4.1.8482.2/lib/System.Data.SqlServerCe.Entity.dll +0 -0
- data/test/RubyODataService/packages/EntityFramework.SqlServerCompact.4.1.8482.2/tools/install.ps1 +3 -0
- data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/Content/web.config.transform +8 -0
- data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/NativeBinaries/amd64/Microsoft.VC90.CRT/Microsoft.VC90.CRT.manifest +6 -0
- data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/NativeBinaries/amd64/Microsoft.VC90.CRT/README_ENU.txt +0 -0
- data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/NativeBinaries/amd64/Microsoft.VC90.CRT/msvcr90.dll +0 -0
- data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/NativeBinaries/amd64/sqlcecompact40.dll +0 -0
- data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/NativeBinaries/amd64/sqlceer40EN.dll +0 -0
- data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/NativeBinaries/amd64/sqlceme40.dll +0 -0
- data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/NativeBinaries/amd64/sqlceqp40.dll +0 -0
- data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/NativeBinaries/amd64/sqlcese40.dll +0 -0
- data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/NativeBinaries/x86/Microsoft.VC90.CRT/Microsoft.VC90.CRT.manifest +6 -0
- data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/NativeBinaries/x86/Microsoft.VC90.CRT/README_ENU.txt +0 -0
- data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/NativeBinaries/x86/Microsoft.VC90.CRT/msvcr90.dll +0 -0
- data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/NativeBinaries/x86/sqlcecompact40.dll +0 -0
- data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/NativeBinaries/x86/sqlceer40EN.dll +0 -0
- data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/NativeBinaries/x86/sqlceme40.dll +0 -0
- data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/NativeBinaries/x86/sqlceqp40.dll +0 -0
- data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/NativeBinaries/x86/sqlcese40.dll +0 -0
- data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/SQLCE_EULA_ENU.rtf +778 -0
- data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/SqlServerCompact.4.0.8482.1.nupkg +0 -0
- data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/Tools/GetSqlCEPostBuildCmd.ps1 +12 -0
- data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/Tools/install.ps1 +11 -0
- data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/Tools/uninstall.ps1 +9 -0
- data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/lib/System.Data.SqlServerCe.dll +0 -0
- data/test/RubyODataService/packages/WCF DataServices October 2011 CTP/DataSvcUtil.exe +0 -0
- data/test/RubyODataService/packages/WCF DataServices October 2011 CTP/License.rtf +708 -0
- data/test/RubyODataService/packages/WCF DataServices October 2011 CTP/Microsoft.Data.Edm.dll +0 -0
- data/test/RubyODataService/packages/WCF DataServices October 2011 CTP/Microsoft.Data.Edm.xml +4150 -0
- data/test/RubyODataService/packages/WCF DataServices October 2011 CTP/Microsoft.Data.OData.dll +0 -0
- data/test/RubyODataService/packages/WCF DataServices October 2011 CTP/Microsoft.Data.OData.xml +1969 -0
- data/test/RubyODataService/packages/WCF DataServices October 2011 CTP/Microsoft.Data.Services.Client.dll +0 -0
- data/test/RubyODataService/packages/WCF DataServices October 2011 CTP/Microsoft.Data.Services.Client.xml +1442 -0
- data/test/RubyODataService/packages/WCF DataServices October 2011 CTP/Microsoft.Data.Services.Design.dll +0 -0
- data/test/RubyODataService/packages/WCF DataServices October 2011 CTP/Microsoft.Data.Services.Design.xml +191 -0
- data/test/RubyODataService/packages/WCF DataServices October 2011 CTP/Microsoft.Data.Services.dll +0 -0
- data/test/RubyODataService/packages/WCF DataServices October 2011 CTP/Microsoft.Data.Services.xml +2559 -0
- data/test/RubyODataService/packages/WCF DataServices October 2011 CTP/Microsoft.Data.Spatial.dll +0 -0
- data/test/RubyODataService/packages/WCF DataServices October 2011 CTP/Microsoft.Data.Spatial.xml +15 -0
- data/test/RubyODataService/packages/WCF DataServices October 2011 CTP/Microsoft.VsDesigner.DataServices.Adapter.dll +0 -0
- data/test/RubyODataService/packages/WCF DataServices October 2011 CTP/README.txt +6 -0
- data/test/RubyODataService/packages/WCF DataServices October 2011 CTP/System.Spatial.dll +0 -0
- data/test/RubyODataService/packages/WCF DataServices October 2011 CTP/System.Spatial.xml +2276 -0
- data/test/RubyODataService/packages/WebActivator.1.0.0.0/WebActivator.1.0.0.0.nupkg +0 -0
- data/test/RubyODataService/packages/WebActivator.1.0.0.0/lib/WebActivator.dll +0 -0
- data/test/RubyODataService/packages/repositories.config +4 -0
- data/test/applicationhost.config.template +2 -2
- data/test/blueprints.rb +5 -4
- data/test/iisExpress x64.bat b/data/test/iisExpress → x64.bat +0 -0
- data/test/iisExpress x86.bat b/data/test/iisExpress → x86.bat +0 -0
- data/test/setpath.rb +13 -3
- data/test/usage_samples/querying.rb +45 -0
- data/test/usage_samples/reflection.rb +16 -0
- data/test/usage_samples/sample_data.rb +30 -0
- metadata +327 -36
- data/test/Cassini x64.bat +0 -1
- data/test/Cassini x86.bat +0 -1
- data/test/SampleService/App_Code/AuditFields.cs +0 -13
- data/test/SampleService/App_Code/Entities.cs +0 -145
- data/test/SampleService/App_Code/Model.Designer.cs +0 -578
- data/test/SampleService/App_Code/Model.edmx +0 -157
- data/test/SampleService/App_Code/ModelContainerExtended.cs +0 -32
- data/test/SampleService/App_Data/_TestDB.mdf +0 -0
- data/test/SampleService/App_Data/_TestDB_Log.ldf +0 -0
- data/test/SampleService/BasicAuth/Entities.svc +0 -1
- data/test/SampleService/Entities.svc +0 -1
- data/test/SampleService/web.config +0 -37
@@ -0,0 +1,10 @@
|
|
1
|
+
module OData
|
2
|
+
class Helpers
|
3
|
+
# Helper to normalize the results of a select result; Ruby 1.9 Hash.select returns a Hash, 1.8 returns an Array
|
4
|
+
# This is for Ruby 1.8 support, but should be removed in the future
|
5
|
+
def self.normalize_to_hash(val)
|
6
|
+
return nil if val.nil?
|
7
|
+
(val.is_a? Hash) ? val : Hash[*val.flatten]
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
data/lib/ruby_odata/operation.rb
CHANGED
@@ -1,18 +1,20 @@
|
|
1
1
|
module OData
|
2
2
|
# Internally used helper class for storing operations called against the service. This class shouldn't be used directly.
|
3
3
|
class Operation
|
4
|
-
attr_accessor :kind, :klass_name, :klass
|
4
|
+
attr_accessor :kind, :klass_name, :klass, :child_klass
|
5
5
|
|
6
6
|
# Creates a new instance of the Operation class
|
7
7
|
#
|
8
8
|
# ==== Required Attributes
|
9
|
-
# - kind:
|
10
|
-
# - klass_name:
|
11
|
-
# - klass:
|
12
|
-
|
9
|
+
# - kind: The operation type (Standard: Add, Update, or Delete | Links: AddLink)
|
10
|
+
# - klass_name: The name/type of the class to operate against
|
11
|
+
# - klass: The actual class
|
12
|
+
# - child_klass: (Optional) Only used for link operations
|
13
|
+
def initialize(kind, klass_name, klass, child_klass = nil)
|
13
14
|
@kind = kind
|
14
15
|
@klass_name = klass_name
|
15
16
|
@klass = klass
|
17
|
+
@child_klass = child_klass
|
16
18
|
end
|
17
19
|
end
|
18
20
|
end
|
@@ -2,15 +2,19 @@ module OData
|
|
2
2
|
# Internally used helper class for storing an entity property's metadata. This class shouldn't be used directly.
|
3
3
|
class PropertyMetadata
|
4
4
|
# The property name
|
5
|
-
|
5
|
+
attr_reader :name
|
6
6
|
# The property EDM type
|
7
|
-
|
7
|
+
attr_reader :type
|
8
8
|
# Is the property nullable?
|
9
|
-
|
9
|
+
attr_reader :nullable
|
10
10
|
# Feed customization target path
|
11
|
-
|
11
|
+
attr_reader :fc_target_path
|
12
12
|
# Should the property appear in both the mapped schema path and the properties collection
|
13
|
-
|
13
|
+
attr_reader :fc_keep_in_content
|
14
|
+
# Is the property a navigation property?
|
15
|
+
attr_reader :nav_prop
|
16
|
+
# Applies only to navigation properties; the association corresponding to the property
|
17
|
+
attr_accessor :association
|
14
18
|
|
15
19
|
# Creates a new instance of the Class Property class
|
16
20
|
#
|
@@ -20,9 +24,10 @@ module OData
|
|
20
24
|
def initialize(property_element)
|
21
25
|
@name = property_element['Name']
|
22
26
|
@type = property_element['Type']
|
23
|
-
@nullable = (property_element['Nullable'] && property_element['Nullable'] == "true") || false
|
27
|
+
@nullable = ((property_element['Nullable'] && property_element['Nullable'] == "true") || property_element.name == 'NavigationProperty') || false
|
24
28
|
@fc_target_path = property_element['FC_TargetPath']
|
25
29
|
@fc_keep_in_content = (property_element['FC_KeepInContent']) ? (property_element['FC_KeepInContent'] == "true") : nil
|
30
|
+
@nav_prop = property_element.name == 'NavigationProperty'
|
26
31
|
end
|
27
32
|
end
|
28
33
|
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:8989/SampleService/
|
5
|
+
# svc = OData::Service.new "http://127.0.0.1:8989/SampleService/RubyOData.svc"
|
6
6
|
# svc.Categories
|
7
7
|
# The *Categories* method would return a QueryBuilder
|
8
8
|
class QueryBuilder
|
@@ -19,6 +19,7 @@ class QueryBuilder
|
|
19
19
|
@order_bys = []
|
20
20
|
@skip = nil
|
21
21
|
@top = nil
|
22
|
+
@links = nil
|
22
23
|
@additional_params = additional_params
|
23
24
|
end
|
24
25
|
|
@@ -89,10 +90,28 @@ class QueryBuilder
|
|
89
90
|
self
|
90
91
|
end
|
91
92
|
|
93
|
+
# Used to return links instead of actual objects
|
94
|
+
# ==== Required Attributes
|
95
|
+
# - navigation_property: The NavigationProperty name to retrieve the links for
|
96
|
+
#
|
97
|
+
# ==== Example
|
98
|
+
# svc.Categories(1).links("Products")
|
99
|
+
# product_links = svc.execute # => returns URIs for the products under the Category with an ID of 1
|
100
|
+
def links(navigation_property)
|
101
|
+
@navigation_property = navigation_property
|
102
|
+
self
|
103
|
+
end
|
104
|
+
|
92
105
|
# Builds the query URI (path, not including root) incorporating expands, filters, etc.
|
93
106
|
# This is used internally when the execute method is called on the service
|
94
107
|
def query
|
95
108
|
q = @root.clone
|
109
|
+
|
110
|
+
# Handle links queries, this isn't just a standard query option
|
111
|
+
if @navigation_property
|
112
|
+
q << "/$links/#{@navigation_property}"
|
113
|
+
end
|
114
|
+
|
96
115
|
query_options = []
|
97
116
|
query_options << "$expand=#{@expands.join(',')}" unless @expands.empty?
|
98
117
|
query_options << "$filter=#{@filters.join('+and+')}" unless @filters.empty?
|
data/lib/ruby_odata/service.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
module OData
|
2
2
|
|
3
3
|
class Service
|
4
|
-
attr_reader :classes, :class_metadata, :options
|
4
|
+
attr_reader :classes, :class_metadata, :options, :collections, :edmx, :function_imports
|
5
5
|
# Creates a new instance of the Service class
|
6
6
|
#
|
7
7
|
# ==== Required Attributes
|
@@ -11,16 +11,15 @@ class Service
|
|
11
11
|
# - password: password for http basic auth
|
12
12
|
# - verify_ssl: false if no verification, otherwise mode (OpenSSL::SSL::VERIFY_PEER is default)
|
13
13
|
# - additional_params: a hash of query string params that will be passed on all calls
|
14
|
+
# - eager_partial: true (default) if queries should consume partial feeds until the feed is complete, false if explicit calls to next must be performed
|
14
15
|
def initialize(service_uri, options = {})
|
15
16
|
@uri = service_uri.gsub!(/\/?$/, '')
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
@save_operations = []
|
20
|
-
@additional_params = options[:additional_params] || {}
|
17
|
+
set_options! options
|
18
|
+
default_instance_vars!
|
19
|
+
set_namespaces
|
21
20
|
build_collections_and_classes
|
22
21
|
end
|
23
|
-
|
22
|
+
|
24
23
|
# Handles the dynamic AddTo<EntityName> methods as well as the collections on the service
|
25
24
|
def method_missing(name, *args)
|
26
25
|
# Queries
|
@@ -29,7 +28,7 @@ class Service
|
|
29
28
|
root << "(#{args.join(',')})" unless args.empty?
|
30
29
|
@query = QueryBuilder.new(root, @additional_params)
|
31
30
|
return @query
|
32
|
-
# Adds
|
31
|
+
# Adds
|
33
32
|
elsif name.to_s =~ /^AddTo(.*)/
|
34
33
|
type = $1
|
35
34
|
if @collections.include?(type)
|
@@ -37,10 +36,11 @@ class Service
|
|
37
36
|
else
|
38
37
|
super
|
39
38
|
end
|
39
|
+
elsif @function_imports.include?(name.to_s)
|
40
|
+
execute_import_function(name.to_s, args)
|
40
41
|
else
|
41
42
|
super
|
42
43
|
end
|
43
|
-
|
44
44
|
end
|
45
45
|
|
46
46
|
# Queues an object for deletion. To actually remove it from the server, you must call save_changes as well.
|
@@ -51,58 +51,58 @@ class Service
|
|
51
51
|
# Note: This method will throw an exception if the +obj+ isn't a tracked entity
|
52
52
|
def delete_object(obj)
|
53
53
|
type = obj.class.to_s
|
54
|
-
if obj.respond_to?(:__metadata) && !obj.send(:__metadata).nil?
|
54
|
+
if obj.respond_to?(:__metadata) && !obj.send(:__metadata).nil?
|
55
55
|
@save_operations << Operation.new("Delete", type, obj)
|
56
56
|
else
|
57
57
|
raise "You cannot delete a non-tracked entity"
|
58
58
|
end
|
59
59
|
end
|
60
|
-
|
60
|
+
|
61
61
|
# Queues an object for update. To actually update it on the server, you must call save_changes as well.
|
62
|
-
#
|
62
|
+
#
|
63
63
|
# ==== Required Attributes
|
64
64
|
# - obj: The object to queue for update
|
65
65
|
#
|
66
|
-
# Note: This method will throw an exception if the +obj+ isn't a tracked entity
|
66
|
+
# Note: This method will throw an exception if the +obj+ isn't a tracked entity
|
67
67
|
def update_object(obj)
|
68
68
|
type = obj.class.to_s
|
69
|
-
if obj.respond_to?(:__metadata) && !obj.send(:__metadata).nil?
|
69
|
+
if obj.respond_to?(:__metadata) && !obj.send(:__metadata).nil?
|
70
70
|
@save_operations << Operation.new("Update", type, obj)
|
71
71
|
else
|
72
72
|
raise "You cannot update a non-tracked entity"
|
73
|
-
end
|
73
|
+
end
|
74
74
|
end
|
75
|
-
|
75
|
+
|
76
76
|
# Performs save operations (Create/Update/Delete) against the server
|
77
77
|
def save_changes
|
78
78
|
return nil if @save_operations.empty?
|
79
79
|
|
80
80
|
result = nil
|
81
|
-
|
81
|
+
|
82
82
|
if @save_operations.length == 1
|
83
|
-
result = single_save(@save_operations[0])
|
84
|
-
else
|
85
|
-
result = batch_save(@save_operations)
|
83
|
+
result = single_save(@save_operations[0])
|
84
|
+
else
|
85
|
+
result = batch_save(@save_operations)
|
86
86
|
end
|
87
|
-
|
88
|
-
# TODO: We should probably perform a check here
|
87
|
+
|
88
|
+
# TODO: We should probably perform a check here
|
89
89
|
# to make sure everything worked before clearing it out
|
90
|
-
@save_operations.clear
|
91
|
-
|
90
|
+
@save_operations.clear
|
91
|
+
|
92
92
|
return result
|
93
93
|
end
|
94
94
|
|
95
|
-
# Performs query operations (Read) against the server
|
96
|
-
def execute
|
95
|
+
# Performs query operations (Read) against the server, returns an array of record instances.
|
96
|
+
def execute
|
97
97
|
result = RestClient::Resource.new(build_query_uri, @rest_options).get
|
98
|
-
|
98
|
+
handle_collection_result(result)
|
99
99
|
end
|
100
|
-
|
101
|
-
# Overridden to identify methods handled by method_missing
|
100
|
+
|
101
|
+
# Overridden to identify methods handled by method_missing
|
102
102
|
def respond_to?(method)
|
103
103
|
if @collections.include?(method.to_s)
|
104
104
|
return true
|
105
|
-
# Adds
|
105
|
+
# Adds
|
106
106
|
elsif method.to_s =~ /^AddTo(.*)/
|
107
107
|
type = $1
|
108
108
|
if @collections.include?(type)
|
@@ -110,13 +110,90 @@ class Service
|
|
110
110
|
else
|
111
111
|
super
|
112
112
|
end
|
113
|
+
# Function Imports
|
114
|
+
elsif @function_imports.include?(method.to_s)
|
115
|
+
return true
|
113
116
|
else
|
114
117
|
super
|
115
118
|
end
|
116
119
|
end
|
117
|
-
|
120
|
+
|
121
|
+
# Retrieves the next resultset of a partial result (if any). Does not honor the :eager_partial option.
|
122
|
+
def next
|
123
|
+
return if not partial?
|
124
|
+
handle_partial
|
125
|
+
end
|
126
|
+
|
127
|
+
# Does the most recent collection returned represent a partial collection? Will aways be false if a query hasn't executed, even if the query would have a partial
|
128
|
+
def partial?
|
129
|
+
@has_partial
|
130
|
+
end
|
131
|
+
|
132
|
+
# Lazy loads a navigation property on a model
|
133
|
+
#
|
134
|
+
# ==== Required Attributes
|
135
|
+
# - obj: The object to fill
|
136
|
+
# - nav_prop: The navigation property to fill
|
137
|
+
#
|
138
|
+
# Note: This method will throw an exception if the +obj+ isn't a tracked entity
|
139
|
+
# Note: This method will throw an exception if the +nav_prop+ isn't a valid navigation property
|
140
|
+
def load_property(obj, nav_prop)
|
141
|
+
raise ArgumentError, "You cannot load a property on an entity that isn't tracked" if obj.send(:__metadata).nil?
|
142
|
+
raise ArgumentError, "'#{nav_prop}' is not a valid navigation property" unless obj.respond_to?(nav_prop.to_sym)
|
143
|
+
raise ArgumentError, "'#{nav_prop}' is not a valid navigation property" unless @class_metadata[obj.class.to_s][nav_prop].nav_prop
|
144
|
+
results = RestClient::Resource.new(build_load_property_uri(obj, nav_prop), @rest_options).get
|
145
|
+
prop_results = build_classes_from_result(results)
|
146
|
+
obj.send "#{nav_prop}=", (singular?(nav_prop) ? prop_results.first : prop_results)
|
147
|
+
end
|
148
|
+
|
149
|
+
# Adds a child object to a parent object's collection
|
150
|
+
#
|
151
|
+
# ==== Required Attributes
|
152
|
+
# - parent: The parent object
|
153
|
+
# - nav_prop: The name of the navigation property to add the child to
|
154
|
+
# - child: The child object
|
155
|
+
def add_link(parent, nav_prop, child)
|
156
|
+
raise ArgumentError, "You cannot add a link on an entity that isn't tracked (#{parent.class})" if parent.send(:__metadata).nil?
|
157
|
+
raise ArgumentError, "'#{nav_prop}' is not a valid navigation property for #{parent.class}" unless parent.respond_to?(nav_prop.to_sym)
|
158
|
+
raise ArgumentError, "'#{nav_prop}' is not a valid navigation property for #{parent.class}" unless @class_metadata[parent.class.to_s][nav_prop].nav_prop
|
159
|
+
raise ArgumentError, "You cannot add a link on a child entity that isn't tracked (#{child.class})" if child.send(:__metadata).nil?
|
160
|
+
@save_operations << Operation.new("AddLink", nav_prop, parent, child)
|
161
|
+
end
|
162
|
+
|
118
163
|
private
|
119
164
|
|
165
|
+
def set_options!(options)
|
166
|
+
@options = options
|
167
|
+
if @options[:eager_partial].nil?
|
168
|
+
@options[:eager_partial] = true
|
169
|
+
end
|
170
|
+
@rest_options = { :verify_ssl => get_verify_mode, :user => @options[:username], :password => @options[:password] }
|
171
|
+
@additional_params = options[:additional_params] || {}
|
172
|
+
@namespace = options[:namespace]
|
173
|
+
end
|
174
|
+
|
175
|
+
def default_instance_vars!
|
176
|
+
@collections = {}
|
177
|
+
@function_imports = {}
|
178
|
+
@save_operations = []
|
179
|
+
@has_partial = false
|
180
|
+
@next_uri = nil
|
181
|
+
end
|
182
|
+
|
183
|
+
def set_namespaces
|
184
|
+
@edmx = Nokogiri::XML(RestClient::Resource.new(build_metadata_uri, @rest_options).get)
|
185
|
+
@ds_namespaces = {
|
186
|
+
"m" => "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata",
|
187
|
+
"edmx" => "http://schemas.microsoft.com/ado/2007/06/edmx",
|
188
|
+
"ds" => "http://schemas.microsoft.com/ado/2007/08/dataservices",
|
189
|
+
"atom" => "http://www.w3.org/2005/Atom"
|
190
|
+
}
|
191
|
+
|
192
|
+
# Get the edm namespace from the edmx
|
193
|
+
edm_ns = @edmx.xpath("edmx:Edmx/edmx:DataServices/*", @namespaces).first.namespaces['xmlns'].to_s
|
194
|
+
@ds_namespaces.merge! "edm" => edm_ns
|
195
|
+
end
|
196
|
+
|
120
197
|
# Gets ssl certificate verification mode, or defaults to verify_peer
|
121
198
|
def get_verify_mode
|
122
199
|
if @options[:verify_ssl].nil?
|
@@ -130,51 +207,135 @@ class Service
|
|
130
207
|
def build_collections_and_classes
|
131
208
|
@classes = Hash.new
|
132
209
|
@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)
|
135
|
-
|
136
|
-
# Get the edm namespace
|
137
|
-
edm_ns = doc.xpath("edmx:Edmx/edmx:DataServices/*", "edmx" => "http://schemas.microsoft.com/ado/2007/06/edmx").first.namespaces['xmlns'].to_s
|
138
|
-
|
139
|
-
# Fill in the collections instance variable
|
140
|
-
collections = doc.xpath("//edm:EntityContainer/edm:EntitySet", "edm" => edm_ns)
|
141
|
-
@collections = collections.collect { |c| c["Name"] }
|
142
210
|
|
143
211
|
# Build complex types first, these will be used for entities
|
144
|
-
complex_types =
|
212
|
+
complex_types = @edmx.xpath("//edm:ComplexType", @ds_namespaces) || []
|
145
213
|
complex_types.each do |c|
|
146
|
-
name = c['Name']
|
147
|
-
props = c.xpath(".//edm:Property",
|
214
|
+
name = qualify_class_name(c['Name'])
|
215
|
+
props = c.xpath(".//edm:Property", @ds_namespaces)
|
148
216
|
methods = props.collect { |p| p['Name'] } # Standard Properties
|
149
|
-
@classes[name] = ClassBuilder.new(name, methods, []).build unless @classes.keys.include?(name)
|
217
|
+
@classes[name] = ClassBuilder.new(name, methods, [], self, @namespace).build unless @classes.keys.include?(name)
|
150
218
|
end
|
151
219
|
|
152
|
-
entity_types =
|
220
|
+
entity_types = @edmx.xpath("//edm:EntityType", @ds_namespaces)
|
153
221
|
entity_types.each do |e|
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
nav_props = nprops.collect { |p| p['Name'] } # Navigation Properties
|
160
|
-
@classes[name] = ClassBuilder.new(name, methods, nav_props).build unless @classes.keys.include?(name)
|
222
|
+
next if e['Abstract'] == "true"
|
223
|
+
klass_name = qualify_class_name(e['Name'])
|
224
|
+
methods = collect_properties(klass_name, e, @edmx)
|
225
|
+
nav_props = collect_navigation_properties(klass_name, e, @edmx)
|
226
|
+
@classes[klass_name] = ClassBuilder.new(klass_name, methods, nav_props, self, @namespace).build unless @classes.keys.include?(klass_name)
|
161
227
|
end
|
228
|
+
|
229
|
+
# Fill in the collections instance variable
|
230
|
+
collections = @edmx.xpath("//edm:EntityContainer/edm:EntitySet", @ds_namespaces)
|
231
|
+
collections.each do |c|
|
232
|
+
entity_type = c["EntityType"]
|
233
|
+
@collections[c["Name"]] = { :edmx_type => entity_type, :type => convert_to_local_type(entity_type) }
|
234
|
+
end
|
235
|
+
|
236
|
+
build_function_imports
|
162
237
|
end
|
163
|
-
|
238
|
+
|
239
|
+
# Parses the function imports and fills the @function_imports collection
|
240
|
+
def build_function_imports
|
241
|
+
# Fill in the function imports
|
242
|
+
functions = @edmx.xpath("//edm:EntityContainer/edm:FunctionImport", @ds_namespaces)
|
243
|
+
functions.each do |f|
|
244
|
+
http_method = f.xpath("@m:HttpMethod", @ds_namespaces).first.content
|
245
|
+
return_type = f["ReturnType"]
|
246
|
+
inner_return_type = nil
|
247
|
+
unless return_type.nil?
|
248
|
+
return_type = (return_type =~ /^Collection/) ? Array : convert_to_local_type(return_type)
|
249
|
+
if f["ReturnType"] =~ /\((.*)\)/
|
250
|
+
inner_return_type = convert_to_local_type($~[1])
|
251
|
+
end
|
252
|
+
end
|
253
|
+
params = f.xpath("edm:Parameter", @ds_namespaces)
|
254
|
+
parameters = nil
|
255
|
+
if params.length > 0
|
256
|
+
parameters = {}
|
257
|
+
params.each do |p|
|
258
|
+
parameters[p["Name"]] = p["Type"]
|
259
|
+
end
|
260
|
+
end
|
261
|
+
@function_imports[f["Name"]] = {
|
262
|
+
:http_method => http_method,
|
263
|
+
:return_type => return_type,
|
264
|
+
:inner_return_type => inner_return_type,
|
265
|
+
:parameters => parameters }
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
# Converts the EDMX model type to the local model type
|
270
|
+
def convert_to_local_type(edmx_type)
|
271
|
+
return edm_to_ruby_type(edmx_type) if edmx_type =~ /^Edm/
|
272
|
+
klass_name = qualify_class_name(edmx_type.split('.').last)
|
273
|
+
klass_name.camelize.constantize
|
274
|
+
end
|
275
|
+
|
276
|
+
# Converts a class name to its fully qualified name (if applicable) and returns the new name
|
277
|
+
def qualify_class_name(klass_name)
|
278
|
+
unless @namespace.nil? || @namespace.blank? || klass_name.include?('::')
|
279
|
+
namespaces = @namespace.split(/\.|::/)
|
280
|
+
namespaces << klass_name
|
281
|
+
klass_name = namespaces.join '::'
|
282
|
+
end
|
283
|
+
klass_name.camelize
|
284
|
+
end
|
285
|
+
|
286
|
+
# Builds the metadata need for each property for things like feed customizations and navigation properties
|
164
287
|
def build_property_metadata(props)
|
165
288
|
metadata = {}
|
166
289
|
props.each do |property_element|
|
167
290
|
prop_meta = PropertyMetadata.new(property_element)
|
291
|
+
# If this is a navigation property, we need to add the association to the property metadata
|
292
|
+
prop_meta.association = Association.new(property_element, @edmx) if prop_meta.nav_prop
|
168
293
|
metadata[prop_meta.name] = prop_meta
|
169
294
|
end
|
170
295
|
metadata
|
171
296
|
end
|
297
|
+
|
298
|
+
# Handle parsing of OData Atom result and return an array of Entry classes
|
299
|
+
def handle_collection_result(result)
|
300
|
+
results = build_classes_from_result(result)
|
301
|
+
while partial? && @options[:eager_partial]
|
302
|
+
results.concat handle_partial
|
303
|
+
end
|
304
|
+
results
|
305
|
+
end
|
306
|
+
|
307
|
+
# Loops through the standard properties (non-navigation) for a given class and returns the appropriate list of methods
|
308
|
+
def collect_properties(klass_name, element, doc)
|
309
|
+
props = element.xpath(".//edm:Property", @ds_namespaces)
|
310
|
+
@class_metadata[klass_name] = build_property_metadata(props)
|
311
|
+
methods = props.collect { |p| p['Name'] }
|
312
|
+
unless element["BaseType"].nil?
|
313
|
+
base = element["BaseType"].split(".").last()
|
314
|
+
baseType = doc.xpath("//edm:EntityType[@Name=\"#{base}\"]", @ds_namespaces).first()
|
315
|
+
props = baseType.xpath(".//edm:Property", @ds_namespaces)
|
316
|
+
@class_metadata[klass_name].merge!(build_property_metadata(props))
|
317
|
+
methods = methods.concat(props.collect { |p| p['Name']})
|
318
|
+
end
|
319
|
+
methods
|
320
|
+
end
|
321
|
+
|
322
|
+
# Similar to +collect_properties+, but handles the navigation properties
|
323
|
+
def collect_navigation_properties(klass_name, element, doc)
|
324
|
+
nav_props = element.xpath(".//edm:NavigationProperty", @ds_namespaces)
|
325
|
+
@class_metadata[klass_name].merge!(build_property_metadata(nav_props))
|
326
|
+
nav_props.collect { |p| p['Name'] }
|
327
|
+
end
|
172
328
|
|
173
329
|
# Helper to loop through a result and create an instance for each entity in the results
|
174
330
|
def build_classes_from_result(result)
|
175
331
|
doc = Nokogiri::XML(result)
|
176
|
-
|
177
|
-
|
332
|
+
|
333
|
+
is_links = doc.at_xpath("/ds:links", @ds_namespaces)
|
334
|
+
return parse_link_results(doc) if is_links
|
335
|
+
|
336
|
+
entries = doc.xpath("//atom:entry[not(ancestor::atom:entry)]", @ds_namespaces)
|
337
|
+
|
338
|
+
extract_partial(doc)
|
178
339
|
|
179
340
|
results = []
|
180
341
|
entries.each do |entry|
|
@@ -186,11 +347,11 @@ class Service
|
|
186
347
|
# Converts an XML Entry into a class
|
187
348
|
def entry_to_class(entry)
|
188
349
|
# Retrieve the class name from the fully qualified name (the last string after the last dot)
|
189
|
-
klass_name = entry.xpath("./atom:category/@term",
|
350
|
+
klass_name = entry.xpath("./atom:category/@term", @ds_namespaces).to_s.split('.')[-1]
|
190
351
|
|
191
352
|
# Is the category missing? See if there is a title that we can use to build the class
|
192
353
|
if klass_name.nil?
|
193
|
-
title = entry.xpath("./atom:title",
|
354
|
+
title = entry.xpath("./atom:title", @ds_namespaces).first
|
194
355
|
return nil if title.nil?
|
195
356
|
klass_name = title.content.to_s
|
196
357
|
end
|
@@ -199,14 +360,14 @@ class Service
|
|
199
360
|
|
200
361
|
# If we are working against a child (inline) entry, we need to use the more generic xpath because a child entry WILL
|
201
362
|
# 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",
|
363
|
+
has_inline = entry.xpath(".//m:inline", @ds_namespaces).any?
|
203
364
|
properties_xpath = has_inline ? ".//m:properties[not(ancestor::m:inline)]/*" : ".//m:properties/*"
|
204
|
-
properties = entry.xpath(properties_xpath,
|
365
|
+
properties = entry.xpath(properties_xpath, @ds_namespaces)
|
205
366
|
|
206
|
-
klass = @classes[klass_name].new
|
367
|
+
klass = @classes[qualify_class_name(klass_name)].new
|
207
368
|
|
208
369
|
# Fill metadata
|
209
|
-
meta_id = entry.xpath("./atom:id",
|
370
|
+
meta_id = entry.xpath("./atom:id", @ds_namespaces)[0].content
|
210
371
|
klass.send :__metadata=, { :uri => meta_id }
|
211
372
|
|
212
373
|
# Fill properties
|
@@ -216,20 +377,20 @@ class Service
|
|
216
377
|
end
|
217
378
|
|
218
379
|
# 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|
|
380
|
+
@class_metadata[qualify_class_name(klass_name)].select { |k,v| v.fc_keep_in_content == false }.each do |k, meta|
|
220
381
|
if meta.fc_target_path == "SyndicationTitle"
|
221
|
-
title = entry.xpath("./atom:title",
|
382
|
+
title = entry.xpath("./atom:title", @ds_namespaces).first
|
222
383
|
klass.send "#{meta.name}=", title.content
|
223
384
|
elsif meta.fc_target_path == "SyndicationSummary"
|
224
|
-
summary = entry.xpath("./atom:summary",
|
385
|
+
summary = entry.xpath("./atom:summary", @ds_namespaces).first
|
225
386
|
klass.send "#{meta.name}=", summary.content
|
226
387
|
end
|
227
388
|
end
|
228
389
|
|
229
|
-
inline_links = entry.xpath("./atom:link[m:inline]",
|
390
|
+
inline_links = entry.xpath("./atom:link[m:inline]", @ds_namespaces)
|
230
391
|
|
231
|
-
for
|
232
|
-
inline_entries = link.xpath(".//atom:entry",
|
392
|
+
for link in inline_links
|
393
|
+
inline_entries = link.xpath(".//atom:entry", @ds_namespaces)
|
233
394
|
|
234
395
|
# TODO: Use the metadata's associations to determine the multiplicity instead of this "hack"
|
235
396
|
property_name = link.attributes['title'].to_s
|
@@ -247,13 +408,39 @@ class Service
|
|
247
408
|
end
|
248
409
|
|
249
410
|
# Assign the array of classes to the property
|
250
|
-
property_name = link.xpath("@title",
|
411
|
+
property_name = link.xpath("@title", @ds_namespaces)
|
251
412
|
klass.send "#{property_name}=", inline_classes
|
252
413
|
end
|
253
414
|
end
|
254
415
|
|
255
416
|
klass
|
256
417
|
end
|
418
|
+
|
419
|
+
# Tests for and extracts the next href of a partial
|
420
|
+
def extract_partial(doc)
|
421
|
+
next_links = doc.xpath('//atom:link[@rel="next"]', @ds_namespaces)
|
422
|
+
@has_partial = next_links.any?
|
423
|
+
@next_uri = next_links[0]['href'] if @has_partial
|
424
|
+
end
|
425
|
+
|
426
|
+
def handle_partial
|
427
|
+
if @next_uri
|
428
|
+
result = RestClient::Resource.new(@next_uri, @rest_options).get
|
429
|
+
results = handle_collection_result(result)
|
430
|
+
end
|
431
|
+
results
|
432
|
+
end
|
433
|
+
|
434
|
+
# Handle link results
|
435
|
+
def parse_link_results(doc)
|
436
|
+
uris = doc.xpath("/ds:links/ds:uri", @ds_namespaces)
|
437
|
+
results = []
|
438
|
+
uris.each do |uri_el|
|
439
|
+
link = uri_el.content
|
440
|
+
results << URI.parse(link)
|
441
|
+
end
|
442
|
+
results
|
443
|
+
end
|
257
444
|
|
258
445
|
# Build URIs
|
259
446
|
def build_metadata_uri
|
@@ -269,6 +456,12 @@ class Service
|
|
269
456
|
uri << "?#{@additional_params.to_query}" unless @additional_params.empty?
|
270
457
|
uri
|
271
458
|
end
|
459
|
+
def build_add_link_uri(operation)
|
460
|
+
uri = "#{operation.klass.send(:__metadata)[:uri]}"
|
461
|
+
uri << "/$links/#{operation.klass_name}"
|
462
|
+
uri << "?#{@additional_params.to_query}" unless @additional_params.empty?
|
463
|
+
uri
|
464
|
+
end
|
272
465
|
def build_resource_uri(operation)
|
273
466
|
uri = operation.klass.send(:__metadata)[:uri]
|
274
467
|
uri << "?#{@additional_params.to_query}" unless @additional_params.empty?
|
@@ -279,6 +472,17 @@ class Service
|
|
279
472
|
uri << "?#{@additional_params.to_query}" unless @additional_params.empty?
|
280
473
|
uri
|
281
474
|
end
|
475
|
+
def build_load_property_uri(obj, property)
|
476
|
+
uri = obj.__metadata[:uri]
|
477
|
+
uri << "/#{property}"
|
478
|
+
uri
|
479
|
+
end
|
480
|
+
def build_function_import_uri(name, params)
|
481
|
+
uri = "#{@uri}/#{name}"
|
482
|
+
params.merge! @additional_params
|
483
|
+
uri << "?#{params.to_query}" unless params.empty?
|
484
|
+
uri
|
485
|
+
end
|
282
486
|
|
283
487
|
def build_inline_class(klass, entry, property_name)
|
284
488
|
# Build the class
|
@@ -287,6 +491,27 @@ class Service
|
|
287
491
|
# Add the property
|
288
492
|
klass.send "#{property_name}=", inline_klass
|
289
493
|
end
|
494
|
+
|
495
|
+
# Used to link a child object to its parent and vice-versa after a add_link operation
|
496
|
+
def link_child_to_parent(operation)
|
497
|
+
child_collection = operation.klass.send("#{operation.klass_name}") || []
|
498
|
+
child_collection << operation.child_klass
|
499
|
+
operation.klass.send("#{operation.klass_name}=", child_collection)
|
500
|
+
|
501
|
+
# Attach the parent to the child
|
502
|
+
parent_meta = @class_metadata[operation.klass.class.to_s][operation.klass_name]
|
503
|
+
child_meta = @class_metadata[operation.child_klass.class.to_s]
|
504
|
+
# Find the matching relationship on the child object
|
505
|
+
child_properties = Helpers.normalize_to_hash(
|
506
|
+
child_meta.select { |k, prop|
|
507
|
+
prop.nav_prop &&
|
508
|
+
prop.association.relationship == parent_meta.association.relationship })
|
509
|
+
|
510
|
+
child_property_to_set = child_properties.keys.first # There should be only one match
|
511
|
+
# TODO: Handle many to many scenarios where the child property is an enumerable
|
512
|
+
operation.child_klass.send("#{child_property_to_set}=", operation.klass)
|
513
|
+
end
|
514
|
+
|
290
515
|
def single_save(operation)
|
291
516
|
if operation.kind == "Add"
|
292
517
|
save_uri = build_save_uri(operation)
|
@@ -301,21 +526,29 @@ class Service
|
|
301
526
|
elsif operation.kind == "Delete"
|
302
527
|
delete_uri = build_resource_uri(operation)
|
303
528
|
delete_result = RestClient::Resource.new(delete_uri, @rest_options).delete
|
304
|
-
return (delete_result.code == 204)
|
305
|
-
|
529
|
+
return (delete_result.code == 204)
|
530
|
+
elsif operation.kind == "AddLink"
|
531
|
+
save_uri = build_add_link_uri(operation)
|
532
|
+
json_klass = operation.child_klass.to_json(:type => :link)
|
533
|
+
post_result = RestClient::Resource.new(save_uri, @rest_options).post json_klass, {:content_type => :json}
|
534
|
+
|
535
|
+
# Attach the child to the parent
|
536
|
+
link_child_to_parent(operation) if (post_result.code == 204)
|
537
|
+
|
538
|
+
return(post_result.code == 204)
|
539
|
+
end
|
306
540
|
end
|
307
541
|
|
308
542
|
# Batch Saves
|
309
543
|
def generate_guid
|
310
544
|
rand(36**12).to_s(36).insert(4, "-").insert(9, "-")
|
311
545
|
end
|
312
|
-
def batch_save(operations)
|
546
|
+
def batch_save(operations)
|
313
547
|
batch_num = generate_guid
|
314
548
|
changeset_num = generate_guid
|
315
549
|
batch_uri = build_batch_uri
|
316
550
|
|
317
551
|
body = build_batch_body(operations, batch_num, changeset_num)
|
318
|
-
|
319
552
|
result = RestClient::Resource.new( batch_uri, @rest_options).post body, {:content_type => "multipart/mixed; boundary=batch_#{batch_num}"}
|
320
553
|
|
321
554
|
# TODO: More result validation needs to be done.
|
@@ -323,7 +556,7 @@ class Service
|
|
323
556
|
return (result.code == 202)
|
324
557
|
end
|
325
558
|
def build_batch_body(operations, batch_num, changeset_num)
|
326
|
-
# Header
|
559
|
+
# Header
|
327
560
|
body = "--batch_#{batch_num}\n"
|
328
561
|
body << "Content-Type: multipart/mixed;boundary=changeset_#{changeset_num}\n\n"
|
329
562
|
|
@@ -333,7 +566,7 @@ class Service
|
|
333
566
|
body << "\n"
|
334
567
|
end
|
335
568
|
|
336
|
-
# Footer
|
569
|
+
# Footer
|
337
570
|
body << "\n\n--changeset_#{changeset_num}--\n"
|
338
571
|
body << "--batch_#{batch_num}--"
|
339
572
|
|
@@ -348,7 +581,7 @@ class Service
|
|
348
581
|
content << "Content-Type: application/http\n"
|
349
582
|
content << "Content-Transfer-Encoding: binary\n\n"
|
350
583
|
|
351
|
-
if operation.kind == "Add"
|
584
|
+
if operation.kind == "Add"
|
352
585
|
save_uri = "#{@uri}/#{operation.klass_name}"
|
353
586
|
json_klass = operation.klass.to_json(:type => :add)
|
354
587
|
|
@@ -367,14 +600,22 @@ class Service
|
|
367
600
|
|
368
601
|
content << "DELETE #{delete_uri} HTTP/1.1\n"
|
369
602
|
content << accept_headers
|
370
|
-
|
603
|
+
elsif
|
604
|
+
save_uri = build_add_link_uri(operation)
|
605
|
+
json_klass = operation.child_klass.to_json(:type => :link)
|
606
|
+
|
607
|
+
content << "POST #{save_uri} HTTP/1.1\n"
|
608
|
+
content << accept_headers
|
609
|
+
content << json_klass
|
610
|
+
link_child_to_parent(operation)
|
611
|
+
end
|
371
612
|
|
372
613
|
return content
|
373
614
|
end
|
374
615
|
|
375
616
|
# Complex Types
|
376
617
|
def complex_type_to_class(complex_type_xml)
|
377
|
-
klass_name = complex_type_xml.attr('type').split('.')[-1]
|
618
|
+
klass_name = qualify_class_name(complex_type_xml.attr('type').split('.')[-1])
|
378
619
|
klass = @classes[klass_name].new
|
379
620
|
|
380
621
|
# Fill in the properties
|
@@ -387,6 +628,25 @@ class Service
|
|
387
628
|
end
|
388
629
|
|
389
630
|
# Field Converters
|
631
|
+
|
632
|
+
# Handles parsing datetimes from a string
|
633
|
+
def parse_date(sdate)
|
634
|
+
# Assume this is UTC if no timezone is specified
|
635
|
+
sdate = sdate + "Z" unless sdate.match(/Z|([+|-]\d{2}:\d{2})$/)
|
636
|
+
|
637
|
+
# This is to handle older versions of Ruby (e.g. ruby 1.8.7 (2010-12-23 patchlevel 330) [i386-mingw32])
|
638
|
+
# See http://makandra.com/notes/1017-maximum-representable-value-for-a-ruby-time-object
|
639
|
+
# In recent versions of Ruby, Time has a much larger range
|
640
|
+
begin
|
641
|
+
result = Time.parse(sdate)
|
642
|
+
rescue ArgumentError
|
643
|
+
result = DateTime.parse(sdate)
|
644
|
+
end
|
645
|
+
|
646
|
+
return result
|
647
|
+
end
|
648
|
+
|
649
|
+
# Parses a value into the proper type based on an xml property element
|
390
650
|
def parse_value(property_xml)
|
391
651
|
property_type = property_xml.attr('type')
|
392
652
|
property_null = property_xml.attr('null')
|
@@ -408,27 +668,80 @@ class Service
|
|
408
668
|
|
409
669
|
# Handle DateTimes
|
410
670
|
# return Time.parse(property_xml.content) if property_type.match(/Edm.DateTime/)
|
411
|
-
if property_type.match(/Edm.DateTime/)
|
412
|
-
sdate = property_xml.content
|
413
|
-
|
414
|
-
# Assume this is UTC if no timezone is specified
|
415
|
-
sdate = sdate + "Z" unless sdate.match(/Z|([+|-]\d{2}:\d{2})$/)
|
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
|
427
|
-
end
|
671
|
+
return parse_date(property_xml.content) if property_type.match(/Edm.DateTime/)
|
428
672
|
|
429
673
|
# If we can't parse the value, just return the element's content
|
430
674
|
property_xml.content
|
431
675
|
end
|
676
|
+
|
677
|
+
# Parses a value into the proper type based on a specified return type
|
678
|
+
def parse_primative_type(value, return_type)
|
679
|
+
return value.to_i if return_type == Fixnum
|
680
|
+
return value.to_d if return_type == Float
|
681
|
+
return parse_date(value.to_s) if return_type == Time
|
682
|
+
return value.to_s
|
683
|
+
end
|
684
|
+
|
685
|
+
# Converts an edm type (string) to a ruby type
|
686
|
+
def edm_to_ruby_type(edm_type)
|
687
|
+
return String if edm_type =~ /Edm.String/
|
688
|
+
return Fixnum if edm_type =~ /^Edm.Int/
|
689
|
+
return Float if edm_type =~ /Edm.Decimal/
|
690
|
+
return Time if edm_type =~ /Edm.DateTime/
|
691
|
+
return String
|
692
|
+
end
|
693
|
+
|
694
|
+
# Method Missing Handlers
|
695
|
+
|
696
|
+
# Executes an import function
|
697
|
+
def execute_import_function(name, *args)
|
698
|
+
func = @function_imports[name]
|
699
|
+
|
700
|
+
# Check the args making sure that more weren't passed in than the function needs
|
701
|
+
param_count = func[:parameters].nil? ? 0 : func[:parameters].count
|
702
|
+
arg_count = args.nil? ? 0 : args[0].count
|
703
|
+
if arg_count > param_count
|
704
|
+
raise ArgumentError, "wrong number of arguments (#{arg_count} for #{param_count})"
|
705
|
+
end
|
706
|
+
|
707
|
+
# Convert the parameters to a hash
|
708
|
+
params = {}
|
709
|
+
func[:parameters].keys.each_with_index { |key, i| params[key] = args[0][i] } unless func[:parameters].nil?
|
710
|
+
|
711
|
+
function_uri = build_function_import_uri(name, params)
|
712
|
+
result = RestClient::Resource.new(function_uri, @rest_options).send(func[:http_method].downcase, {})
|
713
|
+
|
714
|
+
# Is this a 204 (No content) result?
|
715
|
+
return true if result.code == 204
|
716
|
+
|
717
|
+
# No? Then we need to parse the results. There are 4 kinds...
|
718
|
+
if func[:return_type] == Array
|
719
|
+
# a collection of entites
|
720
|
+
return build_classes_from_result(result) if @classes.include?(func[:inner_return_type].to_s)
|
721
|
+
# a collection of native types
|
722
|
+
elements = Nokogiri::XML(result).xpath("//ds:element", @ds_namespaces)
|
723
|
+
results = []
|
724
|
+
elements.each do |e|
|
725
|
+
results << parse_primative_type(e.content, func[:inner_return_type])
|
726
|
+
end
|
727
|
+
return results
|
728
|
+
end
|
729
|
+
|
730
|
+
# a single entity
|
731
|
+
if @classes.include?(func[:return_type].to_s)
|
732
|
+
entry = Nokogiri::XML(result).xpath("atom:entry[not(ancestor::atom:entry)]", @ds_namespaces)
|
733
|
+
return entry_to_class(entry)
|
734
|
+
end
|
735
|
+
|
736
|
+
# or a single native type
|
737
|
+
unless func[:return_type].nil?
|
738
|
+
e = Nokogiri::XML(result).xpath("/*").first
|
739
|
+
return parse_primative_type(e.content, func[:return_type])
|
740
|
+
end
|
741
|
+
|
742
|
+
# Nothing could be parsed, so just return if we got a 200 or not
|
743
|
+
return (result.code == 200)
|
744
|
+
end
|
432
745
|
|
433
746
|
# Helpers
|
434
747
|
def singular?(value)
|