Как все начиналось
Это мой первый 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
И так, что я сделал:
- Добавил spec.summary – без него гем отказывался собираться.
- Добавил в dependency httparty, чтобы делать запросы к API
spec.add_dependency "httparty", "~> 0.13.5"
- Уже на рисовалась первая архитектура приложения:
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
Всем добра! 😉