diff --git a/README.md b/README.md index 5ef4a44..570e670 100644 --- a/README.md +++ b/README.md @@ -241,6 +241,29 @@ Basic HTTP Authentication is supported via sending a username and password as se svc = OData::Service.new "http://127.0.0.1:8989/SampleService/RubyOData.svc", { :username => "bob", :password=> "12345" } +NTLM authentication is also possible. Faraday lacks documentation how to use NTLM, even though multiple backends support it. Therefore, it is unclear what is the best way to achieve NTLM authentication, but a possibility is shown below. + + require 'ruby_odata' + require 'httpclient' + + class ConfigurableHTTPClient < Faraday::Adapter::HTTPClient + def initialize(*, &block) + @block = block + super + end + + def call(env) + @block.call self if @block + super + end + end + Faraday::Adapter.register_middleware(configurable_httpclient: ConfigurableHTTPClient) + + url = "http://127.0.0.1:8989/SampleService/RubyOData.svc" + svc = OData::Service.new url do |faraday| + faraday.adapter(:configurable_httpclient) { |a| a.client.set_auth url, "bob", "12345" } + end + ### SSL/https Certificate Verification The certificate verification mode can be passed in the options hash via the :verify_ssl key. For example, to ignore verification in order to use a self-signed certificate: diff --git a/lib/ruby_odata.rb b/lib/ruby_odata.rb index 0d60a48..2a8b6e1 100644 --- a/lib/ruby_odata.rb +++ b/lib/ruby_odata.rb @@ -7,7 +7,6 @@ require "active_support/inflector" require "active_support/core_ext" require "cgi" -require "excon" require "faraday_middleware" require "faraday" require "nokogiri" diff --git a/lib/ruby_odata/resource.rb b/lib/ruby_odata/resource.rb index 4a0896b..8ef6ce4 100644 --- a/lib/ruby_odata/resource.rb +++ b/lib/ruby_odata/resource.rb @@ -1,46 +1,39 @@ module OData class Resource - attr_reader :url, :options, :block - - def initialize(url, options={}, backwards_compatibility=nil, &block) - @url = url - @block = block - @options = options.is_a?(Hash) ? options : { user: options, password: backwards_compatibility } - - @conn = Faraday.new(url: url, ssl: { verify: verify_ssl }) do |faraday| + def initialize(url, options={}) + @conn = Faraday.new(url: url, ssl: { verify: options[:verify_ssl] }) do |faraday| faraday.use :gzip faraday.response :raise_error - faraday.adapter :excon - faraday.options.timeout = timeout if timeout - faraday.options.open_timeout = open_timeout if open_timeout + faraday.options.timeout = options[:timeout] if options[:timeout] + faraday.options.open_timeout = options[:open_timeout] if options[:open_timeout] - faraday.headers = (faraday.headers || {}).merge(@options[:headers] || {}) + faraday.headers = (faraday.headers || {}).merge(options[:headers] || {}) faraday.headers = (faraday.headers).merge({ :accept => '*/*; q=0.5, application/xml', }) - faraday.basic_auth user, password if user# this adds to headers so must be behind - end + faraday.basic_auth options[:user], options[:password] if options[:user] # this adds to headers so must be behind - @conn.headers[:user_agent] = 'Ruby' + yield faraday if block_given? + end end - def get(additional_headers={}) + def get(url, additional_headers={}) @conn.get do |req| req.url url req.headers = (headers || {}).merge(additional_headers) end end - def head(additional_headers={}) + def head(url, additional_headers={}) @conn.head do |req| req.url url req.headers = (headers || {}).merge(additional_headers) end end - def post(payload, additional_headers={}) + def post(url, payload, additional_headers={}) @conn.post do |req| req.url url req.headers = (headers || {}).merge(additional_headers) @@ -48,7 +41,7 @@ def post(payload, additional_headers={}) end end - def put(payload, additional_headers={}) + def put(url, payload, additional_headers={}) @conn.put do |req| req.url url req.headers = (headers || {}).merge(additional_headers) @@ -56,7 +49,7 @@ def put(payload, additional_headers={}) end end - def patch(payload, additional_headers={}) + def patch(url, payload, additional_headers={}) @conn.patch do |req| req.url url req.headers = (headers || {}).merge(additional_headers) @@ -64,86 +57,17 @@ def patch(payload, additional_headers={}) end end - def delete(additional_headers={}) + def delete(url, additional_headers={}) @conn.delete do |req| req.url url req.headers = (headers || {}).merge(additional_headers) end end - def to_s - url - end - - def user - options[:user] - end - - def password - options[:password] - end - - def verify_ssl - options[:verify_ssl] - end - def headers @conn.headers || {} end - def timeout - options[:timeout] - end - - def open_timeout - options[:open_timeout] - end - - # Construct a subresource, preserving authentication. - # - # Example: - # - # site = RestClient::Resource.new('http://example.com', 'adam', 'mypasswd') - # site['posts/1/comments'].post 'Good article.', :content_type => 'text/plain' - # - # This is especially useful if you wish to define your site in one place and - # call it in multiple locations: - # - # def orders - # RestClient::Resource.new('http://example.com/orders', 'admin', 'mypasswd') - # end - # - # orders.get # GET http://example.com/orders - # orders['1'].get # GET http://example.com/orders/1 - # orders['1/items'].delete # DELETE http://example.com/orders/1/items - # - # Nest resources as far as you want: - # - # site = RestClient::Resource.new('http://example.com') - # posts = site['posts'] - # first_post = posts['1'] - # comments = first_post['comments'] - # comments.post 'Hello', :content_type => 'text/plain' - # - def [](suburl, &new_block) - case - when block_given? then self.class.new(concat_urls(url, suburl), options, &new_block) - when block then self.class.new(concat_urls(url, suburl), options, &block) - else - self.class.new(concat_urls(url, suburl), options) - end - end - - def concat_urls(url, suburl) # :nodoc: - url = url.to_s - suburl = suburl.to_s - if url.slice(-1, 1) == '/' or suburl.slice(0, 1) == '/' - url + suburl - else - "#{url}/#{suburl}" - end - end - def prepare_payload payload JSON.generate(payload) rescue JSON::GeneratorError diff --git a/lib/ruby_odata/service.rb b/lib/ruby_odata/service.rb index b2f7c45..d15724c 100644 --- a/lib/ruby_odata/service.rb +++ b/lib/ruby_odata/service.rb @@ -12,10 +12,10 @@ class Service # @option options [Hash] :rest_options a hash of rest-client options that will be passed to all OData::Resource.new calls # @option options [Hash] :additional_params a hash of query string params that will be passed on all calls # @option options [Boolean, true] :eager_partial true if queries should consume partial feeds until the feed is complete, false if explicit calls to next must be performed - def initialize(service_uri, options = {}) + def initialize(service_uri, options = {}, &block) @uri = service_uri.gsub!(/\/?$/, '') set_options! options - default_instance_vars! + default_instance_vars!(service_uri, &block) set_namespaces build_collections_and_classes end @@ -97,7 +97,7 @@ def save_changes # @raise [ServiceError] if there is an error when talking to the service def execute begin - @response = OData::Resource.new(build_query_uri, @rest_options).get + @response = @resource.get(build_query_uri) rescue Exception => e handle_exception(e) end @@ -147,7 +147,7 @@ def load_property(obj, nav_prop) raise NotSupportedError, "You cannot load a property on an entity that isn't tracked" if obj.send(:__metadata).nil? raise ArgumentError, "'#{nav_prop}' is not a valid navigation property" unless obj.respond_to?(nav_prop.to_sym) raise ArgumentError, "'#{nav_prop}' is not a valid navigation property" unless @class_metadata[obj.class.to_s][nav_prop].nav_prop - results = OData::Resource.new(build_load_property_uri(obj, nav_prop), @rest_options).get + results = @resource.get build_load_property_uri(obj, nav_prop) prop_results = build_classes_from_result(results.body) obj.send "#{nav_prop}=", (singular?(nav_prop) ? prop_results.first : prop_results) end @@ -227,16 +227,17 @@ def set_options!(options) @json_type = options[:json_type] || 'application/json' end - def default_instance_vars! + def default_instance_vars!(service_uri, &block) @collections = {} @function_imports = {} @save_operations = [] @has_partial = false @next_uri = nil + @resource = OData::Resource.new(service_uri, @rest_options, &block) end def set_namespaces - @edmx = Nokogiri::XML(OData::Resource.new(build_metadata_uri, @rest_options).get.body) + @edmx = Nokogiri::XML(@resource.get(build_metadata_uri).body) @ds_namespaces = { "m" => "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata", "edmx" => "http://schemas.microsoft.com/ado/2007/06/edmx", @@ -509,7 +510,7 @@ def extract_partial(doc) def handle_partial if @next_uri - result = OData::Resource.new(@next_uri, @rest_options).get + result = @resource.get(@next_uri) results = handle_collection_result(result.body) end results @@ -600,21 +601,21 @@ def single_save(operation) if operation.kind == "Add" save_uri = build_save_uri(operation) json_klass = operation.klass.to_json(:type => :add) - post_result = OData::Resource.new(save_uri, @rest_options).post json_klass, {:content_type => @json_type} + post_result = @resource.post save_uri, json_klass, {:content_type => @json_type} return build_classes_from_result(post_result.body) elsif operation.kind == "Update" update_uri = build_resource_uri(operation) json_klass = operation.klass.to_json - update_result = OData::Resource.new(update_uri, @rest_options).put json_klass, {:content_type => @json_type} + update_result = @resource.put update_uri, json_klass, {:content_type => @json_type} return (update_result.status == 204) elsif operation.kind == "Delete" delete_uri = build_resource_uri(operation) - delete_result = OData::Resource.new(delete_uri, @rest_options).delete + delete_result = @resource.delete delete_uri return (delete_result.status == 204) elsif operation.kind == "AddLink" save_uri = build_add_link_uri(operation) json_klass = operation.child_klass.to_json(:type => :link) - post_result = OData::Resource.new(save_uri, @rest_options).post json_klass, {:content_type => @json_type} + post_result = @resource.post save_uri, json_klass, {:content_type => @json_type} # Attach the child to the parent link_child_to_parent(operation) if (post_result.status == 204) @@ -633,7 +634,7 @@ def batch_save(operations) batch_uri = build_batch_uri body = build_batch_body(operations, batch_num, changeset_num) - result = OData::Resource.new( batch_uri, @rest_options).post body, {:content_type => "multipart/mixed; boundary=batch_#{batch_num}"} + result = @resource.post batch_uri, body, {:content_type => "multipart/mixed; boundary=batch_#{batch_num}"} # TODO: More result validation needs to be done. # The result returns HTTP 202 even if there is an error in the batch @@ -827,7 +828,7 @@ def execute_import_function(name, *args) func[:parameters].keys.each_with_index { |key, i| params[key] = args[0][i] } unless func[:parameters].nil? function_uri = build_function_import_uri(name, params) - result = OData::Resource.new(function_uri, @rest_options).send(func[:http_method].downcase, {}) + result = @resource.send func[:http_method].downcase, function_uri, {} # Is this a 204 (No content) result? return true if result.status == 204 diff --git a/ruby_odata.gemspec b/ruby_odata.gemspec index 37e31a6..1ab4b29 100644 --- a/ruby_odata.gemspec +++ b/ruby_odata.gemspec @@ -20,7 +20,6 @@ Gem::Specification.new do |s| s.add_dependency("addressable", ">= 2.3.4") s.add_dependency("i18n", ">= 0.7.0") s.add_dependency("activesupport", ">= 3.0.0") - s.add_dependency("excon", "~> 0.45.3") s.add_dependency("faraday_middleware") s.add_dependency("faraday", "~> 0.9.1") s.add_dependency("nokogiri", ">= 1.4.2")