RSpec を 6 倍速くしてカオスな CI を正常化した話

サービスエンジニアリング本部の山本です。

この記事は Enigmo Advent Calendar 2019 の 18 日目の記事です。

普段はフロントエンド中心の開発をしていますが、たまに DX(Developer Experience) 的なことにも手を出しています。 今回はそんな DX のお話です。

やばい CI

f:id:hi_yamamoto:20191217182336p:plain

エニグモが運営している BUYMARuby on Rails アプリケーションとして動いており、自動テストフレームワークとして RSpec を採用しています。
CIツールとしては Jenkins を採用していましたが、1 年以上の期間、常に Fail しているというエニグモのようなイケてるウェブ企業としてはあるまじき状態が続いていました。
Jenkins は素晴らしいソフトウェアですが、当時動いていた Jenkins のバージョンは 1 系かつオンプレミスサーバーで動いていたこともあり色々と問題を抱えていました。
また、エニグモでは Git ホスティングサービスとして Gitlab を採用していて、ちょうど社内では Gitlab CI を使っていこうという流れもあったため RSpec も Gitlab CI に移行することにしました。

Jenkins 時代の課題

  • ローカルと CI で実行結果が違う
    • ローカルだと通ってたのに CI だと通らない
  • 実行するだけで 2 時間くらいかかっていた
    • 並列実行したいがサーバーのリソースの問題で難しい
  • ブランチごとに実行出来ないため、マージしてみないと CI 上の結果がわからない
    • ブランチごとに実行しようとするとサーバーのリソース不足になる
    • ローカルですべてのスペックを実行するのは非現実的なため関係ありそうな箇所だけローカルでテストしたらマージしてしまう

# jenkins という slack チャンネルがありましたが、失敗しているのが当たり前になってしまっていたため誰も気にしておらず、失敗通知が虚しく響き渡っていました。(割れ窓理論)

どうやって解決していったか

ローカルと CI で実行結果を同じにしたい!

まず、ローカルと CI での実行結果が違う問題について。
これは単純に Docker を使うことにしました。 BUYMA の開発環境はもともと Vagrant を使っていて Docker 化はされていませんでした。 今回はローカルの開発環境全部を Docker 化するのではなく、一旦テスト実行環境だけを Docker 化して CI 上でも同じイメージを利用する方針としました。

Gitlab CI にはジョブを実行する Gitlab Runner というツールを利用します。 Gitlab Runner の実行方式である Executor は Shell 、 Docker などの複数のなかから選択することができますが、今回は後述の実行時間短縮を実現するために並列で実行することを考えて Docker を選択しました。
Docker 上で Docker を動かす Docker in Docker という方式があり、それ専用のイメージも用意されているのでそちらを採用しました。 詳しくはこちら

各ブランチのMR ( Merge Request )ごとに実行したい!

Jenkins 時代は開発用メインブランチである develop ブランチにマージされた時だけテストが走るようになっていたため、 MR をマージして初めてテスト結果がわかるという状態でした。
この場合マージした後に失敗したことに気づき修正する、という流れになりますが、テスト時間が長いこともあり失敗していることに気づかず放置されてしまいます。
マージする前にテストを実行する、という当たり前のことが出来ていない状態だったのです。

これを解決するための Gitlab CI の設定はかんたんで、 .gitlab-ci.yml にこのように記述するだけです。

only: 
 - merge_requests

問題はリソースです。
Gitlab CI への移行当初、 Gitlab Runner サーバー はオンプレミスのサーバーだったため、複数の MR が同時に作成され RSpec のジョブがトリガーされた場合リソースが足りなくなってしまう懸念がありました。 そこで Gitlab Runner サーバー を AWS へ移行して Gitlab Runner の AutoScaling 機能を利用しました。
AutoScaling については こちらの記事が参考になります。

f:id:hi_yamamoto:20191217153040p:plain
移行前の構成


f:id:hi_yamamoto:20191217153141p:plain
移行後の構成

この構成の設計や移行作業は全て弊社インフラエンジニアである夏目さんによるものです。ありがとうございました!!

実行速度を速くしたい!

さて、ブランチごとに実行する環境は整いました。
しかし全てのテストが完了するまで2時間ほどかかってしまうという最大の問題が解決されていません。
MR を作成してマージするぞ!っと思ってからテストが終わるまで 2 時間は流石に待ってられません。
RSpec を速くする方法としては、テストコード内で無駄にレコードを作らないなど色々プラクティスがあると思いますが、数百ファイルのテストファイルを全て見て修正するというのは現実的ではありませんでした。

そこで RSpec の並列化という方法で解決することを考えました。
元々 CI 上でのテストは他のテストファイルの影響を無くすため、通常の rspec コマンドではなくそれぞれのテストファイルを独立して実行するスクリプトを利用していました。

#!/bin/bash

# 全てのスペックファイルを単独で実行するためのスクリプト。

fail_count=0

spec_array=()
while IFS=  read -r -d $'\0' spec; do
    spec_array+=("$spec")
done < <(find spec/ -name '*_spec.rb' -print0)

spec_count=${#spec_array[@]}
echo "Running $spec_count spec files"

export SKIP_CODE_COVERAGE=1

for (( i=0; i<$spec_count; i++ )); do
  echo -e "\n#########################################################################"
  echo -e "\nRunning spec $((i+1)) of $spec_count"
  if ! bin/rspec "${spec_array[$i]}"; then
    ((fail_count++))
  fi
  export SKIP_DATABASE_SEEDING=1
done

echo -e "\n$spec_count test files, failures in $fail_count files\n"

[[ $fail_count -eq 0 ]]

このスクリプトと Gitlab CI の parallel オプションを利用して並列化を実現しました。

まず、 .gitlab-ci.yml のジョブに parallel フィールドを追加します。 今回は 10 個のジョブを並列で実行することにしました。

parallel: 10

この parallel フィールドを追加すると、 ジョブの環境変数として CI_NODE_TOTALCI_NODE_INDEX という環境変数がセットされます。 CI_NODE_TOTAL には parallel で指定した値、 CI_NODE_INDEX にはそのジョブのインデックスがセットされるので、この 2 つの環境変数から実行するファイルを算出します。 並列化後のスクリプトはこちらです。

#!/bin/bash

# 全てのスペックファイルを単独で実行するためのスクリプト。

fail_count=0

spec_array=()
while IFS=  read -r -d $'\0' spec; do
    spec_array+=("$spec")
done < <(find spec/ -name '*_spec.rb' -print0)

spec_count=${#spec_array[@]}
echo "CI_NODE_TOTAL is $CI_NODE_TOTAL"
echo "CI_NODE_INDEX is $CI_NODE_INDEX"
count=$((spec_count / CI_NODE_TOTAL))
start_index=$((count * (CI_NODE_INDEX - 1)))

if [[ $CI_NODE_INDEX -eq $CI_NODE_TOTAL ]]; then
  count=$((spec_count - start_index))
fi

echo "Running $count spec files"
echo "Start at $start_index"

export SKIP_CODE_COVERAGE=1

for (( i=$start_index; i<$start_index+$count; i++ )); do
  echo -e "\n#########################################################################"
  echo -e "\nRunning spec $((i+1 - start_index))(total: $(($i+1))) of $count(total: $spec_count)"
  if ! bin/rspec  "${spec_array[$i]}"; then
    ((fail_count++))
  fi

 
  export SKIP_DATABASE_SEEDING=1
done

echo -e "\n$count test files, failures in $fail_count files\n"

[[ $fail_count -eq 0 ]]

テストを 10 分割することにより 2 時間かかっていたテストが約 20 分で完了するようになりました!

残念ながらジョブ実行用のスポットインスタンスの起動にある程度時間がかかるため、 10 分割で 10 倍速くなるというわけにはいきませんでした。
また、これ以上並列数を増やしても速くなることもありませんでした。

結果

  • ローカル環境と CI 環境での差異がなくなったため、ローカルで成功したのに CI では落ちることがなくなった
  • MR ごとに実行できるようになったためテストが成功した状態でマージでき、メインブランチは常にクリアな状態が保てるようになった (たまに落ちてることもある)
  • テストが 20 分で終わるようになった

所感

長らく不満に思っていた、常に Fail している CI から脱却することができました。
人によるとは思いますが、個人的には CI が落ちて放置されているというような状態に精神が乱されるタイプなので心の平穏を取り戻すことができました。

上記の作業をする前に落ちまくって放置されているテストを全て直すという作業があったのですが正直そこが一番きつかったです。


エニグモではカオスな開発環境を一緒に正常化していきたいエンジニアを募集中です!

hrmos.co

k9sで快適なk8sライフを送ろう!

はじめに

みなさん、こんにちは。 主にBUYMAの検索周り、時々機械学習なエンジニアの伊藤です。

今年もあっという間の1年でしたね。
振り返ってみると多くのことを学ばせてもらい、また成長させてもらった1年でした。
その中でもk8sがやはり自分の中では中心だった気がします。
なので今回はk8sネタで今年を締めくくりたいと思います。

k9sとは?

k9sとはk8s上のリソースを監視、管理するためのCLIツールです。
k8sCLIといえばkubectlデファクトですが、そのI/Fをよりリッチに使いやすくしたのがこのツールです。

f:id:pma1013:20191210142021p:plain
公式のページはこちら

読み方はけぃないんずだと思います(たぶん)
個人的になぜ犬のロゴなんだろうというのがまず引っかかったので軽く調べたところ、 canine=犬のヌメロニム表記であるk9と、kubernetesk8sが表記的に近いから犬なんだなと勝手に納得することにしました。

使ってみる

では実際にk9sをインストールして使ってみましょう。
LinuxOSXWindows上で動作するようですが、今回はMac上でインストールして動かしてみたいと思います。

  • インストール

    brew install derailed/k9s/k9s

  • 起動

    k9s

    表示される情報については大きく2つの領域に分かれており、上部に現在使用しているkubectlのcontext、クラスタ名や、コマンドヘルプ等が固定表示され、
    枠内にpodの一覧など、指定した情報が表示されるようになっています。
    デフォルトではkubectl get podで取得できる項目相当の一覧が見れますが、CPU、メモリ使用率なども表示されます。

  • 基本操作について
    基本的なところさえ抑えておけば、あとは直感的に操作が可能です。

    • コマンドモード
      :でコマンドモードになります。
      続けて、k8sのリソース名(alias名でも可)を指定することでその一覧が表示されます。
      指定するリソース名を忘れた場合はCtrl-aで確認できます。
      例えば、deploymentの一覧を確認したい場合は:dpと入力してEnterでdeploymentの一覧が表示できます。
      Escでコマンドモードを抜けます。
      f:id:pma1013:20191213105637g:plain

    • フィルター
      /のあとにフィルターを指定することで絞り込みが可能です。
      kubectlと同じうように-l app=XXXXで任意のラベルで絞りこみが可能ですし、
      任意の文字列だけ入力してもその文字列がpod名やnode名に含まれれば絞り込みが可能です。
      Escでフィルターを解除します。
      f:id:pma1013:20191213121414g:plain

    • ソート
      Shiftと特定のキーで項目毎のソートが可能です
      各項目とキーのマッピング?で確認できます。
      昇順/降順の切り替えはShift-iで行います。

実践的な使いどころ

基本操作が分かったところで、よりk9sの便利さが伝わるように下記のような場面を想定した操作をしてみます。

  • あるdeploymentリソースに異常があり、該当のdeployment及びその配下のpodのdescribeを確認して、ログを確認する
    f:id:pma1013:20191213121938g:plain

  • podへのport-forward f:id:pma1013:20191213122419g:plain

所感

kubectlに慣れている人や、aliasやpeco等でコマンド操作自体をゴリゴリにチューニングしている場合は、 k9sの使い始めは慣れない分、「そこまで便利か?」っていう感じを受けるかもしれません。 自分は最終的にどちらかに移行するというよりは、下記のような使い分けをすることで落ちついています。

  • 単発の確認はkubectl
    例えば今のpod一覧を知りたい時など、特定の情報だけ知りたい場合はわざわざk9sを起動する方が手間なので、kubectlだけで済ませています。

  • 解析作業や複数のコンテキストを跨る作業
    こういう場合は圧倒的にk9sが便利です。
    上でも記載していますが、クラスタ側に何か異常があった場合には、様々なリソースの情報を見ていくことになると思います。 そういった場合にはk9s上であれば、 わざわざコマンドを打つことなく少ないキー操作で確認が可能です。
    また、複数のkubectlコンテキスト内のリソースを同時にみるのはkubectlではできませんが、
    k9sでは別ターミナルで起動してコンテキストを切り替えることで、
    ターミナルの切り替えだけで複数コンテキストの操作や参照が可能になります。

最後に

k8sとその周辺エコシステムは進化が激しく、日々新しい機能やツールが出てきている印象です。 キャッチアップが大変ですが、今後も日々の運用やシステムの安定性に繋がるようなものは時間を見つけて試していきたいと思います。

株式会社エニグモ 正社員の求人一覧

hrmos.co

入社して1年目の振り返り

こんにちは、サービスエンジニアリング本部の穴澤です。

今年の1月に入社して丁度1年になります。良いタイミングなので1年を振り返ってみたいと思います。 これから転職をしてみようかなと考えている人、新しい事に挑戦したいと思っている人に読んでもらえると幸いです。

1-3月

年始に入社。前日は眠れませんでした。6年働いた前職から転職し、人間関係はゼロから。使っている技術、サービスも違う。大丈夫かなー3ヶ月後生きてるかな?と思っていました。入社後は環境構築しつつ、その後運用業務を上司から少しずつ分けてもらい日々の業務全体をみるようになりました。エンジニアとしての経験を買ってもらって入社したものの、プログラミング言語での経験は主にPHPだったのでエニグモでのRailsを使った開発に戸惑うところも未だにあります。このタイミングでチームに新しく入ってきた方も居たので受け入れ時に必要な情報やドキュメントを徐々にまとめて自分がサービスの説明や開発フローを説明できるものを準備しました。

この頃はまだ前職の人たちとの飲み会が多く、ホームシック的?な気持ちになったのを覚えています。ああ、転職したんだなあ、という気持ちでした。 EM見習いとして入社したので、入社したその月からメンバーの方と一人ひとり話をする時間を持ち始めました。ただし全然みんながなにやってるのかわからん。という感じでした。

4-6月

「社内での情報共有サービスを移行しよう」という動きがありチームを横断した形でエンジニア数名、関係者含めて小さいチームが発足しました。 私自身が刷新に前向きだったのでやってみようと思い手をあげることになりました。実際には部署をまたいで新ツールの使い方のレクチャや 既存のデータの移行スケジュールの共有、他の部署の人たちへのヒアリングや、もっている課題などを集約しデータ移設などは山本さん におんぶにだっこでした。 このチームのおかげで普段接点のない部署の方々とお話する機会があったかなと思います。名前もだいぶ覚えてきました。

夏場に大きな施策が控えており、メンテナンスが必要になったため自分が担当になりました。過去メンテのドキュメントを探しマニュアルや手順書の整備をはじめました。 メンテにはインフラエンジニアや他のサーバサイドエンジニアの方もいましたが「入社して半年でメンテ要員わたしで大丈夫かな?」と思いながらテスト環境で手順書の確認やわからないことを少しずつ潰していきました。

7-9月

サーバーメンテナンスは深夜に実行する事となり、夜に関係者が出社しメンテナンスを終えたのは朝でした。そのまま朦朧としたまま会社を出、ファストフード店でおいしい朝ビールをいただきました!色々と学びが多い月でした。メンテをしたことでちょっとシステムの全体構成やアーキテクチャ全般が「わかった気」になったのを覚えています。

開発業務で機能をゴリゴリ書いている身ではないので案件の調整や小さい運用、リリースの調整やドキュメントの整備、問い合わせやMTG、ちょっとした相談・・・。こういう事に忙殺されているとたまに「わたしエンジニアって言っていいんだっけ?」と悲観的になることもあるのです。なんだか何もしてないな、EMで入ってきてるけどわたし大丈夫かな。と不安になったこともありました。チームの成果を伸ばしたい、組織に貢献したい。なので技術力そのものだけじゃないところで活躍したい。自分の達成チケット数=成果ではないという事を上司に確認したこともありました。その際、「そういう前提であなたを迎えているのですから(大丈夫ですよ)」と言ってくれたのはいまでも覚えています。メンテも経験したことで、この辺りからはっきりと自分の役割を理解してきました。技術的な学習や実際の開発から完全に手をひいたわけではないけれどそこに自分の価値があるわけではない。というのが1年たった気持ちです。

10-12月

リリースに関わる改善をしたいと日頃思っていたので、夏過ぎからまた有志のチームで細々と情報の整理と改善の策を練るところからはじめています。ちょっと不安ですが自分にできることを探していこうと思います。

また春先から頑張ってきた採用活動が実を結び、新しい人が入社してくれました!人材育成も過去の経験をそこそこに活かしながら、今わたしの目の前にいる人の顔をみて、自分ができる「お手伝いをしていこう」と思った今日このごろです。

最後に

異なる環境に飛び込むことは色々考える事も多いし悩みも多いです。それでも今年楽しかったな、ここはやりきったな。という達成感はあります。 それと、自分がどんどん自社のサービス・支える人を好きになってると感じます。携われる領域やできることが些細なことでも一つふえると嬉しいものです。 環境や周りの人を強制的に変えることで視野が広まったり自分が少しずつ変わる変化を、来年も楽しんでいこうと思います。

株式会社エニグモ 正社員の求人一覧

hrmos.co

第4回 Google Cloud INSIDE Digital に登壇しました。「ショッピングサイトにおける商品画像への Could Vision API の活用」

少しさかのぼりますが、11/1に「ここでしか聞けない AI 、機械学習サービスの活用例」をテーマに開催された第4回 Google Cloud INSIDE Digital に 弊社エンジニアの木村が登壇しました。

f:id:enigmo7:20191212163632j:plain

弊社で運営しておりますショッピングサイトバイマへ出品された商品画像に規約違反となるような不適切なものが含まれる場合があるため、その検出に Cloud Vision API を活用した事例についてお話ししました。

発表資料を公開します。

Vision APIの他、エニグモでは DWHとしての BigQuery をはじめ、それに伴ったデータ収集・分析・活用のインフラとしてGCPAWSを活用しています。それらを活用してデータの収集、整備、機械学習モデルの運用、サービスへの組み込みなど大規模データを扱ってみたいエンジニアを募集しています。

↓↓↓

株式会社エニグモ 正社員の求人一覧

hrmos.co

dry-validation (1.3) で Form Object を実装する

dry-validation (1.3) で Form Object を実装する

こんにちは、エンジニアの齊藤です。 この記事は Enigmo Advent Calendar 2019 の12日目の記事です。

本日は、バリデーションロジックの開発で Form Object の設計を支える dry-validation について書きたいと思います。

Form Object について

ユーザー向けのウェブアプリケーションの実装で必ずといって発生するのが、インプット値のバリデーション処理です。 ウェブフォームだけでなく API のリクエストパラメーターや CSV ファイルのコンテンツ等そのケースは様々です。

Enigmo で開発しているウェブアプリケーションの多くは Ruby on Rails で実装されています。Rails アプリケーションでは、Model にバリデーションを実装するのが一般的ですが、モデルのアトリビュートを逸脱したこのようなケースではそれぞれ専用のクラスを実装する必要があります。

Form Object 自体は有名な Rails の Fat モデルを改善する 7 Patterns to Refactor Fat ActiveRecord Models という記事のなかで紹介された設計テクニックの一つです。

Before dry-validation

今回紹介する dry-validation 以外の実装では先程の 7 Patterns... にも登場する virtusActiveModel::Validations を使って実装しています。 virtus でアトリビュートを定義して、通常のモデル同様にバリデーションルールを実装します。
実行方法も通常の ActiveModel のインスタンスと差異がないので、バリデーション専用のモデルを実装しているのと同じです。

app/forms/product_form.rb

class ProductForm
  include Virtus.model
  include ActiveModel::Model

  attribute :product_name, String
  attribute :reference_number, Integer
  attribute :images, Array[Hash]

  validates :product_name, presence: true
  validates :reference_number, presence: true
end

実行結果

[11] pry(main)> form = ProductForm.new(product_name: '', reference_number: '1000'); form.valid?; form.errors
=> #<ActiveModel::Errors:0x000055839f9bacd0
 @base=
  #<ProductForm:0x000055839f9bb090
   @errors=#<ActiveModel::Errors:0x000055839f9bacd0 ...>,
   @images=[],
   @product_name="",
   @reference_number=1000,
   @validation_context=nil>,
 @details={:product_name=>[{:error=>:blank}]},
 @messages={:product_name=>["を入力してください。"]}>

dry-validation 1.0 released

virtus 自体は少し前に開発が止まっていたので、作者が新たに開発している dry-validation が気になっていたのですが、全く違う設計のライブラリのため導入までにはいたりませんでした。 バージョン 1.0.0 (執筆時 1.3.1) がリリースされたのをきっかけにこちらに乗り換えてみることにしました。

dry-validation の良いところはスキーマを定義する DSL の記述がわかりやすく、ドメインロジックのバリデーションルールと明確に処理を分離できる点だと思います。

ここからは dry-validation を使った Form Object の実装方法について説明していきます。

Configurations

dry-validation 自体の共通の設定はベースクラスを用意して定義します。 エラーメッセージの出力時のロケールの設定、共通ルールのマクロやカスタムデータタイプをここで定義します。
例えば、ここに定義している StrippedString は、ありがちな前後のスペースを取り除くのと、空白であった場合に nil に強制する文字列のハンドリングのために利用するデータタイプです。

class ApplicationContract < Dry::Validation::Contract
  config.messages.default_locale = :ja
  config.messages.backend = :i18n
  config.messages.load_paths = [
    Rails.root.join('config/locales/ja.yml'),
    Rails.root.join('config/locales/en.yml')
  ]

  module Types
    include Dry::Types()

    StrippedString = Types::String.constructor { |str| str.strip.presence }
  end
end

Schemas

Schemas は dry-validation の重要な機能でデータをプリプロセスしてバリデーションするための DSL です。 例として以下のようなパラメーターを Schemas を定義してバリデーションします。
params は HTTP パラメーター向けのスキーマ定義のメソッドです。 スキーマ定義の DSL はほぼ記述そのままなので理解しやすいのではないでしょうか? バリデーションの実行結果からクレンジングされた入力値とエラーがそれぞれ .values.errors で取得できます。

{
  "product_name": " Rustic Paper Gloves",
  "reference_number": "59142",
  "images": [
    { "url": "http://ruelarmstrong.com/howard", "caption": "" },
    { "url": "boyer.name/rhett_wunsch", "caption": "" }
  ],
  "description": "                 "
}

app/contracts/product_contract.rb

class ProductContract < ApplicationContract
  params do
    required(:product_name).filled(Types::StrippedString, max_size?: 200)
    required(:reference_number).filled(:integer)
    optional(:images).value(:array, min_size?: 1).array(:hash) do
      required(:url).filled(:string, format?: URI::DEFAULT_PARSER.make_regexp)
      optional(:caption).maybe(Types::StrippedString, max_size?: 500)
    end
    optional(:description).maybe(Types::StrippedString, max_size?: 1000)
  end
end

実行結果

[7] pry(main)> result = ProductContract.new.call(product_name: "  Rustic Paper Gloves        ", images: [{ url: 'http://ruelarmstrong.com/howard', caption: '' }, { url: 'boyer.name/rhett_wunsch', caption: '' }], reference_number: '59142', description: '          ')

=> #<Dry::Validation::Result{:product_name=>"Rustic Paper Gloves", :reference_number=>59142, :images=>[{:url=>"http://ruelarmstrong.com/howard"}, {:url=>"boyer.name/rhett_wunsch"}], :description=>nil} errors={:images=>{1=>{:url=>["は不 正な値です。"]}}}>
[8] pry(main)> ap result.values.to_h
{
        :product_name => "Rustic Paper Gloves",
    :reference_number => 59142,
              :images => [
        [0] {
                :url => "http://ruelarmstrong.com/howard",
            :caption => nil
        },
        [1] {
                :url => "boyer.name/rhett_wunsch",
            :caption => nil
        }
    ],
         :description => nil
}
=> nil
[9] pry(main)> ap result.errors
{
    :images => {
        1 => {
            :url => [
                [0] "は不正な値です。"
            ]
        }
    }
}
=> nil

Rules

スキーマ定義をクリアしたデータをさらに Rules を使ってドメインロジックのバリデーションを実行できます。ここで扱うデータはスキーマ定義をクリアしているのでロジック自体の実装に集中することが可能です。

ここでは例として単純な reference_number が存在するかをデータベースに問い合わせるドメインロジックと、各 imagesの URL の妥当性を判定するルールを追加しました。

class ProductContract < ApplicationContract
  ...

  rule(:reference_number) do
    next if Product.exists?(reference_number: value)

    key.failure(:invalid)
  end

  rule(:images).each do
    next if ImageChecker.call(value[:url])

    key(key_name << :url).failure(:invalid)
  end
end

実行結果

[88] pry(main)> result = ProductContract.new.call(product_name: "  Rustic Paper Gloves        ", images: [{ url: 'http://ruelarmstrong.com/howard', caption: '' }, { url: 'http://boyer.name/rhett_wunsch', caption: '' }], reference_number: '59143', description: '          '); ap result.errors
  Product Exists? (1.2ms)  SELECT 1 AS one FROM "products" WHERE "products"."reference_number" = $1 LIMIT $2  [["reference_number", "59143"], ["LIMIT", 1]]
{
    :reference_number => [
        [0] "は不正な値です。"
    ],
              :images => {
        0 => {
            :url => [
                [0] "は不正な値です。"
            ]
        },
        1 => {
            :url => [
                [0] "は不正な値です。"
            ]
        }
    }
}
=> nil

Macros

例えば、登録フォームとログインフォーム、ログイン API で ID であるメールアドレスのフォーマットを判定するロジックを共通化したい場合 Macros が利用できます。 マクロのブロックにはパラメーターを使うことも可能です。

app/contracts/application_contract.rb

class ApplicationContract < Dry::Validation::Contract
  ...

  register_macro(:email_format) do
    unless URI::MailTo::EMAIL_REGEXP.match?(value)
      key.failure(:invalid_email_format)
    end
  end
end

app/contracts/login_contract.rb

class LoginContract < ApplicationContract
  params do
    required(:email).filled(Types::StrippedString)
    required(:password).filled(Types::StrippedString)
  end

  rule(:email).validate(:email_format)
end

app/contracts/registration_contract.rb

class RegistrationContract < ApplicationContract
  params do
    required(:email).filled(Types::StrippedString)
    required(:first_name).filled(Types::StrippedString)
    required(:last_name).filled(Types::StrippedString)
  end

  rule(:email).validate(:email_format)
end

実行結果

[15] pry(main)> result = RegistrationContract.new.call(email: 'foo'); ap result.errors
{
    :first_name => [
        [0] "を入力してください。"
    ],
     :last_name => [
        [0] "を入力してください。"
    ],
         :email => [
        [0] "は不正なメールアドレスです。"
    ]
}
=> nil
[16] pry(main)> result = LoginContract.new.call(email: 'bar', passsword: nil); ap result.errors
{
    :password => [
        [0] "を入力してください。"
    ],
       :email => [
        [0] "は不正なメールアドレスです。"
    ]
}
=> nil

Rspec

ユニットテストでは、シンプルにバリデーションメッセージの期待値をテストしています。

spec/contracts/product_contract_spec.rb

describe ProductContract do
  subject { result.errors.to_h }

  let(:contract) { described_class.new }
  let(:result) { contract.call(params) }

  describe 'params' do
    describe 'product_name' do
      subject { super()[:product_name] }

      describe '.filled?' do
        context 'filled with valid name' do
          let(:params) { { product_name: Faker::Commerce.product_name } }
          it { is_expected.to eq(nil) }
        end

        context 'not filled' do
          context 'without key' do
            let(:params) { {} }
            it { is_expected.to eq(['を入力してください。']) }
          end

          context 'nil' do
            let(:params) { { product_name: nil } }
            it { is_expected.to eq(['を入力してください。']) }
          end
        end
      end
    end
  end
end

実行結果

ProductContract
  params
    product_name
      .filled?
        filled with valid name
          is expected to eq nil
        not filled
          without key
            is expected to eq ["を入力してください。"]
          nil
            is expected to eq ["を入力してください。"]

Finished in 0.57222 seconds (files took 9.7 seconds to load)
3 examples, 0 failures

Conclusion

最近の開発で以前 virtus ベースで実装したかなり大規模な入力値のバリデーションを dry-validation を使って実装したのですが、コントラクト自体のテストも実装しやすく柔軟に構成できるため、今後も利用していきたいと思います。 ここでは紹介しきれない機能がまだまだありますし dry-rb 自体興味深いプロジェクトなので、ぜひドキュメンテーションソースを参照していただければと思います。


参考

株式会社エニグモ 正社員の求人一覧

hrmos.co

App Maker 触ってみた

こんにちは、データマーケター?の嘉松です。

データ活用推進室というチーム(1人なう)で、MAツールの導入から運用といったCRMの推進と、データ活用の推進や業務を効率化するためのツール作成など、現場に近い立ち位置で業務を行っています。

背景

社内のG Suiteが、Businessにアップグレードされたことで、App Makerを無料で利用することができるようになった。

目的

無料で利用できるApp Makerを使わないのはもったいない。
もったいないおばけが出る前に、

  • App Maker、どんな感じで使えるのか?
  • そのためには、どんなスキルセットが必要か? 

を、そこはかとなく捉えること。

Google App Maker とは

App MakerはGoogleが提供するとっても簡単にWebアプリケーションを開発することができるプラットフォーム。

ドラッグ&ドロップなどの操作で、Web上にフォームやテーブルなどのインターフェース、データベースを作ることで、Webアプリケーションとして公開することができる。

「非エンジニア職のビジネスマンにとってITスキルを磨く超ステキなきっかけの一つになるだろう」との意見も。

ExcelAccessVBAの代替手段になり得る。

App MakerのベースはGoogle Apps Script(GAS)

App Makerの特徴

HTML&CSSの知識が不要

ドラッグ&ドロップでページとUIを簡単に作れる。

Googleサービスとの連携

App MakerはベースはGASなので、様々なGoogleサービスとガッツリ連携可能。

データベースの作成が簡単

本格的なデータベースをクリックベースでGoogleドライブまたはGCP内に作成できる。

無料で強固なサーバーに設置できる

G Suite Business以上のユーザーであれば追加料金なしで利用可能。

サーバーの準備は全く不要。

App Makerの入り口

App Maker HOME

developers.google.com

Google Developers

appmaker.google.com

アプリ、作ってみた

作ったアプリ

ランニング計算機

1キロのペース(○分○秒)を入力すると、フルマラソンの完走タイムを計算してくれるアプリ。

f:id:enigmo7:20191209170733p:plain

ランニング計算機

作った感想

  • テキストボックス、ボタン、ラベルなどの作成は、パーツ(Widgets)から作りたいパーツを選んで配置するだけ(ドラックアンドドロップ)なので、スーパー簡単。

パーツの一覧

f:id:enigmo7:20191209170936p:plain

  • 処理、このアプリの場合はボタンを押した時の計算処理は、下のようなウィンドウ内のエディタにGASで記載する。このエディタの使いっぷりが良くない。ウィンドウ、小さいし。 

f:id:enigmo7:20191209171034p:plain

  • UIの見栄え(デザイン)は、基本的には最小限の変更しかできない。ガッツリ見栄えを良くしたいなら、CSSを使えばできる。

まとめ

  • G Suite Businessで無料で使えるApp Maker、どんなものかと触ってみた。
  • UIの作成は、パーツの一覧からドラック&ドロップするだけなので超簡単。
  • 実行したい処理はGASで記述が必要なので、普通にプログラミングが必要。
  • UIの見栄えのカスタマイズは最小限。その範囲だと結構ショボい。CSSでリッチにできるけど、そうすると本末転倒か?
  • App Makerは、G SuiteのBasicからBusinessにアップグレードすることで利用できる数少ないサービス。
  • Googleさんもきっと力を入れて、より一層使いやすくなってくると思う。
  • ということで、今後の進化に期待!!

株式会社エニグモ 正社員の求人一覧

hrmos.co

3年放置してた_variables.scssを整理したよー。

こんにちは、デザイナーの本田 です。 この記事はEnigmo Advent Calendar 2019 の2日目の記事です。

今日はエニグモが運営しているファッションメディアSTYLE HAUS の色管理方法について紹介していきます。

放置されていた_variables.scss

Sassファイルの中で重要な_variables.scssファイル。このファイルにデザインを管理する様々な変数を定義すれば、変数の値を編集するだけで全体的なデザインの更新ができるようになります。 ただ、最初に設計した後、放置されやすいのも事実です。

STYLE HAUS の開発はECサイトBUYMA の開発の空いた時間にちょこっと進めるスタイルなので、デザインの全体を見返すタイミングがなかなか作れず、3年ほど設計を見返せずにいました。

f:id:enigmo777:20191126205423p:plain
variable.scssを3年放置した図

また、変数も最初のうちは問題なかったのですが、繰り返し修正や変更があり、名前がわかりにくいものに。 メインカラーがピンクのサービスなのもあり、ピンクだけで9種類も増えていました。

$pink:         #d04589;
$pink-dark:    #c82676;
$pink-dark2:   #c3196d;
$light-pink:   #f5edf1;
$light-pink2:  #f2e2ea;
$light-pink3:  #f3dee8;
$light-pink4:  #f8f3f6;
$white-pink:   #FCF7FA;
$pink-neue:    #d77eaa;

ぱっと見では$light-pink4$white-pink の判別が・・・ とりあえず、色だけでも変数化すればサイト全体のデザイン見直しに役立つと思ったので、取り組みました。

やったこと

1. サイト上で使われている色の洗い出し

_variables.scssを使わない代わりに、カラーコードはベタ書きで運用していたため、実際に使われている色の洗い出しをしました。 全部で20色以上・・・あったのでこのあたりでストップ。

2. カラーパレットの作成

被ってしまう色がメインのピンクと無彩色だったので、2色のみ100〜900まで作成しました。

// Gray scheme
$color__white:     #ffffff;
$color__black:     #000000;

$color__gray--100: #f6f6f6;
$color__gray--200: #efefef;
$color__gray--300: #dddddd;
$color__gray--400: #bebebe;
$color__gray--500: #969696;
$color__gray--600: #888888;
$color__gray--700: #555555;
$color__gray--800: #3a3a3a;
$color__gray--900: #212121;

// Pink scheme
$color__pink--main:#dc4991;
$color__pink--100: #fcf7fa;
$color__pink--200: #faf6f8;
$color__pink--300: #f5edf1;
$color__pink--400: #f3dee8;
$color__pink--500: #feb4d5;
$color__pink--600: #d77eaa;
$color__pink--700: $color__pink--main;
$color__pink--800: #c3196d;

f:id:enigmo777:20191127100704p:plain
grayは綺麗なカラーパレットに。ピンクは無理やり作ったので若干バランスが...

3. 命名規則の統一

$red だけだと、どの赤なのかわからなくなるので、他の色も揃えました。 色自体はそのままですが、ソースコードを見たときに「カテゴリ指定の色が使われているんだな」ってことが伝わるようになると思います。

// Before
$beauty:     $pink;
$fashion:    $red;
$lifestyle:  #c2a20b;
$celeb:      $pink;
$children:   #d4669c;
$men:        #5a72c0;
$baby_kids:  #f07468;
// After
$category__beauty:   $color__pink--main;
$category__fashion:  #e82152;
$category__lifestyle:#c2a20b;
$category__children: #d4669c;
$category__men:      #5a72c0;
$category__baby_kids:#f07468;
$category__horoscope:#8765c5;

また、メインのカラーはいろんな場所で使いまわしているので、最初に使った変数を他でも利用すると関連性がぱっと見でわかるようになります。

$color__pink--main: #dc4991;
$color__pink--700:  $color__pink--main;
$category__beauty:  $color__pink--main;

4. 残す変数名を決める

2,で作ったカラーパレットは、全体のデザインを知っている場合は使いやすいのですが、アサインされたばかりの人やエンジニアからしたらどの色を使えばいいのかわかりづらいので、 わかりやすい変数名はそのまま残す方向にしました。

// Others
$pink:              $color__pink--main;
$pink-dark:         $color__pink--800;
$light-pink:        $color__pink--300;
$white-pink:        $color__pink--200;
$red:               $category__fashion;
$green:             #89d045;
$blue:              #4589d0;
$white:             $color--white;
$back-black:        $color__gray--800;
$light-gray:        rgba(239,239,239, 0.3);
$default:           $color__gray--800;
$text:              $color__gray--800;
$hr:                $color__gray--300;
$dark:              $color__gray--800;
$super-light:       $color__gray--200;

これをファイルの下に残すことで、「テキストってどの黒だっけ?800?900?」「ボーダーの色って何色だっけ?」と悩むことが少なくなるかなと思います。

5. ちょっとずつリファクタリング

あとはプロジェクト毎に少しずつリファクタリング・・・! ここのモチベーションを保つのは、結構むずかしいかなと思っていたのですが、 ソースレビューの際に、カラーコードをベタ書きしている箇所はレビュアーから指摘してもらえたので、毎回モチベーション高く取り組むことができました。

結果

_variables.scssに指定した色変数は38個あったのですが、全体を見直した結果49個になりました。 同じ色を重複して書いているので、実際の色自体は28と変化なしでした。

感想

プロジェクトにアサインされて、結構最初に取り組んだのですが サービス全体のデザイン理解にとても役立ちました。

最初にもうこれ以上色は増やさないぞ!!という気持ちで作ったので、UIを考えるときに、色について悩むことがなくなりました。 1回も使ってない色ももちろんあります。($color__pink--400 とか)

おそらく今後も使わないですが、この変数名を残すことによって$color__pink--300-2 とかめちゃくちゃな命名規則が発生しなくなると思うので残しています。

ソースコードからデザインデータまで徹底して整備すると、色だけじゃなくフォントや余白も統一したくなって、デザインデータもかなり整理することができました。

f:id:enigmo777:20191126215502p:plain
シンボル化されたデザインパーツ(Sketch)

参考URL

Sassの色管理方法について考えてみた | 株式会社インディバル

変更に強いSassの色管理プラクティス - Qiita

株式会社エニグモ 正社員の求人一覧

hrmos.co