20201216のRubyに関する記事は30件です。

[Ruby on Rails] macで環境構築をしよう

環境

macOS Catalina バージョン 10.15.7

今回インストールするバージョン
Ruby 2.6.6
Ruby on Rails 6.1.0

手順

Homebrewのインストール

Homebrewは、macOSオペレーティングシステム上のパッケージ管理システムのひとつです。
Railsで必要なものをインストールするためにHomebrewを使うので入っていない場合はインストールしましょう。

$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

インストールされたか確認するコマンド

$ brew help

Homebrewで使えるコマンド一覧が表示されればインストール成功です。

rbenvのインストール

rbenvは、複数のRubyのバージョンを管理し、プロジェクトごとにRubyのバージョンを指定して使うことを可能としてくれるツールです。

$ brew install rbenv ruby-build

インストールされたか確認するコマンド

$ rbenv --version

rbenv 1.1.2 などと表示されればインストール成功です。(数字はバージョンによって異なります)

次にターミナルを起動した際にrbenvの初期化を自動で行うように設定をします。

$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
$ echo 'if which rbenv > /dev/null; then eval "$(rbenv init -)"; fi' >> ~/.bash_profile
$ source ~/.bash_profile

Rubyのインストール

インストール可能なRubyのバージョン確認をします。

$ rbenv install -l

インストールしたいバージョン(今回は2.6.6)が一覧に表示されていたらインストールを続けていきます。
他のバージョンをインストールする場合は2.6.6の部分を置き換えればそのバージョンがインストールされます。

rubyのインストールは少し時間がかかるので注意!

$ rbenv install 2.6.6

インストールされたか確認するコマンド

$ rbenv versions

system
* 2.6.6

このように表示されればインストール成功です。

次にrubyのバージョンを確認してみます。

$ ruby -v

rubyのバージョンを確認してみて2.6.6になっていない場合は、先程インストールした2.6.6に切り替える作業をしていきます。

$ rbenv global 2.6.6
$ rbenv rehash

使用するバージョンを切り替えることができたのかを確認するコマンドを実行してみましょう。

$ ruby -v

ruby 2.6.6p146

このように表示されれば成功です。

bundlerをインストール

Gemとは?

rubyのパッケージ管理ツールです。gemを容易に管理でき、gemを配布するサーバの機能も持ちます。

例えばWebアプリケーションにユーザー登録やログイン機能などのユーザー認証機能を実装したい時、これらを一からコードを書くのはとても時間がかかってしまいますが、「devise」と呼ばれるgemをインストールするだけで簡単にユーザー認証で必要な機能が使えます。

bundler とは

bundlerとは、gemのバージョンやgemの依存関係を管理してくれるgemの一つです。 bundlerを使うことで、複数人での開発やgemのバージョンが上がってもエラーを起こさずに開発できます。

以下を実行することによって、bundlerをインストールできます。
インストールしていない場合はインストールしましょう。

$ gem install bundler

インストールされたか確認するコマンド

$ bundler -v

Railsのインストール

次に、 Rails のインストールです。まだしていない場合はインストールしましょう。

$ gem install rails

インストールされたか確認するコマンド

$ rails -v

Railsアプリケーションの開発

作成するアプリケーション用のディレクトリを作成します。今回はsampleという名前で作成します。
作成したらそのディレクトリの中に移動しましょう。

$ mkdir sample
$ cd sample

Railsで開発を始めるには、rails new アプリケーション名 というコマンドを実行します。
このコマンドを実行することで、入力したアプリケーション名と同名のフォルダが作成され、その中に開発に必要なフォルダやファイルが用意されます
今回はsample_appという名前のアプリケーションを作成していきます。

$ rails new sample_app

開発中のアプリケーションをブラウザで表示するためには、サーバーを起動する必要があります。サーバーの起動は、以下のコマンドを実行するだけで完了です。
(サーバーを起動する場所に注意!! cd sample_app  でカレントディレクトリを変更して起動)

$ rails s

サーバーを起動した後、ブラウザでlocalhost:3000というURLにアクセスすると、Railsの初期画面が表示されるようになります。

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

Rubyのコードで「ハッシュっぽいのに括弧は配列?」と一瞬混乱

Rubyでこんなコード片を見かけて混乱した。

obj = [ a: "b", x: "y" ]

括弧は配列のものだが、中身はハッシュの書き方になっている。

業務で実際に動いているコードなので、文法エラーでないことは確か。

答え

irbなどで実行してみればすぐにわかる。同等のコードは以下の通り。

クリックして展開
obj = [{ a: "b", x: "y" }]
#=> [{:a=>"b", :x=>"y"}]

要するに「ハッシュを要素とする配列」。


思い返してみると、メソッド呼び出しの引数の最後ではハッシュの波括弧を省略できていた。配列リテラルの中も、引数と同じような扱いなのだろう。

def mtd(*args)
    p args
end

mtd 1, 2, a: "b", x: "y"
#=> [1, 2, {:a=>"b", :x=>"y"}]

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

FormObjectをうまく使いこなしたい

はじめに

Rails Advent Calendar 2020 16日目の記事です!

業務で毎日、Railsを触れていいコードが書けるように精進しています。

そんな中、今年一扱いに苦戦した(今も苦戦している)FormObjectについて、自分が考えることを共有しておきたいと思います!!

FormObjectとは

そもそもFormObjectについて、「なにそれ????」と思う方もいると思うでの簡単に紹介します!

簡単に言うと、FormObjectは、Railsのデザインパターン設計手法の1つです。

MVC

Railsの基本設計は、MVC(Model, View, Controller)です。

Viewは、ユーザーからの入力を受け付けます。その情報をControllerにわたし、Controllerはその情報を整形・制御します。
そして、Controllerは整形・制御した情報をModelに対して渡してあげます。
Modelは、Controllerから受け取った情報をもとにDBにアクセスし、データの保存・変更・削除を行い、
その結果をControllerViewに返します。
返ってきた結果は、Controllerで整形・制御しViewに表示させたり、制御・整形せずそのまま表示したりします。

これが基本的なMVCの流れですが、、、以下のようなことがあると思います。

  1. 一つのViewで複数のModelの属性を入力してもらい、DBを更新したい。
  2. ModelやDBの制限とは別にView側で制限をかけたい。
  3. ControllerでのViewから受け取った情報の整形が複雑化してしまって、見辛くなった。 ...

これらの実装をMVCだけで行おうとすると、ControllerModelに責務が偏ってしまいます。
これでは、コードが追いづらくなったり、影響範囲が大きくなったりしてしまいます。

そうすると、アプリの保守性が低いことになってしまうので、

新しい機能を追加したり、、、
修正したり、、、
DBの構造を変えたり、、

するときに大変苦労な思いをすることになってしまいます。

それらを防ぐための手法の一つとして、FormObjectという概念が存在します。

メリット・デメリット

FormObjectを使用する一番の目的は、責務を切り離すことだと思います。

Controllerの肥大化の原因のひとつである、Viewからの情報を整形するロジックFormObjectでカプセル化する事で、
Controllerの責務が一つ減ることになります。
これでControllerは、肥大化せずに済みますし、単一責務の状態になるので依存関係が少なくなり、保守性は高くなります。

また、Modelの状態に関係ないロジックをModelにかくと、Modelが肥大化してしまいます。そういったロジックをFormObjectに書くことで、
Modelの責務が一つ減り、肥大化を防ぐことができます。

このように、ControllerModelに偏りがちなロジックをFormObjectにまとめることで、過剰責務にならずに済みます!!

ただ、上記のことはデメリットにもなり得ると思います。

やたら滅多に、FormObjectにロジックを集約させると、今度はFormObjectが肥大化してしまいます
これでは本末転倒です。。。
ControllerModelViewに関するロジックをFromObjectに任せて、責務を切り離すことを目的に使用しているにもかかわらず、
FormObjectの責務がでかくなってしまって、そこの保守性が低くなるのは良くありません。

そのため、使用する前はしっかりとFormObjectの責務を明確にしてから使用しましょう!

FormObjectの責務とは???

FormObjectがなんなのか大体わかってきました。

かなり便利なFromObjectですが、、、いろいろな記事をみて、実際に書いていく中で、ふと思ったことがあります。。。

「ここってFomObjectでやるべき???」
「ControllerやModelのロジックとかぶってない???」
「FormObjectの責務って、、、結局どこまで持てばいいの???」

これらを思ったきっかけを実際のコードを例に説明していきたいと思います。

FormObjectのvalidate

FormObjectを使用するとき、ActiveModel::ModelをincludeしてればModel同様にvalidateを書けることができます。
例えば、記事の登録でタグ情報も入力するフォームがあったとします。

フォームでは、記事のタイトルと内容、タグの名前が必須であった場合、以下のようにかけます。

class ArticlePostForm
  include ActiveModel::Model
  attr_accessor :article_body, :article_title, :tag_name

  validates :article_body, presence: true
  validates :article_title, presence: true
  validates :tag_name, presence: true
end

これでFormの入力に対して制限を設けることができました。ただ、記事とタグのModelにも以下のようなvalidateが設けてありました。

class Article < ApplicationRecord
  validates :title, presence: true, uniqueness: true
  validates :body, presence: true
  validates :is_display, inclusion: [true, false]
end

class Tag < ApplicationRecord
  validates :name, presence: true, uniqueness: true
end

FormObjectでのvalidateとModelでのvalidateでは、意味は違います。
FormObjectでのvalidateでは入力された値に対する制限なのに対して、ModelでのvalidateではDBに保存する際の制限です。

ただ、今回では検証している属性がFormObjectModelでかぶってしまっているので、

FormObjectのvalidateは必要なのか?

という疑問があります。

FormObjectでのsave・update

記事を見ているとよく見かけるものです。
以下のようにFormObjectでActiveRecordの作成を行うsaveメソッドがあります。

class ConractForm
  include ActiveModel::Model
  attr_accessor :first_name, :last_name, :email, :body

  def save
    return false if invalid?

    contact = Contact.new(first_name: first_name, last_name: last_name, email: email, body: body)
    if contact.save
      true
    else
      errors.add(:base, contact.errors.full_messages)
      false
    end
  end
end

Controllerでは、以下のように書けるはずです。

class ContactController < ApplicationController
  def new
    @contact = ContactForm.new
  end

  def create
    @contact = ContactForm.new(set_params)
    if @contact.save
      flash.now = 'お問い合わせを承りました'
      redirect_to :contacts
    else
      flash.alert = '送信に失敗しました'
      render :new
    end
  end

  private

  def set_params
    params.require(:contact).permit(:first_name, :last_name, :email, :body)
  end
end

ここで、FormObjectModelの生成・更新の役割を持っていいのかどうかに疑問を持ちました。
FormObjectは、入力に対してロジックを持たせるのが一般的だと思います(間違っていたらすいません。。。)

ここで僕はFomObjectに対しての責務で、

「Viewの入力に対しての制御のみでModelの生成は別」と「validateをかけているからModelの生成までがいい」

の2通りの考え方できると思ったので、どちらに寄せるべきなのかを迷いました。

FormObjectのエラーハンドリング

Modelの生成において、validateによって発生したエラーをFormObjectで取り扱うかどうかがどうなのか疑問に思いました。

ModelのエラーとFormObjectのエラーは分けたほうがいいのか、一緒にすべきなのかが一番悩ましいところです。。。

(ここは僕もまだ考えがまとまっていないので、知見が増えたらまた共有します。)

def save
    # ここはFormObjectの入力のエラー
    return false if invalid?

    # ここはModel生成時のエラー
    contact = Contact.new(first_name: first_name, last_name: last_name, email: email, body: body)
    if contact.save
      true
    else
      errors.add(:base, contact.errors.full_messages)
      false
    end
  end

まとめ

FormObjectに対しての責務は、その都度考え、その責務以上のことをさせないように実装することが大事だと思っています。

ただ、僕自身もFormObjectを完璧に把握・理解しているわけではなく、他にも設計思想があるので、そこらへんと組み合わせた
FormObjectの考えもあるのかなと感じています。

参考サイト

Form Object という選択肢を検討してみる

【Rails】FormObjectを使ってほしい

Railsのデザインパターン: Formオブジェクト

【Rails】Form Objectを使ってModelに依存しないFormを作成する

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

ジャンクションを使ってWindowsのRubyのバージョンを雑に切り替える

Windowsの複数のRubyのバージョンを検証したいときに、ジャンクションにパスを通すことでうまくいったので投稿。

例えば、Ruby2.6(x86) と Ruby2.7(x64) の検証をしたいとする。
まず、RubyInstaller から Ruby2.6(x86) のインストーラーをダウンロードし、インストール先に C:\Rubies\Ruby26 を選択してインストールする。この時パスは通さない。
同様に 2.7(x64)を C:\Rubies\Ruby27-x64 にインストール。

C:\Rubies に以下のバッチファイルを作成する。

use_26.bat
rmdir RubyCurrent
mklink /J RubyCurrent Ruby26
use_27_x64.bat
rmdir RubyCurrent
mklink /J RubyCurrent Ruby27-x64

use_27_x64.bat をダブルクリックして実行すると RubyCurrent という名前のジャンクションが作成される。
最後に、PATHに C:\Rubies\RubyCurrent\bin を追加する。

これにより、バッチファイルを実行することで、Rubyのバージョンを好きに切り替えることができるようになった。
Ruby2.7(x64) を使いたいときは use_27_x64.bat をダブルクリックし、 Ruby2.6(x86) を使いたいときは use_26.bat をダブルクリックすればよい。

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

rails:db:migrateを実行すると「Directly inheriting from ActiveRecord::Migration is not supported.」と出た

<この記事について>
自作のアプリ製作中にカラムを追加する流れで「rails:db:migrate」を実行すると「Directly inheriting from ActiveRecord::Migration is not supported.」と表示されエラーに。
初歩的なミスでしたが備忘録として投稿!

[環境]
・Ruby 2.6.5,
・Rails 6.0.0
・macOS

rake aborted!
StandardError: An error has occurred, all later migrations canceled:

Directly inheriting from ActiveRecord::Migration is not supported. Please specify the Rails release the migration was written for:

調べてみると、原因はマイグレーションファイルがrailsのバージョンに対応していないことが原因のようでした。。

対象のマイグレーションファイルの1行目末尾にrailsのバージョンを追加。

class AddAttachmentImageToPosts < ActiveRecord::Migration[6.0] #←[6.0]を追加
  def self.up
    change_table :posts do |t|
      t.attachment :image
    end
  end

  def self.down
    remove_attachment :posts, :image
  end
end

その後、再度以下を実行すると解決しました。

$ rails db:migarate

<最後に>
実装してエラーが出る、思うような結果が得られないことは多々あると思います。
そんな時は冷静な気持ちでエラー文と丁寧に向き合うようにしましょう!

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

【個人アプリ作成】情報管理アプリ制作16日目

昨日は仕事に100%割いたので悔いなし。今日の実績。

今日やったこと

・ 顧客データ登録画面の作成
・ 中間テーブルの保存の復習
・ JavaScriptで装飾(ボタンをクリックするとメニューが出る)

中間テーブルの保存

createアクションに対して、{:users_id []}の記述で自動で連携・保存される(ストロングパラメーターは必須だけど)

アソシエーションを組んでいるためのなのか、保存される仕組みは完璧には分からないかったが、仕組みは理解。明日から実装していく。

JavaScriptが全然うまくいかない

Id属性の情報は取れているのに、Chromeでデバックするとすぐリロードされて消える、、、、

function clickbutton(){
  const pushPlusButton = document.getElementById("index-plus")

  pushPlusButton.addEventListener('mouseover', function(){
    console.log("aaaa")
  })
}

window.addEventListener('load', clickbutton)

console.logの記述が一瞬だけ出てすぐ消える、、、
turbolinksの記述は消した方がいいはずなんだけど、ダメだったのか、、、

実装苦戦中です。。。。

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

エラーメッセージの日本語化(rails)

rails-i18n(Gem)

 日本語に対応できるようになるGemのこと。

localeファイル

 エラーメッセージを全て日本語にするために必要なファイルで、さまざまな言語に対応できるファイルのこと。この中に日本語化用のファイルを作成することで、英語を日本語に翻訳できる。

実装方法

①日本語の言語設定

config/application.rb
require_relative 'boot'

require 'rails/all'

Bundler.require(*Rails.groups)

module Asakatu5757
  class Application < Rails::Application
    config.load_defaults 6.0
    config.i18n.default_locale = :ja

    config.time_zone = 'Tokyo'
  end
end

 今回追加した記述は「config.i18n.default_locale = :ja」で、標準言語の日本語にすることができる。

②rails-i18nの導入

Gemfile
gem 'rails-i18n'
ターミナル
% bundle install

③locales内に日本語化用のファイルを作成し、編集する。

例)Userモデルのnicknameカラムを日本語化する場合。

config/locales/ja.yml
ja:
 activerecord:
   attributes:
     user:
       nickname: ニックネーム
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsでオブジェクト配列をハッシュ化するあれこれ(injectとかindex_byとか)

例えばUserモデルがあり、nameやらemailやらageやらを各オブジェクトが持っていたとする。

ある特定のageの人たちだけに施したい処理があって、どの年齢が対象となるかは既に配列のグループがあったとする。(若干例え悪いけどそんな感じだった)

最初、以下の様な処理を書いていた。

sample.rb
# 対象としたい年齢群。
targets = [1,4,5,6,10,23]

targets.each do |target|
  User.all.each do |user|
    # 一致するものがあれば次の処理へ
    next if target != user.age
    user.nextaction
  end
end

これだとeachで二重に回すことになるので、もっと簡単にできないものかと考えた時に、ageをキーとするハッシュつくればいいのでは??となった。

そして最初、injectでハッシュを生成したのがこちら。

sample.rb
# 対象としたい年齢群。
targets = [1,4,5,6,10,23]
# ageをキーとしてUser.allをハッシュ化
users_sort_age = User.all.inject({}) {|hash,user| hash[user.age] = user; hash }
targets.each do |target|
  # targetをキーとした(=targetと同じ値のageが存在した)場合次の処理に飛ばす
  next if users_sort_age[target].blank?
  user.nextaction
end

これでもできるんだけど、index_byを使うともっとみやすくなりました。

sample.rb
# 対象としたい年齢群。
targets = [1,4,5,6,10,23]
# ageをキーとしてUser.allをハッシュ化をなんとこれだけで表している。
users_sort_age = User.all.index_by(&:age)
targets.each do |target|
  next if users_sort_age[target].blank?
  user.nextaction
end

非常にみやすくなった。
ただしindex_byはActiveRecordでサポートされているものなので、デフォルトのRubyではinjectionを使いましょうという話。

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

API Gateway と Lambda と Dynamo

はじめに

前回のものに API Gateway を繋いでみます。こんな感じがゴール

image.png

API Gateway

基本

GUI がかなり直感的でなので頭使わずに使える感じです

  1. エンドポイントのリソース /project 作って
  2. リソースにメソッド GET PUT POST DELETE 作って
  3. メソッド毎にLambda のコード project にマッピングするだけ

image.png

忘れがちなのが、APIの変更は「デプロイ」を押してデプロイしないといけないこと。これでとりあえず動くはず

image.png

APIキー

APIキーを使うときはちょっと面倒

  1. リソースの各メソッド毎にAPIキーでの認証を必須にする
  2. 「使用料プラン」を作成する(ここでAPIに対するレートリミットなどを設定)
  3. 「ステージ」を作成する(開発用とか商用とかそういう意味のステージのことで、ここでは dev_1 とした)
  4. 「APIキー」「使用料プラン」「ステージ」を関連づける

image.png

変更の反映に数分かかることもあるようですが、これでAPIキーを付けてコールできるはず

curl -s -X GET -H "x-api-key: your-api-key-here" https://<project-id>.execute-api.us-east-2.amazonaws.com/dev_1/projects | jq

Lambda 統合プロキシ

「Lambda 統合プロキシ」にチェックを入れるだけでHTTPリクエストに含まれる様々なパラメターがそのまま Lambda に渡される。

image.png

例えば、HTTPメソッドが POST なのか GET なのかが引き渡されるので Lambda 側でわかる.

こんな風に読んだ時に

curl -s -X GET -H "x-api-key:your-api-key-here" https://<project-id>.execute-api.us-east-2.amazonaws.com/dev_1/projects?say=ciao -d '{"say": "hello"}'

Lambda に event として渡されるパラメータはこんな感じ

event
{
  "resource": "/projects",
  "path": "/projects",
  "httpMethod": "GET",
  "headers": {
    "accept": "*/*",
    "content-type": "application/x-www-form-urlencoded",
    "Host": "<project-id>.execute-api.us-east-2.amazonaws.com",
    "User-Agent": "curl/7.64.1",
    "X-Amzn-Trace-Id": "Root=1-5fd44390-5f78ee5a289e7e3a0355bec2",
    "x-api-key": "your-api-key",
    "X-Forwarded-For": "***.**.**.**",
    "X-Forwarded-Port": "443",
    "X-Forwarded-Proto": "https"
  },
  "multiValueHeaders": {
    "accept": [
      "*/*"
    ],
    "content-type": [
      "application/x-www-form-urlencoded"
    ],
    "Host": [
      "<project-id>.execute-api.us-east-2.amazonaws.com"
    ],
    "User-Agent": [
      "curl/7.64.1"
    ],
    "X-Amzn-Trace-Id": [
      "Root=1-5fd44390-5f78ee5a289e7e3a0355bec2"
    ],
    "x-api-key": [
      "<your-api-key>"
    ],
    "X-Forwarded-For": [
      "***.**.**.**"
    ],
    "X-Forwarded-Port": [
      "443"
    ],
    "X-Forwarded-Proto": [
      "https"
    ]
  },
  "queryStringParameters": {
    "say": "ciao"
  },
  "multiValueQueryStringParameters": {
    "say": [
      "ciao"
    ]
  },
  "pathParameters": null,
  "stageVariables": null,
  "requestContext": {
    "resourceId": "<resource-id>",
    "resourcePath": "/projects",
    "httpMethod": "GET",
    "extendedRequestId": "Xa9-iG9kCYcFU8Q=",
    "requestTime": "12/Dec/2020:04:14:08 +0000",
    "path": "/dev_1/projects",
    "accountId": "<account-id>",
    "protocol": "HTTP/1.1",
    "stage": "dev_1",
    "domainPrefix": "<project-id>",
    "requestTimeEpoch": 1607746448145,
    "requestId": "06499aaf-9168-49d7-b4da-2a0a3ad5d4ad",
    "identity": {
      "cognitoIdentityPoolId": null,
      "cognitoIdentityId": null,
      "apiKey": "<your-api-key>",
      "principalOrgId": null,
      "cognitoAuthenticationType": null,
      "userArn": null,
      "apiKeyId": "<api-key-id>",
      "userAgent": "curl/7.64.1",
      "accountId": null,
      "caller": null,
      "sourceIp": "***.**.**.**",
      "accessKey": null,
      "cognitoAuthenticationProvider": null,
      "user": null
    }, 
    "domainName": "<project-id>.execute-api.us-east-2.amazonaws.com",
    "apiId": "<api-id>"
  },
  "body": "{\"say\": \"hello\"}",
  "isBase64Encoded": false
}

クライアントからのデータ bodyevent['body'] でアクセスできるので Lambda 側の CRUD するコードを前回から少し変えて、こんな感じにしときます

require 'json'
require 'aws-sdk-dynamodb'

def add_project(table, body)
  table.put_item({ item: body })  
end

def delete_project(table, body)
  params = { table_name: table, key: { 'project-id': body['project-id'] } }
  begin
    table.delete_item(params)
  rescue Aws::DynamoDB::Errors::ServiceError => error
    puts error.message
  end
end

def update_project(table, body)
  params = {
    table_name: table,
    key: { 'project-id': body['project-id'] },
    attribute_updates: {
      name: {
        value: body['name'],
        action: "PUT"
      }
    }
  }
  table.update_item(params)  
end

def list_project(table)
  scan_output = table.scan({ limit: 50, select: "ALL_ATTRIBUTES" })

  scan_output.items.each do |item|
    keys = item.keys
    keys.each do |k|
      puts "#{k}: #{item[k]}"
    end
  end
end

def lambda_handler(event:, context:)

  http_method = event['httpMethod'] # このへんを追加
  body = JSON.parse(event['body'])  # このへんを追加

  dynamodb = Aws::DynamoDB::Resource.new(region: 'us-east-2')
  table = dynamodb.table('project')

  case http_method
    when 'GET'    then list_project(table)
    when 'PUT'    then update_project(table, body)
    when 'POST'   then add_project(table, body)
    when 'DELETE' then delete_project(table, body)
    else 0
  end

  { statusCode: 200, body: JSON.generate(list_project(table)) }

end

例えばこんな感じでリクエスト投げると、HTTPのメソッドをみて CRUD 処理をする。ちなみに jq しとくと見やすいのでおすすめ

curl -s -X POST -d '{ "project-id":101, "name":"Cycling", "is-default":false }' -H "x-api-key: <your-api-key-here>" https://<project-id>.execute-api.us-east-2.amazonaws.com/dev_1/projects | jq

うむ、動いたぞ

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

#5 Ruby始めました 「たのしいRuby 第6版」第2部基礎を学ぶ 五章 条件判断 ハノイ作成

本日から条件判断=プログラミングの要域に入ります。
一通り、教本通りきましたので、若干独自要素を加えて、プログラミング練習にハノイを作ってみましょうw
アルゴリズムとは条件判断の教科書みたいなものです。あのアルゴリズムアプリ通りに条件判断を作っていきます。

オブジェクトを条件判断のみでプリントするという流れで一つのプログラムを作ってみます。
ごくごく僅かな命令文だけで構成したシンプルな構成でいきます。

#ハノイの塔
#条件判断プログラム練習




コメント

アルゴリズムです。
ここが肝の場所ですからじっくりと学んでいきたいと思いますが、ここだけをちゃんとやると多分一生遊べる場所ですが、まあサラッとw

アルゴリズムは昔の本では小難しいことばっかり書いてあって、さっぱり解らなかったですがw今は本当に素晴らしくわかりやすものがたくさんあります。
コンピューターとアルゴリズム書籍で一番最初に習うのがハノイの塔ですが、ハノイを応用したプログラムの使い方、プログラムの書き方は書いてませんw昔のアルゴリズムとはそういうものでしたwなのでさっぱり分かりませんでしたwどう使うの?みたいなw

ハノイの塔(パズル)

ハノイはアルゴリズム学習で最初に学びますが、それは同時にプログラミング学習の基礎学習になります。基礎練習になりますので、ハノイを作ってみましょうというのが答えですねw

プルグラム教育のツールとして簡単なゲームを作るのがハノイの塔の狙いで、別にこれを使って凄いことが出来るみたいなものでは無いのでしょうねw

アルゴリズムアプリ

アルゴリズム図鑑 370円
https://apps.apple.com/jp/app/%E3%82%A2%E3%83%AB%E3%82%B4%E3%83%AA%E3%82%BA%E3%83%A0%E5%9B%B3%E9%91%91/id1047532631

これを見ているだけで楽しいwアルゴリズムの本なんかより全然理解度がありますw
FortranやっていたNECに行っていたおじさんが言ってましたが、凄腕はアルゴリズムを熟知しているので、処理が速いとのことでした。
プログラマーの腕はやっぱりアルゴリズムだそうです。まあそうですよね。

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

#5 Ruby始めました 「たのしいRuby 第6版」第2部基礎を学ぶ 五章 条件判断

コメント

アルゴリズムです。
ここが肝の場所ですからじっくりと学んでいきたいと思いますが、ここだけをちゃんとやると多分一生遊べる場所ですが、まあサラッとw

アルゴリズムは昔の本では小難しいことばっかり書いてあって、さっぱり解らなかったですがw今は本当に素晴らしくわかりやすものがたくさんあります。
コンピューターとアルゴリズム書籍で一番最初に習うのがハノイの塔ですが、ハノイを応用したプログラムの使い方は書いてませんw昔のアルゴリズムとはそういうものでしたwなのでさっぱり分かりませんでしたwどう使うの?みたいなw

アルゴリズム図鑑 370円
https://apps.apple.com/jp/app/%E3%82%A2%E3%83%AB%E3%82%B4%E3%83%AA%E3%82%BA%E3%83%A0%E5%9B%B3%E9%91%91/id1047532631

これを見ているだけで楽しいwアルゴリズムの本なんかより全然理解度がありますw
FortranやっていたNECに行っていたおじさんが言ってましたが、凄腕はアルゴリズムを熟知しているので、処理が速いとのことでした。
プログラマーの腕はやっぱりアルゴリズムだそうです。まあそうですよね。

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

Rails + Grape + Grape SwaggerでちゃんとOpenAPI/Swaggerドキュメンテーションしようとしたらハマったこと

この記事はCBcloud Advent Calendar 2020 の12日目の記事です。

Rails + Grape + Grape Swaggerで、実際に業務でOpenAPIドキュメントを生成してメンバーに共有しようとしたときにハマったことについて紹介します。

まずはおさらい: OpenAPI、Swaggerってなんだっけ?

OpenAPI

OpenAPIのリポジトリには下記のように記述されています。
https://github.com/OAI/OpenAPI-Specification

The OpenAPI Specification (OAS) defines a standard, programming language-agnostic interface description for HTTP APIs, which allows both humans and computers to discover and understand the capabilities of a service without requiring access to source code, additional documentation, or inspection of network traffic.

つまりOpenAPIは、プログラミング言語に依存しないHTTP APIのインタフェースを記述するための仕様です。
その仕様に従い記述されたドキュメントは、人が読んでも理解できるし、それを解析していい感じのUIを提供することもできるようになっている、ということです。

Swagger

Swaggerのページには下記のように記述されています。
https://swagger.io/about/

Swagger is a powerful yet easy-to-use suite of API developer tools for teams and individuals, enabling development across the entire API lifecycle, from design and documentation, to test and deployment.
〜略〜
Swagger started out as a simple, open source specification for designing RESTful APIs in 2010. Open source tooling like the Swagger UI, Swagger Editor and the Swagger Codegen were also developed to better implement and visualize APIs defined in the specification.
〜略〜
In 2015, the Swagger project was acquired by SmartBear Software. The Swagger Specification was donated to the Linux foundation and renamed the OpenAPI

つまりSwaggerはAPI開発者のためのツール群を提供するプロジェクトということですね。そして、SwaggerのAPI定義のための仕様がLinux foundationに寄付され、OpenAPIに改名されたとのことです。

この記事が対象とする範囲

上記を踏まえ、この記事でやろうとしていること(そしてハマったこと)は、Rails + Grapeの構成で開発しているプロジェクトで、Grape SwaggerのDSLを使ってOpenAPI/Swagger仕様に沿ったJSONを生成することです。
また、その生成したJSONを読み込んで見やすくしてくれるツールで見たときに、意図した構成として表示されることを目指します(そしてハマります)

使用したgem

ドキュメント生成にあたり使用したgemは下記の通りです。

gem 'grape' # RESTful APIを開発するためのDSLを備えたフレームワーク
gem 'grape-swagger' # Grape APIからのドキュメント生成
gem 'grape-entity' # Grapeフレームワークにレスポンス整形のツールを加える
gem 'grape-swagger-entity' # grape-entityからのドキュメント生成

設定と使い方は各リポジトリをご参照のこと。
https://github.com/ruby-grape/grape
https://github.com/ruby-grape/grape-swagger
https://github.com/ruby-grape/grape-entity
https://github.com/ruby-grape/grape-swagger-entity

ハマったこと

1. descブロックのparamsと、descと同階層のparamsは別物

descと同階層のparamsではバリデーションをしてくれる

Grapeを使っているとき、下記のような書き方をすることがあると思います。
これはuser_nameは必須パラメータであることを指しており、実際にリクエストボディを空にしてリクエストを投げると、400エラーになってuser_name is missingとして返してくれます。

class Users < Grape::API
  resources :params_in_same_layer do
    desc 'descと同じ階層にparamsを書いた場合'
    params do
      requires :user_name, type: String, documentation: { desc: 'ユーザ名', type: 'string' }
      optional :address, type: String, documentation: { desc: '届け先情報', type: 'string' }
    end
    post do
      present hoge: 'fuga'
    end
  end
end

descブロック内のparamsではバリデーションはしてくれない

次にGrape Swaggerでドキュメンテーションしていこうとすると、下記のような記述方法が出てきます。
これにも先程と同様にリクエストボディ無しでリクエストを投げると、今度は400エラーにはならず201が返ってきます。

class Users < Grape::API
  resources :params_whitin_desc_block do
    desc 'descブロック内にのみparamsを書いた場合' do
      params SimpleUserParamsEntity.documentation
    end
    post do
      present hoge: 'fuga'
    end
  end
end

class SimpleUserParamsEntity < Grape::Entity
  expose :user_name, documentation: { desc: 'ユーザ名(エンティティで定義)', type: 'string', required: true }
  expose :address, documentation: { desc: '住所(エンティティで定義)', type: 'string' }
end

ただしUIで見ると見た目はほぼ同じ

上記をSwagger UIで見たときにどのような差があるか比べてみます。
どちらもユーザ名が必須であることは表現できています。しかし2つ目のエンドポイントでは実際には必須になっていないので注意が必要です。

image.png
image.png

両方にparamsを書いたら、キーが一致していればdesc内のドキュメンテーションが優先されるが、一致していないものはそれぞれ出力される

それでは両方に書くとどうなるでしょうか。さらにそれぞれでしか定義されていない、age、blood_typeというキーを追加してみました。

class Users < Grape::API
  resources :params_in_same_layer_and_desc_block do
    desc 'descブロック内と、descの同階層の両方にparamsを書いた場合' do
      params SimpleUserParamsEntity.documentation
    end
    params do
      requires :user_name, type: String, documentation: { desc: 'ユーザ名', type: 'string' }
      optional :address, type: String, documentation: { desc: '届け先情報', type: 'string' }
      optional :age, type: Integer, documentation: { desc: '年齢', type: 'string' }
    end
    post do
      present hoge: 'fuga'
    end
  end
end

class SimpleUserParamsEntity < Grape::Entity
  expose :user_name, documentation: { desc: 'ユーザ名(エンティティで定義)', type: 'string', required: true }
  expose :address, documentation: { desc: '住所(エンティティで定義)', type: 'string' }
  expose :blood_type, documentation: { desc: '血液型(エンティティで定義)', type: 'string' }
end

Swagger UIで見るとこうなります。キーが一致している場合は、ドキュメンテーションはdesc内のものが優先され、一致していない場合は、それぞれ出力されるようです。

image.png

2. 複雑な構成のリクエストをUIでちゃんと表示しようと頑張ったが、結局は使用するUIの挙動次第

複雑なパラメータのパターンでUIがどうなるか確かめてみます。またdescブロック内に記述した場合と同階層に書いたエンドポイントを用意します。

class Users < Grape::API
  resources :complex_params_in_same_layer do
    desc 'descと同じ階層にparamsを書いた場合'
    params do
      requires :user_name, type: Integer, documentation: { desc: 'ユーザ名', type: 'string' }
      optional :addresses, type: Array[JSON], documentation: { desc: '届け先情報', type: 'array', collectionFormat: 'multi' } do 
        requires :name, type: String, documentation: { desc: '届け先名', type: 'string' }
        requires :address, type: String, documentation: { desc: '届け先住所', type: 'string' }
        requires :tags, type: Array[JSON], documentation: { desc: 'タグ', type: 'array', collectionFormat: 'multi' } do
          optional :name, type: String, documentation: { desc: 'タグ名', type: 'string'}
        end
      end
    end
    post do
      present hoge: 'fuga'
    end
  end

  resources :complex_params_in_desc_block do
    desc 'descブロック内にのみparamsを書いた場合' do
      params ComplexUserParamsEntity.documentation
    end
    post do
      present hoge: 'fuga'
    end
  end
end

class ComplexUserParamsEntity < Grape::Entity
  class TagEntity < Grape::Entity
    expose :name, documentation: { desc: 'タグ名(エンティティで定義)', type: 'string' }
  end

  class AddressEntity < Grape::Entity
    expose :name, documentation: { desc: '届け先名(エンティティで定義)', type: 'string' }
    expose :address, documentation: { desc: '届け先住所(エンティティで定義)', type: 'string' }
    expose :tags, documentation: { desc: 'タグ(エンティティで定義)', type: 'array', is_array: true }, using: TagEntity
  end

  expose :user_name, documentation: { desc: 'ユーザ名(エンティティで定義)', type: 'string' }
  expose :addresses, documentation: { desc: '届け先情報(エンティティで定義)', type: 'array', is_array: true }, using: AddressEntity
end

Swagger UIで確認してみます。一方は配列内が表示されず、もう一方は表示されるけど階層構造がわかりにくい表示です。

image.png
image.png

見やすくできないかあがいてみます。全パラメータにparam_type: 'body'を追加してみます。

class Users < Grape::API
  resources :complex_params_in_same_layer do
    desc 'descと同じ階層にparamsを書いた場合'
    params do
      requires :user_name, type: Integer, documentation: { desc: 'ユーザ名', type: 'string', param_type: 'body' }
# 以下略

再びSwagger UIで確認します。すると、Entityで定義した方は相変わらず階層構造が失われています。Entityの書き方が悪いのかもしれません。
そしてもう一方は、階層構造がわかりやすくなりましたが、説明が反映されていません。

image.png
image.png

そこで、ふと他のOpenAPIを解釈できるUIを試してみます。使うのはこちら。
https://github.com/Redocly/redoc

Entityの方は相変わらずですが、説明がちょっとマシになっています。
そして同階層の方はなんと完全に思い通りの表示になっています!!(配列のキーにつけた「届け先情報」という説明もちゃんと表示されています)

image.png

image.png

このことからわかるのは、実際の表示は使用するUIツールによるので使用するツールでちゃんと表示されるようになることを確認しましょう、ということです。

3. exampleを出力するにはワークアラウンドが必要

exampleを指定しても出力されないのでなぜだろうと思ったら下記のissuesがありました。
現時点でOpenなので解決されるまではissues内に投稿されているワークアラウンドを入れればできるようです。
https://github.com/ruby-grape/grape-swagger/issues/762

おわりに

リクエストパラメータのところのエンティティの使い方なんかはまだちゃんと理解できていないので間違い等あったらご指摘いただければ助かります。

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

nilガードについて理解してみた

Rubyの勉強をしているなかでnilガードというものがでてきたので勉強の記録用として記事にしてみました。

認識違いがあった場合などは是非ご指摘下さい。

nilガード

Rubyにはnilになってしまう状態を防ぐためにnilガードという書き方が存在します。簡単に説明すると仮に変数がnilだった場合には値を入れるということです。

ということで分かりやすく下記に例を記載したいと思います。

# 例1
> number = nil
> number ||= 6
> puts number
=> 6

# 例2
> number = 5
> number ||= 6
> puts number
=> 5

初めにnilガードを実装するにパイプ演算子||と=を組み合わせます。
例1ではnumberがnilなら6を代入しています。しかし例2ではnumberがnilではないので代入が行われていません。ここでnilガードというものはnilかfalseでないと代入が行われないと分かりました。

まとめ

nilガードは変数にnilが入っているかもしれない状況でnilの代わりに何らかのデフォルト値を入れておきたいという場面で、とても便利に利用できます。

参考

・ 現場で使える Ruby on rails 5 速習実践ガイド
https://www.sejuku.net/blog/19044

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

【Ruby】RubyのAPI問題

任意の文字に対してn番目の文字を消し、
その消した文字を出力するメソッドを作りましょう

呼び出し方
missing_char(string, num)

出力例:
missing_char('kitten', 1) → 'itten'
missing_char('kitten', 2) → 'ktten'
missing_char('kitten', 4) → 'kiten'

1・sliceメソッド
sliceメソッドを用いることで、配列や文字列から指定した要素を取り出すことができます。
文字列の要素を指定する際は数字を用い、先頭の文字列は0からカウントされます。

# 文字列を作成
string = "abcdefg"

# 文字列から引数で指定した要素を取得して変数に代入
str = string.slice(2)

# strに代入した文字列を出力
puts str
#=> "c"

# 文字列はもとのまま
puts string
#=> "abcdefg"

2・slice!メソッド
末尾にエクスクラメーションマーク(!)のつくメソッドを破壊的メソッドといいます。これはもとの配列や文字列を変化させるメソッドです。slice!メソッドを使用することで、指定した要素を削除することができます。

string = "abcdefg"
str = string.slice!(2)
puts str
#=> "c"

# "c"が取り除かれている
puts string
#=> "abdefg"



解答

def missing_char(str, n)
  str.slice!(n - 1)
  puts str
end

任意の文字に対してn番目の文字を消すために、文字列自体から取得した値を取り除く、slice!メソッドを使用。
しかしsliceメソッドは文字列自体の形を変化させることはできないため、エクスクラメーション(!)を付けたslice!メソッドに変更している。

このslice!メソッドはmissing_charメソッド内で処理を行い、
missing_charメソッドの引数strには入力した文字列で、nは何番目の文字列を消すのか指示する数字を入れる仕組み。
そして、slice!メソッドにnから1を引いた数を引数にしているが、なぜ1を引くのか?
文字列の順番を指定する際も、配列と同じように先頭の文字列は0からカウントされるため。

【文字列の順番】(人間
   123
   s t r
   012
【添字の順番】(機械

人間から見た文字の順番は1〜
機械から見た文字の順番は0〜
人間→機械への翻訳なので「n - 1」って感じで理解できました。
奥が深い。

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

第10回

Ubuntu-20.04.1 ruby-2.7.1p83

これは講義用のまとめサイトです。見た人は是非LGTMを押しといてください。

やったこと

  • Fibonacci数列

Fibonacci数列

お題

Fibonacci数列を求めるプログラムを作れ
例えば「0」を入力したら数列の初項の0が表示されてほしい

方針

単に

def fib(n)
  if n==0
    return 0
  end
  if n==1
    return 1
  end
end

とか、もうちょっときれいにして

[[0,0],[1,1]].each do |pair|
  puts assert_equal(pair[0], fib(pair[1]))
end

というように、いちいち条件を書いていくのはきりがない
そこで何を入力しようと勝手に計算してくれるようにすることを考える

実際にどうやるか

勝手に計算してくれるようにするのには初項(0番目)とその次の項だけはこちらで教えてあげないといけない
その次以降は、Fibonacci数列の定義によると、n番目の項は(n-1)番目+(n-2)番目ということなので、それをプログラムにしてやる。

def fib(n)
  if n==0
    return 0
  else if n==1
    return 1
  else
    return fib(n-1) + fib(n-2)
  end
end
p fib(6)

という感じになる。

refactoring

だけどこのままだとコードが長いので、もっとぎゅっとまとめましょうということで

def fib(n)
  return 0 if n==0
  return 1 if n==1
  return fib(n-1) + fib(n-2)
end
p fib(6)

となった。
これだけだと、n=6の時はfib(5)とfib(4)の結果を足すので、fib(5)とfib(4)は定義されてないので動かないはず

> 8

…ってあれ!?動くの!?なんで?試しにfib(30)とかにしてもちゃんと値が返ってきた
でもどこにも再帰的に処理するような記述はした覚えがないのでこれに関してはよく分かりません。誰か教えて

テスト数を増やす

いくつもテストするならeach doを使いましょう。

def fib(n)
  return 0 if n==0
  return 1 if n==1
  return fib(n-1) + fib(n-2)
end

[0,1,2,6].each do |pair|
  puts fib(pair)
end

> 0
> 1
> 1
> 8

と、一気に返ってきますね

assert_equalを使って本当にちゃんと計算で来てるか確認する

先週作ったassert_equalの機能を使い、9番目まで本当にちゃんと計算出来てるか確認する

def fib(n)
  return 0 if n==0
  return 1 if n==1
  return fib(n-1) + fib(n-2)
end

require './assert_equal.rb'
[[0,0],[1,1],[2,1],[3,2],[4,3],
 [5,5],[6,8],[7,13],[8,21],[9,34],
].each do |index, expected|
  puts assert_equal(expected,fib(index))
end

indexとexpectedの組み合わせでできた配列をassert_equal関数に渡して判定してもらうというもの

["expected", 0]
["result", 0]
succeeded in assert_equal

["expected", 1]
["result", 1]
succeeded in assert_equal

["expected", 1]
["result", 1]
succeeded in assert_equal

["expected", 2]
["result", 2]
succeeded in assert_equal

["expected", 3]
["result", 3]
succeeded in assert_equal

["expected", 5]
["result", 5]
succeeded in assert_equal

["expected", 8]
["result", 8]
succeeded in assert_equal

["expected", 13]
["result", 13]
succeeded in assert_equal

["expected", 21]
["result", 21]
succeeded in assert_equal

["expected", 34]
["result", 34]
succeeded in assert_equal

オールクリア

参考記事

チャート式ruby-V(Recursive Fibonacci)


  • source ~/MasahiroOba/grad_members_20f/members/MasahiroOba/chapter10.org
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

(解決方法)NoMethodError in Devise::SessionsController#destroy

エラー文

NoMethodError in Devise::SessionsController#destroy
undefined method `remember_created_at=' for #User:0x00007f84757e1ea8 Did you mean? remember_me=

Extracted source (around line #432):
else
        match = matched_attribute_method(method.to_s)
        match ? attribute_missing(match, *args, &block) : super
      end
    end
    ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)

エラー内容の詳細

NoMethodError in Devise::SessionsController#destroy

devise sessionsコントローラーのdestroyアクションが定義されていません。
仮説①deviseがおかしい?

undefined method `remember_created_at=' for #User:0x00007f84757e1ea8 Did you mean? remember_me=

remember_created_at=は定義されていない。remember_me=ではないか?
remember_created_at=はマイグレーションファイルのデフォルトの記載で書いてあったような、、、
仮説②マイグレーションファイルのremember_created_at=をremember_me=へ修正したら解決されるのでは?

解決方法

仮説②がかすっていました。マイグレーションファイルのデフォルトの記述を手違いで消してしまっており、今回のエラーを発生させていました。そのため、マイグレーションファイルのデフォルトの記述を書いてあげたら、エラーは解決されました。
仮説①は、無関係でした。deviseに関するエラー文が出てきても、deviseは完璧と仮定し、その他の要因から疑っていったほうが良いみたいです。

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

Rubyでスマートホームを作ろう!

はじめに

こんにちは!Life is Tech!#1 Advent Calendarの16日目を担当します、Webサービスプログラミングコースメンターのみずです! よろしくお願いします!

この記事ではRubyを使ってスマートホームを作る方法について書きたいと思います!

スマートホームってなんだ?

日本ではまだあまりメジャーになっていない気もしますが、スマートホームとは「IoT」や「AI」を使ってより住みやすくなった家のことです。
スマートスピーカー(Google Home、Amazon Alexa、HomePodなど)も、従来のスピーカーにIoT/AIが備わってより便利になったものとしてスマートホームの一部だと言えるでしょう。

この記事では、スマートフォンやスマートスピーカーを使って家電をコントロールできるようにしてみたいと思います!

どの技術を使おう?(技術選定)

さて、やることは決まりました。今ある家電の制御です。
ではどんな技術を使いましょうか。

スマートスピーカーを使ってコントロールしたいので、以下のような選択肢が出てくると思います。

  • Amazon Echo
  • Google Home
  • Apple HomePod
  • LINE Clova

いずれも家電の制御に対応しています。僕は身の回りにApple製品が多いのでAppleのHomePodで制御出来るようにしてみたいと思います!

HomePodはHomeKitと呼ばれる仕組みを使って家電をコントロールします。このHomeKitは仕組みが公開されていて、誰でも作る事ができます。
Life is Tech ! のWebサービスプログラミングコースではRubyを使ったプログラミングをしています。なので、Rubyを使って実際にHomeKitを実装していきましょうー!

開発環境について

今回この記事ではMacで開発する事を前提に進めています。WindowsやLinuxでも開発できるかとは思いますが、手順が異なると思うので各自で挑戦してみてほしいです。

下準備

ターミナルで以下のコマンドを打って準備しましょう。

$ mkdir <プロジェクト名> # フォルダ作成
$ cd <プロジェクト名> # フォルダに移動
$ bundle init # Gemfileを作るコマンド
$ code . # VS Codeを開くコマンド

さて、これでVS Codeが起動します。(上手く起動しない場合はVS Codeを起動して先程作ったフォルダを開きましょう。フォルダはユーザーフォルダの中にあるはずです!)

今回はruby_homeというライブラリを使用したいのでGemfileに以下を追記しましょう。

gem 'ruby_home'

追記できたらVS Codeのターミナルでbundleコマンドを実行してライブラリをインストールします。

$ bundle

これで開発の準備は完了です!!

試しに照明を作ってみよう

app.rbというファイルを作り、以下のコードを書いてみます。

app.rb
require 'ruby_home'

accessory_information = RubyHome::ServiceFactory.create(:accessory_information)
fan = RubyHome::ServiceFactory.create(:lightbulb)

fan.on.after_update do |status|
  if status
    puts "照明がオンになりました"
  else
    puts "照明がオフになりました"
  end
end

RubyHome.run

ruby app.rbコマンドで実行してみると以下のように表示されるはずです!

$ ruby app.rb
Please enter this code with your HomeKit app on your iOS device to pair with RubyHome

    ┌────────────┐     
    │ 766-75-746 │    
    └────────────┘     

iPhoneのHomeアプリを起動してアクセサリを追加してみましょう。

このように出てくるので「コードがないか、スキャンできません」を押しましょう。

すると、同じWi-Fiにつながっている対応デバイスを見つけてきてくれます。RubyHomeをタップしましょう!

アラートが出てきますが気にせず進めます。

ここからは続けるボタンを繰り返せばOKです!

無事追加出来たら電球マークのRubyHomeをタップしてみましょう!

このようにオンになりましたか?ターミナルを見てみるとこのようにオンオフが表示されているはずです。

$ ruby app.rb
Please enter this code with your HomeKit app on your iOS device to pair with RubyHome

    ┌────────────┐     
    │ 766-75-746 │    
    └────────────┘     

照明がオンになりました
照明がオフになりました
照明がオンになりました
照明がオフになりました

コード説明

コメントに説明を書いたので読んでみてください。

require 'ruby_home' # ライブラリ読み込み

accessory_information = RubyHome::ServiceFactory.create(:accessory_information) # アクセサリだよ!と言う為に必要な機能を追加
fan = RubyHome::ServiceFactory.create(:lightbulb) # 照明機能を追加

fan.on.after_update do |status| # 照明のオンオフが変更された時
  if status # onだったらTrue、offだったらFalse
    puts "照明がオンになりました"
  else
    puts "照明がオフになりました"
  end
end

RubyHome.run # 起動

このようにかなり短いコードで実現する事が出来ます!
この中のif文の中に実際に照明を制御するコードを書けばスマートホームが作れるわけです!

実際の照明と連携してみる

さて、ここからは照明を制御していきましょう。ごく一部APIが使えるような照明もあるようですが、うちの家の照明は至って普通の照明なので赤外線でコントロールすることにします。

というわけでこんな感じのモジュールを作りました。ESP32というチップ(の開発ボード)をベースにした赤外線信号を発信出来るデバイスです。USBで通信しても良かったのですが、部屋の好きなところに置きたかったのでWi-Fiに接続し、API経由で赤外線を発信できるようにしてみました。

基本的には以下のようなコードになっています。赤外線によるコントロールは機器によって大きく方法が変わるので各自で調べて実装することをおすすめします!
https://gist.github.com/mizucoffee/912bb2032e21568821b8ddedda13d99d

app.rb
require 'ruby_home'
require 'net/http'
require 'uri'

on = "p344A9034A4"
off = "p344A90F464"

def send_ir(code) # APIを呼び出すコード
  uri = URI.parse('http://192.168.101.200/send?data=' + code)
  req = Net::HTTP::Post.new(uri)
  http = Net::HTTP.new(uri.host, uri.port)
  http.request(req)
end

accessory_information = RubyHome::ServiceFactory.create(:accessory_information)
fan = RubyHome::ServiceFactory.create(:lightbulb)

fan.on.after_update do |status|
  if status
    puts "照明がオンになりました"
    send_ir on # オンにする信号を送るコード
  else
    puts "照明がオフになりました"
    send_ir off # オンにする信号を送るコード
  end
end

RubyHome.run

さて、準備は出来ました!実際に動かしてみましょう!

IMG_0625.gif

こんな感じで動きました!!

おわりに

この記事ではRubyを使ってHomeKitに対応したアプリを実際に開発し、照明をコントロールしてみました。結構簡単だったのではないでしょうか!
対応したデバイスを購入してスマートホームを作るのも良いですが、自分の力で作り上げてみるのもオツなものです。

ここからは自分の話になりますが、僕は普段「身近な面倒を快適に!」というモットーで開発をしています。
これには自分はもちろん、家族や友達、それこそメンターやメンバーのみんなも含まれています!
今回のスマートホームは主に自分用ですが、家族に一緒に使ってもらうことも出来るし、もしかしたらオフィスにだって導入出来るかもしれませんね。

いままで、家族の介護用にベッドから降りたことを通知するセンサーを作ったり、Twitterのフォロー・フォロワーをバックアップするサービスを作ったりしてきたのですが、ある程度開発する力があればこうやって周りを幸せにすることが出来たりします!

人それぞれ開発をする目的は違うと思いますが、ぜひぜひ自分や周りの人が嬉しくなるような開発にもチャレンジしてみてほしいです!

最後まで読んでいただきまして、ありがとうございました!
明日はつっちー(@taznica)が何か書いてくれるようです!お楽しみにー!!

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

インストール後のパス設定。みんなどうしてる?

みなさん、こんにちは。本日のお悩み相談の時間です。

前書き

「見た目は JavaScript、頭脳(中身)は Ruby、(安定感は AC/DC)」 でおなじみのスクリプト言語 Kinx。詳しくは Web(下記)で。

実は最近 バージョン 0.17.0 を(プレ)リリース しまして、そこで初めてインストーラを付けてみました。NSIS を使ってビルドしています。

ところが、今現在はインストール後に実行ファイルへのパスを環境変数 Path に追加 していません。手動でパスを通してください的な。

元々環境変数 Path をイジるとか おっかないなー、というのもあったのですが、どうも NSIS で実現しようとすると問題がありそうなのですね(この辺とか)。ただ、やっぱりパスを自分で通せ、というのは心苦しいので次版では付けたいところ。

そこで、皆さんはインストーラーで環境変数 Path へのパスの追加/削除はどうしてますか?というのが本日のクエスチョンです。

色々調べたのですが、あんまりいいアイデアが見つからずで。一応、以下のような基準で探してました。

  • シンプルなソースコード
  • 使いやすい機能
  • 便利なライセンス

全部満たすものはなかなか見つからず。ライセンスが書いてないものとか、GPL とか1

結論

そこでだ、若旦那!

簡単なプログラムを作成しました。こちらをご覧ください。

これは...

  • シンプルなソースコード... 1つのファイル だけで単機能ツールとして実現。
  • 使いやすい機能... 単機能ツールなので 使い方も単純
  • 便利なライセンス... ザ・MIT!(私がそう設定したのですが)

ですが...

このプログラムはシステム環境を変更するので、やはりとても 怖い のですよ。ですから、たくさんの人にソースコードをチェック してもらいたく、問題があれば修正 したいのです。

ということで、ソースコードを色々な人に見て貰えると嬉しいです。問題見つかれば大変感謝するでしょう(それ以上のことは力不足で何もして差し上げられませんが)。もしくは、この問題(パスの追加/削除)に対するより良い解決策があれば教えてください。

ではまた。

P.S.
もし、これ (https://github.com/Kray-G/addpath) 自体気に入ってくれるようでしたら、★ください。待ってます!


  1. 念のため、GPL が悪いものとは言ってませんので...。自分のが MIT なので採用しづらいなと。 

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

railsでグラフ機能を導入

はじめに

今回学習記録をとるアプリ開発をしていたところ学習状況をグラフ化したい!
と思い比較的簡単に実装ができたので備忘録として残します。
筆者はrails6を使っての実装となります。
初学者のため間違え等ありましたらご指摘頂けると幸いです!

手順1 gem chartkickを導入

グラフ機能を簡単に実装するにはgemのchartkickを使います。
gemファイルに以下を追加しbundleinstallします。
groupdateはDBから様々な情報をグループ化して取り出せる便利なメソッドがあるので導入してますが、chartkickを使うだけであればgem "chartkick"のみで問題ありません。

gem "chartkick"
gem "groupdate"

rails6の場合は以下コマンドを実行

yarn add chartkick chart.js

手順2 jsファイルの読み込み

chartkickではjsを使って実装するためjsファイルをapp/javascript/packs/application.jsに記述します。
rails6の場合

require("chartkick")
require("chart.js")

rails5の場合

//= require chartkick
//= require Chart.bundle

手順3 htmlにグラフを挿入

ここまで行けばあとは公式ページにあるサンプルコードをhtmlに記述するだけで簡単にグラフを作成する事ができます。
私の場合このように使いました。
<%= column_chart @tweets.group_by_day_of_week(:created_at, format: "%a").count %>
https://gyazo.com/4c24263ba93c7e3bc2ea6974bfb0143f
ここでgroup_by_day_of_weekはgemのgroupdateを導入していると使えるメソッドです。
今回使ったのは棒グラフですが他にも折れ線や円グラフも簡単に実装できるのでchartkickの公式サイト見て見てください!!

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

Rubyでlsコマンド実装

はじめに

こんにちは!
DMM WEBCAMP Advent Calendar 2020の24日目を担当するメンターの@shinoooooです。

クリスマスまであと1日ですね!
皆様は、どうお過ごしになられるでしょうか?
人それぞれ予定は異なるとは思いますが、まずは貴重なクリスマスイブの時間を割いてこの記事を読んでいただいていることに、感謝申し上げます。
ありがとうございます!

この記事では、Rubyで実装したlsコマンドの解説及び、実装を通して得られた知見を書きました。
自分自身もRubyを学習中の身ですので、記事の内容やコードに関するアドバイスをいただけましたら幸いです。

開発環境

  • macOS Catalina 10.15.7
  • Ruby 2.7.2
  • zsh 5.8

lsコマンドとは

プログラミングを学習されている方は、一度は実行したことがあるコマンドかと思います。

lsの機能は、Linux関連のドキュメントを和訳しているサイトによると、

ls, dir, vdir - ディレクトリの中身をリスト表示する

と書かれています。

実際に実行すると、

$ ls
hoge huga index.html     #カレントディレクトリ内のファイルが出力される。

このような表示が出てくるかと思います。

今回は、このlsコマンドをRubyで実装し、

$ ruby ls.rb
hoge huga index.html

と出すことが目標です。

仕様

今回実装したls.rbの仕様は、下記の通りです。

  • ls,ls -l,ls -a, ls -laと同等の機能を実装
    • 'optparse'というライブラリを使った都合上、-laのように複数のオプションをまとめて指定することができない。組み合わせる場合は、-l -aと入力する必要がある。
  • ACLと呼ばれる機能(説明は割愛します)の表示は未実装
  • 実際のlsのソースコードを参考にしていないので、アルゴリズムや関数名が違うかもしれません。

出力形式

いきなりコードを書いて模倣することはできないので、lsの出力の分析を行います。
ディレクトリとファイルを用意した方がわかりやすいので、下記のコマンドを実行します。

# directory1~directory10とfile1~file10をtestディレクトリ内に作成。
$ mkdir test/directory{1..10} && touch test/file{1..10} 

そして、ls testを実行して出力結果を確認します。

$ ls test
directory1      directory3      directory6      directory9      file2           file5           file8
directory10     directory4      directory7      file1           file3           file6           file9
directory2      directory5      directory8      file10          file4           file7

当たり前といえば、当たり前ですが綺麗に並んでいます。
闇雲に出力するだけではlsのような整った出力結果にはならなそうなので、課題を1つ1つ洗い出しながら完成を目指すことにします。

表示順

lsで出力されているファイル名は以下のような順番で昇順で並んでいます。
「ファイルの数」や「ターミナルの画面の大きさ」、「ファイル名の長さ」で、「1行に表示されるファイルの数」と「行数」は若干出力は変化しますが、表示順で確認したls testの出力は以下のようになっていました。

表示されている順番
名前が1番目に若いファイル     名前が4番目に若いファイル     名前が7番目に若いファイル   ... 名前が19番目に若いファイル
名前が2番目に若いファイル     名前が5番目に若いファイル          ︙                ... 名前が20番目に若いファイル
名前が3番目に若いファイル     名前が6番目に若いファイル          ︙

1行ずつ名前が1,4,7,10,13,16,19番目に若いファイル,名前が2,5,8,11,14,17,20番目に若いファイル,名前が3,6,9,12,15,18番目に若いファイルを順に出力すればlsの出力の再現ができそうです。

配列の処理

ディレクトリ内のファイル一覧を取得するために用いたDirクラスは、ファイル一覧を配列で渡してくれます。今回は、これをfilesという変数に代入して扱うこととします。
ですが、受け取った配列の中身が昇順ではなかったのでArrayクラスsortメソッドで昇順に並び替えます。

sortメソッド
files = ["file2", "file3", "file1"]
files.sort #=> ["file1", "file2", "file3"] 

配列の添字は0から始まります。
昇順で並び替えたことにより、files内の1番目に若いファイル名はfiles[0]とすることで取得することができます。
表示順で書いた名前が1,4,7,10,13,16,19番目に若いファイルを出力するということは、files[0],files[3],files[6],files[9],files[12],files[15],files[18]を出力することと言えます。

1行に出力できるファイル数や、行数を求め、filesをうまく出力する処理を書くと、下記のような出力になります。

$ ruby ls.rb test
directory1directory3directory6directory9file2file5file8
directory10directory4directory7file1file3file6file9
directory2directory5directory8file10file4file7

可読性は低いですが、一歩lsに近づきました。

ファイル名の間の区切り

このままだとどこからどこまでが1つのファイル名か分からないので、ファイルとファイルを区切る必要があります。
lsの出力結果のファイル名とファイル名はどう区切られているか、確認していきたいと思います。
適当にprint.rbというような名前のファイルを作成し、ls testの実行結果の

directory1      directory3

をコピーし、貼り付けます。

これを文字列としてターミナルに出力してみたいと思います。
" "で囲み、先頭にpを付け、

print.rb
p "directory1   directory13"

として保存します。

ターミナル上でruby print.rbと実行してみると以下のように出力されます。

$ ruby print.rb
"directory1\tdirectory13"

directory1とdirectory3の間に\tという文字が現れました。
これは、タブ文字を表しています。
このことから、ファイル名がタブ文字で区切られていることが確認できます。

これを模倣してファイル名の末尾に\tを追加することにします。
そのためにprintfメソッドを使用します。
ここではprintfについて詳しく解説はしないので、気になる方はドキュメントを読んでみてください。

printfの使用例
printf("%s\t", files[0]) # => %sの中にfiles[0]の中身が入り、出力される。
printf("%s\t", files[1])
# => "directory1    ""directory10   "

ls.rbにも実装すると、以下のような出力になります。

$ ruby ls.rb test
directory1  directory3  directory6  directory9  file2   file5   file8
directory10 directory4  directory7  file1   file3   file6   file9
directory2  directory5  directory8  file10  file4   file7

先ほどと比べると綺麗に見えますが、まだlsの出力とは異なります。

ファイル名の長さの統一

最後に、綺麗に見せるためにファイル名の長さを統一します。

directory10file1file2file3を使い、考えてみましょう。
現時点での出力はこのようになっています。

directory10\tfile2 
file1\tfile3

ファイル名の長さが統一されていないことで、1行毎の出力に文字数のズレが生じています。

全てのファイル名の長さが同じであれば、

ファイル名\tファイル名\t
ファイル名\tファイル名\t

のように綺麗な見た目にすることができるので空白文字を用いて、文字数が一番長いファイル名の長さに統一します。
今回の例では、directory1が一番ファイル名が長いので,directory1の長さに統一することにします。

# 空白文字を使うことで見た目を整えることができる!
directory\tfile2    \t 
file1    \tfile3    \t

文字列の長さの統一もprintfメソッドで実現することができます。
%sを少し改変することで幅指定を行うことができます。

printfの幅指定
printf("%15s", files[0])
# => "     directory1" 15文字になるよう空白文字が追加される。

printf("%-15s", files[0])
# => "directory1     " -をつけると左詰めになる。

name_len = 15

printf("%-#{name_len}s\t", files[0]) # => 式展開もできる。
# => "directory1     "

ls.rbでも幅指定をすると

$ ruby ls.rb test
directory1      directory3      directory6      directory9      file2           file5           file8
directory10     directory4      directory7      file1           file3           file6           file9
directory2      directory5      directory8      file10          file4           file7

lsと同じ出力を得ることができました!

解説

全てを解説すると、長くなってしまうので、出力形式で説明した箇所のみを解説します。
また、呼び出しているメソッドに関しては細かい説明はせず、機能の解説のみをコメント文で行っています。

ソースコードは 完成コードにあります。
全体の実装が気になる方は、こちらを参照してください。

self.display_normal

  def self.display_normal(dir) 
    files = get_files(dir) # ディレクトリ内のファイル名の配列を、filesに代入する。

    name_len = 1 # ファイル名の最大値を格納するname_lenを用意する。
    files.map { |file| name_len = file.length if name_len < file.length } # 一番長いファイル名を、name_lenに代入する。

    total_length = (name_len + 5) * files.count # 出力すべき文字数を算出。
    columns = `tput cols`.to_i # ターミナルの1行の文字数を取得。

    line_count = (total_length + (columns - 1)) / columns # 必要な出力行数を求める。
    column_count = (files.count + (line_count / 2)) / line_count # 1行に表示できるファイル数を求める。
    line_count = 1 if line_count == 0 # 最低でも1行は出力する必要があるため、line_countが0になっていた場合、1を代入する。

    (0...line_count).each do |line| 
      (0..column_count).each do |column| 
        idx = line_count * column + line 
        printf("%-#{name_len}s\t", files[idx]) if idx < files_count # 添字が配列内の大きさを超えていなければ、ターミナルに出力する
      end
      print("\n") # ここに到達すると1行分の出力が終わっているので改行する。
    end
  end

実行時間

timeコマンドを使い、実行時間を計測します。

$ time ls test                                                                    
directory1  directory3  directory6  directory9  file2       file5       file8
directory10 directory4  directory7  file1       file3       file6       file9
directory2  directory5  directory8  file10      file4       file7
ls test  0.00s user 0.00s system 78% cpu 0.008 total

#  ls testの実行時間 0.00s

$  time ruby ls.rb test                                                            
directory1  directory3  directory6  directory9  file2       file5       file8
directory10 directory4  directory7  file1       file3       file6       file9
directory2  directory5  directory8  file10      file4       file7
ruby ls.rb test  0.10s user 0.07s system 86% cpu 0.191 total 

# ruby ls.rbの実行時間 0.10s

目で見てわかるぐらいにはlsよりも遅いですね。

所感

実用性はあまりないですが、かなりlsコマンドを再現できたかと思います。
車輪の再開発と呼ぶには拙いコードですが、取り組む前よりも実装力は上がったと思います。

期日に余裕があればテストを書いたり、機能拡張をしたかったです。
また、常にドキュメント片手に作業をしていたので、ドキュメントを読み漁る力も身についたと思います。

当記事では解説はできませんでしたが、ls -lの実装も大変学びが多かったです。

おわりに

当アドベントカレンダーはWeb系の記事が多いので少し変わったことを記事にしたいと思いました。
他の方よりも、直接学びになることは少ないかもしれませんが、少しでも参考になりましたら幸いです。
いよいよ明日が最終日です。
@hide9138の記事で当アドベントカレンダーが良い締めくくりを迎えれるよう願っています。
それでは、良いクリスマスを:thumbsup:

完成コード

Github : https://github.com/shinooooo/Ruby-sh

参考

Ruby 2.7.0 リファレンスマニュアル
Man page of LS

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

【ActiveAdmin】レコードが無い場合の、まだありません。「作成する」を非表示に【blank_slate content link】

ActiveAdminで、

1件もレコードが無い状態のまま、

indexのページを表示すると、


"%{resource_name} はまだありません。「作成する」


というようなメッセージが表示されます。






この、

「作成する」がリンクになっているので、

非表示にしようと思ったのですが、

ActiveAdminのデフォルトの機能には無い様子...






色々調べていると、

一応解決策が見つかったのと、

今後もたまに使うかと思ったので備忘録にしておきます。






解決策


ActiveAdmin::Views::Pages::Indexというクラスにある、

render_blank_slate というメソッドにモンキーパッチを当てます。


  • render_blank_slate

[https://www.rubydoc.info/github/gregbell/active_admin/ActiveAdmin%2FViews%2FPages%2FIndex:render_blank_slate:embed:cite]


  • ja.ymlのキー (blank_slate、content、link)

[https://github.com/activeadmin/activeadmin/blob/master/config/locales/ja.yml#L66:embed:cite]






やり方


1、

initializerで、モンキーパッチを実装するファイルを読み込む。


クックパッドさんの記事を参考にしました。


[https://techlife.cookpad.com/entry/a-guide-to-monkey-patchers:embed:cite]






lib/monkey_patches/active_admin というディレクトリを作ります。


他への影響を抑えるため、

initializer内で読み込むディレクトリも最小限にします。


config/initializer/active_admin_monkey_patches.rb
Dir[Rails.root.join('lib/monkey_patches/active_admin/**/*.rb')].sort.each do |file|
  require file
end












2、

1で指定したところにモンキーパッチを実装。


例えば、

monkey_patches/active_admin/index_monkey_patch.rb のようなファイルを作ります。






今回は、

ActiveAdmin::Views::Pages::Indexというクラスにある、

render_blank_slate というメソッドにモンキーパッチを当てるため、

下記のようにします。


monkey_patches/lib/active_admin/views/pages/index_monkey_patch.rb
require 'active_admin/helpers/collection'

module ActiveAdmin
  module Views
    module Pages
      class Index
        protected
        def render_blank_slate
          blank_slate_content = I18n.t("active_admin.blank_slate.content", resource_name: active_admin_config.plural_resource_label)
          insert_tag(view_factory.blank_slate, blank_slate_content)
        end
      end
    end
  end
end






ソースは下記を参考にしました。

[https://waiyanyoon.com/blog/2015-07-31---replacing-content-displayed-in-active-admin-blank-slate/:embed:cite]












3、

サーバー再起動。












4、

これで、

"%{resource_name} はまだありません。「作成する」

「作成する」が消えているはずです!






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

has_manyで中間テーブルから親モデルの別のassociationを取ってきたい

概要

Railsを使用してデータを扱う際、殆どこんな機会は訪れない(別の方法などで代替できることがほとんど)ですが、どうしよもない事情で中間テーブルから関連付けを貼る必要があったので備忘録としてメモっておきます。

事前知識

class User < ApplicationRecord
   has_many :group_users
   has_many :groups, through: :group_users
   has_many :comments
end

class Group < ApplicationRecord
  has_many :group_users
  has_many :users, through: :group_users
  has_many :comments
end

class Comment < ApplicationRecord
  belongs_to :user
  belongs_to :group
end

class GroupUser < ApplicationRecord
  belongs_to :group
  belongs_to :user
end

このようなクラスが存在しているとします。
通常であればGroupUserという中間テーブルを起点として何かすることはほとんどないですし、あったとしてもUserやGroup経由でCommentを取得することができるので、困ることはほとんどありません。

一方、GraphQLを使用していたり、何かアソシエーションのタイミングで整ったデータが欲しいなどの稀な事情でGroupUserのインスタンスからgroupとuserが絞り込まれたcommentの一覧が欲しい場合があったりしました。

普通にthroughをして取得しようとすると、例えば下記のようなコードになります。

class GroupUser < ApplicationRecord
  belongs_to :group
  belongs_to :user
  has_many :comments, through: :user #groupでもいい
end

ただ、この方法だとthroughしたどちらかのカラムの値では絞り込まれますが、もう片方のカラムでは絞り込みが行われません。

どちらでthroughしたとしても、userかgroupのどちらかでの絞り込みしか行われず、意図した値は取得できません。

解決策

じゃあどうするのかというと、socpeを使います。

class GroupUser < ApplicationRecord
  belongs_to :group
  belongs_to :user
  has_many :comments,->(group_user) { where(group_id: group_user.group_id) }, through: :user #groupならuserでwhere
end

has_manyはスコープの引数として自分自身だけは取ることができます。なので、このようにすることでgroup_userインスタンスのgroup_idとuser_idを持ったコメントの一覧を取得することができます。

どうしても中間テーブルを起点にして取得しなければいけない状況になった場合に役立てば幸いです。

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

Pinterestのように写真を表示する

はじめに

絶賛ポートフォリオ製作中の駆け出しエンジニアです

インスタグラムのような古着専用の写真投稿サイトを作っています

その時に写真の投稿一覧画面でPinterest(https://www.pinterest.jp/) というサイトのように写真を並べるために奮闘しましたので備忘録として残したいと思います

コード

before

post.html.erb
 <div class = contents-row >
    <% @items.each do |item| %>
      <ul class='item-lists'>
          <li class='list'>
              <%= link_to items_path(item.id), method: :get do %>
                <%= image_tag(item.image, class:"item-img") %>
              <% end %>
          </li>
      </ul>
    <% end %>
  </div>
post.scss
.contents-row {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  margin: 0 auto;
  width: 95vw;
}

.item-lists {
  margin: 5px;
}

.list {
  width:25vw;
}

.item-img {
  width: 100%;
  height: auto;
  border-radius: 30px;
  vertical-align: bottom;
}

after

post.html.erb
 <div class = contents-row >
  <ul class='item-lists'>
    <% @items.each do |item| %>
          <li class='list'>
              <%= link_to items_path(item.id), method: :get do %>
                <%= image_tag(item.image, class:"item-img") %>
              <% end %>
          </li>
    <% end %>
  </ul>
  </div>
post.scss
.contents-row {
  display: flex;
  justify-content: center;
  align-items: center;
  margin: 0 auto;
  width: 95vw;
}

.item-lists {
  column-count: 3;
  column-gap: 15px;
  column-fill: auto;
 }

.list {
  width:25vw;
}

.item-img {
  width: 100%;
  height: auto;
  border-radius: 30px;
  vertical-align: bottom;
  margin: 5px;
}

行ったこと

• 【html】 ulタグがeach文の中に入っていたのでeach文の外に出した
• 【css】 item-listsにcolumn-countなどを記述し、それに合わせてそれぞれ調整した

参考サイト

https://designsupply-web.com/media/suplog/1862/

https://www.pinterest.jp/

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

RubyでGoogleの問題を解く

Mac OS-11.1 ruby-2.6.6p146

Googleの問題を解く

素数判定

回答(リファクタリング後)

def getNums()
  line = readlines
  len = line.length
  num = ''

  (0..len-1).each do |i|
    num = num + line[i].chomp
  end

  return num
end

def prime?(num)
  (2..Math.sqrt(num).to_i).each do |i|
    if(num%i == 0)
      return false
    end
  end
  return true
end

digits = 10

if $PROGRAM_NAME == __FILE__
  num = getNums().delete('.')
  res = 0

  (0..num.length-digits).each do |i|
    res = num[i, digits].to_i
    prime?(res) ? break : next 
  end

  puts "e(自然対数の底)の値で連続する10桁の数のうち, 最初の素数は#{res}"
end

解説

随時更新


  • source ~/Library/Mobile Documents/com~apple~CloudDocs/KG/class/M1/multi_scale_sim/grad_members_20f/members/ryomichi56/./qiita/google_recruit.org
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby on Railsプロジェクトの開発環境をDocker化する

ACCESS Advent Calendar 2020 の16日目の記事です。

概要

現在運用中のRuby on Railsプロジェクトの開発環境をDocker化する案件があり、
その際に行った作業の手順をまとめました。

Dockerについて

Dockerとは、コンテナと呼ばれる仮想環境を構築・実行できるようにするためのプラットフォームです。

私は今回初めてDockerを触ったのですが、Dockerの理解には入門Dockerが大変参考になりました。

前提

バージョン

  • macOS Catalina 10.15.7
  • Docker 19.03.13
  • docker-compose 1.27.4
  • Ruby 2.4.5
  • mongoDB 3.0.15
  • postgres 10

システム構成

こちらのシステム構成図の通りにDocker化していきます。
(※大分簡略化しています)

system.png

初めにRailsアプリケーションのDocker Containerを作成し、
その後オーケストレーションツールであるdocker-composeによって、
RailsアプリケーションをmongoDB及びpostgreSQLに接続します。

手順

Dockerのインストール

Docker HubよりDocker Desktop for Macを導入します。

ターミナルで下記の2つのコマンドが実行できればOKです。

$ docker -v
Docker version 19.03.13, build 4484c46d9d
$ docker-compose -v
docker-compose version 1.27.4, build 40524192

必要なファイルの作成

Docker及びdocker-composeを動かすのに必要なファイルをプロジェクトのルートに作成します。

Dockerfile

ruby 2.4.5環境が予めインストールされているruby:2.4.5-slimというDockerイメージをDocker Hubから取得し、そのイメージ上にRails環境をセットアップしています。

Dockerfile
FROM ruby:2.4.5-slim

# Dockerコンテナ上におけるプロジェクトルートを指定
ENV APP_ROOT=/app
RUN mkdir $APP_ROOT
WORKDIR $APP_ROOT

# apt-utilsインストールの時の警告を抑制する
# https://qiita.com/haessal/items/0a83fe9fa1ac00ed5ee9
ENV DEBCONF_NOWARNINGS yes

# aptパッケージのインストール
RUN apt-get update -y -qq && \
    apt-get install -y -qq build-essential libpq-dev libmagickwand-dev

# Railsのセットアップ
COPY Gemfile Gemfile
COPY Gemfile.lock Gemfile.lock
RUN gem install bundler -v 1.17.3 && bundle install

# プロジェクトディレクトリをDocker Imageにコピー
COPY . $APP_ROOT

docker-compose.yml

postgres, mongo, webの3つのサービスを定義し、
webpostgres及びmongoに依存させています。

docker-compose.yml
version: "3"

services:
    # postgreSQL containerの定義
    postgres: 
        image: postgres:10
        ports:
            # <Host Port>:<Container Port>
            - "5432:5432"
        environment:
            POSTGRES_USER: xxxxxx
            POSTGRES_PASSWORD: xxxxxx

    # mongoDB containerの定義
    mongo:
        image: mongo:3.0.15
        ports:
           - "27017:27017"

    # Rails app containerの定義
    web: 
        build: . 
        env_file: .env
        # pid error の回避のため、server.pidを削除したのちにrails sを実行
        # https://qiita.com/sakuraniumarete/items/ac07d9d56c876601748c
        command: /bin/sh -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
        # 依存関係の定義 (webをビルドするとpostgresとmongoが同時にビルドされる)
        depends_on:
            - postgres
            - mongo

ビルド実行

これら2つのファイルを作成すると、

$ docker-compose build

でコンテナをビルドできるようになります。

DBの永続化

現在の状態ではDBがコンテナ内部のストレージに生成されており、
コンテナを削除して再ビルドすると、DBに保存されていたデータは全て消失してしまいます。

DB上のデータを永続化するためには、Dockerが提供しているvolumeという機能を利用します。
volumeは、Docker Containerのライフサイクルからは独立して生成されるデータ保存領域です。
volume上にDBを生成することにより、コンテナを再ビルドしてもDB上のデータが残り続けるようになります。

image.png
引用元: https://matsuand.github.io/docs.docker.jp.onthefly/storage/volumes/

volumeを利用するためには、docker-compose.ymlに以下の内容を追記します。

docker-compose.yml
 services:
    postgres: 
+        volumes:
+            - "postgres-data:/var/lib/postgresql/data"
    mongo:
+        volumes:
+            - "mongo-data:/data/db"
+ volumes:
+    postgres-data:
+    mongo-data:

上記を追記した上で改めてビルドすると、Docker Volumeが作成されているはずです。

$ docker volume ls
local               mongo-data
local               postgres-data

ホストとコンテナのソースコードを同期

現在の状態では、Dockerfileをビルドした時点で、ホストの全データををImageにコピーしています。

Dockerfile
COPY . $APP_ROOT

つまり、これより後にホスト側でソースコードを変更した場合、動作しているコンテナを一旦停止させ、
docker-compose build からやり直す必要があります。

開発環境において毎度ビルドからやり直しているのでは非常に効率が悪いので、
ホストのコード変更をコンテナに即時反映できるようにします。

よくある方法は、下記のように、プロジェクトのルートディレクトリを無名volumeとしてコンテナにマウントする方法です。

docker-compose.yml
services:
    # ${APP_ROOT}はDockerfileにおいてENVで定義した環境変数
    web:.:${APP_ROOT}

しかし、上記の方法を採った場合の問題として、
Docker for Mac特有のVolume I/Oの遅さが大きく影響してしまうということがあります。
この問題については、docker/for-macのGitHubリポジトリにおいて議論されています
(この問題により最も影響を受けたのは、RSpecの実行でした。上記の方法でマウントした場合、
普段5分ほどで完了していたテストが30分ほどかかりました...)

docker-sync

現状とりうる有効な解決策として、docker-syncというサードパーティライブラリが提供されています。

新たにdocker-sync.ymldocker-compose-dev.ymlを作成します。
作成にあたってはdocker-syncのドキュメントを参考にしました。

docker-sync.yml
version: "2"

syncs:
  sync-volume:
    src: "."
    sync_excludes:
      - "log"
      - "tmp"
      - ".git"

docker-compose-dev.yml
version: "3"

services:
  web:
    volumes:
      - "sync-volume:/app:nocopy"

volumes:
  sync-volume:
    external: true

DBセットアップ

以下のコマンドで、postgreSQLとmongoDBをセットアップします。

$ docker-compose run --rm -e RAILS_ENV=development -T web rake db:setup
$ docker-compose run --rm -e RAILS_ENV=devlopment -T web rake db:mongoid:create_indexes

docker-compose run --rm <container name> <command>は、
指定したコンテナサービスを起動し、任意のコマンドを実行後、そのコンテナを削除するというコマンドです。
DBはvolumeで永続化されているので、セットアップのためだけにコンテナを作成し、その後削除してしまっても問題ないということです。

Railsサーバーの実行

ここまでの手順を実施した上で、下記コマンドを実行することでRailsサーバーが起動します。

$ docker-sync-stack start

これは以下のコマンドを短縮したものです。

$ docker-sync start
$ docker-compose -f docker-compose.yml -f docker-compose.yml up

-fオプションを使って複数のdocker-composeファイルを指定すると、
コンテナ作成時の各種パラメーターを上書きすることができます。
参考: 設定の追加と上書き - Docker-docs-ja

また、上記はフォアグラウンドで起動するためのコマンドで、
バックグラウンドで起動する場合は以下のコマンドを実行します。

# 起動
$ docker-sync start
$ docker-compose -f docker-compose.yml -f docker-compose.yml up
# 停止
$ docker-compose down
$ docker-sync stop

テスト実行

RSpecを実行するためには、サーバーを起動した状態で以下のコマンドを実行します。

$ docker-compose exec -e COVERAGE=true -T web bundle exec rspec

docker-compose execで、起動中のDockerコンテナに対して任意のコマンドを実行できます。

もしくは、以下のようにコンテナに入って実行することもできます。

$ docker-compose exec web bash
root@container:/app# bundle exec rspec

CI対応

CIツールとしてJenkinsを使用しています。

テストを実行するシェルスクリプト

ビルドジョブにおいて、下記のシェルを実行することで自動テストが行われるようにしました。

# 終了時の処理
# docker-composeが失敗した際でもJenkinsビルドマシンにゴミが残らないよう後処理をかける
# https://qiita.com/ryo0301/items/7bf1eaf00b037c38e2ea
function finally {
    # Clean project
    docker-compose down --rmi local --volumes --remove-orphans
}
trap finally EXIT

# 並列実行のために、プロジェクト名としてJenkinsの環境変数である$BUILD_TAGを指定
export COMPOSE_PROJECT_NAME=$BUILD_TAG

# 環境変数で予めビルドするdocker-composeファイルを指定することで、
# -fオプションによる指定を省略できる
# https://docs.docker.jp/compose/reference/envvars.html
export COMPOSE_PATH_SEPARATOR=:
export COMPOSE_FILE=docker-compose.yml:docker-compose-test.yml

# Build Container
docker-compose build --no-cache
docker-compose up -d

# Setup DB
docker-compose exec -e RAILS_ENV=test -T web rake db:setup
docker-compose exec -e RAILS_ENV=test -T web rake db:mongoid:create_indexes

# Run RSpec
docker-compose exec  -e COVERAGE=true -T web bundle exec rspec

ビルドジョブを並列実行できるようにする

RailsプロジェクトをDocker化していない時の問題として、
2つ以上のビルドジョブを並列実行すると、同じマシン上でDBの取り合いが起こり、
エラーが発生する問題がありました。

Docker化したことで、それぞれのビルドが独立したコンテナの中で実行されるようになり、
並列実行してもエラーが起こらないようになります。
ただし、並列ビルドの実行時にコンテナのポート番号が重複しないよう、
ポートフォワーディングの設定を変更する必要があります。
参考: ホスト上にコンテナのポートを割り当て - Docker-docs-ja

export COMPOSE_FILE=docker-compose.yml:docker-compose-test.yml

で指定している docker-compose-test.ymlの中身でそれを行っています。

docker-compose-test.yml
version: "3"

services:
    postgres:
        ports:
            - "5432"
    mongo:
        ports:
            - "27017"
    web:
        ports:
            - "3000"

また、docker-compose.ymlに記載したポート番号をdocker-compose-dev.ymlに移動する必要があります。
なぜなら、このまま docker-compose up -d を実行すると、docker-compose.ymldocker-compose-test.ymlがマージされ、
結果としてポートの指定が以下のようになってしまい、docker-compose-test.ymlでわざわざポート指定した意味がなくなってしまうためです。

services:
    postgres:
        ports:
            - "5432:5432"
            - "5432"
    mongo:
        ports:
            - "27017:27017"
            - "27017"
    web:
        ports:
            - "3000:3000"
            - "3000"

Railsサーバーの実行の項で、

-fオプションを使って複数のdocker-composeファイルを指定すると、
コンテナ作成時の各種パラメーターを上書きすることができます。

と述べましたが、複数指定可能なパラメータの場合は設定値は上書きされずにマージされるので注意が必要です。

docker-compose.yml
services:
    postgres:
-        ports:
-            - "5432:5432"
    mongo:
-        ports:
-            - "27017:27017"
    web:
-        ports:
-            - "3000:3000"
docker-compose-dev.yml
services:
    postgres:
+        ports:
+            - "5432:5432"
    mongo:
+        ports:
+            - "27017:27017"
    web:
+        ports:
+            - "3000:3000"

おまけ: RubyMineへの対応

JetBrainsのIDEであるRubyMineは、Docker Container上のRuby on Railsの開発環境に完全対応しており、以下の手順で設定することができます。
チュートリアル : リモートインタープリターとしての Docker Compose — RubyMine

まとめ

今回新たに作成したファイル

開発環境をDocker化するにあたり、新たに作成したファイルは以下の通りです。

.
├── Dockerfile
├── docker-compose.yml
├── docker-compose-dev.yml
├── docker-compose-test.yml
└── docker-sync.yml

その他

今回初めてコンテナ技術に触れ、Docker化にあたっては様々な試行錯誤を重ねました。
これまでに書いた中で、もっと良い対応方法がある、或いは対応方法として正しくない箇所があるかもしれませんが、その時はご指摘いただければ幸いです。

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

Rubyでローマ数字を求める

Mac OS-11.1 ruby-2.6.6p146

ローマ数字(roman numerals)

アラビア数字を入力としてローマ数字を返すメソッドを作成する。

回答(リファクタリング前)

無駄が多いし長くて見ない方がいいレベルなので折り畳んでます。
def getNums()
  lines = readlines
  len = lines.length

  (0..len-1).each do |i|
    lines[i] = lines[i].chomp.to_i
  end

  return lines
end

class Integer
  def to_roman
    sym = ['I', 'V', 'X', 'L', 'C', 'D', 'M']
    roman = ''
    to_arr(4).each_with_index do |num, i|
      case i
      when 0 then
    roman += sym[6] * num
      when 1 then
    case num
    when 0..3 then
      roman += sym[4] * num
    when 4 then
      roman += sym[4] + sym[5]
    when 5..8 then
      roman += sym[5] + sym[4] * (num-5)
    when 9 then
      roman += sym[4] + sym[6]
    end
      when 2 then
    case num
    when 0..3 then
      roman += sym[2] * num
    when 4 then
      roman += sym[2] + sym[3]
    when 5..8 then
      roman += sym[3] + sym[2] * (num-5)
    when 9 then
      roman += sym[2] + sym[4]
    end
      when 3 then
    case num
    when 0..3 then
      roman += sym[0] * num
    when 4 then
      roman += sym[0] + sym[1]
    when 5..8 then
      roman += sym[1] + sym[0] * (num-5)
    when 9 then
      roman += sym[0] + sym[2]
    end
      end
    end
    roman
  end

  def to_arr(n)
    arr = []
    (Math.log10(self).to_i + 1).upto(n-1) do
      arr.unshift(0)
    end
    arr + digits.reverse
  end
end

if $PROGRAM_NAME == __FILE__
  getNums().each do |i|
    puts i.to_roman
  end
end

回答(リファクタリング後)

def getNums()
  lines = readlines

  (0..lines.length-1).each do |i|
    lines[i] = lines[i].chomp.to_i
  end
  lines
end

class Integer
  def to_roman
    sym = ['I', 'V', 'X', 'L', 'C', 'D', 'M']
    roman = ''
    base = sym.length-1
    to_arr(4).each_with_index do |num, i|
      case num
      when 0..3 then
    roman += sym[base-i*2] * num
      when 4 then
    roman += sym[base-i*2] + sym[base-i*2+1]
      when 5..8 then
    roman += sym[base-i*2+1] + sym[base-i*2] * (num-5)
      when 9 then
    roman += sym[base-i*2] + sym[base-(i-1)*2]
      end
    end
    roman
  end

  def to_arr(n)
    arr = Array.new(n - Math.log10(self).to_i - 1, 0)
    arr + digits.reverse
  end
end

if $PROGRAM_NAME == __FILE__
  getNums().each do |i|
    # puts "#{i} => #{i.to_roman}"
    printf("%4d => %s\n", i, i.to_roman)
  end
end

解説

def getNums()
  lines = readlines

  (0..lines.length-1).each do |i|
    lines[i] = lines[i].chomp.to_i
  end
  lines
end

随時更新


  • source ~/Library/Mobile Documents/com~apple~CloudDocs/KG/class/M1/multi_scale_sim/grad_members_20f/members/ryomichi56/./qiita/roman_numerals.org
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Sprockets::DoubleLinkError を解消した方法

やんばるエキスパートというオンラインスクールにて教材に取り組んでいた時の出来事です。

Sprockets::DoubleLinkErrorの出現

Railsのメッセージ投稿アプリを作成しており、その中でstylesheetsフォルダのcssファイルをscssファイルに拡張子を変更し、動作確認を行ったところ、「Sprockets::DoubleLinkError」が出現しました。

原因はcssファイルとscssファイルの干渉

stylesheetsフォルダ内にcssファイル、min.cssファイル、scssファイルの3つが存在しており、cssファイルとscssファイルが干渉してエラーが起こっていたようです。

cssファイルとmin.cssファイルを削除し、動作確認を行った所、エラーは解消されました。

しかし、、、

拡張機能Easy Sassにも注意

VScodeを再起動すると、再度cssファイルとmin.cssファイルが生成されていました。
拡張機能にEasy Sassを入れており、どうやらこいつが悪さをしていたようです。
Easy Sassを無効化した所、cssファイルとmin.cssファイルは自動生成されなくなりました。

そして、エラーも出ず、問題なく動作できるようになりました。

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

【Active Admin】Unable to find input class JsonInput

エラー内容


json型のDBカラムを、

active_adminの管理画面フォームで表示させようとすると、

Unable to find input class JsonInput のエラー。


  • backtraceの一部
@input_class_finder.find(as)
      rescue Formtastic::InputClassFinder::NotFoundError
        raise Formtastic::UnknownInputError, "Unable to find input #{$!.message}"
      end

      # @api private






解決方法


Formtasticのクラスを継承した、

XxxxInputというクラスを作れば良い。


今回の場合は、

エラー文の通り、(Unable to find input class JsonInput)

JsonInputというクラスを作ればOK。


# app/inputs/json_input.rb
class JsonInput < Formtastic::Inputs::StringInput; end






その後、

サーバーを再起動してエラーは出なくなりました。






参考

https://github.com/activeadmin/activeadmin/issues/4178

https://qiita.com/hirokik-0076/items/7dacbb76b1d0b84ec75a

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

【Ruby】ifのない条件式の?(クエスチョンマーク)の意味。三項演算子の使い方。

個人メモです。

以下のように、ifがなく、?(はてな)と:(コロン)が記述された式の意味を理解する。

x = y == "aaa" ? v : w

三項演算子

?(はてな)と:(コロン)を使った式は、if else文の省略形で三項演算子と呼ぶ。

以下の2つの処理は同じものとなる。

通常
if 条件式
  戻り値1
else
  戻り値1
end
三項演算子
条件式 ? 戻り値1 : 戻り値2

(1)条件式、(2)戻り値1、(3)戻り値2の3つの項目があるため、三項演算子と呼ぶ。


参考演算子の実例

条件式がtrueの場合は戻り値1が返る。

x = 10
x == 10 ? "Yes" : "No"

=> "Yes"

条件式がfalseの場合は戻り値2が返る。

x = 10
x != 10 ? "Yes" : "No"

=> "No"


x = y == "aaa" ? v : wの処理内容

これは、三項演算子の結果を変数xに代入する処理となる。

y = "aaa"
v = "Yes"
w = "No"

x = y == "aaa" ? v : w

p x
=> "Yes"

y == "aaa"がtrueなので前方の戻り値vxに代入する。

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

【Ruby】uniqメソッドで重複する要素を除外する。

個人メモです。

uniqメソッドを使うと重複する要素を排除することができる。

  • 非破壊処理。
  • ブロックを使って、各要素を処理したあとで比較できる
  • 重複する場合は、最初の要素が残る。(後ろの要素を削除)

配列の場合

arr = [1, 4, 5, 3, 5, 6, 1, 3, 2, 2, 1, 1, 1]
arr.uniq

=> [1, 4, 5, 3, 6, 2]


##非破壊の確認
p arr
=> [1, 4, 5, 3, 5, 6, 1, 3, 2, 2, 1, 1, 1]



▼処理後の値
ブロックを使うと処理後の値を比較できる。削除するのは該当する元の値。(※処理後ではない)

オブジェクト.uniq{|変数| 処理}

例えば、文字列が入った要素を、to_sで全ての要素を文字列にしてから比較する。

arr1 = [1, 3, 2, "2", "3", 1]

arr1.uniq{|i| i.to_s}
=> [1, 3, 2]
元の値 1 3 2 "2" "3" 1
処理後 "1" "3" "2" "2" "3" "1"
残る要素 × × ×

よって出力は重複なしか1つ目に該当する元の値[1, 3, 2]となる。


オブジェクトの場合

オブジェクトで値が重複している要素の削除もできる。

オブジェクト.uniq{|変数1, 変数2| 処理}

  • ブロックは必須
  • 変数は2つ必須。変数1にキー、変数2に値が入る。
  • 出力は配列になるため、to_hでオブジェクト(シンボル)に戻す。

シンボルにする場合

obj = {:a=>1, :b=>2, :c=>3, :d=>2, :f=>3}
obj2 = obj.uniq{|x,y| y}
=> [[:a, 1], [:b, 2], [:c, 3]]}

obj2.to_h
=> {:a=>1, :b=>2, :c=>3}


ハッシュにする場合

ハッシュにしたい場合は、to_hメソッドのブロックでキーにto_sを適用し文字列にする。

obj = {:a=>1, :b=>2, :c=>3, :d=>2, :f=>3}

obj.uniq{|x,y| y}.to_h{|k ,v| [k.to_s, v]}
=> {"a"=>1, "b"=>2, "c"=>3}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む