Comparing changes

v0.3.2 v1.0.0
14 commits 22 files changed

Commits

1f46a41 Prepare CHANGELOG for 1.0.0 release mo khan 2020-08-01 05:34:11
12af3b0 Enable new rubocop rules mo khan 2020-08-01 05:18:06
6d1a48b Add entry to CHANGELOG mo khan 2020-06-13 14:33:59
ecc326c Extract Http Connection class mo khan 2020-06-13 14:32:45
10a9b0b Update CHANGELOG and CI configuration mo khan 2020-06-13 00:12:49
d98902a Reduce # of instance variables mo khan 2020-06-13 00:00:48
5e327cc Create default_client to simplify usage mo khan 2020-06-12 23:06:14
35fc529 Remove legacy API mo khan 2020-06-12 22:28:11
9b92ff7 Clean up build scripts mo khan 2020-06-12 22:24:49
c109f8c Update vcr requirement from ~> 5.1 to ~> 6.0 dependabot-preview[bot] 2020-05-28 12:15:18
5ad3f61 Update vcr requirement from ~> 4.0 to ~> 5.1 dependabot-preview[bot] 2020-05-24 14:15:20
.github/workflows/test.yml
@@ -5,11 +5,13 @@ on: [push, pull_request]
 jobs:
   test:
     runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        ruby: [ '2.5', '2.6', '2.7' ]
     steps:
     - uses: actions/checkout@v1
-    - name: Set up Ruby
-      uses: actions/setup-ruby@v1
+    - uses: actions/setup-ruby@v1
       with:
-        version: 2.7.x
-    - name: Build and test
+        ruby-version: ${{ matrix.ruby }}
+    - name: cibuild
       run: bin/cibuild
bin/cibuild
@@ -1,15 +1,10 @@
 #!/bin/sh
 
-# script/cibuild: Setup environment for CI to run tests. This is primarily
-#                 designed to run on the continuous integration server.
-
 set -e
+[ -z "$DEBUG" ] || set -x
 
 cd "$(dirname "$0")/.."
 
-echo [$(date "+%H:%M:%S")] "==> Started at…"
-
-# GC customizations
 export RUBY_GC_MALLOC_LIMIT=79000000
 export RUBY_GC_HEAP_INIT_SLOTS=800000
 export RUBY_HEAP_FREE_MIN=100000
@@ -17,7 +12,11 @@ export RUBY_HEAP_SLOTS_INCREMENT=400000
 export RUBY_HEAP_SLOTS_GROWTH_FACTOR=1
 export CIBUILD=1
 
-ruby -v
-gem install bundler:2.0.1 --conservative
+echo "[$(date "+%H:%M:%S")] ==> Running setup…"
+bin/setup
+
+echo "[$(date "+%H:%M:%S")] ==> Running tests…"
 bin/test
+
+echo "[$(date "+%H:%M:%S")] ==> Running linters…"
 bin/lint
bin/lint
@@ -2,7 +2,4 @@
 
 set -e
 
-[ -z "$DEBUG" ] || set -x
-
-echo [$(date "+%H:%M:%S")] "==> Running linters…"
 bundle exec rake lint
bin/setup
@@ -1,8 +1,7 @@
 #!/usr/bin/env bash
 set -euo pipefail
 IFS=$'\n\t'
-set -vx
 
+ruby -v
+gem install bundler -v '~> 2.0'
 bundle install
-
-# Do any other automated setup that you need to do here
bin/test
@@ -1,17 +1,7 @@
 #!/bin/sh
 
-# script/test: Run test suite for application. Optionally pass in a path to an
-#              individual test file to run a single test.
-
-
 set -e
 
 cd "$(dirname "$0")/.."
 
-[ -z "$DEBUG" ] || set -x
-
-echo [$(date "+%H:%M:%S")] "==> Running setup…"
-bin/setup
-
-echo [$(date "+%H:%M:%S")] "==> Running tests…"
 bundle exec rake test
lib/net/hippie/api.rb
@@ -1,29 +0,0 @@
-# frozen_string_literal: true
-
-module Net
-  module Hippie
-    # A no nonsense class to perform HTTP requests.
-    class Api
-      attr_reader :uri, :verify_mode
-
-      def initialize(url, verify_none: false)
-        @uri = URI.parse(url)
-        @verify_mode = OpenSSL::SSL::VERIFY_NONE if verify_none
-      end
-
-      def get
-        client.get(uri).body
-      end
-
-      def execute(request)
-        client.execute(uri, request)
-      end
-
-      private
-
-      def client
-        @client ||= Client.new(headers: {}, verify_mode: verify_mode)
-      end
-    end
-  end
-end
lib/net/hippie/client.rb
@@ -10,27 +10,25 @@ module Net
         'User-Agent' => "net/hippie #{Net::Hippie::VERSION}"
       }.freeze
 
-      attr_accessor :mapper, :read_timeout, :open_timeout, :logger
-      attr_accessor :follow_redirects
-
-      def initialize(certificate: nil, headers: DEFAULT_HEADERS,
-        key: nil, passphrase: nil, verify_mode: Net::Hippie.verify_mode)
-        @certificate = certificate
-        @default_headers = headers
-        @key = key
-        @mapper = ContentTypeMapper.new
-        @passphrase = passphrase
-        @read_timeout = 30
-        @verify_mode = verify_mode
-        @logger = Net::Hippie.logger
-        @follow_redirects = 0
+      attr_reader :mapper, :logger, :follow_redirects
+
+      def initialize(options = {})
+        @options = options
+        @mapper = options.fetch(:mapper, ContentTypeMapper.new)
+        @logger = options.fetch(:logger, Net::Hippie.logger)
+        @follow_redirects = options.fetch(:follow_redirects, 0)
+        @default_headers = options.fetch(:headers, DEFAULT_HEADERS)
+        @connections = Hash.new do |hash, key|
+          scheme, host, port = key
+          hash[key] = Connection.new(scheme, host, port, options)
+        end
       end
 
       def execute(uri, request, limit: follow_redirects, &block)
-        http = http_for(uri)
-        response = http.request(request)
+        connection = connection_for(uri)
+        response = connection.run(request)
         if limit.positive? && response.is_a?(Net::HTTPRedirection)
-          url = build_url_for(http, response['location'])
+          url = connection.build_url_for(response['location'])
           request = request_for(Net::HTTP::Get, url)
           execute(url, request, limit: limit - 1, &block)
         else
@@ -78,8 +76,7 @@ module Net
 
       private
 
-      attr_reader :default_headers, :verify_mode
-      attr_reader :certificate, :key, :passphrase
+      attr_reader :default_headers
 
       def attempt(attempt, max)
         yield
@@ -91,46 +88,21 @@ module Net
         sleep delay
       end
 
-      def http_for(uri)
-        uri = URI.parse(uri.to_s)
-        http = Net::HTTP.new(uri.host, uri.port)
-        http.read_timeout = read_timeout
-        http.open_timeout = open_timeout if open_timeout
-        http.use_ssl = uri.scheme == 'https'
-        http.verify_mode = verify_mode
-        http.set_debug_output(logger)
-        apply_client_tls_to(http)
-        http
-      end
-
       def request_for(type, uri, headers: {}, body: {})
         final_headers = default_headers.merge(headers)
-        uri = URI.parse(uri.to_s)
-        type.new(uri, final_headers).tap do |x|
+        type.new(URI.parse(uri.to_s), final_headers).tap do |x|
           x.body = mapper.map_from(final_headers, body) unless body.empty?
         end
       end
 
-      def private_key(type = OpenSSL::PKey::RSA)
-        passphrase ? type.new(key, passphrase) : type.new(key)
-      end
-
-      def apply_client_tls_to(http)
-        return if certificate.nil? || key.nil?
-
-        http.cert = OpenSSL::X509::Certificate.new(certificate)
-        http.key = private_key
-      end
-
       def run(uri, http_method, headers, body, &block)
         request = request_for(http_method, uri, headers: headers, body: body)
         execute(uri, request, &block)
       end
 
-      def build_url_for(http, path)
-        return path if path.start_with?('http')
-
-        "#{http.use_ssl? ? 'https' : 'http'}://#{http.address}#{path}"
+      def connection_for(uri)
+        uri = URI.parse(uri.to_s)
+        @connections[[uri.scheme, uri.host, uri.port]]
       end
     end
   end
lib/net/hippie/connection.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module Net
+  module Hippie
+    # A connection to a specific host
+    class Connection
+      def initialize(scheme, host, port, options = {})
+        http = Net::HTTP.new(host, port)
+        http.read_timeout = options.fetch(:read_timeout, 10)
+        http.open_timeout = options.fetch(:open_timeout, 10)
+        http.use_ssl = scheme == 'https'
+        http.verify_mode = options.fetch(:verify_mode, Net::Hippie.verify_mode)
+        http.set_debug_output(options.fetch(:logger, Net::Hippie.logger))
+        apply_client_tls_to(http, options)
+        @http = http
+      end
+
+      def run(request)
+        @http.request(request)
+      end
+
+      def build_url_for(path)
+        return path if path.start_with?('http')
+
+        "#{@http.use_ssl? ? 'https' : 'http'}://#{@http.address}#{path}"
+      end
+
+      private
+
+      def apply_client_tls_to(http, options)
+        return if options[:certificate].nil? || options[:key].nil?
+
+        http.cert = OpenSSL::X509::Certificate.new(options[:certificate])
+        http.key = private_key(options[:key], options[:passphrase])
+      end
+
+      def private_key(key, passphrase, type = OpenSSL::PKey::RSA)
+        passphrase ? type.new(key, passphrase) : type.new(key)
+      end
+    end
+  end
+end
lib/net/hippie/content_type_mapper.rb
@@ -5,6 +5,8 @@ module Net
     # Converts a ruby hash into a JSON string
     class ContentTypeMapper
       def map_from(headers, body)
+        return body if body.is_a?(String)
+
         content_type = headers['Content-Type'] || ''
         return JSON.generate(body) if content_type.include?('json')
 
lib/net/hippie/version.rb
@@ -2,6 +2,6 @@
 
 module Net
   module Hippie
-    VERSION = '0.3.2'
+    VERSION = '1.0.0'
   end
 end
lib/net/hippie.rb
@@ -6,10 +6,10 @@ require 'logger'
 require 'net/http'
 require 'openssl'
 
-require 'net/hippie/version'
-require 'net/hippie/content_type_mapper'
 require 'net/hippie/client'
-require 'net/hippie/api'
+require 'net/hippie/connection'
+require 'net/hippie/content_type_mapper'
+require 'net/hippie/version'
 
 module Net
   # net/http for hippies.
@@ -31,7 +31,7 @@ module Net
     ].freeze
 
     def self.logger
-      @logger ||= Logger.new(STDOUT)
+      @logger ||= Logger.new(STDERR)
     end
 
     def self.logger=(logger)
@@ -53,5 +53,19 @@ module Net
     def self.bearer_auth(token)
       "Bearer #{token}"
     end
+
+    def self.method_missing(symbol, *args)
+      default_client.with_retry(retries: 3) do |client|
+        client.public_send(symbol, *args)
+      end || super
+    end
+
+    def self.respond_to_missing?(name, _include_private = false)
+      Client.public_instance_methods.include?(name.to_sym)
+    end
+
+    def self.default_client
+      @default_client ||= Client.new(follow_redirects: 3, logger: logger)
+    end
   end
 end
test/net/api_test.rb
@@ -1,23 +0,0 @@
-require 'test_helper'
-
-class ApiTest < Minitest::Test
-  def test_get
-    VCR.use_cassette('get_breaches') do
-      subject = Net::Hippie::Api.new('https://haveibeenpwned.com/api/breaches')
-      response = subject.get
-      refute_nil response
-      assert_equal(283, JSON.parse(response).count)
-    end
-  end
-
-  def test_execute
-    VCR.use_cassette('get_breaches') do
-      subject = Net::Hippie::Api.new('https://haveibeenpwned.com/api/breaches')
-      request = Net::HTTP::Get.new('https://haveibeenpwned.com/api/breaches')
-      request['Range'] = 'bytes=0-511'
-      response = subject.execute(request)
-      refute_nil response
-      assert_equal(283, JSON.parse(response.body).count)
-    end
-  end
-end
test/net/client_test.rb
@@ -6,7 +6,6 @@ class ClientTest < Minitest::Test
   def initialize(*args)
     super
     @subject = Net::Hippie::Client.new
-    @subject.logger = ENV['CIBUILD'] ? Logger.new('/dev/null') : Logger.new(STDOUT)
   end
 
   def test_get
@@ -27,7 +26,7 @@ class ClientTest < Minitest::Test
         https://pypi.org/pypi/pytz/2019.2/json
         https://pypi.org/pypi/requests/2.5.3/json
       }.each do |url|
-        subject.follow_redirects = 3
+        subject = Net::Hippie::Client.new(follow_redirects: 3)
         response = subject.get(url)
         refute_nil response
         assert_equal Net::HTTPOK, response.class
@@ -38,7 +37,7 @@ class ClientTest < Minitest::Test
 
   def test_does_not_follow_redirect
     VCR.use_cassette('does_not_follow_redirect') do
-      subject.follow_redirects = 0
+      subject = Net::Hippie::Client.new(follow_redirects: 0)
       response = subject.get('https://pypi.org/pypi/django/1.11.3/json')
       refute_nil response
       assert_kind_of Net::HTTPRedirection, response
@@ -48,7 +47,7 @@ class ClientTest < Minitest::Test
 
   def test_does_follow_redirects
     VCR.use_cassette('does_follow_redirects') do
-      subject.follow_redirects = 10
+      subject = Net::Hippie::Client.new(follow_redirects: 10)
       response = subject.get('https://pypi.org/pypi/django/1.11.3/json')
       refute_nil response
       assert_kind_of Net::HTTPOK, response
@@ -58,7 +57,7 @@ class ClientTest < Minitest::Test
 
   def test_follow_redirects_with_relative_paths
     VCR.use_cassette('follow_redirects_with_relative_paths') do
-      subject.follow_redirects = 10
+      subject = Net::Hippie::Client.new(follow_redirects: 10)
       response = subject.get("http://go.microsoft.com/fwlink/?LinkId=329770")
       refute_nil response
       assert_kind_of Net::HTTPOK, response
@@ -76,7 +75,7 @@ class ClientTest < Minitest::Test
       .then
       .to_return(status: 200, body: { success: true }.to_json)
 
-    subject.follow_redirects = n
+    subject = Net::Hippie::Client.new(follow_redirects: n)
     response = subject.get(url)
     refute_nil response
     assert_equal Net::HTTPOK, response.class
@@ -304,10 +303,4 @@ class ClientTest < Minitest::Test
     end
     assert(@called)
   end
-
-  def test_open_timeout_setting
-    assert_nil subject.open_timeout
-    @subject.open_timeout = 10
-    assert_equal subject.open_timeout, 10
-  end
 end
test/net/hippie_test.rb
@@ -21,4 +21,17 @@ class HippieTest < Minitest::Test
     Net::Hippie.verify_mode = OpenSSL::SSL::VERIFY_NONE
     assert Net::Hippie.verify_mode == OpenSSL::SSL::VERIFY_NONE
   end
+
+  def test_get_with_retry
+    uri = URI.parse('https://www.example.org/api/scim/v2/schemas')
+    WebMock.stub_request(:get, uri.to_s)
+      .to_timeout.then
+      .to_timeout.then
+      .to_timeout.then
+      .to_return(status: 200, body: { 'success' => 'true' }.to_json)
+    response = Net::Hippie.get(uri)
+    refute_nil response
+    assert_equal Net::HTTPOK, response.class
+    assert_equal JSON.parse(response.body)['success'], 'true'
+  end
 end
test/test_helper.rb
@@ -7,6 +7,8 @@ require 'securerandom'
 require 'vcr'
 require 'webmock'
 
+Net::Hippie.logger = ENV['CIBUILD'] ? Logger.new('/dev/null') : Logger.new(STDERR)
+
 VCR.configure do |config|
   config.cassette_library_dir = 'test/fixtures'
   config.hook_into :webmock
.rubocop.yml
@@ -5,7 +5,8 @@ AllCops:
     - 'test/**/*'
     - 'tmp/**/*'
     - 'vendor/**/*'
-  TargetRubyVersion: 2.4
+  NewCops: enable
+  TargetRubyVersion: 2.5
 
 Layout/ParameterAlignment:
   EnforcedStyle: with_fixed_indentation
.travis.yml
@@ -1,9 +0,0 @@
-sudo: false
-language: ruby
-rvm:
-  - 2.4.9
-  - 2.5.7
-  - 2.6.5
-  - 2.7.0
-script:
-  - bin/cibuild
CHANGELOG.md
@@ -1,4 +1,4 @@
-Version 0.3.2
+Version 1.0.0
 
 # Changelog
 All notable changes to this project will be documented in this file.
@@ -7,7 +7,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 ## [Unreleased]
-- nil
+
+## [1.0.0] - 2020-07-31
+### Added
+- Add simpler API to remove the need to instantiate a `Client` directly.
+- Default to 3 retries using simple API.
+- Re-use client connection for connections to the same scheme, host, and port.
+
+### Removed
+- Remove support for Ruby 2.4
+- Remove legacy `Api` class.
+
+### Changed
+- Limit mutable options on Client.
+- Change default `read_timeout` to 10 seconds.
+- Change default `open_timeout` to 10 seconds.
+- Log to `STDERR` by default instead of `STDOUT`.
 
 ## [0.3.2] - 2020-01-28
 ### Fixed
@@ -54,7 +69,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - with\_retry.https://www.mokhan.ca/%F0%9F%92%8E/2018/11/10/net-hippie-0-2-0.html
 - authorization header helpers
 
-[Unreleased]: https://github.com/mokhan/net-hippie/compare/v0.3.2...HEAD
+[Unreleased]: https://github.com/mokhan/net-hippie/compare/v1.0.0...HEAD
+[1.0.0]: https://github.com/mokhan/net-hippie/compare/v0.3.2...v1.0.0
 [0.3.2]: https://github.com/mokhan/net-hippie/compare/v0.3.1...v0.3.2
 [0.3.1]: https://github.com/mokhan/net-hippie/compare/v0.3.0...v0.3.1
 [0.3.0]: https://github.com/mokhan/net-hippie/compare/v0.2.7...v0.3.0
Gemfile
@@ -2,7 +2,5 @@
 
 source 'https://rubygems.org'
 
-git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
-
 # Specify your gem's dependencies in net-hippie.gemspec
 gemspec
net-hippie.gemspec
@@ -21,11 +21,11 @@ Gem::Specification.new do |spec|
   spec.bindir        = 'exe'
   spec.executables   = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
   spec.require_paths = ['lib']
-  spec.required_ruby_version = '~> 2.4'
+  spec.required_ruby_version = '~> 2.5'
 
   spec.add_development_dependency 'minitest', '~> 5.0'
   spec.add_development_dependency 'rake', '~> 13.0'
   spec.add_development_dependency 'rubocop', '~> 0.55'
-  spec.add_development_dependency 'vcr', '~> 4.0'
+  spec.add_development_dependency 'vcr', '~> 6.0'
   spec.add_development_dependency 'webmock', '~> 3.4'
 end
Rakefile
@@ -6,7 +6,7 @@ require 'rubocop/rake_task'
 
 Rake::TestTask.new(:test) do |t|
   t.libs << 'test'
-  t.libs << 'lib'
+  t.verbose = true
   t.test_files = FileList['test/**/*_test.rb']
 end
 RuboCop::RakeTask.new(:rubocop)
README.md
@@ -1,10 +1,10 @@
 # Net::Hippie
 
 [![Gem Version](https://badge.fury.io/rb/net-hippie.svg)](https://rubygems.org/gems/net-hippie)
-[![Build Status](https://travis-ci.org/mokhan/net-hippie.svg?branch=master)](https://travis-ci.org/mokhan/net-hippie)
+[![Build Status](https://github.com/mokhan/net-hippie/workflows/Test/badge.svg)](https://github.com/mokhan/net-hippie/actions)
 
-Net::Hippie is a light weight wrapper around `net/http` that defaults to
-sending JSON messages.
+`Net::Hippie` is a light weight wrapper around `net/http` that defaults to
+sending `JSON` messages.
 
 ## Installation
 
@@ -27,23 +27,22 @@ Or install it yourself as:
 ```ruby
 require 'net/hippie'
 
-Net::Hippie.logger = Rails.logger
-
-client = Net::Hippie::Client.new
+Net::Hippie.logger = Logger.new(STDERR)
 
 headers = {
   'Accept' => 'application/vnd.haveibeenpwned.v2+json'
 }
 
-uri = URI.parse('https://haveibeenpwned.com/api/breaches')
-response = client.get(uri, headers: headers)
+uri = 'https://haveibeenpwned.com/api/breaches'
+response = Net::Hippie.get(uri, headers: headers)
 puts JSON.parse(response.body)
 ```
 
 ```ruby
-client = Net::Hippie::Client.new
-body = { user: { name: 'hippie' } }
-response = client.post(URI.parse('https://example.org'), body: body)
+response = Net::Hippie.post(
+  'https://example.org',
+  body: { name: 'hippie' }
+)
 puts JSON.parse(response.body)
 ```
 
@@ -69,24 +68,30 @@ client = Net::Hippie::Client.new(
 ### Basic Auth
 
 ```ruby
-client = Net::Hippie::Client.new
-headers = { 'Authorization' => Net::Hippie.basic_auth('username', 'password') }
-client.get('https://www.example.org', headers: headers)
+Net::Hippie.get(
+  'https://www.example.org',
+  headers: {
+    'Authorization' => Net::Hippie.basic_auth('username', 'password')
+  }
+)
 ```
 
 ### Bearer Auth
 
 ```ruby
-client = Net::Hippie::Client.new
 headers = { 'Authorization' => Net::Hippie.bearer_auth('token') }
-client.get('https://www.example.org', headers: headers)
+Net::Hippie.get('https://www.example.org', headers: headers)
 ```
 
 ## Development
 
-After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
+After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/test` to run the tests.
+You can also run `bin/console` for an interactive prompt that will allow you to experiment.
 
-To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
+To install this gem onto your local machine, run `bundle exec rake install`.
+To release a new version, update the version number in `version.rb`,
+and then run `bin/shipit`, which will create a git tag for the version,
+push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
 
 ## Contributing