Учим Ruby вместе! Урок 1

// Май 20th, 2011 // Ruby, Веб-разработка

«Привет! Чем занимаешься?» — «Да так, одну штуку на руби программлю.» — «Это заразно :-)»
Вот такой диалог состоялся у меня сегодня с одним приятелем. Почему ruby?

 

Зачем учить Ruby

Этот вопрос касается тех, кто уже долго и успешно программирует на PHP. Вы освоились в одном языке, это здорово, но не надо останавливаться на достигнутом. Многие могут возразить, что они знают язык, ориентируются в нёи и уже изучили все грабли с ним. Я назову несколько причин для изучения Ruby.

  1. Любопытство. Мне, например, было очень интересно поработать с открытыми классами. Вот так вот взять и инжектировать свой метод в системный класс — по моему это здорово. А не будет ли путаницы? А как это сделать? Вообщем интересно всё новое.
  2. Ruby vs PHP. Т.к. я давно программлю на PHP, мне интересно чем же Ruby может похвастаться перед PHP/
  3. Скорость работы Ruby. На Ruby сделан твиттер (хотя в последнее время от него и отказались). Хочется в реальности проверить его производительность.
  4. Класс решаемых задач. Руби хорош для веб-приложений. А так ли это?
  5. Метапрограммирование. Пожалуй самая главная причина.

Как изучать Ruby. Учебный план.

Вот здесь моей первой ошибкой было то, что я начал изучать фреймворк не зная язык. Теперь я понял, что так делать не надо. Забыв на время о Ruby on Rails я начал изучать сам Ruby, благо в тикет-системе долгое время висела системная задача, которую на php решать было тяжело. Уж очень не хотелось давать ему права root :-) Итак план такой.

  1. Установка Ruby, настройка окружения.
  2. Общий синтаксис. Типы данных Ruby.
  3. Функции, классы, открытые классы. Атрибуты (аксессоры и мутаторы).
  4. Работа со строками, с массивами. Поиск и замена подстрок и т.д. Преобразование типов.
  5. Работа с файлами.
  6. Работа с системным окружением.
  7. Оформление приложения, работа с гемами (модулями).
  8. Работа с БД.
  9. Установка Ruby on Rails, создание первого приложения.

В этом посте я опубликую своё первое приложение на Ruby и приглашаю всех желающих к обсуждению. Указывайте на ошибки, предлагайте best practice, задавайте вопросы.

Давайте учить Ruby вместе!

Урок 1. Первое приложение на Ruby.

Задача стоит такая. Есть DNS сервер на хостинге и надо при вызове консольной утилиты на ruby добавлять зону для домена а также запись зоны в список зон (domains.list) и изменять одну запись в БД, куда прописывать этот домен. Настройки доступа к БД хранятся в php приложении, а конкретно в его INI файле. После всех действий надо перезагрузить DNS сервер (bind).

Рабочая среда для Ruby

В качестве IDE буду использовать RubyMine от JetBrains. Уж очень мне понравился их доклад на последнем ZFConf. Хоть он и был про phpStorm, но качество видно сразу. Руби ставим через RVM сначала для всех юзеров, потом настраиваем для рута и своего юзера.

Расширение открытых классов Ruby

Для работы с INI файлами в Ruby используем gem inifile. Но в нём есть небольшая проблема. В ZF INI файле можно спокойно использовать константы, и строки получаются такого вида:

includePaths[] = APPLICATION_PATH "/../vendors/Doctrine/"

Вот от APPLICATION_INI то и сносит парсер гема. А конкретно то, что эта строка не подходит ни под один паттерн:

    @rgxp_comment = %r/\A\s*\z|\A\s*[#{@comment}]/
    @rgxp_section = %r/\A\s*\[([^\]]+)\]/o
    @rgxp_param   = %r/\A([^#{@param}]+)#{@param}\s*"?([^"]*)"?\z/

Вот и ситуация для применения открытых классов. Заменим фунуию IniFile::parse на свою. Все дополнения я буду складывать в файл fucntion.rb

class IniFile
    private
  #
  # call-seq
  #    parse
  #
  # Parse the ini file contents.
  #
  def parse
    return unless File.file?(@fn)

    section = nil
    tmp_value = ""
    tmp_param = ""

    fd = (RUBY_VERSION >= '1.9' && @encoding) ?
         File.open(@fn, 'r', :encoding => @encoding) :
         File.open(@fn, 'r')

    while line = fd.gets
      line = line.chomp

      # mutline start
      # create tmp variables to indicate that a multine has started
      # and the next lines of the ini file will be checked
      # against the other mutline rgxps.
      if line =~ @rgxp_multiline_start then

        tmp_param = $1.strip
        tmp_value = $2 + "\n"

      # the mutline end-delimiter is found
      # clear the tmp vars and add the param / value pair to the section
      elsif line =~ @rgxp_multiline_end && tmp_param != "" then

        section[tmp_param] = tmp_value + $1
        tmp_value, tmp_param = "", ""

      # anything else between multiline start and end
      elsif line =~ @rgxp_multiline_value && tmp_param != ""  then

        tmp_value += $1 + "\n"

      # ignore blank lines and comment lines
      elsif line =~ @rgxp_comment then

        next

      # this is a section declaration
      elsif line =~ @rgxp_section then

        section = @ini[$1.strip]

      # otherwise we have a parameter
      elsif line =~ @rgxp_param then

        begin
          section[$1.strip] = $2.strip
        rescue NoMethodError
          raise Error, "parameter encountered before first section"
        end

      elsif line =~ %r/APPLICATION_/ then
         next

      else
        raise Error, "could not parse line '#{line}"
      end
    end  # while

  ensure
    fd.close if defined? fd and fd
  end
end

Также я расширю класс String, чтобы можно было валидировать домены.

class String
  def valid_domain_name?
    domain_name = self.split(".")
    name = /(?:[A-Z0-9\-][0-9a-z.+][0-9a-z]+)+/.match(domain_name[0]).nil?
    tld = /(?:[A-Z]{2}|aero|ag|asia|at|be|biz|ca|cc|cn|com|de|edu|eu|fm|gov|gs|jobs|jp|in|info|me|mil|mobi|museum|ms|name|net|nu|nz|org|tc|tw|tv|uk|us|vg|ws)/.match(domain_name[1]).nil?
    (domain_name.count > 1 and name != false and tld != false)
  end
end

 

Исходники

Ну а теперь покажу вам собственно исходники.
index.rb

#coding: utf-8

require "mysql2"
require "socket"
require "inifile"
require "./functions.rb"

# Хэш адресов серверных машин
hosts = {
    :production => '83.168.22.1',
    :test       => '84.22.11.1'
}

util = Util.new(hosts)
util.releative_config_path='/site.com/application/config/application.ini'

# Проверка параметров
quit if (ARGV.count != 2)
domain = ARGV[0]
hostname = ARGV[1].split(".")[0]
quit('Invalid domain name') if (not domain.valid_domain_name?)

# Поиск компаниив БД
result = Mysql2::Client.new(util.get_db_settings).query("SELECT id  FROM `sites`   WHERE `hostname` = '#{hostname}'")
quit('Company not found') if result.count != 1

# Обновление её hostname
rows = Array.new
result.each{|row| rows << row}
company_id = rows[0]['id']
result = Mysql2::Client.new(util.get_db_settings).query("UPDATE `dbname`.`sites` SET `domain` = '#{domain}' WHERE `dao_companies`.`id` =#{company_id};")

# Добавление зоны
bind_config_path = '/etc/bind'
default_zone_file = bind_config_path + '/zones/DEFALT'
new_zone_file = bind_config_path + "/zones/#{domain}.zone"
zones_list_file = bind_config_path + "/domains.lst"
quit('File with default zone does not exists') unless File.exist?(default_zone_file)
quit('File with zones list does not exists') unless File.exist?(zones_list_file)

zone = IO.read(default_zone_file).gsub('SERIAL',Time.now.strftime("%Y%m%d%S")).gsub('DOMAIN', domain)
if not File.exist?(new_zone_file) then
  File.open(new_zone_file, "w") {|f| f.puts(zone) }
else
  quit('Domain '+domain+' zone already exists!')
end

# Добавление зоны в список
zone = "zone \"#{domain}\" {
	type master;
	file \"/etc/bind/zones/#{domain}.zone\";
};"
if not IO.read(zones_list_file).include?(domain) then
  File.open(zones_list_file, "a") {|f| f.puts(zone) }
end

# Перезапуск сервисов (bind9)
system("service bind9 restart")

puts "Completed"

Gemfile
В этом файле описываются зависимости проекта.

source :rubygems

gem 'mysql2', '0.2.6'
gem 'inifile'

Ну и собственно включаемые функции.
functions.rb

#coding: utf-8

class String
  def valid_domain_name?
    domain_name = self.split(".")
    name = /(?:[A-Z0-9\-][0-9a-z.+][0-9a-z]+)+/.match(domain_name[0]).nil?
    tld = /(?:[A-Z]{2}|aero|ag|asia|at|be|biz|ca|cc|cn|com|de|edu|eu|fm|gov|gs|jobs|jp|in|info|me|mil|mobi|museum|ms|name|net|nu|nz|org|tc|tw|tv|uk|us|vg|ws)/.match(domain_name[1]).nil?
    (domain_name.count > 1 and name != false and tld != false)
  end
end

class IniFile
    private
  #
  # call-seq
  #    parse
  #
  # Parse the ini file contents.
  #
  def parse
    return unless File.file?(@fn)

    section = nil
    tmp_value = ""
    tmp_param = ""

    fd = (RUBY_VERSION >= '1.9' && @encoding) ?
         File.open(@fn, 'r', :encoding => @encoding) :
         File.open(@fn, 'r')

    while line = fd.gets
      line = line.chomp

      # mutline start
      # create tmp variables to indicate that a multine has started
      # and the next lines of the ini file will be checked
      # against the other mutline rgxps.
      if line =~ @rgxp_multiline_start then

        tmp_param = $1.strip
        tmp_value = $2 + "\n"

      # the mutline end-delimiter is found
      # clear the tmp vars and add the param / value pair to the section
      elsif line =~ @rgxp_multiline_end && tmp_param != "" then

        section[tmp_param] = tmp_value + $1
        tmp_value, tmp_param = "", ""

      # anything else between multiline start and end
      elsif line =~ @rgxp_multiline_value && tmp_param != ""  then

        tmp_value += $1 + "\n"

      # ignore blank lines and comment lines
      elsif line =~ @rgxp_comment then

        next

      # this is a section declaration
      elsif line =~ @rgxp_section then

        section = @ini[$1.strip]

      # otherwise we have a parameter
      elsif line =~ @rgxp_param then

        begin
          section[$1.strip] = $2.strip
        rescue NoMethodError
          raise Error, "parameter encountered before first section"
        end

      elsif line =~ %r/APPLICATION_/ then
         next

      else
        raise Error, "could not parse line '#{line}"
      end
    end  # while

  ensure
    fd.close if defined? fd and fd
  end
end

def quit(message=nil)
  banner = "
   ============================
  |      DNS Addition tool     |
   ============================
  Usage: ruby ./index.rb domain.com olddomain.site.com"
  if not message.nil? then banner = message end
  puts banner
  exit
end

class Util

  attr_accessor :hosts, :releative_config_path, :environment

  def initialize(hosts =Array.new)
    self.hosts = hosts
  end

  # Получение локального IP-адреса
  def local_ip
    orig, Socket.do_not_reverse_lookup = Socket.do_not_reverse_lookup, true  # turn off reverse DNS resolution temporarily

  UDPSocket.open do |s|
      s.connect '64.233.187.99', 1
      s.addr.last
    end
  ensure
    Socket.do_not_reverse_lookup = orig
  end

  #  Получение среды окружения
  def get_environment
    if @environment.nil? then
      hosts = self.hosts.invert
      if(hosts.include?(self.local_ip)) then
        @environment = hosts[self.local_ip]
      else
        @environment = 'development'
      end
    else
      @environment.to_s
    end
  end

  def get_config_path
    local_username = get_local_username
    '/home/'+local_username+'/sandbox'+self.releative_config_path
  end

  # Возвращает имя пользователя, в случае если утилита запущена через rvmsudo или напрямую
  def get_local_username
    if ENV['SUDO_USER'].nil?
      quit("Util should be run over rmvsudo, \r\nexample: rvmsudo ruby ./index.rb domain.ru some.subdomain.ru")
    else
      ENV['SUDO_USER']
    end
  end

  def get_db_settings
    config = IniFile::load(self.get_config_path)
    section_name = self.get_environment.to_s + ' : bootstrap'
    quit('No suitable section in config file') unless config.has_section?(section_name)
    dsn = config.to_h[section_name]['resources.doctrinedata.connections.default.dsn']

    # Parse dsn
    dsn.sub!('mysql://', '')
    arr = dsn.split('@')
    dbconfig = {
      :username => arr[0].split(':')[0],
      :password => arr[0].split(':')[1],
      :host     => arr[1].split('/')[0],
      :database   => arr[1].split('/')[1]
    }
  end

end

А как же PHP?

Этот пост не сводится к тому, что надо бросить PHP и начать изучать руби. PHP — самый популярный язык веб-программирования, на нём реализованы тысячи интересных вещей и алгоритмов, в т.ч. даже и нейросети. И я его люблю) За многие годы, можно сказать, что я с ним сроднился, несмотря на все его недостатки. Но это не значит, что не надо изучать для себя что-то новое.

Что почитать. Книги по Ruby

Мне в последнее время задают вопрос, по каким книжкам учить Ruby. Я сейчас читаю вот эту.

Эта книга — официальное руководство по динамическому языку программирования Ruby. Авторский состав воистину звездный: Дэвид Флэнаган — известнейший специалист в области программирования, автор ряда бестселлеров по JavaScript и Java; Юкихиро «Matz» Мацумото — создатель и ведущий разработчик Ruby.
В книге приведено детальное описание всех аспектов языка: лексической и синтаксической структуры Ruby, разновидностей данных и элементарных выражений, определений методов, классов и модулей. Кроме того, книга содержит информацию об API-функциях платформы Ruby.

Издание будет интересно опытным программистам, знакомящимся с новым для себя языком Ruby, а также тем, кто уже программирует на Ruby и хочет достичь более высокого уровня понимания и мастерства работы. Найти книжку можно на озоне или библио-глобусе.

Жду ваших комментов по коду и любые интересные идеи!)

Share

Спасибо!


Если вам помогла статья, или вы хотите поддержать мои исследования и блог - вот лучший способ сделать это:


3 Responses to “Учим Ruby вместе! Урок 1”

  1. В свое время делал выбор между Ruby и Python уже кодя на PHP (кстати про грабли тоже в блоге писал). Выбрал Python, по многим параметрам. Быстрее, старше, веб-фреймворков несколько, системных скриптов в Ubuntu на нем множество, Google его юзает вовсю (разработчик Python сейчас там работает). Ruby интересный конечно, но из практических соображений пришлось отказаться…

  2. Andy:

    В данном конкретном случае нельзя расширять класс строк методом valid_domain_name?, т.к. это не совсем укладывается в парадигму ООП. Этот метод имеет узкую специализацию, поэтому, логичнее было бы вынести его в отдельный класс или модуль, либо же определять этот метод в конкретном инстансе строки, а не во всех строках подряд.

    Ну и реально, код написан в php-style, здесь не видно «красоты» ruby :)

Комментировать