ンンンパ

ふとしです

移転しました

AWS Lambdaで使う関数をローカルでテストするサーバーを建てる。

AWS Lambda + AWS Api Gateway + AWS DynamoDBでなにかをつくることにはまっています。

AWS Lambdaで使う関数自体はmochaなどでテストできますが、実際にブラウザから叩くテストをローカルでしたいと思いました。

そこでNode.jsで簡単なサーバーを建てます。

まず、AWS Lambdaはこのような関数です

"use strict";

const Workbook = require('./src/workbook').Workbook;
const Marker = require('./src/marker').Marker;

exports.handler = (data, context, callback) => {
  let workbook = new Workbook(data);
  let marker = new Marker(workbook, data);

  marker.mark((result) => {
    if (result) {
      context.succeed({result: 'correct', data: marker.explain()});
    } else {
      context.fail(JSON.stringify({result: 'incorrect', data: marker.explain()}));
    }
  });
};

WorkbookMarkerがDynamoDBにアクセスします。環境変数を使って、内部で参照ファイルを切りかえ、ローカルのDynamoDBにアクセスするようにしているので、そちらは大丈夫です。

テストサーバーです

サーバー側の言語をまともに触ったのはNode.jsがはじめてで、はじめて書いたのもこのような簡易なサーバーでした。非常に懐かしいですね。

import * as http from 'http';
import * as qs from 'querystring';
import * as marker from '../marker/index';

http.createServer(function (req, res) {
  if (req.method !== 'POST' && req.method !== 'OPTIONS') {
    res.end('');
    return;
  }

  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Methods', 'POST');
  res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type');
  res.setHeader('Content-Type', 'application/json');

  if(req.method === 'OPTIONS'){
    res.end(``);
    return;
  }

  let body = '';
  req
    .on('data', (data) => {
      console.log('data', data);
      body += data
    })
    .on('end', () => {
      qs.parse(body);

      marker.handler(JSON.parse(body), {
        succeed: (data)=> {
          res.end(JSON.stringify(data));
        },
        fail: (stringifiedData)=> {
          res.end(stringifiedData);
        }
      });
    });

}).listen(process.env.PORT || 3080, '127.0.0.1');

OPTIONSによるプリフライト対応処理を追加しました。(昨日は素のPOSTだけ飛んでたのにな……)

まずPOSTOPTIONS以外はハネます。

つぎに、データの整形ですね。こういうのは自動でやってくれないんでしょうか。

marker.handlerがAWS Lambdaでの関数本体なので、そこへデータを投げます。

context.succeescontext.failで関数を終了するつくりなので、そこに関数を妖異します。その関数呼び出しをもってJSONレスポンスを作成します。

実際のfailでは{errorMessage: 引数}というレスポンスになりますが、これはAWS Api Gatewayで整形します。ので、それを踏まえたJSONとします。

$ TEST=true babel-node test-server/index.js

テストできた

これでブラウザ側からSuperagentなどを使いPOSTすると、実際のレスポンスが得られるようになりました。

クリッククリックで行うテストがはかどりますね。

`Regexp`などActiveRecordでメソッドが用意されていない標準SQLの演算子を清く正しく使う。

清く正しくとは、生SQL文字列を書かない程度の意味です。

下の方の長いやつはRails 4までの話です

REGEXPRails 5もしくはArel 7からmatches_regexpとしてメソッドが用意されました。

Writer.where(Writer.arel_table[:email].matches_regexp('.*@gmail.com'))

# SELECT "writers".* FROM "writers" WHERE ("writers"."email" ~ '.*@gmail.com')

Rails 5になってなお下のような書き方をしていると殴られる可能性があります。

このようにどんどん便利になっていってるので、たまに知識更新するのも大切ですね。

たとえばRegexpwhereする

グーグル先生にたずねるとStack Overflowが出てきて以下のようなコードが出てきます。

Writer.where("email REGEXP ?", '.*@gmail.com')

# SELECT "writers".* FROM "writers" WHERE (email ~ '.*@gmail.com')

動きますが、せっかくのORMですから、ORMらしく書きましょう。

だいたいの演算子や句はArelに用意されている

ドキュメントやグーグル先生より、ソース内を検索したほうが早いのですが、大体の演算子のビルダーが用意されています。

REGEXPなら以下のように書けます。

Writer
  .where(
    Arel::Nodes::Regexp.new(
      Writer.arel_table[:email],
      Arel::Nodes::Casted.new('.*@gmail.com', Writer.arel_table[:email])
    )
  )

# SELECT "writers".* FROM "writers" WHERE "writers"."email" ~ '.*@gmail.com'

WHERE句のカラム指定にテーブル名が入りました。

より具体的な記述になっていますが、テーブル名が欲しくない場合もあります('AS'した場合とか)。

ActiveRecord#arel_tableで得られるArel::Tableはその名の通りテーブルを前提とした値しか返してくれませんので、自分で組みたてる必要があります。

Writer
  .where(
    Arel::Nodes::Regexp.new(
      Arel.sql(Writer.connection.quote_column_name(:email)),
      Arel::Nodes::Quoted.new('.*@gmail.com')
    )
  )

# SELECT "writers".* FROM "writers" WHERE "email" ~ '.*@gmail.com'

カラムのクォーティングのルールはデータベースによって違うので、quote_column_nameで万全を期すと良いでしょう。

値は値用にクォーティングします。

WHERE句の()が欲しい場合はさらにこうします。

Writer
 .where(
    Arel::Nodes::Grouping.new(
      Arel::Nodes::Regexp.new(
        Arel.sql(Writer.connection.quote_column_name(:email)),
        Arel::Nodes::Quoted.new('.*@gmail.com')
      )
    )
  )

# SELECT "writers".* FROM "writers" WHERE ("email" ~ '.*@gmail.com')

大変な騒ぎになってきましたね。

Arelのルール

Arelは安全性を維持するために、生のStringは受けつけてくれず、なんらかのArelクラスでラップする必要があります。

ところでArel.sqlArel::Nodes::SqlLiteral.newのショートカットで、これは何の評価も加工もされず、SQLに渡ります。

カジュアルに使うと生SQLをいじくりまわしているのと大して変わらないので気をつけましょう。

長い

記述が長くなってうれしみが少ないですか?

最初はウッとなりましたが、自分で文字列を組みたてるのと比べると、これが別に気にならなくなったりします。

フシギですね。

AWS Lambdaを使って、ブラウザ側とサーバー側で同じバリデーションをするということをやった。

日記です。今日の日記コードはこれ。

github.com

qiitaにはさすがにもうAWS Lambdaでフォームなんていう記事は山盛りあったのでこっちで。

日記

Node.jsはサーバーとして動かせるので、ブラウザ側とサーバー側で同じスクリプトを用いることが可能です。(アイソモーフィック?)

しかし動かすとなるとサーバーを用意して〜、など色々と難関がありました。その難関を軽く解消してくれたのがAWS Lambdaであることはみなさんご存知のとおりですね。

バリデーションの準備

まずバリデーターライブラリを用意します。

これにはvalidatorを使いました。

github.com

これをラップしたRecordというクラスをつくり、以下のようなコンフィグ(config.jsに入っています)をあたえ、validateメソッドで確認できるようにしました。

exports.config = {
  attributes: ['name', 'email', 'age', 'gender'],
  validation: {
    name: {
      isLength: {min: 1, max: 20, message: '1-20文字で入力してください'}
    },
    email: {
      isEmail: {message: 'メールアドレスを入力してください'}
    },
    age: {
      isNumeric: {message: '数字を入力してください'}
    },
    gender: {
      isIn: ['female', 'male', 'other']
    }
  }
};

ブラウザ側バリデーション

あとはこのバリデーターを、ブラウザ側で使えるようにします。

ライブラリやコンフィグファイルが別ファイルになっているので、毎度おなじみのbrowserifyで連結します。

const Record = require('../lib/src/record').Record;
const config = require('../lib/src/config').config;
const record = new Record(config);

const Validator = window.Validator = {};

Validator.validate = (data) => {
  if (record.assign(data).validate()) {
    return {result: 'success', data: record.parameters};
  } else {
    return {result: 'failure', errors: record.errors};
  }
};

あとはこのバリデーションをサーバーに送信する前に行い、validであれば送信します。

サーバー側バリデーション

送信されてくるデータは全く安全でないのは常識ですから、サーバー側でもバリデーションする必要があります。

ブラウザ側とほぼ同じコードですが、AWS LambdaではNode.jsが動いていますから、browserifyは必要ありません。

今回はバリデーションの成否で終了していますが、そこらへんにdynamoDBやメール送信の処理を書けばいいんじゃないでしょうか。

"use strict";

const Record = require('./src/record').Record;
const config = require('./src/config').config;

exports.handler = (event, context, callback) => {
  let record = new Record(config);

  if (record.assign(event).validate()) {
    context.succeed({result: 'success', data: record.parameters});
  } else {
    context.fail(JSON.stringify({result: 'failure', errors: record.errors}));
  }
};

エラー時の戻り値がブラウザ側と違ってしまうのが気になるところですが(failの引数は{"errorMessage": 引数}JSONとして飛んでいく)、これはAWS Api Gateway側でfailureをキャッチして、同じ形にします。

.*"result":"failure".*で失敗ステータスコードに振り、本文マッピングテンプレートでエラー部を取り出したものをレスポンスとします。

$input.path('$.errorMessage')

ブラウザ側送信時処理

送信せずとも同じバリデーションが行えるわけですから、送信前にバリデーションし、invalidであればdeployErrorsというメソッドが、各項目のinputの上や下にエラーを表示します。

もし何らかの都合でinvalidなデータが送信されても、サーバー側でinvalid判定されて、同じエラーが返ってきてdeployErrorsというメソッドが、各項目のinputの上や下にエラーを表示します。

    function send($form, $thanks) {
      var data = traceValue($form);

      var validated = Validator.validate(data);
      if(validated.result == 'failure'){
        deployErrors($form, validated.errors);
        return;
      }

      disablizeButton($form, true);
      superagent
              .post(endPoint)
              .set('Accept', 'application/json')
              .send(data)
              .end(function (err, res) {
                if (err) {
                  deployErrors($form, res.body.errors);
                  disablizeButton($form, false);
                } else {
                  $form.hide();
                  $thanks.show();
                }
              });
    }

わりと長年の夢であった、サーバーとクライアントで同じコードでバリデーションするというのが、比較的簡単にできてしまいました。しかも課金の心配が結構軽い。

フォーム作成というこまごまとした仕事は、前職前前職ともによくあって、cgiの、メンテされてるんだがされてないんだかわからないスクリプトを使用していました。

Rails大好きなわたしもさすがに一個の問い合わせフォームにRailsというわけにもいかなくて難儀していましたが、これならヤバイ脆弱性も自分の責任のうちで作成できるのでいいかもしれませんね。

HerokuにSymbolic Link入りを投げるとき気をつけること

リソースを再利用しようとしました。

そこでディレクトリに対してln -sで作成しますが、例えば同ディレクトリ内の何がしかへ張る場合、

$ ln -s development staging

とする必要があり

$ ln -s ./development staging

これだと辿れなくなります。

セキュリティかなにかの都合でスラッシュアクセスがブロックされているのでは。同じ理由で上には登れないんじゃないかな(試してない)。

レールズにプルリクエストがマージされてハッピーだった

ので、思いつく限りのSNSやブログに書いています。

ここにも書きます。

どれ

以下の記事のやつを作ってる時に「バグかしら?」と思ってモンキーパッチで回避してて、せっかくだからとpull requestを飛ばしてみたら忘れた頃にマージされたという話です。

qiita.com

Rails5.0.0がつい先日リリースされましたが、その時には自分でも忘れていて、ほん今日、なんか通知が出てるなと思って見てみたらマージされていました。

無職してた甲斐があった

仕事をしないで自分で思うがままにコードを書きつづけていたところで出会ったバグ的なものでしたから、無職でなかったら出会わなかった可能性があります。

これがここのコードが原因で起こってる、っていうのも結局pメソッドとクリッククリックで追いつめて見つけたみたいなもんですから、よっぽど時間に余裕がないと無理だったでしょう。

無職時代のいい思い出になったな〜〜と言いたいところですが、まだ就職先は決まっておりません。

大変ですね。

まぁとにかく嬉しい

好きなフレームワークに貢献できたということで、本当に嬉しいですね。

Discourseにみる権限管理。

権限管理に興味があって、ちょろちょろ読んでいます。

qiita.com

Spreeを読む前はDiscourseを読んでいました。Discourseでは権限管理にこれといったGemは用いず、独自の権限管理機能を実装しています。

Guardianというクラスです。

ただの感想みたいになったのでこっちのブログにメモとして残します。

Guardian

用法はCancancanと大きくかわるところはありません。

まずcurrent_userに類するものでGuardianのインスタンスを作成します。

app/controllers/application_controller.rb

def guardian
  @guardian ||= Guardian.new(current_user)
end

そしてそのインスタンスに処理内容を冠したメソッドに処理対象のなんらかのオブジェクを渡すと権限の確認が行われます。

app/controllers/users_controller.rb

guardian.ensure_can_edit_username!(user)

許可された動作ならそのまま処理はすすみ、許可されていない動作であればDiscourse::InvalidAccess例外を発生します。

設定

Spreeにおいて、Cancancanはユーザー単位で権限を設定していました。

Guardianは処理対象のオブジェクトのクラスごとにモジュールを用意します。

lib/guardian/user_guardian.rb

module UserGuardian
  # 略
  def can_edit_username?(user)
    return false if (SiteSetting.sso_overrides_username? && SiteSetting.enable_sso?)
    return true if is_staff?
    return false if SiteSetting.username_change_period <= 0
    is_me?(user) && (user.post_count == 0 || user.created_at > SiteSetting.username_change_period.days.ago)
  end
  # 略
end

そしてGuardianincludeするというシンプルな仕組みです。

lib/guardian.rb

class Guardian
  include EnsureMagic
  include CategoryGuardian
  include PostGuardian
  include TopicGuardian
  include UserGuardian
  include PostRevisionGuardian
  include GroupGuardian
  #略
end

実行

前述の例のように接頭辞としてensure_をつけた場合、設定されたメソッドがありません。

app/controllers/users_controller.rb

guardian.ensure_can_edit_username!(user)

そこでEnsureMagicというmoduleに実装されたmethod_missingがあらためてメソッドを探索し、その結果をうけてtrueを返すか例外を発生します。

感想

最初は単一のクラスで、その後どんどん増改築した結果、基本となるGuardianにもわりとコードがありつつ、includeするmoduleにもコードがもりもりあるという感じになっています。

それを除けば、method_missingから定義済みメソッドを探索して実行、権限を確認するという動作は、メソッドにより権限をドバっと設定するCancancanより好ましさを感じます。

自分でも

引数でわたされるオブジェクトのクラスを見て該当のGuardianを検索してインスタンス化、そこからメソッドを探索して権限確認という実装を自分でも書いてみましたが、なかなかいい塩梅でした。

github.com

でも多分仕事でつかうならCancancanを使いますね。

rails-erdでRails Engineベースのアプリケーション(Spree、Solidusなど)のER図を得る。

たっぷりとしたコードを含むRails Engineは上から読んでいくにはつらいものです。

いくつかのgemに分割されているSpreeのようなアプリケーションを読むにはインストールして起動しつつ、その上で読んでいくのが楽ですが、手がかりがないのはつらい。ということてrails-erdを使います。しかし、

Engine以下のmodelsは読んでくれない

という問題があるのでそれに対処します。

Engine以下のmodels以下のファイルをapp/models以下にコピー

これでrails-erdのスコープに入ります。

Solidusを読みたいので、Solidusをインストールしたうえでコピーします。

$ cp -R ./vendor/bundle/ruby/2.3.0/gems/solidus*/app/models/* app/models/

rails-erdインストール

graphvizぐらいはすでに入っているかもしれませんがインストール。

$ sudo apt-get install graphviz
group :development do
  gem "rails-erd"
end
$ bundle install

書きだし

$ bundle exec erd

でかい

でかい

f:id:mmmpa:20160615061941j:plain

ちなみにネームスペース、クラス名は特に汚染されないのでコピーしたままでもアプリケーションは動きます。