developmentでridgepole applyしたときにtest DBもつくる

Rails標準のMigrationを使う場合はdevelopmentのDBをアップデートしたらschema.rbが更新されてspec/rails_helper.rbとかに書いてある

ActiveRecord::Migration.maintain_test_schema!

っていうのでrspec実行前にtest DBをmigrationしてくれる。

ridgepoleを使っている場合はschema.rb使わないので上記の行を消してtest DBにschemaの変更を反映するのにdevelopmentとは別途applyしてたんだけど、面倒なのでridgepoleのapply taskをこんな感じにしてみた。

namespace :ridgepole do
  desc 'Apply schema definition'
  task :apply do
    sh 'ridgepole', '--config', 'config/database.yml', '--env', ENV.fetch('RAILS_ENV', 'development'), '--apply', '--file', 'db/Schemafile.rb'

    unless Rails.env.production?
      Rake::Task['db:schema:dump'].invoke
      Rake::Task['db:test:prepare'].invoke
      Rails.root.join('db/schema.rb').delete
    end
  end
end

ridgepole applyするとtest DBも一緒に作られて便利。

ruby 2.5で変更されたtoplevel constant lookupの挙動

こういうコードがあったとして

class Foo; end
class Bar; end
p Foo::Bar #=> Bar

ruby 2.4まではwarning: toplevel constant Bar referenced by Foo::Bar を出しつつFoo::BarBarを返す。

これはrubyの定数探索が継承関係を遡って探すという仕様と、トップレベルで定義された定数がObjectに所属する、という仕様によるもの。わかりやすく書くとこういう感じ。

class Foo < Object; end
Object::Bar = 1
p Foo::Bar #=> 1

Foo::BarとしたときFooにはBarがないので、次にFooの継承関係を辿って親クラスのObjectからBarを探す。するとObject::Barが見つかるのでObject::Barを返す。

2.5だとこれがエラーになるように変更された。

class Foo; end
class Bar; end
p Foo::Bar #=> NameError: uninitialized constant Foo::Bar

ref: https://bugs.ruby-lang.org/issues/11547

ngrepでリクエストだけ出力する

ngrepで3000番ポートに対する通信を見る場合こんな感じで見れるけど

$ sudo ngrep -W byline -q -d any port 3000

レスポンスはいらないのでリクエストだけみたい、という場合はdst portにすればよい。

$ sudo ngrep -W byline -q -d any '' dst port 3000

'' を入れないとdstがmatchingのキーワードに認識されてしまった。

レスポンスだけみたい場合はsrcにする。

$ sudo ngrep -W byline -q -d any '' src port 3000

RailsのAPI onlyでOmniAuthがエラる

RailsAPI onlyのアプリケーションだとセッションが無効化されているのでOmniAuthを使おうとするとOmniAuth::NoSessionErrorで落ちる。

READMEに書いてあった。 https://github.com/omniauth/omniauth/blob/650943c16de33c7cdf16708b74766e95ad369610/README.md#integrating-omniauth-into-your-rails-api

config/application.rbとかにこんな感じの設定を足せばよいらしい。

config.session_store :cookie_store, key: '_interslice_session'
config.middleware.use ActionDispatch::Cookies # Required for all session management
config.middleware.use ActionDispatch::Session::CookieStore, config.session_options

OmniAuthがセッションを必要とする理由は、ログイン前のURLとかparamsを保存してcallbackフェーズで復元できるようにしているためっぽいので、これらが必要ない場合はセッション無効でも動くようにしたいなと一瞬思った。

が、omniauth-auth2はStateというパラメータを保存するのにセッションを使っていて、これはOAuthプロバイダへのリクエストパラメータにstateを渡すことでcallbackにもそのstateを渡してくれ、その値をアプリケーション側でセッションなりに保存しておいて検証することでCSRFを防ぐというものらしい。

https://auth0.com/docs/protocols/oauth2/oauth-state

この検証機構も無効にすることはできるけど、そこまでしてセッションを無効にするモチベーションもないので諦めてAPI onlyでもセッションを有効にすることにした。

頑張らないeslint

自分で頑張って真心込めて設定書いてたけどメンテ不可能なので頑張らないことにした。

  • フォーマット系はprettierに委ねる
    • printWidthだけ120にして後は委ねる
  • 基本設定はrecommendedに委ねる
    • eslint:recommendedは控えめな感じでよい
  • 本当にほしいやつだけ追加で設定する
    • eqeqeqprefer-constだけにした
    • eqeqeqは他の言語と行き来してるとたまに間違えるから、prefer-constconst使うという意思表示
    • 他にも便利な設定いっぱいあるし今後も増えるんだろうけど頑張らない

こんな感じです。

env:
  browser: true
  es6: true
  node: true
  mocha: true

parserOptions:
  sourceType: module
  ecmaFeatures:
    jsx: true

plugins:
  - react

extends:
  - eslint:recommended
  - plugin:react/recommended
  - prettier
  - prettier/react

rules:
  eqeqeq: [error, allow-null]
  prefer-const: error

TypeScriptのLint

JavaScriptもTypeScriptも混じっているプロジェクトで、Lintのルールをなるべく共通化して運用したいというモチベーションがある。

JavaScriptのLinterはESLintを使いたい。TypeScriptのLinterはTSLintがデファクトだが、TSLintはESLintと比べて実装されているルールがだいぶ少なかったり、同じルールでも名前が違ったりする。

解決するアプローチは以下の二つがある。

typescript-eslint-parser

https://github.com/eslint/typescript-eslint-parser

ESLintのパーサーにTypeScriptを使うことでTypeScriptのLintをESLintで行う。JSもTSも同じ設定でいけるので最高便利。なんだけどREADMEに

Important: This parser is not fully compatible with all ESLint rules and plugins. Some rules will improperly mark source code as failing or not find problems where it should.

とあるように対応が不完全なところもあるのが難点。

現状で一番致命的なのは未定義の変数を検出するno-undefを有効にしていると、以下の箇所でエラーになるというバグもの。

class Foo {
  a: string; //=> error  'a' is not defined  no-undef

  constructor() {
    this.a = "a";
  }
}

ワークアラウンドな対応として、TypeScriptのときだけno-undefoffにすればいい。未定義な変数はTypeScriptのコンパイラで検出できるし。

overrides:
  files: ['**/*.ts', '**/*.tsx']
  parser: typescript-eslint-parser
  rules:
    no-undef: off

こういう感じで。

また、TypeScript specificなルールを使うには以下のようなプラグインがある。 https://github.com/nzakas/eslint-plugin-typescript

tslint-eslint-rules

https://github.com/buzinas/tslint-eslint-rules

もう一つがTSLintをESLintに寄せるためのモジュール。ESLintにあってTSLintにないルールを補完する*1。方向性としてはよいんだけど、現時点では対応していないルールもかなり多い。あと、TSLint本体のルールがESLintのルールと名前が違う問題は解決されない。

どっちを使うか

状況にもよると思うけどtypescript-eslint-parserを使ってESLintに寄せるようにした。ESLintとTSLintを併用しないで済むし、ESLintの豊富なルールも使える(使うかどうかは置いといて)ので、多少バグっていることを差し引いてもこちらのアプローチのほうが運用が楽そうだった。

*1:なんでTSLint本体にコントリビュートしないのかよくわからない

BdashというBIツールをリリースしました

BdashというアプリケーションをElectronで作りました。

bdash-app/bdash: A simple business intelligence application.

以下からダウンロードしてインストールできます(現状まだMac版だけ)。

https://github.com/bdash-app/bdash/releases

ざっくりとこんな感じのことができる。

  • SQLを書いて保存&実行できる
  • 結果を元にグラフを書ける
  • gistで共有できる
  • 現状で対応しているデータソースはMySQLPostgreSQL(Redshift含む)、BigQuery

仕事でRedshiftを使って分析SQLを書くことが増えて、手元ではJupyter Notebookを使ってたんだけど、SQL書いてグラフを書くだけの用途には若干オーバースペックでもうちょっと簡単にできるといいなと思ったのがきっかけで去年から作り始めたのがようやく形になったので1.0.0をリリースした。

社内でしばらくベータ版を使ってもらっていたので、まともに使えるぐらいのクオリティにはなっていると思う。(ただしBigQueryは1.0.0リリース直前に実装してまだ使い込まれてないので完成度は低いかもしれない)

利用例

例えば僕が運用しているAdventarの、年ごとのエントリ数と、実際にエントリした人が記事(URL)を投稿したかどうかの割合を出してみる。

こんな感じでクエリを書いて実行できて

f:id:hokaccha:20170207221708p:plain

フォームちょいちょいといじるとグラフが書ける。年々エントリ数は増えているものの記事の投稿率が下がっているのがわかる。改善しよう。

f:id:hokaccha:20170207221712p:plain

そして結果をこんな感じでgistにワンクリックで共有できる。

https://gist.github.com/hokaccha/e128e1c3a68527ebf2c50d5e95a089b1

自画自賛だけどこのgist共有が本当に便利。

まだまだ実装したい機能はたくさんあってこれからガシガシ開発していくので興味がある人は使ってみてフィードバックもらえると嬉しいです。