Tạo Gem của chính bạn

Khi bạn là một Ruby developer thì bạn không lạ lẫm gì với các Gem. Bạn có từng muốn có một Gem thật sự hữu ích để mọi người có thể sử dụng, mà tác giả là chính mình không? Hôm nay, tôi sẽ giới thiệu đến các bạn cách tạo một Gem cho Ruby on Rails.

Một Gem đơn giản mà tôi đã đẩy lên Github simple_banana

YOUR FIRST GEM

Tôi sẽ bắt đầu với một Gem, cái tên khá là "chuối" - simple_banana. Bạn có thể đặt tên thật độc đáo hoặc mang dấu ấn nào đó của bạn, tốt nhất nếu nó hữu dụng thì hãy đặt một tên gợi nhớ cho Gem. Cách đặt tên được hướng dẫn ở basic recommendations.

Đặt tên thư mục và tên file sẽ có dạng cơ bản như thế này, gồm 1 file .rb đặt trong tư mục lib, và một file .gemspec ngang hàng với thư mục lib

tree
.
├── lib
│       └── simple_banana.rb
└── simple_banana.gemspec

Bây giờ, chúng ta sẽ viết code demo cho Gem của chúng ta. Bên trong file lib/simple_banana:

class SimpleBanana
  def self.hi language = "english"
     puts "Hello world!"
  end
end

Trong file .gemspec sẽ định nghĩa các thuộc tính của gem, tác giả, cũng như version, description, và các files được load ... Đó cũng là một interface đến RubyGems.org. Tất cả các thông tin bạn thấy ở trang gem đều được định nghĩa từ file .gemspec này.

Gem::Specification.new do |s|
  s.name        = 'simple_banana'
  s.version     = '0.0.0'
  s.date        = '2017-02-01'
  s.summary     = "banana"
  s.description = "A simple hello world gem"
  s.authors     = ["banana"]
  s.email       = '[email protected]'
  s.files       = ["lib/simple_banana.rb", "lib/simple_banana/translator.rb"]
  s.homepage    =
    'http://rubygems.org/gems/simple_banana'
  s.license       = 'MIT'
end

*description có thể dài hơn đoạn text mà bạn thấy trong ví dụ của tôi, miễn là nó có thể match với điều kiện /^== [A-Z]/ sau đó, description sẽ được chạy thông qua RDoc’s markup formatter để hiển thị trên trang RubyGems web site. *

Có phải bạn thấy rất quen thuộc? gemspec này cũng như Ruby, bạn có thể thay đổi wrap scripts để tạo các file names, và còn có cả version number. Có rất nhiều fields được include trong file .gemspec này, bạn có thể xem cụ thể tại reference của guides ruby gems.

Sau khi tạo một gem, ta có thể build gem từ đó. Và sau đó là install gem này để test các chức năng mà mình vừa viết (yaoming)

gem build simple_banana.gemspec 
  Successfully built RubyGem
  Name: simple_banana
  Version: 0.0.0
  File: simple_banana-0.0.0.gem
gem install ./simple_banana-0.0.0.gem
Successfully installed simple_banana-0.0.0
Parsing documentation for simple_banana-0.0.0
Installing ri documentation for simple_banana-0.0.0
Done installing documentation for simple_banana after 0 seconds
1 gem installed

Có thể install gần giống như Gem "xịn" rồi ấy nhỉ!

Đương nhiên, các smoke test vẫn chưa xong, bây giờ bạn có thể require gem và test sản phẩm của mình được rồi. Ở đây mình test trên rails console

 require 'simple_banana'
 => true 
2.3.0 :002 > SimpleBanana.hi
Hello world!

Hmm, có vẻ ra sản phẩm rồi đấy. Một chú ý nhỏ khi bạn sử dụng bản Ruby trước 1.9.2 thì phải start session với irb -rubygems hoặc là require ruby gems sau khi launch irb.

Bây giờ bạn có thể chia sẻ sản phẩm Gem của bạn với cộng đồng Ruby.

curl -u bapboy18  https://rubygems.org/api/v1/api_key.yaml > ~/.gem/credentials; chmod 0600 ~/.gem/credentials
Enter host password for user 'bapboy18':

Push gem lên rubygems:

gem push simple_banana-0.0.0.gem 
Pushing gem to https://rubygems.org...
Successfully registered gem: simple_banana (0.0.0)

Ở bước này, có thể bạn cần phải đăng ký/ đăng nhập tài khoản trên Ruby gem

Đây là Gem của tôi

Chỉ trong một khoảng thời gian ngắn sau khi Gem được đẩy lên (dưới 1 phút) thì Gem này có thể được cài đặt từ bất kỳ ai. Bạn có thể xem simple Gem mà tôi vừa tạo ở đây

gem list -r simple_banana

*** REMOTE GEMS ***

simple_banana (0.0.0)

Cài thử dùng luôn (yaoming)

gem install simple_banana
Successfully installed simple_banana-0.0.0
Parsing documentation for simple_banana-0.0.0
Done installing documentation for simple_banana after 0 seconds
1 gem installed

Cài được như Gem "hịn" này! Test luôn:

irb -Ilib -rsimple_banana
2.3.0 :001 > SimpleBanana.hi "english"
 => "hello world" 
2.3.0 :002 > SimpleBanana.hi "spanish"
 => "hola mundo" 

Chạy có vẻ đúng chức năng rồi đấy. Việc chia sẻ code với Ruby và RubyGems khá dễ dàng nhỉ!

REQUIRING MORE FILES

Khi mở rộng chức năng Gem thì việc dồn tất cả vào 1 files quả là điều rất bất tiện. Phần tiếp theo đây, tôi sẽ đề cập đến phần mở rộng số lượng files, cũng như chức năng của Gem như thế nào. Giả sử ta có thêm chức năng muốn thêm trong file simple_banana.rb

class SimpleBanana
  def self.hi language = "english"
    translator = Translator.new(language)
    translator.hi
  end
end

class SimpleBanana::Translator
  def initialize(language)
    @language = language
  end

  def hi
    case @language
    when "spanish"
      "hola mundo"
    else
      "hello world"
    end
  end
end

Có vẻ quá nhiều code trong cùng một file, điều này làm code trở nên thật sự rối khi bạn có nhiều code hơn nữa. Bây giờ ta sẽ tách class Translator thành một file riêng. Như đã đề cập từ trước, file root có trách nhiệm load code cho gem, các file code khác sẽ được đặt trong thư mục có cùng tên với file root này. Ở đây, tôi đặt là thư mục simple_banana, dĩ nhiên là bên trong thư mục lib.

tree
.
├── lib
│   ├── simple_banana
│   │   └── translator.rb
│   └── simple_banana.rb

Bây giờ class Translator ở trong thư mục lib/simple_banana, và code trong file translator.rb này chính là phần mở rộng:

class SimpleBanana::Translator
  def initialize(language)
    @language = language
  end

  def hi
    case @language
    when "spanish"
      "hola mundo"
    else
      "hello world"
    end
  end
end

Đặt thêm dòng code trong file simple_banana.rb để nó có thể load code trong phần vừa mở rộng:

class SimpleBanana
  def self.hi language = "english"
    translator = Translator.new(language)
    translator.hi
  end
end

require "simple_banana/translator"

Và trong file .gemspec không quên khai báo các file mà mình đã tạo:

Gem::Specification.new do |s|
  ...
  s.files       = ["lib/simple_banana.rb", "lib/simple_banana/translator.rb"]
  ...
end

Kết quả khi test trên rails console:

irb -Ilib -rsimple_banana
2.3.0 :001 > SimpleBanana.hi
 => "hello world" 
2.3.0 :002 > SimpleBanana.hi "spanish"
 => "hola mundo" 

Tôi muốn nhấn mạnh, command có flag -Ilib. Thông thường, RubyGems includes thư mục lib cho bạn, và người dùng (end users) cũng không cần phải quá quan tâm về flag này. Tuy nhiên, nếu như bạn chạy dòng lệnh bên ngoài thư mục chứa Gem, thì bạn cần phải configure những thứ này. Nó có thể thao tác với $LOAD_PATH từ bên trong bản thân của code. Tuy nhiên đó không phải là patterns trong hầu hết các trường hợp. Có rất nhiều các patterns không tốt (và cả patterns tốt nữa) cho gems, được giải thích ở this guides.

Nếu bạn thêm vào các files vào Gem của bạn, thì hãy nhớ khai báo chúng trong trong files của gemspec trước khi push chúng lên như là một new gem. Hoặc có thể khai báo để nó có thể nhận dạng một cách tự động như just a dynamic gemspec.

Việc thêm nhiều thư mục và thêm nhiều code khi xây dựng Gem cũng theo một quy trình giống nhau. Hãy chia nhỏ các file của bạn khi nó có ý nghĩa. Hãy sắp xếp code của bạn thật khoa học để không bị rối mắt khi nhìn vào đống code đó 😄

ADDING AN EXECUTABLE

mkdir bin
touch bin/simple_banana
chmod a+x bin/simple_banana
ruby -Ilib ./bin/simple_banana 
hello world
 ruby -Ilib ./bin/simple_banana spanish
hola mundo
head -4 simple_banana.gemspec 
Gem::Specification.new do |s|
  s.name        = 'simple_banana'
  s.version     = '0.0.1'
  s.executables << 'simple_banana'

WRITING TESTS

Testing Gem là một phần vô cùng quan trọng. Không chỉ giúp bạn chắc rằng code của bạn hoạt động, testing còn giúp người khác biết rằng Gem của bạn đang làm công việc gì. Khi đánh giá một Gem, các nhà thẩm định có xu hướng xem bộ solid test có tốt không (hoặc có sai sót gì không), là một trong những nguyên nhân để code của bạn được tin tưởng.

Gem supports việc thêm vào các test files vào trong packages của chính nó để test có thể chạy khi một gem được download.

Tóm lại là: TEST YOUR GEM! Please!

Tạo Rakefile và thư mục test

 tree
.
├── bin
│   └── simple_banana
├── lib
│   ├── simple_banana
│   │   └── translator.rb
│   └── simple_banana.rb
├── Rakefile
├── simple_banana-0.0.0.gem
├── simple_banana-0.0.1.gem
├── simple_banana.gemspec
└── test
    └── test_simple_banana.rb

Nội dung Rakefile:

require 'rake/testtask'

Rake::TestTask.new do |t|
  t.libs << 'test'
end

desc "Run tests"
task :default => :test

Code của file test:

require 'minitest/autorun'
require 'simple_banana'

class SimpleBananaTest < Minitest::Test
  def test_english_hello
    assert_equal "hello world",
      SimpleBanana.hi("english")
  end

  def test_any_hello
    assert_equal "hello world",
      SimpleBanana.hi("ruby")
  end

  def test_spanish_hello
    assert_equal "hola mundo",
      SimpleBanana.hi("spanish")
  end
end

Cuối cùng là run test:

rake test
Run options: --seed 24756

# Running:

...

Finished in 0.001203s, 2493.7283 runs/s, 2493.7283 assertions/s.

3 runs, 3 assertions, 0 failures, 0 errors, 0 skips

Bây giờ, bạn có thể build lại Gem và push lên Ruby Gemspec - nhớ thay đổi version của gem để build nhé. Các bạn có thể tham khảo simple_banana của mình tại Ruby gem simple_banana, Github simple_banana

Chúc các bạn có nhiều Gem hữu ích do chính tay mình làm ra!

Tham khảo Ruby Gem