创建 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 账号 #

  1. 访问 RubyGems.org
  2. 点击 “Sign up” 注册账号
  3. 验证邮箱

配置 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