diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 97c0804..3fcb839 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,21 +4,22 @@ on: [push] jobs: test: runs-on: ubuntu-latest + strategy: + matrix: + ruby-version: ['3.4', '4.0'] steps: - - uses: actions/checkout@v2 - - uses: ruby/setup-ruby@477b21f02be01bcb8030d50f37cfec92bfa615b6 + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 with: - ruby-version: 2.7 + ruby-version: ${{ matrix.ruby-version }} bundler-cache: true - - run: bundle install - run: bundle exec rspec rubocop: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: ruby/setup-ruby@477b21f02be01bcb8030d50f37cfec92bfa615b6 + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 with: - ruby-version: 2.7 + ruby-version: '4.0' bundler-cache: true - - run: bundle install - run: bundle exec rubocop diff --git a/.ruby-gemset b/.ruby-gemset deleted file mode 100644 index a145603..0000000 --- a/.ruby-gemset +++ /dev/null @@ -1 +0,0 @@ -vertebrae \ No newline at end of file diff --git a/.ruby-version b/.ruby-version index be94e6f..1454f6e 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.2.2 +4.0.1 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..53fac5e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog + +## [2.0.0] - 2026-09-28 + +### Fixed +- The default `/` prefix was applied twice when building request paths, so a caller path that + already started with `/` produced a leading `//`. Faraday treats that as a protocol-relative URL + and mangles it, yielding malformed URLs like `https://host////api/path` that some upstream APIs + reject at the edge with a bare 403. + +### Breaking Changes +- Dropped support for Ruby versions older than 3.4. Officially supported versions are Ruby 3.4 and 4.0. +- Pinned `faraday` to `~> 2.0` (previously unbounded above `2.0`). diff --git a/Gemfile b/Gemfile index 340a1e9..44e12b7 100644 --- a/Gemfile +++ b/Gemfile @@ -1,16 +1,6 @@ -source "http://rubygems.org" +# frozen_string_literal: true -gem 'activesupport', '>= 5.1.4' -gem 'faraday', '> 2.0' -gem 'faraday-mashify' -gem 'faraday-multipart' +source 'https://rubygems.org' -# Add dependencies to develop your gem here. -# Include everything needed to run rake, tests, features, etc. -group :development do - gem "rspec" - gem "bundler" - gem "webmock" - gem "rspec-its" - gem "rubocop" -end +# Specify your gem's dependencies in vertebrae.gemspec +gemspec diff --git a/README.md b/README.md index 9495850..cc6453d 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,11 @@ Some basic infrastructure for writing beautiful API clients. See tijuana\_client [![CI Status](https://github.com/controlshift/vertebrae/actions/workflows/ci.yml/badge.svg)](https://github.com/controlshift/vertebrae/actions/workflows/ci.yml) +## Requirements + +- Ruby 3.4 or 4.0 +- Faraday ~> 2.0 + ## Development After checking out the repo, run `bundle install` to install dependencies. Then, run `rake spec` to run the tests. diff --git a/Rakefile b/Rakefile index 39261f9..82bb534 100644 --- a/Rakefile +++ b/Rakefile @@ -1,39 +1,8 @@ -# encoding: utf-8 +# frozen_string_literal: true -require 'rubygems' -require 'bundler' require 'bundler/gem_tasks' - - -begin - Bundler.setup(:default, :development) -rescue Bundler::BundlerError => e - $stderr.puts e.message - $stderr.puts "Run `bundle install` to install missing gems" - exit e.status_code -end -require 'rake' - -require 'rspec/core' require 'rspec/core/rake_task' -RSpec::Core::RakeTask.new(:spec) do |spec| - spec.pattern = FileList['spec/**/*_spec.rb'] -end - -desc "Code coverage detail" -task :simplecov do - ENV['COVERAGE'] = "true" - Rake::Task['spec'].execute -end - -task :default => :spec -require 'rdoc/task' -Rake::RDocTask.new do |rdoc| - version = File.exist?('VERSION') ? File.read('VERSION') : "" +RSpec::Core::RakeTask.new(:spec) - rdoc.rdoc_dir = 'rdoc' - rdoc.title = "vertebrae #{version}" - rdoc.rdoc_files.include('README*') - rdoc.rdoc_files.include('lib/**/*.rb') -end +task default: :spec diff --git a/lib/vertebrae/request.rb b/lib/vertebrae/request.rb index 40ce49e..d580fe7 100644 --- a/lib/vertebrae/request.rb +++ b/lib/vertebrae/request.rb @@ -32,7 +32,8 @@ def request(method, path, params, options) # :nodoc: end connection.options = default_options.merge(initialisation_options.merge(options)) - path = connection.configuration.prefix + '/' + path + # Avoid a leading '//' here — Faraday treats it as a protocol-relative URL and mangles the path. + path = File.join(connection.configuration.prefix.to_s, path.to_s.delete_prefix('/')) ::Vertebrae::Base.logger.debug "EXECUTED: #{method} - #{path} with #{params} and #{options}" diff --git a/lib/vertebrae/version.rb b/lib/vertebrae/version.rb index d931217..72c2a50 100644 --- a/lib/vertebrae/version.rb +++ b/lib/vertebrae/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Vertebrae - VERSION = '1.0.5' + VERSION = '2.0.0' end diff --git a/spec/request_spec.rb b/spec/request_spec.rb index ed5c93d..ad1a15a 100644 --- a/spec/request_spec.rb +++ b/spec/request_spec.rb @@ -49,6 +49,22 @@ end end + context 'with a custom prefix and a path without a leading slash' do + let(:options) { {prefix: '/rest/v1'} } + it 'should join the prefix and path with a single slash' do + stub_request(:get, 'https://test.com/rest/v1/user/') + vb.request(:get, 'user/', {}, options) + end + end + + context 'with the default prefix and an already-absolute path' do + let(:options) { {} } + it 'should not produce a malformed URL with duplicate slashes' do + stub_request(:post, 'https://test.com/api/member/details') + vb.request(:post, '/api/member/details', {}, options) + end + end + context 'with a different host' do let(:options) { {host: 'test2.com'} } it 'should make the request to the default host' do diff --git a/vertebrae.gemspec b/vertebrae.gemspec index f769c79..af69c23 100644 --- a/vertebrae.gemspec +++ b/vertebrae.gemspec @@ -4,45 +4,45 @@ lib = File.expand_path('lib', __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'vertebrae/version' -Gem::Specification.new do |s| - s.name = "vertebrae".freeze - s.version = Vertebrae::VERSION - - s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= - s.require_paths = ["lib".freeze] - s.authors = ["Nathan Woodhull".freeze] - s.date = "2021-07-23" - s.description = "A set of low level infrastructure and reusable code for building API clients".freeze - s.email = "nathan@controlshiftlabs.com".freeze - s.extra_rdoc_files = [ - "LICENSE.txt", - "README.md" +Gem::Specification.new do |spec| + spec.name = 'vertebrae' + spec.version = Vertebrae::VERSION + spec.authors = ['Nathan Woodhull', 'Owens Ehimen'] + spec.email = ['talk@controlshiftlabs.com'] + + spec.summary = 'API Client Infrastructure' + spec.description = 'A set of low level infrastructure and reusable code for building API clients' + spec.homepage = 'https://github.com/controlshift/vertebrae' + spec.license = 'MIT' + + spec.extra_rdoc_files = [ + 'LICENSE.txt', + 'README.md' ] # Specify which files should be added to the gem when it is released. # The `git ls-files -z` loads the files in the RubyGem that have been added into git. - s.files = Dir.chdir(File.expand_path(__dir__)) do + spec.files = Dir.chdir(File.expand_path(__dir__)) do `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } end + spec.require_paths = ['lib'] - s.homepage = "http://github.com/controlshift/vertebrae".freeze - s.licenses = ["MIT".freeze] - s.summary = "API Client Infrastructure".freeze + spec.required_ruby_version = ['>= 3.4', '< 5.0'] - if s.respond_to? :specification_version then - s.specification_version = 4 - end + # Runtime dependencies + spec.add_runtime_dependency 'activesupport', '>= 6.1' + spec.add_runtime_dependency 'faraday', '~> 2.0' + spec.add_runtime_dependency 'faraday-mashify', '~> 1.0' + spec.add_runtime_dependency 'faraday-multipart', '~> 1.0' + + # Development dependencies + spec.add_development_dependency 'bundler', '>= 2.0', '< 5.0' + spec.add_development_dependency 'rake', '~> 13.0' + spec.add_development_dependency 'rspec', '~> 3.0' + spec.add_development_dependency 'rspec-its', '~> 2.0' + spec.add_development_dependency 'rubocop', '~> 1.0' + spec.add_development_dependency 'webmock', '~> 3.0' - s.add_runtime_dependency(%q.freeze, [">= 5.1.4"]) - s.add_runtime_dependency(%q.freeze, ["~> 2"]) - s.add_runtime_dependency(%q.freeze, [">= 0"]) - s.add_runtime_dependency(%q.freeze, [">= 0"]) - - s.add_development_dependency(%q.freeze, [">= 0"]) - s.add_development_dependency(%q.freeze, [">= 0"]) - s.add_development_dependency(%q.freeze, [">= 0"]) - s.add_development_dependency(%q.freeze, [">= 0"]) - s.add_development_dependency(%q.freeze, [">= 0"]) - s.add_development_dependency(%q.freeze, [">= 0"]) + spec.metadata['rubygems_mfa_required'] = 'true' end