Создаем веб-сервисы на рельсах!

Ввиду ограниченности доступной документации по использованию набирающего популярность фреймворка Ruby-on-Rails, я решил опубликовать собственный опыт по использованию RoR для создания веб-сервисов.
В этой  статье мы будем создавать сервис учета и выдачи телефонных номеров из имеющегося пула. 
Для этого мы будем использовать пакет ActionWebService, входящий в состав рельс. По ходу реализации будет создан веб-сервисный интерфейс, создана база номеров и даже будет проведена проверка совместимости созданного веб-сервиса с внешней системой Sonic ESB, используемой для оркестровки различных сервисов. 
 Итак, приступим:

1. Обновляем версии пакетов
D:\InstantRails\rails_apps\aws>gem update actionwebservice --include-dependenciesUpdating installed gems...Attempting remote update of actionwebserviceSuccessfully installed actionwebservice-1.2.3Installing ri documentation for actionwebservice-1.2.3...Installing RDoc documentation for actionwebservice-1.2.3...Gems: [actionwebservice] updatedD:\InstantRails\rails_apps>gem update rails --include-dependenciesUpdating installed gems...Bulk updating Gem source index for: http://gems.rubyforge.orgAttempting remote update of railsSuccessfully installed rails-1.2.3Gems: [rails] updated
Если что-то будет глючить, можно посмотреть вот эту статью  
2. Создаём новое приложение
D:\InstantRails\rails_apps>rails aws      create      create  app/controllers      create  app/helpers      create  app/models      create  app/views/layouts      create  config/environments      create  components      create  db      create  doc      create  lib      create  lib/tasks      create  log      create  public/images      create  public/javascripts      create  public/stylesheets      create  script/performance      create  script/process      create  test/fixtures      create  test/functional      create  test/integration      create  test/mocks/development      create  test/mocks/test      create  test/unit      create  vendor      create  vendor/plugins      create  tmp/sessions      create  tmp/sockets      create  tmp/cache      create  tmp/pids      create  Rakefile      create  README      create  app/controllers/application.rb      create  app/helpers/application_helper.rb      create  test/test_helper.rb      create  config/database.yml      create  config/routes.rb      create  public/.htaccess      create  config/boot.rb      create  config/environment.rb      create  config/environments/production.rb      create  config/environments/development.rb      create  config/environments/test.rb      create  script/about      create  script/breakpointer      create  script/console      create  script/destroy      create  script/generate      create  script/performance/benchmarker      create  script/performance/profiler      create  script/process/reaper      create  script/process/spawner      create  script/process/inspector      create  script/runner      create  script/server      create  script/plugin      create  public/dispatch.rb      create  public/dispatch.cgi      create  public/dispatch.fcgi      create  public/404.html      create  public/500.html      create  public/index.html      create  public/favicon.ico      create  public/robots.txt      create  public/images/rails.png      create  public/javascripts/prototype.js      create  public/javascripts/effects.js      create  public/javascripts/dragdrop.js      create  public/javascripts/controls.js      create  public/javascripts/application.js      create  doc/README_FOR_APP      create  log/server.log      create  log/production.log      create  log/development.log      create  log/test.log
 
3. Разрабатываем дизайн сервиса
Сервис будет использоваться внешним ИТ-приложением через SOAP-интерфейс. А также сервис должен иметь административный интерфейс, через который можно было бы завести пул номеров, удалить или отредактировать их значения.
Операции, которые будут использоваться внешним приложением:

  • запрос доступных номеров, сервис должен предложить доступный номер
  • резервирование номера, что значит что данный номер принадлежит определенному абоненту
  • снятие резервирование с номера

Для идентификации абонента используется текстовая строка, например, содержащая URI от OpenID абонента 
 
4. Создаем описание сервисов
Создаем web_service с методами suggest, allocate и free: 
D:\InstantRails\rails_apps\aws>ruby script/generate web_service Telnum suggest allocate free      exists  app/apis/      exists  app/controllers/      exists  test/functional/      create  app/apis/telnum_api.rb      create  app/controllers/telnum_controller.rb      create  test/functional/telnum_api_test.rbФайл telnum_api.rb содержит описание интерфейса Telnum API
class TelnumApi < ActionWebService::API::Base  api_method :suggest  api_method :allocate  api_method :freeend
5. Пишем реализацию сервисов
Сначала уточняем методы интерфейса, указывая типы параметров принимаемых на входе и выдаваемых на выходе. 
Редактируем файл telnum_api.rbclass TelnumApi < ActionWebService::API::Base  api_method :suggest,            :expects => [:string],            :returns => [:string]  api_method :allocate,            :expects => [:string, :string],            :returns => [:int]  api_method :free,            :expects => [:string, :string],            :returns => [:int]endДалее редактируем контроллер telnum_controller.rb, пока что реализуя функциональность в виде заглушек, а также указываем привязку к TelnumAPI.
class TelnumController < ApplicationController  wsdl_service_name 'Telnum'    web_service_api TelnumApi  def suggest(openid)      return "1230001"  end  def allocate(openid, phone)      return 1  end  def free(openid, phone)      return 1  endendТаким образом, сейчас реализация нашего сервиса всегда выдает успешный ответ на запросы о резервировании и освобождении номера phone абонентом openid. А также на запрос доступных номеров всегда предлагает 1230001.
Для того чтобы проверить получившийся API, воспользуемся инструментом в составе пакета ActionWebService, который создает html-интерфейс для работы с веб-сервисом. Для этого в классе контроллера указываем web_service_scaffold:
 class TelnumController < ApplicationController  wsdl_service_name 'Telnum'    web_service_api TelnumApi    web_service_scaffold :invoke  def suggest(openid)      return "1230001"  end…
6. Первый запуск сервиса
D:\InstantRails\rails_apps\aws>ruby script/server=> Booting Mongrel (use 'script/server webrick' to force WEBrick)=> Rails application starting on http://0.0.0.0:3000=> Call with -d to detach=> Ctrl-C to shutdown server** Starting Mongrel listening at 0.0.0.0:3000** Starting Rails with development environment...** Rails loaded.** Loading any Rails specific GemPlugins** Signals ready.  INT => stop (no restart).** Mongrel available at 0.0.0.0:3000** Use CTRL-C to stop.
7. Проверка работоспособности
Открываем в браузере http://127.0.0.1:3000/Telnum/invoke

Вызываем метод suggest с текстовым параметром:  

Получаем ответ и видим какой был послан запрос и какой получен ответ в SOAP: 

 
Также можно посмотреть описание веб-сервиса в виде WSD по ссылке http://127.0.0.1:3000/Telnum/wsdl

 
8.Использование сервиса из внешней системы
 Из-за большого количества скриншотов  вынес в отдельную статью:
Подключение веб-сервиса в сервисной шине Sonic ESB
9. Разделяем контроллер и сервис
Итак, мы убедились в работоспособности сервиса с точки зрения поддержки протокола SOAP.
Теперь надо и функциональность реализовать. 
Для начала рекомендую разделять функциональность сервиса и контроллер, который обуспечивает работу SOAP-методов. Для этого мы создадим класс TelnumService, а в контроллере будем использовать его инстанс. 
Создаем файл /apps/apis/telnum_service.rb и помещаем туда наши функции из контроллера. class TelnumService < ActionWebService::Base  web_service_api TelnumApi    def suggest(openid)      return "1230001"  end  def allocate(openid, phone)      return 1  end  def free(openid, phone)      return 1  endend
Теперь, когда функциональность перенесена в класс TelnumService, создаем его инстанс в контроллере, а также указываем что режим работы с сервисом теперь у нас многоуровневый. 
class TelnumController < ApplicationController    web_service_dispatching_mode :layered      wsdl_service_name 'Telnum'      web_service_scaffold :invoke      web_service :telnum, TelnumService.new  end
10. Создаем модель для БД
Наконец-то сделаем шаг, который давно уже напрашивался — создадим таблицу для хранения телефонных номеров и их резервирований.
Будем использовать класс Telnum,  который будет автоматически сохраняться в таблице telnums.
D:\InstantRails\rails_apps\aws>ruby script/generate model telnum      exists  app/models/      exists  test/unit/      exists  test/fixtures/      create  app/models/telnum.rb      create  test/unit/telnum_test.rb      create  test/fixtures/telnums.yml      create  db/migrate      create  db/migrate/001_create_telnums.rb
Кроме прочего, у нас создался файл  app/models/telnum.rb с классом Telnum, а также  db/migrate/001_create_telnums.rb, в котором мы можем прописать привязку к столбцам таблицы.
class CreateTelnums < ActiveRecord::Migration  def self.up    create_table :telnums do |t|        t.column :phone, :string        t.column :openid, :string    end  end  def self.down    drop_table :telnums  endend
Для того, чтобы создать таблицу с указанными столбцами используем механизм миграций, заложенный в фрймворк Ruby-on-Rails:
D:\InstantRails\rails_apps\aws>rake db:migrate(in D:/InstantRails/rails_apps/telnum) == CreateTelnums: migrating ==================================================-- create_table(:telnums)   -> 0.0310s== CreateTelnums: migrated (0.0310s) =========================================
Видим, что таблица успешно создана.  
 
11. Создаем CRUD-методы для заполнения значений telnum
Теперь необходимо реализовать административный интерфейс, позволяющий создавать, читать, редактировать и удалять объекты Telnum. 
Не будем сильно мудрствовать и создавать сложный интерфейс, а возмользуемся инструментом скаффолдинга, который для нашей модели (класса Telnum), создаст набор CRUD-методов (create, read, update, delete).
D:\InstantRails\rails_apps\aws>ruby script/generate scaffold telnum      exists  app/controllers/      exists  app/helpers/      create  app/views/accounts      exists  app/views/layouts/      exists  test/functional/  dependency  model      exists    app/models/      exists    test/unit/      exists    test/fixtures/   identical    app/models/telnum.rb   identical    test/unit/telnum_test.rb   identical    test/fixtures/telnums.yml      create  app/views/telnums/_form.rhtml      create  app/views/telnums/list.rhtml      create  app/views/telnums/show.rhtml      create  app/views/telnums/new.rhtml      create  app/views/telnums/edit.rhtml      create  app/controllers/telnums_controller.rb      create  test/functional/telnums_controller_test.rb      create  app/helpers/telnums_helper.rb      create  app/views/layouts/telnums.rhtml      create  public/stylesheets/scaffold.css
Проверим, что получилось.
Для этого стартуем наш сервис
D:\InstantRails\rails_apps\aws>ruby script/server --port=3001Теперь через веб-интерфейс заводим несколько номеров, чтобы заполнить пул.

 

 
12. Создаем более читаемый wsdl для сервера
Сделаем небольшое улучшение WSDL, для большей читаемости. 
Для этого в описание в файле telnum_api.rb внесем названия параметров 
class TelnumApi < ActionWebService::API::Base  api_method :suggest,               :expects => [{:openid=>:string}],               :returns => [{:phones=>:string}]  api_method :allocate,               :expects => [{:openid=>:string}, {:phone=>:string}],               :returns => [{:resultcode=>:int}]  api_method :free,               :expects => [{:openid=>:string}, {:phone=>:string}],               :returns => [{:resultcode=>:int}]end
13. Связываем сервис с моделью
Кажется мы забыли самое главное — реализовать функциональность, которую мы вынесли в TelnumService.

  • Добавляем поиск свободных номеров по базе
  • Делаем резервирование, путем внесения идентификатора абонента в поле openid для выбранного номера
  • Выполняем освобождение зарезервированного номера

class TelnumService < ActionWebService::Base  web_service_api TelnumApi    def suggest(openid)      t = Telnum.find_by_openid("")      return t.phone  end  def allocate(openid, phone)      t = Telnum.find_by_phone(phone)      t.openid = openid      t.save          return 1  end  def free(openid, phone)      t = Telnum.find_by_phone(phone)      t.openid = ""      t.save          return 1  endend
14. Добавляем проверки корректности данных
Заметим, что если объект не был найден в базе, то мы обратимся к nil, и наш сервис обязательно упадет.
Добавим проверки данных:  
class TelnumService < ActionWebService::Base  web_service_api TelnumApi    def suggest(openid)      t = Telnum.find_by_openid("")      return t.phone  end  def allocate(openid, phone)      t = Telnum.find_by_phone(phone)      if t == nil          return 0     else                t.openid = openid        t.save            return 1    end  end  def free(openid, phone)      t = Telnum.find_by_phone(phone)      if t == nil          return 0      else        t.openid = ""        t.save              return 1      end    endendВсё хорошо, только наш сервис не следит за тем
чтобы резервируемый номер был свободен или что освобождаемый номер
принадлежит тому абоненту, который решил от него отказаться.
class TelnumService < ActionWebService::Base  web_service_api TelnumApi    def suggest(openid)      t = Telnum.find_by_openid("")    return t.phone  end  def allocate(openid, phone)      t = Telnum.find_by_phone(phone)      if t == nil        return 0      else                if t.openid == ""            t.openid = openid            t.save                  return 1          else              return 0          end      end  end  def free(openid, phone)      t = Telnum.find_by_phone(phone)      if t == nil        return 0     else        if t.openid == openid            t.openid = ""            t.save                  return 1          else              return 0          end      end    endend

Сервис готов!