20200103のRubyに関する記事は18件です。

Rack入門 Rack Middleware編 (3/3)

前回 は、Rackのプロトコルを理解するために簡単なアプリを作りました。
今回はRackの重要な概念であるRack Middlewareについて学びます。

目次

  1. Rack入門 概念編(1/3)
  2. Rack入門 Rack Application編 (2/3)
  3. [本記事] Rack入門 Rack Middleware編 (3/3)

Rack Middlewareとは

はじめにややこしいことを言いますが、Rackはミドルウェア(Middleware)です。
アプリサーバーとフレームワーク間のやりとりを仲介しているため、ミドルウェアと呼ばれます。

今回学ぶのはミドルウェアとは何か、ではなくてRack Middlewareについてです。
Rackには以下2つの概念があります。

  • Rack Application

    • 前回学んだ、callメソッドを持つオブジェクトのことです
    • StatusCode・Headers・Bodyの3つをレスポンスとして返します
    • Rack Endpointとも呼ばれます
  • Rack Middleware

    • 今回学ぶものです
    • Rack Middlewareはcallメソッドを持つclassである必要があります
    • Rack Applicationと違い、Responseを直接返すのではなく別の処理を呼び出しデータを加工するために使います

Rack Middlewareは、渡ってきたenv情報を加工し、次のmiddlewareまたはendpointに処理を引き渡すものです。

rack03_01.png

Hello Rack Middleware

Rack Middlewareはenv情報を加工し、次に引き渡すために利用します。
Bodyに「Hello Rack Middleware」と追加するだけのRack Middlewareを作成してみます。

class App
  def call(env)
    [200, { "Content-Type" => "text/plain" }, ["HELLO Rack Endpoint!\n\n"]]
  end
end

# Rack Middlewareは以下条件を満たす必要がある
#   - classであること
#   - initializeでappを受け取ること
#   - callメソッドを実装し、Status/Headers/Bodyを返すこと (Rack Endpointと同じ条件)
class HelloRackMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    status, headers, body = @app.call(env)

    fixed_body = ["Hello Rack Middleware!\n"] + body

    [status, headers, fixed_body]
  end
end

use HelloRackMiddleware
run App.new

Rack Middlewareをclassとして作り、useメソッドを呼び出すことでmiddlewareを追加できます。
アプリを起動し、レスポンスを見てみます。

$ curl http://localhost:9292/

*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 9292 (#0)
> GET / HTTP/1.1
> Host: localhost:9292
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Transfer-Encoding: chunked
<
Hello Rack Middleware!
HELLO Rack Endpoint!

* Connection #0 to host localhost left intact
* Closing connection 0

Hello Rack Middleware! という文字列がBodyに追加されていますね。

Middlewareはいくつでも追加できます。ただし、useの順序によってMiddlewareの動作順が異なることには注意が必要です。

class App
  def call(env)
    [200, { "Content-Type" => "text/plain" }, ["HELLO Rack Endpoint!\n\n"]]
  end
end

class HelloRackMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    status, headers, body = @app.call(env)

    fixed_body = ["Hello Rack Middleware!\n"] + body

    [status, headers, fixed_body]
  end
end

class AnotherMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    status, headers, body = @app.call(env)

    fixed_body = ["Another Middleware!\n"] + body

    [status, headers, fixed_body]
  end
end

use HelloRackMiddleware
use AnotherMiddleware
run App.new

レスポンスは以下のとおりです。

Hello Rack Middleware!
Another Middleware!
HELLO Rack Endpoint!

Content-Lengthを追加するMiddleware

もう少し実用的なmiddlewareを作ってみましょう。
Response HeaderにContent-Lengthを挿入するmiddlewareを作ります。

class App
  def call(env)
    [200, { "Content-Type" => "text/plain" }, ["HELLO WORLD!", "Hello"]]
  end
end

class ContentLengthMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    status, headers, body = @app.call(env)
    body_size = 0

    body.each do |b|
      body_size += b.bytesize
    end
    headers["Content-Length"] = body_size.to_s

    [status, headers, body]
  end
end

use ContentLengthMiddleware

run App.new

アクセスしてみましょう。

$ curl -v http://localhost:9292/ 

*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 9292 (#0)
> GET / HTTP/1.1
> Host: localhost:9292
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Content-Length: 17
<
* Connection #0 to host localhost left intact
HELLO WORLD!Hello* Closing connection 0

ちゃんとContent-Lengthが設定されていますね。

Rackにあらかじめ用意されているRack Middleware

Content-Lengthを設定するmiddlewareなど、よく使うと思われるMiddlewareはrack本家で実装されています。
いくつか主要なものを紹介します。

その他のmiddlewareは https://github.com/rack/rack/tree/master/lib/rack を参照してください。

Railsで使われているRack Middleware

Railsもrackのプロトコルに従い作成されています。
Railsで実際に使われているmiddlewareを覗いてみます。

$ rails new racktest

...省略...


$ cd racktest

$ bin/rake middleware

Running via Spring preloader in process 10795
use Webpacker::DevServerProxy
use ActionDispatch::HostAuthorization
use Rack::Sendfile
use ActionDispatch::Static
use ActionDispatch::Executor
use ActiveSupport::Cache::Strategy::LocalCache::Middleware
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use ActionDispatch::RemoteIp
use Sprockets::Rails::QuietAssets
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use WebConsole::Middleware
use ActionDispatch::DebugExceptions
use ActionDispatch::ActionableExceptions
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActiveRecord::Migration::CheckPending
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ContentSecurityPolicy::Middleware
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
use Rack::TempfileReaper
run Racktest::Application.routes

いくつかrackで実装されているMiddlewareも利用しているのがわかります。
Railsで利用しているMiddlewareについては、https://guides.rubyonrails.org/rails_on_rack.html#internal-middleware-stack にて詳しく説明されています。

まとめ

  • Rack Middlewareは、渡ってきたリクエスト/レスポンスを加工するために利用する
  • Middlewareはcallメソッドを実装したclassである必要がある

Rackをより理解するためには

全3回にわたって、Rackの基本的な概念を簡単なアプリを作りながら学びました。
思ったよりシンプルな構造でしたね。

よりRackを理解するための参考資料を、以下に記載しておきます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rack入門 Rack Application編 (2/3)

前回 はRackが必要とされた背景と、基本的な概念について説明しました。
今回は実際にRackプロトコルを使いアプリサーバーと通信するプログラムを作りながら、Rackに関する理解を深めていきます。

目次

  1. Rack入門 概念編(1/3)
  2. [本記事] Rack入門 Rack Application編 (2/3)
  3. Rack入門 Rack Middleware編 (3/3)

Hello Rack Application

基本を理解するために、簡単なRackアプリケーションを作ってみます。ブラウザーからアクセスされたら、「Hello Rack!」と返すだけのシンプルなアプリです。

まずは以下Gemfileを用意し、bundle installをしてください。アプリサーバーはpumaを使います。

source "https://rubygems.org"

gem "rack"
gem "puma"

次に config.ru というファイルを作成します。拡張子がruですが、中身はrubyのコードです。

class HelloRackApp
  # callメソッドはenvを受け取り、3つの値(StatusCode, Headers, Body)を配列として返す
  def call(env)
    [200, { "Content-Type" => "text/plain" }, ["Hello Rack!"]]
  end
end

# callメソッドを呼び出せるObjectをrunに渡し、rackアプリを起動する
run HelloRackApp.new

ターミナル上で以下のコマンドを実行し、Rackアプリを起動します。

bundle exec rackup -s puma

rackupコマンドはデフォルトではconfig.ruを読み込み、Rackアプリを起動します。
bundle exec rackup config.ru としてファイルを指定することもできます。
-s pumaはアプリサーバーの指定です。指定しない場合はpumaかthinかwebrickのいずれかが使われます。

$ bundle exec rackup -s puma

Puma starting in single mode...
* Version 4.3.1 (ruby 2.7.0-p0), codename: Mysterious Traveller
* Min threads: 0, max threads: 16
* Environment: development
* Listening on tcp://127.0.0.1:9292
* Listening on tcp://[::1]:9292
Use Ctrl-C to stop

port9292でRackアプリが起動しました。
pumaはWebサーバーとしての機能も持っているため、ブラウザーやコマンドからもアクセスできます。
curlでアクセスしてみます。

$ curl -v http://localhost:9292

*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 9292 (#0)
> GET / HTTP/1.1
> Host: localhost:9292
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Transfer-Encoding: chunked
<
* Connection #0 to host localhost left intact
Hello Rack!* Closing connection 0

Hello Rack!とレスポンスが返ってきました。Statusは200 OK、Headerには指定したContent-Type: text/plain も含まれています。ブラウザーからもアクセスしてみます。

rack02_01.png

Rackアプリケーションの形式

Rackアプリはアプリサーバーと通信するために、以下の形式(プロトコル)にしたがって作成する必要があります。

  1. RackアプリはObjectであり、callメソッドを呼び出せること。callメソッドは引数を一つ受け取ること
  2. callメソッド呼び出し後、以下3つの値を配列として返すこと
    • (HTTP) Status Code
    • Headers
    • Body
  3. Status Codeは3桁のHTTPのステータスコード(100以上の数値)であること
  4. Headersはeachメソッドを実装し、yieldの際にkey/valueのペアを渡すこと。key/valueは必ずStringであること
  5. Bodyはeachメソッドを実装し、yieldの際にStringを渡すこと

実際はさらに細かく仕様が決まっていますが、要はこれだけです。
Rackの詳細な仕様は https://github.com/rack/rack/blob/master/SPEC に記載されています。

yieldの際にStringを渡すこと、というのがわかりにくいですね。
rubyのyieldについて復習しておきます。

[1,2,3,4,5].each { |v| puts v }

上記コードの実行結果は以下となります。

1
2
3
4
5

このvがyieldしたときに渡ってくる値です。eachは以下のように実装できます。

class MyArray
  def initialize(values)
    @values = values
  end

  def each
    for v in @values
      yield v
    end
  end
end

MyArray.new([1,2,3,4,5]).each { |v| puts v }

RackのHeadersはkey/valueの2つの値を渡しますが、この値がStringであれば良いです。
Bodyは、yieldの引数として渡す値がStringであれば良いですね。

Rackアプリが満たすべき要求はこれだけです。
もう一度最初のRackアプリのコードを見てみます。

class HelloRackApp
  def call(env)
    [ 
      200,                                 # 1番目はStatusで、3桁の数値を返す

      { "Content-Type" => "text/plain" },  # 2番目はHeaders, Hashはeachメソッドを実装済
                                           # key/valueはStringあること

      ["Hello Rack!"]                      # 3番目はBody, Arrayはeachメソッドを実装済み
                                           # 中身はStringであること
    ]
  end
end
run HelloRackApp.new

上記の形式に従っていれば、立派なRackアプリケーションの完成です。

HelloRackAppはclassである必要はありません。callメソッドを持ったObjectであればよくて、Headers/Bodyもeachを持ってさえいればよいので、以下のコードでも動きます。

class MyHeader
  def initialize(bodies)
    @bodies = bodies
  end

  def each
    yield "Content-Type", "text/plain"
    yield "Content-Length", @bodies.join.bytesize.to_s # NOTE: ValueはStringにしないといけない
  end
end

class MyBody
  def initialize(bodies)
    @bodies = bodies
  end

  def each
    for v in @bodies
      yield v
    end
  end
end

# NOTE: procはcallで呼び出し可能
app = proc do |env|
  bodies = ["こんにちはRack!\n\n", "Hello Rack!\n"]

  [200, MyHeader.new(bodies), MyBody.new(bodies)]
end

run app
$ curl -v http://localhost:9292

*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 9292 (#0)
> GET / HTTP/1.1
> Host: localhost:9292
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Content-Length: 34
<
こんにちはRack!

Hello Rack!
* Connection #0 to host localhost left intact
* Closing connection 0

envを使ってリクエストを取り出す

callメソッドの引数envには、ブラウザーなどからのリクエストが格納されています。
以下のコードで中身を覗いてみます。

class HelloRackApp
  def call(env)
    require "pp"
    pp env

    [200, {}, []]
  end
end

run HelloRackApp.new

rackup後、サーバーにアクセスして結果を見てみます。

{"rack.version"=>[1, 3],
 "rack.errors"=>
  #<Rack::Lint::ErrorWrapper:0x00007f90a41c0c00 @error=#<IO:<STDERR>>>,
 "rack.multithread"=>true,
 "rack.multiprocess"=>false,
 "rack.run_once"=>false,
 "SCRIPT_NAME"=>"",
 "QUERY_STRING"=>"",
 "SERVER_PROTOCOL"=>"HTTP/1.1",
 "SERVER_SOFTWARE"=>"puma 4.3.1 Mysterious Traveller",
 "GATEWAY_INTERFACE"=>"CGI/1.2",
 "REQUEST_METHOD"=>"GET",
 "REQUEST_PATH"=>"/",
 "REQUEST_URI"=>"/",
 "HTTP_VERSION"=>"HTTP/1.1",
 "HTTP_HOST"=>"localhost:9292",
 "HTTP_USER_AGENT"=>"curl/7.64.1",
 "HTTP_ACCEPT"=>"*/*",
 "puma.request_body_wait"=>0,
 "SERVER_NAME"=>"localhost",
 "SERVER_PORT"=>"9292",
 "PATH_INFO"=>"/",
 "REMOTE_ADDR"=>"::1",
 "puma.socket"=>#<TCPSocket:fd 18, AF_INET6, ::1, 9292>,
 "rack.hijack?"=>true,
 "rack.hijack"=>
  #<Proc:0x00007f90a41c0fc0 /Users/nishio/.rbenv/versions/2.7.0/lib/ruby/gems/2.7.0/gems/rack-2.0.8/lib/rack/lint.rb:525>,
 "rack.input"=>
  #<Rack::Lint::InputWrapper:0x00007f90a41c0c28
   @input=#<Puma::NullIO:0x00007f90a585a8f8>>,
 "rack.url_scheme"=>"http",
 "rack.after_reply"=>[],
 "puma.config"=> #<Puma::Configuration:0x00007f90a41d7798>
 "rack.tempfiles"=>[]
 }

envには以下の全てが混ざったデータがHash形式で格納されています。

  1. HTTPリクエストヘッダー

    • HTTP_VERSIONやHTTP_USER_AGENTなど、HTTPリクエストヘッダーの値がkey/value形式で入っている
    • Keyは他の情報と区別するために、HTTP_ というprefixを付与している
  2. HTTPリクエスト情報

    • QUERY_STRING や REQUEST_URIなど、リクエストヘッダー以外のリクエスト情報が入っている
  3. pumaやrackなどのアプリからの情報

    • rack.version や puma.config など、各種アプリが付与した情報が入っている

envの値を利用することで、リクエストに応じたレスポンスを返せます。
試しに、以下アプリを作ってみます。

  1. http://localhost:9292/hello にアクセスしたら、Hello!を返す
  2. 上記以外のURIにアクセスしたら、404 Not Found を返す
class App
  def call(env)
    # NOTE:
    #   REQUEST_PATHも利用できるが、実装されていないケースが存在する
    #   PATH_INFOは必ず存在するので、urlはPATH_INFOを使って取り出す方が安全である
    #   https://github.com/envato/jwt_signed_request/issues/15
    path = env["PATH_INFO"]

    if path == "/hello"
      [200, { "Content-Type" => "text/plain" }, ["HELLO!"]]
    else
      [404, { "Content-Type" => "text/plain" }, ["404 Not Found"]]
    end
  end
end

run App.new

上記アプリにアクセスしてみましょう。

$ curl -v http://localhost:9292/hello

*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 9292 (#0)
> GET /hello HTTP/1.1
> Host: localhost:9292
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Transfer-Encoding: chunked
<
* Connection #0 to host localhost left intact
HELLO!* Closing connection 0
$ curl -v http://localhost:9292/testtest

*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 9292 (#0)
> GET /testtest HTTP/1.1
> Host: localhost:9292
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 404 Not Found
< Content-Type: text/plain
< Transfer-Encoding: chunked
<
* Connection #0 to host localhost left intact
404 Not Found* Closing connection 0

ファイルを返すアプリを作成する

画像ファイルをレスポンスとして返すアプリを作ってみます。
RackのBodyは文字列でさえあればなんでもよいので、以下のように実装すれば画像を返せます。

class App
  def call(env)
    path = "./neko.jpg"  # 画像ファイルは好きなものを用意

    file = File.open(path, "rb")
    image_data = [file.read]  # Bodyは必ずeachメソッドを持つ必要があるのでArrayとする

    [200, { "Content-Type" => "image/jpeg" }, image_data]
  ensure
    file.close
  end
end

run App.new

上記コードにブラウザーからアクセスしてみます。

rack02_02.png

確かにこれでも動きはしますが、ファイル全体を一度メモリ上に読み出してBodyに詰める必要があります。
読み込み効率が悪いので、レスポンスがしんどいですね。

Rackはファイルをレスポンスすることも考え、以下のようなコードを記述できます。

class App
  def call(env)
    path = "./neko.jpg"  # 画像ファイルは好きなものを用意

    file = File.open(path, "rb")

    [200, { "Content-Type" => "image/jpeg" }, file]
  end
end

run App.new

RubyのFileは以下メソッドを持っています。

  1. eachメソッド

    • eachを使えば、一定データ量ずつファイルから読み出せます
    • 例えば File.each(100) とすれば、ファイルを100バイトずつ読み出せます
  2. closeメソッド

    • ファイルはopenしたら必ずcloseする必要があります

Rackは、 close メソッドを持ったオブジェクトをbodyとして渡すと、各種処理を実行後にcloseを呼び出してくれます。

このように、Fileオブジェクトを渡すことを想定した仕様があらかじめ定義されています。

rackupコマンドを使わずrackアプリを起動する

最後に、rackupコマンドを使わずにアプリを起動するコードを紹介します。

$ bundle exec ruby config.ru

実装は以下コードとなります。

class App
  def call(env)
    [200, { "Content-Type": "text/plain" }, ["Hello Rack!"]]
  end
end

require "rack"

app = Rack::Builder.new do
  run App.new
end

require "rack/handler/puma"
Rack::Handler::Puma.run app

rackpuma関係のファイルをrequireし、Rack::Builderで初期化したものをPumaのHandlerに渡せばよいです。
rackupコマンドは、このあたりのファイル読み込みをあらかじめ自動で行ってくれる便利コマンドです。

まとめ

  • Rackアプリはあらかじめ決められたRackプロトコルにしたがって作成する
  • Rackアプリはcallメソッドを持ったオブジェクトであること
  • callメソッドは、引数を1つ受け取り、StatusCode・Headers・Bodyのペアを返す
  • callメソッドの引数(env)から、リクエスト情報を取り出せる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rack入門 概念編 (1/3)

RailsやSinatraなどのRuby製Webフレームワークを利用されている方は、Rackというキーワードを一度は目にしたことがあるのではないでしょうか。

よく聞くけど詳しくは知らない、そんなやつがRackです。

今回は自分の知識の整理も兼ねて、Rackとは何ものなのかについて調べたメモを、ここに残します。長かったので、全3回に分割しています。

目次

  1. [本記事] Rack入門 概念編(1/3)
  2. Rack入門 Rack Application編 (2/3)
  3. Rack入門 Rack Middleware編 (3/3)

Rackとは何か。ひとことで

  • Webアプリケーションサーバーとアプリケーションを接続するための標準化されたインターフェースのこと

Rackとは何か。詳細

Rackの公式リポジトリでは、Rackは以下のように記載されています。

a modular Ruby webserver interface

訳) モジュール化されたRuby Webサーバーインターフェース

そして、 Supported web servers として PumaやUnicorn が上げられています。

このWebサーバーという表現はいろいろとややこしいので、本稿ではPuma/Unicornのことは アプリケーションサーバー(アプリサーバー) と呼びます。

Rackはなぜ必要なのか。その前に基本的な概念についておさらいします。

アプリケーションサーバーとは

アプリケーションサーバーって、なんでしたでしょうか。
ここで一度振り返っておきます。

Webサーバー、アプリサーバーの代表的なソフトウェアは以下のとおりです。

  • Webサーバー

    • nginx, apache など
  • アプリケーションサーバー

    • Rubyでは Puma, Unicorn など
    • JavaではTomcat など

Webシステムの多くは、いわゆる3層アーキテクチャと呼ばれる構成で設計されており、Webサーバーとアプリサーバーは別に運用します。
以下のような概念図をよく目にするのではないでしょうか。

rack01_01.png

DBサーバーについては、今回は関係ないので説明を割愛します。

別にこの構成に従わなくてもウェブシステムは運用できますが、システムの性能を引き出すために3層アーキテクチャを採用することが多いです。

Webサーバー

Webサーバーは、ユーザーからのリクエストを受け取り、処理結果をレスポンスとして返すソフトウェアのことです。

受け取ったリクエストはWebサーバー自身が処理することもありますし、そのまま処理をアプリサーバーに委譲することもあります。
静的なコンテンツ(画像やファイル)は、Webサーバーでデータを引っ張ってきて返すことが多いです。
ビジネスロジックはRubyなどで書いたアプリに処理を委譲して、処理結果をレスポンスとして返すことが多いです。

nginxなどのWebサーバーは、大量のアクセスが来ても素早く効率的にさばくための機構を持っています。
またSSL通信やデータを圧縮して返すなど、Webの面倒事を引き受けてくれるソフトウェアです。

アプリケーションサーバー

アプリサーバーは、アプリをあらかじめ起動しておき、リクエストが来たらすばやく処理をするためのサーバーです。

アプリサーバーがないと、どうなるでしょうか。

昔のWebシステムでは、アプリサーバーは立てませんでした。
CGI全盛期は、Webサーバーにリクエストが来た後、プログラムを都度起動していました。
リクエストのたびにプログラムの起動から始めるというのは、巨大なアプリケーションになればなるほど起動コストが無視できないですよね。

なので、あらかじめプログラムをメモリ上に読み込んでおき、アクセスが来たら処理を高速に返せるようアプリサーバーを用意します。

PumaやUnicornなどのアプリサーバーは、Webサーバーとしての最低限の機能も持っています。
Webサーバーを立てずにPumaだけでもシステムを運用できます。
ただ、大量のアクセスを効率的にさばくことはPuma単体ではできない(そこはWebサーバーの役割としている)ので、一般的には前段にWebサーバーを立ててリクエストを処理をします。

Rackはどこで使われ、なぜ必要か

Rackはアプリサーバーとアプリ(Webフレームワーク)間のデータのやり取りに利用します。

PumaやUnicornといったアプリサーバーは、特定のアプリ専用(たとえばRails専用)というわけではなく、RackのプロトコルにしたがっているWebフレームワークであれば何でも利用できます。

rack01_02.png

Rackに対応しているRuby製のフレームワークは、Railsの他にSinatra, Padrino, Hanamiなどがあります。

アプリサーバーとアプリ間の通信仕様を定めておく(=インターフェースの標準化をしておく)ことで、
アプリサーバーとアプリケーションフレームワークの組み合わせを自由に変えることができます
Rails専用のアプリサーバーを作る、Sinatra専用のアプリサーバーを作るっていうのは大変ですよね。

アプリ・フレームワーク間の標準インターフェースを作るという流れは、PythonのWSGI(Web Server Gateway Interface)がはじめました。

PythonはもともとWebフレームワークとフレームワーク専用のアプリサーバーが乱立していましたが、WSGIはアプリとサーバーのその組み合わせを自由に変えられるよう標準インターフェースを定めました。
これにより、移植性の高いアプリサーバー/フレームワークを作ることができるようになりました。

Rackの細かいところはWSGIと異なりますが、このWSGIの影響を強く受けています。

まとめ

  • アプリサーバーとは、アプリをあらかじめ起動しておきレスポンスを高速に返すためのサーバー
  • Rackはアプリサーバーとフレームワーク(アプリ)間のインターフェースを定めている
  • Rackのプロトコルに従うことで、アプリサーバーとWebフレームワークの組み合わせを自由に変えることができる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】devise_token_authで新規ユーザー登録時にUnpermitted parameters: が出たときの対処法

はじめに

gem devise_token_authを使っていて、新規ユーザー登録時に以下エラーが出たときの対処法を残します。

Unpermitted parameters: 保存されて欲しいカラム名

自分でUserテーブルにカラムを追加して、保存しようとしたら
発生する内容です。

今回、自分の場合は:age:genderを追加しようとしたところ発生しました。(以下参照)

Unpermitted parameters: :age, :gender

これを解決していきます。

環境

OS: macOS Catalina 10.15.1
zsh: 5.7.1
Ruby: 2.6.5
Rails: 6.0.2.1

結論:registrations_controllerに追記

registrations_controller
class Api::V1::Auth::RegistrationsController < DeviseTokenAuth::RegistrationsController

  private

  def sign_up_params
    # ここに :age, :genderを追記
    params.permit(:name, :email, :age, :gender, :password, :password_confirmation)
  end

  def account_update_params
    params.permit(:name, :email)
  end
end

※反映するにはrails server再起動が必要です。

理由

Strong Parameterではじかれてしまっているのが問題でした。

sign_up_paramsの中に追記することで、新規ユーザー登録のときだけ許可されるようになります。

こいつも許可してあげてよ!という指定をすればOKですね。

おわりに

最後まで読んで頂きありがとうございました:bow_tone1:

どなたかの参考になれば幸いです:relaxed:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

#Stripe API / サブスクリプションのスケジュール登録が開始済みでも予約中でも SubscriptionSchedule を Update してキャンセル登録する例 / #Ruby

points

  • 1ヶ月サイクルのプラン1個だけをスケジュール登録して、あとから end_date = キャンセル日を登録する場合
  • Subscription に対してであれば Update で cance_at を登録すれば良いのだが、SubscriptionSchedule が開始していない場合は、そもそも Subscription が発生していないため、できない
  • どちらの場合も SubscriptionSchedule への update で APIリクエストを完結させたいが、SubscriptionSchedule はちょっと癖があるやつ
  • SubscriptionSchedule が active = 開始している場合は、 current_phase.start_date を起点に、そこから数ヶ月後の end_date をぴったりと指定する。なぜなら中途半端な日時を指定すると、日割り計算的な請求が発生してしまうから。
  • SubscriptionSchedule が not_started= 開始していない場合は、 未来に開始される最初のフェーズ = phasse の start_date を起点に、そこから数ヶ月後の end_date をぴったりと指定する。なぜなら中途半端な日時を指定すると、日割り計算的な請求が発生してしまうから。ちなみに現在フェーズの期間 ( current_phase ) はサブスクリプションの請求サイクルとは異なる。
  • SubscriptionSchedule Update で phase 単位で iterations を指定することもできるだろうが、start_date / end_date を明確に与えてあげたほうが、場合によってはやりやすそうだ。いや、やりやすい方でお願いします。

Docs

参考 - Stripeの基本

日本正式リリースしたStripeを使ってサブスクリプション型決済システムを実装する - Qiita
https://qiita.com/tady/items/7617e62b2a5402ebd0fb

Stripe Billing 101 - Qiita
https://qiita.com/y_toku/items/235b5e7ee00792edcbbf

Stripe初心者のための基本的な使い方(Rails編) - Qiita
https://qiita.com/ryouzi/items/6ee8f277471aa3b02f7b

Docs

Code

# Docs

# https://stripe.com/docs/billing/subscriptions/subscription-schedules
# https://stripe.com/docs/billing/subscriptions/subscription-schedules/use-cases
#
# https://stripe.com/docs/api/subscription_schedules/release
# https://support.stripe.com/questions/create-update-and-schedule-subscriptions

require 'active_support/core_ext'
require 'stripe'

Stripe::api_key = ENV['STRIPE_SECRET_KEY']

[1.month, 3.months].each do |end_at_from|
  product1 = Stripe::Product.create(name: "Gold plan #{rand(9999999999)}")
  plan1 = Stripe::Plan.create(interval: 'month', currency: 'jpy', amount: 5000, product: product1.id, usage_type: 'licensed')

  tax_rate = Stripe::TaxRate.create(display_name: 'Tax Rate', percentage: 10.0, inclusive: false)
  customer = Stripe::Customer.create
  payment_method = Stripe::PaymentMethod.create(type: 'card', card: { number: '4242424242424242', exp_year: 2030, exp_month: 01})
  customer_payment_method = Stripe::PaymentMethod.attach(payment_method.id, customer: customer.id)

  def put_subscription_schedule(subscription_schedule, message)
    puts '-' * 100
    puts "Subscription Schedule"
    puts message
    puts '-' * 100
    puts subscription_schedule
    puts '-' * 100
    puts "https://dashboard.stripe.com/test/subscription_schedules/#{subscription_schedule.id}"
  end

  # https://stripe.com/docs/api/subscription_schedules/create
  soon_start_subscription_schedule = Stripe::SubscriptionSchedule.create(
    {
      customer: customer.id,
      start_date: Time.now.to_i + 5,
      default_settings: {
        default_payment_method: customer_payment_method.id,
      },
      phases: [
        {
          plans:
            [
              { plan: plan1.id, quantity: 1 },
            ],
          prorate: false,
          default_tax_rates: [tax_rate],
        },
      ],
    }
  )

  puts '-' * 100
  puts "Wait until subscription schedule starts"
  puts '-' * 100
  until soon_start_subscription_schedule.status == 'active' do
    soon_start_subscription_schedule = Stripe::SubscriptionSchedule.retrieve(soon_start_subscription_schedule.id)
    puts soon_start_subscription_schedule.status
    sleep 2
  end

  started_subscription_schedule = Stripe::SubscriptionSchedule.retrieve(id: soon_start_subscription_schedule.id, expand: ['subscription'])

  future_subscription_schedule = Stripe::SubscriptionSchedule.create(
    {
      customer: customer.id,
      start_date: Time.now.to_i + 100*24*60*60,
      default_settings: {
        default_payment_method: customer_payment_method.id,
      },
      phases: [
        {
          plans:
            [
              { plan: plan1.id, quantity: 1 },
            ],
          prorate: false,
          default_tax_rates: [tax_rate],
        },
      ],
    }
  )

  # current_phase.start_date is not subscription cycle start_date
  def update_subscription_schedule(base_subscription_schedule)
    if base_subscription_schedule.status == 'active'
      start_date = base_subscription_schedule.current_phase.start_date
    elsif base_subscription_schedule.status == 'not_started'
      start_date = base_subscription_schedule.phases[0].start_date
    else
      raise
    end

    end_date = Time.at(start_date).since(3.months).to_i

    Stripe::SubscriptionSchedule.update(
      base_subscription_schedule.id,
      {
        # Set "cancel" not "releasse"
        end_behavior: 'cancel',
        phases: [
          {
            plans:
              [
                {
                  plan:     base_subscription_schedule.phases[0].plans[0].plan,
                  quantity: base_subscription_schedule.phases[0].plans[0].quantity
                },
              ],
            # prorate does not effect to Subscription or SubscriptionSchedule Canceling
            # prorate: true / false,
            default_tax_rates: base_subscription_schedule.phases[0].default_tax_rates.map(&:id),
            # For Update You must set start_date in first phase not in subscription schedule directly
            start_date: start_date,
            # If you do not want to get upcoming invoice on Subscription
            # Then you must specify end_date with Subscription natural cycle interval
            end_date: end_date
          },
        ]
      }
    )
  end

  updated_started_subscription_schedule = update_subscription_schedule(started_subscription_schedule)
  updated_future_subscription_schedule = update_subscription_schedule(future_subscription_schedule)

  put_subscription_schedule(updated_started_subscription_schedule, 'UPDATED')
  put_subscription_schedule(updated_future_subscription_schedule, 'UPDATED')
end

# NOTE
# SubscriptionSchedule current_phase is not same as Subscription cycle
# it is "phase" start_date and end_date
#
# If phase end_date is 3.months after then ...
#
# Time.at(updated_started_subscription_schedule.current_phase.start_date)
# => 2020-01-02 17:44:23 +0900
# Time.at(updated_started_subscription_schedule.current_phase.end_date)
# => 2020-04-02 17:44:23 +0900

Started Subscription - cancel at current phase

Upcoming invoice leaves from dashboard

image

Not Started Subscription - cancel at first phase

Theres Starts Ends in both in future
Theres upcoming invoice becauce Schedule will start in future
Not current invoice because Schedule not started

image

Started Subscription - cancel at future phase

Change end_date in code and run Case

image

Not Started Subscription - cancel at not first phase

Change end_date in code and run Case

image

Result

  • This is run code resutls example but not correctly same as Image capture images in this article.
----------------------------------------------------------------------------------------------------
Wait until subscription schedule starts
----------------------------------------------------------------------------------------------------
not_started
not_started
not_started
not_started
not_started
not_started
active
----------------------------------------------------------------------------------------------------
Subscription Schedule
CREATED
----------------------------------------------------------------------------------------------------
{
  "id": "sub_sched_1FwPNZCmti5jpytUe3xhuCdN",
  "object": "subscription_schedule",
  "canceled_at": null,
  "completed_at": null,
  "created": 1577955005,
  "current_phase": null,
  "customer": "cus_GTM5yQ0vRozYUW",
  "default_settings": {
    "billing_thresholds": null,
    "collection_method": "charge_automatically",
    "default_payment_method": "pm_1FwPNHCmti5jpytURUDXSKtE",
    "default_source": null,
    "invoice_settings": null
  },
  "end_behavior": "release",
  "livemode": false,
  "metadata": {
  },
  "phases": [
    {
      "application_fee_percent": null,
      "billing_thresholds": null,
      "collection_method": null,
      "coupon": null,
      "default_payment_method": null,
      "default_tax_rates": [
        {
          "id": "txr_1FwPNHCmti5jpytUA4yh9gmW",
          "object": "tax_rate",
          "active": true,
          "created": 1577954987,
          "description": null,
          "display_name": "Tax Rate",
          "inclusive": false,
          "jurisdiction": null,
          "livemode": false,
          "metadata": {
          },
          "percentage": 10.0
        }
      ],
      "end_date": 1589187005,
      "invoice_settings": null,
      "plans": [
        {
          "billing_thresholds": null,
          "plan": "plan_GTM5XKzi7c1qy8",
          "quantity": 1,
          "tax_rates": [

          ]
        }
      ],
      "prorate": false,
      "start_date": 1586595005,
      "tax_percent": 10.0,
      "trial_end": null
    }
  ],
  "released_at": null,
  "released_subscription": null,
  "renewal_interval": null,
  "revision": "sub_sched_rev_1FwPNZCmti5jpytUWUBqK4lG",
  "status": "not_started",
  "subscription": null
}
----------------------------------------------------------------------------------------------------
https://dashboard.stripe.com/test/subscription_schedules/sub_sched_1FwPNZCmti5jpytUe3xhuCdN
----------------------------------------------------------------------------------------------------
Subscription Schedule
UPDATED
----------------------------------------------------------------------------------------------------
{
  "id": "sub_sched_1FwPNJCmti5jpytU8hFkMssH",
  "object": "subscription_schedule",
  "canceled_at": null,
  "completed_at": null,
  "created": 1577954989,
  "current_phase": {
    "end_date": 1585817393,
    "start_date": 1577954993
  },
  "customer": "cus_GTM5yQ0vRozYUW",
  "default_settings": {
    "billing_thresholds": null,
    "collection_method": "charge_automatically",
    "default_payment_method": "pm_1FwPNHCmti5jpytURUDXSKtE",
    "default_source": null,
    "invoice_settings": {
      "days_until_due": null
    }
  },
  "end_behavior": "cancel",
  "livemode": false,
  "metadata": {
  },
  "phases": [
    {
      "application_fee_percent": null,
      "billing_thresholds": null,
      "collection_method": null,
      "coupon": null,
      "default_payment_method": null,
      "default_tax_rates": [
        {
          "id": "txr_1FwPNHCmti5jpytUA4yh9gmW",
          "object": "tax_rate",
          "active": true,
          "created": 1577954987,
          "description": null,
          "display_name": "Tax Rate",
          "inclusive": false,
          "jurisdiction": null,
          "livemode": false,
          "metadata": {
          },
          "percentage": 10.0
        }
      ],
      "end_date": 1585817393,
      "invoice_settings": null,
      "plans": [
        {
          "billing_thresholds": null,
          "plan": "plan_GTM5XKzi7c1qy8",
          "quantity": 1,
          "tax_rates": [

          ]
        }
      ],
      "prorate": true,
      "start_date": 1577954993,
      "tax_percent": 10.0,
      "trial_end": null
    }
  ],
  "released_at": null,
  "released_subscription": null,
  "renewal_interval": null,
  "revision": "sub_sched_rev_1FwPNZCmti5jpytUoCQuoPaf",
  "status": "active",
  "subscription": "sub_GTM59xT4BS5E3j"
}
----------------------------------------------------------------------------------------------------
https://dashboard.stripe.com/test/subscription_schedules/sub_sched_1FwPNJCmti5jpytU8hFkMssH
----------------------------------------------------------------------------------------------------
Subscription Schedule
UPDATED
----------------------------------------------------------------------------------------------------
{
  "id": "sub_sched_1FwPNZCmti5jpytUe3xhuCdN",
  "object": "subscription_schedule",
  "canceled_at": null,
  "completed_at": null,
  "created": 1577955005,
  "current_phase": null,
  "customer": "cus_GTM5yQ0vRozYUW",
  "default_settings": {
    "billing_thresholds": null,
    "collection_method": "charge_automatically",
    "default_payment_method": "pm_1FwPNHCmti5jpytURUDXSKtE",
    "default_source": null,
    "invoice_settings": {
      "days_until_due": null
    }
  },
  "end_behavior": "cancel",
  "livemode": false,
  "metadata": {
  },
  "phases": [
    {
      "application_fee_percent": null,
      "billing_thresholds": null,
      "collection_method": null,
      "coupon": null,
      "default_payment_method": null,
      "default_tax_rates": [
        {
          "id": "txr_1FwPNHCmti5jpytUA4yh9gmW",
          "object": "tax_rate",
          "active": true,
          "created": 1577954987,
          "description": null,
          "display_name": "Tax Rate",
          "inclusive": false,
          "jurisdiction": null,
          "livemode": false,
          "metadata": {
          },
          "percentage": 10.0
        }
      ],
      "end_date": 1594457405,
      "invoice_settings": null,
      "plans": [
        {
          "billing_thresholds": null,
          "plan": "plan_GTM5XKzi7c1qy8",
          "quantity": 1,
          "tax_rates": [

          ]
        }
      ],
      "prorate": true,
      "start_date": 1586595005,
      "tax_percent": 10.0,
      "trial_end": null
    }
  ],
  "released_at": null,
  "released_subscription": null,
  "renewal_interval": null,
  "revision": "sub_sched_rev_1FwPNaCmti5jpytUnlvLBTke",
  "status": "not_started",
  "subscription": null
}
----------------------------------------------------------------------------------------------------
https://dashboard.stripe.com/test/subscription_schedules/sub_sched_1FwPNZCmti5jpytUe3xhuCdN
----------------------------------------------------------------------------------------------------
Wait until subscription schedule starts
----------------------------------------------------------------------------------------------------
not_started
not_started
not_started
not_started
not_started
not_started
not_started
not_started
not_started
not_started
not_started
not_started
not_started
not_started
not_started
not_started
not_started
not_started
not_started
not_started
not_started
not_started
not_started
not_started
not_started
active
----------------------------------------------------------------------------------------------------
Subscription Schedule
CREATED
----------------------------------------------------------------------------------------------------
{
  "id": "sub_sched_1FwPOZCmti5jpytUqLGjGd5a",
  "object": "subscription_schedule",
  "canceled_at": null,
  "completed_at": null,
  "created": 1577955067,
  "current_phase": null,
  "customer": "cus_GTM5Wlau0N9HHj",
  "default_settings": {
    "billing_thresholds": null,
    "collection_method": "charge_automatically",
    "default_payment_method": "pm_1FwPNcCmti5jpytUPG6hdjRk",
    "default_source": null,
    "invoice_settings": null
  },
  "end_behavior": "release",
  "livemode": false,
  "metadata": {
  },
  "phases": [
    {
      "application_fee_percent": null,
      "billing_thresholds": null,
      "collection_method": null,
      "coupon": null,
      "default_payment_method": null,
      "default_tax_rates": [
        {
          "id": "txr_1FwPNbCmti5jpytUOMYY1jCE",
          "object": "tax_rate",
          "active": true,
          "created": 1577955007,
          "description": null,
          "display_name": "Tax Rate",
          "inclusive": false,
          "jurisdiction": null,
          "livemode": false,
          "metadata": {
          },
          "percentage": 10.0
        }
      ],
      "end_date": 1589187067,
      "invoice_settings": null,
      "plans": [
        {
          "billing_thresholds": null,
          "plan": "plan_GTM5akDaPHCKOr",
          "quantity": 1,
          "tax_rates": [

          ]
        }
      ],
      "prorate": false,
      "start_date": 1586595067,
      "tax_percent": 10.0,
      "trial_end": null
    }
  ],
  "released_at": null,
  "released_subscription": null,
  "renewal_interval": null,
  "revision": "sub_sched_rev_1FwPOZCmti5jpytUsHM0JqYJ",
  "status": "not_started",
  "subscription": null
}
----------------------------------------------------------------------------------------------------
https://dashboard.stripe.com/test/subscription_schedules/sub_sched_1FwPOZCmti5jpytUqLGjGd5a
----------------------------------------------------------------------------------------------------
Subscription Schedule
UPDATED
----------------------------------------------------------------------------------------------------
{
  "id": "sub_sched_1FwPNdCmti5jpytUnvii1fUR",
  "object": "subscription_schedule",
  "canceled_at": null,
  "completed_at": null,
  "created": 1577955009,
  "current_phase": {
    "end_date": 1585817414,
    "start_date": 1577955014
  },
  "customer": "cus_GTM5Wlau0N9HHj",
  "default_settings": {
    "billing_thresholds": null,
    "collection_method": "charge_automatically",
    "default_payment_method": "pm_1FwPNcCmti5jpytUPG6hdjRk",
    "default_source": null,
    "invoice_settings": {
      "days_until_due": null
    }
  },
  "end_behavior": "cancel",
  "livemode": false,
  "metadata": {
  },
  "phases": [
    {
      "application_fee_percent": null,
      "billing_thresholds": null,
      "collection_method": null,
      "coupon": null,
      "default_payment_method": null,
      "default_tax_rates": [
        {
          "id": "txr_1FwPNbCmti5jpytUOMYY1jCE",
          "object": "tax_rate",
          "active": true,
          "created": 1577955007,
          "description": null,
          "display_name": "Tax Rate",
          "inclusive": false,
          "jurisdiction": null,
          "livemode": false,
          "metadata": {
          },
          "percentage": 10.0
        }
      ],
      "end_date": 1585817414,
      "invoice_settings": null,
      "plans": [
        {
          "billing_thresholds": null,
          "plan": "plan_GTM5akDaPHCKOr",
          "quantity": 1,
          "tax_rates": [

          ]
        }
      ],
      "prorate": true,
      "start_date": 1577955014,
      "tax_percent": 10.0,
      "trial_end": null
    }
  ],
  "released_at": null,
  "released_subscription": null,
  "renewal_interval": null,
  "revision": "sub_sched_rev_1FwPOaCmti5jpytU2OqMiUUC",
  "status": "active",
  "subscription": "sub_GTM64npF4L1rop"
}
----------------------------------------------------------------------------------------------------
https://dashboard.stripe.com/test/subscription_schedules/sub_sched_1FwPNdCmti5jpytUnvii1fUR
----------------------------------------------------------------------------------------------------
Subscription Schedule
UPDATED
----------------------------------------------------------------------------------------------------
{
  "id": "sub_sched_1FwPOZCmti5jpytUqLGjGd5a",
  "object": "subscription_schedule",
  "canceled_at": null,
  "completed_at": null,
  "created": 1577955067,
  "current_phase": null,
  "customer": "cus_GTM5Wlau0N9HHj",
  "default_settings": {
    "billing_thresholds": null,
    "collection_method": "charge_automatically",
    "default_payment_method": "pm_1FwPNcCmti5jpytUPG6hdjRk",
    "default_source": null,
    "invoice_settings": {
      "days_until_due": null
    }
  },
  "end_behavior": "cancel",
  "livemode": false,
  "metadata": {
  },
  "phases": [
    {
      "application_fee_percent": null,
      "billing_thresholds": null,
      "collection_method": null,
      "coupon": null,
      "default_payment_method": null,
      "default_tax_rates": [
        {
          "id": "txr_1FwPNbCmti5jpytUOMYY1jCE",
          "object": "tax_rate",
          "active": true,
          "created": 1577955007,
          "description": null,
          "display_name": "Tax Rate",
          "inclusive": false,
          "jurisdiction": null,
          "livemode": false,
          "metadata": {
          },
          "percentage": 10.0
        }
      ],
      "end_date": 1594457467,
      "invoice_settings": null,
      "plans": [
        {
          "billing_thresholds": null,
          "plan": "plan_GTM5akDaPHCKOr",
          "quantity": 1,
          "tax_rates": [

          ]
        }
      ],
      "prorate": true,
      "start_date": 1586595067,
      "tax_percent": 10.0,
      "trial_end": null
    }
  ],
  "released_at": null,
  "released_subscription": null,
  "renewal_interval": null,
  "revision": "sub_sched_rev_1FwPObCmti5jpytU5eKvVg9W",
  "status": "not_started",
  "subscription": null
}
----------------------------------------------------------------------------------------------------
https://dashboard.stripe.com/test/subscription_schedules/sub_sched_1FwPOZCmti5jpytUqLGjGd5a


Original by Github issue

https://github.com/YumaInaura/YumaInaura/issues/2915

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

3ヶ月で Rails 4.2 → 5.2 にした軌跡

概要

資格スクエアの PM をやっています岩瀬と言います。資格スクエアは、法律系難関資格に特化したオンライン学習サービスです。

サービスを開始してから数年が経ち、1つの大きな問題がありました。それは Rails のメジャーアップグレードです。

長年の負債が溜まっていたので、なかなかメジャーアップグレードできずに苦しんでいたのですが、フリーランスのカルパスさん(yhirano55)の加入で一気に進みました。

カルパスさんが方針を考えて実装し、それをレビューでサポートしたラスタムさん(rastamhadi)はじめ、チーム一丸となって取り組んだ結果、わずか3ヶ月で成し遂げられるミラクルを目撃したので、その内容をまとめました。

記事の目的

テストカバレッジが低くても、Rails のメジャーアップグレードができるという知見を共有するためです。

背景

Rails メジャーアップグレードは適宜対応していくのが最善ですが、以下の課題があったため、作業が難航していた状態でした。

  • サービス成長を優先していたため、Rails のメジャーアップグレードが後回しになっていた
  • サービス開始から5年以上が経過していたため、負債が溜まりメンテナンスコストが高くなっていた
  • サービス立ち上げ時はテストコードを書いていなかったため、カバレッジが 32 %と低い状態であった

ですが、Rails 6.0 が出るにあたって、いよいよ Rails 4 系のサポートが終了するため、早急に Rails 5 系に上げる必要がありました。

どうしようか悩んでいた時、ラスタムさんの紹介によってカルパスさんが加入して本格的に着手することになりました。

バージョン

以下は Rails のメジャーアップグレード作業に着手した際に使用していたバージョンです。

名称 バージョン
Rails 4.2.11.1
Ruby 2.3.6

規模

資格スクエア は、2013年11月にサービスを立ち上げてから5年以上が経過しており、中規模と言えるような状態になっていました。規模の目安として、あくまで参考ですが2020年1月現在での各モジュールの数は以下になります。

名称
Controllers 177個
Models 190個
Code LOC 108,612行

戦略

上記の背景と規模を踏まえて、次の戦略で進めることにしました。

  1. メジャーアップグレードを優先して最小のテストカバレッジで進める
  2. テストでカバーできないものは動作確認で洗い出す
  3. 動作確認でも洗い出せなかったものは本番環境に反映後、即対応する

タイムライン

今回の取り組んだアップグレードのざっくりしたタイムラインは以下になります。

年月日 内容
2019年8月27日 Rails 5.0 へメジャーアップグレード作業開始
2019年10月17日 Rails 5.0.7.2 にアップグレード完了
2019年10月23日 Ruby 2.6.5 にアップグレード完了
2019年11月19日 Rails 5.2.3 にアップグレード完了

手順

Rails 4.2 → 5.0

それではポイントを押さえて、実際に行ったメジャーアップグレード手順を紹介します。

1. 足場の確保

まずは開発環境を整え、テスト実行の足場を確保しました。

  • 落ちているテストを直す
  • 開発用途の gem を更新する(lock_diff を活用して changelog を追う)

2. モデルとテーブル定義の不一致を解消する

モデルによるレコード生成をテストコードで担保したいのですが、そもそもモデルとテーブル定義が不一致だったので、それを解消しました。

  • annotate でモデルとテーブル定義の制約条件(validation)が一致していることを確認しやすいようにする
  • 開発環境と本番環境のテーブル定義を一致させる
  • テーブルの NOT NULL 制約とモデルの presence validation を一致させる
  • テーブルの unique index とモデルの uniqueness validation を一致させる

3. 全モデルに最低限の単体テストを書く

ここで全モデルのテストコードを書きました。単体テストを網羅するのはあまりにも大変なため、最低限レコード生成を担保するテストを整備しました。

  • 全モデルの factory を定義する
  • レコードの生成が valid となる最低限のテストを用意する

ex. spec/models/certificate_spec.rb

require 'rails_helper'

RSpec.describe Certificate do
  describe 'factory' do
    it 'has a valid factory' do
      expect(build(:certificate)).to be_valid
      expect(create(:certificate)).to be_persisted
    end
  end
end

4. 管理画面に最低限の結合テストを書く

管理画面は gem を使わず、独自実装であったため結合テストで最低限画面表示できることを担保しました。

ex. spec/features/admin/certificates_spec.rb

require 'rails_helper'

RSpec.feature '管理ツール 資格管理', type: :feature do
  scenario '一覧が閲覧できる' do
    prepare_logged_in_admin_user

    within(:css, '#sidebar') do
      click_link '資格管理'
    end

    expect(page).to have_current_path admin_certificates_path
    expect(page).to have_selector 'h1.page-header', text: '資格管理'
  end
end

5. Rails 5.0 の変更点に応じて修正する

Rails 4.2からRails 5.0へのアップグレードに記載されている内容を1つ1つ確認しながら対応しました。

  • Rails 5.0 から全 Model は ApplicationRecord という抽象クラスを継承する形式のため変更する
  • ActionController::Parameters は今後 HashWithIndifferentAccess を継承しないので対応する
  • migrationファイルを置き換える

ex. db/migrate/20150302014237_create_admin_users.rb

- class CreateAdminUsers < ActiveRecord::Migration
+ class CreateAdminUsers < ActiveRecord::Migration[4.2]

6. Rails 5.0 に上げる Pull Request を作成する

ここで Rails 5.0 に上げる目処が立ったので、 Pull Request を作成しました。

  • gem の依存バージョンを解決する
  • Rails 5.0.7.2 にアップグレードする
  • 設定ファイルの更新(app:update)
  • テストが実行できる
  • テストを実行し、落ちているものを修正する
  • すべての警告を抑止する

7. テスト環境で隅々まで動作確認する

最低限のテストコードでカバーしているため、テスト環境で動作確認を行いました。その動作確認の結果、エラーになった箇所を1つ1つ解決していきました。資格スクエアで遭遇したエラーは以下になります。

  • Rails 5.0 では存在しないコールバックをスキップすると ArgumentError になるので不要なコールバックを削除する
  • params をそのまま使うと Rails 5.0 からセキュリティの都合で ArgumentError になるため、hash に変更する
  • Rails 5.0 で collection_check_boxes の hidden 要素が最後から最初に変わったため、修正する

8. Rails 5.0 を本番環境にデプロイする

いよいよ Rails 5.0 に変更できる準備が整ったのでデプロイしました。
何か問題が発生しても対応できるようにエンジニアメンバーが稼働している勤務時間帯に反映しました。

9. 本番環境で発生した問題に対処する

実際、デプロイ後にやはり問題が発生しました。具体的には以下のものになります。

  • 本番反映前の Rails 4.2 で作られたジョブを Rails 5.0 で動作させるために一時的にパッチを当てる

ex. config/initializers/temporary_active_record_patch.rb

class ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter
  class MysqlDateTime < ActiveRecord::Type::DateTime
  end
end
  • Rails アップグレードに伴い、セッションが切れる問題が発生したためにセッションをクリアするコマンドを実行する
$ RAILS_ENV=production bundle exec rails r 'Rails.cache.clear'

これで Rails 4.2 → 5.0 へメジャーアップグレード完了です。

Rails 5.0 → 5.2

Rails 5.0 にメジャーアップグレードできましたが、ここで立ち止まらず引き続きマイナーアップグレードを行いました。それは Rails 5.2 まで上げると system spec が使えるようになり、javascript の動作をテストコードで担保できるようになるからです。

1. 整理整頓

メジャーアップグレードで把握した使用 gem の内、不要なものは削除するなど、メンテコストを下げました。

2. Rails 5.1 の変更点に応じて修正する

Rails 5.0からRails 5.1へのアップグレードに記載されている内容を1つ1つ確認しながら対応しました。

  • ActionController::Parameters における HashWithIndifferentAccess のメソッド呼び出しのサポートが削除されたので対応する
  • render :text を render :plain に変更する
  • render :nothing を head :ok に変更する

3. ついでに API のテストカバレッジを上げて整理する

Rails で API を担っている領域のテストカバレッジが低かったので、requests spec で担保しました。

  • 正常系と異常系の requests spec を整備する
  • 不要なエンドポイントを削除する

4. Rails 5.1 に上げる Pull Request を作成する

ここで Rails 5.1 に上げる目処が立ったので、 Pull Request を作成しました。

  • gem の依存バージョンを解決する
  • Rails 5.1 にアップグレードする
  • 設定ファイルの更新(app:update)
  • テストが実行できる
  • テストを実行し、落ちているものを修正する
  • すべての警告を抑止する

5. Rails 5.2 の変更点に応じて修正する

引き続きRails 5.1からRails 5.2へのアップグレードに記載されている内容を1つ1つ確認しながら対応しました。

  • uniq は Rails 5.1 で削除されるので distinct に置換する

6. Rails 5.2 に上げる Pull Request を作成する

ここで Rails 5.2 に上げる目処が立ったので、 Pull Request を作成しました。

  • gem の依存バージョンを解決する
  • Rails 5.2 にアップグレードする
  • 設定ファイルの更新(app:update)
  • テストが実行できる
  • テストを実行し、落ちているものを修正する
  • すべての警告を抑止する

7. テスト環境で隅々まで動作確認する

この段階でテストカバレッジは 47 % と以前よりはだいぶ上がっていますが、
まだまだテストコードで担保できていない機能があるので、テスト環境で動作確認を行いました。

8. Rails 5.2 を本番環境にデプロイする

Rails 5.2 に変更できる準備が整ったのでデプロイしました。

9. 本番環境で発生した問題に対処する

Rails 5.2 のデプロイ後は特にエラーは発生しませんでした。Rails 5.0 の時と同様にセッションが切れる問題は再度発生することが予想されたので対応しました。

これで Rails 5.0 → 5.2 へマイナーアップグレード完了です。

その後

Rails 6.0 にはすぐに上げることができるのですが、特に使いたい機能があるわけではなかったので、ここで一旦立ち止まり強化することにしました。対応したことは以下になります。

  • system spec による結合テストの整備(2020年1月時点でカバレッジ 53 %)
  • Dependabot によるバージョン追従
  • CI の実行速度の改善と整備
  • CD の開発

まとめ

Rails 4.2 から 5.2 までアップグレードした3ヶ月の軌跡をまとめました。

最初、自分が資格スクエアをメジャーアップグレードしようと考えた時は、正直テストカバレッジが低いので相当難しいなという印象でした。

しかし、効果的な戦略と的確な変更によってわずか3ヶ月で成し遂げられた状況を見て、とても感動しました。そしてシステム面では新しい機能が使用できたり、パフォーマンスが向上したりと様々な恩恵を受けています。

この方法が自分たちと同じようにメジャーアップグレードに悩んでいる方々にとって参考になれば幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Stripe API / Update SubscriptionSchedule and set cancel_at / started case and not started case / without "adjusted" Upcoming invoice / Ruby code example / #Stripe #API #Ruby

Docs

Code

# Docs

# https://stripe.com/docs/billing/subscriptions/subscription-schedules
# https://stripe.com/docs/billing/subscriptions/subscription-schedules/use-cases
#
# https://stripe.com/docs/api/subscription_schedules/release
# https://support.stripe.com/questions/create-update-and-schedule-subscriptions

require 'active_support/core_ext'
require 'stripe'

Stripe::api_key = ENV['STRIPE_SECRET_KEY']

[1.month, 3.months].each do |end_at_from|
  product1 = Stripe::Product.create(name: "Gold plan #{rand(9999999999)}")
  plan1 = Stripe::Plan.create(interval: 'month', currency: 'jpy', amount: 5000, product: product1.id, usage_type: 'licensed')

  tax_rate = Stripe::TaxRate.create(display_name: 'Tax Rate', percentage: 10.0, inclusive: false)
  customer = Stripe::Customer.create
  payment_method = Stripe::PaymentMethod.create(type: 'card', card: { number: '4242424242424242', exp_year: 2030, exp_month: 01})
  customer_payment_method = Stripe::PaymentMethod.attach(payment_method.id, customer: customer.id)

  def put_subscription_schedule(subscription_schedule, message)
    puts '-' * 100
    puts "Subscription Schedule"
    puts message
    puts '-' * 100
    puts subscription_schedule
    puts '-' * 100
    puts "https://dashboard.stripe.com/test/subscription_schedules/#{subscription_schedule.id}"
  end

  # https://stripe.com/docs/api/subscription_schedules/create
  soon_start_subscription_schedule = Stripe::SubscriptionSchedule.create(
    {
      customer: customer.id,
      start_date: Time.now.to_i + 5,
      default_settings: {
        default_payment_method: customer_payment_method.id,
      },
      phases: [
        {
          plans:
            [
              { plan: plan1.id, quantity: 1 },
            ],
          prorate: false,
          default_tax_rates: [tax_rate],
        },
      ],
    }
  )

  puts '-' * 100
  puts "Wait until subscription schedule starts"
  puts '-' * 100
  until soon_start_subscription_schedule.status == 'active' do
    soon_start_subscription_schedule = Stripe::SubscriptionSchedule.retrieve(soon_start_subscription_schedule.id)
    puts soon_start_subscription_schedule.status
    sleep 2
  end

  started_subscription_schedule = Stripe::SubscriptionSchedule.retrieve(id: soon_start_subscription_schedule.id, expand: ['subscription'])

  future_subscription_schedule = Stripe::SubscriptionSchedule.create(
    {
      customer: customer.id,
      start_date: Time.now.to_i + 100*24*60*60,
      default_settings: {
        default_payment_method: customer_payment_method.id,
      },
      phases: [
        {
          plans:
            [
              { plan: plan1.id, quantity: 1 },
            ],
          prorate: false,
          default_tax_rates: [tax_rate],
        },
      ],
    }
  )

  # current_phase.start_date is not subscription cycle start_date
  def update_subscription_schedule(base_subscription_schedule)
    if base_subscription_schedule.status == 'active'
      start_date = base_subscription_schedule.current_phase.start_date
    elsif base_subscription_schedule.status == 'not_started'
      start_date = base_subscription_schedule.phases[0].start_date
    else
      raise
    end

    end_date = Time.at(start_date).since(3.months).to_i

    Stripe::SubscriptionSchedule.update(
      base_subscription_schedule.id,
      {
        # Set "cancel" not "releasse"
        end_behavior: 'cancel',
        phases: [
          {
            plans:
              [
                {
                  plan:     base_subscription_schedule.phases[0].plans[0].plan,
                  quantity: base_subscription_schedule.phases[0].plans[0].quantity
                },
              ],
            # prorate does not effect to Subscription or SubscriptionSchedule Canceling
            # prorate: true / false,
            default_tax_rates: base_subscription_schedule.phases[0].default_tax_rates.map(&:id),
            # For Update You must set start_date in first phase not in subscription schedule directly
            start_date: start_date,
            # If you do not want to get upcoming invoice on Subscription
            # Then you must specify end_date with Subscription natural cycle interval
            end_date: end_date
          },
        ]
      }
    )
  end

  updated_started_subscription_schedule = update_subscription_schedule(started_subscription_schedule)
  updated_future_subscription_schedule = update_subscription_schedule(future_subscription_schedule)

  put_subscription_schedule(updated_started_subscription_schedule, 'UPDATED')
  put_subscription_schedule(updated_future_subscription_schedule, 'UPDATED')
end

# NOTE
# SubscriptionSchedule current_phase is not same as Subscription cycle
# it is "phase" start_date and end_date
#
# If phase end_date is 3.months after then ...
#
# Time.at(updated_started_subscription_schedule.current_phase.start_date)
# => 2020-01-02 17:44:23 +0900
# Time.at(updated_started_subscription_schedule.current_phase.end_date)
# => 2020-04-02 17:44:23 +0900

Started Subscription - cancel at current phase

Upcoming invoice leaves from dashboard

image

Not Started Subscription - cancel at first phase

Theres Starts Ends in both in future
Theres upcoming invoice becauce Schedule will start in future
Not current invoice because Schedule not started

image

Started Subscription - cancel at future phase

Change end_date in code and run Case

image

Not Started Subscription - cancel at not first phase

Change end_date in code and run Case

image

Result

  • This is run code resutls example but not correctly same as Image capture images in this article.
----------------------------------------------------------------------------------------------------
Wait until subscription schedule starts
----------------------------------------------------------------------------------------------------
not_started
not_started
not_started
not_started
not_started
not_started
active
----------------------------------------------------------------------------------------------------
Subscription Schedule
CREATED
----------------------------------------------------------------------------------------------------
{
  "id": "sub_sched_1FwPNZCmti5jpytUe3xhuCdN",
  "object": "subscription_schedule",
  "canceled_at": null,
  "completed_at": null,
  "created": 1577955005,
  "current_phase": null,
  "customer": "cus_GTM5yQ0vRozYUW",
  "default_settings": {
    "billing_thresholds": null,
    "collection_method": "charge_automatically",
    "default_payment_method": "pm_1FwPNHCmti5jpytURUDXSKtE",
    "default_source": null,
    "invoice_settings": null
  },
  "end_behavior": "release",
  "livemode": false,
  "metadata": {
  },
  "phases": [
    {
      "application_fee_percent": null,
      "billing_thresholds": null,
      "collection_method": null,
      "coupon": null,
      "default_payment_method": null,
      "default_tax_rates": [
        {
          "id": "txr_1FwPNHCmti5jpytUA4yh9gmW",
          "object": "tax_rate",
          "active": true,
          "created": 1577954987,
          "description": null,
          "display_name": "Tax Rate",
          "inclusive": false,
          "jurisdiction": null,
          "livemode": false,
          "metadata": {
          },
          "percentage": 10.0
        }
      ],
      "end_date": 1589187005,
      "invoice_settings": null,
      "plans": [
        {
          "billing_thresholds": null,
          "plan": "plan_GTM5XKzi7c1qy8",
          "quantity": 1,
          "tax_rates": [

          ]
        }
      ],
      "prorate": false,
      "start_date": 1586595005,
      "tax_percent": 10.0,
      "trial_end": null
    }
  ],
  "released_at": null,
  "released_subscription": null,
  "renewal_interval": null,
  "revision": "sub_sched_rev_1FwPNZCmti5jpytUWUBqK4lG",
  "status": "not_started",
  "subscription": null
}
----------------------------------------------------------------------------------------------------
https://dashboard.stripe.com/test/subscription_schedules/sub_sched_1FwPNZCmti5jpytUe3xhuCdN
----------------------------------------------------------------------------------------------------
Subscription Schedule
UPDATED
----------------------------------------------------------------------------------------------------
{
  "id": "sub_sched_1FwPNJCmti5jpytU8hFkMssH",
  "object": "subscription_schedule",
  "canceled_at": null,
  "completed_at": null,
  "created": 1577954989,
  "current_phase": {
    "end_date": 1585817393,
    "start_date": 1577954993
  },
  "customer": "cus_GTM5yQ0vRozYUW",
  "default_settings": {
    "billing_thresholds": null,
    "collection_method": "charge_automatically",
    "default_payment_method": "pm_1FwPNHCmti5jpytURUDXSKtE",
    "default_source": null,
    "invoice_settings": {
      "days_until_due": null
    }
  },
  "end_behavior": "cancel",
  "livemode": false,
  "metadata": {
  },
  "phases": [
    {
      "application_fee_percent": null,
      "billing_thresholds": null,
      "collection_method": null,
      "coupon": null,
      "default_payment_method": null,
      "default_tax_rates": [
        {
          "id": "txr_1FwPNHCmti5jpytUA4yh9gmW",
          "object": "tax_rate",
          "active": true,
          "created": 1577954987,
          "description": null,
          "display_name": "Tax Rate",
          "inclusive": false,
          "jurisdiction": null,
          "livemode": false,
          "metadata": {
          },
          "percentage": 10.0
        }
      ],
      "end_date": 1585817393,
      "invoice_settings": null,
      "plans": [
        {
          "billing_thresholds": null,
          "plan": "plan_GTM5XKzi7c1qy8",
          "quantity": 1,
          "tax_rates": [

          ]
        }
      ],
      "prorate": true,
      "start_date": 1577954993,
      "tax_percent": 10.0,
      "trial_end": null
    }
  ],
  "released_at": null,
  "released_subscription": null,
  "renewal_interval": null,
  "revision": "sub_sched_rev_1FwPNZCmti5jpytUoCQuoPaf",
  "status": "active",
  "subscription": "sub_GTM59xT4BS5E3j"
}
----------------------------------------------------------------------------------------------------
https://dashboard.stripe.com/test/subscription_schedules/sub_sched_1FwPNJCmti5jpytU8hFkMssH
----------------------------------------------------------------------------------------------------
Subscription Schedule
UPDATED
----------------------------------------------------------------------------------------------------
{
  "id": "sub_sched_1FwPNZCmti5jpytUe3xhuCdN",
  "object": "subscription_schedule",
  "canceled_at": null,
  "completed_at": null,
  "created": 1577955005,
  "current_phase": null,
  "customer": "cus_GTM5yQ0vRozYUW",
  "default_settings": {
    "billing_thresholds": null,
    "collection_method": "charge_automatically",
    "default_payment_method": "pm_1FwPNHCmti5jpytURUDXSKtE",
    "default_source": null,
    "invoice_settings": {
      "days_until_due": null
    }
  },
  "end_behavior": "cancel",
  "livemode": false,
  "metadata": {
  },
  "phases": [
    {
      "application_fee_percent": null,
      "billing_thresholds": null,
      "collection_method": null,
      "coupon": null,
      "default_payment_method": null,
      "default_tax_rates": [
        {
          "id": "txr_1FwPNHCmti5jpytUA4yh9gmW",
          "object": "tax_rate",
          "active": true,
          "created": 1577954987,
          "description": null,
          "display_name": "Tax Rate",
          "inclusive": false,
          "jurisdiction": null,
          "livemode": false,
          "metadata": {
          },
          "percentage": 10.0
        }
      ],
      "end_date": 1594457405,
      "invoice_settings": null,
      "plans": [
        {
          "billing_thresholds": null,
          "plan": "plan_GTM5XKzi7c1qy8",
          "quantity": 1,
          "tax_rates": [

          ]
        }
      ],
      "prorate": true,
      "start_date": 1586595005,
      "tax_percent": 10.0,
      "trial_end": null
    }
  ],
  "released_at": null,
  "released_subscription": null,
  "renewal_interval": null,
  "revision": "sub_sched_rev_1FwPNaCmti5jpytUnlvLBTke",
  "status": "not_started",
  "subscription": null
}
----------------------------------------------------------------------------------------------------
https://dashboard.stripe.com/test/subscription_schedules/sub_sched_1FwPNZCmti5jpytUe3xhuCdN
----------------------------------------------------------------------------------------------------
Wait until subscription schedule starts
----------------------------------------------------------------------------------------------------
not_started
not_started
not_started
not_started
not_started
not_started
not_started
not_started
not_started
not_started
not_started
not_started
not_started
not_started
not_started
not_started
not_started
not_started
not_started
not_started
not_started
not_started
not_started
not_started
not_started
active
----------------------------------------------------------------------------------------------------
Subscription Schedule
CREATED
----------------------------------------------------------------------------------------------------
{
  "id": "sub_sched_1FwPOZCmti5jpytUqLGjGd5a",
  "object": "subscription_schedule",
  "canceled_at": null,
  "completed_at": null,
  "created": 1577955067,
  "current_phase": null,
  "customer": "cus_GTM5Wlau0N9HHj",
  "default_settings": {
    "billing_thresholds": null,
    "collection_method": "charge_automatically",
    "default_payment_method": "pm_1FwPNcCmti5jpytUPG6hdjRk",
    "default_source": null,
    "invoice_settings": null
  },
  "end_behavior": "release",
  "livemode": false,
  "metadata": {
  },
  "phases": [
    {
      "application_fee_percent": null,
      "billing_thresholds": null,
      "collection_method": null,
      "coupon": null,
      "default_payment_method": null,
      "default_tax_rates": [
        {
          "id": "txr_1FwPNbCmti5jpytUOMYY1jCE",
          "object": "tax_rate",
          "active": true,
          "created": 1577955007,
          "description": null,
          "display_name": "Tax Rate",
          "inclusive": false,
          "jurisdiction": null,
          "livemode": false,
          "metadata": {
          },
          "percentage": 10.0
        }
      ],
      "end_date": 1589187067,
      "invoice_settings": null,
      "plans": [
        {
          "billing_thresholds": null,
          "plan": "plan_GTM5akDaPHCKOr",
          "quantity": 1,
          "tax_rates": [

          ]
        }
      ],
      "prorate": false,
      "start_date": 1586595067,
      "tax_percent": 10.0,
      "trial_end": null
    }
  ],
  "released_at": null,
  "released_subscription": null,
  "renewal_interval": null,
  "revision": "sub_sched_rev_1FwPOZCmti5jpytUsHM0JqYJ",
  "status": "not_started",
  "subscription": null
}
----------------------------------------------------------------------------------------------------
https://dashboard.stripe.com/test/subscription_schedules/sub_sched_1FwPOZCmti5jpytUqLGjGd5a
----------------------------------------------------------------------------------------------------
Subscription Schedule
UPDATED
----------------------------------------------------------------------------------------------------
{
  "id": "sub_sched_1FwPNdCmti5jpytUnvii1fUR",
  "object": "subscription_schedule",
  "canceled_at": null,
  "completed_at": null,
  "created": 1577955009,
  "current_phase": {
    "end_date": 1585817414,
    "start_date": 1577955014
  },
  "customer": "cus_GTM5Wlau0N9HHj",
  "default_settings": {
    "billing_thresholds": null,
    "collection_method": "charge_automatically",
    "default_payment_method": "pm_1FwPNcCmti5jpytUPG6hdjRk",
    "default_source": null,
    "invoice_settings": {
      "days_until_due": null
    }
  },
  "end_behavior": "cancel",
  "livemode": false,
  "metadata": {
  },
  "phases": [
    {
      "application_fee_percent": null,
      "billing_thresholds": null,
      "collection_method": null,
      "coupon": null,
      "default_payment_method": null,
      "default_tax_rates": [
        {
          "id": "txr_1FwPNbCmti5jpytUOMYY1jCE",
          "object": "tax_rate",
          "active": true,
          "created": 1577955007,
          "description": null,
          "display_name": "Tax Rate",
          "inclusive": false,
          "jurisdiction": null,
          "livemode": false,
          "metadata": {
          },
          "percentage": 10.0
        }
      ],
      "end_date": 1585817414,
      "invoice_settings": null,
      "plans": [
        {
          "billing_thresholds": null,
          "plan": "plan_GTM5akDaPHCKOr",
          "quantity": 1,
          "tax_rates": [

          ]
        }
      ],
      "prorate": true,
      "start_date": 1577955014,
      "tax_percent": 10.0,
      "trial_end": null
    }
  ],
  "released_at": null,
  "released_subscription": null,
  "renewal_interval": null,
  "revision": "sub_sched_rev_1FwPOaCmti5jpytU2OqMiUUC",
  "status": "active",
  "subscription": "sub_GTM64npF4L1rop"
}
----------------------------------------------------------------------------------------------------
https://dashboard.stripe.com/test/subscription_schedules/sub_sched_1FwPNdCmti5jpytUnvii1fUR
----------------------------------------------------------------------------------------------------
Subscription Schedule
UPDATED
----------------------------------------------------------------------------------------------------
{
  "id": "sub_sched_1FwPOZCmti5jpytUqLGjGd5a",
  "object": "subscription_schedule",
  "canceled_at": null,
  "completed_at": null,
  "created": 1577955067,
  "current_phase": null,
  "customer": "cus_GTM5Wlau0N9HHj",
  "default_settings": {
    "billing_thresholds": null,
    "collection_method": "charge_automatically",
    "default_payment_method": "pm_1FwPNcCmti5jpytUPG6hdjRk",
    "default_source": null,
    "invoice_settings": {
      "days_until_due": null
    }
  },
  "end_behavior": "cancel",
  "livemode": false,
  "metadata": {
  },
  "phases": [
    {
      "application_fee_percent": null,
      "billing_thresholds": null,
      "collection_method": null,
      "coupon": null,
      "default_payment_method": null,
      "default_tax_rates": [
        {
          "id": "txr_1FwPNbCmti5jpytUOMYY1jCE",
          "object": "tax_rate",
          "active": true,
          "created": 1577955007,
          "description": null,
          "display_name": "Tax Rate",
          "inclusive": false,
          "jurisdiction": null,
          "livemode": false,
          "metadata": {
          },
          "percentage": 10.0
        }
      ],
      "end_date": 1594457467,
      "invoice_settings": null,
      "plans": [
        {
          "billing_thresholds": null,
          "plan": "plan_GTM5akDaPHCKOr",
          "quantity": 1,
          "tax_rates": [

          ]
        }
      ],
      "prorate": true,
      "start_date": 1586595067,
      "tax_percent": 10.0,
      "trial_end": null
    }
  ],
  "released_at": null,
  "released_subscription": null,
  "renewal_interval": null,
  "revision": "sub_sched_rev_1FwPObCmti5jpytU5eKvVg9W",
  "status": "not_started",
  "subscription": null
}
----------------------------------------------------------------------------------------------------
https://dashboard.stripe.com/test/subscription_schedules/sub_sched_1FwPOZCmti5jpytUqLGjGd5a


Original by Github issue

https://github.com/YumaInaura/YumaInaura/issues/2914

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

railsのDBで型をintegerからfloatへ変更しnumber_fieldで小数点を扱えるようにする

前回の記事の続きで初めてfloat型を扱ったのでメモ

やりたい事

  • DBに間違ってinteger型で作ったカラムをfloat型に変更
  • form_for内でnumber_fieldで小数点を扱えるようにする
  • DBに保存した値を合計するメソッドを作る

参考

【Rails】form_for の number_field で小数を入力できるようにする
https://qiita.com/tegnike/items/07f789eb22c7a7bf6a19

マイグレーションを使ったカラムの追加、削除、データ型の変更 [ 自分用メモ ]
https://qiita.com/dawn_628/items/13fa64dc6d600e921ce3

【Rails】カラムの合計値を求める!
https://qiita.com/tomokichi_ruby/items/8758a91566957cfc5429

number_fieldでは整数のみしか扱えない

new.html.erb
<%= form_for @count_time do |f| %>
 <%= f.number_field :count_hour, class:'input_form' %>
 <%= f.submit '登録', class:'input_submit' %>
<% end %>

スクリーンショット 2020-01-03 17.33.24.png

というフォームがあって,

小数点の数字をいれると,
スクリーンショット 2020-01-03 17.35.00.png
というふうになるので

new.html.erb
<%= form_for @count_time do |f| %>
 <%= f.number_field :count_hour, step: '0.1', class:'input_form' %>
<%# step: '0.1'を追加 %>
 <%= f.submit '登録', class:'input_submit' %>
<% end %>

step: '0.1'を追加すると小数点以下第一位までの数値を扱えるようになります

このままだとDBには整数でしか保存されない

DBの count_hourinteger型なので整数のみしか保存してくれないのでカラムの型を変更する

ターミナル
$ rails g migration change_data_count_hour_to_count_time
Running via Spring preloader in process 94544
      invoke  active_record
      create    db/migrate/20200103074431_change_data_count_hour_to_count_time.rb

カラム名がcount_hourでテーブル名がcount_timeになります

20200103074431_change_data_count_hour_to_count_time.rb
class ChangeDataCountHourToCountTime < ActiveRecord::Migration[5.2]
  def change
    change_column :count_times, :count_hour, :float #追記。テーブル名は複数形で
  end
end

rails db:migrateします

これでfloat型に変更できたのでnumber_fieldから送られる小数点の値がDBへ保存できるようになりました

DBの値をビューへ描画

index.html.erb
  <table>
    <tr>
      <th>日付</th>
      <th>時間</th>
    </tr>
    <% @count_time.each do |count_time| %>
      <tr>
        <td><%= count_time.created_at.to_date %></td>
        <td><%= count_time.count_hour %>時間</td>
      </tr>
    <% end %>
  </table>

スクリーンショット 2020-01-03 17.50.13.png

sumメソッドを使い学習時間の合計を表示させる

count_time_controller.rb
class CountTimesController < ApplicationController
 def index
  @count_time = CountTime.where(user_id: current_user.id).order('updated_at DESC').limit(5)
  @count_hour = @count_time.sum(:count_hour)
 end
end

sumメソッドの引数にカラムを指定するで合計値を出します

完成

スクリーンショット 2020-01-03 18.04.07.png

まとめ

ひとまずこれで自分が最初に想像していた主要機能が完成しました。開発にそんな時間がかからないアプリでしたが知らない事も学べたので良かったかなと。あとは思い付いた追加の機能を開発していこうと思います。

終わり

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

コントローラー名とアクション名でSCSSを呼び出す

例えば WelcomeController の index アクションが呼ばれた場合、デフォルトの状態だとビューには welcome.css(.scss) がロードされるのだけど、これを呼ぶかどうかについて、ファイルの有無とか controller での変数で判定できたらいいのになという話。

色々方法はあるかと思うのですが、以下のようにしてみました。

assets/stylesheets/application.css

= require_self
*= require_directory .
デフォルトだと require_tree . となっている箇所を require_directory . にする。こうすることでルートディレクトリ( assets/stylsheets/
)の css しか自動でインポートしないようにする。

ついでにスタイルシートのディレクトリも以下のように views っぽくする。

f:id:ltcmdr927:20121209105229p:plain

HEAD タグは application.html.erb で編集しているので、そこを修正する。

views/layouts/shared/application.html.erb

<%= stylesheet_link_tag "application", :media => "all" %>
<%= partial_stylesheet_link_tag controller.controller_name, controller.action_name, @partial_css_disabled %>
<%= javascript_include_tag "application" %>
stylesheet_link_tag と javascript_include_tag の間に新しい helper メソッドを追加する。引数でビューを読んだ時のコントローラ名とアクション名を指定。@partial_css_disabled は後述。

app/helpers/application_helper.rb

module ApplicationHelper

def partial_stylesheet_link_tag(controller_name, action_name, disabled = true)
if disabled == false || disabled.nil?
if File.exist?("#{Rails.root.to_s}/app/assets/stylesheets/#{controller_name}/#{action_name}.css.scss")
return stylesheet_link_tag "#{controller_name}/#{action_name}"
end
end
end

end
partial_stylesheet_link_tag メソッドを書く。disabled が false かあるいは nil かを判定し、正なら scss ファイルの有無を判定し、最終的にコントローラ/アクションの stylesheet_link_tag を戻す、という感じ。

基本的にはファイルの有無で stylesheet タグが追加されるかどうかを判定するようになるので、例えばファイルは存在するけどタグは追加したくないという場合は、下記のように controller で @partial_css_disabled 変数に true を指定してやる。 true を指定するとファイルが存在していてもタグが追加されない。

app/controller/welcome_controller.rb

class WelcomeController < ApplicationController

def index
@partial_css_disabled = true
end

end
若干イモっぽい感じがしなくもないですが、個人的にはこれで管理しやすくなりました。(コントローラの階層構造が深くなった場合については調査して後々追記予定。)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsチュートリアルメモ - 第4章・第5章

メモ目次はこちら

Railsチュートリアル第4章はこちら
Railsチュートリアル第5章はこちら

Railsチュートリアルで気づいたことのメモを記載していきます

4.1 動機

Rubyに関する知識を深めていく章。
手始めに条件分岐で異なる文字列を返すhelperを自作する。

4.2 文字列とメソッド

ポイント

  • 文字列の式展開 => Rubyはシングルクォート文字列の中では式展開を行わない(シェルスクリプトと同じ)
  • putsメソッド(printメソッド + 行末の改行)
  • nil
  • 暗黙の戻り値 => メソッド内にreturnを明示しなくても最後に評価した式をreturnする
  • mixed in => moduleをincludeすること
  • Railsでは自動的にヘルパーモジュールをmixed inしてくれるため、include行を書かなくてもOK

4.3 他のデータ構造

たぶんここらへんで眠くなって落脱する人が多い気がする、、、
初心者は流し読みにして、後でわからないところが出てきたら戻ってくるくらいの方が良さそう

ポイント

  • Rubyの配列はゼロオリジン(ゼロ・インデックス)
  • 比較演算子(==、!=)
  • 破壊的メソッド(メソッドの最後が!)
  • pushメソッド (または同等の<<演算子)
  • 範囲 (range) e.g (0..9).to_a

Rubyは改行と空白を区別しない(式の途中で改行してもOK)

4.4 Rubyにおけるクラス

ポイント

  • リテラルコンストラクタ、名前付きコンストラクタ
  • superclassメソッド
  • selfキーワード
  • eachメソッド
  • ブロック e.g. { |i| puts 2 * i } => pythonでいうlambda?
  • ハッシュとシンボル
  • 以下は等価
    • { :name => "Michael Hartl" }
    • { name: "Michael Hartl" }
  • 組み込みクラスの変更が可能
  • attr_accessor => attributeに対しるaccessorを作成するための記法
  • initialize => 初期化メソッド(pythonでいうinitと同じ)

4.5 最後に

割愛

5.1 構造を追加する

html/cssについての解説の章。

ポイント

  • パーシャル(DOMの一部を_始まりのファイルに切り出せる)

5.2 Sassとアセットパイプライン

SASS(CSSプリプロセッサー)の説明とRailsでの指定の仕方についての章。

ポイント

  • アセットディレクトリ(*/assetsで表される静的ファイルを目的別に分類するディレクトリ)
  • マニフェストファイル(アセットディレクトリにあるファイルをどのようにまとめるかを定義するファイル。実際の処理はSprocketsというgemが行う)
  • プリプロセッサエンジン(.scssや.erbなどを変換して、html/css/jsに置き換えてくれるやつ。Sprocketsがアセットをまとめてくれた後にプリプロセッサエンジンが実行される)
  • Sassの変数
    • $始まりで変数を定義できる
    • bootstrap lessで定義されている変数(@gray-light)なども$gray-lightと書けば使用可能

5.3 レイアウトのリンク

  • 名前付きルート
    • config/routes.rbの中でget '/help', to: 'static_pages#help', as: 'helf'のように書くとhelp_pathやhelp_urlのような形で呼び出せるようになる
  • 統合テストの作成
    • rails generate integration_test site_layoutで統合テストを作成できる
    • rails test:integrationで統合テストの実行

5.4 ユーザー登録: 最初のステップ

6章以降でユーザー登録処理を作成していくための導入。ここではこれまでの章の振り返り的な内容。

5.5 最後に

割愛

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rails: belongs_to :user, optional: trueとは?

簡単に説明すると、belongs_toでoptional: trueを設定することにより外部キーのnilを許可できるようになります。

optional: trueはどんなときに使われるのか?

例えば、Q&Aのようなサービスを作る際にuserテーブルとquestionテーブルがあるとする。
その際に、ログインしていないユーザーからでも質問をポストできるシステムを作るときに使われたりする。
基本的にアソシエーションを書くときは外部キーが無いっていうことはあまりないがたまにある時もあるのでその際にoptional: trueをつけなかったりするとバグの温床になるので気をつける必要がある。

ただDBの設計上、外部キーのnilを許可することが少ない気がするので、あまり使う設定ではないかもしれません。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

pickadate.jsを使ってフォームにカレンダーを表示させる

こんにちは!スージーです!
久し振りにrailsでアプリ開発を開始したのでその際に使ったdatepickerについてまとめ

datepickerとは

スクリーンショット 2020-01-02 18.25.33.png

日付入力フォームを選択した時に表れるカレンダー

参考記事

結局、どのdatepickerが一番使い勝手がよいのかhttps://qiita.com/knt45/items/6d74f6785cd4547ae53b

Rails Application Build Guides
https://rails.densan-labs.net/form/datetime_register_form.html

pickadate.js
https://amsul.ca/pickadate.js/

pickadate-rails
https://github.com/veracross/pickadate-rails

準備

Gemfile
gem 'pickadate-rails'

bundle install

application.js
//= require pickadate/picker 追記
//= require pickadate/picker.date 追記
//= require pickadate/picker.time 追記
//= require jquery_ujs
//= require_tree .
application.scss
@import "pickadate/classic";
@import "pickadate/classic.date";
@import "pickadate/classic.time";

cssレイアウトはdefault又はclassicが選べるようになっている

Jsファイル

datepicker.js
$(function() {
  $( "#datepicker" ).pickadate(); // カレンダー表示のイベント
});

Jsはこれだけで使えるようになる。リファレンスには色々なレイアウトのカレンダーが用意されている。

turbokinksでイベント発火しない

jQueryあるあるですがturbolinksをtrueのままにしているとページをリロードしないとイベント発火しないので、今回は不要なので削除。application.js,application.html.erbのturbolinksに関するコードは削除。gem 'turbolinks'も削除してbundle installします。

contorollerのアクション作成

datepickers_controller.rb
class datepickersController < ApplicationController

  def new
    @datepicker = Datepicker.new
  end

  def create
    @datepicker = Datepicker.create(datepicker_params)
    if @datepicker.save
      redirect_to :root
    else
      render :new
    end
  end

  private

  def datepicker_params
    params.require(:start_day).permit(:start_day).merge(user_id: current_user.id)
  end
end

この辺はお決まりのnew/createアクションです

完成

カレンダーで日付を選択して登録できている事をパラメータを確認

ターミナル
Started POST "/start_days" for ::1 at 2020-01-03 12:50:13 +0900
Processing by StartDaysController#create as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"4Ug39AI0MM0azF2XqNEcTOSQNjpNWQ+ovCSI7PFrPPatvEM8avbMZ6MII5JdxZnni+Hssbo7x6NTAjA0udiH7Q==", "datepicker"=>{"datepicker"=>"3 January, 2020"}, "commit"=>"登録"}

パラメータの値がちゃんとDBに保存されています

まとめ

日付の選択は何かを登録する場面で使う事が多いかと思いますし、年月日をそれぞれ入力するのはUI/UX的に煩雑になってしまいますが、datepickerを使えば便利になりますし、簡単に実装もできました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Stripe API / 例えば1ヶ月単位のサブスクリプションのキャンセルを予約した時に、サイクル請求の期間に対して中途半端な終了日を指定すると、次回のインヴォイスが日割計算されることが分かる。 ( #Ruby #Stripe )

Points

prorate: true
prorate: false

どちらでも挙動は変わらないように思えた

Code

#! /usr/bin/env ruby

require 'stripe'

# You Need
# `$ gem install activesupport` befure run ruby script
require 'active_support/core_ext'

# Docs
# https://stripe.com/docs/api/subscriptions/create
# https://stripe.com/docs/billing/subscriptions/billing-cycle#prorations


Stripe::api_key = ENV['STRIPE_SECRET_KEY']

def create_subscription_and_cancel(cancel_at_natural_cycle_end:, prorate:)
  product1 = Stripe::Product.create(name: "Gold plan #{rand(9999999999)}")
  plan1 = Stripe::Plan.create(interval: 'month', currency: 'jpy', amount: 980, product: product1.id, usage_type: 'licensed')

  tax_rate = Stripe::TaxRate.create(display_name: 'Tax Rate', percentage: 10.0, inclusive: false)
  customer = Stripe::Customer.create
  payment_method = Stripe::PaymentMethod.create(type: 'card', card: { number: '4242424242424242', exp_year: 2030, exp_month: 01})
  customer_payment_method = Stripe::PaymentMethod.attach(payment_method.id, customer: customer.id)

  subscription = Stripe::Subscription.create(
    {
      customer: customer.id,
      default_payment_method: customer_payment_method.id,
      items: [
        [
          { plan: plan1.id },
        ],
      ],
      prorate: prorate,
      default_tax_rates: [tax_rate],
    }
  )

  cancel_at = if cancel_at_natural_cycle_end
                Time.at(subscription.current_period_start).since(1.month).to_i
              else
                Time.at(subscription.current_period_start).since(1.month).ago(1.day).to_i
              end

  updated_subscription = Stripe::Subscription.update( subscription.id, cancel_at: cancel_at )

  puts '-' * 100
  puts "SUBSCRIPTION"
  puts '-' * 100
  puts "prorate: #{prorate}"
  puts "cancel_at: #{updated_subscription.cancel_at} ( #{Time.at(updated_subscription.cancel_at)} ) "
  if ENV['VERBOSE']
    puts '-' * 100
    puts updated_subscription
  end

  puts '-' * 100
  puts 'UPCOMING INVOICE'
  puts '-' * 100

  begin
    upcoming_invoice = Stripe::Invoice.upcoming(customer: updated_subscription.customer)
    puts "subotal: #{upcoming_invoice.subtotal}"
    puts "tax: #{upcoming_invoice.tax}"
    puts "total: #{upcoming_invoice.total}"
  rescue Stripe::InvalidRequestError => e
    puts e.message
  end

  if ENV['VERBOSE']
    puts '-' * 100
    puts upcoming_invoice
  end

  puts '-' * 100
  puts "https://dashboard.stripe.com/test/subscriptions/#{subscription.id}"

  updated_subscription
end

create_subscription_and_cancel(cancel_at_natural_cycle_end: true, prorate: true)
create_subscription_and_cancel(cancel_at_natural_cycle_end: false, prorate: true)

create_subscription_and_cancel(cancel_at_natural_cycle_end: true, prorate: false)
create_subscription_and_cancel(cancel_at_natural_cycle_end: false, prorate: false)


Result

----------------------------------------------------------------------------------------------------
SUBSCRIPTION
----------------------------------------------------------------------------------------------------
prorate: true
cancel_at: 1580610835 ( 2020-02-02 11:33:55 +0900 )
----------------------------------------------------------------------------------------------------
UPCOMING INVOICE
----------------------------------------------------------------------------------------------------
No upcoming invoices for customer: cus_GTG1yiBZ6EXEEh
----------------------------------------------------------------------------------------------------
https://dashboard.stripe.com/test/subscriptions/sub_GTG1xUoFpeHjYG
----------------------------------------------------------------------------------------------------
SUBSCRIPTION
----------------------------------------------------------------------------------------------------
prorate: true
cancel_at: 1580524440 ( 2020-02-01 11:34:00 +0900 )
----------------------------------------------------------------------------------------------------
UPCOMING INVOICE
----------------------------------------------------------------------------------------------------
subotal: -32
tax: -3
total: -35
----------------------------------------------------------------------------------------------------
https://dashboard.stripe.com/test/subscriptions/sub_GTG1TVlYdpyHgl
----------------------------------------------------------------------------------------------------
SUBSCRIPTION
----------------------------------------------------------------------------------------------------
prorate: false
cancel_at: 1580610845 ( 2020-02-02 11:34:05 +0900 )
----------------------------------------------------------------------------------------------------
UPCOMING INVOICE
----------------------------------------------------------------------------------------------------
No upcoming invoices for customer: cus_GTG1UAKVPjh6uR
----------------------------------------------------------------------------------------------------
https://dashboard.stripe.com/test/subscriptions/sub_GTG1ZiZpe6qBoW
----------------------------------------------------------------------------------------------------
SUBSCRIPTION
----------------------------------------------------------------------------------------------------
prorate: false
cancel_at: 1580524452 ( 2020-02-01 11:34:12 +0900 )
----------------------------------------------------------------------------------------------------
UPCOMING INVOICE
----------------------------------------------------------------------------------------------------
subotal: -32
tax: -3
total: -35
----------------------------------------------------------------------------------------------------
https://dashboard.stripe.com/test/subscriptions/sub_GTG1KIKsD5Q2qb

A

image

B

image

C

image

D

image

Original by Github issue

https://github.com/YumaInaura/YumaInaura/issues/2913

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

初学者によるプログラミングMemo #16 sliceメソッド([]メソッド)

はじめに

今回はsliceメソッドを使用するお話です
破壊的メソッドにも触れます
なお、本記述はMacにおいて、Railsでの開発を前提としています
また、まだまだひよっこですので、不備等ございましたらご指摘いただけると幸いです

目次

  • "slice"メソッドについて
  • []について
  • 破壊的メソッドについて
  • 問題
  • 破壊的メソッドを使わない様に書いてみる

"slice"メソッドについて

こちらによると「sliceメソッドは、[]の別名です。引数にしたがって文字列の中から部分文字列を取り出します。」とあります
引数に整数一つを指定すると、その一文字を取り出す事ができます *Ruby1.9以降
少し使ってみます

array = "school"
p array.slice(0)
=> "s"

0番目の一文字を取り出せました
元の変数には変化がありません

array = "school"
p array.slice(0)
p array
=> "s"
=> "school"

[]について

silceメソッドの別名というだけあって、使い方は同じです

array = "school"
p array[0]
=> "s"

0番目の一文字を取り出せました
同じく元の変数には変化がありません

array = "school"
p array[0]
p array
=> "s"
=> "school"

記述的にはシンプルなので、こちらを使うほうが良さそうですね

破壊的メソッドについて

破壊的メソッドとは、実行した時に元となったのオブジェクト自身の内容を変更してしまうメソッドのことです。
基本的には、メソッドの最後に"!"がつきます
先ほどの配列"array"でやってみましょう

array = "school"
p array.slice!(0)
=> "s"

ここまでは同じです
しかし、元の配列の中身を示すとこうなります

array = "school"
p array.slice!(0)
p array
=> "s"
=> "chool"

"s"が取り出されたことにより、元の配列から"s"が消えてしまいました
この様に、元のオブジェクトの値自体を変更してしまうメソッドのことを破壊的メソッドと呼びます

問題

私の通っているスクールで、この様な問題が出ました
同じだと問題があるので、少し変えます

array('Ruby') => 'byRu'
array('python') => 'thonpy'
array('Hi') => 'Hi'

配列"array"に対して、「最初から2文字を最後尾に持ってくるメソッドを作ろう」という問題です

先ほどの"slice"メソッドと、破壊的メソッドを使用すれば、簡単に答えを導く事ができます
前提知識として、slice(0,2)とすれば、「前から数て"0"番目から"2"文字分を取り出す」という意味になることが必要です

def array(str)
  array = str.slice!(0,2)
  new_array = str + array
  return new_array
end

p "文字を入力してください"
input = gets.chomp
p array(input)

"input"のなかに好きな文字を入れれば最初から2文字を最後尾に持ってくる事が可能です

しかし、問題は破壊的メソッドを使用しているという点です
破壊的メソッドに関して、何名かのご意見を拝見いたしましたが、初学者の私にとってどうすべきかの判断は付けづらかったので、状況によっては使って良いという解釈で行きたいと思います。
使わないほうがいいと思った理由の一つには、元のオブジェクトの中身を変えてしまうので、自分以外の人がメンテナンスをする際に辿るのが大変な事があげられます。
使ってもいいシチュエーションがあるなと感じた理由の一つには、配列オブジェクトに対してハッシュオブジェクトを格納していく際、"<<"メソッドを使用します(これも破壊的メソッドです)が、そういった場合は使ってもいいのではないかなあと思います。今の私では、それ以外で配列にハッシュオブジェクトを格納する方法を知らないということもありますが…

破壊的メソッドを使わない様に書いてみる

では、先ほどの問題を、破壊的メソッドを使わずに解いてみましょう
といっても、最初から2文字を取り出せているので、そこまで難しくはありません。
前提知識として、「数値をマイナスにすると後ろから数えて何番目を指定する」ということくらいです

def array(str)
  array = str.slice(0,2)
  length = str.length
  back = str.slice(2-length,length-2)
  new_array = back + array
  return new_array
end

p "文字を入力してください"
input = gets.chomp
p array(input)

こんな感じですね
ポイントは"(2-length,length-2)"のところでしょうか
入力される文字列が何文字か決まっていないので、"2-length"とすることで、後ろから3番目を必ず指定できます
今回は最初から2文字と決まっているので、2から引きましたが、これも何文字までと指定する様に変えてあげれば汎用性(あるのか?)が増えますね

今回はこれで以上です

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Ruby] EnumerableとEnumeratorとEnumerator.produce

はじめに

  • Enumerator.produce は、Ruby 2.7.0 で新たに追加されたメソッドです。
  • 本記事ではEnumerator.produceを紹介したいんですが、便宜上Enumerator とは、そもそもEnumerableとは一体まで遡ってまとめていますので、そんなことはいいから produceメソッドを教えろという方は下の方までスクロールお願いします。

Enumerable

  • Enumerableモジュール
  • 標準クラスだと Array とか Hash なんかがインクルードしている (他にもたくさん)
  • インクルードする場合、each メソッドの実装が必須
  • each メソッドを用いて、 all? collect first max など、様々なメソッドを提供する

もちろん自作のクラスをEnumerable化することもできます。

class MyClass
  include Enumerable

  def initialize(a, b, c, d)
    @a = a
    @b = b
    @c = c
    @d = d
  end

  def each
    yield @a
    yield @b
    yield @c
    yield @d
  end
end

上記クラスは、4つの値 a b c dを受け取って、そのままの順番のシーケンスを定義したeachメソッドを持ちます。

実用性は皆無ですが、Enumerableをincludeしていて、eachメソッドが定義されているので、ArrayHashと同様に、Enumerableの恩恵を受けることができます。

irb(main):030:0> MyClass.new(1,2,3,4).to_a
=> [1, 2, 3, 4]
irb(main):031:0> MyClass.new(20,30,10,0).max
=> 30
irb(main):032:0> MyClass.new(1,2,3,4).filter(&:even?)
=> [2, 4]

もちろん定義したeachメソッドをそのまま使うこともできます。

irb(main):033:0> MyClass.new(1,2,3,4).each { |n| p n }
1
2
3
4
=> 4

Enumerator

  • Enumeratorクラス
  • Enumerableをインクルードしている
  • each 以外のメソッドでも Enumerableの機能を利用できるようにするためのラッパークラスとして使用

前述のMyClassでは、a b c d正順のシーケンスをeachメソッドに定義して、それを使ってEnumerableの機能を扱えるようにしました。

それはそれとして、 d c b a逆順のシーケンスを使って、同様にEnumerableの機能を使いたくなったとします。

そこで今回は、以下のように Enumeratorオブジェクトを戻す、each_reverseメソッドを定義します。

class MyClass
  include Enumerable

  def initialize(a, b, c, d)
    @a = a
    @b = b
    @c = c
    @d = d
  end

  def each
    yield @a
    yield @b
    yield @c
    yield @d
  end

  def each_reverse
    Enumerator.new do |y|
      y << @d
      y << @c
      y << @b
      y << @a
    end
  end

end

Enumeratorオブジェクトは、オブジェクト生成時の定義を元にしたeachメソッドを持っているため、d c b a順のシーケンスに対してもEnumerableの機能が使えるようになります。

irb(main):020:0> MyClass.new(1,2,3,4).each_reverse
=> #<Enumerator: #<Enumerator::Generator:0x000055b74e71a380>:each>
irb(main):021:0> MyClass.new(1,2,3,4).each_reverse.first
=> 4
irb(main):022:0> MyClass.new(1,2,3,4).each_reverse.map { |n| n ** 2 }
=> [16, 9, 4, 1]

ちなみに ArrayHasheachメソッドはEnumeratorを返します。

irb(main):041:0> [1,2,3,4,5].each
=> #<Enumerator: [1, 2, 3, 4, 5]:each>
irb(main):042:0> {hoge: 'Hoge'}.each
=> #<Enumerator: {:hoge=>"Hoge"}:each>

Enumerator.produce

  • Enumerator.produceクラスメソッド
  • ブロックで定義した無限リストを簡易的に生成できる

Enumerator.produce は、初期値とブロックを定義することで無限リストになるEnumeratorオブジェクトを返します。以下は、初期値が0で、1ずつ加算するだけの単純な無限リストです。Enumerable#take を使って、先頭の10件を取得していますが、10件分だけ無限リストを評価するようになっています。

irb(main):007:0> enumerator = Enumerator.produce(0) { |n| n + 1 }
irb(main):008:0> enumerator.take(10)
=> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

無限リストになるので、もちろんto_aとかやると無限にリストを走査し続けるので死にます。

irb(main):027:0> enumerator = Enumerator.produce(0) { |n| n + 1 }
irb(main):028:0> enumerator.to_a
# ゆるやかな死

以下のように、日付の無限リストを作ることもできるので実用性も充分です。

require 'date'
enum = Enumerator.produce(Date.today, &:next_day)
pp enum.take(10)
[#<Date: 2020-01-02 ((2458851j,0s,0n),+0s,2299161j)>,
 #<Date: 2020-01-03 ((2458852j,0s,0n),+0s,2299161j)>,
 #<Date: 2020-01-04 ((2458853j,0s,0n),+0s,2299161j)>,
 #<Date: 2020-01-05 ((2458854j,0s,0n),+0s,2299161j)>,
 #<Date: 2020-01-06 ((2458855j,0s,0n),+0s,2299161j)>,
 #<Date: 2020-01-07 ((2458856j,0s,0n),+0s,2299161j)>,
 #<Date: 2020-01-08 ((2458857j,0s,0n),+0s,2299161j)>,
 #<Date: 2020-01-09 ((2458858j,0s,0n),+0s,2299161j)>,
 #<Date: 2020-01-10 ((2458859j,0s,0n),+0s,2299161j)>,
 #<Date: 2020-01-11 ((2458860j,0s,0n),+0s,2299161j)>]

以下の例はフィボナッチ数列の無限リストを生成するコードです。(参考)

現在の値と直前の値があれば次の値を計算できるので、実に Enumerator.produceに向いていて美しいですね。

Enumerator.produce([0, 1]) { |prev, current| [current, prev + current] }.take(10).map(&:first)
=> [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

anyenv で rbenv を使っているときの最新版 Ruby のインストール

anyenv で rbenv を入れていて、通常の brew upgrade rbenv ruby-build をやっても最新版の Ruby が表示されなかったのでメモ。

anyenv を使っていると以下のコマンドで env 系を一括でアップデートできる。

anyenv update

上記コマンド実行後、 rbenv install -l を実行すると最新版の Ruby が表示された。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WSLでRails6の環境構築

VSCode上で環境構築します。

WSLを有効にし、Ubuntuをインストール

こちらの記事通りに進めていけば簡単にできます。画像付きですごく分かりやすかったです。
UbuntuはLTS版を入れました。

VSCodeの設定

VSCodeの拡張機能Remote Developmentをインストール。これでWSLに接続して開発ができるようになる。
インストールしたら、VSCodeの一番左下に緑のアイコンがでるのでクリック。
Remote-WSL: New Windowを選択。これで設定終了。

Ubuntuのパッケージを最新化

VScodeのターミナルを開いて、2つのコマンドを実行。かなり時間かかりました。
sudoコマンドを使うときに、Ubuntuで設定したパスワードを聞かれるかも。

パッケージの最新化
$ sudo apt update
$ sudo apt upgrade -y

rbenvのインストール

rbenvは、複数のRubyのバージョンを管理し、プロジェクトごとにRubyのバージョンを指定して使える。バージョンを変更したい場合も簡単。
参考: rbenv

rbenvのインストールと設定
#rbenvをインストール
$ git clone https://github.com/rbenv/rbenv.git ~/.rbenv

#パス指定と初期化処理を.bashrcに追加
$ echo 'export PATH="$HOME"/.rbenv/bin:$PATH' >> ~/.bashrc
$ echo 'eval "$(rbenv init -)"' >> ~/.bashrc

#パス指定と初期化処理の反映
$ source ~/.bashrc

#バージョンが表示されれば成功
$ rbenv -v

ruby-buildのインストール

ruby-buildはRubyのインストールを簡単にしてくれる。
参考: ruby-buildruby-buildのWiki

ruby-buildのインストール、その他必要パッケージ
# ruby-buildのインストール
$ mkdir -p "$(rbenv root)"/plugins ←これいるの?やらなくてもできた。
$ git clone https://github.com/rbenv/ruby-build.git "$(rbenv root)"/plugins/ruby-build

#Rubyのインストールに必要なパッケージ。ruby-buildのWiki参照。かなり時間かかる。
$ sudo apt-get install autoconf bison build-essential libssl-dev libyaml-dev libreadline6-dev zlib1g-dev libncurses5-dev libffi-dev libgdbm5 libgdbm-dev

Rubyのインストール

ようやくrbenvを使ってRubyのインストールをします。安定版をインストールする。
恐ろしいくらい時間がかかりました。
最新の安定版の確認: こちら

Rubyのインストール
#Rubyのインストール
$ rbenv install 2.7.0

#PC全体で使うRubyのバージョンを指定する
$ rbenv global 2.7.0

$ ruby -v

RubyGemsのアップデート

RubyGemsを最新版にアップデートする。

$ gem update --system

Railsのインストール

Railsをインストールします。
最新のバージョンの確認: こちら

$ gem install rails -v 6.0.2.1
$ rails -v

Node.jsのインストール

Rails6から標準のWebpackerのインストールに必要。

# npmのインストール
$ sudo apt install -y npm

# Node.jsのインストール
$ sudo npm install n -g
$ sudo n stable

$ node -v

yarnのインストール

Webpackerのインストールに必要。
最初この方法でやったがうまくできなかった。
参考: yarn

$ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
$ echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
$ sudo apt-get update && sudo apt-get install yarn
$ yarn -v

#エラー出た時のメモ
$ sudo apt remove cmdtest
$ sudo apt remove yarn
$ sudo apt autoremove

SQLite3のインストール

Rails標準のデータベースなのでインストール。

$ sudo apt-get install libsqlite3-dev

アプリの作成

$ rails new sample_app
$ cd sample_app
$ rails s

その他メモ

railsインストールするときノードキュメント

# gemでライブラリーをインストールした際にドキュメントを生成すると時間がかかる。
# そこで、ドキュメントを生成しない設定にする
$ cd
$ touch .gemrc
$ echo "install: --no-document" >> .gemrc
$ echo "update: --no-document" >> .gemrc

# rbenvのアップデート
# Rubyの最新バージョンを使いたいときは、rbenvをアップデートしてから導入
$ cd ~/.rbenv
$ git pull
$ cd plugins/ruby-build
$ git pull

# 新しいコマンドを要するgemをインストールしたとき使用
$ rbenv rehash

# 新しく作ったディレクトリで作業するときは、そこで使うRubyのバージョンを指定する
$ rbenv local バージョン

感想

ほとんどエラー無くできたが、各種パッケージのインストールが長すぎて疲れました笑
それにすごい動作が遅い

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby on Rails を学ぶ _No.1

view(ビュー)

ページや「見た目」を作るためのHTMLファイルです。
ブラウザとrailsの通信でrailsからビューが返され、ページが表示されます。

controller(コントローラー)

ページを表示するときにrailsの中ではコントローラを経由してビューをブラウザに返します。
具体的には、コントローラと同じ名前のビューフォルダから、あくしょんと同じ名前のHTMLファイルを探してブラウザに返します。

■ コントローラファイルの中身
ファイルの中にtopメソッドが追加されます。コントローラの中のメソッドはアクションと呼ばれます。
→ アクションと同じ名前のHTMLファイルをブラウザに返します。

html_controller.rb
class HomeController < ApplicationController
  def top #homeコントローラのtopアクションに対応するhtmlファイルを返す
  end
end

ルーティング

ブラウザとコントローラを繋ぐ役割があります。
送信されたURLに対してどのコントローラのどのアクションで対応するかを決める対応表のような物です。

■ ルーティングファイルの中身
ルーティングはconfig/routes.rb内に定義され以下のような文法で書かれます。

routes.rb
Rails.application.routes.draw do
  get "home/top" => "home#top"
  #get "URL名" => コントローラ名#アクション名
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む