Upgrading¶ ↑
From 2.x to 3.0¶ ↑
Version 3.0 replaces the option hashes, the V1 and V2 clients, and the Hash responses of 2.x with keyword arguments, one Gems::Client, and resource objects, and it renames or removes what 2.x had under old names without deprecating them first. This guide covers what code written for 2.x needs to change. See CHANGELOG.md for everything that was added.
Ruby¶ ↑
Version 3.0 requires Ruby 3.4 or later.
Clients and configuration¶ ↑
Gems::Client.new and Gems.new take keyword arguments, and an unknown option raises ArgumentError. Gems::V1, Gems::V2, Gems::AbstractClient, Gems::Request, Gems::BaseClient, Gems.options, Gems::Configuration::VALID_OPTIONS_KEYS, and Gems::Configuration::DEFAULT_KEY are gone:
# 2.x client = Gems::Client.new(:key => "rubygems_701243f217cdf23b1370c7b66b65ca97") Gems::V2.info "rails", "7.0.6" # 3.0 client = Gems::Client.new(key: "rubygems_701243f217cdf23b1370c7b66b65ca97") Gems.version "rails", "7.0.6"
The Gems module delegates the API methods alone to one client, Gems.client, which is built from the global configuration, where 2.x delegated every client method to a new client for every call. It is built again only when the configured host or ID token changes, or the key it falls back to when none is configured; any other change to the configuration, such as a key, a passcode, or a timeout, is applied to the client it has. The raw request methods are on that client:
# 2.x Gems.get "/api/v1/gems/rails.json" # 3.0 Gems.client.get "/api/v1/gems/rails.json"
Those methods take a path on the client’s host, and a path that is a URL of another scheme, host, or port, or that climbs out of the prefix the host carries, raises ArgumentError rather than sending the request there with the credentials the client resolved for its own host. Send a request to another host with the host argument, which resolves the key stored for the host it names:
# 3.0 Gems.client.get "https://gems.example.com/api/v1/gems/rails.json" # ArgumentError Gems.client.get "/api/v1/gems/rails.json", host: "https://gems.example.com"
The API key in ~/.gem/credentials is read when it is first needed rather than when the library is required, and assigning nil to key turns that fallback off, where 2.x fell back to it anyway. The default key is resolved as gem push resolves it: the GEM_HOST_API_KEY environment variable comes first, then the key gem signin --host stored for the configured host, then, for RubyGems.org itself, the RubyGems.org key, where 2.x read only the RubyGems.org key:
# 3.0 Gems.key = nil # send requests without an API key Gems.configure { |config| config.key = nil }
The default key is resolved for the host a request is sent to, so a client built for another host, and a request made to one with host:, send the key gem signin --host stored for that host rather than the RubyGems.org key, where 2.x sent the RubyGems.org key wherever push was pointed. A host nothing is stored for is sent no key at all, rather than the RubyGems.org key that gem push --host falls back to for it. A key you configure yourself is sent wherever the client sends a request, as gem push --key is:
# 3.0 Gems.push "gems-0.0.8.gem", host: "https://gems.example.com" # uses the key stored for gems.example.com
A host that is not an HTTP or HTTPS URL raises ArgumentError where it is assigned, whether that is Gems.host, a client’s host, or the host: a client is built with, and an invalid proxy_url does the same. 2.x let URI::BadURIError or URI::InvalidURIError escape when a request was made:
# 3.0 Gems.host = "rubygems.org" # ArgumentError: Invalid host: rubygems.org
A client given both an API key and a username and password sends the key, and sends the username and password only to create_api_key, update_api_key, and me, the endpoints RubyGems.org takes them for. 2.x sent the username and password in place of the key to every endpoint, where the endpoints that take a key refused them.
An empty GEM_HOST_API_KEY counts as no key rather than as an empty one, so a continuous integration job whose secret is not set sends an unauthenticated request instead of an empty Authorization header.
The one-time passcode is resolved the way the API key is: until one is assigned, it is the GEM_HOST_OTP_CODE environment variable gem push falls back to, and an empty one counts as no passcode. Assigning nil to otp, as to key, disables that fallback.
The options otp, id_token, open_timeout, read_timeout, write_timeout, debug_output, proxy_url, max_redirects, max_retries, max_retry_delay, ca_file, ca_path, cert_store, client_cert, and client_key are new, and every option can be set globally or per client. A request the server turns away with 429, 502, 503, or 504, or that the network loses, is now sent again twice before it raises, where 2.x raised straight away; max_retries of zero restores that. A 429 is retried for every request, push included, since RubyGems.org rate limits a request before it reaches the endpoint. A 502, 503, 504, or network failure is retried only for a request that reads, such as rubygem, rather than for one that acts on a gem, such as push or yank, since the origin may have acted on it. See the README for the whole of it.
Renamed methods¶ ↑
| 2.x | 3.0 |
|---|---|
| ‘Gems.info(name)` | ‘Gems.rubygem(name)` |
| ‘Gems::V2.info(name, version)` | ‘Gems.version(name, version)` |
| ‘Gems.gems(handle)` | ‘Gems.owned_gems(handle)` |
| ‘Gems.total_downloads(name, version)` | ‘Gems.downloads(name, version)` |
| ‘Gems::GemError` | ‘Gems::Error` |
Gems::GemError remains as a deprecated alias of Gems::Error, and is the only name kept.
Keyword arguments¶ ↑
The methods that took an option hash take keyword arguments, and the host of push is a keyword that defaults to the client’s host rather than to RubyGems.org:
# 2.x Gems.search "cucumber", {:page => 2} Gems.push File.new("gems-0.0.8.gem"), "https://gems.example.com" Gems.yank "gems", "0.0.8", {:platform => "java"} # 3.0 Gems.search "cucumber", page: 2 Gems.push File.new("gems-0.0.8.gem"), host: "https://gems.example.com" Gems.yank "gems", "0.0.8", platform: "java"
yank needs to be told which version to yank. 2.x yanked the latest version of a gem when it was given none, which a yank cannot undo; 3.0 raises ArgumentError instead, and takes the version from a gem or version object that carries one:
# 2.x Gems.yank "gems" # yanks whatever the latest version is # 3.0 Gems.yank "gems" # ArgumentError: No version of gems given to yank Gems.yank "gems", Gems.latest_version("gems")
reverse_dependencies changed the same way as search. latest and just_updated now take no arguments at all: their endpoints answer with the 50 gems they name whatever page is asked for, so the page 2.x took was never more than the first page under another name.
# 2.x Gems.latest :page => 2 # the same 50 gems as page one # 3.0 Gems.latest
Pagination¶ ↑
The endpoints that take a page option have an _each counterpart that walks the pages, so the loops 2.x needed are no longer yours to write:
# 2.x page = 1 gems = [] loop do results = Gems.search("cucumber", :page => page) break if results.empty? gems += results page += 1 end # 3.0 gems = Gems.search_each("cucumber").to_a
timeframe_versions_each does the same for its endpoint. latest and just_updated have no counterpart, since their endpoints have no pages to walk.
Return values¶ ↑
The methods that returned parsed JSON return objects: Gems::Gem, Gems::Version, Gems::Dependency, Gems::Owner, Gems::Profile, Gems::WebHook, Gems::Downloads, and Gems::APIKey. Each reads its fields with methods, parses timestamps into Time objects, and offers [] and to_h for the raw response, so code that read the Hash can keep doing so:
# 2.x Gems.info("rails")["version"] Gems.versions("rails").map { |version| version["number"] } Gems.owners("rails").map { |owner| owner["handle"] } # 3.0 Gems.rubygem("rails").version Gems.versions("rails").map(&:number) Gems.owners("rails").map(&:handle) Gems.rubygem("rails")["version"] # still works
The attributes are frozen, at every level, so an object is an immutable value. to_h answers with a copy of them that the caller owns, copied at every level too, so code that changed the Hash a 2.x method returned keeps working as long as it changes what to_h gave it rather than the object:
# 2.x gem = Gems.info("rails") gem["downloads"] += 1 gem["dependencies"]["runtime"] << dependency # 3.0 gem = Gems.rubygem("rails").to_h gem["downloads"] += 1 gem["dependencies"]["runtime"] << dependency
attributes answers with the frozen Hash itself, for reading it without the copy.
Some return values changed shape:
# 2.x Gems.latest_version("rails") # => {"version" => "8.1.3.1"} Gems.total_downloads # => {:total => 123} Gems.total_downloads("rails", "8.1.3.1") # => {:version_downloads => 1, :total_downloads => 2} Gems.most_downloaded # => [[{"full_name" => "rails-8.1.3.1", ...}, 123], ...] Gems.web_hooks # => {"all gems" => [{"url" => ...}], "rails" => [...]} # 3.0 Gems.latest_version("rails") # => "8.1.3.1" Gems.total_downloads # => 123 Gems.downloads("rails", "8.1.3.1") # => #<Gems::Downloads full_name="rails-8.1.3.1" total=2 version_downloads=1> Gems.most_downloaded # => [#<Gems::Version name="rails" number="8.1.3.1">, ...], with downloads_count Gems.web_hooks # => [#<Gems::WebHook gem_name="*" url=...>, ...]
A web hook on all gems has a gem_name of "*". Objects are accepted wherever their identifier is expected, so Gems.versions(gem), Gems.remove_owner(gem, owner), and Gems.key = api_key work, and they compare by identity, so Gems.rubygem("rails") == Gems.rubygem("rails") even when the download counts have changed in between. A gem or version given where a gem is expected stands in for the version it carries, so Gems.version(gem), Gems.contents(gem), Gems.attestations(gem), Gems.downloads(gem), and Gems.yank(gem) act on that version; without one the first four act on the latest version of the gem.
Errors¶ ↑
Every error is a Gems::Error. Gems::NotFound is one of the Gems::HTTPError subclasses, which expose the response and the status code as an Integer, with Gems::ClientError or Gems::ServerError for any 4xx or 5xx status without a class of its own:
# 2.x rescue Gems::NotFound, Gems::GemError # 3.0 rescue Gems::NotFound # a 404 rescue Gems::HTTPError # any error status; error.code and error.response say which rescue Gems::Error # everything the library raises
Failures that 2.x let through as other exceptions are Gems::Error subclasses now:
-
Connection failures, DNS errors, timeouts, and SSL errors raise
Gems::NetworkErrorrather than the underlyingErrno,Net,Socket,OpenSSL, andTimeouterrors. -
A successful response whose body is not JSON raises
Gems::InvalidResponserather thanJSON::ParserError, andrubygemandversionraise it rather than return{}. A response whose JSON lacks a field the library reads, such as theversionoflatest_versionor the key of an API key, raises it rather thanKeyError, and a timestamp that cannot be parsed raises it rather thanArgumentError. -
Asking for the latest version of a gem that has none, directly or by omitting the version from
downloads, raisesGems::NoLatestVersionrather than sending"unknown"as the version. -
A redirect loop raises
Gems::TooManyRedirectsaftermax_redirectshops rather than recursing forever.
SSL certificates are verified, where 2.x disabled verification. A host with a certificate that Ruby’s OpenSSL does not trust, such as a private gem server with a self-signed certificate, raises Gems::NetworkError until the certificate is trusted, which the ca_file, ca_path, and cert_store options are for; client_cert and client_key present a certificate to a host that asks for one. There is no option to turn verification off. A redirect to another scheme, host, or port is followed without the credentials of the request, and one that would send the body of the request again is not followed at all, where 2.x sent the API key wherever the redirect led.
Removed methods and constants¶ ↑
dependencies, api_key, and unyank are gone, since RubyGems.org retired their endpoints. PUT /api/v1/gems/unyank has answered 403 “This version of the Gemcutter plugin has been deprecated.” for years, so 2.x’s Gems.unyank could not have worked against RubyGems.org. create_api_key replaces api_key:
# 2.x Gems.configure do |config| config.username = "nick@gemcutter.org" config.password = "schwwwwing" end Gems.api_key # 3.0 Gems.configure do |config| config.username = "nick@gemcutter.org" config.password = "schwwwwing" end Gems.create_api_key("ci-push", scopes: %i[push_rubygem]).key
create_api_key and update_api_key are new in 3.0: they take the scopes to grant a key as a list, and raise ArgumentError for a scope the RubyGems API does not define.
Gems::Version::MAJOR, MINOR, PATCH, and PRE are gone. Gems::VERSION is the library’s version string, and Gems::Version is the object returned for a version of a gem.
Gems::Configuration::DEFAULT_HOST is the RubyGems.org host, where 2.x froze the RUBYGEMS_HOST environment variable into it when the library was required. The environment variable is still the default host, read by Gems.default_host when the host is resolved, as the API key is read:
# 2.x Gems::Configuration::DEFAULT_HOST # => ENV["RUBYGEMS_HOST"], read when the library was required # 3.0 Gems.default_host # => ENV["RUBYGEMS_HOST"], read now Gems.host # => the same, until another host is configured
Internals¶ ↑
Everything documented as @api private can change within 3.x, and is hidden from the generated documentation: Gems::Connection, Gems::RequestBuilder, Gems::RedirectHandler, Gems::ResponseParser, Gems::RetryHandler, Gems::ConnectionPool, and the mixins beneath them, such as Gems::DeepCopy, Gems::Idempotence, Gems::Identifiers, Gems::JSONParsing, Gems::Pagination, Gems::PathEscaping, Gems::RedactedOutput, Gems::RetryAfter, and Gems::URLValidation. Configure them through the options of Gems::Client, such as user_agent, max_redirects, and max_retries. The class methods that declare the readers of a resource, such as Gems::Resource.attribute and Gems::Resource.identified_by, are internals too, since no endpoint answers with a resource of your own; Gems::Resource.attribute_names, which a pattern matches by, is public. The authenticators, such as Gems::APIKeyAuthenticator and Gems::TrustedPublisherAuthenticator, and Gems::Client#authenticator, which reads the one a client uses, are internals too; change a credential with the setters of Gems::Client or the Gems module, which build the authenticator again, and exchange an ID token by hand with exchange_trusted_publisher_token.