RailsでPunditとshoulda-matchersを併用して、Rspecのcustom matchersが被った

Punditとshoulda-matchersにはpermitという同名のカスタムマッチャーがあって、衝突してしまう。

ので、punditのpermitは、spec_helper.rbでのrequire "pundit/rspec"をやめて、punditのカスタムマッチャーのpermitを使いたい個別のspecファイル、例えばuser_policy_spec.rbで、require "pundit/rspec"して解決した。

RailsのResourcesでNestしてshallow: trueしたresourcesをAngularJSの$resourceで扱う

例えばこういう感じのresourcesを定義したとする。

APIの例。newとeditは省いた。

# routes.rb
namespace :api, defaults: { format: :json } do
  resources :reports, shallow: true do
    resources :comments, except: [:new, :edit]
  end
end

定義されるルーティングはこうなる。reportsのは話に関係ないので省略

GET  /api/reports/:report_id/comments(.:format)  api/comments#index {:format=>:json}

POST /api/reports/:report_id/comments(.:format) api/comments#create {:format=>:json}

GET /api/comments/:id(.:format) api/comments#show {:format=>:json}

PATCH   /api/comments/:id(.:format) api/comments#update {:format=>:json}

PUT /api/comments/:id(.:format) api/comments#update {:format=>:json}

DELETE  /api/comments/:id(.:format) api/comments#destroy {:format=>:json}

Railsではよくあるパターンだと思う。

このコメントのresourceをAngularJSの$resourceで使いたい。

$resourceでよく見かけるサンプルだとシンプルなものが多い.

こういったことは出来ないんだろうかと、AngularJSのリファレンスで$resourceの項目を見てみたら、ちゃんとやり方が書いてあった。

url – {string} – action specific url override. The url templating is supported just like for the resource-level urls.

$resourceの第3引数のactionsでurlをoverrideすればよいと。

やってみた。

下記はqueryとかsaveとか自動のやつは無視してRails的なactionを定義

# Comment.coffee
'use strict'

angular.module('myApp')
.factory 'Comment', ($resource) ->
  $resource("/api/comments/:commentId", {commentId: "@id", reportId: "@report_id"}, {
    index: {method: "GET", url: "/api/reports/:reportId/comments", isArray: true}
    create: {method: "POST", url: "/api/reports/:reportId/comments"}
    update: {method: "PATCH",params: {commentId: "@id"}}
    show: { method: "GET",params: {commentId: "@id"}}
    destroy: {method: "DELETE",params: {commentId: "@id"}}
  })

使う例

# sample.coffee
angular.module('myApp')
.controller "SampleCtrl", ($scope, $stateParams, Comment) ->
  $scope.comments = Comment.index(reportId: $stateParams.reportId)

  $scope.comment = new Comment(report_id: $stateParams.reportId)

  $scope.save = ->
    $scope.comment.$create((comment)->
      # do something
 

上記の場合だと、report_idとreportIdでもにょってしまうので、サービス側でもう少しラップしてオブジェクトの引数ではなく、パラメータで渡した方が幸せになれるかもしれない。

RailsでつくるAPIサーバのドキュメントを自動生成してくれるAutodocを使っていて気をつけること

Autodocとはなんぞやという方はこちらの記事を参照。 公式はこちら

で、大変便利なAutodocだけど、注意点が2つ。

まず一点目はRspc3で動かない点。プルリクが上がってるが、取り込まれてない。

なので、forkして使ってる人が結構いる感じ。

2点目、通信が発生しているところでマークダウンが生成されるので、shared_exampleとか使って、そこで実際の通信が発生するようなものは、ドキュメントが全てshared_exampleがあるファイル名で生成されてしまう。

具体的に言うと、例えばこちらの記事のようなヘルパーメソッドを用意して、これでAUTODOC=1でAPIのテストを全て行った場合、shared_exmpleを呼び出している個別のspecファイルではなく、doc/support/api_helper.mdというドキュメントが生成されてしまう。

こうなるのは仕方がない気がするので、autodocを使ってドキュメントを生成する場合は、Autodocで記録する個別のexampleの中でgetやpostを投げる感じで。

Rails4をJSON APIとして構築していてCreateのAPIに関連のID一覧をparameterとしてPOSTしてはまった

前提

1.Railsのwrap_parametersはJSONRailsへ送った時にルート要素を省いてくれる。というか、省いて送ってもよしなにwrapしてくれる。

Railsのwrap_parametersは何をしてくれるのか?

2.has_manyを定義するとrelation_ids,relation_ids=というメソッドが使えるようになる

#group.rb
Class Group
  has_many :groups_users
  has_many :users, through: :groups_users
end
#user.rb
class User
  has_many :groups_users
  has_many :groups, through: :groups_users
end
#irb
group = Group.first
group.user_ids
# =>[1,2,3]
group.user_ids=[2,3,4]

3.Railsのstrong_parametersは許可されたパラメータ以外を取り除く

#groups_controller.rb
class GroupsController < ApplicationController
  def create
    @group = Group.new(group_params)
    # 省略
  end

  private
  def group_params
    params.require(:group).permit(:name, user_ids: [])
  end
end

問題

このgroups#createに対して以下のようなJSONを送付したところ、groupモデルは作成されたが、userとの関連は作成されなかった。

// json
{
  name: "新規グループ",
  user_ids: [1,2,3]
}

user_idsがstrong_parametesに書いたにもかかわらず弾かれる。

実際のparamsを見るとこんな感じ

 Parameters: {"name"=>"新規グループ", "user_ids"=>[1,2,3], "group"=>{"name"=>"新規グループ"}}

調べるとどうもnested_attributes_forも引っかかるようだ。

解決

strong_paramtersのREADMEに書いてあった。

以下のようにrequire(:group)を取り除く

# groups_controller.rb
class GroupsController < ApplicationController
  # 省略

  private
  def group_params
    params.permit(:name, user_ids: [])
  end
end

これで、wrap_parametersもきき、groupとuserの関連も作成される。

Yeomanのgenerator-angularとRailsの組み合わせでの開発環境構築

grunt-connect-proxyを使って、rails server と、grunt serve の2つを叩いてLiveReloadで開発出来るようにしようという話。
あとフロント側をビルドするとRailsのpublicディレクトリに静的ファイルとして配備するように。

RailsのAsset PipelineにAngularJSを載せるんじゃなくて、サーバーサイド(Rails)とクライアントサイド(AngularJS)を分離しての開発の話。

こういう事↓
理想的な Rails, AngularJS 環境の構築 - ボクココ

前提

Railsアプリ作成して、apiのnamespaceでAPIを作成済み、Railsアプリのルートにngappとかディレクトリ作って、yo angular --coffee でフロントエンド部を生成済みとする。*1

作業

% cd ngapp
% npm install -D grunt-connect-proxy
# Gruntfile.coffee

"use strict"

proxySnippet = require('grunt-connect-proxy/lib/utils').proxyRequest
# ↑追加

module.exports = (grunt) ->

  require("load-grunt-tasks")(grunt)

  require("time-grunt")(grunt)

  # Configurable paths for the application
  appConfig =
    app: require("./bower.json").appPath or "app"
    dist: "../public"  # ←修正. railsのpublicをdistに

  # Define the configuration for all the tasks
  grunt.initConfig

  # Project settings
    yeoman: appConfig

# ................................省略.................................

    connect:
      options:
        port: 9000

      # Change this to '0.0.0.0' to access the server from outside.
        hostname: "localhost"
        livereload: 35729

      livereload:
        options:
          open: true
          middleware: (connect) ->
            [
              proxySnippet  #<- 追加
              connect.static(".tmp")
              connect().use("/bower_components", connect.static("./bower_components"))
              connect.static(appConfig.app)
            ]
      proxies: [      #<- proxies: [...] 追加
        context: '/api'
        host: 'localhost'
        port: '3000'
      ]
 
# ................................省略.................................     

  grunt.registerTask "serve", "Compile then start a connect web server", (target) ->
    if target is "dist"
      return grunt.task.run([
        "build"
        "connect:dist:keepalive"
      ])
    grunt.task.run [
      "clean:server"
      "wiredep"
      "concurrent:server"
      "configureProxies"  #<- 追加
      "autoprefixer"
      "connect:livereload"
      "watch"
    ]

# ................................省略.................................   
% bundle exec rails server
% grunt serve

これで、localhost:9000/にアクセスするとフロント側、localhost:9000/apiRailsAPI側に*2
APIとは別に、管理系とかを普通にRailsアプリとして作って同じプロジェクト上にある場合は、Railsのお作法に則り、localhost:3000/下にアクセスでいいと思う。

ビルドは,--forceを忘れずに

grunt build --force

2014/8/23追記

Vagrantの場合↓ Yeomanのgenerator-angularで作ったプロジェクトをVagrant環境で開発する場合の設定 - 仙台 Ruby Vim JavaScript 社長

参考

*1:generator-angular

*2:localhost:9000/apiRailsのrootという意味ではありません

Deviseでnamespace使う場合はdevise_for :users, path: :adminとかしとけという話

メモ。

Devise使用時にroutesでadminとかのnamespaceにdevise_forを入れると,Devise::SessionsController等で authenticate_user! が authenticate_admin_user! とかなってしまう*1

# config/routes.rb
namespace :admin do
  devise_for :users, controllers: {
    sessions: "admin/users/sessions",
    ......
  }
end

devise_for with namespace generate wrong methods · Issue #412 · plataformatec/devise · GitHub

こう書く。

# config/routes.rb
 devise_for :users, path: :admin, controllers: {
    sessions: "admin/users/sessions",
    ......
}

今回は諸事情があってDevise使ってるけど、認証はRails提供のsecure_passwordとか使って、自前で書いたほうがはまらなくていいと思う。大分楽に書けるようになったし。Deviseはカスタマイズしようとすると面倒くさい。

*1:注:カスタムしたコントローラーに任意のアクションを突っ込んでしまっているからです。