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.
Files changed (173) hide show
  1. data/.gitignore +9 -3
  2. data/.travis.yml +4 -0
  3. data/CHANGELOG.rdoc +18 -1
  4. data/README.rdoc +77 -19
  5. data/config/cucumber.yml +4 -3
  6. data/features/basic_auth.feature +5 -7
  7. data/features/batch_request.feature +11 -11
  8. data/features/complex_types.feature +11 -11
  9. data/features/query_builder.feature +27 -18
  10. data/features/service.feature +8 -8
  11. data/features/service_manage.feature +27 -12
  12. data/features/service_methods.feature +37 -0
  13. data/features/ssl.feature +4 -4
  14. data/features/step_definitions/pickle_steps.rb +100 -0
  15. data/features/step_definitions/service_steps.rb +99 -65
  16. data/features/support/constants.rb +3 -0
  17. data/features/support/custom_helpers.rb +54 -0
  18. data/features/support/env.rb +0 -10
  19. data/features/support/hooks.rb +1 -1
  20. data/features/support/pickle.rb +82 -0
  21. data/features/type_conversion.feature +10 -10
  22. data/lib/ruby_odata.rb +4 -1
  23. data/lib/ruby_odata/association.rb +36 -0
  24. data/lib/ruby_odata/class_builder.rb +96 -16
  25. data/lib/ruby_odata/helpers.rb +10 -0
  26. data/lib/ruby_odata/operation.rb +7 -5
  27. data/lib/ruby_odata/property_metadata.rb +11 -6
  28. data/lib/ruby_odata/query_builder.rb +20 -1
  29. data/lib/ruby_odata/service.rb +407 -94
  30. data/lib/ruby_odata/version.rb +1 -1
  31. data/ruby_odata.gemspec +2 -0
  32. data/spec/association_spec.rb +48 -0
  33. data/spec/class_builder_spec.rb +11 -2
  34. data/spec/fixtures/inheritance/edmx_pluralsight.xml +111 -0
  35. data/spec/fixtures/inheritance/result_pluralsight_courses.xml +229 -0
  36. data/spec/fixtures/links/result_links_query.xml +6 -0
  37. data/spec/fixtures/partial/partial_feed_metadata.xml +25 -0
  38. data/spec/fixtures/partial/partial_feed_part_1.xml +42 -0
  39. data/spec/fixtures/partial/partial_feed_part_2.xml +42 -0
  40. data/spec/fixtures/partial/partial_feed_part_3.xml +40 -0
  41. data/spec/fixtures/sample_service/edmx_categories_products.xml +1 -0
  42. data/spec/fixtures/sample_service/result_category_names.xml +5 -0
  43. data/spec/fixtures/sample_service/result_entity_category_web_get.xml +29 -0
  44. data/spec/fixtures/sample_service/result_entity_single_category_web_get.xml +23 -0
  45. data/spec/fixtures/sample_service/result_first_category_id.xml +2 -0
  46. data/spec/fixtures/sample_service/result_multiple_category_products.xml +57 -0
  47. data/spec/fixtures/sample_service/result_single_category.xml +18 -0
  48. data/spec/fixtures/sample_service/result_single_product.xml +26 -0
  49. data/spec/fixtures/sample_service/result_single_product_not_found.xml +4 -0
  50. data/spec/fixtures/sap/edmx_sap_demo_flight.xml +62 -58
  51. data/spec/property_metadata_spec.rb +9 -2
  52. data/spec/query_builder_spec.rb +11 -0
  53. data/spec/revised_service_spec.rb +197 -0
  54. data/spec/service_spec.rb +463 -4
  55. data/test/RubyODataService/RubyODataService.sln +20 -0
  56. data/test/RubyODataService/RubyODataService/App_Start/EntityFramework.SqlServerCompact.cs +12 -0
  57. data/test/RubyODataService/RubyODataService/BasicAuth/RubyOData.svc +3 -0
  58. data/test/RubyODataService/RubyODataService/BasicAuth/RubyOData.svc.cs +107 -0
  59. data/test/RubyODataService/RubyODataService/Global.asax +1 -0
  60. data/test/RubyODataService/RubyODataService/Global.asax.cs +49 -0
  61. data/test/RubyODataService/RubyODataService/Models/AuditFields.cs +16 -0
  62. data/test/RubyODataService/RubyODataService/Models/Category.cs +16 -0
  63. data/test/RubyODataService/RubyODataService/Models/Product.cs +20 -0
  64. data/test/RubyODataService/RubyODataService/Properties/AssemblyInfo.cs +35 -0
  65. data/test/RubyODataService/RubyODataService/RubyOData.svc +3 -0
  66. data/test/RubyODataService/RubyODataService/RubyOData.svc.cs +57 -0
  67. data/test/RubyODataService/RubyODataService/RubyODataContext.cs +16 -0
  68. data/test/RubyODataService/RubyODataService/RubyODataService.csproj +159 -0
  69. data/test/RubyODataService/RubyODataService/RubyODataService.csproj.user +31 -0
  70. data/test/RubyODataService/RubyODataService/Web.Debug.config +30 -0
  71. data/test/RubyODataService/RubyODataService/Web.Release.config +31 -0
  72. data/test/RubyODataService/RubyODataService/Web.config +27 -0
  73. data/test/RubyODataService/RubyODataService/bin/EntityFramework.dll +0 -0
  74. data/test/RubyODataService/RubyODataService/bin/Microsoft.Data.Edm.dll +0 -0
  75. data/test/RubyODataService/RubyODataService/bin/Microsoft.Data.OData.dll +0 -0
  76. data/test/RubyODataService/RubyODataService/bin/Microsoft.Data.Services.Client.dll +0 -0
  77. data/test/RubyODataService/RubyODataService/bin/Microsoft.Data.Services.dll +0 -0
  78. data/test/RubyODataService/RubyODataService/bin/Microsoft.Data.Spatial.dll +0 -0
  79. data/test/RubyODataService/RubyODataService/bin/RubyODataService.dll +0 -0
  80. data/test/RubyODataService/RubyODataService/bin/System.Data.SqlServerCe.Entity.dll +0 -0
  81. data/test/RubyODataService/RubyODataService/bin/System.Data.SqlServerCe.dll +0 -0
  82. data/test/RubyODataService/RubyODataService/bin/System.Spatial.dll +0 -0
  83. data/test/RubyODataService/RubyODataService/bin/WebActivator.dll +0 -0
  84. data/test/RubyODataService/RubyODataService/bin/amd64/Microsoft.VC90.CRT/Microsoft.VC90.CRT.manifest +6 -0
  85. data/test/RubyODataService/RubyODataService/bin/amd64/Microsoft.VC90.CRT/README_ENU.txt +0 -0
  86. data/test/RubyODataService/RubyODataService/bin/amd64/Microsoft.VC90.CRT/msvcr90.dll +0 -0
  87. data/test/RubyODataService/RubyODataService/bin/amd64/sqlcecompact40.dll +0 -0
  88. data/test/RubyODataService/RubyODataService/bin/amd64/sqlceer40EN.dll +0 -0
  89. data/test/RubyODataService/RubyODataService/bin/amd64/sqlceme40.dll +0 -0
  90. data/test/RubyODataService/RubyODataService/bin/amd64/sqlceqp40.dll +0 -0
  91. data/test/RubyODataService/RubyODataService/bin/amd64/sqlcese40.dll +0 -0
  92. data/test/RubyODataService/RubyODataService/bin/x86/Microsoft.VC90.CRT/Microsoft.VC90.CRT.manifest +6 -0
  93. data/test/RubyODataService/RubyODataService/bin/x86/Microsoft.VC90.CRT/README_ENU.txt +0 -0
  94. data/test/RubyODataService/RubyODataService/bin/x86/Microsoft.VC90.CRT/msvcr90.dll +0 -0
  95. data/test/RubyODataService/RubyODataService/bin/x86/sqlcecompact40.dll +0 -0
  96. data/test/RubyODataService/RubyODataService/bin/x86/sqlceer40EN.dll +0 -0
  97. data/test/RubyODataService/RubyODataService/bin/x86/sqlceme40.dll +0 -0
  98. data/test/RubyODataService/RubyODataService/bin/x86/sqlceqp40.dll +0 -0
  99. data/test/RubyODataService/RubyODataService/bin/x86/sqlcese40.dll +0 -0
  100. data/test/RubyODataService/RubyODataService/packages.config +7 -0
  101. data/test/RubyODataService/packages/EntityFramework.4.2.0.0/EntityFramework.4.2.0.0.nupkg +0 -0
  102. data/test/RubyODataService/packages/EntityFramework.4.2.0.0/lib/net40/EntityFramework.dll +0 -0
  103. data/test/RubyODataService/packages/EntityFramework.4.2.0.0/lib/net40/EntityFramework.xml +13488 -0
  104. data/test/RubyODataService/packages/EntityFramework.SqlServerCompact.4.1.8482.2/Content/App_Start/EntityFramework.SqlServerCompact.cs.pp +12 -0
  105. data/test/RubyODataService/packages/EntityFramework.SqlServerCompact.4.1.8482.2/EULA_ENU.rtf +969 -0
  106. data/test/RubyODataService/packages/EntityFramework.SqlServerCompact.4.1.8482.2/EntityFramework.SqlServerCompact.4.1.8482.2.nupkg +0 -0
  107. data/test/RubyODataService/packages/EntityFramework.SqlServerCompact.4.1.8482.2/lib/System.Data.SqlServerCe.Entity.dll +0 -0
  108. data/test/RubyODataService/packages/EntityFramework.SqlServerCompact.4.1.8482.2/tools/install.ps1 +3 -0
  109. data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/Content/web.config.transform +8 -0
  110. data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/NativeBinaries/amd64/Microsoft.VC90.CRT/Microsoft.VC90.CRT.manifest +6 -0
  111. data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/NativeBinaries/amd64/Microsoft.VC90.CRT/README_ENU.txt +0 -0
  112. data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/NativeBinaries/amd64/Microsoft.VC90.CRT/msvcr90.dll +0 -0
  113. data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/NativeBinaries/amd64/sqlcecompact40.dll +0 -0
  114. data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/NativeBinaries/amd64/sqlceer40EN.dll +0 -0
  115. data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/NativeBinaries/amd64/sqlceme40.dll +0 -0
  116. data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/NativeBinaries/amd64/sqlceqp40.dll +0 -0
  117. data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/NativeBinaries/amd64/sqlcese40.dll +0 -0
  118. data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/NativeBinaries/x86/Microsoft.VC90.CRT/Microsoft.VC90.CRT.manifest +6 -0
  119. data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/NativeBinaries/x86/Microsoft.VC90.CRT/README_ENU.txt +0 -0
  120. data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/NativeBinaries/x86/Microsoft.VC90.CRT/msvcr90.dll +0 -0
  121. data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/NativeBinaries/x86/sqlcecompact40.dll +0 -0
  122. data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/NativeBinaries/x86/sqlceer40EN.dll +0 -0
  123. data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/NativeBinaries/x86/sqlceme40.dll +0 -0
  124. data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/NativeBinaries/x86/sqlceqp40.dll +0 -0
  125. data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/NativeBinaries/x86/sqlcese40.dll +0 -0
  126. data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/SQLCE_EULA_ENU.rtf +778 -0
  127. data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/SqlServerCompact.4.0.8482.1.nupkg +0 -0
  128. data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/Tools/GetSqlCEPostBuildCmd.ps1 +12 -0
  129. data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/Tools/install.ps1 +11 -0
  130. data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/Tools/uninstall.ps1 +9 -0
  131. data/test/RubyODataService/packages/SqlServerCompact.4.0.8482.1/lib/System.Data.SqlServerCe.dll +0 -0
  132. data/test/RubyODataService/packages/WCF DataServices October 2011 CTP/DataSvcUtil.exe +0 -0
  133. data/test/RubyODataService/packages/WCF DataServices October 2011 CTP/License.rtf +708 -0
  134. data/test/RubyODataService/packages/WCF DataServices October 2011 CTP/Microsoft.Data.Edm.dll +0 -0
  135. data/test/RubyODataService/packages/WCF DataServices October 2011 CTP/Microsoft.Data.Edm.xml +4150 -0
  136. data/test/RubyODataService/packages/WCF DataServices October 2011 CTP/Microsoft.Data.OData.dll +0 -0
  137. data/test/RubyODataService/packages/WCF DataServices October 2011 CTP/Microsoft.Data.OData.xml +1969 -0
  138. data/test/RubyODataService/packages/WCF DataServices October 2011 CTP/Microsoft.Data.Services.Client.dll +0 -0
  139. data/test/RubyODataService/packages/WCF DataServices October 2011 CTP/Microsoft.Data.Services.Client.xml +1442 -0
  140. data/test/RubyODataService/packages/WCF DataServices October 2011 CTP/Microsoft.Data.Services.Design.dll +0 -0
  141. data/test/RubyODataService/packages/WCF DataServices October 2011 CTP/Microsoft.Data.Services.Design.xml +191 -0
  142. data/test/RubyODataService/packages/WCF DataServices October 2011 CTP/Microsoft.Data.Services.dll +0 -0
  143. data/test/RubyODataService/packages/WCF DataServices October 2011 CTP/Microsoft.Data.Services.xml +2559 -0
  144. data/test/RubyODataService/packages/WCF DataServices October 2011 CTP/Microsoft.Data.Spatial.dll +0 -0
  145. data/test/RubyODataService/packages/WCF DataServices October 2011 CTP/Microsoft.Data.Spatial.xml +15 -0
  146. data/test/RubyODataService/packages/WCF DataServices October 2011 CTP/Microsoft.VsDesigner.DataServices.Adapter.dll +0 -0
  147. data/test/RubyODataService/packages/WCF DataServices October 2011 CTP/README.txt +6 -0
  148. data/test/RubyODataService/packages/WCF DataServices October 2011 CTP/System.Spatial.dll +0 -0
  149. data/test/RubyODataService/packages/WCF DataServices October 2011 CTP/System.Spatial.xml +2276 -0
  150. data/test/RubyODataService/packages/WebActivator.1.0.0.0/WebActivator.1.0.0.0.nupkg +0 -0
  151. data/test/RubyODataService/packages/WebActivator.1.0.0.0/lib/WebActivator.dll +0 -0
  152. data/test/RubyODataService/packages/repositories.config +4 -0
  153. data/test/applicationhost.config.template +2 -2
  154. data/test/blueprints.rb +5 -4
  155. data/test/iisExpress x64.bat b/data/test/iisExpress → x64.bat +0 -0
  156. data/test/iisExpress x86.bat b/data/test/iisExpress → x86.bat +0 -0
  157. data/test/setpath.rb +13 -3
  158. data/test/usage_samples/querying.rb +45 -0
  159. data/test/usage_samples/reflection.rb +16 -0
  160. data/test/usage_samples/sample_data.rb +30 -0
  161. metadata +327 -36
  162. data/test/Cassini x64.bat +0 -1
  163. data/test/Cassini x86.bat +0 -1
  164. data/test/SampleService/App_Code/AuditFields.cs +0 -13
  165. data/test/SampleService/App_Code/Entities.cs +0 -145
  166. data/test/SampleService/App_Code/Model.Designer.cs +0 -578
  167. data/test/SampleService/App_Code/Model.edmx +0 -157
  168. data/test/SampleService/App_Code/ModelContainerExtended.cs +0 -32
  169. data/test/SampleService/App_Data/_TestDB.mdf +0 -0
  170. data/test/SampleService/App_Data/_TestDB_Log.ldf +0 -0
  171. data/test/SampleService/BasicAuth/Entities.svc +0 -1
  172. data/test/SampleService/Entities.svc +0 -1
  173. 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
@@ -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: The operation type (Add, Update, or Delete)
10
- # - klass_name: The name/type of the class to operate against
11
- # - klass: The actual class
12
- def initialize(kind, klass_name, klass)
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
- attr_accessor :name
5
+ attr_reader :name
6
6
  # The property EDM type
7
- attr_accessor :type
7
+ attr_reader :type
8
8
  # Is the property nullable?
9
- attr_accessor :nullable
9
+ attr_reader :nullable
10
10
  # Feed customization target path
11
- attr_accessor :fc_target_path
11
+ attr_reader :fc_target_path
12
12
  # Should the property appear in both the mapped schema path and the properties collection
13
- attr_accessor :fc_keep_in_content
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/Entities.svc"
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?
@@ -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
- @options = options
17
- @rest_options = { :verify_ssl => get_verify_mode, :user => @options[:username], :password => @options[:password] }
18
- @collections = []
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
- build_classes_from_result(result)
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 = doc.xpath("//edm:ComplexType", "edm" => edm_ns) || []
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", "edm" => edm_ns)
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 = doc.xpath("//edm:EntityType", "edm" => edm_ns)
220
+ entity_types = @edmx.xpath("//edm:EntityType", @ds_namespaces)
153
221
  entity_types.each do |e|
154
- name = e['Name']
155
- props = e.xpath(".//edm:Property", "edm" => edm_ns)
156
- @class_metadata[name] = build_property_metadata(props)
157
- methods = props.collect { |p| p['Name'] } # Standard Properties
158
- nprops = e.xpath(".//edm:NavigationProperty", "edm" => edm_ns)
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
- entries = doc.xpath("//atom:entry[not(ancestor::atom:entry)]", "atom" => "http://www.w3.org/2005/Atom")
177
- return entry_to_class(entries[0]) if entries.length == 1
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", "atom" => "http://www.w3.org/2005/Atom").to_s.split('.')[-1]
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", "atom" => "http://www.w3.org/2005/Atom").first
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", { "m" => "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" }).any?
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, { "m" => "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" })
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", "atom" => "http://www.w3.org/2005/Atom")[0].content
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", "atom" => "http://www.w3.org/2005/Atom").first
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", "atom" => "http://www.w3.org/2005/Atom").first
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]", { "m" => "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata", "atom" => "http://www.w3.org/2005/Atom" })
390
+ inline_links = entry.xpath("./atom:link[m:inline]", @ds_namespaces)
230
391
 
231
- for link in inline_links
232
- inline_entries = link.xpath(".//atom:entry", "atom" => "http://www.w3.org/2005/Atom")
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", "atom" => "http://www.w3.org/2005/Atom")
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
- end
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
- end
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)