Retired experiment:
namespaced-gemwas a prototype for URI and namespace-aware gem dependencies. That experiment bore fruit ingemserver-purland the related preset gems, includinggemserver-gem_coopandgemserver-gems_bridgetownrb_com. New work should use those gems instead; this project remains as historical context for the original investigation.
A RubyGems plugin that enables gemspec dependencies to be declared as full URIs, pointing to namespaced gem sources such as gem.coop namespaces.
This implements the ideas discussed in gem-coop/gem.coop#12.
gem.coop's public beta introduced namespaces — isolated gem registries per user or organization:
https://beta.gem.coop/@myspace # namespace index
https://beta.gem.coop/@myspace/my-gem # canonical gem URI
Today, gemspecs declare dependencies as plain names:
spec.add_dependency "rack", "~> 3.0"There is no standard way to express which gem server (and which namespace)
a dependency comes from inside the gemspec itself. The user must manually add
a source block to their Gemfile — which defeats the purpose of publishing a
self-describing gemspec.
The question this prototype asks: can a gemspec declare its own source for a dependency, using the dependency name string alone?
spec.add_dependency "https://beta.gem.coop/@myspace/my-gem", "~> 1.0"
# or using a Package URL (purl-spec):
spec.add_dependency "pkg:gem/@myspace/my-gem?repository_url=https://beta.gem.coop", "~> 1.0"RubyGems 4.0.5 removed the old Gem::Dependency::VALID_NAME_PATTERN
restriction entirely — any String is now accepted as a dependency name:
dep = Gem::Dependency.new("https://beta.gem.coop/@ns/foo", "~> 1.0")
dep.name # => "https://beta.gem.coop/@ns/foo"However, RubyGems has no idea what to do with a URI-named dependency. It
will happily store the string, but gem install will try to look up that
literal name on rubygems.org — and fail. Neither RubyGems nor Bundler knows
how to extract the real gem name, derive the namespace source URL, or resolve
transitive dependencies that use URI names.
This gem bridges that gap. It teaches both RubyGems' resolver (gem install) and Bundler's resolver (bundle install) how to parse URI dependency
names, route them to the correct namespace source, and remap transitive deps
on the fly.
This gem ships a rubygems_plugin.rb that is automatically loaded by RubyGems
at boot — before any gemspec is parsed.
Prepends helper methods #uri_gem? and #uri_dependency onto
Gem::Dependency.
dep = Gem::Dependency.new("https://beta.gem.coop/@myspace/my-gem", "~> 1.0")
dep.uri_gem? # => true
dep.uri_dependency # => #<Namespaced::Gem::UriDependency gem_name="my-gem" source_url="https://beta.gem.coop/@myspace">Parses a URI dependency name into its components:
| Part | Example |
|---|---|
server_base |
https://beta.gem.coop |
namespace |
@myspace |
gem_name |
my-gem |
source_url |
https://beta.gem.coop/@myspace |
Supports three forms:
- Full URI:
https://beta.gem.coop/@myspace/my-gem - Shorthand:
@myspace/my-gem(defaults tohttps://beta.gem.coop) - Package URL (purl-spec):
pkg:gem/@myspace/my-gem(namespace in path, default server)pkg:gem/@myspace/my-gem?repository_url=https://beta.gem.coop(explicit server)pkg:gem/my-gem?repository_url=https://beta.gem.coop/@myspace(namespace in qualifier)
All three forms resolve to the same internal representation and are interchangeable anywhere a dependency name is accepted.
Patches Bundler::Dsl#gemspec so that after standard gemspec processing, any
URI-named runtime dependencies automatically inject a source block:
# What the user writes in their Gemfile:
gemspec
# What this patch injects automatically for URI deps:
# source "https://beta.gem.coop/@myspace" do
# gem "my-gem", "~> 1.0"
# endThis means the Gemfile needs no manual source declarations for URI deps found in the gemspec.
There are four ways to use namespaced-gem today, depending on your situation.
Add namespaced-gem as a runtime dependency of your gem. It is published on
rubygems.org and acts as a bridge to gem.coop namespaces.
Gem::Specification.new do |spec|
spec.name = "my-gem"
spec.version = "1.0.0"
# This gem must be a runtime dependency so that its rubygems_plugin.rb
# is installed and loaded by RubyGems at boot — before any gemspec
# containing URI dependencies is evaluated.
spec.add_dependency "namespaced-gem"
# Traditional dependency from RubyGems.org:
spec.add_dependency "rack", "~> 3.0"
# Namespaced dependency from gem.coop (full URI):
spec.add_dependency "https://beta.gem.coop/@myspace/special-gem", "~> 0.5"
# Shorthand (defaults to beta.gem.coop):
spec.add_dependency "@myorg/internal-tool", ">= 2.0"
# Package URL (purl-spec):
spec.add_dependency "pkg:gem/@myorg/another-tool", ">= 1.0"
spec.add_dependency "pkg:gem/@myorg/extra?repository_url=https://beta.gem.coop", "~> 3.0"
endWhen a downstream user uses Bundler (the expected path), their Gemfile can remain:
source "https://rubygems.org"
gemspecThe Bundler integration automatically injects the correct source blocks for
any URI dependencies found in the gemspec. Bundler uses only the Compact Index
API, which gem.coop namespace servers already support.
Both bundle install and gem install my-gem work — see
Use Case 4 for the
gem install path.
If you are not publishing a gem but want to use URI-style dependencies in an
application, install namespaced-gem directly:
gem install namespaced-gemBecause rubygems_plugin.rb files are only loaded from installed gems, the
gem must be present in the gem path before RubyGems evaluates any gemspec
that contains URI dependencies. In practice this means:
- Install the gem first:
gem install namespaced-gem - Then declare URI dependencies in your gemspec or Gemfile as usual.
Note: Simply listing
gem "namespaced-gem"in a Gemfile is not sufficient on its own — Bundler evaluates the Gemfile (and itsgemspecdirective) before it installs gems, so the plugin would not yet be loaded. The gem must already be installed viagem install(or as a transitive dependency of another installed gem, as in Use Case 1).
Install namespaced-gem once into your Ruby environment and every subsequent
gem install and bundle install in that Ruby will be able to resolve
URI-named dependencies — no per-project configuration needed.
gem install namespaced-gemThat's it. The rubygems_plugin.rb is now in the gem path and will be loaded
by RubyGems on every boot. From this point forward:
gem install some-gemwill automatically resolve any URI-named transitive dependencies found insome-gem's gemspec.bundle installin any project will automatically inject the correctsourceblocks for URI dependencies found in gemspecs.
This is useful for CI images, Docker containers, or development machines where
you want namespace support available globally without requiring each gem or
project to explicitly depend on namespaced-gem.
# Example: Dockerfile
RUN gem install namespaced-gem
# All subsequent gem/bundle commands in this image now support URI deps.# Example: CI setup step
gem install namespaced-gem
bundle install # URI deps in any gemspec are resolved automaticallyOnce namespaced-gem is installed, you can install namespaced gems directly:
gem install namespaced-gem # one-time setup (if not already installed)
gem install @kaspth/oaken # shorthand (defaults to beta.gem.coop)
gem install https://beta.gem.coop/@kaspth/oaken # full URIThis works because the plugin patches multiple layers of RubyGems' native
gem install pipeline:
GemResolverPatchintercepts the resolver and routes URI-named dependencies to the correct namespace source via the Compact Index (versions/info/endpoints).ApiSpecPatchsynthesizes aGem::Specificationfrom the Compact Index data already fetched — bypassing the legacyquick/Marshal.4.8/endpoint that namespace servers don't serve.DownloadPatchprovides clear, actionable error messages if the namespace server fails to serve the.gemfile.
All three forms of URI dependency names are supported:
gem install @kaspth/oaken
gem install https://beta.gem.coop/@kaspth/oaken
gem install "pkg:gem/@kaspth/oaken"lib/
rubygems_plugin.rb # Loaded by RubyGems at boot (or hot-loaded during install)
namespaced/
gem.rb # Main module
gem/
version.rb
uri_dependency.rb # URI parser (value object)
namespace_source_registry.rb # Thread-safe registry of namespace source URLs
dependency_patch.rb # Gem::Dependency patch (helper methods)
api_spec_patch.rb # Gem::Resolver::APISpecification — Compact Index spec synthesis
download_patch.rb # Gem::Source#download — namespace download error handling
bundler_integration.rb # Bundler::Dsl#gemspec patch
bundler_resolver_patch.rb # Bundler::Definition / Resolver transitive dep handling
gem_resolver_patch.rb # Gem::RequestSet / InstallerSet for `gem install`
metadata_deps_hook.rb # Gem.done_installing hook for hot-load deferred deps
-
Plugin must be installed before first use (application developers only). This gem works as a RubyGems plugin (
rubygems_plugin.rb), which means it must be installed in the gem path so that RubyGems loads the plugin at boot before any gemspec containing URI dependencies is evaluated. For gem authors (Use Case 1), this happens automatically — when a user installs your gem,namespaced-gemis installed as a transitive dependency and available on the next boot. For global installations (Use Case 3), the plugin is already in the gem path by definition. For application developers (Use Case 2), the gem must be installed explicitly withgem install namespaced-gembefore runningbundle install, because Bundler evaluates the Gemfile before it installs gems. In Ruby 4.0+, RubyGems auto-loadsbundler/setupwhen it detects a Gemfile in the working directory, and this happens beforeRUBYOPT-rflags are processed — so the plugin must already be in the gem path. -
Gemspec linting: Tools that validate gemspecs (e.g.
gem build,rake release) work fine becauseSpecificationPolicy#validate_nameonly validates the gem's own name — it does not check dependency names. -
gem.coop(production) returns HTTP 200 with body"404"for namespace endpoints. Namespace resolution only works againstbeta.gem.coopcurrently. The productiongem.coopserver returns HTTP 200 with a plain-text body of"404"for namespace paths, which RubyGems misinterprets as valid Compact Index data. The shorthand default server isbeta.gem.coopfor this reason. See ISSUE.md for details.
Version requirements work exactly as they always have — they are the second
argument to add_dependency, completely separate from the name:
spec.add_dependency "https://beta.gem.coop/@myspace/my-gem", "~> 1.0"
# ^^^^^^^^^^ URI name ^^^^^^^^^^^^^^^^^^ ^^^^^^^^
# version
spec.add_dependency "pkg:gem/@myspace/my-gem", "~> 1.0"
# ^^^^^^^ purl name ^^^^^^ ^^^^^^^^
# versionAll standard operators (~>, >=, =, etc.) are supported unchanged.
Note: The purl spec allows a
@versioncomponent in the name itself (e.g.pkg:gem/@ns/foo@2.0). If present it is ignored — version constraints always come from the second argument toadd_dependency.
bundle install
bundle exec rspec # unit + offline integration tests
bundle exec rspec --tag network # network integration tests (hits beta.gem.coop)
bundle exec rake # tests + rubocopThe network integration tests resolve a real gem (@kaspth/oaken) from
beta.gem.coop and verify bundle lock produces a correct Gemfile.lock.
They are excluded from the default rspec run and must be opted into with
--tag network.
Bug reports and pull requests are welcome on GitLab at https://gitlab.com/galtzo-floss/namespaced-gem.
This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.
Everyone interacting in the namespaced-gem project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.