02.09.2009Автоподстановка задач rake в терминале с помощью ruby
Введение
Периодически натыкаюсь на хвалебные отзывы о zsh и о совершенной необходимости перехода с bash на него всему прогрессивному человечеству. В качестве демонстрации удобства демонстрируют лёгкость создания скриптов автоподстановки, например для задач rake. Неужели из-за этого следует сменить оболочку терминала?
Задача
Найти решение для автоподстановки задач rake с помощью bash несложно. Для этого используется complete. Огромное число примеров можно посмотреть у себя же в системе в /etc/bash_completion.
При ближайшем рассмотрении выяснилось, что complete может использовать не только функцию, но и команду. То есть отдельный запускаемый скрипт, который может быть написан и на руби в том числе.
Можно ли из этого извлечь пользу?
Решение
Итак, чтобы подключить наш скрипт, нужно добавить в файл ~/.bashrc такую строчку.
complete -C ~/.bash/autocomplete/rake_complete -o default rake
Для начала положим по адресу ~/.bash/autocomplete/rake_complete элементарную реализацию автоподстановки, которая будет запускать rake и фильтровать результат.
Следует знать два момента:
- Введенная строка, после которой была нажата табуляция, находится в ENV["COMP_LINE"].
- В задачах могут попасться пространства имён. Поэтому из окончательного результата нужно убрать часть введенной строки, содержащей двоеточие.
Элементарная реализация выглядит следующим образом:
#!/usr/bin/env ruby
class RakeComplete
def initialize(cmd)
@command = cmd[ /\s(.+)$/, 1 ] || ""
end
def search
`rake -T`.split("\n")[1..-1].map{ |l| l.split(/\s+/)[1] }.select{ |cmd| cmd[0, @command.length] == @command }.map{ |cmd| cmd.gsub(cmd_before_column, "") }
end
private
def cmd_before_column
@command[ /.+\:/ ] || ""
end
end
puts RakeComplete.new(ENV["COMP_LINE"]).search
exit 0
Вроде бы всё понятно: из результата вывода rake -T , отбросив паразитную первую строчку, вытаскиваем только названия задач, подбираем те, начало который совпадает с введенным названием задачи, выводим массив, предварительно удалив у элементов введённую до двоеточия часть. Но уж больно медленно работает. Конечно же, встает вопрос о кешировании.
Кеширование
Приведу сразу результат:
#!/usr/bin/env ruby
class RakeComplete
CACHE_NAME = ".rake_complete~"
def initialize(cmd)
@command = cmd[ /\s(.+)$/, 1 ] || ""
end
def search
exit 0 if rake_file.nil?
selected_tasks.map do |cmd|
cmd.gsub(cmd_before_column, "")
end
end
private
def cmd_before_column
@command[ /.+\:/ ] || ""
end
def rake_file
["Rakefile", "Rakefile.rb", "rakefile", "rakefile.rb"].detect do |name|
File.file?(File.join(Dir.pwd, name))
end
end
def cache_file
File.join(Dir.pwd, CACHE_NAME)
end
def generate_tasks
tasks = `rake -T`.split("\n")[1..-1].map{ |l| l.split(/\s+/)[1] }
File.open(cache_file, "w") do |f|
tasks.each do |task|
f.puts task
end
end
tasks
end
def cached_tasks
File.read(cache_file).split("\n")
end
def cache_valid?
File.exist?(cache_file) && (File.mtime(cache_file) >= File.mtime(rake_file))
end
def tasks
cache_valid? ? cached_tasks : generate_tasks
end
def selected_tasks
tasks.select do |cmd|
cmd[0, @command.length] == @command
end
end
end
puts RakeComplete.new(ENV["COMP_LINE"]).search
exit 0
Для самостоятельного изучения
Думаю, теперь не составит труда написать подобное для задач capistrano.
27.08.2009Приёмы работы с массивами и блоками в качестве аргументов в руби
Введение
Главная причина, по которой я испытываю нежные чувства к руби — это гибкость синтаксиса. Сегодня я хочу рассказать об особенностях передачи блоков и массивов в качестве параметров. Базовые вещи можно прочитать по ссылкам в последнем параграфе.
Массивы
О том, как принять неограниченное число аргументов, знают все:
$KCODE = "utf-8"
def my_args(*args)
puts args.inspect
end
my_args(1, 2, "собачка", "котик") # => [1, 2, "собачка", "котик"]
Но что, если сами объекты, которые необходимо передать в качестве аргументов, уже находятся в массиве по какой-то причине?
arr = ["собачка", "котик", "ёжик", "медвежонок"]
selection = arr.select{ |a| a.chars.to_a.length > 5 }
my_args(selection) # => [["собачка", "медвежонок"]]
Внезапно массив (args) стал двумерным, что неудивительно, т.к. метод интерпретировал переданный ему массив как первый аргумент типа Array. В данном случае, чтобы передать массив как список аргументов, нужно использовать ту же звезду:
my_args(*selection) # => ["собачка", "медвежонок"]
Ура!
Кстати, звезду можно использовать и при получении значения от функции. Например, в таком случае:
def get_values
["красный", "зелёный", "синий"]
end
r, g, b = get_values
puts r.inspect # => "красный"
puts g.inspect # => "зелёный"
puts b.inspect # => "синий"
r, gb = get_values
puts r.inspect # => "красный"
puts gb.inspect # => "зелёный"
r, *gb = get_values
puts r.inspect # => "красный"
puts gb.inspect # => ["зелёный", "синий"]
По-моему, здорово!
Блоки
Бывают случаи, когда блок нужно сохранить на будущее, чтобы потом его использовать. Тогда можно указать его в списке параметров метода, что, конечно же, не делает его обязательным само по себе.
class Button
def on_click(&block)
if block_given?
@stored_block = block
else
puts "Блока нет, но жизнь продолжается"
end
end
def click(*args)
@stored_block.call(*args)
end
end
my_btn = Button.new
my_btn.on_click # => Блока нет, но жизнь продолжается
my_btn.on_click do |*args|
puts "Произошёл клик!"
end
my_btn.click # => Произошёл клик!
Теперь предположим, опять же, что по каким-то причинам блок у нас уже есть. Он определён до вызова метода.
handler = lambda{ |*args| puts "Вот аргументы: #{args.inspect}" }
my_btn.on_click(handler) # => wrong number of arguments (1 for 0) (ArgumentError)
То есть наш блок превратился в аргумент типа Proc. А метод совсем этого не ожидал. Можно модифицировать сам метод, чтобы он выдерживал подобные условия, но не для этого я всё это пишу :) Если мы чему-нибудь научились в предыдущей главке, то решение по аналогии приходит само:
my_btn.on_click(&handler)
my_btn.click("усы", "хвост") # => Вот аргументы: ["усы", "хвост"]
Надеюсь, что было полезно.
Материалы для самостоятельного изучения
06.07.2009Упрощение работы с путями в руби
Введение
Недавно наткнулся на интересное решение объединения путей в питоне. Вспомнил, как недавно приходилось довольно много работать с файлами и путями. И решил подбить всё в одну библиотеку (конечно же, беззастенчиво позаимствовав такой способ объединения путей). В статье более подробно хочу остановиться на пути к текущему файлу.
Текущий путь
Довольно часто встречающаяся конструкция, после объединения путей, в моём случае — это:
File.dirname(__FILE__)
Если делать класс для работы с путями файлов, то он должен наследоваться от String, чтобы можно было сделать:
File.open(filepath)
А также должен уметь определять путь файла, в котором инициализируется или вызывается.
Начнём, конечно же, с тестов. Кроме всего прочего, я предпочитаю оперировать с развёрнутыми путями, т.к. если загружать библиотеку из разных мест, то пути могут не опознаваться как одинаковые, и интерпретатор ругается, например, на повторную инициализацию констант. Итак, тест с использованием RSpec:
describe FilePath do
it "should show correct current path" do
FilePath.current.should == File.expand_path(__FILE__)
end
end
Если использовать FILE внутри класса FilePath, то там окажется путь к файлу, в котором определяется класс.
Использование $0 так же не подходит, т.к. выдает путь только главного файла. В случае запуска теста $0 будет где-то в библиотеках.
Нам бы пригодилось что-нибудь вида:
eval("__FILE__", binding_of_caller)
Но binding_of_caller работало с помощью бага, который уже давно исправлен, а Binding.of_caller выглядит очень громоздко (можно там кликнуть на ссылочку Source). Мало того, что он ломает trace_func, так он требует, чтобы метод, в котором он используется, вызывали только внутри метода.
Можно ещё передавать внутрь метода пустой Proc, вытаскивая его binding, но требовать это от человека, использующего библиотеку для упрощения жизни, как-то нелепо.
Решение
На помощь спешит Kernel.caller, знакомый нам по трейсам ошибок. Если разобраться, как он работает, то решение приходит сразу:
caller(1)[0].split(":")[0]
Остальное можно посмотреть в исходниках file_path@github. Когда соберётся джем-библиотека, я обновлю инструкции и опубликую rdoc. Вдруг кому пригодится!
16.06.2009Управление фотокамерой с помощью руби
Введение
По заказу neq4 в рамках одного из проектов сделал библиотеку для работы с камерами. Точнее, это оболочка вокруг библиотеки на C (libgphoto2) для использования в руби.
Пришлось, конечно, изучать тему расширения руби с помощью C, но сегодня я хотел написать не об этом, а наоборот о том, как пользоваться библиотекой на руби. Что называется, программировал на C, чтобы другим не пришлось :)
Оболочка собрана в виде gem и работает на Linux и Mac OS. То есть везде, где работает gphoto2.
Установка
Для того, чтобы собрать gem, кроме средств сборки и версии руби для разработчиков, нужна установденная версия libgphoto2 для разработчиков:
sudo apt-get install ruby-dev build-essential libgphoto2-dev
Теперь можно ставить gem:
sudo gem i gphoto4ruby
Инициализация и настройки камеры
О том, какие камеры совместимы, и как их подключать для работы достаточно много написано у создателей gphoto2. Скажу лишь, что те камеры, с которыми я работал, для подключения нужно было переводить в PTP-mode. После подключения:
require "rubygems"
require "gphoto4ruby"
c = GPhoto2::Camera.new
Чтобы получить текущее значение настройки камеры и управлять состоянием конкретной настройки, нужно проделать примерно следующее.
cfg = c.config #{"capturemode" => "Single Shot",
# "exptime" => "0.125",
# "f-number" => "f/3.2", ...}
cfg.keys # возможные настройки
c["f-number", :all] # ["f/2.8", "f/3.2", "f/3.5", ...]
# возможные значения
c["f-number"] # текущее значение
c["f-number"] = "f/8" # вуаля! настройка на камере изменена
Не все настройки, имеющие несколько возможных значений, можно переключить с компьютера. Нельзя переключить то, для чего нужно что-то поворачивать или открывать на камере. Если вы что-то поменяли вручную на камере, то чтобы увидеть изменения параметра в объекте программы, нужно написать, например:
c["focusmode", :no_cache] # на Nikon D80 меняется переключателем
Съёмка
c.capture
c.capture.save.delete # сохранить снятую фотографию в текущую
# папку и удалить её с камеры
c.save :file => :last, :to_folder => "/home/sweet/home"
# сохранить последний из файлов в папку
c.save :file => c.files[3], :type => :preview, :new_name => "PREVIEW.JPG"
# загрузить превью фотографии под номером 4
# в списке файлов на карточке
При сохранении, если вы не делали съемку, могут быть тонкости связанные с тем, что нужно перейти в камере в нужную папку. Для этого есть ряд методов. Они хорошо описаны в документации по ссылке ниже.
Материалы для самостоятельного изучения
Библиотека libgphoto2 и инструмент для работы с камерами из командной строки gphoto2: есть FAQ. Документация оболочки gphoto4ruby: полный список классов и методов с примерами. Исходный код gphoto4ruby Расширение руби с помощью C
10.06.2009Ruby daemon, или как сделать демона на руби
Задача
Иногда в процессе работы с разными сторонними библиотеками, которые содержат тяжелые блокирующие методы, не обойтись обычным Thread. В таком случае на помощь приходят демоны :) Вот несколько приемов, которые я освоил в работе с нимим.
Базовый механизм
Запустить демона можно с помощью метода Kernel.fork, передав ему блок. Метод возвращает pid процесса, который можно записать в файл и просто использовать в дальнейшем.
pid = fork do
puts "from daemon"
exit!(1)
end
Определение статуса
Я не нашёл удобного способа определения методами руби, запущен ли процесс. Есть возможность фильтровать вывод ps ax, ища в нём pid процесса. Но есть метод Process.waitpid, который можно использовать хитрым образом. Так же для будущих задач, упакуем наш код в класс:
require 'timeout'
class Daemon
class << self
def start
@pid = fork do
puts "from daemon"
sleep 1
exit!(1)
end
end
def running?
if @pid
begin
Timeout::timeout(0.01) do
Process.waitpid(@pid)
if $?.exited?
return false
end
end
rescue Timeout::Error
end
return true
else
return false
end
end
end
end
Daemon.start
puts "running: #{Daemon.running?}"
sleep 1.5
puts "running: #{Daemon.running?}"
Сообщение об ошибках
Предположим, что у нашего демона есть некоторый процесс инициализации, и мы хотим знать, завершился ли он или произошла ошибка до того, как покинем метод Daemon.start.
Для передачи подобных сообщений хорошо подходит IO.pipe. Для двустороннего общения нужно создавать два канала, но нам хватит и одного:
require 'timeout'
class Daemon
def initialize
puts "from daemon: initializing"
end
class << self
def start
@rd, @wr = IO.pipe
@pid = fork do
@rd.close
begin
dmn = new
@wr.write "ok"
@wr.close
sleep 1
rescue Exception => e
@wr.write e.to_s
@wr.close
ensure
exit!(1)
end
end
@wr.close
str = @rd.read
if str == "ok"
puts "daemon started ok"
else
puts "error while initializing daemon: #{str}"
end
@rd.close
end
def running?
if @pid
begin
Timeout::timeout(0.01) do
Process.waitpid(@pid)
if $?.exited?
return false
end
end
rescue Timeout::Error
end
return true
else
return false
end
end
end
end
Daemon.start
puts "running: #{Daemon.running?}"
sleep 1.5
puts "running: #{Daemon.running?}"
Остановка
У каждого демона (для того они обычно и создаются) есть циклическая часть. Нам хотелось бы запускать и останавливать процесс тогда, когда нам нужно. Если с запуском всё понятно, то для остановки потребуется ещё одна вещь. Ведь в тот момент, когда мы создали демона, он создает копии всех переменных и дальнейшее их изменение внутри и снаружи демона становится независимым. Ещё один способ общения с демоном — сигнал:
require 'timeout'
class Daemon
def initialize
puts "from daemon: initializing"
@cnt = 0
end
def main_loop
@cnt += 1
puts "from daemon: running loop ##{@cnt}"
sleep 0.1
end
class << self
def start
@rd, @wr = IO.pipe
@pid = fork do
@rd.close
running = true
Signal.trap("TERM") do
running = false
end
begin
dmn = new
@wr.write "ok"
@wr.close
while running
dmn.main_loop
end
rescue Exception => e
@wr.write e.to_s
@wr.close
ensure
exit!(1)
end
end
@wr.close
str = @rd.read
if str == "ok"
puts "daemon started ok"
else
puts "error while initializing daemon: #{str}"
end
@rd.close
end
def stop
unless @pid.nil?
Process.kill("TERM", @pid)
@pid = nil
end
end
def running?
if @pid
begin
Timeout::timeout(0.01) do
Process.waitpid(@pid)
if $?.exited?
return false
end
end
rescue Timeout::Error
end
return true
else
return false
end
end
end
end
Daemon.start
puts "running: #{Daemon.running?}"
sleep 1
Daemon.stop
puts "running: #{Daemon.running?}"
Теперь, если наследовать от этого класса свой класс и переопределить методы initialize и main_loop, получится вполне себе демон :)
Для самостоятельного изучения
Есть, конечно, ещё недостатки. Например:
- Если ошибка возникает в main_loop, то канал вывода уже закрыт. А если не закрывать канал, то метод IO#read не позволит нам выйти из метода start. Что делать?
- Если нужно периодически общаться с демоном общирными объемами информации, что делать? (я в своей задаче использовал TCPSocket, но это, ведь, не панацея)
- Хорошо бы хранить pid в pid-файле на случай нашествия зомби. И, соответственно, обрабатывать возникающие зомбо-проблемы.
Но это я оставлю на самостоятельное решение пытливым читателям.
30.04.2009Тестирование Paperclip с помощью Factory Girl
Введение
Регулярно в своей работе я встречаюсь с новыми инструментами и решаю разные задачи. Иногда новые решения кажутся незначительными и слишком простыми для того, чтобы посвящать им целый пост в блоге. Но в нашем блоггерском деле главное — это регулярность. :)
Задача
Протестировать вновь изученный инструмент Paperclip с помощью вновь изученного инструмента Factory Girl.
Решение
Допустим, прикрепленный файл называется photo. То есть в модели написано следующее:
class Something < ActiveRecord::Base
has_attached_file :photo
end
Кладем файл с фото, например, в папку с fixtures. Тогда файл test/factories.rb (или spec/factories.rb) будет содержать:
Factory.define :something do |smth|
smth.photo File.new("path_to_file_in_fixtures", "rb")
end
И всё! Теперь в тестах, когда нужно создать объект пишем:
Factory.create(:something)
# или
Factory.build(:something)
А в тестах контроллера пишем:
Factory.attributes_for(:something)
Выводы
Если вы хотите написать проект за короткое ограниченное время, то лучше всего использовать инструменты, которые вы уже опробовали и знаете.
Если вы хотите изучить новые инструменты, то лучше всего сделать небольшой проектик с их использованием.
Проекты в первом и втором случае значительно отличаются по объему. :)
Материалы для изучения
20.04.2009Тестирование OpenID с помощью Cucumber
Задача
Какое-то время назад решил приобщиться к bdd (behavior driven development). С помощью такого подхода очень удобно описывать функционал приложения с точки зрения финального пользователя. И, соответственно, писать код так, чтобы выполнялись сценарии.
Для этого подхода существует прекрасный инструмент Cucumber, сценарии которого пишутся непосредственно текстом. Их, соответственно, можно даже обсуждать в качестве технического задания.
В качестве хорошего примера рассмотрим сценарий комментирования в этом блоге. Комментарии здесь можно оставлять только с помощью идентификации по OpenID.
Ресурсы
Нам понадобится, собственно, Cucumber, который для более лёгкого старта лучше использовать вместе с RSpec (фреймворк для удобного написания тестов и test driven development) и WebRat (инструмент для интегральных тестов, который содержит удобные методы работы с клиентским интерфейсом приложения).
Для работы с OpenID со стороны конечного пользователя нам понадобится локальный тестовый сервер OpenID. Есть хороший вариант: ROTS.
Затребуем библиотеки в тестовом окружении. В файле config/environments/test.rb:
config.gem "rspec", :lib => false, :version => ">=1.2.4"
config.gem "rspec-rails", :lib => false, :version => ">=1.2.4"
config.gem "webrat", :lib => false, :version => ">=0.4.4"
config.gem "cucumber", :lib => false, :version => ">=0.3.0"
config.gem "roman-rots", :lib => "rots", :version => ">=0.2.1"
Установим всё это:
sudo apt-get install libxml2-dev libxslt-dev
gem sources -a http://gems.github.com
rake gems:install RAILS_ENV=test
Решение
Предполагается, что как написать аутентификацию через OpenID мы знаем. Наша задача сейчас только протестировать функционал. В любом случае, когда мы приступаем к данному сценарию, у нас уже должны быть описаны и запрограммированы статьи и хотя бы создана модель для комментариев.
Теперь чтобы начать использовать Cucumber в приложении нужно запустить генератор:
script/generate cucumber
Процесс разработки в bdd выглядит так: сначала пишем сценарий, потом пишем всё остальное. Но в данном посте я просто покажу результат.
OpenID сервер
После установки библиотеки ROTS появляется консольная комманда:
rots
При запуске тестов необходимо, чтобы сервер был запущен. Для работы нам хватит параметров по умолчанию. В данном сервере уже есть пользователь, параметры для которого мы и будем использовать в тестах.
Сценарий Сucumber
Сценарии называются features. Создаем файл features/create_comments.feature:
Feature: Create Comments
In order to give feedback
As a reader
I want to create comments
Scenario: Create Comment
Given I visit a page for the published post with 1 approved comment
When I fill in "OpenID" with "http://localhost:1123/john.doe?openid.success=true"
And I fill in "Имя" with "John Doe"
And I fill in "Текст" with "Nice post!"
And I press "Отправить"
Then I should verify my OpenID
And I should see "John Doe"
And I should see "Nice post!"
And the post should have 2 approved comments
Многие из шагов уже определены с помощью WebRat и RSpec. Доопределим то, чего не хватает.
Шаги для статей
Тут есть моё внутреннее соглашение (с самим собой :). Когда я использую определенный артикль the, то имею в виду статью, которую я запоминаю в течение каждого сценария. В файле features/step_definitions/post_steps.rb:
Given /^I visit a page for the (published|draft) post with (\d+) (approved )?comments?$/ do |pub, cmnts_count, appr|
@post = Post.create!(:title => "Cucumber test post",
:body => "This is test post for cucumber comments",
:published => (pub == "published"))
cmnts_count.to_i.times do |cnt|
comment = @post.comments.build(:name => "Test User", :body => "Test body #{cnt}")
comment.openid_url = "http://test.example"
comment.approved = true unless appr.nil?
comment.save!
end
visit post_url(@post)
end
Then /^the post should have (\d+) (approved )?comments?$/ do |count, appr|
if appr.nil?
@post.comments.count.should == count.to_i
else
@post.approved_comments.count.should == count.to_i
end
end
Шаги для комментариев
Этот шаг специально написан для работы с ROTS, который, не требуя от пользователя никакого интерактивного взаимодействия в процессе теста отвечает на запрос редиректом подтверждая или отвергая авторизацию. В файле features/step_definitions/comment_steps.rb:
Then /^I should verify my OpenID$/ do
response = Net::HTTP.get_response(URI.parse(headers['location']))
response.class.should == Net::HTTPSeeOther
visit response['location']
end
Заключение
Вот и всё. Чтобы запустить тест, нужно выполнить:
cucumber features
Далее, конечно же, следует написать сценарии для пустых полей, для отказа в авторизации, для неправильного адреса OpenID и прочее. Но это, как мне кажется, не составляет труда.
Так же известно, что Cucumber поддерживает и русский язык. То есть сценарии можно писать и на русском. Но я пока не пробовал.
Материалы для изучения
RailsCast про авторизацию с помощью OpenID RailsCast про использование Cucumber Документация по Cucumber
14.04.2009Фильтрация rss-потоков с помощью Sinatra и HTTParty
Задача
Для фильтрации rss-потоков сужествует множество инструментов. Для своей задачи мне захотелось написать простейшее решение и заодно попробовать пару новых инструментов.
Надо: собрать воедино несколько единообразных rss-потоков, отфильтровав только нужное, и выдать единый rss-поток.
Для удобства предположим, что потоки имеют одинаковый формат — atom. Адреса нужных нам потоков будут находиться в текстовом файле, разделенные переносом строки. Так же как и необходимые нам ключевые слова. Так же допустим, что наличие ключевых слов будем отслеживать в заголовках.
Ресурсы
Поскольку я собираюсь фильтровать на лету, мне не нужно ничего нигде хранить, я решил попробовать лёгкий руби-фреймворк под названием Sinatra.
А для работы с самими потоками, для получения их с их серверов используем простой и удобный инструмент HTTParty.
sudo gem i sinatra
sudo gem i httparty
Сбор и фильтрация
Создадим библиотечный файл feed_fetcher.rb:
require 'rubygems'
require 'httparty'
class FeedFetcher
include HTTParty
format :xml # позволяет получть результат сразу расфасованный
# в Hash
def self.get_items
urls = nil # будет массив адресов
titles = nil # будет массив нужных частей заголовков
items = [] # будет массив записей
File.open("path_to_feed_urls_file") do |f|
urls = f.readlines.each(&:strip!)
end
File.open("path_to_titles_file") do |f|
titles = f.readlines.each(&:strip!)
end
# составим единое регулярное выражение для фильтрации
retitles = Regexp.union(titles.reject(&:empty?).map { |t| %r{\b#{Regexp.escape(t)}\b}i })
# соберём записи со всех адресов в единый масив
urls.each do |u|
items += get(u)["rss"]["channel"]["item"] unless u.empty?
end
# отфильтруем по регулярному выражению и упорядочим по дате
items.select { |i| i["title"] =~ retitles }.sort do |x, y|
DateTime.parse(y["pubDate"]) <=> DateTime.parse(x["pubDate"])
end
end
end
Выдача результата
Результат будем так же выдавать в формате atom, поэтому нам понадобится builder, который, например, входит в состав active_support. Но можно установить его и отдельно.
Файл feed_filter.rb:
require 'rubygems'
require 'sinatra'
require 'active_support'
require 'feed_fetcher.rb'
get '/' do
content_type 'application/xml', :charset => 'utf-8'
@items = FeedFetcher.get_items
builder :index
end
По-умолчанию Sinatra хранит шаблоны в папке views. Файл views/index.builder:
xml.instruct!
xml.rss "version" => "2.0", "xmlns:atom" => "http://www.w3.org/2005/Atom" do
xml.channel do
xml.title "My Filtered Feed"
xml.link "http://lonelyelk.com"
xml.pubDate CGI::rfc1123_date Time.parse(@items.first["pubDate"]) if @items.any?
xml.description "Some description"
@items.each do |item|
xml.item do
item.each_pair do |key, value|
xml.tag!(key, value)
end
end
end
end
end
Запуск приложения с помощью passenger
Для запуска приложения мы будем использовать Passenger, который поддерживает не только rails, но и rack. Для этого нам понадобится создать папку public и указать к ней путь.
В установках виртуального сервера для apache:
<VirtualHost *:80>
ServerAdmin webmaster@mydomain.ru
ServerName feedfilter.mydomain.ru
DocumentRoot /path/to/feed_filter/public
...
</VirtualHost>
А в папке приложения нужно создать файл config.ru:
require 'rubygems'
require 'sinatra'
Sinatra::Application.set(:run, false)
Sinatra::Application.set(:environment, ENV['RACK_ENV'])
require 'feed_filter'
run Sinatra::Application
Вот и всё. Естественно, ещё следует написать тесты. Так же для публикации можно использовать capistrano. Но это, я думаю, всем под силу.
Материалы для изучения
28.03.2009Автосохранение с помощью JavaScript
О стандартизации
Ещё недавно я говорил о том, что не очень люблю JavaScript за то, что результат работает неодинаково в разных браузерах разных версий и ратовал за Flash и иже с ним, как за прекрасную альтернативу. Но судьба предоставила мне шанс убедиться, что и Flash с его ActionScript вполне может себя вести непредсказуемо в различный браузерах и разных версиях плеера, если программа достаточно сложная.
Я сразу же вспомнил о своём научном руководителе, который в школе курировал мою работу для поступления в ВУЗ. В своей работе он использовал FORTRAN. И на наши недоумённые вопросы отвечал, что это чуть ли не единственный язык, для которого можно записать текст программы любой сложности на дискетту и перенести на любой компьютер любой платформы. Видимо, так оно до сих пор и есть.
Задача
Чтобы отдать дань не очень справедливо заклейменному JavaScript, я решил написать для своего блога и для блога Иры автосохранение.
Ресурсы
Для работы нам понадобится Prototype, который входит в стандартную поставку Rails и обязательно крутилочка. Без крутилочки не стоит даже начинать. :)
Решение
Решение оказалось не очень сложное, поэтому я просто его опубликую с дополнительными пояснениями по мере надобности.
config/routes.rb
map.resources :posts, :member => {:autosave => :post}
Контроллер
...
def autosave
@post = Post.find(params[:id])
if @post.update_attributes(params[:post])
render :nothing => true
else
render :nothing => true, :status => 400
end
end
...
Шаблон
Привожу только код JavaScript, который понадобится для сохранения. Рекомендую обратить внимание, на решение проблемы authenticity_token
var cachedBody = $("post_body").value;
var cachedTitle = $("post_title").value;
var saveTimeout = 20000;
var modTimeout = 200;
var requestInProgress = false;
function checkSave() {
if ((((cachedBody != $("post_body").value) && ($("post_body").value != "")) ||
((cachedTitle != $("post_title").value) && ($("post_title").value != ""))) &&
!requestInProgress) {
cachedTitle = $("post_title").value;
cachedBody = $("post_body").value;
$("spinner").show();
requestInProgress = true;
new Ajax.Request("<%= autosave_post_url(@post) -%>", {
method: "post",
postBody: Form.serializeElements([$("post_title"), $("post_body")]) +
'&authenticity_token=' + encodeURIComponent('<%= form_authenticity_token -%>'),
onSuccess: function (transport) {
var now = new Date();
$("spinner").hide();
$("autosave_text").innerHTML = "Saved at " +
(now.getHours() < 10 ? "0" : "") + now.getHours() + ":" +
(now.getMinutes() < 10 ? "0" : "") + now.getMinutes() + ":" +
(now.getSeconds() < 10 ? "0" : "") + now.getSeconds();
setTimeout("checkSave()", saveTimeout);
requestInProgress = false;
},
onFailure: function (transport) {
$("spinner").hide();
$("autosave_text").innerHTML = "Failed to autosave";
requestInProgress = false;
}
});
} else {
setTimeout("checkSave()", saveTimeout);
}
}
function checkMod() {
if (((cachedBody != $("post_body").value) && ($("post_body").value != "")) ||
((cachedTitle != $("post_title").value) && ($("post_title").value != ""))) {
if ($("autosave_text").innerHTML.charAt(0) != "*") {
$("autosave_text").innerHTML = "* " + $("autosave_text").innerHTML;
}
}
setTimeout("checkMod()", modTimeout);
}
function checks() {
checkSave();
checkMod();
}
document.observe('dom:loaded', checks);
В данном случае spinner — это id крутилочки. Крутилочка показывается, когда проходит запрос. Так же здесь имеется дополнительный цикл вызовов. Раз уж мы всё равно храним копию того, что сохранено на сервере, то почему бы не показывать, когда текст уже изменен, но не сохранен на сервере. С помощью звездочки.
А как сделать, чтобы звездочка исчезала, если текст в полях снова стал такой, оставлю на самостоятельное изучение.
Материалы для изучения
Интересный пример ненавязчивого JavaScript для создания «бесконечной» страницы
24.03.2009Jabber бот
Возникла идея написать jabber-бота. Выбор протокола обусловлен тем, что по нему достаточно много документации. И стандарт открытый — это хорошо. Много информации на code.google.com, но я решил пойти более простым путём и нашёл библиотеку на руби xmpp4r. Благодаря тому, что разработка ведется на руби, время разработки простого бота значительно сокращается. Устанавливается как библиотека:
sudo gem i xmpp4r
Регистрация
Это прекрасно, что бот может сам себя зарегистрировать.
require "rubygems"
require "xmpp4r"
include Jabber
client = Client.new(JID.new("login@server"))
client.connect
begin
client.register "secretpassword"
puts "Success!"
rescue ServerError => e
puts "Error: #{e.error.text}"
end
При регистрации можно передавать хэш с параметрами для визитной карточки. После регистрации для последующей авторизации вместо register вызываеься:
client.auth "secretpassword"
Список контактов
Список контактов называется roster. Чтобы с ним работать нужно его запросить:
require "xmpp4r/roster"
roster = Roster::Helper.new(client)
А после этого обработать ответы при обратных вызовах. Если добавлять друзей будет тоже бот, и не будет заморачиваться с группами контактов, то для вывода списка друзей на данный момент достаточно описать вызов:
roster.add_query_callback do |iq|
puts "current roster:"
roster.find_by_group(nil).each do |item|
puts "#{item.jid}"
end
end
Простейшее разрешение видеть свой статус и добавление в контакты в ответ на то, что добавляют бота выглядит так:
roster.add_subscription_request_callback do |item, pres|
puts "Added by #{pres.from}"
roster.accept_subscription(pres.from) # Разрешить видеть свой статус
new_pres = Presence.new.set_type(:subscribe).set_to(pres.from)
client.send(new_pres) # Запросить разрешение видеть статус
end
Теперь наши контакты смогут видеть наш бот онлайн, когда мы сообщаем об этом:
client.send(Presence.new.set_type(:available))
Неплохо бы что-нибудь ещё добавить, типа аватара.
Визитная карточка
Визитная карточка называется vcard:
require "xmpp4r/vcard"
require "base64"
vcard = Vcard::IqVcard.new
vcard["FN"] = "Full Name"
vcard["NICKNAME"] = "bot-nickname"
vcard["PHOTO/TYPE"]= "image/jpeg"
avatar_file = File.new("avatar.jpg", "r")
avatar_b64 = Base64.b64encode(avatar_file.read())
avatar_file.rewind
avatar_sha1 = Digest::SHA1.hexdigest(avatar_file.read())
vcard["PHOTO/BINVAL"] = avatar_b64
avatar_file.close
begin
Vcard::Helper.new(client).set(vcard)
puts "Success!"
rescue Exception => e
puts "VCARD operation failed: #{e.to_s}"
end
В соответствии со спецификацией xmpp относительно аватаров следует транслировать с какой-то периодичностью sha своего аватара:
Thread.new do
while true do
unless avatar_sha1.nil?
pres = Presence.new
x = REXML::Element.new("x")
x.add_namespace("vcard-temp:x:update")
photo = REXML::Element.new("photo")
avatar_hash = REXML::Text.new(avatar_sha1)
photo.add(avatar_hash)
x.add(photo)
pres.add_element(x)
client.send(pres)
end
sleep 60
end
end
Дело за малым.
Ответ на сообщения
cl.add_message_callback do |m|
if m.type != :error and m.body
client.send(m.answer.set_body(bot_main_function(m.body)))
end
end
Внутри bot_main_function находится собственно то, что будет делать ваш бот, как он будет отвечать на то, что написали ему.