module Users
class NameService
def initialize(user)
@user = user
end
def name
if user.name.present?
user.name
else
::ExternalAPI.new(user).get_name
end
end
private
attr_reader :user
end
end
require 'spec_helper'
describe Users::NameService do
describe '#name' do
it 'nameを返す' do
user = User.create!(name: 'Mike Black')
service = described_class.new(user)
expect(service.name).to eq(user.name)
end
it 'nameを返す' do
user = User.create!(name: nil)
service = described_class.new(user)
expect(service.name).to eq('Mike Jr Black')
end
end
end
describe Users::NameService do
describe '#name' do
it 'ユーザーに名前が1つある場合はユーザー名を返す' do
user = User.create!(name: 'Mike Black')
service = described_class.new(user)
expect(service.name).to eq(user.name)
end
it 'ユーザーが名前を持ってない場合はAPIから取ったユーザー名を返す' do
user = User.create!(name: nil)
service = described_class.new(user)
expect(service.name).to eq('Mike Jr Black')
end
end
end
describe Users::NameService do
describe '#name' do
it 'ユーザーに名前が1つある場合はユーザー名を返す' do
user = User.create!(name: 'Mike Black')
service = described_class.new(user)
expect(service.name).to eq(user.name)
end
it 'ユーザーが名前を持ってない場合はAPIから取ったユーザー名を返す' do
user = User.create!(name: nil)
external_api = instance_double(ExternalAPI, get_name: 'Mike Jr Black')
allow(ExternalAPI).to receive(:new).with(user).and_return(external_api)
service = described_class.new(user)
expect(service.name).to eq('Mike Jr Black')
end
end
end
describe Users::NameService do
describe '#name' do
it 'ユーザーに名前が1つある場合はユーザー名を返す' do
user = instance_double(User, name: 'Mike Black')
service = described_class.new(user)
expect(service.name).to eq(user.name)
end
it 'ユーザーが名前を持ってない場合はAPIから取ったユーザー名を返す' do
user = instance_double(User, name: nil)
external_api = instance_double(ExternalAPI, get_name: 'Mike Jr Black')
allow(ExternalAPI).to receive(:new).with(user).and_return(external_api)
service = described_class.new(user)
expect(service.name).to eq('Mike Jr Black')
end
end
end
# actionview/lib/action_view/context.rb#L10
# Action View contexts are supplied to Action Controller to render a template.
# The default Action View context is ActionView::Base.
#
- # In order to work with ActionController, a Context must just include this module.
- # The initialization of the variables used by the context (@output_buffer, @view_flow,
- # and @virtual_path) is responsibility of the object that includes this module
- # (although you can call _prepare_context defined below).
+ # In order to work with Action Controller, a Context must just include this
+ # module. The initialization of the variables used by the context
+ # (@output_buffer, @view_flow, and @virtual_path) is responsibility of the
+ # object that includes this module (although you can call _prepare_context
+ # defined below).
#
- ENV.fetch("HOST", default_host)
+
+ if ENV["HOST"] && !ENV["BINDING"]
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Using the `HOST` environment to specify the IP is deprecated and will be removed in Rails 6.1.
+ Please use `BINDING` environment instead.
+ MSG
+
+ return ENV["HOST"]
+ end
+
+ ENV.fetch("BINDING", default_host)
# 同リポジトリより
class PostPolicy < ApplicationPolicy
# everyone can see any post
def show?
true
end
def update?
# `user` is a performing subject,
# `record` is a target object (post we want to update)
user.admin? || (user.id == record.user_id)
end
end
# 同記事の悪例コード
class Computer
def turn_on; end
def type; end
def replace_cpu; end
def change_user_password; end
end
class ComputerUser
def initialize
@computer = Computer.new
end
end
class Programmer < ComputerUser
def write_code
@computer.turn_on
@computer.type
end
end
class Administrator < ComputerUser
def update_user
@computer.turn_on
@computer.change_user_password
end
end
class Technician < ComputerUser
def fix_computer
@computer.replace_cpu
@computer.turn_on
end
end
# 同リポジトリより
# Use the class methods to get down to business quickly
response = HTTParty.get('http://api.stackexchange.com/2.2/questions?site=stackoverflow')
puts response.body, response.code, response.message, response.headers.inspect
# Or wrap things up in your own class
class StackExchange
include HTTParty
base_uri 'api.stackexchange.com'
def initialize(service, page)
@options = { query: { site: service, page: page } }
end
def questions
self.class.get("/2.2/questions", @options)
end
def users
self.class.get("/2.2/users", @options)
end
end
stack_exchange = StackExchange.new("stackoverflow", 1)
puts stack_exchange.questions
puts stack_exchange.users
# 同リポジトリより
User.all
# GET "https://api.example.com/users" and return an array of User objects
User.find(1)
# GET "https://api.example.com/users/1" and return a User object
@user = User.create(fullname: "Tobias Fünke")
# POST "https://api.example.com/users" with `fullname=Tobias+Fünke` and return the saved User object
@user = User.new(fullname: "Tobias Fünke")
@user.occupation = "actor"
@user.save
# POST "https://api.example.com/users" with `fullname=Tobias+Fünke&occupation=actor` and return the saved User object
@user = User.find(1)
@user.fullname = "Lindsay Fünke"
@user.save
# PUT "https://api.example.com/users/1" with `fullname=Lindsay+Fünke` and return the updated User object
"At GitHub, we restart our application server maybe every 10 minutes. It's not because we have to restart it every 10 minutes, it's because we're deploying code maybe every 10 or 15 minutes" 衝撃的なデプロイ頻度だ…
# 同書き込みより
# こうなりがち
def date=(val)
class="keyword">case val
when Date
@date = val
when Time
@date = Date.new(val.year, val.month, val.day)
when String
if val =~ /(\d{4})\s*[-\/\\]\s*(\d{1,2})\s*[-\/\\]\s*(\d{1,2})/
@date = Date.new($1.to_i,$2.to_i,$3.to_i)
else
raise ArgumentError, "Unable to parse #{val} as date"
end
when Array
if val.length == 3
@date = Date.new(val[0], val[1], val[2])
end
else
raise ArgumentError, "Unable to parse #{val} as date"
end
end
-- 同記事より
=# CREATE TABLE new_example (a int, b int, c int);
CREATE TABLE
=# INSERT INTO new_example
SELECT 3 * val, 3 * val + 1, 3 * val + 2
FROM generate_series(0, 1000000) as val;
INSERT 0 1000001
=# CREATE UNIQUE INDEX new_unique_idx ON new_example(a, b)
INCLUDE (c);
CREATE INDEX
=# VACUUM ANALYZE;
VACUUM
=# EXPLAIN ANALYZE SELECT a, b, c FROM new_example WHERE a < 10000;
QUERY PLAN
-----------------------------------------------------
Index Only Scan using new_unique_idx on new_example
(cost=0.42..116.06 rows=3408 width=12)
(actual time=0.085..2.348 rows=3334 loops=1)
Index Cond: (a < 10000)
Heap Fetches: 0
Planning Time: 1.851 ms
Execution Time: 2.840 ms
(5 rows)
Some pretty big claims in here, and it has a lot to prove, but I've been praying for something like this to come along. How big a grain of salt should I take with it?https://t.co/rk7F6i9xVB
A short word of advice on learning. Learn the actual underlying technologies, before learning abstractions. Don’t learn jQuery, learn the DOM. Don’t learn SASS, learn CSS. Don’t learn HAML, learn HTML. Don’t learn CoffeeScript, learn JavaScript. Don’t learn Handlebars, learn JavaScript ES6 templates. Don’t just use Bootstrap, learn UI patterns.
// 同リポジトリより
// GetTCPProbe returns the TCPProbeFunc if installed with AddTCPProbe, nil
// otherwise.
func (s *Stack) GetTCPProbe() TCPProbeFunc {
s.mu.Lock()
p := s.tcpProbeFunc
s.mu.Unlock()
return p
}
// RemoveTCPProbe removes an installed TCP probe.
//
// NOTE: This only ensures that endpoints created after this call do not
// have a probe attached. Endpoints already created will continue to invoke
// TCP probe.
func (s *Stack) RemoveTCPProbe() {
s.mu.Lock()
s.tcpProbeFunc = nil
s.mu.Unlock()
}
つっつきボイス: 「おお、この手の技術は実は結構前からあるんですよ」「PLCって?」「まさに電力線搬送通信(Power Line Communication)で、屋内の電源コードを使ってデータ通信する技術」「へー!」「ちなみにPLCやるときはデータをPLC経由では屋外に出せないんですよ」「出せないっていうのは、法律的に?」「そう: 厳しく制限されている」「そっかー、日本は電波法厳しいし」
class UsersController < ApplicationController
def show
@user = User.find(params[:id])
rescue ActiveRecord::RecordNotFound => e
redirect_to :root, alert: 'User not found'
end
def edit
@user = User.find(params[:id])
rescue ActiveRecord::RecordNotFound => e
redirect_to :root, alert: 'User not found'
end
end
class UsersController < ApplicationController
rescue_from ActiveRecord::RecordNotFound do |exception|
redirect_to :root, alert: 'User not found'
end
def show
@user = User.find(params[:id])
end
def edit
@user = User.find(params[:id])
end
end
class UsersController < ApplicationController
rescue_from ActiveRecord::RecordNotFound, with: :redirect_to_homepage
def show
@user = User.find(params[:id])
end
def edit
@user = User.find(params[:id])
end
protected
def redirect_to_homepage
redirect_to :root, alert: 'User not found'
end
end
require 'spec_helper'
describe NameService do
let(:name_service) { described_class.new(user) }
let(:user) { instance_double(User, full_name: 'Mike Willson') }
let(:admin_policy) { instance_double(AdminPolicy, admin?: false) }
before do
allow(AdminPolicy).to receive(:new).with(user).and_return(admin_policy)
end
describe '#full_name' do
subject { name_service.full_name }
it 'ユーザーのフルネームを返す' do
expect(subject).to eq(user.full_name)
end
context 'ユーザーがadminの場合' do
before { allow(admin_policy).to receive(:admin?).and_return(true) }
it 'フルネームの前に`ADMIN`を付けて返す' do
expect(subject).to eq("ADMIN #{user.full_name}")
end
end
end
end
class Message
def initialize(user: user)
@user = user
end
def send
user.update_message_sent_at
name = user.name(format: 'message')
Emailer.send_message(name)
end
private
attr_reader :user
end
require 'spec_helper'
describe Message do
describe '#send' do
it 'sends message' do
user = instance_double(User)
name = 'Tim'
allow(user).to receive(:name).with(format: 'message').and_return(name)
allow(user).to receive(:update_message_sent_at)
alloW(Emailer).to receive(:send_message).with(name)
message = Message.new(user: user)
message.send
expect(user).to have_received(:update_message_sent_at).once
expect(user).to have_received(:name).with(format: 'message').once
expect(Emailer).to have_received(:send_message).with(name).once
end
end
end
class SlowQueryLogger
MAX_DURATION = 3.0
def self.initialize!
ActiveSupport::Notifications.subscribe('sql.active_record') do |name, start, finish, id, payload|
duration = finish.to_f - start.to_f
if duration >= MAX_DURATION
SomeLogger.log("slow query detected: #{payload[:sql]}, duration: #{duration}")
end
end
end
end
SlowQueryLogger.initialize!
# activerecord/lib/active_record/relation/calculations.rb#L39
def count(column_name = nil)
- return super() if block_given?
+ if block_given?
+ unless column_name.nil?
+ ActiveSupport::Deprecation.warn \
+ "When `count' is called with a block, it ignores other arguments. " \
+ "This behavior is now deprecated and will result in an ArgumentError in Rails 5.3."
+ end
+
+ return super()
+ end
+
calculate(:count, column_name)
end
# 同バグより
module M3; end
module M1
include M3
end
module M2
prepend M3
end
class Sub
include M1
include M2
end
# [Sub, M1, M3, M2, Object, Kernel, BasicObject]
p Sub.ancestors
# 同記事より
class Foo
NOT_PROVIDED = Object.new
def bar(one, two: NOT_PROVIDED)
puts one.inspect
if two == NOT_PROVIDED
puts "not provided"
else
puts two.inspect
end
end
private_constant :NOT_PROVIDED
end
Russ Cox出たのか。後できこう。 / Go Time #77: Dependencies and the future of Go with Russ Cox | News and podcasts for developers | Changelog https://t.co/gLl6k91oFS
class SampleClass
def call
api.login
end
private
def api
SampleApi.new
end
end
そしてSampleClass#callメソッドをテストしたいとします。
require 'spec_helper'
describe SampleClass do
describe '#call' do
it 'calls API' do
api = SampleApi.new
allow(SampleApi).to receive(:new).and_return(api)
allow(api).to receive(:login)
sample_class = SampleClass.new
sample_class.call
expect(api).to have_received(:login).once
end
end
end
class OrderDecorator < Draper::Decorator
delegate_all
def checkout_possible?
line_items.count > 0
end
def can_be_cancelled?
cancelled_at.nil? && !complete?
end
def complete?
unpaid == 0
end
def unpaid
total - paid
end
end
ロジックをデコレータに切り出してみると、かなり読みやすいビューになりました。
<div class="checkout">
<% if @order.checkout_possible? %>
<% unless @order.complete? %>
<div class="outstanding-amount">$ <%= number_to_currency(@order.unpaid) %></div>
<% end %>
<div class="all-the-line-items"></div>
<% if @order.can_be_cancelled? %>
<%= link_to "Cancel your order", cancel_order_path(@order) %>
<% end %>
<% else %>
Your order is empty!
<% end %>
</div>
考えるまでもないことですが、アプリがユーザー入力を受け取ったらバリデーションが必要になります。Ruby on Railsアプリでバリデーションといえば真っ先に思い当たるのがモデルのバリデーションです。しかしそれ以外のレベルのバリデーションについてはどうでしょう。モデルのバリデーションがあれば完璧なソリューションになるのでしょうか?今回はRailsアプリの4つのレベルのバリデーションを簡単にご紹介しつつ、それぞれのメリットとデメリットについて説明したいと思います。お題として、Userモデルのemailカラムを使います。
class Worker
include Sidekiq::Worker
def perform(user)
# 何かすごいことをやる
end
end
user = User.first
Worker.perform_async(user)
これを扱おうと思ったら、次のようにUser#idを渡してユーザーを手動で代入しなければなりません。
class Worker
include Sidekiq::Worker
def perform(user_id)
user = User.find(user_id)
# 何かすごいことをやる
end
end
user = User.first
Worker.perform_async(user.id)
def serialize(location)
super(
"longitude" => location.longitude,
"latitude" => location.latitude
)
end
def deserialize(hash)
Location.new(hash["latitude"], hash["longitude"])
end
クラス全体は次のようになります。
class LocationSerializer < ActiveJob::Serializers::ObjectSerializer
def serialize?(argument)
argument.kind_of?(Location)
end
def serialize(location)
super(
"longitude" => location.longitude,
"latitude" => location.latitude
)
end
def deserialize(hash)
Location.new(hash["latitude"], hash["longitude"])
end
end
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def index; end # GET /users
def show; end # GET /users/1
def new; end # GET /users/new
def create; end # POST /users
def edit; end # GET /users/1/edit
def update; end # PATCH /users/1
def destroy; end # DELETE /users/1
end
# apps/web/controllers/users/index.rb
module Web::Controllers::Users
class Index
include Web::Action
def call(params)
end
end
end
# apps/web/controllers/users/show.rb
module Web::Controllers::Users
class Show
include Web::Action
def call(params)
end
end
end
...
# apps/web/controllers/users/set_user.rb
module Web::Controllers::Users
module SetUser
def self.included(action)
action.class_eval do
before :set_user
end
end
private
def set_user
@user = UserRepository.new.find(params[:id])
halt 404 if @user.nil?
end
end
end
# apps/web/controllers/users/show.rb
require_relative './set_user'
module Web::Controllers::Users
class Show
include Web::Action
include SetUser
def call(params)
# ...
end
end
end
# apps/web/controllers/users/edit.rb
require_relative './set_user'
module Web::Controllers::Users
class Edit
include Web::Action
include SetUser
def call(params)
# ...
end
end
end
# apps/web/controllers/users/index.rb
module Web::Controllers::Users
class Index
include Web::Action
expose :users # @usersインスタンス変数を公開する
def call(params)
@users = UserRepository.new.all
@another_instance_variable = {} #これはビュー/テンプレートからアクセス不可能
end
end
end
まとめ
ここまで読んだ方なら「RailsでやれることをHanamiでやろうとするとコード量が増える」とお思いかもしれませんし、実際そうかもしれません。Railsアーキテクチャの設計は「設定より規約(Convention over Configuration)」に依存する部分が多くなっています。Railsで作業していると魔法のように感じられるのはそのためです。
# PRより
# before
POST_ATTRIBUTES.map { |attr_name| [ attr_name, public_send(attr_name) ] }.to_h
# after
POST_ATTRIBUTES.index_with { |attr_name| public_send(attr_name) }
# こんなこともできる: Enumerableをハッシュに変換するときとか
# before
WEEKDAYS.each_with_object(Hash.new) do |day, intervals|
intervals[day] = [ Interval.all_day ]
end
# after
WEEKDAYS.index_with([ Interval.all_day ])
# https://github.com/rails/rails/pull/32523/files#diff-33d6b41213775779b35c3f31915e9f98R60
+ # Enumerableの項目をキーにし、ブロックで戻された値をハッシュに変換する
+ #
+ # post = Post.new(title: "hey there", body: "what's up?")
+ #
+ # %i( title body ).index_with { |attr_name| post.public_send(attr_name) }
+ # # => { title: "hey there", body: "what's up?" }
+ def index_with(default = INDEX_WITH_DEFAULT)
+ if block_given?
+ result = {}
+ each { |elem| result[elem] = yield(elem) }
+ result
+ elsif default != INDEX_WITH_DEFAULT
+ result = {}
+ each { |elem| result[elem] = default }
+ result
+ else
+ to_enum(:index_with) { size if respond_to?(:size) }
+ end
+ end
# 同PRより
class Employee < ActiveRecord::Base
end
class Company < ActiveRecord::Base
has_many(:employees)
end
company = Company.new
employee = company.employees.new
company.save # ここで失敗しても親がロールバックしなかった
# https://github.com/rails/rails/pull/32796/files#diff-829fd5510b886395117cc530518ef7f7R400
if autosave != false && (@new_record_before_save || record.new_record?)
if autosave
saved = association.insert_record(record, false)
- else
- association.insert_record(record) unless reflection.nested?
+ elsif !reflection.nested?
+ association_saved = association.insert_record(record)
+ if reflection.validate?
+ saved = association_saved
+ end
end
elsif autosave
saved = record.save(validate: false)
end
# https://github.com/rails/rails/pull/32911/files#diff-52c8dd8e01c039c37b33b3fcbdfe406aR19
def committed?
- @state == :committed
+ @state == :committed || @state == :fully_committed
+ end
+
+ def fully_committed?
+ @state == :fully_committed
end
def rolledback?
- @state == :rolledback
+ @state == :rolledback || @state == :fully_rolledback
+ end
+
+ def fully_rolledback?
+ @state == :fully_rolledback
end
...
def rollback
connection.rollback_to_savepoint(savepoint_name)
- super
+ @state.rollback!
end
def commit
connection.release_savepoint(savepoint_name)
- super
+ @state.commit!
end
# https://github.com/rails/rails/pull/32911/files#diff-174733f2db65ef1bc53e3222d6ac0e61R473
def update_attributes_from_transaction_state(transaction_state)
if transaction_state && transaction_state.finalized?
- restore_transaction_record_state if transaction_state.rolledback?
+ restore_transaction_record_state(transaction_state.fully_rolledback?) if transaction_state.rolledback?
+ force_clear_transaction_record_state if transaction_state.fully_committed?
clear_transaction_record_state if transaction_state.fully_completed?
end
end
# 同記事より
module Lettable
def let name, &blk
iv = "@#{name}"
define_method name do
return instance_variable_get iv if instance_variable_defined? iv
instance_variable_set iv, instance_eval(&blk)
end
helper_method name
define_method :"#{name}=" do |value|
instance_variable_set iv, value
end
private :"#{name}="
end
end
RuboCop 0.57 is out! A few new features, plenty of bug-fixes and small improvements https://t.co/uXfJwk98q0 This should be our highest quality release ever, as it's the only one mostly made in Japan. I dedicate the new release to the awesome #RubyKaigi conference! Enjoy!
# 同リポジトリより
class Person
# `@dynamic` annotation is to tell steep that
# the `name` and `contacts` methods are defined without def syntax.
# (Steep can skip checking if the methods are implemented.)
# @dynamic name, contacts
attr_reader :name
attr_reader :contacts
def initialize(name:)
@name = name
@contacts = []
end
def guess_country()
contacts.map do |contact|
# With case expression, simple type-case is implemented.
# `contact` has type of `Phone | Email` but in the `when` clause, contact has type of `Phone`.
case contact
when Phone
contact.country
end
end.compact.first
end
end
他にもType Systemといえば、Dmitry Petrashko, Paul Tarjan, Nelson Elhage A practical type system for Ruby at Stripe.があります。彼らはStripeの方ですが、なんとStripeにある数百万行ものRubyのコードには漸進的に型を適用するしくみがあるそうです。本当なのか。この発表はその取り組についての紹介になる予定です。 RubyKaigi 2018 タイムテーブル徹底解説より(強調は編集部)
*Cassandra of Troy -swears she psychic -never causes drama but always manages to get caught in the middle of it -to pure for the world pic.twitter.com/tHhIFS7zIW
# hashスタイルの場合
> Person.where(name: 'Andy', hidden_at: nil).to_sql
=> "SELECT \"people\".* FROM \"people\" WHERE \"people\".\"name\" = 'Andy' AND \"people\".\"hidden_at\" IS NULL"
# stringスタイルの場合
> Person.where('name = ? and hidden_at is null', 'Andy').to_sql
=> "SELECT \"people\".* FROM \"people\" WHERE (name = 'Andy' and hidden_at is null)"
Active RecordのRakeタスクを見てみると、ActiveRecord::Base.connection.migration_context.migrateに対してcallを行っていることがわかります。ここがマイグレーション実行のエントリポイントでなければなりません。(ENV['VERSION']のような)引数を付けずに呼び出すと、MigrationContext#migrateがマイグレーション用のクラスごとにMigrationProxyを作成し、続いてMigrator.new.migrateを呼び出します。
class AsyncMigration < ApplicationRecord
after_commit :enqueue_processing_job, on: :create
private
def enqueue_processing_job
MigrationProcessingJob.perform_later(async_migration_id: id)
end
end