Пишем свой Ruby Gem для самых маленьких

Пишем свой Ruby Gem для самых маленьких

Как все начиналось

Это мой первый Ruby gem, и я хотел бы поделится своими достижением и интересными фишками которые я постиг. Рассказ будет от создания и до аплоада на rubygems.org
Сначала гем имел название ipinfodb но во время разработки потерпел два переименования, я позже объясню почему 😉

Весь код хранится здесь: max-si-m/ip_info
А сам гем можно попробовать здесь: gems/ip_info

Все баги, предложение пишем куда вам угодно. Всем приятного прочтения!

Мой выбор пал на написание гема. В то время в одном slack-чате, товарищ по имени Frey во всю пилил свой гем meta_nexus для работы с API Blizzard’s (Гем полезный, советую)

Тем на выбор было не так уж и много и я решил почему бы не написать какую-то обертку для API?
Не долго поискав я нашел http://ipinfodb.com/ – сервис который позволяет по выбранному IP адресу или доменному имени получить данные о местоположение адреса. Полезная штука, так что к практике.
Прежде всего Вам нужно регистрироваться на сайте и получить API_KEY.

Создание gem-a

Создать гем, задача оказалась не тяжелой, всего-то нужно было выполнить команду:

$ bundle gem ipinfodb

Все, в текущем каталоге создаться новая папка с полной архитектурой нового гема как это выглядит:

$ tree ipinfodb
ipinfodb
├── .gitignore
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── ipinfodb.gemspec
└── lib
    ├── ipinfodb
    │   └── version.rb
    └── ipinfodb.rb

 

Все отлично, двигаемся дальше.
Дальше нужно заполнить файл спецификации гема, он называется: ipinfodb.gemspec

# coding: utf-8
lib = File.expand_path('../lib', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'ipinfodb/version'

Gem::Specification.new do |spec|
  spec.name          = "ipinfodb"
  spec.version       = Ipinfodb::VERSION
  spec.authors       = ["Maxim Djuliy"]
  spec.email         = ["mak7.dj@gmail.com"]
  spec.description   = %q{API interface for http://ipinfodb.com }
  spec.homepage      = ""
  spec.license       = "MIT"

  spec.files         = `git ls-files -z`.split("\x0")
  spec.executables   = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
  spec.test_files    = spec.files.grep(%r{^(test|spec|features)/})
  spec.require_paths = ["lib"]

  spec.add_development_dependency "bundler", "~> 1.7"
  spec.add_development_dependency "rake", "~> 10.0"
end

Да, я здесь удалил поле spec.summary думал оно не нужно, оказалось нужно. Все что я сделал вы сможете найти здесь: 20e811
Все это чудо нужно было как-то проверить, проверка осуществляется сборкой гема и установкой в системе. Для меня этот процесс оказался каким-то слишком геморройным, может я неправильным способом все это делал, надеюсь более опытные пользователи мне подскажут как все это нужно делать. Так как у нас за поиск всех используемых файлов(spec.files) отвечает git то используем git:

# Добавим все файлы в git
$ git add .
# Делаем сборку гема
$ gem build ipinfodb.gemspec 
# на выходе получаем файл ipinfodb-0.0.1.gem
# Затем инсталим в системе
$ gem install ipinfodb-0.0.1.gem
# После этого заходим в консоль, подключаем(require) гем и тестируем 

Это оказалось достаточно нудной и рутинной задачей, по этому я сократил количество операций объединив две последние:

$ gem build ipinfo.gemspec | gem install ipinfo-0.0.1.gem

Будьте осторожны с версиями!Второй коммит заставил себя подождать так как я хотел закомитить уже первую хоть какую-то рабочею версию, итог можно посмотреть в этом коммите: 61d96
И так, что я сделал:

  1. Добавил spec.summary – без него гем отказывался собираться.
  2. Добавил в dependency httparty, чтобы делать запросы к API
    spec.add_dependency "httparty", "~> 0.13.5"
  3. Уже на рисовалась первая архитектура приложения:
    API
    ├── Request
    ├── Parser
    

Начнем разбор пожалу с самого важного файла: lib/ipinfodb/api.rb

#...
# Инклудим модуль чтобы иметь возможность его использовать 
  include HTTParty
#...   
# Добавим собственный класс ошибки, чтобы выбрасывать ее если пользователь не установил API_KEY
	class ApiKeyError < ArgumentError; end
# Установим базовый url для httparty
	base_uri "http://api.ipinfodb.com/v3/ip-"
# Добавим два простых метода, для инициализации и для поиска данных
	def lookup(ip, options = {})
		raise ApiKeyError.new("Error! Add your API key") if self.api_key.nil?
		query(ip, options) #вызываем метод с класса Ipinfodb::API::Request
	end

На самом деле ничего сложного. Далее следует файл: lib/ipinfodb/request.rb

# принимаем два параметра: ip - это ip адрес или host сайта, второй параметр это опции
def query ip, options
  raise InvalidIpError.new(ip) unless ip.to_s =~ IPV4_REGEXP
  raise InvalidOptionsError.new("Invalid options") unless options.kind_of? Hash

  # выставляем параметры запроса
  type = (options[:type] == "city") ? "city" : "country" 
  time_zone = (options[:time_zone] == true ) ? true : false

  # формируем данные для запроса
  params = {} 
  params[:key] = self.api_key
  params[:ip]  = ip
  params[:timezone] = time_zone
  params[:format] = "json"

  # отправляем запрос и передаем в Ipinfodb::API::Parser
  response = self.class.get("#{type}/", query: params)
  parse_response(response)
end

Дальше в парсере не происходит ничего необычного, по этому мы его сейчас опустим.
Далее было переименование в 4816ad затем были не большие баг фиксы.
Также я добавил RSpec для тестирования и добавил Travis CI: aa42e 

Как тестировать gem?

Как известно в мире ruby тестируют все и вся, и я ничем не хуже 😉
Через некоторое время на свет родились первые рабочие тесты: 5d6b1
Тесты очень простые но для начала самый раз. Первый же тест дал понять что проверку ключа нужно делать при инициализации а не когда уже запрос отправляется.

Однако не все так сладко как казалось. Меня интересовало как тестировать запросы к API используя ключ? Хранить свой валидный API_KEY в тестах как-то не очень, совсем не очень. Тогда пришло озарение что ключи к всем API все ровно хранят в переменных окружения, так почему бы мне не использовать ее?
Эта мысль родила новый коммит: cfc08b

Тестирование запросов к удаленному API

Первое что я сделал добавил три development_dependency:

  spec.add_development_dependency 'vcr', '~> 2.9', '>= 2.9.3' # для эмуляции http запросов, детальней смотри документацию
  spec.add_development_dependency 'webmock', '~>1.21'
  spec.add_development_dependency 'dotenv', '~>2.0' # для доступа к переменым окружения

Теперь нужно немножко изменить метод инициализации в api.rb :

    def initialize()
      self.api_key ||= ENV['IP_INFO_KEY']
      raise ApiKeyError.new("Error! Add your API key") if api_key.nil?
    end

Дело осталось за малым, сделать конфиг для RSpec и добавить конфигурационный файл для vcr

require_relative '../lib/ipinfo' # load gem

require 'dotenv'
require 'vcr'
require 'support/vcr'

Dotenv.load

 

Затем добавляем файл конфигурации support/vcr.rb:

VCR.configure do |config|
  config.cassette_library_dir = 'spec/cassettes'
  config.hook_into :webmock
  config.filter_sensitive_data('api_key') { ENV["IP_INFO_KEY"] }
end

RSpec.configure do |c|
  c.around(:each, :vcr) do |example|
    name = example.metadata[:full_description].split(/\s+/, 2).join("/").downcase.gsub(/\s+/,"_")
    VCR.use_cassette(name) { example.call }
  end
end

Здесь нету ничего сложного, если буду вопросы задавайте в комментарии.  Теперь чтобы стабить запросы к спекам нужно добавлять :vcr выглядит это так:

  it "check type of request", :vcr do    
    expect(ip_info.lookup(ip)).to be_kind_of(Hash)
  end

Первый раз vcr оправляет запрос и результат сохраняет  в папке config.cassette_library_dir и при следующих запросах использует уже сохраненные результаты. А дальше понеслась рутинная работа…

Ничего нового и интересного эти коммиты не принесли 3е61 и 9579  но посмотреть их стоит, может что-то интересное для себя найдете.
В принципе это все, в создании гемов нету ничего необычного. Но это очень интересно и увлекательно.

Загружаем свой гем на RubyGems.org

Процедура загрузки гема просто на удивление проста. Все что нужно это за регистрироваться на сайте. И выполнить команду которая добавит ваш API токен который нужен для загрузки гема. После этого нужно добавить команду

 gem push <gem_name>-<version>.gem

Когда я выполнял эту команду, у меня все время все фейлилось и я не понимал почему. Все оказалось очень просто, ошибки были потому что на rubygems уже был гем с таким названием. И мне в очередной раз пришлось переименовывать. И все. Готово! Смотрите свой профайл, там будет ваш новенький прекрасный гем. 

Заключение

В итоге хотел бы сказать что это была отличная практика! Может даже кому-то будет полезно мое творение 🙂
На по следок я поделюсь ссылками где вы можете найти мое творение:

Весь код хранится здесь: max-si-m/ip_info
А сам гем можно попробовать здесь: gems/ip_info

Всем добра! 😉