创建 Gem #
概述 #
创建自己的 RubyGem 是分享代码的最佳方式。本节将详细介绍如何从零开始创建、开发和发布一个 RubyGem。
快速开始 #
使用 bundle gem 创建 #
bash
# 创建基础 Gem
bundle gem mygem
# 创建带测试的 Gem
bundle gem mygem --test=rspec
# 创建带 MIT 许可证的 Gem
bundle gem mygem --mit
# 创建带可执行文件的 Gem
bundle gem mygem --exe
# 创建完整配置的 Gem
bundle gem mygem --test=rspec --mit --exe --ci=github
生成的目录结构 #
text
mygem/
├── bin/
│ ├── console # 交互式控制台
│ └── setup # 安装脚本
├── lib/
│ ├── mygem/
│ │ └── version.rb # 版本信息
│ └── mygem.rb # 主入口
├── spec/
│ ├── spec_helper.rb # 测试配置
│ └── mygem_spec.rb # 测试文件
├── .gitignore # Git 忽略文件
├── .rspec # RSpec 配置
├── Gemfile # 依赖管理
├── LICENSE.txt # 许可证
├── README.md # 说明文档
├── Rakefile # Rake 任务
└── mygem.gemspec # Gem 规范
gemspec 配置 #
基本 gemspec #
ruby
# mygem.gemspec
# frozen_string_literal: true
require_relative 'lib/mygem/version'
Gem::Specification.new do |spec|
spec.name = "mygem"
spec.version = Mygem::VERSION
spec.authors = ["Your Name"]
spec.email = ["your.email@example.com"]
spec.summary = "A short summary"
spec.description = "A longer description"
spec.homepage = "https://github.com/username/mygem"
spec.license = "MIT"
spec.required_ruby_version = ">= 2.6.0"
spec.metadata["homepage_uri"] = spec.homepage
spec.metadata["source_code_uri"] = "https://github.com/username/mygem"
spec.metadata["changelog_uri"] = "https://github.com/username/mygem/blob/main/CHANGELOG.md"
# 指定文件
spec.files = Dir.chdir(__dir__) do
`git ls-files -z`.split("\x0").reject do |f|
(f == __FILE__) || f.match(%r{\A(?:(?:test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
end
end
spec.bindir = "exe"
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]
# 运行时依赖
spec.add_dependency "nokogiri", "~> 1.14"
# 开发依赖
spec.add_development_dependency "rspec", "~> 3.12"
spec.add_development_dependency "rubocop", "~> 1.50"
end
gemspec 字段详解 #
| 字段 | 必需 | 描述 |
|---|---|---|
name |
✅ | Gem 名称 |
version |
✅ | 版本号 |
authors |
✅ | 作者列表 |
summary |
✅ | 简短描述 |
files |
✅ | 包含的文件 |
email |
❌ | 邮箱 |
description |
❌ | 详细描述 |
homepage |
❌ | 主页地址 |
license |
❌ | 许可证 |
bindir |
❌ | 可执行文件目录 |
executables |
❌ | 可执行文件列表 |
require_paths |
❌ | require 路径 |
dependencies |
❌ | 运行时依赖 |
development_dependencies |
❌ | 开发依赖 |
元数据配置 #
ruby
spec.metadata["homepage_uri"] = "https://example.com"
spec.metadata["source_code_uri"] = "https://github.com/user/repo"
spec.metadata["changelog_uri"] = "https://github.com/user/repo/blob/main/CHANGELOG.md"
spec.metadata["bug_tracker_uri"] = "https://github.com/user/repo/issues"
spec.metadata["documentation_uri"] = "https://docs.example.com"
spec.metadata["mailing_list_uri"] = "https://groups.google.com/group/mygem"
spec.metadata["wiki_uri"] = "https://wiki.example.com"
代码组织 #
入口文件 #
ruby
# lib/mygem.rb
# frozen_string_literal: true
require_relative "mygem/version"
require_relative "mygem/client"
require_relative "mygem/request"
require_relative "mygem/response"
module Mygem
class Error < StandardError; end
class ConfigurationError < Error; end
class << self
attr_accessor :configuration
def configure
self.configuration ||= Configuration.new
yield(configuration) if block_given?
configuration
end
def reset!
self.configuration = nil
end
end
class Configuration
attr_accessor :api_key, :timeout, :base_url
def initialize
@api_key = nil
@timeout = 30
@base_url = "https://api.example.com"
end
end
end
版本文件 #
ruby
# lib/mygem/version.rb
# frozen_string_literal: true
module Mygem
VERSION = "0.1.0"
end
模块组织 #
ruby
# lib/mygem/client.rb
# frozen_string_literal: true
module Mygem
class Client
def initialize(options = {})
@api_key = options[:api_key] || Mygem.configuration.api_key
@timeout = options[:timeout] || Mygem.configuration.timeout
end
def get(path, params = {})
Request.new(@api_key, @timeout).get(path, params)
end
def post(path, data = {})
Request.new(@api_key, @timeout).post(path, data)
end
end
end
ruby
# lib/mygem/request.rb
# frozen_string_literal: true
require "net/http"
require "json"
module Mygem
class Request
def initialize(api_key, timeout)
@api_key = api_key
@timeout = timeout
end
def get(path, params = {})
uri = build_uri(path, params)
request = Net::HTTP::Get.new(uri)
add_headers(request)
execute(uri, request)
end
def post(path, data = {})
uri = build_uri(path)
request = Net::HTTP::Post.new(uri)
add_headers(request)
request.body = data.to_json
execute(uri, request)
end
private
def build_uri(path, params = {})
uri = URI("#{Mygem.configuration.base_url}#{path}")
uri.query = URI.encode_www_form(params) if params.any?
uri
end
def add_headers(request)
request["Authorization"] = "Bearer #{@api_key}"
request["Content-Type"] = "application/json"
request["Accept"] = "application/json"
end
def execute(uri, request)
response = Net::HTTP.start(uri.host, uri.port,
use_ssl: uri.scheme == "https",
read_timeout: @timeout
) do |http|
http.request(request)
end
Response.new(response)
end
end
end
ruby
# lib/mygem/response.rb
# frozen_string_literal: true
require "json"
module Mygem
class Response
attr_reader :status, :headers, :body
def initialize(http_response)
@status = http_response.code.to_i
@headers = http_response.each_header.to_h
@body = parse_body(http_response.body)
end
def success?
status >= 200 && status < 300
end
def error?
!success?
end
private
def parse_body(raw_body)
return nil if raw_body.nil? || raw_body.empty?
JSON.parse(raw_body)
rescue JSON::ParserError
raw_body
end
end
end
可执行文件 #
创建可执行文件 #
bash
# 创建目录
mkdir -p exe
# 创建可执行文件
touch exe/mygem
chmod +x exe/mygem
可执行文件内容 #
ruby
#!/usr/bin/env ruby
# frozen_string_literal: true
require "mygem"
# 解析命令行参数
command = ARGV[0]
args = ARGV[1..-1]
case command
when "version", "-v", "--version"
puts "Mygem version #{Mygem::VERSION}"
when "help", "-h", "--help"
puts "Usage: mygem [command] [options]"
puts ""
puts "Commands:"
puts " version Show version"
puts " help Show this help"
puts " get Make GET request"
puts " post Make POST request"
when "get"
path = args[0]
client = Mygem::Client.new
response = client.get(path)
puts JSON.pretty_generate(response.body)
when "post"
path = args[0]
data = JSON.parse(args[1] || "{}")
client = Mygem::Client.new
response = client.post(path, data)
puts JSON.pretty_generate(response.body)
else
puts "Unknown command: #{command}"
puts "Run 'mygem help' for usage information"
exit 1
end
更新 gemspec #
ruby
spec.bindir = "exe"
spec.executables = ["mygem"]
测试 #
RSpec 配置 #
ruby
# spec/spec_helper.rb
# frozen_string_literal: true
require "bundler/setup"
require "mygem"
RSpec.configure do |config|
config.expect_with :rspec do |expectations|
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
end
config.mock_with :rspec do |mocks|
mocks.verify_partial_doubles = true
end
config.shared_context_metadata_behavior = :apply_to_host_groups
config.filter_run_when_matching :focus
config.example_status_persistence_file_path = "spec/examples.txt"
config.disable_monkey_patching!
config.warnings = true
config.default_formatter = "doc" if config.files_to_run.one?
config.order = :random
Kernel.srand config.seed
end
单元测试 #
ruby
# spec/mygem_spec.rb
# frozen_string_literal: true
RSpec.describe Mygem do
describe ".configure" do
before { Mygem.reset! }
it "yields configuration" do
expect { |b| Mygem.configure(&b) }.to yield_control
end
it "sets configuration values" do
Mygem.configure do |config|
config.api_key = "test_key"
config.timeout = 60
end
expect(Mygem.configuration.api_key).to eq("test_key")
expect(Mygem.configuration.timeout).to eq(60)
end
end
describe ".reset!" do
it "resets configuration" do
Mygem.configure do |config|
config.api_key = "test_key"
end
Mygem.reset!
expect(Mygem.configuration).to be_nil
end
end
end
ruby
# spec/mygem/client_spec.rb
# frozen_string_literal: true
RSpec.describe Mygem::Client do
let(:client) { described_class.new(api_key: "test_key") }
before do
Mygem.configure do |config|
config.base_url = "https://api.example.com"
end
end
describe "#initialize" do
it "accepts options" do
client = described_class.new(api_key: "custom_key", timeout: 60)
expect(client.instance_variable_get(:@api_key)).to eq("custom_key")
expect(client.instance_variable_get(:@timeout)).to eq(60)
end
end
describe "#get" do
it "makes GET request" do
stub_request(:get, "https://api.example.com/users")
.with(headers: { "Authorization" => "Bearer test_key" })
.to_return(body: '{"users": []}', status: 200)
response = client.get("/users")
expect(response.success?).to be true
expect(response.body).to eq({ "users" => [] })
end
end
describe "#post" do
it "makes POST request" do
stub_request(:post, "https://api.example.com/users")
.with(
headers: { "Authorization" => "Bearer test_key" },
body: '{"name":"John"}'
)
.to_return(body: '{"id":1}', status: 201)
response = client.post("/users", { name: "John" })
expect(response.success?).to be true
expect(response.body).to eq({ "id" => 1 })
end
end
end
运行测试 #
bash
# 运行所有测试
bundle exec rspec
# 运行特定文件
bundle exec rspec spec/mygem_spec.rb
# 运行特定测试
bundle exec rspec spec/mygem_spec.rb:10
# 显示详细输出
bundle exec rspec --format documentation
# 显示覆盖率
bundle exec rspec --format documentation --color
文档 #
README.md #
markdown
# Mygem
A Ruby client for the Example API.
## Installation
Add this line to your application's Gemfile:
```ruby
gem 'mygem'
And then execute:
bash
$ bundle install
Or install it yourself as:
bash
$ gem install mygem
Usage #
Configuration #
ruby
require 'mygem'
Mygem.configure do |config|
config.api_key = 'your_api_key'
config.timeout = 30
config.base_url = 'https://api.example.com'
end
Making Requests #
ruby
client = Mygem::Client.new
# GET request
response = client.get('/users')
puts response.body
# POST request
response = client.post('/users', { name: 'John' })
puts response.body
Development #
After checking out the repo, run bin/setup to install dependencies.
Then, run rake spec to run the tests.
Contributing #
Bug reports and pull requests are welcome on GitHub.
License #
The gem is available as open source under the terms of the MIT License.
text
### YARD 文档
```ruby
# lib/mygem/client.rb
module Mygem
# Client for making API requests
#
# @example Creating a client
# client = Mygem::Client.new(api_key: 'your_key')
# response = client.get('/users')
#
class Client
# Initialize a new client
#
# @param options [Hash] Configuration options
# @option options [String] :api_key API key for authentication
# @option options [Integer] :timeout Request timeout in seconds
#
# @example
# client = Mygem::Client.new(api_key: 'secret', timeout: 60)
#
def initialize(options = {})
@api_key = options[:api_key] || Mygem.configuration.api_key
@timeout = options[:timeout] || Mygem.configuration.timeout
end
# Make a GET request
#
# @param path [String] API endpoint path
# @param params [Hash] Query parameters
# @return [Response] The response object
#
# @example
# response = client.get('/users', page: 1)
# puts response.body
#
def get(path, params = {})
# ...
end
end
end
生成文档 #
bash
# 安装 YARD
gem install yard
# 生成文档
yard doc
# 启动文档服务器
yard server
构建 Gem #
本地构建 #
bash
# 构建 Gem
gem build mygem.gemspec
# 输出
# Successfully built RubyGem
# Name: mygem
# Version: 0.1.0
# File: mygem-0.1.0.gem
本地安装测试 #
bash
# 安装本地 Gem
gem install ./mygem-0.1.0.gem
# 测试安装
irb -r mygem
使用 Rake 构建 #
ruby
# Rakefile
# frozen_string_literal: true
require "bundler/gem_tasks"
require "rspec/core/rake_task"
RSpec::Core::RakeTask.new(:spec)
task default: :spec
desc "Build the gem"
task :build do
sh "gem build mygem.gemspec"
end
desc "Install the gem locally"
task :install => :build do
sh "gem install mygem-*.gem"
end
desc "Release the gem"
task :release => :build do
sh "gem push mygem-*.gem"
end
发布 Gem #
注册 RubyGems 账号 #
- 访问 RubyGems.org
- 点击 “Sign up” 注册账号
- 验证邮箱
配置 API Key #
bash
# 登录
gem signin
# 或手动设置 API Key
curl -u your_username https://rubygems.org/api/v1/api_key.yaml > ~/.gem/credentials
chmod 600 ~/.gem/credentials
发布到 RubyGems.org #
bash
# 推送 Gem
gem push mygem-0.1.0.gem
# 输出
# Pushing gem to https://rubygems.org...
# Successfully registered gem: mygem (0.1.0)
使用 Rake 发布 #
bash
# 使用 Bundler 的发布任务
bundle exec rake release
# 这会:
# 1. 检查是否有未提交的更改
# 2. 创建 git tag
# 3. 推送到 git 仓库
# 4. 构建 gem
# 5. 推送到 RubyGems.org
发布到私有源 #
bash
# 推送到私有源
gem push mygem-0.1.0.gem --host https://gems.example.com
# 或配置默认源
gem sources --add https://gems.example.com/
gem push mygem-0.1.0.gem
版本管理 #
语义化版本 #
text
MAJOR.MINOR.PATCH
MAJOR: 不兼容的 API 变更
MINOR: 向后兼容的功能新增
PATCH: 向后兼容的 bug 修复
更新版本 #
ruby
# lib/mygem/version.rb
module Mygem
VERSION = "0.2.0"
end
预发布版本 #
ruby
# Alpha 版本
VERSION = "1.0.0.alpha.1"
# Beta 版本
VERSION = "1.0.0.beta.1"
# RC 版本
VERSION = "1.0.0.rc.1"
持续集成 #
GitHub Actions #
yaml
# .github/workflows/main.yml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
ruby-version: ['2.7', '3.0', '3.1', '3.2']
steps:
- uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby-version }}
bundler-cache: true
- name: Run tests
run: bundle exec rspec
- name: Run linter
run: bundle exec rubocop
自动发布 #
yaml
# .github/workflows/release.yml
name: Release
on:
release:
types: [published]
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'
bundler-cache: true
- name: Build gem
run: gem build mygem.gemspec
- name: Push to RubyGems
run: |
mkdir -p $HOME/.gem
touch $HOME/.gem/credentials
chmod 0600 $HOME/.gem/credentials
printf -- "---\n:rubygems_api_key: ${RUBYGEMS_API_KEY}\n" > $HOME/.gem/credentials
gem push mygem-*.gem
env:
RUBYGEMS_API_KEY: ${{ secrets.RUBYGEMS_API_KEY }}
下一步 #
现在你已经学会了创建和发布 RubyGem,接下来学习 高级特性 了解更多高级功能!
最后更新:2026-03-28