diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 7b1d23ba..f5654dae 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2016-03-28 20:14:45 +0900 using RuboCop version 0.39.0. +# on 2016-11-21 15:47:20 -0800 using RuboCop version 0.39.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -31,22 +31,26 @@ Lint/UnusedMethodArgument: - 'lib/neo4j/relationship.rb' - 'lib/neo4j/session.rb' -# Offense count: 16 +# Offense count: 24 Metrics/AbcSize: Max: 17 -# Offense count: 9 +# Offense count: 12 # Configuration parameters: CountComments. Metrics/ClassLength: Max: 191 -# Offense count: 508 +# Offense count: 1 +Metrics/CyclomaticComplexity: + Max: 8 + +# Offense count: 657 # Configuration parameters: AllowHeredoc, AllowURI, URISchemes. # URISchemes: http, https Metrics/LineLength: Max: 180 -# Offense count: 20 +# Offense count: 27 # Configuration parameters: CountComments. Metrics/MethodLength: Max: 14 @@ -56,6 +60,10 @@ Metrics/MethodLength: Metrics/ModuleLength: Max: 113 +# Offense count: 1 +Metrics/PerceivedComplexity: + Max: 8 + # Offense count: 8 Style/AccessorMethodName: Exclude: @@ -72,7 +80,7 @@ Style/ClassVars: Exclude: - 'lib/neo4j/session.rb' -# Offense count: 72 +# Offense count: 84 Style/Documentation: Enabled: false @@ -82,7 +90,7 @@ Style/GuardClause: Exclude: - 'lib/neo4j-core/query_clauses.rb' -# Offense count: 56 +# Offense count: 65 # Cop supports --auto-correct. Style/MutableConstant: Enabled: false @@ -92,10 +100,3 @@ Style/MutableConstant: Style/RedundantSelf: Exclude: - 'lib/neo4j-core/query_clauses.rb' - -# Offense count: 1 -# Cop supports --auto-correct. -Style/RescueEnsureAlignment: - Exclude: - - 'spec/shared_examples/node_with_tx.rb' - diff --git a/.travis.yml b/.travis.yml index d71d7cf2..1c6fb57f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ before_script: - "echo 'dbms.memory.heap.max_size=1000' >> ./db/neo4j/development/conf/neo4j-wrapper.conf" - "echo 'dbms.memory.heap.initial_size=1000' >> ./db/neo4j/development/conf/neo4j-wrapper.conf" - "bin/rake neo4j:start --trace" - - "sleep 10" + - "sleep 11" script: - "bundle exec rspec $RSPEC_OPTS" language: ruby diff --git a/Gemfile b/Gemfile index cb5bc98f..1cd08ee4 100644 --- a/Gemfile +++ b/Gemfile @@ -23,5 +23,11 @@ group 'test' do gem 'rspec', '~> 3.0' gem 'rspec-its' gem 'dotenv' - gem 'activesupport', '~> 4.0' + gem 'activesupport', RUBY_VERSION.to_f >= 2.2 ? '>= 4.0' : '~> 4' + + gem 'em-http-request', '>= 1.1', require: 'em-http', platforms: :ruby + gem 'em-synchrony', '>= 1.0.3', require: ['em-synchrony', 'em-synchrony/em-http'], platforms: :ruby + gem 'excon', '>= 0.27.4' + gem 'patron', '>= 0.4.2', platforms: :ruby + gem 'typhoeus', '>= 0.3.3' end diff --git a/README.md b/README.md index ac54d9c9..fa070f64 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,36 @@ To make a basic connection to Neo4j to execute Cypher queries, first choose an a neo4j_adaptor = Neo4j::Core::CypherSession::Adaptors::Embedded.new('/file/path/to/graph.db') +The `http_adaptor` can also take `:faraday_options`. Multiple middlewares with the same key can be passed through using arrays: + + http_adaptor = Neo4j::Core::CypherSession::Adaptors::HTTP.new('http://neo4j:pass@localhost:7474', + faraday_options: { + adapter: :typhoeus, + # This will pass 2 items to Faraday. You must use multidimensional arrays to pass single args separately + request: [ + [:multipart], + [:url_encoded] + ], + # This will only pass a single item + response: [:multi_json, symbolize_keys: true, content_type: 'application/json'] + }) + +This will initialize Faraday like so: + + Faraday.new(url: 'http://neo4j:pass@localhost:7474') do |faraday| + faraday.request :multipart + faraday.request :url_encoded + + faraday.response :multi_json, symbolize_keys: true, content_type: 'application/json' + faraday.adapter: :typhoeus + end + +The order that the keys are passed through to Faraday will remain hash ordered *except* that `adapter` is always last. Arrays within each key are passed in order. + +Note you **must** install any required http adaptor gems yourself as per [Faraday](https://github.com/lostisland/faraday). Ex for `:typhoeus`, add to your Gemfile: + + gem 'typhoeus' + Once you have an adaptor you can create a session like so: neo4j_session = Neo4j::Core::CypherSession.new(http_adaptor) diff --git a/lib/neo4j-server/cypher_session.rb b/lib/neo4j-server/cypher_session.rb index d49aee5d..08d3b854 100644 --- a/lib/neo4j-server/cypher_session.rb +++ b/lib/neo4j-server/cypher_session.rb @@ -1,4 +1,5 @@ require 'uri' +require 'neo4j/core/cypher_session/adaptors/faraday_helpers' module Neo4j module Server @@ -7,7 +8,23 @@ module Server end class CypherSession < Neo4j::Session + module PrivateMethods + private + + def extract_faraday_options(params, defaults = {}) + verify_faraday_options(params.delete(:faraday_options) || params.delete('faraday_options') || {}, defaults) + end + + def extract_basic_auth(url, params) + uri = URI(url) if url + return unless url && uri.userinfo + params[:basic_auth] = {username: uri.user, password: uri.password} + end + end + include Resource + extend Neo4j::Core::CypherSession::Adaptors::FaradayHelpers + extend Neo4j::Server::CypherSession::PrivateMethods alias super_query query attr_reader :connection @@ -24,15 +41,16 @@ def initialize(data_url, connection) # @see https://github.com/lostisland/faraday def self.create_connection(params, url = nil) init_params = params[:initialize] && params.delete(:initialize) + + request = [:multi_json] + request.unshift([:basic_auth, params[:basic_auth][:username], params[:basic_auth][:password]]) if params[:basic_auth] + + faraday_options = extract_faraday_options( + params, request: request, response: [:multi_json, symbolize_keys: true, content_type: 'application/json']) conn = Faraday.new(url, init_params) do |b| - b.request :basic_auth, params[:basic_auth][:username], params[:basic_auth][:password] if params[:basic_auth] - b.request :multi_json # b.response :logger, ::Logger.new(STDOUT), bodies: true - - b.response :multi_json, symbolize_keys: true, content_type: 'application/json' # b.use Faraday::Response::RaiseError - b.use Faraday::Adapter::NetHttpPersistent - # b.adapter Faraday.default_adapter + set_faraday_middleware b, faraday_options end conn.headers = {'Content-Type' => 'application/json', 'User-Agent' => ::Neo4j::Session.user_agent_string} conn @@ -58,13 +76,6 @@ def self.establish_session(root_data, connection) CypherSession.new(data_url, connection) end - def self.extract_basic_auth(url, params) - return unless url && URI(url).userinfo - params[:basic_auth] = {username: URI(url).user, password: URI(url).password} - end - - private_class_method :extract_basic_auth - def db_type :server_db end diff --git a/lib/neo4j/core/cypher_session/adaptors/faraday_helpers.rb b/lib/neo4j/core/cypher_session/adaptors/faraday_helpers.rb new file mode 100644 index 00000000..66f1d655 --- /dev/null +++ b/lib/neo4j/core/cypher_session/adaptors/faraday_helpers.rb @@ -0,0 +1,49 @@ +module Neo4j + module Core + class CypherSession + module Adaptors + module FaradayHelpers + private + + def verify_faraday_options(faraday_options = {}, defaults = {}) + faraday_options.symbolize_keys!.reverse_merge!(defaults) + faraday_options[:adapter] ||= :net_http_persistent + require 'typhoeus/adapters/faraday' if faraday_options[:adapter].to_sym == :typhoeus + faraday_options + end + + def set_faraday_middleware(faraday, options = {adapter: :net_http_persistent}) + adapter = options.delete(:adapter) + send_all_faraday_options(faraday, options) + faraday.adapter adapter + end + + def send_all_faraday_options(faraday, options) + options.each do |key, value| + next unless faraday.respond_to? key + if value.is_a? Array + if value.none? { |arg| arg.is_a? Array } + faraday.send key, *value + else + value.each do |args| + arg_safe_send faraday, key, args + end + end + else + faraday.send key, value + end + end + end + + def arg_safe_send(object, msg, args) + if args.is_a? Array + object.send(msg, *args) + else + object.send(msg, args) + end + end + end + end + end + end +end diff --git a/lib/neo4j/core/cypher_session/adaptors/http.rb b/lib/neo4j/core/cypher_session/adaptors/http.rb index c1019a3e..f4070884 100644 --- a/lib/neo4j/core/cypher_session/adaptors/http.rb +++ b/lib/neo4j/core/cypher_session/adaptors/http.rb @@ -1,5 +1,6 @@ require 'neo4j/core/cypher_session/adaptors' require 'neo4j/core/cypher_session/adaptors/has_uri' +require 'neo4j/core/cypher_session/adaptors/faraday_helpers' require 'neo4j/core/cypher_session/responses/http' # TODO: Work with `Query` objects @@ -16,7 +17,8 @@ def initialize(url, options = {}) end def connect - @requestor = Requestor.new(@url, USER_AGENT_STRING, self.class.method(:instrument_request)) + @requestor = Requestor.new(@url, USER_AGENT_STRING, self.class.method(:instrument_request), + @options[:faraday_options] || @options['faraday_options'] || {}) rescue Faraday::ConnectionFailed => e raise CypherSession::ConnectionFailedError, "#{e.class}: #{e.message}" end @@ -91,15 +93,16 @@ def connected? # - Sets headers, including user agent string class Requestor include Adaptors::HasUri + include FaradayHelpers default_url('http://neo4:neo4j@localhost:7474') validate_uri { |uri| uri.is_a?(URI::HTTP) } - def initialize(url, user_agent_string, instrument_proc) + def initialize(url, user_agent_string, instrument_proc, faraday_options) self.url = url @user = user @password = password @user_agent_string = user_agent_string - @faraday = faraday_connection + @faraday = faraday_connection(faraday_options) @instrument_proc = instrument_proc end @@ -134,22 +137,23 @@ def get(path, body = '', options = {}) private - def faraday_connection + def faraday_connection(faraday_options = {}) + verify_faraday_options(faraday_options, + request: [ + [:basic_auth, user, password], + :multi_json + ], + response: [:multi_json, symbolize_keys: true, content_type: 'application/json'] + ) require 'faraday' require 'faraday_middleware/multi_json' - Faraday.new(url) do |c| - c.request :basic_auth, user, password - c.request :multi_json - - c.response :multi_json, symbolize_keys: true, content_type: 'application/json' - c.use Faraday::Adapter::NetHttpPersistent - + conn = Faraday.new(url) do |c| # c.response :logger, ::Logger.new(STDOUT), bodies: true - - c.headers['Content-Type'] = 'application/json' - c.headers['User-Agent'] = @user_agent_string + set_faraday_middleware c, faraday_options end + conn.headers = {'Content-Type' => 'application/json', 'User-Agent' => @user_agent_string} + conn end def request_body(body) diff --git a/spec/neo4j-server/cypher_session/shared_examples/adaptor.rb b/spec/neo4j-server/cypher_session/shared_examples/adaptor.rb new file mode 100644 index 00000000..598071c3 --- /dev/null +++ b/spec/neo4j-server/cypher_session/shared_examples/adaptor.rb @@ -0,0 +1,29 @@ +RSpec.shared_examples 'Neo4j::Server::CypherSession::Adaptor' do + describe 'faraday_options' do + describe 'a faraday connection type adapter option' do + it 'can use a user supplied faraday connection for a new session' do + connection = Faraday.new do |faraday| + faraday.request :basic_auth, basic_auth_hash[:username], basic_auth_hash[:password] + + faraday.request :multi_json + faraday.response :multi_json, symbolize_keys: true, content_type: 'application/json' + faraday.adapter Faraday.default_adapter + end + connection.headers = {'Content-Type' => 'application/json'} + + expect(connection).to receive(:get).at_least(:once).and_call_original + create_server_session(connection: connection) + end + + it 'will pass through a symbol key' do + expect_any_instance_of(Faraday::Connection).to receive(:adapter).with(:typhoeus).and_call_original + create_server_session(faraday_options: {adapter: :typhoeus}) + end + + it 'will pass through a string key' do + expect_any_instance_of(Faraday::Connection).to receive(:adapter).with(:typhoeus).and_call_original + create_server_session('faraday_options' => {'adapter' => :typhoeus}) + end + end + end +end diff --git a/spec/neo4j-server/e2e/cypher_session_spec.rb b/spec/neo4j-server/e2e/cypher_session_spec.rb index 163f5a47..3fe26327 100644 --- a/spec/neo4j-server/e2e/cypher_session_spec.rb +++ b/spec/neo4j-server/e2e/cypher_session_spec.rb @@ -1,4 +1,5 @@ require 'spec_helper' +require './spec/neo4j-server/cypher_session/shared_examples/adaptor' module Neo4j module Server @@ -18,27 +19,14 @@ def open_session Neo4j::Session.set_current(@before_session) end - it 'can use a user supplied faraday connection for a new session' do - connection = Faraday.new do |faraday| - faraday.request :basic_auth, basic_auth_hash[:username], basic_auth_hash[:password] - - faraday.request :multi_json - faraday.response :multi_json, symbolize_keys: true, content_type: 'application/json' - faraday.adapter Faraday.default_adapter - end - connection.headers = {'Content-Type' => 'application/json'} - - expect(connection).to receive(:get).at_least(:once).and_call_original - create_server_session(connection: connection) - end - it 'adds host and port to the connection object' do connection = Neo4j::Session.current.connection expect(connection.port).to eq ENV['NEO4J_URL'] ? URI(ENV['NEO4J_URL']).port : 7474 expect(connection.host).to eq 'localhost' end - end + it_behaves_like 'Neo4j::Server::CypherSession::Adaptor' + end describe 'named sessions' do before { Neo4j::Session.current && Neo4j::Session.current.close } diff --git a/spec/neo4j/core/cypher_session/adaptors/http_spec.rb b/spec/neo4j/core/cypher_session/adaptors/http_spec.rb index f3d61a03..27d0ddd7 100644 --- a/spec/neo4j/core/cypher_session/adaptors/http_spec.rb +++ b/spec/neo4j/core/cypher_session/adaptors/http_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' require 'neo4j/core/cypher_session/adaptors/http' require './spec/neo4j/core/shared_examples/adaptor' +require './spec/neo4j/core/shared_examples/http' describe Neo4j::Core::CypherSession::Adaptors::HTTP do before(:all) { setup_http_request_subscription } @@ -22,6 +23,34 @@ expect { adaptor_class.new('bolt://localhost:7474').connect }.to raise_error ArgumentError, /Invalid URL/ expect { adaptor_class.new('foo://localhost:7474').connect }.to raise_error ArgumentError, /Invalid URL/ end + + describe 'the faraday_options param' do + describe 'the adapter option' do + it 'uses net_http_persistent by default' do + expect_any_instance_of(Faraday::Connection).to receive(:adapter).with(:net_http_persistent) + adaptor_class.new(url).connect + end + + it 'will pass through a symbol key' do + expect_any_instance_of(Faraday::Connection).to receive(:adapter).with(:typhoeus) + adaptor_class.new(url, faraday_options: {adapter: :typhoeus}).connect + end + + it 'will pass through a string key' do + expect_any_instance_of(Faraday::Connection).to receive(:adapter).with(:typhoeus) + adaptor_class.new(url, 'faraday_options' => {'adapter' => :typhoeus}).connect + end + + adaptors = Faraday::Adapter.instance_variable_get(:@registered_middleware).keys - [:test, :rack] + adaptors -= [:patron, :em_synchrony, :em_http] if RUBY_PLATFORM == 'java' + adaptors.each do |adapter_name| + describe "the :#{adapter_name} adapter" do + let(:http_adapter) { adapter_name } + it_behaves_like 'Neo4j::Core::CypherSession::Adaptors::Http' + end + end + end + end end let(:session_double) { double('session', adaptor: subject) } diff --git a/spec/neo4j/core/shared_examples/http.rb b/spec/neo4j/core/shared_examples/http.rb new file mode 100644 index 00000000..9803a1fd --- /dev/null +++ b/spec/neo4j/core/shared_examples/http.rb @@ -0,0 +1,6 @@ +# Requires that an `http_adapter` let variable exist with the Faraday adaptor name +RSpec.shared_examples 'Neo4j::Core::CypherSession::Adaptors::Http' do + it 'should connect properly' do + subject.class.new(url, faraday_options: {adapter: http_adapter}).connect.get('/') + end +end