RubyのTimeクラスのメソッド「strptime」の使い方

こんにちは!kossyです!



さて、今回はRubyのTimeクラスのメソッドである「strptime」の使い方をブログに残してみたいと思います。




環境
Ruby 2.5.1
Rails 5.2.3
MacOS Mojave



2020-03-28T21:05:06+09:00 みたいな表示をparseする

ログの時刻表示が2020-03-28T21:05:06+09:00のようになるパターンがあるかと思いますが、
ニーズによっては、2020-03-28 21:05:06 + 09:00 のように表示したいということもあるかと思います。

そこで、Time#strptimeの出番です。

$ Time.strptime('2020-03-28T21:05:06+09:00',  '%Y-%m-%dT%H:%M:%S%z')

=>  '2020-03-28 21:05:06 + 09:00'

このように、第一引数に渡した時刻を、第二引数で指定したフォーマット通りに変換してくれます。



勉強になりました。

RequestSpecを書く際にiso8601メソッドを使ってcreated_atのテストを通す

こんにちは!kossyです!




さて、今回はAPIモードで実装されたRailsのRequestSpecを書く際に、
iso8601メソッドを使ってcreated_atのテストを通す方法について、ブログに残してみたいと思います。




環境
Ruby 2.5.1
Rails 6.0.2.1
MacOS Mojave




普通に比較すると通らない

require 'rails_helper'

RSpec.describe 'Pokemon API', type: :request do
  describe 'index' do
    it '' do
      create_list(:pokemon, 50)
      pokemon = Pokemon.first

      get '/pokemons'

      expect(response.status).to eq 200
      json = JSON.parse(response.body)
      pokemon_json = json[0]
      expect(pokemon_json['created_at']).to eq pokemon.created_at
    end
  end
end

上記のようなテストがあったとして、

expect(pokemon_json['created_at']).to eq pokemon.created_at

この書き方をすると、

expected: 2020-03-18 21:59:31.000000000 +0900
            got: "2020-03-18T21:59:31.000+09:00"

日付の中身がちゃうやん!と怒られてしまいます。

そこで登場するのが、DateTimeクラスのインスタンスメソッドであるiso8601です。
iso8601 (DateTime) - APIdock

ISO8601についてはこちらが詳しかったです。
ISO 8601 - Wikipedia


このメソッドを、

expect(pokemon_json['created_at']).to eq pokemon.created_at.iso8601(3)

このようにcreated_atに適用します。

すると、

should eq "2020-03-18T22:04:09.000+09:00"

このように、テストをパスすることができます。




勉強になりました。

STIを使っている時のFactoryBotでのモックデータの作成

こんにちは!kossyです!




さて、今回はSTI(SingleTableInheritance)を使ってクラス定義をしているクラスの
FactoryBotでのモックデータの作成の仕方について、ブログに残してみたいと思います。




環境
Ruby 2.5.1
Rails 5.2.3
MacOS Mojave




なお、STIが何かについての説明は割愛します。
みんなRailsのSTIを誤解してないか!? - Qiita
[Rails] STI(単一テーブル継承)とメタプログラミングでDRY - Qiita

STIについて詳しく知りたい方は上記の記事をご覧になってみてください。




initialize_withメソッドを使う

例えば、STIで、
親 Commentクラス
子 ArticleComment, EntryCommentクラス

が定義されているとします。

この場合、

FactoryBot.define do
  factory :comment do
    body { Faker::Book.title }
  end

  trait :article do
    type 'ArticleComment'
  end
end

のようにtraitを使って定義することで、

create :comment, :article

とすることで、typeにarticleが指定されたCommentインスタンスを作成することができますが、
クラスはArticleCommentクラスではなく、Commentクラスになっています。

この場合、ArticleCommentに定義されたインスタンスメソッドのテストを行うことができません。

なので、

FactoryBot.define do
  factory :comment do
    body { Faker::Book.title }
  end

  trait :article do
    type 'ArticleComment'
      initialize_with do
        klass = type.constantize
        klass.new(attributes)
      end
  end
end

このようにinitialize_with メソッドを使うことで、
ArticleCommentクラスのインスタンスを生成することができます。



参考にさせていただいた記事
STIを利用している場合のFactoryBotのfactory定義について - indilog

Rspecで画像アップロードのテストデータを準備する

こんにちは!kossyです!




さて、今回はRspecで画像アップロードのテストデータを準備する方法について、
ブログに残してみたいと思います。




環境
Ruby 2.5.1
Raila 5.2.3
MacOS Mojave



Rack::Test::UploadedFileクラスを使う

Wraps a Tempfile with a content type. Including one or more UploadedFile's in the params causes Rack::Test to build and issue a multipart request.

出典: www.rubydoc.info

Tempfileをcontent_typeでラップしてくれるクラスです。


これを使って、画像アップロードのためのモックデータを準備できます。



使い方は簡単で、

params = {
  image: Rack::Test::UploadedFile.new(File.join(Rails.root, 'spec/fixtures/profile.png'), 'image/png')
}

みたいにパラメータとして定義したり、

FactoryBot.define do
  factory :article do
    title { Faker::Book.title }
    body { Faker::Lorem.sentences }
    released_at { (Date.today - 1) }
    expired_at { (Date.today + 1) }
    member_only { false }
    thumbnail {  Rack::Test::UploadedFile.new(File.join(Rails.root, 'spec/fixtures/profile.png'), 'image/png') }
  end
end


みたいにFactoryBotのモックデータとしても定義できます。

第一引数で画像のパスを指定し、第二引数でコンテンツのタイプを指定します。
ちなみに第二引数に何も指定しないと、コンテンツタイプがtext/plainになるので、コンテンツタイプでバリデーションを
かけている場合は気をつけましょう。(私は30分ハマりました)



参考にさせていただいた記事

https://github.com/rack-test/rack-test/blob/master/lib/rack/test/uploaded_file.rb
https://www.rubydoc.info/github/brynary/rack-test/Rack/Test/UploadedFile
https://qiita.com/selmertsx/items/2beb0d7ec0774cbbf050
https://qiita.com/tatsuya1156/items/f03c53917d72cdca053a
http://sissoko.hatenablog.com/entry/2016/07/08/131344

Reactの学習に使えそうなサイトまとめ

こんにちは!kossyです!




さて、今回はReact初心者の学習に使えそうなサイトをまとめてみたいと思います。



1. Deep Dive Into Modern Web Development

fullstackopen.com

Reactだけでなく、周辺ライブラリや、GraphQLとの連携までカバーしています。

初心者向けというよりは中級者向けの内容かもしれません。

2. React/ReduxでGoogleカレンダー風カレンダーアプリケーションを作ろう

www.techpit.jp


こちらも中級者向けかもしれません 笑
ディレクトリやコンポーネンt設計までカバーした本格的な内容になっています。

3. 【React × Ruby】サーバーレスでゴルフ場検索サービスを作ってみよう!

www.techpit.jp

こちらはAPI側の実装も網羅した教材になっています。
現場ではReactだけでアプリケーションを構築することはおそらく稀で、
APIを作成するのも同時に行うのが当たり前かと思われます。

上記の教材はサーバーレス構成でモダンなアプリケーションを作成できる教材になっているので、
ある程度Reactに慣れて、バックエンドにも手を出してみたい方におすすめです。

4. Progate React コース

prog-8.com

ベタですが、全く何もわからない状態からの学習にはProgateの教材が一番適していると思います。
わかりやすいスライドが常に確認でき、環境構築も必要ないので、
学習の入り口としては最適かと思います。

番外編 TypeScript Deep Dive

typescript-jp.gitbook.io

ReactはJavaScriptでもTypeScriptでもアプリケーションを作成することができますが、
近年はTypeScriptで開発するのが主流になりつつあるようです。

上記の記事は体系的にTypeScriptについて学習できる内容になっており、おすすめです。

babel-polyfillでIE対応

こんにちは!kossyです!



さて、今回はVueCLI 3系で作った Vue.js 2系のWebアプリをbabel-polyfillでIE対応する方法について、
ブログに残してみたいと思います。



環境
Vue.js 2.5.17
VueCLI 3.0.1
MacOS Mojave
npm 6.5.0
node 11.8.0



babel-polyfillをnpm install

まずはbabel-polyfillをnpm install します。

$ npm install --save babel-polyfill

そして、src/main.jsでbabel-polyfill をimport します。

import 'babel-polyfill'

あとはnpm run build でbuildを行い、distファイルをデプロイするだけです。




勉強になりました。

Rubyのany?でレコードの配列が特定の日にちの範囲内に含まれているかを判定する

こんにちは!kossyです!




さて、今回はRubyのEnumerableクラスのインスタンスメソッドである、any?メソッドを使って、
レコードが特定の日にちの範囲内に含まれているかを判定するやり方をブログに残してみたいと思います。



環境
Ruby 2.6.3
Rails 6.0.2.1
MacOS Mojave



前提として、User has_many Books という関連が組んであるとします。



コード全晒し

user.books.any? do |book|
  base_date = book.created_at.localtime.to_date
  base_date >= Date.today.ago(3.days) && base_date <= Date.today.in(3.days)
end

userが持っているBookの中で、bookの作成日が前後3日間のレコードがあれば、trueが返り、
一つもなければfalseが返ります。


ちなみに、selectを使えば、前後3日間の条件に合致したレコードを配列にして返すことができます。
findを使うと、条件に合致したレコード1つを返すことができます。



勉強になりました。

ServiceWorkerが原因で最新のデプロイが反映されなかった話

こんにちは!kossyです!




さて、今回はServiceWorkerが原因で最新のデプロイがすぐに反映されなかった話をブログに残してみたいと思います。




今まで何回かキャッシュ関連のエントリを書いていました。
kossy-web-engineer.hatenablog.com

kossy-web-engineer.hatenablog.com


今回の動作環境は、APIに専念するRailsとVue.jsという技術スタックで、コンパイルしたVue.jsのコードは
GCSに置いて配信していました。



HTMLを返すRailsであれば、ResponseのHeaderを上書きするやり方は効果アリだったかもしれません。
でも今回のRailsjsonを返すものなので、意味がなかったです、、、

GCSにコンテンツをアップロードするときにcacheの設定をするのは、正しい打ち手だと思います。

ただ、問題の元凶であるServiceWorkerがローカルのcacheを返してしまい、
デプロイ直後に最新の変更が反映されない状態となっていました、、、



ServiceWorkerはPWAにするために動作環境の中に組み込まれていたようなのですが、
今回の要件にPWAは必須ではなかったので、ServiceWorkerを仕込むコードを削除し、

if ('serviceWorker' in navigator) {
      navigator.serviceWorker.getRegistrations().then(function (registrations) {
        for (const registration of registrations) {
          // unregister service worker
          console.log('serviceWorker unregistered');
          registration.unregister();
        }
      });
};

上記のコードをコンソールで実行するか、

chrome://serviceworker-internals/

上記のページでServiceWorkerをunregistしたところ、デプロイ直後に最新の変更が反映されるようになりました。




大変勉強になりました。

Railsで同一モデル内で関連を組む

こんにちは!kossyです!




さて、今回はRailsで同一モデル内で関連を組む方法について、ブログに残してみたいと思います。




環境
Rails 6.0.2.1
Ruby 2.5.1
MacOS Mojave





実装手順

まず、Appointmentモデルがあったとします。

$ rails g model Appointment date:date start_time:time parend_id:bigint

dateカラムとstart_timeカラム、それと同一モデル内で関連を表すための外部キーであるparent_idカラムを作成します。


app/models/appointment.rbの実装は下記のようにします。

class Appointment

  belongs_to :parent, class_name: :Appointment, optional: true
  has_one :child, class_name: :Appointment, foreign_key: :parent_id, dependent: :destroy

end

belongs_toとhas_oneメソッドで関連を表しています。

class_nameを指定しているのは、指定しなかった場合はchildというモデルを探しに行ってしまうため、
Appointmentをclass_nameとして明示的に指定しています。

foreign_keyオプションは、デフォルトでは外部キーのネーミングがmodel_idと決まっているため、
指定しなかった場合はappointment_idを探しに行ってしまいます。
明示的にparent_idと指定することで、「外部キーはparent_idだよ」とrailsに教えることができます。


こうすることで、appointment.childとするとappointmentに紐づく子レコードが取得できるようになります。



勉強になりました。

RailsでOpenWeatherMapAPIを使ってjsonを返すAPIを作る

こんにちは!kossyです!




さて、今回はOpenWeatherMapAPIをRailsから叩いて、JSONとして返却する方法について、
ブログに残してみたいと思います。




環境
Rails 6.0.2.1
Ruby 2.5.1
MacOS Mojave



API keyの取得

yuukiyg.hatenablog.jp

上記の記事を参考に、API keyを取得してください。



取得できたら、

$ cd  your-rails-app

$ EDITOR="vi" bin/rails credentials:edit

でcredentialsの設定を行います。

# aws:
#   access_key_id: 123
#   secret_access_key: 345
# Used as the base secret for all MessageVerifiers in Rails, including the o    ne protecting cookies.
secret_key_base: 1234567812345678
weather_map_id: your api key

これで準備完了です。



コード全晒し

searchメソッドの返り値は、引数keywordの地名の天気5日間分の情報が返ります。


lib/open_weather_map_client.rb

require 'net/https'

class OpenWeatherMapClient
  include Singleton

  URL = 'http://api.openweathermap.org/data/2.5'
  API_KEY = Rails.application.credentials.dig(:weather_map_id)

  def initialize
    @uri = URI.parse URL
    @http = Net::HTTP.new @uri.host, @uri.port
  end

  def search(keyword)
    create_request(keyword)

    @res['list'].map do |list|
      main_info = list['main']
      weather_info = list['weather']
      {
        time: list['dt'],
        temp: main_info['temp'],
        feels_like: main_info['feels_like'],
        temp_min: main_info['temp_min'],
        temp_max: main_info['temp_max'],
        pressure: main_info['pressure'],
        humidity: main_info['humidity'],
        main_weather: weather_info[0]['main'],
        description: weather_info[0]['description'],
        clouds: list['clouds']['all'],
        wind: list['wind']['speed'],
      }
    end
  end

  private

    def create_request(keyword)
      req = Net::HTTP::Get.new @uri.path + "/forecast?q=#{keyword},jp&units=metric&APPID=#{API_KEY}"
      _res = @http.request req
      @res = JSON.parse(_res.body)
    end

end


返り値はこんな感じ。
もっと値は受け取れますが、必要そうなものだけピックアップしてます。

=>
[ {:time=>1583226000, # UNIXタイムで返る。 Timeクラスのatメソッドで、datetimeに変換できる。 Time.at(1583226000) => 2020-03-03 18:00:00 +0900
  :temp=>12.22, # 気温
  :feels_like=>8.54, # 体感気温
  :temp_min=>12.22, # 最低気温
  :temp_max=>12.22, # 最高気温
  :pressure=>1020, # 気圧
  :humidity=>65, # 湿度
  :main_weather=>"Clouds", # メインの天気
  :description=>"overcast clouds", # 説明
  :clouds=>100, # 雲の濃度
  :wind=>3.9}, # 風速
]

あとはcontrollerのアクション内で @weathersOpenWeatherMapClient.instance.search('Tokyo') 的な変数を定義してviewに渡してやればOKかと思います。



libファイルの読み込み方法は
qiita.com

上記の記事が参考になるかと思います。