Разные мелочи

July 2, 2012

3 недели в блог не писал и уже тяжело снова начинать. Так однажды я 2 года спортом не позанимался и тоже лень было возвращаться. Есть правда хорошое решение - нужно себя заставлять. И вот сегодня я хочу рассказать про разные мелочи, с которыми столкнулся в свое время.

Kernel.caller

Вы будете смеятся, но раньше, чтобы вывести стек вызова методов приложения я использовал конструкцию:

def some
  begin
    raise 
  rescue => e
    p e.backtrace # => ["demo.rb:3:in `some'", "demo.rb:9:in `<main>'"]
  end
end

some

После C++ мне казалось, что это вполне нормально (в C++ попробуйте вывести стек приложения в лог, там не побалуешь). Но это же руби, здесь можно все, причем очень просто! Случайно в интернете обратил внимание на системный вызов Kernel.caller. Напечатать цепочку вызовов можно так:

def some
  p caller # => ["demo.rb:5:in `<main>'"]
end

some

Добавление ошибки валидации на :base

Отображая ошибки валидации с помощью стандартного object.errors.full_messages, мы иногда получаем такие сообщения: "Password Укажите пожалуйста пароль". Чтобы быстро избавиться от обязательно названия атрибута в начале сообщения, можно добавлять ошибки валидации следующим образом: object.errros.add(:base, 'Укажите пожалуйста пароль'). full_message не будет добавлять название атрибута в этом случае - activemodel/lib/active_model/errors.rb#L287.

Rescue в одну строку

Этим шаблоном я еще не воспользовался, но он интригует. Все мы используем конструкции для установки значений по умолчанию:

a = some || "default"
b = other.presence || "start" # Rails only, обрабатывает nil? и blank?

Оказывается можно так же легко установить дефалтовое значение, если метод вызывает исключение:

a = another rescue "after exception"

Я обычно не вызываю эксепшенов в своем коде, которые где-то нужно обрабатывать. Но с внешними вызовами возможно пригодится.

IndexBy

Чего только не придумают руби-программисты, чтобы писать меньше кода. Недавно наткнулся на удивительный метод index_by в рейлс:

# Выдержка из документации:

# Convert an enumerable to a hash:
people.index_by(&:login)
  # => { "nextangle" => <Person ...>, "chade-" => <Person ...>, ...}

Ассерты

В С++ очень активно используются ассерты для проверки входных параметров, при выполнении каких-то подозрительных действий, для проверки возвращаемых значений и т.д. Это является частным случаем программирования по контракту. В рейлс, для собственного спокойствия, я тоже часто вставляю такого рода проверки. Приведу несколько типовых случаев:

# 1.
# не обращайте внимание на цепочку if-elseif, не могу себя
# отучить в пользу case.
if mode == :some
  # ...
elsif mode == :another
  # ...
else
  raise "unreacheable: #{mode}"
end

# 2.
options.assert_valid_keys(:key_a, :key_b)

# 3.
raise "wrong #{state}" unless %w[start finish].include?(state)

Я обычно не заморачиваюсь внятными сообщения в эксепшенах, так как если ошибка и случится, то по трейсу всегда легко отследить проблему. Данные конструкции призваны подсказывать программисту, что в коде возник какой-то случай, о котором раньше не позаботились. Также эти конструкции легко расширяются, когда появляются новые режимы, ключи или состояния.

Для первого раза думаю хватит. Рад возвращению в онлайн. Раньше мы работали над большим количеством маленьких проектов и я прежде всего оптимизировал скорость и стандартность разработки. Сейчас у нас есть парочку средних проектов, на несколько человеко-месяцев (мифических :-) каждый. При увеличении кодовой базы возникают проблемы другого сорта, с которыми я буду сталкиваться, осмыслять и рассказывать вам. До новых встреч!

Правила и комфорт

June 5, 2012

Добрый день, дорогие читатели! Я сейчас в отпуске и не в курсе последних рейлс-новостей, поэтому хочу написать пост на тему, не связанную с программированием.

Однако, чтобы оправдывать тематику блога, отмечу про выход рейлс 3.2.5 из-за двух критических уязвимостей. Я пробовал их применить на наших проектах, но у меня не получилось их сломать, хакер из меня некудышный, но Егору Хомякову эти уязвимости нравятся. А раз он доволен - обязательно нужно обновляться, особенно если вы поддерживаете какой-нибудь крупный проект :) Ну вот про рейлс на сегодня все.

Я люблю размышлять про смысл жизни и все такое, благо профессия программиста позволяет сохранять силы на размышления и обеспечивает материально. Живя в Москве, особенно остро ощущаю насколько сильно, искусственно разогнана жизнь современных людей. Мне нравится следующая аналогия - мне даже кажется, что я ее придумал сам.

Представьте какой-нибудь ограниченный ресурс, допустим комнату, в которой четыре стула, и на них хотят отдохнуть пять людей. В первом случае люди заходят степенно, никуда не спешат, но одному человеку придется стоять. Однако в такой благоприятной обстановке, кто-нибудь, возможно, уступит ему место или предложит сидеть по очереди. Во втором случае люди пытаются захватить стул как можно быстрее. Математически понятно, что одному человеку все равно места не хватит. А в процессе потасовки возможно еще один стул сломают, и уже два человека останутся без места. Уровень агрессивности и дискомфорта будет гораздо выше, но результат такой же, даже хуже.

Реклама, социальное давление провоцируют нас на поведение по второму сценарию, но по-человечески гораздо симпатичнее первый вариант. Почему же он так редко встречается? Главный недостаток данного сценария, что он очень уязвим для жуликов. Ушлый человек из второй группы, находясь в первом варианте, будет сидеть на стуле всегда, а то и на двух, посмеивась над остальными. Думаю, такое поведение вам прекрасно знакомо. Проще всего данную ситуацию проиллюстрировать известным рисунком про равновесие, первый вариант конечно лучше, но он слишком неустойчивый:

Я мало был в других странах, но, по ощущениям, уровень комфорта и спокойствия в европейских странах выше. Мне кажется, местных жителей силой, для их же блага, загоняют и удерживают в первом сценарии. Однажды в Дрездене мы перешли дорогу на красный цвет. Машин не было в обе стороны настолько, насколько в принципе позволяет увидеть зрение. Вообще сложно представить более пустую дорогу. Неприятное, скажу я вам, ощущение переходить, когда все остальные 20 или 30 человек остались ждать зеленого сигнала. Они соблюдали правила.

Я фанат правил и считаю, что именно они обеспечивают весь прогресс и комфорт. В гипотетической ситуации со стульями достаточно четырех правил и их соблюдения: не шумите, не спешите, не ломайте мебель, уступайте место. В С++ вообще 2 правила: чтите Страуструпа и не разыименовывайте нулевые указатели.

В профессиональной деятельности я тоже стараюсь ответственно относится к правилам и самоограничениям. Вначале я маньячил и работал с утра до позднего вечера, в будни и выходные. Очевидно, что человек, который работает по 12 часов в день успевает больше, чем тот, который работает стандартные 8. Но таким образом поднимается общая планка (и подрывается здоровье), поэтому я сейчас в выходные отдыхаю, а в будни стараюсь работать не больше, чем в офисе. К сожалению, в этот отпуск мне не удалось отказаться от компьютера, я все равно раз в день, в два выхожу в интернет, проверяю почту, что-то поправляю. Но это показатель, что у нас еще плохо налажена система. Надеюсь, что в следующий отпуск я буду отдыхать как положено, по правилам.

Желаю вам комфортной и спокойной жизни!

100 комитов в Rails

May 28, 2012

В сентябре 2011, убедив клиента, что мы работаем исключительно на последних версиях рейлс, я начал свой первый Rails 3 проект. Дополнительная пикантность ситуации была в том, что версию 3.0 зарелизили в августе 2010, однако мне казалось, что там не могло появиться ничего хорошего, рейлс 2.3 мне казались самодостаточными.

Случайно, исследуя создание нового приложения, я нашел, что метод environment в генераторах ставит неправильный отступ в config/appliction.rb. Так я сделал свой первый комит в рейлс. Потом еще. И еще. Ощущение причастности к большому сложному открытому проекту оказалось настолько приятным, что в феврале 2012 я добавил в годовой план цель - достичь к концу года 100 комитов. С радостью сообщаю, что на днях я этой цели достиг. Вообще 2012 - очень ответственный год, так как непонятно сохранится ли спрос на программистов после конца света. Поэтому желательно освободиться от всех обязательств как можно раньше, чтобы успеть сделать действительно важные дела.

Возвращаюсь к рейлс, хочу поделиться с вами тем, что я нашел и узнал в процессе работы. Самое главное, что цель "N комитов" - довольно плохая. Она провоцирует на мелкие подтасовки и, выполняя ее, слишком нервничаешь, когда комит не принимают или игнорируют. Если гнаться просто за количеством комитов, то можно использовать много мелких хитростей, например делать док-фиксы, комментируя каждый метод (док-фиксы проходят прямым комитом в lifo/docrails), можно, решая одну проблему, разбивать ее на несколько частей, или найти направление, которое принесет много комитов. По мелочам, можно забыть сделать git pull --rebase перед комитом опять же док-фиксов. Если вы наблюдаете за мастером, думаю, что не раз видели подобные штуки.

Но у цели "N комитов" есть очень важный нюанс, который делает ее на самом деле хорошей. Не зависимо от того, исправляете ли вы маленькую опечатку в комментарии, рефакторите метод, исправляете дефект или предлагаете новую функциональность, приходится проделывать довольно серьезную работу. Необходимо пройтись по истории, запустить тесты, исследовать граничные случаи, провести анализ и так далее. Вольно или невольно вы изучаете систему очень глубоко и такого рода рутина приучает к методичности. Когда я всерьез увлекся рейлс, то долгое время вставал в 8 утра и 2-3 часа изучал код, файл за файлом. Сейчас в эти утренние часы я подключил ведение блога, работу над персональными опен-сорс проектами, изучение новых технологий и подходов.

Один из моих крайних комитов, самый любимый, исправляет метод Range#sum. Посмотрите, пожалуйста, какую работу пришлось проделать, чтобы найти и исправить 2 ошибки в данном методе. Увидел этот дефект совершенно случайно, я хотел написать в твиттер про этот метод, что люблю, когда люди продумывают такие нюансы, но что-то в этом коде мне не понравилось, и я стал разбираться.

Поэтому цель "N комитов" вполне хорошая, и если вы сделали 10, 20, 50, 100 или 10000 :) комитов, то независимо ни от чего, это по настоящему здорово. Вы помогли комьюнити и помогли себе, получая еще большее удовольствие от профессии. Мне кажется, что последнее время количество русскоязычных контрибьютеров сильно возросло, и это очень хорошо. Евтухович Иван говорил, что гуру рейлс не очень охотно едут в Россию на конференции, из-за недостаточной активности нашего комьюнити. Возможно, это не единственная причина, но все-таки. Поэтому хочу особо отметить активных русскоязычных коллег: Сергея Нартымова (Сергей возглавляет русскоязычный топ), Богдана Гусева, Василия Ермоловича, Равиля Байрамгалина и Алексея Газиева. Отдельно следует упомянуть Егора Хомякова, который получил доступ в основной репозитарий не самым прямолинейным способом.

Некоторое время назад я очень сильно застрял на 80-ти комитах и более 2-х недель ни мог найти ни одной шероховатости (кстати раскрываю рыбные места - ActionPack и ActiveSupport, но ни в коем случае не ActiveRecord. Я нашел 3 ошибки в AR, но пока не знаю как их исправить, потому-что код там - гребанный ад, как однажды сказал Андрей Руденко про бандлер). К счастью потом дело пошло лучше и я успел поразить цель перед самым отпуском, завтра поеду отдыхать от компьютеров и от рейлс. Всем удачного программирования!

Разработка стандарта кодирования

May 24, 2012

Если вам необходимо разработать стандарт кодирования обязательно прислушайтесь к совету Герба Саттера и Андрея Александреску. Этот совет я нашел в книге по C++, но его можно применять к любому языку программированию и к жизни вообще:

Скажем кратко: не мелочитесь.
Герб Саттер, Андрей Александреску
Стандарты программирования на C++ (Глава 0. Что не следует стандартизировать)

Чтобы стало более понятно приведу еще несколько выдержек:

  • Не следует определять конкретный размер отступа, но следует использовать отступы для подчеркивания структуры программы. Для отступа используйте то количество символов, которое вам нравится, но это количество должно быть одинаково, как минимум, в пределах файла.
  • Не определяйте конкретную длину строки, но она должна оставлять текст удобочитаемым. Используйте ту длину строки, которая вам по душе, но не злоупотребляйте ею. Исследования показали, что легче всего воспринимается текст, в строке которого находится до десяти слов.
  • Следует использовать непротиворечивые соглашения об именовании, не слишком мелочно регламентируя его.

По правде говоря Герб и Андрей очень сильно рискуют, говоря, что количество пробелов в отступе не важно. Это острая тема и нужно обладать сильным духом, чтобы затронуть ее на вечеринке с коктелями. В C++ существует 3 фундаментальные школы: 8 пробелов — кернел си хакеры, такой огромный отступ удерживает от вложенности больше трех, 4 пробела — классика С++, 2 пробела — неофиты. На прошлой работе проблему размера стандартного отступа разрешили с изяществом Александра Македонского — отступ был 3 пробела. Для крупных проектов, где большая вложенность все же встречается — это на удивление удобно.

И еще одна цитата, моя любимая (жирным выделил я):

1
2
3
4
5
6
7
8
9
10
11
12
// Размещение фигурных скобок
void using_k_and_r_style() {
// ...
}
void putting_each_brace_on_its_own_line()
{
// ...
}
void or_putting_each_brace_on_its_own_line_indented()
    {
// ...
    }

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

Итак, не мелочитесь!

Создание Edge Rails приложения

May 22, 2012

Если вы нашли какую-нибудь ошибку в рейлс, то нужно проверить, что ее еще не исправили в мастере. Для этого иногда необходимо создать приложение из edge. В интернете много устаревших статей на эту тему, поэтому сегодня расскажу, как это делаю я.

Простой способ, это сгенерировать пустое приложение на последней стабильной версии рейлс и в гемфайле исправить запись с rails:

# gem 'rails', '3.2.3'
gem 'rails', path: '../rails'

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

Лучше создавать приложение прямо из edge. Для этого достаточно клонировать исходники рейлс к себе, запустить bundle, чтобы установить необходимые гемы и вызвать rails new из исходников:

git clone https://github.com/rails/rails
pushd rails
  bundle
popd
./rails/railties/bin/rails new demo-app --edge

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

Я уже рассказывал, как я создаю новое приложение. По аналогии я создал шаблон, для нового edge-приложения:

def file_force(name, content)
  f = File.open(name, 'w')
  f.puts content
  f.close
end

gem 'haml'
gem 'jquery-rails'
gem 'therubyracer', platform: :ruby, group: :assets
gem 'rails-footnotes', github: 'avakhov/rails-footnotes', branch: 'custom', group: :development
gem 'factory_girl_rails', group: :test
gem 'timecop', group: :test
gem 'database_cleaner', group: :test
gem 'rspec-rails', group: [:development, :test]

file '.rspec', <<-CODE
--colour
CODE

file 'spec/spec_helper.rb', <<-CODE
ENV["RAILS_ENV"] ||= 'test'
require File.expand_path("../../config/environment", __FILE__)
require 'rspec/rails'
require 'rspec/autorun'

Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f}

RSpec.configure do |config|
  config.before(:suite) do
    DatabaseCleaner.strategy = :transaction
  end

  config.before(:each) do
    DatabaseCleaner.start
  end

  config.after(:each) do
    DatabaseCleaner.clean
  end

  config.include FactoryGirl::Syntax::Methods
end
CODE

file 'spec/factories.rb', <<-CODE
FactoryGirl.define do
  # factory :demo do
  #   name 'name'
  # end
end
CODE

file 'app/controllers/home_controller.rb', <<-CODE
class HomeController < ApplicationController
  def index
  end
end
CODE

file 'app/views/home/index.html.haml', <<-CODE
%h1 Home#index
%p Find me in app/views/home/index.html.haml
CODE

file 'spec/controllers/home_controller_spec.rb', <<-CODE
require 'spec_helper'

describe HomeController do
  it "index" do
    get 'index'
    response.should be_success
  end
end
CODE

head = File.readlines('config/routes.rb').first
file_force 'config/routes.rb', <<-CODE
#{head.strip}
  root to: 'home#index'
end
CODE

FileUtils.rm('public/index.html')
FileUtils.rm_rf('test')

system "bundle install"
system "rake db:create db:migrate db:test:prepare"
system "git init"
system "git add ."
system "git ci -amInitial"

Использую шаблон с помощью команды:

./rails/railties/bin/rails new demo-app --edge -m rails-templates/edge-app.rb

По умолчанию будет использоваться рейлс из гитхаба gem 'rails', github: 'rails/rails', можно поменять вручную на gem 'rails', path: '../rails', чтобы экспрериментировать со своими изменениями.

CSS-спрайты на компасе

May 15, 2012

Идеалогически мне очень нравится технология css-спрайтов, когда мелкие картинки рисуются в одном файле и отображаются с помощью свойства background-position, экономя коннекты к серверу. Спрайты напоминают мне первую работу, где я работал с 3D-графикой и дизайнеры размазывали трехмерные модели по текстурам. А также раньше были спрайтовые монстры. Эх.

Кроме оптимизации отрисовки сайта в браузере (для маленьких и средних проектов это не так уж и важно) спрайты следует применять для кнопок с картинкой, которая меняется при наведении указатели мыши. Если :hover-состояние кнопки сделать через отдельный рисунок, то она противно моргнет при первой отрисовке. Я слышал про библиотеку компас, но обычно просил дизайнера или верстальщика подготовить файл с двумя состояниями. Наконец, недавно, попробовал использовать спрайты с компасом и хочу рассказать про несколько нюансов, с которыми столкнулся.

Самая главная новость - технология работает! Это очень важно, так как иногда библиотеки перестают работать (например недавно отвалился haml в рейлс 3.2.3). В нашем случае учебник на официальном сайте актуальный:

Я создал простую демку на хероку, итоговый спрайт получился такой:

Минимальный код, необходимый для работы:

# Gemfile
gem 'compass-rails'

# app/assets/buttons/*
# Файлы кнопок (red.png и red2.png в моем случае)

# app/assets/stylesheets/application.css.scss
@import "buttons/*.png";

.twitter {
  display: block;
  width: 300px;
  height: 300px;
  @include buttons-sprite(red);    # red и red2 - имена соответствующих картинок
  &:hover {
    @include buttons-sprite(red2);
  }
}

# app/views/home/index.html.haml
= link_to '', "https://twitter.com/#!/avakhov", class: 'twitter'

Теперь нюансы:

Возможно я просмотрел где это написано, но библиотека обрабатывает только png-файлы. Для итогового спрайта - это оправдано, так как необходима картинка без потери качества, но для исходных картинок мне кажется это излишне. К сожалению когда я попробовал импортировать jpg-файлы, система выдала не очень информативную ошибку:

File to import not found or unreadable: buttons/*.jpg.
Load paths:
  Sass::Rails::Importer(/home/alex/m/ideas/blog-1-css-sprite/app/assets/stylesheets/application.css.scss)
  /home/alex/m/ideas/blog-1-css-sprite/app/assets/stylesheets
  /home/alex/.rbenv/versions/1.9.3-p0-ff/lib/ruby/gems/1.9.1/gems/compass-0.12.1/frameworks/blueprint/stylesheets
  /home/alex/.rbenv/versions/1.9.3-p0-ff/lib/ruby/gems/1.9.1/gems/compass-0.12.1/frameworks/compass/stylesheets
  Compass::SpriteImporter
  (in /home/alex/m/ideas/blog-1-css-sprite/app/assets/stylesheets/application.css.scss)

Также есть ограничения на имена файлов, которые можно использовать для спрайтов. Простые и понятные 1.png и 2.png также не подходят. В этот раз описание ошибки было хорошее:

Sprite file names must be legal css identifiers. Please rename 1.png

С первой ошибкой я провозился довольно долго, так как не знал насколько хорошо @import должен вообще работать и сначала думал, что что-то с путями. Раньше я бы сильно разозлился, но теперь понимаю, что большая часть рабочего времени программиста проходит в режиме "что-то где-то работает не так как хочется". Сейчаc я сам поддерживаю несколько гемов, сегодня в первый раз создал демо-проект для поста, и вижу сколько времени уходит на эти, казалось бы не сложные операции. Поэтому, то, что многие программы работают и работают достойно - это само по себе большое чудо, и, если мои рассказы помогут сэкономить вам немного времени, то буду очень рад.

И снова руби, после выходных

May 10, 2012

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

Массив и Хэш

В руби 1.9 ошибка при обращении по сломанному индексу к массиву стала более невнятной:

irb(main):001:0> RUBY_VERSION
=> "1.8.7"
irb(main):002:0> [1, 2, 3][:key]
TypeError: Symbol as array index
irb(main):001:0> RUBY_VERSION
=> "1.9.3"
irb(main):002:0> [1, 2, 3][:key]
TypeError: can't convert Symbol into Integer

Если вы случайно передадите масcив в функцию, которая принимает хэш, то поймаете ошибку TypeError: can't convert Symbol into Integer. Я один раз довольно долго искал проблему, так как вызов метода и работа с аргументом были окружены дополнительным кодом и я грешил на другую конструкцию.

Функция "притвориться"

Во всех сайтах с бэкэндом в модель администратора мы обычно добавляем булевские флажки с правами доступа. Примерно так:

# model
class User < ActiveRecord::Base
  has_fields do
    string :username

    boolean :acl_seo
    boolean :acl_posts
    boolean :acl_orders
    # ...
  end

  # ...
end

# controller
class PostsController < ApplicationController
  layout 'admin'
  before_filter :authenticate_user
  before_filter { raise AccessDenied unless current_user.acl_posts? }
  # ...
end

# view
<% if current_user.acl_posts? %>
  <%= link_to "Posts", posts_path %>
<% end %>

Эта простая и надежная схема верно служит нам уже довольно долго. Мы создаем суперюзеров, чтобы скрыть настройки от клиента, о которых ему не следуют беспокоится. Клиенты часто просят создать аккаунты с ограниченными правами (в душе мы все параноики и к тому же любим ущемить чьи-нибудь интересы :-)). Недавно я придумал добавить ещу одну функцию "притвориться другим пользователем". Теперь суперюзер может выбрать из комбобокса любой аккаунт и залогиниться им. Схематически это реализовано так:

class ApplicationController < ActionController::Base
  # ...
  def owner_user
    @owner_user ||= User.find_by_id(session[:owner_user_id])
  end
end

class AdminController < ApplicationController
  # ...

  def pretend
    user = User.where('id <> ?', current_user.id).find(params[:user_id])
    session[:owner_user_id] = current_user.id
    sign_in user
    redirect_to :back
  end

  def stop_pretend
    if owner_user
      sign_in owner_user
      session[:owner_user_id] = nil
    end
    redirect_to :back
  end
end

Эта маленькая хитрость позволяет решить сразу же 2 проблемы: всегда можно под любым аккаунтом посмотреть не осталось ли чего-нибудь лишнего, и можно зайти под клиентом, который говорит, что у него "все сломалось" и убедиться, что это действительно так.

Всегда запоминать меня

Коллега прислал ссылку на статью Always remember me, о том что пора отказаться от галочки "запомнить меня". В принципе я согласен с этой статьей и сам не долюбливаю эту галочку. Даже если сломался интернет и вы не сможете нажать ссылку "выйти", то всегда можно очистить куки или пользоваться режимом приватного просмотра. В рейлс 3.2 реализовать логин без экспирации очень легко:

cookies.signed.permanent[:user_id] = current_user.id
# Для чтения permanent указывать не обязательно, а signed - обязательно
cookies.signed[:user_id] # => 123

Мы новые проекты делаем без галочки в бэкэнде, проверяем концепцию.

Хорошей рабочей недели!

Гемы - require и bundler

May 4, 2012

Мне не хватит духу написать подробное руководство как создать гем, к тому же я сам не эксперт в этом вопросе, лучше расскажу сегодня про еще одну ошибку, на которую я потратил пару часов, дойдя даже до отладки. Она настолько глупая, что не хочется, чтобы еще кто-то терял свое время. Точнее говоря, это даже не ошибка, а заблуждение.

В одном гемспеке я прописал зависимость от хамла:

### secret.gemspec
Gem::Specification.new do |gem|
  gem.authors       = ["Alexey Vakhov"]
  # ..

  gem.add_dependency 'haml'

Этот гем является энджином, в нем есть контроллеры и вьюхи. Все прекрасно работало на тестовом приложении, но когда я прикрутил его к новому проекту, то хамл-вьюхи перестали подцеплятся. Я заглядывал в Gemfile.lock файл и видел зависимость от хамла, но вьюхи не работали, не находился обработчик хамл. Так как я был совершенно уверен, что эта схема правильная, то мне пришлось потратить довольно много времени, чтобы разобраться.

Думаю, вы уже поняли в чем была проблема. Я уже настолько привык к магии бандлера в рейлс, что не задумывался, как это работает. Однако, когда мы используем бандлер вне рейлс, то написав гем-файл и вставив require "bundler/setup" мы просто ограничиваем область видимости для require:

require "rubygems"
require "bundler/setup"

Nokorigi # => uninitialized constant Nokogiri (NameError)

# require your gems as usual (Nokorigi прописан в гем-файле)
require "nokogiri"

Nokorigi # => OK

require "unicorn" # => cannot load such file -- unicorn (LoadError)

Это пример взят с официального сайта бандлера. Рейлс идет дальше и автоматически включает все гемы из гем-файла (ориентируясь на енвайромент) в проект с помощью Bundler.require:

# config/application.rb чистого проекта

if defined?(Bundler)
  Bundler.require(*Rails.groups(:assets => %w(development test)))
end

Таким образом в моем проекте бандлер включил по умолчанию мой гем, но не включил хамл, так как он не был прописан в гем-файле проекта, а была только зависимость! Гемспек используется при установке и в данном случае игнорируется бандлером. В тестовом приложении, на котором я разрабатывал гем, хамл был включен в гем-файл и поэтому ошибки не было. Чтобы бы больше не попадаться в эту ловушку, во всех гемах я явно включаю все зависимости в главном файле:

### secret.gemspec
Gem::Specification.new do |gem|
  gem.authors       = ["Alexey Vakhov"]
  # ..

  gem.add_dependency 'haml'
  gem.add_dependency 'modularity'
  gem.add_dependency 'nokorigi'
end

### lib/secret.rb
require 'haml'
require 'modularity'
require 'nokorigi'
require 'secret/version'
# ...

Практика показывает, что сначала нужно искать ошибки в своем коде, потом в фреймворке, потом в компиляторе, потом в железе, потом ... хм, в мироздании наверное :) Из сложных ситуаций, я ловил один раз крэш руби на домашнем компьютере из-за немного битой памяти. Руби - прожорливый язык и когда залезал в сбойные ячейки, то падал. Я догадался запустить мемтест, хотя никогда с таким не сталкивался. К слову сказать и сама система вела себя немного станно.

Второй случай, может быть вы поможете в нем разобраться. Я все собираюсь запустить что-нибудь в продакшн на JRuby с мультитредовым рейлс. В MRI пугают GIL (у-у-у-у, страшно. Не знаю что это, но похоже какая-то пакость, которая не позволяет параллелить программу на несколько ядер) и плохими тредами. Поэтому готовлюсь заранее.

Насколько я понимаю, чтобы ничего не шлепнулось, необходимо избегать использовать члены класса. Я обернул код из статьи Let’s stop polluting the Thread.current hash в гем threadlocalaccessor, но он (хвала travis-ci) падал на руби 1.9.3-p125 (на p0 работал хорошо) на конструкции ObjectSpace.define_finalizer Thread.current, FINALIZER. Видимо в p125 нельзя прилеплять финалайзер к Thread.current (я нахожусь на чертовско тонком льду понятий, которые очень слабо понимаю, поэтому могу сморозить что-нибудь не то).

Я переписал код таким образом, что вроде он должен быть потоко-безопасным и работает на всех версиях руби. Сейчас проверяю эту библиотеку на живых проектах, пока в однопоточном режиме:

# https://github.com/avakhov/thread_local_accessor/blob/master/lib/thread_local_accessor.rb

class Class
  def thread_local_accessor name, options = {}
    m = Module.new
    m.module_eval %{
      def #{name}
        k = ((Class === self ? self : self.class).object_id.to_s + '_#{name}').to_sym
        if Thread.current.key?(k)
          Thread.current[k]
        else
          #{options[:default].inspect}
        end
      end

      def #{name}=(val)
        k = ((Class === self ? self : self.class).object_id.to_s + '_#{name}').to_sym
        Thread.current[k] = val
      end
    }

    class_eval do
      include m
      extend m
    end
  end
end

Вы знаете почему падал предыдущий код и, как вы думаете, новый подход нормальный?

Экономим в честь дня труда

May 2, 2012

Оказывается в гем-файле при указании репозитариев из гитхаба можно использовать краткую форму:

# Тоже самое
gem 'rack-test', :git => "git://github.com/brynary/rack-test.git"
gem 'rack-test', github: "brynary/rack-test"

Узнал из вчерашнего комита Петра: 17f2958d85. После насыщенных праздников в честь дня труда экономия нажатий кнопок очень актуальна.

Более того если имя пользователя и название репозитария совпадают можно использовать еще более краткую форму:

# Тоже самое
gem "rails", github: "rails/rails"
gem "rails", github: "rails"

При указании источника гемов раньше приходилось выбирать один из 4-х вариантов:

source 'https://rubygems.org'
source :gemcutter
source :rubyforge
source :rubygems

Естественно я бы посоветовал использовать sourсe :rubygems, так как в нем меньше всего букв. Но в свежей версии бандлера 1.1.0 можно не указывать явно рубиджемс и гем-файл может состоять только из определений зависимых гемов:

# Абсолютно правильный гем-файл
gem 'sqlite3'

Сегодня хороший день перейти полностью на руби 1.9, если вы это до сих пор не сделали. Так как экономия на каждой паре ключ-значение достигает 3 символа:

{:key => 'value'} # 17 символов.
{key: 'value'}    # 14 символов!

И наконец самый мудрый и важный совет от мэтра С++:

24.2.4. Отказ от программирования
Программирование дорого и непредсказуемо по сравнению со многими другими видами деятельности, и полученная программа часто не на 100% надежна. Программирование трудоемко, и — по многим причинам — многие серьезные проекты задерживаются из-за неготовности кода. Так почему бы программирование как род деятельности совсем не устранить из процесса? Бьерн Страуструп, Язык программирования С++ (Третье издание)

Таинственные гемы - Prepend Routes

April 27, 2012

Хочу поделиться интересной ситуацией, которая у меня возникла при разработке гемов. Вообще в любом случае попробуйте написать гем и обязательно выложите его на рубиджемс, так вы сделаете задачу придумывания имени для следующих коллег более интересной :)

Если говорить серьезно, я сталкивался с дискуссиями о том, что пространство имен рубиджемс очень сильно замусоренно и лучше не создавать гемы для своих внутренних нужд. Мне кажется проблема надуманная. Я не выкидываю бумажки из под мороженного на землю на основании того, что наша планета и так замусоренная, но экономия на именах мне напоминает шуточною статью, которая рекомендовала беречь GUID'ы, так как их осталось очень мало. (Если вы программировали под виндовс, то заметили, что гуиды там используются очень активно. Это смешная шутка, правда?)

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

# lib/custom_gem/engine.rb
module CustomGem
  class Engine < ::Rails::Engine
  end
end

# app/controllers/custom_controller.rb
class CustomController
  def index
    # ...
  end
end

# config/routes.rb
Rails.application.routes.draw do
  get 'custom/index'
end

Это будет работать прекрасно во всех случаях, кроме одного, если кто-то использует раут со звездочкой для мэтчинга по шаблону (например для статических страниц о компании, контакты и т.д.):

MyApp::Application.routes.draw do
  # ...
  get '*permalink' => 'home#page'
end

В этом случае кастомный раут у гема будет затерт. Я столкнулся с этим в своем форке rails-footnotes, когда контроллер, который в девелопменте позволяет посмотреть исходный руби-файл прямо в браузере, перестал работать. Есть очень простая, но недокументированная возможность - метод prepend actiondispatch/routing/routeset.rb#L306, с помощью которого можно поместить раут на самый верх:

# 1-й вариант (НЕ ПРАВИЛЬНО)
# ------------
# Вот этот вариант НЕ ЗАРАБОТАЛ! Не знаю почему, мне лень стало разбираться.

# config/routes.rb
# Заменяем draw на prepend
Rails.application.routes.prepend do
  get 'custom/index'
end
# 2-й вариант (рабочий)
# ------------

# config/routes.rb - удаляем вообще

# lib/custom_gem/engine.rb
module CustomGem
  class Engine < ::Rails::Engine
    initializer 'custom' do |app|
      ActiveSupport.on_load :after_initialize do
        app.routes.prepend do
          get 'custom/index'
        end
      end
    end
  end
end

С необходимостью расширять рауты (именно расширять, а не делать маунтед-енджин) я сталкивался всего два раза. В обоих случаях я сделал как в варианте 2. Первый вариант выглядит привлекательнее, но у меня пока не дошли руки разобраться почему он не работает.