Criando sua própria RubyGem


Leia em 4 minutos

Todo mundo já deve ter usado pelo menos uma vez na vida as famosas gems. Elas ajudam muito na hora distribuir e usar bibliotecas Ruby, sem dúvida nenhuma. Mas o que nem todo mundo sabe é que criar uma gem é muito mais fácil do que parece. Neste artigo você verá como criar e distribuir sua própria gem.

Entendo a gem

Uma gem nada mais é que um arquivo tar.

$ tar -ztvf bundler-0.9.26.gem
-rw-r--r--  0 wheel  wheel   47579 Dec 31  1969 data.tar.gz
-rw-r--r--  0 wheel  wheel     993 Dec 31  1969 metadata.gz

Neste arquivo, você encontra o código Ruby propriamente dito (data.tar.gz) e os metadados (metadata.gz), que é a gem specification (ou apenas gemspec) em formato YAML. Esta gemspec informa, dentre outras coisas, qual é o nome da gem, a versão e dependências do pacote, além dos arquivos que compoem a biblioteca.

Uma gem é composta por uma estrutura mais ou menos definida de arquivos e diretórios, como você pode ver abaixo.

Estrutura de arquivos e diretórios de uma gem

O diretório lib é de longe o mais importante. É nele que todo código Ruby fica armazenado. E é ele que é adicionado ao GEM_PATH, utilizado pelo RubyGems (gerenciador de pacotes) para saber quais bibliotecas estão disponíveis para uso.

O RubyGems substitui o método Kernel#require com sua própria implementação, que faz uma busca pela gem que você precisa utilizando os diretórios adicionados ao GEM_PATH. Então, sempre que você utilizar o método require "some_library", o RubyGems irá procurar por um arquivo lib/some_library.rb, independente de qual gem tenha este arquivo.

Criando a biblioteca HelloWorld

Vamos criar nossa biblioteca HelloWorld, que será empacotada posteriormente como uma gem. O primeiro passo é gerar nossa estrutura de diretórios.

$ mkdir -p hello_world/{lib/hello_world,test}

Crie um arquivo de testes em test/hello_world_test.rb. Nosso módulo terá um único método chamado say.

require "minitest/autorun"

class HelloWorldTest < Minitest::Test
  def test_say_hello_to_the_world
    assert_equal "Hello World!", HelloWorld.say
  end
end

Você pode executar este teste com o comando ruby test/hello_world_test.rb. Ele irá falhar, como era de se esperar.

Run options: --seed 51020

# Running:

E

Finished in 0.001068s, 936.5488 runs/s, 0.0000 assertions/s.

  1) Error:
HelloWorldTest#test_say_hello_to_the_world:
NameError: uninitialized constant HelloWorldTest::HelloWorld
    test/hello_world_test.rb:5:in `test_say_hello_to_the_world'

1 runs, 0 assertions, 0 failures, 1 errors, 0 skips

Agora, crie o arquivo lib/hello_world.rb. Nossa implementação é bastante simples.

module HelloWorld
  def self.say
    "Hello World!"
  end
end

Execute o comando ruby test/hello_world_test.rb mais uma vez. Como era de se esperar, o teste irá… falhar novamente. Isso acontece porque não estamos carregando o módulo HelloWorld. Embora você possa resolver isso de várias maneiras diferentes, vamos fazer do modo mais organizado: iremos criar uma rake task para executar nossos testes, adicionando o diretório lib ao $LOAD_PATH.

Crie um arquivo Rakefile com o seguinte código:

require "rake/testtask"

Rake::TestTask.new do |t|
  t.libs << "lib"
  t.test_files = Dir["test/**/*_test.rb"]
end

Altere o arquivo test/hello_world_test.rb, adicionando a linha que irá carregar o arquivo hello_world.rb.

require "minitest/autorun"
require "hello_world"  # Added

class HelloWorldTest < Test::Unit::TestCase
  def test_say_hello_to_the_world
    assert_equal "Hello World!", HelloWorld.say
  end
end

Rode o teste mais uma vez, só que agora usando o comando rake test.

$ rake test
Run options: --seed 63119

# Running:

.

Finished in 0.001127s, 887.1926 runs/s, 887.1926 assertions/s.

1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

Ótimo! Nossos testes estão passando e como terminamos toda nossa implementação, já podemos criar e distribuir nossa gem.

Gerando nosso pacote

Para gerar um arquivo .gem, precisamos escrever nossa gemspec. Crie um arquivo hello_world.gemspec e adicione o código abaixo.

Gem::Specification.new do |s|
  s.name        = "hello_world"
  s.version     = "0.1.0"
  s.description = "A simple gem that says hello to the world!"
  s.summary     = "Say hello!"
  s.author      = "Nando Vieira"
  s.files       = Dir["{lib/**/*.rb,README.rdoc,test/**/*.rb,Rakefile,*.gemspec}"]
end

As opções da classe Gem::Specification são auto-descritivas; para saber mais sobre estas e outras opções, consulte a documentação.

Agora vem a parte mais fácil. Execute o comando gem build hello_world.gemspec e um arquivo hello_world-0.1.0.gem será gerado.

Você pode instalar este arquivo localmente utilizando o comando gem install hello_world-0.1.0.gem --local. Para listar o que foi instalado, execute gem contents hello_world.

$ gem contents hello_world
/Users/fnando/.gem/spaces/active/gems/hello_world-1.0/lib/hello_world.rb
/Users/fnando/.gem/spaces/active/gems/hello_world-1.0/test/hello_world_test.rb
/Users/fnando/.gem/spaces/active/gems/hello_world-1.0/Rakefile
/Users/fnando/.gem/spaces/active/gems/hello_world-1.0/hello_world.gemspec

E, claro, nossa gem também será listada com o comando gem list.

$ gem list hello_world -d

*** LOCAL GEMS ***

hello_world (1.0)
    Author: Nando Vieira
    Installed at: /Users/fnando/.gem/spaces/active

    Say hello!

Distribuindo nossa gem

Para distribuir sua gem, é preciso criar uma conta no site RubyGems.org.

Depois, basta executar gem push hello_world-0.1.0.gem; informe seu e-mail e senha cadastrados no RubyGems.org quando solicitado. Se tudo der certo, você verá uma mensagem como esta:

$ gem push hello_world-0.1.0.gem
Pushing gem to RubyGems.org...
Successfully registered gem: hello_world (0.1.0)

Você pode acessar sua gem em http://rubygems.org/gems/hello_world.

DICA: Antes de escrever sua gem, verifique se o nome está disponível. Um simples gem list nome_da_gem -rd já te dará a resposta. A gem hello_world já existia, por isso foi publicada como http://rubygems.org/gems/fnando-hello_world.

Versionamento dos pacotes

É sua responsabilidade definir como será o versionamento de sua gem. Eu utilizo o formato MAJOR.MINOR.PATCH sugerido pelo Semantic Versioning, como em 1.2.3.

No arquivo lib/hello_world/version.rb você pode ter algo como:

module HelloWorld
  module Version
    MAJOR = 0
    MINOR = 1
    PATCH = 0
    STRING = "#{MAJOR}.#{MINOR}.#{PATCH}"
  end
end

No seu arquivo hello_world.gemspec, carregue o arquivo de versão e referencie-o em vez de deixar o valor fixo neste arquivo.

require './lib/hello_world/version'

Gem::Specification.new do |s|
  s.name        = "hello_world"
  s.version     = HelloWorld::Version::STRING
  s.description = "A simple gem that says hello to the world!"
  s.summary     = "Say hello!"
  s.author      = "Nando Vieira"
  s.files       = Dir["{lib/**/*.rb,README.rdoc,test/**/*.rb,Rakefile,*.gemspec}"]
end

Usando o Bundler

O Bundler possui um gerador de gems que facilita toda esta configuração. Para gerar a estrutura de sua biblioteca, execute o comando bundle gem.

$ bundle gem hello_world -t minitest --coc
Creating gem 'hello_world'...
Code of conduct enabled in config
      create  hello_world/Gemfile
      create  hello_world/.gitignore
      create  hello_world/lib/hello_world.rb
      create  hello_world/lib/hello_world/version.rb
      create  hello_world/hello_world.gemspec
      create  hello_world/Rakefile
      create  hello_world/README.md
      create  hello_world/bin/console
      create  hello_world/bin/setup
      create  hello_world/CODE_OF_CONDUCT.md
      create  hello_world/.travis.yml
      create  hello_world/test/test_helper.rb
      create  hello_world/test/hello_world_test.rb
Initializing git repo in /Users/fvieira/Projects/samples/hello_world

A ideia é basicamente a mesma; crie seu código no diretório lib e os testes no diretório test. A maior diferença do Bundler é que ele configura a geração e publicação da gem através de Rake tasks.

$ rake -T
rake build          # Build hello_world-0.1.0.gem into the pkg directory
rake install        # Build and install hello_world-0.1.0.gem into system gems
rake install:local  # Build and install hello_world-0.1.0.gem into system gems without network access
rake release        # Create tag v0.1.0 and build and push hello_world-0.1.0.gem to Rubygems
rake test           # Run tests

Finalizando

Entender o processo de criação e publicação de uma gem é fundamental para qualquer desenvolvedor Ruby. Além de permitir que você distribua seu código, ajuda na hora de fazer o debugging de uma biblioteca que você esteja usando.