ruby_odata 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. data/.gitignore +3 -2
  2. data/.travis.yml +2 -1
  3. data/.yardopts +6 -0
  4. data/CHANGELOG.md +102 -0
  5. data/Guardfile +14 -0
  6. data/README.md +285 -0
  7. data/Rakefile +0 -7
  8. data/features/basic_auth.feature +3 -2
  9. data/features/batch_request.feature +7 -6
  10. data/features/cassettes/basic_auth_protected_resource.yml +57 -0
  11. data/features/cassettes/batch_request_additions.yml +69 -0
  12. data/features/cassettes/batch_request_deletes.yml +69 -0
  13. data/features/cassettes/batch_request_updates.yml +69 -0
  14. data/features/cassettes/clean_database_for_testing.yml +46 -0
  15. data/features/cassettes/cucumber_tags/basic_auth.yml +297 -0
  16. data/features/cassettes/cucumber_tags/batch_request.yml +1459 -0
  17. data/features/cassettes/cucumber_tags/complex_types.yml +326 -0
  18. data/features/cassettes/cucumber_tags/error_handling.yml +64 -0
  19. data/features/cassettes/cucumber_tags/query_builder.yml +2025 -0
  20. data/features/cassettes/cucumber_tags/service.yml +234 -0
  21. data/features/cassettes/cucumber_tags/service_manage.yml +937 -0
  22. data/features/cassettes/cucumber_tags/service_methods.yml +647 -0
  23. data/features/cassettes/cucumber_tags/ssl.yml +203 -0
  24. data/features/cassettes/cucumber_tags/type_conversion.yml +337 -0
  25. data/features/cassettes/service_manage_additions.yml +65 -0
  26. data/features/cassettes/service_manage_deletions.yml +58 -0
  27. data/features/cassettes/service_manage_deletions_2.yml +58 -0
  28. data/features/cassettes/unsecured_metadata.yml +89 -0
  29. data/features/complex_types.feature +4 -3
  30. data/features/error_handling.feature +14 -0
  31. data/features/query_builder.feature +30 -9
  32. data/features/service.feature +4 -3
  33. data/features/service_manage.feature +6 -5
  34. data/features/service_methods.feature +3 -2
  35. data/features/ssl.feature +8 -8
  36. data/features/step_definitions/service_steps.rb +38 -24
  37. data/features/support/env.rb +1 -3
  38. data/features/support/hooks.rb +3 -2
  39. data/features/support/pickle.rb +29 -18
  40. data/features/support/vcr.rb +24 -0
  41. data/features/type_conversion.feature +16 -17
  42. data/lib/ruby_odata/association.rb +7 -6
  43. data/lib/ruby_odata/class_builder.rb +6 -7
  44. data/lib/ruby_odata/exceptions.rb +4 -0
  45. data/lib/ruby_odata/helpers.rb +11 -0
  46. data/lib/ruby_odata/operation.rb +5 -6
  47. data/lib/ruby_odata/property_metadata.rb +4 -5
  48. data/lib/ruby_odata/query_builder.rb +98 -63
  49. data/lib/ruby_odata/service.rb +118 -103
  50. data/lib/ruby_odata/version.rb +3 -1
  51. data/lib/ruby_odata.rb +20 -18
  52. data/ruby_odata.gemspec +16 -12
  53. data/spec/query_builder_spec.rb +78 -14
  54. data/spec/service_spec.rb +83 -83
  55. data/spec/support/sample_service_matcher.rb +15 -0
  56. data/test/RubyODataService/RubyODataService/App_Data/.gitkeep +0 -0
  57. data/test/blueprints.rb +15 -9
  58. data/test/usage_samples/querying.rb +5 -1
  59. data/test/usage_samples/sample_data.rb +1 -3
  60. metadata +213 -39
  61. data/CHANGELOG.rdoc +0 -88
  62. data/README.rdoc +0 -259
@@ -1,23 +1,28 @@
1
- require 'pickle/world'
2
- require File.expand_path('../../../lib/ruby_odata', __FILE__)
1
+ require "pickle/world"
2
+ require "vcr"
3
+ require File.expand_path("../../../lib/ruby_odata", __FILE__)
3
4
 
4
5
  module OData
5
-
6
6
  module PickleAdapter
7
7
  include Pickle::Adapter::Base
8
-
9
- @@service = OData::Service.new "http://#{WEBSERVER}:#{HTTP_PORT_NUMBER}/SampleService/RubyOData.svc"
10
-
8
+ @@service = nil
9
+ def self.get_service
10
+ return @@service if @@service
11
+ VCR.use_cassette("unsecured_metadata") do
12
+ return OData::Service.new "http://#{WEBSERVER}:#{HTTP_PORT_NUMBER}/SampleService/RubyOData.svc"
13
+ end
14
+ end
15
+
11
16
  # Do not consider these to be part of the class list
12
17
  def self.except_classes
13
18
  @@except_classes ||= []
14
19
  end
15
-
20
+
16
21
  # Gets a list of the available models for this adapter
17
22
  def self.model_classes
18
- @@service.classes.values
23
+ self.get_service.classes.values
19
24
  end
20
-
25
+
21
26
  # get a list of column names for a given class
22
27
  def self.column_names(klass)
23
28
  klass.properties.keys
@@ -26,7 +31,9 @@ module OData
26
31
  # Get an instance by id of the model
27
32
  def self.get_model(klass, id, expand = false)
28
33
  collection = klass.to_s.split('::').last.pluralize
29
- query = @@service.send collection, id
34
+ service = self.get_service
35
+
36
+ query = service.send collection, id
30
37
 
31
38
  if expand then
32
39
  # Expand all navigation properties
@@ -38,24 +45,28 @@ module OData
38
45
  end
39
46
  end
40
47
 
41
- @@service.execute.first
48
+ service.execute.first
42
49
  end
43
50
 
44
51
  # Find the first instance matching conditions
45
52
  def self.find_first_model(klass, conditions)
46
53
  collection = klass.to_s.split('::').last.pluralize
47
- q = @@service.send collection
54
+ service = self.get_service
55
+
56
+ q = service.send collection
48
57
  q.filter(conditions)
49
58
  q.take(1)
50
- @@service.execute.first
59
+ service.execute.first
51
60
  end
52
61
 
53
62
  # Find all models matching conditions
54
63
  def self.find_all_models(klass, conditions)
55
64
  collection = klass.to_s.split('::').last.pluralize
56
- q = @@service.send collection
65
+ service = self.get_service
66
+
67
+ q = service.send collection
57
68
  q.filter(conditions)
58
- @@service.execute
69
+ service.execute
59
70
  end
60
71
 
61
72
  # Create a model using attributes
@@ -63,10 +74,10 @@ module OData
63
74
  instance = klass.send :make, attributes
64
75
 
65
76
  collection = klass.to_s.split('::').last.pluralize
66
- @@service.send "AddTo#{collection}", instance
67
- @@service.save_changes.first
77
+ service = self.get_service
78
+ service.send "AddTo#{collection}", instance
79
+ service.save_changes.first
68
80
  end
69
-
70
81
  end
71
82
  end
72
83
 
@@ -0,0 +1,24 @@
1
+ require "vcr"
2
+ require File.expand_path("../../../spec/support/sample_service_matcher", __FILE__)
3
+
4
+ VCR.configure do |c|
5
+ c.hook_into :webmock
6
+ c.cassette_library_dir = "features/cassettes"
7
+ c.default_cassette_options = {
8
+ :record => :none,
9
+ :match_requests_on => [:method, OData::Support::SampleServiceMatcher]
10
+ }
11
+ end
12
+
13
+ VCR.cucumber_tags do |t|
14
+ t.tags "@basic_auth",
15
+ "@batch_request",
16
+ "@complex_types",
17
+ "@error_handling",
18
+ "@query_builder",
19
+ "@service",
20
+ "@service_manage",
21
+ "@service_methods",
22
+ "@ssl",
23
+ "@type_conversion"
24
+ end
@@ -1,6 +1,7 @@
1
+ @type_conversion
1
2
  Feature: Type conversion
2
3
  In order to accurately perform operations
3
- As a user of the API
4
+ As a user of the API
4
5
  I want types returned to be accurately represented
5
6
 
6
7
  Background:
@@ -9,29 +10,29 @@ Background:
9
10
 
10
11
  Scenario: Integers should be Fixnums
11
12
  Given I call "AddToProducts" on the service with a new "Product" object
12
- And I save changes
13
+ And I save changes
13
14
  When I call "Products" on the service
14
15
  And I run the query
15
- Then the "Id" method on the object should return a Fixnum
16
-
16
+ Then the "Id" method on the object should return a Fixnum
17
+
17
18
  Scenario: Decimals should be BigDecimals
18
- Given I call "AddToProducts" on the service with a new "Product" object
19
+ Given I call "AddToProducts" on the service with a new "Product" object
19
20
  And I save changes
20
- When I call "Products" on the service
21
+ When I call "Products" on the service
21
22
  And I run the query
22
23
  Then the "Price" method on the object should return a BigDecimal
23
24
 
24
25
  Scenario: DateTimes should be Times
25
- Given I call "AddToProducts" on the service with a new "Product" object
26
+ Given I call "AddToProducts" on the service with a new "Product" object
26
27
  And I save changes
27
- When I call "Products" on the service
28
+ When I call "Products" on the service
28
29
  And I run the query
29
- Then the "AuditFields.CreateDate" method on the object should return a Time
30
-
30
+ Then the "AuditFields.CreateDate" method on the object should return a Time
31
+
31
32
  Scenario: Verify that DateTimes don't change if not modified on an update
32
- Given I call "AddToProducts" on the service with a new "Product" object with Name: "Test Product"
33
- When I save changes
34
- And I call "Products" on the service with args: "1"
33
+ Given I call "AddToProducts" on the service with a new "Product" object with Name: "Test Product"
34
+ When I save changes
35
+ And I call "Products" on the service with args: "1"
35
36
  And I run the query
36
37
  Then I store the first last query result for comparison
37
38
  When I set "Name" on the first result to "Changed Test Product"
@@ -44,10 +45,8 @@ Scenario: Verify that DateTimes don't change if not modified on an update
44
45
  Then the new query first result's time "AuditFields.CreateDate" should equal the saved query result
45
46
 
46
47
  Scenario: DateTimes should be able to be null
47
- Given I call "AddToProducts" on the service with a new "Product" object
48
+ Given I call "AddToProducts" on the service with a new "Product" object
48
49
  And I save changes
49
- When I call "Products" on the service
50
+ When I call "Products" on the service
50
51
  And I run the query
51
52
  Then the "DiscontinuedDate" method on the object should return a NilClass
52
-
53
-
@@ -1,8 +1,9 @@
1
1
  module OData
2
+ # Internal class used to represent object associations
2
3
  class Association
3
-
4
+
4
5
  attr_reader :name, :namespace, :relationship, :from_role, :to_role
5
-
6
+
6
7
  def initialize(nav_prop_element, edmx)
7
8
  @edmx = edmx
8
9
 
@@ -11,9 +12,9 @@ module OData
11
12
  @edmx_namespaces = { "edmx" => "http://schemas.microsoft.com/ado/2007/06/edmx", "edm" => edm_ns }
12
13
  parse_nav_prop(nav_prop_element)
13
14
  end
14
-
15
+
15
16
  private
16
-
17
+
17
18
  def parse_nav_prop(element)
18
19
  @relationship = element['Relationship']
19
20
  relationship_parts = @relationship.split('.')
@@ -22,12 +23,12 @@ module OData
22
23
  @from_role = role_hash(@name, element['FromRole'])
23
24
  @to_role = role_hash(@name, element['ToRole'])
24
25
  end
25
-
26
+
26
27
  def role_hash(association_name, role_name)
27
28
  # Find the end role based on the assocation name
28
29
  role_xpath = "/edmx:Edmx/edmx:DataServices/edm:Schema[@Namespace='#{@namespace}']/edm:Association[@Name='#{association_name}']/edm:End[@Role='#{role_name}']"
29
30
  role_element = @edmx.xpath(role_xpath, @edmx_namespaces).first
30
- { role_name => {
31
+ { role_name => {
31
32
  :edmx_type => "#{role_element['Type']}",
32
33
  :multiplicity => "#{role_element['Multiplicity']}"
33
34
  }}
@@ -1,14 +1,13 @@
1
1
  module OData
2
- # Internally used helper class for building a dynamic class. This class shouldn't be called directly.
2
+ # Internal helper class for building a dynamic class. This class shouldn't be called directly.
3
3
  class ClassBuilder
4
4
  # Creates a new instance of the ClassBuilder class
5
5
  #
6
- # ==== Required Attributes
7
- # - klass_name: The name/type of the class to create
8
- # - methods: The accessor methods to add to the class
9
- # - nav_props: The accessor methods to add for navigation properties
10
- # - context: The service context that this entity belongs to
11
- # - namespaces: Optional namespace to create the classes in
6
+ # @param [String] klass_name the name/type of the class to create
7
+ # @param [Array] methods the accessor methods to add to the class
8
+ # @param [Array] nav_props the accessor methods to add for navigation properties
9
+ # @param [Service] context the service context that this entity belongs to
10
+ # @param [String, nil] namespace optional namespace to create the classes in
12
11
  def initialize(klass_name, methods, nav_props, context, namespace = nil)
13
12
  @klass_name = klass_name.camelcase
14
13
  @methods = methods
@@ -0,0 +1,4 @@
1
+ module OData
2
+ # Raised when a user attempts to do something that is not supported
3
+ class NotSupportedError < StandardError; end
4
+ end
@@ -1,4 +1,5 @@
1
1
  module OData
2
+ # Helper methods
2
3
  class Helpers
3
4
  # Helper to normalize the results of a select result; Ruby 1.9 Hash.select returns a Hash, 1.8 returns an Array
4
5
  # This is for Ruby 1.8 support, but should be removed in the future
@@ -6,5 +7,15 @@ module OData
6
7
  return nil if val.nil?
7
8
  (val.is_a? Hash) ? val : Hash[*val.flatten]
8
9
  end
10
+
11
+ # Wrapper for URI escaping that switches between URI::Parser#escape and
12
+ # URI.escape for 1.9-compatibility (thanks FakeWeb https://github.com/chrisk/fakeweb/blob/master/lib/fake_web/utility.rb#L40)
13
+ def self.uri_escape(*args)
14
+ if URI.const_defined?(:Parser)
15
+ URI::Parser.new.escape(*args)
16
+ else
17
+ URI.escape(*args)
18
+ end
19
+ end
9
20
  end
10
21
  end
@@ -2,14 +2,13 @@ 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
4
  attr_accessor :kind, :klass_name, :klass, :child_klass
5
-
5
+
6
6
  # Creates a new instance of the Operation class
7
7
  #
8
- # ==== Required Attributes
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
8
+ # @param [String] kind the operation type (Standard: Add, Update, or Delete | Links: AddLink)
9
+ # @param [String] klass_name the name/type of the class to operate against
10
+ # @param [Object] klass the actual class
11
+ # @param [Object, nil] child_klass used for link operations only
13
12
  def initialize(kind, klass_name, klass, child_klass = nil)
14
13
  @kind = kind
15
14
  @klass_name = klass_name
@@ -15,13 +15,12 @@ module OData
15
15
  attr_reader :nav_prop
16
16
  # Applies only to navigation properties; the association corresponding to the property
17
17
  attr_accessor :association
18
-
18
+
19
19
  # Creates a new instance of the Class Property class
20
20
  #
21
- # ==== Required Attributes
22
- # property_element: The property element from the EDMX
23
-
24
- def initialize(property_element)
21
+ # @param [Nokogiri::XML::Node] property_element from the EDMX
22
+
23
+ def initialize(property_element)
25
24
  @name = property_element['Name']
26
25
  @type = property_element['Type']
27
26
  @nullable = ((property_element['Nullable'] && property_element['Nullable'] == "true") || property_element.name == 'NavigationProperty') || false
@@ -1,117 +1,152 @@
1
1
  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
- # For example, given the following code snippet:
5
- # svc = OData::Service.new "http://127.0.0.1:8989/SampleService/RubyOData.svc"
6
- # svc.Categories
7
- # The *Categories* method would return a QueryBuilder
4
+ # @example For example, given the following code snippet:
5
+ # svc = OData::Service.new "http://127.0.0.1:8989/SampleService/RubyOData.svc"
6
+ # svc.Categories
7
+ # The *Categories* method would return a QueryBuilder
8
8
  class QueryBuilder
9
9
  # Creates a new instance of the QueryBuilder class
10
10
  #
11
- # ==== Required Attributes
12
- # - root: The root entity collection to query against
13
- # ==== Optional
14
- # Hash of additional parameters to use for a query
11
+ # @param [String] root entity collection to query against
12
+ # @param [Hash, {}] additional_params hash of additional parameters to use for a query
15
13
  def initialize(root, additional_params = {})
16
- @root = root.to_s
14
+ @root = Helpers.uri_escape(root.to_s)
17
15
  @expands = []
18
16
  @filters = []
19
17
  @order_bys = []
18
+ @navigation_paths = []
20
19
  @skip = nil
21
20
  @top = nil
22
- @links = nil
21
+ @count = nil
22
+ @links_navigation_property = nil
23
23
  @additional_params = additional_params
24
24
  end
25
-
25
+
26
26
  # Used to eagerly-load data for nested objects, for example, obtaining a Category for a Product within one call to the server
27
- # ==== Required Attributes
28
- # - path: The path of the entity to expand relative to the root
29
27
  #
30
- # ==== Example
31
- # # Without expanding the query (no Category will be filled in for the Product)
32
- # svc.Products(1)
33
- # prod1 = svc.execute
28
+ # @param [String] path of the entity to expand relative to the root
29
+ # @example
30
+ # # Without expanding the query (no Category will be filled in for the Product)
31
+ # svc.Products(1)
32
+ # prod1 = svc.execute
34
33
  #
35
- # # With expanding the query (the Category will be filled in)
36
- # svc.Products(1).expand('Category')
37
- # prod1 = svc.execute
34
+ # # With expanding the query (the Category will be filled in)
35
+ # svc.Products(1).expand('Category')
36
+ # prod1 = svc.execute
38
37
  def expand(path)
39
38
  @expands << path
40
39
  self
41
40
  end
42
-
41
+
43
42
  # Used to filter data being returned
44
- # ==== Required Attributes
45
- # - filter: The conditions to apply to the query
46
43
  #
47
- # ==== Example
48
- # svc.Products.filter("Name eq 'Product 2'")
49
- # products = svc.execute
44
+ # @param [String] filter conditions to apply to the query
45
+ #
46
+ # @example
47
+ # svc.Products.filter("Name eq 'Product 2'")
48
+ # products = svc.execute
50
49
  def filter(filter)
51
50
  @filters << CGI.escape(filter)
52
51
  self
53
52
  end
54
-
53
+
55
54
  # Used to order the data being returned
56
- # ==== Required Attributes
57
- # - order_by: The order by statement. Note to specify direction, use "desc" or "asc"; must be lowercase
58
55
  #
59
- # ==== Example
60
- # svc.Products.order_by("Name")
61
- # products = svc.execute
56
+ # @param [String] order_by the order by statement. Note to specify direction, use "desc" or "asc"; must be lowercase
57
+ #
58
+ # @example
59
+ # svc.Products.order_by("Name")
60
+ # products = svc.execute
62
61
  def order_by(order_by)
63
62
  @order_bys << CGI.escape(order_by)
64
63
  self
65
64
  end
66
-
67
- # Used to skip a number of records
68
- # This is typically used for paging, where it would be used along with the +top+ method.
69
- # ==== Required Attributes
70
- # - num: The number of items to skip
71
- #
72
- # ==== Example
73
- # svc.Products.skip(5)
74
- # products = svc.execute # => skips the first 5 items
65
+
66
+ # Used to skip a number of records
67
+ # This is typically used for paging, where it would be used along with the `top` method.
68
+ #
69
+ # @param [Integer] num the number of items to skip
70
+ #
71
+ # @example
72
+ # svc.Products.skip(5)
73
+ # products = svc.execute # => skips the first 5 items
75
74
  def skip(num)
76
75
  @skip = num
77
76
  self
78
77
  end
79
-
80
- # Used to take only the top X records
81
- # This is typically used for paging, where it would be used along with the +skip+ method.
82
- # ==== Required Attributes
83
- # - num: The number of items to return
84
- #
85
- # ==== Example
86
- # svc.Products.top(5)
87
- # products = svc.execute # => returns only the first 5 items
78
+
79
+ # Used to take only the top X records
80
+ # This is typically used for paging, where it would be used along with the `skip` method.
81
+ #
82
+ # @param [Integer] num the number of items to return
83
+ #
84
+ # @example
85
+ # svc.Products.top(5)
86
+ # products = svc.execute # => returns only the first 5 items
88
87
  def top(num)
89
88
  @top = num
90
89
  self
91
90
  end
92
-
91
+
93
92
  # Used to return links instead of actual objects
94
- # ==== Required Attributes
95
- # - navigation_property: The NavigationProperty name to retrieve the links for
96
93
  #
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
94
+ # @param [String] navigation_property the NavigationProperty name to retrieve the links for
95
+ #
96
+ # @raise [NotSupportedError] if count has already been called on the query
97
+ #
98
+ # @example
99
+ # svc.Categories(1).links("Products")
100
+ # product_links = svc.execute # => returns URIs for the products under the Category with an ID of 1
100
101
  def links(navigation_property)
101
- @navigation_property = navigation_property
102
+ raise OData::NotSupportedError.new("You cannot call both the `links` method and the `count` method in the same query.") if @count
103
+ @links_navigation_property = navigation_property
104
+ self
105
+ end
106
+
107
+ # Used to return a count of objects instead of the objects themselves
108
+ #
109
+ # @raise [NotSupportedError] if links has already been called on the query
110
+ #
111
+ # @example
112
+ # svc.Products
113
+ # svc.count
114
+ # product_count = svc.execute
115
+ def count
116
+ raise OData::NotSupportedError.new("You cannot call both the `links` method and the `count` method in the same query.") if @links_navigation_property
117
+ @count = true
102
118
  self
103
119
  end
104
-
120
+
121
+ # Used to navigate to a child collection, typically used to filter or perform a similar function against the children
122
+ #
123
+ # @param [String] navigation_property the NavigationProperty to drill-down into
124
+ #
125
+ # @example
126
+ # svc.Genres('Horror Movies').navigate("Titles").filter("Name eq 'Halloween'")
127
+ def navigate(navigation_property)
128
+ @navigation_paths << Helpers.uri_escape(navigation_property)
129
+ self
130
+ end
131
+
105
132
  # Builds the query URI (path, not including root) incorporating expands, filters, etc.
106
133
  # This is used internally when the execute method is called on the service
107
134
  def query
108
135
  q = @root.clone
109
-
136
+
137
+ # Navigation paths come first in the query
138
+ q << "/" + @navigation_paths.join("/") unless @navigation_paths.empty?
139
+
110
140
  # Handle links queries, this isn't just a standard query option
111
- if @navigation_property
112
- q << "/$links/#{@navigation_property}"
141
+ if @links_navigation_property
142
+ q << "/$links/#{@links_navigation_property}"
113
143
  end
114
-
144
+
145
+ # Handle count queries, this isn't just a standard query option
146
+ if @count
147
+ q << "/$count"
148
+ end
149
+
115
150
  query_options = []
116
151
  query_options << "$expand=#{@expands.join(',')}" unless @expands.empty?
117
152
  query_options << "$filter=#{@filters.join('+and+')}" unless @filters.empty?
@@ -121,7 +156,7 @@ class QueryBuilder
121
156
  query_options << @additional_params.to_query unless @additional_params.empty?
122
157
  if !query_options.empty?
123
158
  q << "?"
124
- q << query_options.join('&')
159
+ q << query_options.join('&')
125
160
  end
126
161
  return q
127
162
  end