odata4 0.8.2 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a9465568c8c8b575d04c9367e8c51b3c6161a8b9
4
- data.tar.gz: 1da130b782964c9ccf410e74a6309c78bd13e050
3
+ metadata.gz: d39ce48da9f2bd4c5e58b4be1e0d5ac4a60c4f3e
4
+ data.tar.gz: 0428d049a8e814d727fc7796c795fc4fddbe8265
5
5
  SHA512:
6
- metadata.gz: c2949929b4b43e3fde0b26d845f630d59dfc7d5936717dca0531c11bba7514487c72e889a372863f8be2c2934ff85dee1cd8df9dc67193d145ca0245f42133cf
7
- data.tar.gz: a1de80f33ab0719567cabbc948dc645a31363697ee705ba0654cabb31fbaffeb130cb751ed061b55dccf169f977e62689b96d3b6180a4052d1fdd31a10b7b5ca
6
+ metadata.gz: 6d7a7d71b67e188600e7c7ccce3dd55721a27e3cc3371d790eb2d430f4b045e365d37871cd6dd9629586553e22029b52020c622cb0840ab94a52ef05475eb66d
7
+ data.tar.gz: 70712da5e1e2e2dfc6b9d244b97cdd95c1fa4f727dac590aea3786e8494efa1f8244f82a65b778a2a824fd7bf16ac43a177e1842017789e7cd681ed28819f9f4
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 0.9.0
4
+
5
+ * [Breaking] Use Faraday instead of Typhoeus as HTTP connection adapter.
6
+ * [Breaking] Deprecate `Service.open` in favor of using constructor directly.
7
+
3
8
  ## 0.8.2
4
9
 
5
10
  * [Refactor] Moved `ComplexType` and `EnumType` class into `Schema`, respective property types into `Properties` namespace
data/README.md CHANGED
@@ -41,16 +41,18 @@ The nice thing about `OData4::Service` is that it automatically registers with t
41
41
  To create an `OData4::Service` simply provide the location of a service endpoint to it like this:
42
42
 
43
43
  ```ruby
44
- OData4::Service.open('http://services.odata.org/V4/OData/OData.svc')
44
+ OData4::Service.new('http://services.odata.org/V4/OData/OData.svc')
45
45
  ```
46
46
 
47
47
  You may also provide an options hash after the URL.
48
48
  It is suggested that you supply a name for the service via this hash like so:
49
49
 
50
50
  ```ruby
51
- OData4::Service.open('http://services.odata.org/V4/OData/OData.svc', name: 'ODataDemo')
51
+ OData4::Service.new('http://services.odata.org/V4/OData/OData.svc', name: 'ODataDemo')
52
52
  ```
53
53
 
54
+ For more information regarding available options and how to configure a service instance, refer to [Service Configuration](#service-configuration) below.
55
+
54
56
  This one call will setup the service and allow for the discovery of everything the other parts of the OData4 gem need to function.
55
57
  The two methods you will want to remember from `OData4::Service` are `#service_url` and `#name`.
56
58
  Both of these methods are available on instances and will allow for lookup in the `OData4::ServiceRegistry`, should you need it.
@@ -66,6 +68,112 @@ Both of the above calls would retrieve the same service from the registry.
66
68
  At the moment there is no protection against name collisions provided in `OData4::ServiceRegistry`.
67
69
  So, looking up services by their service URL is the most exact method, but lookup by name is provided for convenience.
68
70
 
71
+ ### Service Configuration
72
+
73
+ #### Metadata File
74
+
75
+ Typically the metadata file of a service can be quite large.
76
+ You can speed your load time by forcing the service to load the metadata from a file rather than a URL.
77
+ This is only recommended for testing purposes, as the metadata file can change.
78
+
79
+ ```ruby
80
+ service = OData4::Service.new('http://services.odata.org/V4/OData/OData.svc', {
81
+ name: 'ODataDemo',
82
+ metadata_file: "metadata.xml",
83
+ })
84
+ ```
85
+
86
+ #### Headers & Authorization
87
+
88
+ The OData protocol does not deal with authentication and authorization at all, nor does it need to, since [HTTP already provides many different options][http-auth] for this, such as HTTP Basic or token authorization.
89
+ Hence, this gem does not implement any special authentication mechanisms either, and relies on the underlying HTTP library ([Faraday][faraday]) to take care of this.
90
+
91
+ ##### Setting Custom Headers
92
+
93
+ You can customize request headers with the **:connection** option key.
94
+ This allows you to e.g. set custom headers (such as `Authorization`) that may be required by your service.
95
+
96
+ ```ruby
97
+ service = OData4::Service.new('http://services.odata.org/V4/OData/OData.svc', {
98
+ name: 'ODataDemo',
99
+ connection: {
100
+ headers: {
101
+ "Authorization" => "Bearer #{access_token}"
102
+ }
103
+ }
104
+ })
105
+ ```
106
+
107
+ ##### Using Authentication Helpers
108
+
109
+ You may also set up authorization by directly accessing the underlying `Faraday::Connection` object (as explained in [Advanced Customization](#advanced-connection-customization) below).
110
+ This allows you to make use of Faraday's [authentication helpers][faraday-auth], such as `basic_auth` or `token_auth`.
111
+
112
+ For instance, if your service requires HTTP basic authentication:
113
+
114
+ ```ruby
115
+ service = OData4::Service.new('http://services.odata.org/V4/OData/OData.svc', {
116
+ name: 'ODataDemo'
117
+ })
118
+ service.connection.basic_auth('username', 'password')
119
+ ```
120
+
121
+ You may also use these helpers when passing a block to the constructor (see second example [below](#passing-a-block-to-the-constructor)).
122
+
123
+ [http-auth]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication
124
+ [faraday]: https://github.com/lostisland/faraday
125
+ [faraday-auth]: https://github.com/lostisland/faraday#authentication
126
+
127
+ #### Advanced Connection Customization
128
+
129
+ Under the hood, the gem uses the [Faraday][faraday] HTTP library to provide flexible
130
+ integration of various Ruby HTTP backends.
131
+
132
+ There are several ways to access the underlying `Faraday::Connection`:
133
+
134
+ ##### As a service option
135
+
136
+ If you already have a `Faraday::Connection` instance that you want the service to use, you can simply pass it to the constructor *instead* of the service URL as first parameter.
137
+ In this case, you'll be setting the service URL on the connection object, as shown below:
138
+
139
+ ```ruby
140
+ conn = Faraday.new('http://services.odata.org/V4/OData/OData.svc') do |conn|
141
+ # ... customize connection ...
142
+ end
143
+
144
+ service = OData4::Service.new(conn, name: 'ODataDemo')
145
+ ```
146
+
147
+ ##### Passing a block to the constructor
148
+
149
+ Alternatively, the connection object is also `yield`ed by the constructor, so you may customize it by passing a block argument.
150
+ For instance, if you wanted to use [Typhoeus][typhoeus] as your HTTP library:
151
+
152
+ ```ruby
153
+ service = OData4::Service.new('http://services.odata.org/V4/OData/OData.svc', {
154
+ name: 'ODataDemo'
155
+ }) do |conn|
156
+ conn.adapter :typhoeus
157
+ end
158
+ ```
159
+
160
+ **IMPORTANT**
161
+
162
+ Please be aware that if you use this method to customize the connection, you must ALWAYS specify an adapter:
163
+
164
+ ```ruby
165
+ service = OData4::Service.new('http://services.odata.org/V4/OData/OData.svc', {
166
+ name: 'ODataDemo'
167
+ }) do |conn|
168
+ conn.basic_auth('username', 'password')
169
+ conn.adapter Faraday.default_adapter
170
+ end
171
+ ```
172
+
173
+ Otherwise, your requests WILL fail!
174
+
175
+ [typhoeus]: https://github.com/typhoeus/typhoeus
176
+
69
177
  ### Exploring a Service
70
178
 
71
179
  Once instantiated, you can request various information about the service, such as the names and types of entity sets it exposes, or the names of the entity types (and custom datatypes) it defines.
@@ -121,56 +229,6 @@ Get a list of enum types
121
229
 
122
230
  For more examples, refer to [usage_example_specs.rb](spec/odata4/usage_example_specs.rb).
123
231
 
124
- ### Authentication
125
-
126
- When authenticating with your service you can set parameters to the Typhoeus gem which uses libcurl.
127
- Use the **:typhoeus** option to set your authentication.
128
-
129
- For example using **ntlm** authentication:
130
-
131
- ```ruby
132
- conn = OData4::Service.open('http://services.odata.org/V4/OData/OData.svc', {
133
- name: 'ODataDemo',
134
- typhoeus: {
135
- username: 'username',
136
- password: 'password',
137
- httpauth: :ntlm
138
- }
139
- })
140
- ```
141
-
142
- For more authentication options see [libcurl][libcurl] or [typhoeus][typhoeus].
143
-
144
- [libcurl]: http://curl.haxx.se/libcurl/c/CURLOPT_HTTPAUTH.html
145
- [typhoeus]: https://github.com/typhoeus/typhoeus
146
-
147
- ### Metadata File
148
-
149
- Typically the metadata file of a service can be quite large.
150
- You can speed your load time by forcing the service to load the metadata from a file rather than a URL.
151
- This is only recommended for testing purposes, as the metadata file can change.
152
-
153
- ```ruby
154
- conn = OData4::Service.open('http://services.odata.org/V4/OData/OData.svc', {
155
- name: 'ODataDemo',
156
- metadata_file: "metadata.xml",
157
- })
158
- ```
159
-
160
- ### Headers
161
-
162
- You can set the headers with the **:typhoeus** param like so:
163
-
164
- ```ruby
165
- conn = OData4::Service.open('http://services.odata.org/V4/OData/OData.svc', {
166
- name: 'ODataDemo',
167
- typhoeus: {
168
- headers: {
169
- "Authorization" => "Bearer #{token}"
170
- }
171
- }
172
- })
173
- ```
174
232
 
175
233
  ### Entity Sets
176
234
 
@@ -179,7 +237,7 @@ Under normal circumstances you should never need to worry about an `OData4::Enti
179
237
  For example, to get an `OData4::EntitySet` for the products in the ODataDemo service simply access the entity set through the service like this:
180
238
 
181
239
  ```ruby
182
- service = OData4::Service.open('http://services.odata.org/V4/OData/OData.svc')
240
+ service = OData4::Service.new('http://services.odata.org/V4/OData/OData.svc')
183
241
  products = service['ProductsSet'] # => OData4::EntitySet
184
242
  ```
185
243
 
@@ -305,7 +363,7 @@ Simply add `strict: false` to the service constructor options.
305
363
  In this mode, any property validation error will log a warning instead of raising an exception. The corresponding property value will be `nil` (even if the property is declared as not allowing NULL values).
306
364
 
307
365
  ```ruby
308
- service = OData4::Service.open('http://services.odata.org/V4/OData/OData.svc', strict: false)
366
+ service = OData4::Service.new('http://services.odata.org/V4/OData/OData.svc', strict: false)
309
367
  # -- alternatively, for an existing service instance --
310
368
  service.options[:strict] = false
311
369
  ```
data/lib/odata4.rb CHANGED
@@ -3,7 +3,8 @@ require 'date'
3
3
  require 'time'
4
4
  require 'bigdecimal'
5
5
  require 'nokogiri'
6
- require 'typhoeus'
6
+ require 'faraday'
7
+ require 'logger'
7
8
  require 'andand'
8
9
  require 'json'
9
10
 
@@ -5,12 +5,14 @@ module OData4
5
5
  # Encapsulates the basic details and functionality needed to interact with an
6
6
  # OData4 service.
7
7
  class Service
8
+ # The Faraday connection object used by the service to make requests
9
+ attr_reader :connection
8
10
  # The OData4 Service's URL
9
11
  attr_reader :service_url
10
- # Options to pass around
12
+ # Service options
11
13
  attr_reader :options
12
14
 
13
- HTTP_TIMEOUT = 20
15
+ DEFAULT_TIMEOUT = 20
14
16
 
15
17
  METADATA_TIMEOUTS = [20, 60]
16
18
 
@@ -24,24 +26,32 @@ module OData4
24
26
  # Opens the service based on the requested URL and adds the service to
25
27
  # {OData4::Registry}
26
28
  #
27
- # @param service_url [String] the URL to the desired OData4 service
29
+ # @param service_url [String|Faraday::Connection]
30
+ # The URL to the desired OData4 service, or a Faraday connection object
28
31
  # @param options [Hash] options to pass to the service
29
32
  # @return [OData4::Service] an instance of the service
30
- def initialize(service_url, options = {})
31
- @service_url = service_url
32
- @options = default_options.merge(options)
33
+ def initialize(service_url, options = {}, &block)
34
+ if service_url.is_a? Faraday::Connection
35
+ @connection = service_url
36
+ @service_url = connection.url_prefix
37
+ else
38
+ @service_url = service_url
39
+ @connection = Faraday.new(service_url, options[:connection], &block)
40
+ end
41
+ @options = default_options.merge(options)
33
42
  OData4::ServiceRegistry.add(self)
34
43
  register_custom_types
35
44
  end
36
45
 
37
46
  # Opens the service based on the requested URL and adds the service to
38
47
  # {OData4::Registry}
48
+ # @deprecated Use {Service.new} instead.
39
49
  #
40
50
  # @param service_url [String] the URL to the desired OData4 service
41
51
  # @param options [Hash] options to pass to the service
42
52
  # @return [OData4::Service] an instance of the service
43
- def self.open(service_url, options = {})
44
- Service.new(service_url, options)
53
+ def self.open(service_url, options = {}, &block)
54
+ Service.new(service_url, options, &block)
45
55
  end
46
56
 
47
57
  # Returns user supplied name for service, or its URL
@@ -176,14 +186,6 @@ module OData4
176
186
  schemas[namespace].properties_for_entity(entity_name)
177
187
  end
178
188
 
179
- # Returns the log level set via initial options, or the
180
- # default log level (`Logger::WARN`) if none was specified.
181
- # @see Logger
182
- # @return [Fixnum|Symbol]
183
- def log_level
184
- options[:log_level] || Logger::WARN
185
- end
186
-
187
189
  # Returns the logger instance used by the service.
188
190
  # When Ruby on Rails has been detected, the service will
189
191
  # use `Rails.logger`. The log level will NOT be changed.
@@ -191,18 +193,13 @@ module OData4
191
193
  # When no Rails has been detected, a default logger will
192
194
  # be used that logs to STDOUT with the log level supplied
193
195
  # via options, or the default log level if none was given.
194
- # @see #log_level
195
196
  # @return [Logger]
196
197
  def logger
197
- @logger ||= lambda do
198
- if defined?(Rails)
199
- Rails.logger
200
- else
201
- logger = Logger.new(STDOUT)
202
- logger.level = log_level
203
- logger
204
- end
205
- end.call
198
+ @logger ||= options[:logger] || if defined?(Rails)
199
+ Rails.logger
200
+ else
201
+ default_logger
202
+ end
206
203
  end
207
204
 
208
205
  # Allows the logger to be set to a custom `Logger` instance.
@@ -215,14 +212,19 @@ module OData4
215
212
 
216
213
  def default_options
217
214
  {
218
- typhoeus: {
219
- headers: { 'OData-Version' => '4.0' },
220
- timeout: HTTP_TIMEOUT
215
+ request: {
216
+ timeout: DEFAULT_TIMEOUT
221
217
  },
222
218
  strict: true # strict property validation
223
219
  }
224
220
  end
225
221
 
222
+ def default_logger
223
+ Logger.new(STDOUT).tap do |logger|
224
+ logger.level = options[:log_level] || Logger::WARN
225
+ end
226
+ end
227
+
226
228
  def read_metadata
227
229
  # From file, good for debugging
228
230
  if options[:metadata_file]
@@ -18,9 +18,10 @@ module OData4
18
18
  def initialize(service, url_chunk, options = {})
19
19
  @service = service
20
20
  @url_chunk = url_chunk
21
- @method = options[:method] || :get
22
- @format = options[:format] || :auto
23
- @query = options[:query]
21
+ @method = options.delete(:method) || :get
22
+ @format = options.delete(:format) || :auto
23
+ @query = options.delete(:query)
24
+ @options = options
24
25
  end
25
26
 
26
27
  # Return the full request URL (including service base)
@@ -43,38 +44,41 @@ module OData4
43
44
 
44
45
  # Execute the request
45
46
  #
46
- # @param additional_options [Hash] options to pass to Typhoeus
47
+ # @param additional_options [Hash] Request options to pass to Faraday
47
48
  # @return [OData4::Service::Response]
48
49
  def execute(additional_options = {})
49
- options = request_options(additional_options)
50
- request = ::Typhoeus::Request.new(url, options)
51
- logger.info "Requesting #{method.to_s.upcase} #{url}..."
52
- request.run
50
+ request_options = service.options[:request].merge(additional_options)
53
51
 
54
- Response.new(service, request.response, query)
52
+ logger.info "Requesting #{method.to_s.upcase} #{url}..."
53
+ Response.new(service, query) do
54
+ connection.run_request(method, url_chunk, nil, headers) do |conn|
55
+ conn.options.merge! request_options
56
+ end
57
+ end
55
58
  end
56
59
 
57
60
  private
58
61
 
59
62
  attr_reader :url_chunk
60
63
 
61
- def logger
62
- service.logger
64
+ def connection
65
+ service.connection
63
66
  end
64
67
 
65
- def request_options(additional_options = {})
66
- options = service.options[:typhoeus]
67
- .merge({ method: method })
68
- .merge(additional_options)
68
+ def default_headers
69
+ {
70
+ 'Accept' => content_type,
71
+ 'Content-Type' => content_type,
72
+ 'OData-Version' => '4.0'
73
+ }
74
+ end
69
75
 
70
- # Don't overwrite Accept header if already present
71
- unless options[:headers]['Accept']
72
- options[:headers] = options[:headers].merge({
73
- 'Accept' => content_type
74
- })
75
- end
76
+ def headers
77
+ default_headers.merge(@options[:headers] || {})
78
+ end
76
79
 
77
- options
80
+ def logger
81
+ service.logger
78
82
  end
79
83
  end
80
84
  end
@@ -17,19 +17,19 @@ module OData4
17
17
  attr_reader :query
18
18
 
19
19
  # Create a new response given a service and a raw response.
20
- # @param service [OData4::Service]
21
- # @param response [Typhoeus::Result]
22
- def initialize(service, response, query = nil)
20
+ # @param service [OData4::Service] The executing service.
21
+ # @param query [OData4::Query] The related query (optional).
22
+ # @param
23
+ def initialize(service, query = nil, &block)
23
24
  @service = service
24
- @response = response
25
25
  @query = query
26
- check_content_type
27
- validate!
26
+ @timed_out = false
27
+ execute(&block)
28
28
  end
29
29
 
30
30
  # Returns the HTTP status code.
31
31
  def status
32
- response.code
32
+ response.status
33
33
  end
34
34
 
35
35
  # Whether the request was successful.
@@ -66,7 +66,7 @@ module OData4
66
66
 
67
67
  # Whether the response failed due to a timeout
68
68
  def timed_out?
69
- response.timed_out?
69
+ @timed_out
70
70
  end
71
71
 
72
72
  # Iterates over all entities in the response, using
@@ -95,30 +95,39 @@ module OData4
95
95
  #
96
96
  # @return [self]
97
97
  def validate!
98
- raise "Bad Request. #{error_message(response)}" if response.code == 400
99
- raise "Access Denied" if response.code == 401
100
- raise "Forbidden" if response.code == 403
101
- raise "Not Found" if [0,404].include?(response.code)
102
- raise "Method Not Allowed" if response.code == 405
103
- raise "Not Acceptable" if response.code == 406
104
- raise "Request Entity Too Large" if response.code == 413
105
- raise "Internal Server Error" if response.code == 500
106
- raise "Service Unavailable" if response.code == 503
98
+ raise "Bad Request. #{error_message(response)}" if status == 400
99
+ raise "Access Denied" if status == 401
100
+ raise "Forbidden" if status == 403
101
+ raise "Not Found" if [0,404].include?(status)
102
+ raise "Method Not Allowed" if status == 405
103
+ raise "Not Acceptable" if status == 406
104
+ raise "Request Entity Too Large" if status == 413
105
+ raise "Internal Server Error" if status == 500
106
+ raise "Service Unavailable" if status == 503
107
107
  self
108
108
  end
109
109
 
110
110
  private
111
111
 
112
- def logger
113
- service.logger
114
- end
115
-
116
- def check_content_type
112
+ def execute(&block)
113
+ @response = block.call
117
114
  logger.debug <<-EOS
118
115
  [OData4: #{service.name}] Received response:
119
116
  Headers: #{response.headers}
120
117
  Body: #{response.body}
121
118
  EOS
119
+ check_content_type
120
+ validate!
121
+ rescue Faraday::TimeoutError
122
+ logger.info "Request timed out."
123
+ @timed_out = true
124
+ end
125
+
126
+ def logger
127
+ service.logger
128
+ end
129
+
130
+ def check_content_type
122
131
  # Dynamically extend instance with methods for
123
132
  # processing the current result type
124
133
  if is_atom?