20200205のRubyに関する記事は27件です。

Rubyの基礎まとめ

基礎文法

繰り返し(while文)

while 条件 do
  処理
 if 中断条件
    break
  end
end

各桁の和

a = 11
puts a.to_s.split('').map{|x| x.to_i}.inject(:+)
#=> 2

条件分岐(書き換えパターン)

pattern(1)

s = "hoge"
if s==hoge
  puts "YES"
else
  puts "NO"
end

#=>YES

pattern(2)

s = "hoge"
puts s == hoge ? "YES":"NO"

#=>YES

pattern(3)

s = "hoge"
case s
when "hoge" then
  puts "YES"
when "no_hoge" then
  puts "NO"

#=>YES

.gsub(パターンマッチング)

string = "ruby ruby ruby"
puts string.gsub(/ruby/, 'python')

#=> python python python

配列の操作

配列の割り算

a = [2, 4, 6]
b = a.map(|x| x/2)
puts b

#=> [1, 2, 3]

配列の合計

a = [1, 2, 3]
puts a.inject(:+)

#=> 6

奇数or偶数番目要素の取得

ary = ('a'..'j').to_a
#=> ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]

# 偶数番目
ary.select.with_index { |_, i| i.even? }
#=> ["a", "c", "e", "g", "i"]

# 奇数番目
ary.select.with_index { |_, i| i.odd? }
#=> ["b", "d", "f", "h", "j"] 

並べ替え

ary = [2, 4, 1, 5, 3]

# 昇順 
ary.sort
#=> [1, 2, 3, 4, 5]

# 降順
ary.sort.reverse
#=> [5, 4, 3, 2, 1]

重複要素を統一

ary = [1, 2, 2, 3, 3]

ary.uniq
#=> [1, 2, 3]

配列→文字列

ary = [1, 2, 3, 4, 5]

ary.join(' ')
#=> '1 2 3 4 5'

要素の追加

ary = ['a', 'b', 'c']

ary << 'd'
#=> ['a', 'b', 'c', 'd']

テクニック

メソッドを定義

def method(n)
  n += 1
end

method(3)
#=> 4
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rubyの基礎まとめ[メモ]

コーディングテスト用のメモ。

基礎文法

繰り返し

  • for
for i in 1..3 do
  puts i
end

※配列でも可

for num in [1, 2, 3] do
  puts num
end
  • each
[1, 2, 3, 4].each do |num|
  p num
end
  • while
num = 0 #初期値
while num <= 12 do #条件
  p num #処理
  num += 3 #値の更新
end
  • times
3.times do |num|
  p num
end

各桁の和

a = 11
puts a.to_s.split('').map{|x| x.to_i}.inject(:+)
#=> 2

String#chars

a = 11
puts a.to_s.chars.map{|x| x.to_i}.sum

### 奇数or偶数番目要素の取得

Integer#digits

a = 11
puts a.digits.sum

条件分岐(書き換えパターン)

  • pattern(1)
s = "hoge"
if s==hoge
  puts "YES"
else
  puts "NO"
end

#=>YES
  • pattern(2)
s = "hoge"
puts s == hoge ? "YES":"NO"

#=>YES
  • pattern(3)
s = "hoge"
case s
when "hoge" then
  puts "YES"
when "no_hoge" then
  puts "NO"

#=>YES

.gsub(パターンマッチング)

string = "ruby ruby ruby"
puts string.gsub(/ruby/, 'python')

#=> python python python

数値の切り捨て

1.4.floor # 1
1.5.floor # 1
-1.4.floor # -2
-1.5.floor # -2

数値の切り上げ

1.4.ceil  # 2
1.5.ceil  # 2
-1.4.ceil # -1
-1.5.ceil # -1

配列の操作

配列の割り算

a = [2, 4, 6]
b = a.map(|x| x/2)
puts b

#=> [1, 2, 3]

配列の合計

a = [1, 2, 3]
puts a.inject(:+)

#=> 6
ary = ('a'..'j').to_a
#=> ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]

# 偶数番目
ary.select.with_index { |_, i| i.even? }
#=> ["a", "c", "e", "g", "i"]

# 奇数番目
ary.select.with_index { |_, i| i.odd? }
#=> ["b", "d", "f", "h", "j"] 

並べ替え

ary = [2, 4, 1, 5, 3]

# 昇順 
ary.sort
#=> [1, 2, 3, 4, 5]

# 降順
ary.sort.reverse
#=> [5, 4, 3, 2, 1]

重複要素を統一

ary = [1, 2, 2, 3, 3]

ary.uniq
#=> [1, 2, 3]

配列→文字列

ary = [1, 2, 3, 4, 5]

ary.join(' ')
#=> '1 2 3 4 5'

要素の追加

ary = ['a', 'b', 'c']

ary << 'd'
#=> ['a', 'b', 'c', 'd']

最大値最小値

ary = [1, 25, 50, 75, 100]

ary.minmax
#=> [1, 100]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Leetcode: Serialize And Deserialize Binary Tree

require 'minitest/autorun'
require 'pry'
class SerializeAndDeserializeBinaryTree < Minitest::Test

  def test_run
    expected = [1, 2, 3, nil, nil, 4, 5]
    tree = TreeNode.new(1)
    tree.left = TreeNode.new(2)
    tree.right = TreeNode.new(3)
    tree.right.left = TreeNode.new(4)
    tree.right.right = TreeNode.new(5)

    serialized_tree = "12XX34XX5XX"
    assert_equal(serialized_tree,
                 serialize(tree))
    deserialize(serialized_tree)
  end

  def serialize(root)
    return "X" if root.nil?

    root.val

    left = serialize(root.left)
    right = serialize(root.right)
    root.val.to_s + left.to_s + right.to_s
  end

  def deserialize(serialized_tree)
    nodes_left = serialized_tree.split('')
    deserialize_helpder(nodes_left)
  end

  def deserialize_helpder(nodes_left)
    value_for_node = nodes_left.shift
    return nil if value_for_node == "X"

    new_node = TreeNode.new(value_for_node)
    new_node.left = deserialize_helpder(nodes_left)
    new_node.right = deserialize_helpder(nodes_left)
    new_node
  end

end

class TreeNode
  attr_accessor :val, :left, :right

  def initialize(val)
    @val = val
    @left, @right = nil, nil
  end
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】Docker環境下でbinding.pryを使えないとき確認すべきポイント4つ

はじめに

Docker環境下でこんなシーンに遭遇したことはありませんか?

「Railsでバグ発生!デバッグしなきゃ!よーし、binding.pryしよう!」

「うわ、コンソール出ない!!!これじゃ何も出来ない!!!」

こんなやるせない気持ちになった方のために、自分がこれまで詰まった箇所とその解決法を残しておこうと思います。

環境

OS: macOS Catalina 10.15.3
Ruby: 2.6.5
Rails: 6.0.2.1
Docker: 19.03.5
docker-compose: 1.24.1

1.binding.pryのコンソールが出ない

docker-compose upなどでrails serverを立ち上げていて、binding.pryを入力した箇所で動作が止まっているのに、

「コンソールが出ない!どうしよう!」

という状態を想定しています。

解決法

$docker container ls

でRailsアプリのあるコンテナ名を確認します。
※ここではrails_app_web_1とします。

$ docker attach rails_app_web_1

docker attachで該当コンテナにattachします。
これで、binding.pryしたときにコンソールが表示されます。

※もし反応がなければ、Enter押下するとコンソールが出るかもです。

2.せっかくコンソールが出たのに終了の仕方がわからない

多くの場合はbinding.pryを一回だけして終了、ということはなく、続けて何度かデバッグ作業を行うかと思います。

下手にCtrl + Cで終了してしまうと、コンテナが停止してしまうので、立ち上げ直しになってしまってかなり面倒です。

解決法

pryの画面から終了するならcontinueと入力してrails serverを通常動作に戻し、docker attachを維持するのが便利です。

この状態であれば、次にbinding.pryしたときもスムーズにデバッグ作業が可能です。

※デバッグが終了してコンテナから抜けたい場合、Ctrl + P + Q(Macの場合)でコンテナを停止せずに抜けられます。

3.pryで日本語入力出来ない

少し外れますが、そもそもrails consolepryを使っていて、日本語が入力できないパターンもあるかもしれません。

解決法

Dockerfile
ENV LANG C.UTF-8

Dockerfileに上記のように追記すれば解決できます。

これを忘れるとpry日本語入力が効きません。

4.コンテナがすぐ落ちる、コンソールに文字が入力できない

2020/2/6追記

解決法

docker-compose.yml
web:
  tty: true
  stdin_open: true

上記がdocker-compose.ymlのRailsに関係する箇所に書かれているかどうか確認します。(今回はwebとしています。)

  • tty: true ポート待受などをしていないコンテナを起動させ続けるオプション
  • stdin_open: true 標準入力出来るようになるオプション

おわりに

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

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

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

【Rails】Docker環境下でbinding.pryを使えないとき確認すべきポイント3つ

はじめに

Docker環境下でこんなシーンに遭遇したことはありませんか?

「Railsでバグ発生!デバッグしなきゃ!よーし、binding.pryしよう!」

「うわ、コンソール出ない!!!これじゃ何も出来ない!!!」

こんなやるせない気持ちになった方のために、自分がこれまで詰まった箇所とその解決法を残しておこうと思います。

環境

OS: macOS Catalina 10.15.3
Ruby: 2.6.5
Rails: 6.0.2.1
Docker: 19.03.5
docker-compose: 1.24.1

1.binding.pryのコンソールが出ない

docker-compose upなどでrails serverを立ち上げていて、binding.pryを入力した箇所で動作が止まっているのに、

「コンソールが出ない!どうしよう!」

という状態を想定しています。

解決法

$docker container ls

でRailsアプリのあるコンテナ名を確認します。
※ここではrails_app_web_1とします。

$ docker attach rails_app_web_1

docker attachで該当コンテナにattachします。
これで、binding.pryしたときにコンソールが表示されます。

※もし反応がなければ、Enter押下するとコンソールが出るかもです。

2.せっかくコンソールが出たのに終了の仕方がわからない

多くの場合はbinding.pryを一回だけして終了、ということはなく、続けて何度かデバッグ作業を行うかと思います。

下手にCtrl + Cで終了してしまうと、コンテナが停止してしまうので、立ち上げ直しになってしまってかなり面倒です。

解決法

pryの画面から終了するならcontinueと入力してrails serverを通常動作に戻し、docker attachを維持するのが便利です。

この状態であれば、次にbinding.pryしたときもスムーズにデバッグ作業が可能です。

※デバッグが終了してコンテナから抜けたい場合、Ctrl + P + Q(Macの場合)でコンテナを停止せずに抜けられます。

3.pryで日本語入力出来ない

少し外れますが、そもそもrails consolepryを使っていて、日本語が入力できないパターンもあるかもしれません。

解決法

Dockerfile
ENV LANG C.UTF-8

Dockerfileに上記のように追記すれば解決できます。

これを忘れるとpry日本語入力が効きません。

おわりに

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

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

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

Rails、hamlでFontAwesomeを使う

今日、だいぶつまいたので、メモ程度に書いておきます。

①ハッシュロケットをシンボル型に直す。

ハッシュロケットは時代遅れになっているようで、
取り残されないようにシンボル型に書き換えます。



基本形
ハッシュロケット
:rocket => "rocket"

シンボル型
hash: "hash"


書き換えてみる
ハッシュロケット
%meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/

シンボル型
%meta{content: "text/html; charset=UTF-8", "http-equiv": "Content-Type"}/

②link_toでFontAwesomeを表示

a(href)を
= link_to "#" doに変更

= link_to new_group_path do

= fa_icon 'edit'

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

iOSエンジニアがSign in with Appleをサーバーサイドで実装したときに行き詰りを感じたところとか

Sign in with Apple 実装のデッドラインは4月 ですね☺️
Web/iOS/Androidなど、複数プラットフォームをサポートするためWebViewでログイン機能を実装しているサービスや、外部サービスAPI利用の際に独自のAPIを噛ませているなど、サーバーサイドでSign in with Appleに対応するケースはあるかと思います。

iOSエンジニアであるわたしがサーバーサイドで実装した際に、わかりにくいな〜?と感じたポイントについて記載します。
自分のスキルセットとして、認証・認可、Webの知識はほぼ0でした?
実装したソースコードをの抜粋を載せていますが、言語はrubyです。

やること

今回ご説明する Sign in with Apple の全体のざっくりフローは、

  1. Apple Developer サイトでの設定
  2. Apple ID サインイン画面表示(Authorizeエンドポイント)、認可コード受取り
  3. 認可コードを使い、AppleのユーザーIDを取得(Tokenエンドポイント)

です。

取得したいデータがemailとnameのみの場合 → ステップ2まで
ユーザーIDを取得したい場合 → ステップ3まで
実装する必要があります。

参考までに前知識

Auth関連

まず最初に、OAuth周りの知識があると理解が早いと思います。
この方のqiitaが非常に参考になりました。(ありがとうございました。)

Appleガイドライン関連

Apple Developer サイトでのもろもろ

ではさっそくApple Developerサイトを開きます。

Servise ID作成からドメイン認証

  1. 対象の App ID を作成し、Capabilities として Sign in with Apple を追加する
  2. Apple Developer の Certificates, Identifiers & Profiles から Service ID を追加する
  3. Primary App ID を 1 のAppに指定する
  4. Domainsに認可コードのリダイレクト先のドメインと Return URLs を指定する
  5. Domainの検証ファイル apple-developer-domain-association.txt をダウンロードする
  6. Domainの検証ファイルをWebサイトの https://.../.well-known/ ディレクトリに置く

Service IDReturn URLs はAuthorize、Tokenエンドポイントで利用します。

ドメイン検証に失敗したら

  • https://{yourdomain}/.well-known/apple-developer-domain-association.txt にアクセスできるか
  • 検証ファイルは有効期限内か(7日間のみ有効)
  • 検証ファイルのダウンロードは何回でも可能なので、常に最新のファイルを使う
  • サーバーがTLSの条件を満たしているか

を確認する。

明文化されていなさそうですが、Appleさんからサポートを受けた開発者がフォーラムに書き込んでいます。
https://forums.developer.apple.com/thread/122124

そのほか、ネットワーク構成などに問題がないか等、公式の Troubleshooting Domain Verification を読んでみるとよいです。

Key をダウンロード

Tokenエンドポイントのリクエストパラメータの JSON Web Token 作成に使います。
当然ですが、センシティブなファイルなので暗号化などよしなに行い、サーバーのどこか安全なところに置いておく。
今回はわたしは、.txtに変換して使いました。

公式ドキュメントは Create a Sign in with Apple private key

Apple IDサインインページ表示

Appleサインインボタン

公式ドキュメント Incorporating Sign in with Apple into Other PlatformsAdd a Custom “Sign in with Apple” Button にダウンロードURLやサイズ、カラー指定のクエリパラメータの説明があります。
Human Interface Guidelines にしたがって正しくつかいましょう。

Authorizeエンドポイント

Appleの認証ページURLのクエリパラメータ、レスポンスについては Incorporating Sign in with Apple into Other Platforms に記載があります。

大事なのは、

client_id = Service IDで指定した文字列
redirect_uri = Service IDに設定したリダイレクトURI

です。

  • response_type = id_tokenにする場合、response_modefragmentまたはform_postである必要がある
  • 氏名やメールアドレスを取得したい(scopeを指定する)場合、response_mode = form_postにする必要がある

など、諸々の制約があります。

サインインページが表示されると、ユーザーによるApple IDの認証を受け付けます。
認証が完了すると、指定したリダイレクトURIにリダイレクトが行われます。
受け取れる値は認可コードと、scopeに指定がある場合初回1度のみユーザーID以外の個人情報(メールアドレス、氏名など)が受け取れます。

サインインページがうまく表示されない

invalid_uriinvalid_client が表示されたら、焦らず client_idredirect_uri を今一度確認しましょう。
開発・本番など環境別でドメインを分けている場合、 redirect_uri が変わると思いますので要チェックです。
Apple Developer 上で作成した Service ID の Returns URLs に該当のリダイレクト先が登録されているか確認しましょう。

アクセストークン・ユーザーID取得

Tokenエンドポイント

Appleのトークン発行エンドポイントにリクエストを行い、トークンの発行を行います。
パラメータ、レスポンスについては Generate and validate tokens に記載があります。
パラメータは Authorization のものを使います。

client_secretの値のJSON Web Token(JWT)作成

Apple Developers で発行したKeyを用いて、リクエストパラメータ client_secret に付与するJWTの作成を行います。
JWTとは、 headerpayload から成るJSON構造をエンコードし電子署名をつけたものです。
デコード&エンコードしたJWTの検証は、 https://jwt.io/ がおすすめです。

rubyでのJWTのエンコード&署名の例は以下です。
jwt/ruby-jwt というGemを利用しています。

kidisssubなどは暗号化しておくと良いでしょう。

require 'jwt'
require 'openssl'

  # 署名用の鍵を取得
  def pem
    file = "path/to/key.txt" # Apple Developerで発行したKeyファイル
    # key.txtを読み出し、暗号鍵を生成する
    OpenSSL::PKey::EC.new(File.read(file))
  end

  # 署名付きIDトークンを取得
  def id_token
    header = {
      'kid' => 'Apple Developerで発行したKey ID'
    }
    claim = {
      'iss' => 'Apple DeveloperでのTeam ID',
      'iat' => Time.now.to_i,
      'exp' => Time.now.to_i + 15777000,
      'aud' => 'https://appleid.apple.com',
      'sub' => 'Apple Developerで発行したService ID'
    }
    JWT.encode(
      claim,
      pem,
     'ES256',
      header
    )
  end

レスポンスの検証

レスポンスに含まれるIDトークンの検証には、Appleの公開鍵取得が必要です。
エンドポイントについては Fetch Apple's public key for verifying token signature に記載があります。

jwt/ruby-jwt を用いた検証の実装例です。

  # IDトークンをデコードし署名を検証する
  # public_keysに渡すのは、https://appleid.apple.com/auth/keys の "keys"の値です。
  def decode(id_token, public_keys)
    JWT.decode(
      id_token,
      nil,
      true,
      { algorithms: 'E256', jwks: { keys: public_keys } }
    )
  end

IDなどのユーザー情報

subの値がユーザーの一意の識別子(ユーザーID)です。
そのほかのレスポンスのIDトークンに含まれる情報については Retrieve the User’s Information from Apple ID Servers
に記載があります。
あとは取得したデータをもとに自サービスでのユーザー認証を完了させましょう。

おしまい

書くと簡単ですが実際はたいへんだったのでiOS boys and girlsの皆様はできる限りネイティブでやりましょう:tada:

Thanks to

https://qiita.com/kiwi26/items/5b8cc53ed8d10a403f00
https://developer.okta.com/blog/2019/06/04/what-the-heck-is-sign-in-with-apple

以上です???
ありがとうございました?

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

LINE の スマートスピーカー Clova Friends と Clova Desk の HTTP リクエストを調べる

概要

  • LINE Clova から Custom Extension (スキル) 起動時に送信される HTTP リクエストを調べる
  • Ruby + Heroku による調査用 Web サーバ (Extension サーバー) を立てて HTTP リクエストの内容を調べる

Clova とは

LINE が提供しているスマートスピーカー。

LINE Clova公式サイト

ClovaはLINEが開発したAIアシスタントです。
話しかけるだけでLINEの送受信や家電の操作など、あなたの様々なリクエストに応えます。

Clova の HTTP リクエスト

Clova は CEK (Clova Extensions Kit) を介して HTTPS リクエストを Extension サーバーに送信している。

CEKの概要 - Clova Developer Center β

CEKは、Clova Extension(以下、Extension)を開発および配布する際に必要なツールとインターフェースを提供するプラットフォームです。ClovaプラットフォームとExtension間のデータの送受信をサポートします。Extensionは、音楽、ショッピングなどの外部のサービス(サードパーティサービス)、または家庭のIoTデバイスの制御など、Clovaの機能を拡張して、ユーザーに様々な体験を提供するWebアプリケーションです。

CEK APIのリファレンス - Clova Developer Center β

・HTTP/1.1バージョンでHTTP通信し、POSTメソッドを使用します。
・Hostとリクエストパスは、Extensionの開発者があらかじめ定義したURIに設定されます。
・リクエストボディのデータはJSON形式で、UTF-8エンコーディングを使用します。
・SignatureCEKフィールドとRSA公開鍵を使用して、Clovaから送信されたリクエストかどうかを検証することができます。

Extension サーバーとは

Clova から送信される HTTPS リクエストを受信して応答を返す Web サーバ。

Custom Extensionを作成する - Clova Developer Center β

Clova Developer Centerに登録するExtensionサーバーです。このサーバーは、Clovaがユーザーの音声入力を解析した結果や、デフォルトで提供されるインテントを渡された際に、そのインテントを処理して適切な応答を返す必要があります。

今回の調査対象

  • Clova Friends (画面なしモデル)
  • Clova Desk (画面ありモデル)
  • Clova Developer Center テストツール (Web ブラウザ上で使用できるテストツール)

Clova Friends とは

内蔵バッテリーを搭載している Clova。
画面は付いていない。

clova-friends.jpg

Clova Friends | LINE Clova公式サイト

コンパクトなサイズで、見ためもPOPな、スマートスピーカーです。
バッテリーを内蔵しているので、音楽再生、LINE通話、占いなどの便利な機能を、お出かけ先などでご利用いただくことができます。

Clova Desk とは

内蔵バッテリー、画面、赤外線送受信機を搭載している Clova。

clova-desk.jpg

Clova Desk | LINE Clova公式サイト

天気やレシピ、歌詞も画面表示でもっと楽しめる
7インチの画面で、子供と一緒の画面を見ながら、お気に入りの曲を聴いたり、料理を楽しむことができます。

Clova Desk | LINE Clova公式サイト

IRと赤外線リモコンを搭載。リモコンを探すことなく、テレビやエアコンを操作することができます。

Clova Developer Center テストツールとは

実際に Custom Extension (スキル) を配布する前にテストすることができるツール。

clova-dev.png

Extensionをテストする - Clova Developer Center β

テスト画面では、次の2種類のテストを実行できます。

・対話モデルテストモード:任意のインテントのサンプル発話を入力して、インテントやスロットの解析結果やExtensionへのリクエストメッセージを確認できます。
・シナリオテストモード:LaunchRequestからSessionEndedRequestまでの一連のシナリオをテストできます。

HTTP リクエストを受ける調査用 Web サーバ (Extension サーバー)

  • HTTP リクエスト確認用 Ruby スクリプト を Heroku に設置する
  • Clova 実機等で Custom Extension (スキル) を起動して、その際に送信される HTTP リクエストを Web サーバ (Extension サーバー) で受信してログに出力する

HTTP リクエスト確認用 Ruby スクリプト

require 'socket'

# 標準出力を同期モードに設定
$stdout.sync = true

# 接続を受け付けるポート番号を決定
# 環境変数 PORT が設定されているならそれを設定
port = 8000
port = ENV['PORT'].to_i if ENV['PORT']

# サーバー接続をオープン
server = TCPServer.open(port)

# HTTP リクエストを待ち続ける
loop do

  begin

    # TCPSocket オブジェクトを取得
    socket = server.accept

    # 受け付けた日時を出力
    puts "[info]#{Time.new}"

    # HTTP リクエスト開始行を出力
    if not req_start_line = socket.gets
      puts '[info]req_start_line is nil'
      next
    end
    puts "#{req_start_line}"

    # HTTP リクエストヘッダーを1行ずつ出力
    while req_header = socket.gets.chomp
      puts "#{req_header}"
      break if req_header == '' # ヘッダー終了
      # Content-Length ヘッダーがあれば値を変数にセット
      h = req_header.split(':')
      content_length = h[1].strip.to_i if h[0].strip.downcase == 'content-length'
    end

    # Content-Length がある場合はボディを出力
    if content_length != nil
      puts socket.read(content_length)
    end

    # HTTP レスポンスを返す
    # 本文データ
    body = <<-'__EOS__'
    {
      "version": "1.0",
      "sessionAttributes": {},
      "response": {
        "outputSpeech": {
          "type": "SimpleSpeech",
          "values": {
              "type": "PlainText",
              "lang": "ja",
              "value": "こんにちは、クローバ?"
          }
        },
        "card": {},
        "directives": [],
        "shouldEndSession": true
      }
    }
    __EOS__
    # ステータス行
    socket.write "HTTP/1.1 200 OK\r\n"
    # ヘッダー
    socket.write "Server: #{RUBY_DESCRIPTION}\r\n"
    socket.write "Content-Type: application/json\r\n"
    socket.write "Content-Length: #{body.bytesize}\r\n"
    socket.write "Connection: close\r\n"
    # 空行
    socket.write "\r\n"
    # 本文
    socket.write body

  rescue => e
    puts e.full_message

  ensure
    # HTTP 接続を閉じる
    puts '[info]close this socket'
    socket.close
  end

end

server.close

調査結果

留意点

  • ユーザー毎等で一意になるような値などは文字「X」による伏せ字に置き換えておく
  • Heroku 経由なので一部の HTTP ヘッダ等が加工・追加されている可能性がある
  • Signaturecek ヘッダ: Clova から送信されたかどうかを検証するためのヘッダ
  • X-B3 ではじまるヘッダ: LINE 側でつけていると思われるヘッダ (分散トレーシングシステム Zipkin で使われるヘッダ)
  • Heroku のリバースプロキシサーバ Vegur がつけていると思われるヘッダ: X-Request-Id, X-Forwarded-For, X-Forwarded-Proto, X-Forwarded-Port, Via, Connect-Time, X-Request-Start, Total-Route-Time

Clova Friends (ディスプレイ無しモデル) の HTTP リクエスト

HTTP リクエスト全体

POST /hello-clova/ HTTP/1.1
Host: example.herokuapp.com
Connection: close
User-Agent: Go-http-client/1.1
Content-Type: application/json; charset=utf-8
Signaturecek: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
X-B3-Flags: 0
X-B3-Parentspanid: XXXXXXXXXXXXXXXX
X-B3-Sampled: 1
X-B3-Spanid: XXXXXXXXXXXXXXXX
X-B3-Traceid: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
X-Clova-Experiments: 1=A&10=A&2=B&3=A&4=B&5=A&6=C&7=B&8=A&9=B
X-Clova-Request-Id: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
Accept-Encoding: gzip
X-Request-Id: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
X-Forwarded-For: XXX.XXX.XXX.XXX
X-Forwarded-Proto: https
X-Forwarded-Port: 443
Via: 1.1 vegur
Connect-Time: 0
X-Request-Start: 1580853072763
Total-Route-Time: 0
Content-Length: 744

{"version":"1.0","session":{"new":true,"sessionAttributes":{},"sessionId":"XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX","user":{"userId":"UXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"}},"context":{"System":{"application":{"applicationId":"info.maigo.lab.helloclova"},"device":{"deviceId":"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX","display":{"size":"none","contentLayer":{"width":0,"height":0}}},"user":{"userId":"UXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"}}},"request":{"type":"LaunchRequest","requestId":"XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX","timestamp":"2020-02-04T21:51:12Z","locale":"ja-JP","extensionId":"info.maigo.lab.helloclova","intent":{"intent":"","name":"","slots":{}},"event":{"namespace":"","name":"","payload":null}}}

HTTP メッセージボディの JSON を整形したもの

{
  "version": "1.0",
  "session": {
    "new": true,
    "sessionAttributes": {},
    "sessionId": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
    "user": {
      "userId": "UXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
    }
  },
  "context": {
    "System": {
      "application": {
        "applicationId": "info.maigo.lab.helloclova"
      },
      "device": {
        "deviceId": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
        "display": {
          "size": "none",
          "contentLayer": {
            "width": 0,
            "height": 0
          }
        }
      },
      "user": {
        "userId": "UXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
      }
    }
  },
  "request": {
    "type": "LaunchRequest",
    "requestId": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
    "timestamp": "2020-02-04T21:51:12Z",
    "locale": "ja-JP",
    "extensionId": "info.maigo.lab.helloclova",
    "intent": {
      "intent": "",
      "name": "",
      "slots": {}
    },
    "event": {
      "namespace": "",
      "name": "",
      "payload": null
    }
  }
}

Clova Desk (ディスプレイ付きモデル) の HTTP リクエスト

HTTP リクエスト全体

POST /hello-clova/ HTTP/1.1
Host: example.herokuapp.com
Connection: close
User-Agent: Go-http-client/1.1
Content-Type: application/json; charset=utf-8
Signaturecek: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
X-B3-Flags: 0
X-B3-Parentspanid: XXXXXXXXXXXXXXXX
X-B3-Sampled: 1
X-B3-Spanid: XXXXXXXXXXXXXXXX
X-B3-Traceid: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
X-Clova-Experiments: 1=A&10=A&2=B&3=A&4=B&5=A&6=C&7=B&8=A&9=B
X-Clova-Request-Id: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
Accept-Encoding: gzip
X-Request-Id: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
X-Forwarded-For: XXX.XXX.XXX.XXX
X-Forwarded-Proto: https
X-Forwarded-Port: 443
Via: 1.1 vegur
Connect-Time: 0
X-Request-Start: 1580853042398
Total-Route-Time: 0
Content-Length: 787

{"version":"1.0","session":{"new":true,"sessionAttributes":{},"sessionId":"XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX","user":{"userId":"UXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"}},"context":{"System":{"application":{"applicationId":"info.maigo.lab.helloclova"},"device":{"deviceId":"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX","display":{"size":"custom","dpi":160,"orientation":"landscape","contentLayer":{"width":1024,"height":552}}},"user":{"userId":"UXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"}}},"request":{"type":"LaunchRequest","requestId":"XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX","timestamp":"2020-02-04T21:50:41Z","locale":"ja-JP","extensionId":"info.maigo.lab.helloclova","intent":{"intent":"","name":"","slots":{}},"event":{"namespace":"","name":"","payload":null}}}

HTTP メッセージボディの JSON を整形したもの

{
  "version": "1.0",
  "session": {
    "new": true,
    "sessionAttributes": {},
    "sessionId": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
    "user": {
      "userId": "UXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
    }
  },
  "context": {
    "System": {
      "application": {
        "applicationId": "info.maigo.lab.helloclova"
      },
      "device": {
        "deviceId": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
        "display": {
          "size": "custom",
          "dpi": 160,
          "orientation": "landscape",
          "contentLayer": {
            "width": 1024,
            "height": 552
          }
        }
      },
      "user": {
        "userId": "UXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
      }
    }
  },
  "request": {
    "type": "LaunchRequest",
    "requestId": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
    "timestamp": "2020-02-04T21:50:41Z",
    "locale": "ja-JP",
    "extensionId": "info.maigo.lab.helloclova",
    "intent": {
      "intent": "",
      "name": "",
      "slots": {}
    },
    "event": {
      "namespace": "",
      "name": "",
      "payload": null
    }
  }
}

Clova Developer Center テストツール の HTTP リクエスト

HTTP リクエスト全体

POST /hello-clova/ HTTP/1.1
Host: example.herokuapp.com
Connection: close
Signaturecek: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Content-Type: application/json;charset=UTF-8
User-Agent: Apache-HttpClient/4.5.6 (Java/1.8.0_202)
Accept-Encoding: gzip,deflate
X-Request-Id: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
X-Forwarded-For: XXX.XXX.XXX.XXX
X-Forwarded-Proto: https
X-Forwarded-Port: 443
Via: 1.1 vegur
Connect-Time: 0
X-Request-Start: 1580853979149
Total-Route-Time: 0
Content-Length: 594

{"version":"1.0","session":{"sessionId":"XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX","sessionAttributes":{},"user":{"userId":"XXXXXXXXXXXXXXXXXXXXXX","accessToken":"XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"},"new":true},"context":{"System":{"application":{"applicationId":"info.maigo.lab.helloclova"},"user":{"userId":"XXXXXXXXXXXXXXXXXXXXXX","accessToken":"XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"},"device":{"deviceId":"XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX","display":{"size":"l100","orientation":"landscape","dpi":96,"contentLayer":{"width":640,"height":360}}}}},"request":{"type":"LaunchRequest"}}

HTTP メッセージボディの JSON を整形したもの

{
  "version": "1.0",
  "session": {
    "sessionId": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
    "sessionAttributes": {},
    "user": {
      "userId": "XXXXXXXXXXXXXXXXXXXXXX",
      "accessToken": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
    },
    "new": true
  },
  "context": {
    "System": {
      "application": {
        "applicationId": "info.maigo.lab.helloclova"
      },
      "user": {
        "userId": "XXXXXXXXXXXXXXXXXXXXXX",
        "accessToken": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
      },
      "device": {
        "deviceId": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
        "display": {
          "size": "l100",
          "orientation": "landscape",
          "dpi": 96,
          "contentLayer": {
            "width": 640,
            "height": 360
          }
        }
      }
    }
  },
  "request": {
    "type": "LaunchRequest"
  }
}

参考資料

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

Shift_jis(CP932) -> UTF-8へのcsvの変換機能

Shift_jis(CP932) -> UTF-8へのcsvの変換機能

(変換機能というか例外処理ですね。)
今回はrubyで開発をしたので、rubyの書き方で説明致します。
Shift_jisで書かれたcsvをUTF-8に変換する際、エラーが起きて変換出来ない件について。

課題

エラー文
"\xFB\xFC" from Shift_JIS to UTF-8

コード

CSV.foreach({ファイル名}, encoding: "Shift_JIS:UTF-8", headers: true) do |row|

Shift_JIS→UTF-8に変換してcsv出力したいのに、、エラーが。。
どう対処すれば、、。

何が起きている??

このエラー文は、変換出来ない文字が含まれているよー!だから変換出来ません!と教えてくれています。

変換出来ない文字とは、Shift_JISの範囲外の文字(旧字体)の事!
→Shift_JISで書かれたcsvに、Shift_JISが認識出来ない文字が含まれてる。

Shift_JISが認識出来ない文字(Shift_JISにはない文字):
「髙(はしご高), ①, ㈱, 﨑, ✖︎」などの旧字体!

csvにこの旧字体が含まれている事によって、エラーが起きてしまいます。

解決策

解決策は何通りかありますが、(1)一番確実に変換出来るものと、(2)簡単に変換出来るものをご紹介します!

(1) 強制変換

初めから、UTF-8としてcsvを読み込ませる!

CSV.parse(NKF::nkf('-w',File.read({ファイル名})), headers: true) do |row|

Shift_JIS → UTF-8 ( Shift_JISで書かれたcsv を UTF-8に変換する!)
のではなく、

Shift_JISで書かれたcsvを初めからUTF-8として変換する
初めからUTF-8として変換する事によって、文字化けも起こる事なく、エラーも回避する事が出来ます!

(2) CP932を使う

単純です

CSV.foreach({ファイル名}, encoding: "CP932:UTF-8", headers: true) do |row|

Shift_JIS → UTF-8 
CP932 → UTF-8 (ただ、Shift_JISをCP932に変えただけ)

こちらでも、文字化け、エラーを防ぐ事が出来ます!

が、

これは、macでcsvファイルを作ったと想定した場合です。
Windowsでcsvファイルを作った場合を想定していません。。

Windowsでcsvファイルを作った場合:
csvファイルの文字コードが Windows-31Jの場合があります。
その場合は、もちろん、CP932 → UTF-8 と変換しようとしたら、、

Windows-31Jで書かれているファイルなのでCP932なんて知りませんよー。と怒られる可能性があります。

mac,Windowsどちらも対応したいのであれば、(1) 強制変換 がオススメ!!
で、確実!!

ぜひ、お試しください^^

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

Shift_jis(CP932) -> UTF-8変換の例外処理

Shift_jis(CP932) -> UTF-8へのcsvの変換機能

今回はrubyで開発をしたので、rubyの書き方で説明致します。
Shift_jisで書かれたcsvをUTF-8に変換する機能を実装した際、エラーが起きてしまい変換出来ない件について。

課題

エラー文
"\xFB\xFC" from Shift_JIS to UTF-8

コード

CSV.foreach({ファイル名}, encoding: "Shift_JIS:UTF-8", headers: true) do |row|

Shift_JIS→UTF-8に変換してcsv出力したいのに、、エラーが。。
何を言っているんだ?
どーいうエラー?
どう対処すれば、、。

何が起きている??

このエラー文は、変換出来ない文字が含まれているよー!だから変換出来ません!と教えてくれています。

変換出来ない文字とは、Shift_JISの範囲外の文字(旧字体)の事!
→Shift_JISで書かれたcsvに、Shift_JISが認識出来ない文字が含まれてる。

Shift_JISが認識出来ない文字(Shift_JISにはない文字):
「髙(はしご高), ①, ㈱, 﨑, ✖︎」などの旧字体!

csvにこの旧字体が含まれている事によって、エラーが起きてしまいます。

例)
sample.csv :
山田花子,前田敦子,髙田啓介    ← 「髙」が紛れ込んでいる

解決策

解決策は何通りかありますが、(1)一番確実に変換出来るものと、(2)簡単に変換出来るものをご紹介します!

(1) 強制変換

初めから、UTF-8としてcsvを読み込ませる!

CSV.parse(NKF::nkf('-w',File.read({ファイル名})), headers: true) do |row|

Shift_JIS → UTF-8 ( Shift_JISで書かれたcsv を UTF-8に変換する!)
のではなく、

Shift_JISで書かれたcsvを初めからUTF-8として変換する
初めからUTF-8として変換する事によって、文字化けも起こる事なく、エラーも回避する事が出来ます!

(2) CP932を使う

単純です

CSV.foreach({ファイル名}, encoding: "CP932:UTF-8", headers: true) do |row|

Shift_JIS → UTF-8 
CP932 → UTF-8 (ただ、Shift_JISをCP932に変えただけ)

こちらでも、文字化け、エラーを防ぐ事が出来ます!

お試しください^^

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

RailsでHamlを書く

概要

初心者向けにhamlの基礎的な書き方とhamlにおけるrailsヘルパーメソッドのルールを書きました。
特にヘルパーメソッドの書き方は初心者がよくsyntaxエラーを起こしやすい箇所なので参考になればと思います。

HTMLタグ

基礎

HTMLタグは基本的に「%」をつければOKです。

haml
%div
%a
%input
%header
結果(HTML)
<div></div>
<a></a>
<input>
<header></header>

クラス名、ID名

クラス名は「.(ドット)」、ID名は「#(シャープ)」をそれぞれHTMLタグの後ろにつけることで作成できます。
「.」や「#」を連続することで複数のクラス名やID名をつけることができます。

また、HTMLタグ無しでクラス名やID名を記述するとdivタグになります。(divタグの省略記法)

haml
%div.class-name
%div#id-name
%div.class1.class2.class3
.no-html-tag
#no-html-tag
結果(HTML)
<div class="class-name"></div>
<div id="id-name"></div>
<div class="class1 class2 class3"></div>
<div class="no-html-tag"></div>
<div id="no-html-tag"></div>

入れ子(ネスト)

ネストは半角2スペースで表現します。
(厳密に言うと2スペース以外もできますが、通例では2スペースです。)

haml
.parent
  .child
結果(HTML)
<div class="parent">
  <div class="child"></div>
</div>
haml
.parent
  .child
    子供だよ
結果(HTML)
<div class="parent">
  <div class="child">子供だよ</div>
</div>

ネストの深さでどのHTMLタグがどのHTMLタグの親かが決定します。
最初は見慣れないですが、慣れると可読性が高く、記述もHTMLより素早くできます。

読むコツとしてはそのHTMLタグから視線をまっすぐ下に降ろすことです。

<下記hamlコードを例に>
「.parent」の子供はどこまでかを把握したい場合
「.parent」と同じネストの深さ(0 space)で書かれているのは「.parent-brother」なので、それまでの「.child」「.grand.child」「.brother」が子供です。

「.child」の子供はどこまでかを把握したい場合
「.child」と同じネストの深さ(2 space)で書かれているのは「.brother」なので、それまでの「.grand.child」が子供です。

「.brother」の子供はどこまでかを把握したい場合
次の行が自分より浅いネスト(0 space)の「.parent-brother」なので「.brother」は子供がありません。

haml
.parent
  .child
    .grandchild
  .brother
.parent-brother
  .child
結果(HTML)
<div class="parent">

  <div class="child">
    <div class="grandchild"></div>
  </div>

  <div class="brother"></div>
</div>

<div class="parent-brother">
  <div class="child"></div>
</div>

railsヘルパーメソッド

基礎

erbでは変数やヘルパーメソッドなどの表示したいものは「<%= 記述 %>」
ifやeachなどの表示したくない処理等は「<% 記述 %>」で記述していました。

hamlでは以下になります。

  • 表示したいものは「= 記述」
  • 表示したくないものは「- 記述」
erb
<%= link_to root_path %>
<% hello = "こんにちわ" %>
<%= hello %>
haml
= link_to root_path
- hello = "こんにちわ"
= hello
結果(HTML)
<a href="/">/</a>
こんにちわ

<!-- 「- hello = "こんにちわ"の部分は表示されない」 -->

クラス名・ID名

ヘルパーメソッドにクラス名・ID名を付与する場合はHTMLタグとは違い、それぞれのヘルパーメソッドの構文に従います。
例えばLink_toは以下のような書き方が可能なのでそれに従って書きます。

構文
link_to(body, url = {}, html_options = {})
  # url is a String; you can use URL helpers like
  # posts_path

link_to(url, html_options = {}) do
  # name
end
haml
= link_to "リンクだよ",root_path,{class:"class-name",id:"id-name"}
= link_to root_path,{class:"class-name",id:"id-name"} do
  リンクだよ
HTML(結果、どちらも同じ)
<a class="class-name" id="id-name" href="/">リンクだよ</a>

ネスト

ネストのルールはHTMLの項目で述べたものと同じく半角2スペースが通例です。
ここで意識すべきは記述の始まりは「=」や「-」であることです。
以下は「link_to」の始まりが揃っていますが、「=」の位置が違うのでネストの深さが違います。
初心者はよくここでエラーを起こしやすいです。

haml
%div
  = link_to root_path -#=>正 2space nest
 =  link_to root_path -#=>誤 1space nest
結果(HTML)
<div>
  <a href="/">/</a>
</div>

また、逆にrailsヘルパーメソッドへ他の要素をネストさせる場合はヘルパーメソッドの構文に従います。

ここで意識すべきは「do end」で閉じる系のメソッドはendの閉じタグが不要になることです。
例えば「do end」で閉じて他の要素をネストさせることができるlink_toは以下のようになります。

erb
<%= link_to root_path do %>
  リンクだよ
<% end %>
haml
= link_to root_path do
  リンクだよ
結果(HTML)
<a href="/">リンクだよ</a>

おわりに

初心者に「公式ドキュメントを読め!」は流石に鬼畜な気もするので近日中に頻出ヘルパーメソッドについてはまとめようかと思います。
また、当記事を読んで疑問・修正等があればコメントいただけると助かります。
いいねをしていただけるとモチベーションになります!!

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

【heroku】ActiveRecord::IrreversibleMigration が出たときの対処法

nemlog検索サイト https://searchnemlog.herokuapp.com/
作成の時herokuデプロイで詰まってしまったので解決方法をメモしときます。

ActiveRecord::IrreversibleMigration

おそらくスクレイピングを通してデータベースに保存していたので起きてしまったエラーと思われます。

以下からはこのエラーの解決手順を記したいと思います。

ロールバック

heroku run rake db:migrate:reset

恐らくエラー文が表示されると思います。

そこに

DISABLE_DATABASE_ENVIRONMENT_CHECK=1

と書かれていたらそれを利用してDB生成が可能になります。

ドロップ

heroku run RAILS_ENV=production DISABLE_DATABASE_ENVIRONMENT_CHECK=1 bundle exec rake db:drop

先ほどのエラー文を利用してドロップするとなんとかドロップしてくれます。

DB生成

あとはdb:createを実行。

heroku run rails db:create

マイグレーション

最後はいつも通り heroku run をしましょう。

heroku run rake db:migrate

この手順でいけばなんとかアプリが立ち上がると思います。

まとめ

データベース系のエラーにハマるとデリケートなイメージなのでめちゃくちゃビビり倒してしまいます。

herokuエラーは前回大概ハマったと思っていましたがまだまだ見知らぬエラーがたくさんである意味奥が深いですね。

とりあえず誰かの参考になればと思います。

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

bin とは

binファイルとは?
binとはbinaryの略で、binファイルとは
テキスト形式ではなくバイナリ形式で書かれているデータを扱うファイル。

バイナリデータとは、コンピュータが理解するためのプログラムが書かれたデータのことである。

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

rails_same_site_cookie gemで、RailsアプリにChrome 80向けのSameSite属性を指定する

はじめに

以下の記事にあるとおり、Chrome 80では2020年2月17日の週以降にデフォルトのSameSite属性が変更されます。

Chrome 80が密かに呼び寄せる地獄 ~ SameSite属性のデフォルト変更を調べてみた - Qiita

この変更が入ると、次のように「決済や認証などで外部サービスを利用し、外部サイトからPOSTで戻ってくるサイト」でユーザーが識別できないエラーが発生します。

https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_117852_84f60298-2a3f-65b0-5ff6-daacf12a1c4b.png
(画像の引用元:Chrome 80が密かに呼び寄せる地獄 ~ SameSite属性のデフォルト変更を調べてみた - Qiita

本記事ではRailsアプリケーションでこの問題に対応する方法を紹介します。

免責事項 (disclaimer)

この記事のとおりにあなたのRailsアプリケーションを変更して、何らかの不具合やセキュリティ上の問題が発生しても筆者は一切の責任を負いません。

技術的な背景と最新の技術情報を十分理解した上で、本記事の内容を適用してください。
(適用する必要がない場合は何もしないでください)

対応方法

rackのバージョンを2.1.0以上に上げます。
(2.1.0以上でないと、SameSite=None属性に対応していないため - 参考

(訂正:rails_same_site_cookieはrackの機能ではなく、独自にCookieを設定しているため、必ずしもrack 2.1.0以上に上げる必要はないようです)

rails_same_site_cookie gemをインストールします。

Gemfile
gem 'rails_same_site_cookie'
$ bundle install

対応は以上です。

rails_same_site_cookie gemがやってくれること

rails_same_site_cookie gemをインストールすると、自動的に全cookieにSameSite=None; Secure属性が追加されます。

ただし、iOS 12とmacOS 10.14のSafariなど、SameSite=None; Secure属性を付けると不具合が発生するブラウザ(参考)に対してはこの属性を付与しません。

rails_same_site_cookie gemがやってくれないこと

rails_same_site_cookie gemはRails側のCookieを変更するだけで、JavaScript側で設定するCookieには何も変更を加えません。
もしJS内でCookieを設定しているコードがある場合は、何らかの対応が必要になります。(詳細未調査)

参考1:動作確認の手順(例)

「決済で外部サービスを利用し、外部サイトからPOSTで戻ってくるサイト」を想定した場合の確認手順です。
いきなり本番環境で試すのではなく、テスト環境(ステージング環境)で試すようにしてください。

  1. こちらの手順に従って、Chromeの設定を変更する
  2. Railsアプリサイト上で商品を購入し、外部の決済サービスに遷移する
  3. 2分以上待つ(2分以上待たないとChromeがCookieを送信してしまうため)
  4. 決済を実行して、自サイトに戻る
  5. ログイン情報が維持されたまま、正常に決済が完了することを確認する(修正前は何らかの不具合が発生することも事前に確認しておく)

参考2:リクエストスペックでテストを書く(例)

SameSite=None属性が適切に付与されているかどうかを確認するリクエストスペックの記述例です。
(非SSLで接続する場合、rails_same_site_cookie gemはSameSite=None属性だけを付与し、Secure属性は付与しません)

ログイン処理はDeviseで実現されていることを前提とします。
テストコード内の_your_app_sessionは、適宜ご自身のアプリケーション名に合わせて変更してください。

spec/requests/cookies_spec.rb
require 'rails_helper'

RSpec.describe 'Cookies', type: :request do
  describe 'Cookie の SameSite 属性' do
    before do
      user = create :user
      login_as user
    end
    it 'User-Agent指定無しの場合 SameSite=None がつく' do
      get new_user_session_path
      expect(response.headers['Set-Cookie']).to match /_your_app_session=.*SameSite=None/
    end

    it 'SameSite=Lax がデフォルトになる Chrome 80 では SameSite=None がつく' do
      mac_chrome_80 = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.42 Safari/537.36      '
      win_chrome_80 = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.16 Safari/537.36'

      get new_user_session_path, headers: { 'User-Agent' => mac_chrome_80 }
      expect(response.headers['Set-Cookie']).to match /_your_app_session=.*SameSite=None/

      get new_user_session_path, headers: { 'User-Agent' => win_chrome_80 }
      expect(response.headers['Set-Cookie']).to match /_your_app_session=.*SameSite=None/
    end

    it 'SameSite=Noneの扱いにバグがある iOS12 Safari では SameSite=None がつかない' do
      iphone_ios12_user_agent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1'
      get new_user_session_path, headers: { 'User-Agent' => iphone_ios12_user_agent }
      expect(response.headers['Set-Cookie']).to include '_your_app_session='
      expect(response.headers['Set-Cookie']).not_to include 'SameSite'
    end
  end
end

参考文献

本記事を書くにあたって、下記記事を参考にさせてもらいました。
どうもありがとうございました。

Chrome 80が密かに呼び寄せる地獄 ~ SameSite属性のデフォルト変更を調べてみた - Qiita

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

【Rails】ActiveRecordの`find_by`で大文字と小文字を区別しないで取得する方法

はじめに

find_byメソッドで値を取得する際にハマったので解決方法を探してみました。

Rails : 6.0
Ruby: 2.7
SQLite3

以上の環境しか確認していません。

検索対象と検索したい文字列を大文字に変換

保存されたタグを取得する際、大文字と小文字の違いで期待した値を取得することができない。

$ rails console 
 # 大文字と小文字を混ぜた状態で保存します
 > Tag.create(tag_name: "RuBy")
 >
 # 小文字で先程作成したタグ名を検索してみます
 > Tag.find_by(tag_name: "ruby")
 #=> nil

Tags テーブルに保存された値は"RuBy"と保存しているためfind_byメソッドで"ruby"と検索しても取得することができません。

ユーザーがタグを検索したい場合、"ruby""Ruby""RUBY"など様々な入力で検索する可能性があるため、大文字と小文字で区別されてしまうのは不便です。


大文字と小文字を区別せずfind_byメソッドで取得するためには、SQL のupper()関数を使いカラムに含まれる文字列を大文字に変換して、検索したい文字列も Ruby の upcaseメソッドで大文字に変換して取得します。

$ rails console
# 大文字と小文字を混ぜた状態で保存します
 > Tag.create(tag_name: "RuBy")
 >
 # 検索対象のカラムと検索したい文字列をそれぞれ大文字に変換
 > Tag.find_by('UPPER(tag_name) = ?', "ruby".upcase)
 #=> tag_name: "RuBy"

これで Tags テーブルの値を大文字と小文字を区別することなく取得することができました。

'UPPER()'と大文字にしているのは SQL 的な記述のためなので、特に意味はありません upper()としても動作します。

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

Leetcode: Longest Common Ancestor Of Two Nodes

require 'minitest/autorun'
require 'pry'
class LongestCommonAncestorOfTwoNodes < Minitest::Test

  def test_run
    root = [3, 5, 1, 6, 2, 0, 8, nil, nil, 7, 4]
    p = 5
    q = 1

    tree = TreeNode.new(3)
    tree.left = TreeNode.new(5)
    tree.left.left = TreeNode.new(6)
    tree.left.right = TreeNode.new(2)
    tree.left.right.right = TreeNode.new(4)
    tree.left.right.left = TreeNode.new(7)

    tree.right = TreeNode.new(1)
    tree.right.left = TreeNode.new(0)
    tree.right.right = TreeNode.new(8)
    assert_equal(3, lowest_common_ancestor(tree, p, q))
  end

  def lowest_common_ancestor(root, p, q)
    return nil if root.nil?

    if root.val == p || root.val == q
      return root
    end

    left = lowest_common_ancestor(root.left, p, q)
    right = lowest_common_ancestor(root.right, p, q)

    if left != nil && right != nil
      root
    else
      left ? left : right
    end
  end

end

class TreeNode
  attr_accessor :val, :left, :right

  def initialize(val)
    @val = val
    @left, @right = nil, nil
  end
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Leetcode: Lowest Common Ancestor Of Two Nodes

require 'minitest/autorun'
require 'pry'
class LowestCommonAncestorOfTwoNodes < Minitest::Test

  def test_run
    root = [3, 5, 1, 6, 2, 0, 8, nil, nil, 7, 4]
    p = 5
    q = 1

    tree = TreeNode.new(3)
    tree.left = TreeNode.new(5)
    tree.left.left = TreeNode.new(6)
    tree.left.right = TreeNode.new(2)
    tree.left.right.right = TreeNode.new(4)
    tree.left.right.left = TreeNode.new(7)

    tree.right = TreeNode.new(1)
    tree.right.left = TreeNode.new(0)
    tree.right.right = TreeNode.new(8)
    assert_equal(3, lowest_common_ancestor(tree, p, q))
  end

  def lowest_common_ancestor(root, p, q)
    return nil if root.nil?

    if root.val == p || root.val == q
      return root
    end

    left = lowest_common_ancestor(root.left, p, q)
    right = lowest_common_ancestor(root.right, p, q)

    if left != nil && right != nil
      root
    else
      left ? left : right
    end
  end

end

class TreeNode
  attr_accessor :val, :left, :right

  def initialize(val)
    @val = val
    @left, @right = nil, nil
  end
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

google api  住所から経度緯度取得してgoogle map に表示

やりたいこと

レストラン(写真。店名。店の説明。住所)の投稿の際に
住所入力したら、selfで経度緯度所得してgoogle map にピン立てる。

環境

ruby 2.5.1
rails 5.2.3

実装

DBの中身
postsテーブル

カラム名 内容
address string 住所
latitude float 住所緯度
longitude float 住所経度
title text 店の名前
description text 店の説明
image string レストランの写真

google API 所得

参考サイト
(https://nendeb.com/276)

Maps JavaScript API
Geocoding API
この二つの有効化

gem 導入

Gem.file
gem "gmaps4rails"
gem "geocoder"

JS

ターミナル.
rails g gmaps4rails:copy_js
application.html.haml
    %script(src="//maps.google.com/maps/api/js?v=3.23")
    %script(src="//cdn.rawgit.com/mahnunchik/markerclustererplus/master/dist/markerclusterer.min.js")
    %script(src="//cdn.rawgit.com/printercu/google-maps-utility-library-v3-read-only/master/infobox/src/infobox_packed.js" type="text/javascript")
    %script(src="/javascripts/gmaps_google.js")

Model

post.rb
  geocoded_by :address
  after_validation :geocode

  private
  def geocode
    uri = URI.escape("https://maps.googleapis.com/maps/api/geocode/json?address="+self.address.gsub(" ", "")+"&key=#{Rails.application.credentials.google_map_api}")
    res = HTTP.get(uri).to_s
    response = JSON.parse(res)
    self.latitude = response["results"][0]["geometry"]["location"]["lat"]
    self.longitude = response["results"][0]["geometry"]["location"]["lng"]
  end

controller

posts_controller.rb
 def show
    @post = Post.find(params[:id])
  end

  private
  def post_params
    params.require(:post).permit(:image, :description, :text, :address, :latitude, :longitude)
  end

veiw

show.html.haml
    #map
      :javascript
        function initMap() {

          var test = {lat: #{@post.latitude}, lng: #{@post.longitude}};
          var map = new google.maps.Map(document.getElementById('map'), {
            zoom: 15,
            center: test
          });
          var transitLayer = new google.maps.TransitLayer();
          transitLayer.setMap(map);

          var contentString = '住所:#{@post.address}';
          var infowindow = new google.maps.InfoWindow({
            content: contentString
          });

          var marker = new google.maps.Marker({
            position:test,
            map: map,
            title: contentString
          });

          marker.addListener('click', function() {
            infowindow.open(map, marker);
          });
        }
      %script{:async => "", :defer => "defer", :src => "https://maps.googleapis.com/maps/api/js?v=3.exp&key=#{Rails.application.credentials.google_map_api}&callback=initMap"}
show.scss
#map {
  height: 400px;
  margin-left:auto;
  margin-right:auto;
  text-align:left;
  width: 80%
}

大切なこと

post.rbとshow.html.hamlのところに
下記の記載があると思います。

#{Rails.application.credentials.google_map_api}

この記載を消して自分で所得したAPIを打ち込めば動きますが
gitでプロジェクト管理してる時にgitにあげるとAPI_KEYが悪用される可能性があり、gitパトロールから注意が入ります。
なので環境変数を使います。

環境変数の設定

rails のバージョンによって異なります。
今回はrails 5.2.3を使用しています。

ターミナル.
EDITOR="vi" bin/rails credentials:edit

上記コマンドで環境変数の設定を行います。
aws:~~~~はデフォルトで書いてあると思います。
今回はgoogle_map_apiという変数に所得したAPI_KEYを代入している形です。
変数は自分の好きな名前で結構です。

iで入力モードで編集追加できます。
:wqで保存、上書き保存できます。

# aws:
#   access_key_id: 123
#   secret_access_key: 345


google_map_api: 所得したAPI_KEY

終わりに

私は今回このようなコードで住所から緯度経度所得してmapに表示させました。
gitにもあげているので詳細なコード確認したい方がおられましたら、キータの私のページにgitのリンクありますので見てください。
favorite_food_shareというプロジェクト名です。(キータ記事とコード少し違うところあります。)

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

factory_botでモデルのenum別のtraitを一発で書く小ネタ

image.png

今回はRailsでテストデータを作成する”factroy_bot”に関する小ネタです。

image.png

TL DR;

こんな感じでイケちゃう

User.account_type.values.each do |type|
    trait :"#{type}" do
        account_type { type }
    end
end

動作環境

  • Rails: 5.2.3
  • Ruby: 2.6.5
  • factory_bot: 5.0.2
  • factory_bot_rails: 5.0.1

※今回はRubyの記法に依る部分が大きいので、上記バージョンはあまり気にしなくても良いです

コード例

想定するモデル

この記事では以下のようなUserモデルを例として考えます。プロダクトコードによくある「複数のユーザー種類をenumのカラム(今回は例としてaccount_typeとする)で持ち判別する」モデルです。

app/models/user.rb
class User < ApplicationRecord
  validates :name, presence: true
  # 中略
  extend Enumerize
  enumerize :account_type, in: { normal: 0, admin: 1, client: 2, development: 3 }, scope: true
end

どうでもいい話ですが、色々な種類のユーザーが存在し、しかも同じユーザーが複数の種類を持つといった、ユーザー周りが複雑になるのはあるあるですが、本当にツラいですよね。AdminかEndUserかの2択ぐらいで収まれば、それぞれのユーザーから見える画面もきれいに分割できて丁度いいのですが...。

普通にfactoryを書く場合

さて、このようなenumカラムを持つUserモデルを素直にfacrotyで表現すると、このようになります。ああめんどくさい。モデルが複雑になれば、factoryが複雑になるのは当然のことですが...

spec/factories/user.rb
FactoryBot.define do
  factory :user do
    name { 'test' }
    # 中略
    trait :normal do
      account_type { normal }
    end

    trait :admin do
      account_type { admin }
    end

    trait :client do
      account_type { client }
    end

    trait :development do
      account_type { development }
    end
  end
end

やりたいこと

こんな意味のないコードをチマチマ全部書くのはやりたくない。いやでも書くしか無いし、一旦Pushするか...。いや、なんかそれらしいカッコいいやり方があるはずだ。多分。ほんの数行で全部のenumのtraitを生み出すやり方が。ついでに、カラムのenum定義が増えたときにも勝手に増えるようにしてほしい。忘れそうだし。

小技

そこで小技を使うと、こんな感じで書けます。「まさか動かないだろう」と思い、冗談で書いたら動きました。Rubyってすごい。

spec/factories/user.rb
User.account_type.values.each do |type|
    trait :"#{type}" do
        account_type { type }
    end
end

User.account_type.valuesと、enumのカラムから直接traitを作っているので、enumの定義が増えたときにテストデータを作り忘れることもありません。ユーザーの氏名やアドレスを表すカラムがあれば、そこに同様にtype変数を突っ込めば、RSpecのテスタビリティも向上しそうです。

ちなみに、User.account_type.valuesの部分はこんな感じで動作しています。ハッシュでenumを取り出して、そのvalueをvaluesメソッドで配列化し、その配列の値でtraitを定義しているわけです。もちろん必要であれば、keyとvalueの両方を取り出して使うこともできます。

[1] pry(main)> User.account_type.values
=> ["normal", "admin", "client", "development"]
[2] pry(main)> User.account_type
=> #<Enumerize::Attribute:0x0000558ff649f058
 @i18n_scopes=["enumerize.user.account_type"],
 @klass=User (call 'User.connection' to establish a connection),
 @name=:account_type,
 @skip_validations_value=false,
 @value_hash=
  {"0"=>"normal",
   "1"=>"admin",
   "2"=>"client",
   "3"=>"development",
   "normal"=>"normal",
   "admin"=>"admin",
   "client"=>"client",
   "development"=>"development"},
 @values=["normal", "admin", "client", "development"]>

余談...

まあ、実際のプロダクトコードでは、各enumごとに紐付くアソシエーションも変更する必要があったりするので、なかなかこれ一つで完璧にtraitを表現することは難しいです。とはいえ、必要であれば切り出して書けばよいですし、アソシエーションが複雑になる前にスピーディーにテストデータを作りたい場合はぜひ。

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

Railsでbundle installができない

問題

bundle installをすると

Installing mysql2 0.5.2 with native extensions
Gem::Ext::BuildError: ERROR: Failed to build gem native extension.
(略)
An error occurred while installing mysql2 (0.5.2), and Bundler cannot continue.
Make sure that gem install mysql2 -v '0.5.2' --source 'https://rubygems.org/' succeeds before bundling.

と出る

指示通り
gem install mysql2 -v '0.5.2' --source 'https://rubygems.org/'
を走らせても解決しない

解決策

bundle configを実行すると

build.mysql2
Set for your local app (/Users/ユーザー名/アプリケーション名/.bundle/config): "--with-cppflags=-I/usr/local/opt/openssl@1.1/include"
と出る

そこで
bundle config --local build.mysql2 "--with-ldflags=-L/usr/local/opt/openssl@1.1/lib"
を実行した

bundle configで確認すると
build.mysql2
Set for your local app (/Users/GO/source_code/clubru/.bundle/config): "--with-ldflags=-L/usr/local/opt/openssl@1.1/lib"
となっていた

再度bundle installを実行すると無事mysqlをインストールできた

調べて出てきた結果と違ったこと

自分の場合は
bundle config --local build.mysql2 "--with-ldflags=-L/usr/local/opt/openssl@1.1/lib"
で解決したが、調べて出てきた解決策は
bundle config --local build.mysql2 "--with-ldflags=-L/usr/local/opt/openssl@1.1/lib --with-cppflags=-I/usr/local/opt/openssl@1.1/include"

bundle config --local build.mysql2 "--with-cppflags=-I/usr/local/opt/openssl@1.1/include"
datta

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

【Ruby】gsub()で複数の文字を変換する方法

概要

まずgsubメソッドはこのように書きます。

文字列.gsub(置換したい文字列, 置換後の文字列)
test.rb
puts "hogehugu".gsub(/hugu/){"hoge"}

#hogehoge   実行結果

本題

例えば",を同時に削除したい場合、間にこれ|を挟むだけでOK

test.rb
puts 'あい,うえ"お'.gsub(/,|"/){""}

#あいうえお   実行結果

参照元

【Ruby】 gsubで複数の文字パターンを置換する方法

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

「rbenv: rails: command not found」の対処法

発生原因

これは主にRuby(rbenv)のversionを新しくした場合に発生するそうです。
私の場合は2.5.1から2.6.4にして$ rails newした際に発生しました。

~/MyApp
❯ rails _5.1.6_ new portfolio
rbenv: rails: command not found

The `rails' command exists in these Ruby versions:
  2.5.1

状況確認

まずrbenv(2.6.4)の中身を確認してみます。

~/.rbenv/versions/2.6.4/bin
❯ ls
bundle  bundler erb     gem     irb     rake    rdoc    ri      ruby

注目すべきはbundleとrailsの有無みたいです。
私の場合はbundleは有ってrailsが無いですね。
ちなみに今まで使用していた(2.5.1)の中身を見てみると

~/.rbenv/versions/2.5.1/bin
❯ ls
annotate        gem             nokogiri        rspec           spring
aws.rb          gem2gv          pry             rubocop         sprockets
bundle          git2gv          puma            ruby            thor
bundler         htmldiff        pumactl         ruby-parse      tilt
byebug          i18n-tasks      rackup          ruby-rewrite    update_rubygems
coderay         irb             rails           ruby2gv         xml2gv
dot2ruby        launchy         rake            sass            yard
erb             ldiff           rdoc            sass-convert    yardoc
erd             listen          ri              scss            yri

こんな感じでした。当然ですがbundleもrailsも有りますね。

対処する

私の場合はrailsが無かったのでインストールします。
まずRailsチュートリアルに沿って.gemrcに対して次のコマンドを設定した後に実行しました。

~
❯ printf "install: --no-document \nupdate:  --no-document\n" >> ~/.gemrc

~
❯ gem install rails -v 5.1.6
Fetching concurrent-ruby-1.1.5.gem
Fetching thread_safe-0.3.6.gem
Fetching tzinfo-1.2.6.gem
Fetching i18n-1.8.2.gem
Fetching activesupport-5.1.6.gem
Fetching nokogiri-1.10.7.gem
Fetching mini_portile2-2.4.0.gem
...
# 省略

bundleが無かった方は$ bundle installで解決するそうです。
両方無い方は$ bundle installした後に$ gem install railsを行ってください。

動作確認

実行できるか確認してみます。

~/MyApp
❯ rails _5.1.6_ new portfolio
      create  
      create  README.md
      create  Rakefile
      create  config.ru
      create  .gitignore
      create  Gemfile
...
# 省略

無事うまくいきました!

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

生に近い HTTP リクエスト情報を調査するために Ruby で Web サーバを作って Heroku にデプロイ

概要

  • できるだけ生に近い HTTP リクエスト情報を調査するために Web サーバを作る
  • Ruby の標準ライブラリのみを使用する
  • Web サーバを macOS Catalina で動作させて curl からの HTTP リクエストを確認する
  • Web サーバを Heroku で動作させて curl からの HTTP リクエストを確認する

Web サーバのソースコード

Ruby 標準ライブラリのみを使用する。
マルチスレッドには非対応。

myserver.rb
require 'socket'

# 標準出力を同期モードに設定
$stdout.sync = true

# 接続を受け付けるポート番号を決定
# 環境変数 PORT が設定されているならそれを設定
port = 8000
port = ENV['PORT'].to_i if ENV['PORT']

# サーバー接続をオープン
server = TCPServer.open(port)

# HTTP リクエストを待ち続ける
loop do

  begin

    # TCPSocket オブジェクトを取得
    socket = server.accept

    # 受け付けた日時を出力
    puts "[info]#{Time.new}"

    # HTTP リクエスト開始行を出力
    if not req_start_line = socket.gets
      puts '[info]req_start_line is nil'
      next
    end
    puts "#{req_start_line}"

    # HTTP リクエストヘッダーを1行ずつ出力
    while req_header = socket.gets.chomp
      puts "#{req_header}"
      break if req_header == '' # ヘッダー終了
      # Content-Length ヘッダーがあれば値を変数にセット
      h = req_header.split(':')
      content_length = h[1].strip.to_i if h[0].strip.downcase == 'content-length'
    end

    # Content-Length がある場合はボディを出力
    if content_length != nil
      puts socket.read(content_length)
    end

    # HTTP レスポンスを返す
    # 本文データ
    body = "<html><body>Hello, world</body></html>\r\n"
    # ステータス行
    socket.write "HTTP/1.1 200 OK\r\n"
    # ヘッダー
    socket.write "Server: #{RUBY_DESCRIPTION}\r\n"
    socket.write "Content-Type: text/html; charset=utf-8\r\n"
    socket.write "Content-Length: #{body.bytesize}\r\n"
    socket.write "Connection: close\r\n"
    # 空行
    socket.write "\r\n"
    # 本文
    socket.write body

  rescue => e
    puts e.full_message

  ensure
    # HTTP 接続を閉じる
    puts '[info]close this socket'
    socket.close
  end

end

server.close

macOS Catalina で動かす

macOS Catalina + Ruby 2.7.0 の環境で Web サーバプログラム myserver.rb を起動する。

$ ruby myserver.rb

curl から HTTP GET リクエストする例

クライアント側のコマンドと出力結果。

$ curl -i -H 'small: sss' -H 'LARGE: LLL' http://localhost:8000/foo/?aaa=xxx
HTTP/1.1 200 OK
Server: ruby 2.7.0p0 (2019-12-25 revision 647ee6f091) [x86_64-darwin19]
Content-Type: text/html; charset=utf-8
Content-Length: 40
Connection: close

<html><body>Hello, world</body></html>

サーバ側の出力結果。

[info]2020-02-04 08:03:08 +0900
GET /foo/?aaa=xxx HTTP/1.1
Host: localhost:8000
User-Agent: curl/7.68.0
Accept: */*
small: sss
LARGE: LLL

[info]close this socket

curl から HTTP POST リクエストする例

クライアント側のコマンドと出力結果。

$ curl -i -H 'Content-Type: application/json' http://localhost:8000/bar/ -d '{"foo": {"bar": ["あいうえお"]}}'
HTTP/1.1 200 OK
Server: ruby 2.7.0p0 (2019-12-25 revision 647ee6f091) [x86_64-darwin19]
Content-Type: text/html; charset=utf-8
Content-Length: 40
Connection: close

<html><body>Hello, world</body></html>

サーバ側の出力結果。

[info]2020-02-04 08:03:14 +0900
POST /bar/ HTTP/1.1
Host: localhost:8000
User-Agent: curl/7.68.0
Accept: */*
Content-Type: application/json
Content-Length: 37

{"foo": {"bar": ["あいうえお"]}}
[info]close this socket

Heroku で動かす

Heroku + Ruby 2.7.0 の環境で Web サーバを起動する。

できるだけ生に近い HTTP リクエストを調査したいが、Heroku のリバースプロキシが間に入るためリクエストヘッダがいくつか増えてしまう問題がある(許容するしかない)。

Heroku にデプロイするために必要なファイル

サーバプログラム myserver.rb 以外に Gemfile, Gemfile.lock, Procfile が必要。

Gemfile

ruby '2.7.0'

Gemfile.lock

Gemfile.lock
GEM
  specs:

PLATFORMS
  ruby

DEPENDENCIES

RUBY VERSION
   ruby 2.7.0p0

BUNDLED WITH
   2.1.2

Procfile

web: ruby myserver.rb

curl から HTTP GET リクエストする例

クライアント側のコマンドと出力結果。

$ curl -i -H 'small: sss' -H 'LARGE: LLL' https://example.herokuapp.com/foo/?aaa=xxx
HTTP/1.1 200 OK
Date: Mon, 03 Feb 2020 23:03:28 GMT
Connection: keep-alive
Server: ruby 2.7.0p0 (2019-12-25 revision 647ee6f091) [x86_64-linux]
Content-Type: text/html; charset=utf-8
Content-Length: 40
Via: 1.1 vegur

<html><body>Hello, world</body></html>

サーバ側の出力結果。

[info]2020-02-03 23:03:28 +0000
GET /foo/?aaa=xxx HTTP/1.1
Host: example.herokuapp.com
Connection: close
User-Agent: curl/7.68.0
Accept: */*
Small: sss
Large: LLL
X-Request-Id: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
X-Forwarded-For: XXX.XXX.XXX.XXX
X-Forwarded-Proto: https
X-Forwarded-Port: 443
Via: 1.1 vegur
Connect-Time: 1
X-Request-Start: 1580771008907
Total-Route-Time: 0

[info]close this socket

curl から HTTP POST リクエストする例

クライアント側のコマンドと出力結果。

$ curl -i -H 'Content-Type: application/json' https://example.herokuapp.com/bar/ -d '{"foo": {"bar": ["あいうえお"]}}'
HTTP/1.1 200 OK
Date: Mon, 03 Feb 2020 23:03:36 GMT
Connection: keep-alive
Server: ruby 2.7.0p0 (2019-12-25 revision 647ee6f091) [x86_64-linux]
Content-Type: text/html; charset=utf-8
Content-Length: 40
Via: 1.1 vegur

<html><body>Hello, world</body></html>

サーバ側の出力結果。

[info]2020-02-03 23:03:37 +0000
POST /bar/ HTTP/1.1
Host: example.herokuapp.com
Connection: close
User-Agent: curl/7.68.0
Accept: */*
Content-Type: application/json
X-Request-Id: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
X-Forwarded-For: XXX.XXX.XXX.XXX
X-Forwarded-Proto: https
X-Forwarded-Port: 443
Via: 1.1 vegur
Connect-Time: 1
X-Request-Start: 1580771017243
Total-Route-Time: 0
Content-Length: 37

{"foo": {"bar": ["あいうえお"]}}
[info]close this socket

参考資料

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

【RSpec】重複のバリデーションエラーを回避するFactoryBotの書き方

ユーザー登録でemailを重複させないようにバリデーションをかけましが、テストのときにでFactoryBotで複数ユーザを予め生成する必要があったので方法を調べました。シーケンスを使ってユニークなデータを生成するやり方です。備忘録として残します。

問題点

ファクトリーで複数のユーザをセットアップする際に、emailをバリデーションでユニークなデータとして設定している場合、下記のようにするとテストコードが走る前に例外が発生します。

spec/factories/users.rb
FactoryBot.define do
  factory :user do
    user_name { "tester" }
    email { "tester@example.com" }
    password { "password" }
  end
end
spec/models/user_spec.rb
  it "複数のユーザー登録" do
    user1 = FactoryBot.create(:user)
    user2 = FactoryBot.create(:user)
  end

これでテストを実行すると、当然ですが次のようなバリデーションエラーが発生します。

ターミナル
Failures:
  1) 複数のユーザー登録
     Failure/Error: user2 = FactoryBot.create(:user)

     ActiveRecord::RecordInvalid:
       バリデーションに失敗しました: メールアドレスはすでに存在します

解決策

シーケンスを使います。シーケンスはファクトリから新しいオブジェクトを作成するたびにカウンタの値を1づつ増やしながら値を設定します。

spec/factories/users.rb
FactoryBot.define do
  factory :user do
    user_name { "tester" }
    sequence(:email) { |n| "tester#{n}@example.com" }
    password { "password" }
  end
end

email { "tester@example.com" }sequence(:email) { |n| "tester#{n}@example.com" }と書き換えることで、userが生成されるたびにが1づつ増えていき、tester1@example.comtester2@example.comのようにユニークで連続したemailが設定されます。

ターミナル
#見やすいように加工してあります
user1
<User id: 13, 
user_name: "tester", 
email: "tester1@example.com", 
password: "password", 
created_at: "2020-02-05 01:56:36", 
updated_at: "2020-02-05 01:56:36">
ターミナル
#見やすいように加工してあります
user2
<User id: 14, 
user_name: "tester", 
email: "tester2@example.com", 
password: "password",
created_at: "2020-02-05 01:57:21", 
updated_at: "2020-02-05 01:57:21">
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby コーティング規約について 構文編 3

はじめに

レイアウト編はこちらをクリック願います。
構文編 2 はこちらをクリック願います。
Rubyの基礎を学習中の方に向けて記載致します。
私自身これからチーム開発を行う上で大事にしたい。知っておきたいことをOutputします。

構文について

① 否定形のときはifよりunlessを優先的に使う。(もしくは||構文を使う。)

qiita.rb
# 悪い例
do_something if !some_condition

# 悪い例
do_something if not some_condition

# 良い例
do_something unless some_condition

# 良い例(||を使う場合)
some_condition || do_something

② unlessをelse付きで使ってはいけない。 肯定条件を先にしてから書き換える。

qiita.rb
# 悪い例
unless success?
  puts 'fail'
else
  puts 'success'
end

# 良い例
if success?
  puts 'success'
else
  puts 'fail'
end

さいごに

毎日更新します。
皆様の復習等にご活用頂けますと幸いです。

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

UUIDをHashidsに変換して戻してみた。

目的

DBでUUIDで生成したキーをもとにテーブル分割すると「table_dd6ee086-936c-4dae-bbd7-f94e4203d868」みたいに長くなるのが嫌でなんとか短くしたいです。
base64とかやってみたけど「+,/,=」みたいな記号のハンドリングがめんどくさかったです。
そこでhashidsという数値とアルファベットだけで変換してくれるライブラリがあったのでこれを使いました。
RustとRubyとPostgreSQLで使いたかったので、それぞれ試してみました。

コード

Rust

Cargo.toml
[package]
name = "test"
version = "0.1.0"
edition = "2018"

[dependencies]
uuid = { version = "^0.8", features = ["v4"] }
harsh = "^0.1"
main.rs
fn main() {
    let converter = harsh::HarshBuilder::new().salt("salt goes here!").init().unwrap();
    let my_uuid = uuid::Uuid::new_v4();
    println!("{}", my_uuid);
    let target = converter.encode_hex(&my_uuid.to_simple().to_string()).unwrap();
    println!("{}", target);
    let my_uuid = uuid::Uuid::parse_str(&converter.decode_hex(&target).unwrap()).unwrap();
    println!("{}", my_uuid);
}

Ruby

Gemfile
# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem "hashids"
main.rb
require 'securerandom'
require 'hashids'

uuid = SecureRandom.uuid
puts uuid
hashids = Hashids.new "this is my salt"
id = hashids.encode_hex(uuid.gsub(/-/,""))
puts id
decoded = hashids.decode_hex(id).downcase
uuid = "#{decoded[0..7]}-#{decoded[8..11]}-#{decoded[12..15]}-#{decoded[16..19]}-#{decoded[20..35]}"
puts uuid

PostgreSQL

select 
    t3.src
    ,t3.simple
    ,t3.id
    ,hashids.decode_hex(id := t3.id, salt := 'salt')::uuid as dst
from (
select 
    t2.src
    ,t2.simple
    ,hashids.encode_hex(hex := t2.simple, salt := 'salt') as id
from 
(
select
    t1.src
    ,regexp_replace(t1.src::text, '-', '', 'g') as simple
from (
 select gen_random_uuid() as src
) as t1
) as t2
) as t3

結果

3f44b2e6-d6c4-4d2e-9956-bb56bfd58032
gyBm1Kw7M9u8Ral478g6ixxRXZrB
3f44b2e6-d6c4-4d2e-9956-bb56bfd58032

ハイフンを抜くと4文字しか得しませんね?

参考

hashids
PL/pgSQL implementation of Hashids

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

手に馴染む道具(プログラミング言語)

はじめに

職人は、自分の道具を使えば使うほど手に馴染んで使いやすくなっていく。プログラミング環境でもエディタなんかはそう。そしてプログラミング言語もそう。

プログラミング言語の老舗といえばCであり、多くの職業プログラマはCを通ってきているはず。

そうでもないか。

いや、ある程度のスキルを持っている人はCに精通しているし、アセンブラも分かるだろう。

その前提で、やはり言語仕様(文法)はCライクなものが手に馴染みやすいと思うんだ。そういった点で、JavaScriptは手に馴染みやすい。JavaScript系は割りと好きな部類だ。

本題

しかし、世の人気を二分するメジャー・スクリプト言語RubyとPython、どちらもCライクではない。なぜだ。慣れてしまえばそれまでなのだが。

動的型付け言語は散々な言われようだが、スキルさえ問題なければアドホックでスピーディーな開発では超絶な生産性を達成できると思うんだ。

JavaScriptでnode.jsでも良いのだか、あれはもう全くLightweightではないよね。挙動も独特だし。

なぜ動的型付け言語でCライクな汎用かつlightweightなスクリプト言語がない、もしくは人気が無いのか。

という気持ちだけ表明してみた。

本日は以上。

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