open_sessionで特異メソッドを定義するときの注意

open_sessionで特異メソッドを定義したら、変数の持ち主に注意しましょうというお話。


次のように定義したとします。

def user
  open_session do |u|
    def u.access_to_home
      get '/home/index'
    end
  end
end


まず、これはエラーなく実行できます。

get '/home/index'
assert_response :success


ところが、これはエラーしてしまいます。

@user = user
@user.access_to_home
assert_response :success   #=> NoMethodError: undefined method `success?' for nil:NilClass

assert_response内で呼びだそうとした@responseは、テストを実行するコントローラがもつ変数です。
一方、@user.access_to_homeのレスポンスを受ける変数は@userセッションがもっているもので、コントローラから見た@responseとは別物なのですね。(あるいは、コントローラ自体がひとつのセッションだと見做すこともできる)


@user.access_to_homeのレスポンスを見るには、userの定義内で呼び出してやる必要があります。

def user
  open_session do |u|
    def u.access_to_home
      get '/home/index'
      assert_response :success
    end
  end
end


今度はエラーなく実行できました。

@user = user
@user.access_to_home


@user.responseと明示してやる方法もあります。

def user
  open_session do |u|
    def u.access_to_home
      get '/home/index'
    end
  end
end
@user = user
@user.access_to_home
@user.response.success?

# こちらも実行可
@user.assert_response :success


もしくは、instance_evalとか。

def user
  open_session do |u|
    def u.access_to_home(&block)
      get '/home/index'

      instance_eval(&block) if block
    end
  end
end
@user = user
@user.access_to_home do
  assert_response :success
end

vimで編集中のrubyテストを実行できるプラグイン

vimrubyのテストを編集しているとき、テストメソッドごとに結果を確認しながら書き進めたいことなどないでしょうか。
ruby test/unit/hoge.rb -n test_fuga でテストメソッド単位の実行はできるけど、テストケース名が長かったりするとかなり面倒・・・


ところが、そんな悩みを解消してくれる子に出会いました。
GitHub - janx/vim-rubytest: Run ruby test in vim
vimで編集中のテストをその場で実行できるプラグインです。

install
$ git clone git://github.com/janx/vim-rubytest.git

で落として、 plugin/rubytest.vim を ~/.vim/ に配置するだけです。
(READMEには 'Copy all files to your ~/.vim directory.' とありますが、こちらの方法ではできませんでした)
ちなみにvimのバージョンは7.2でした。

usage

vimでの編集中にコマンドを打つとテストが実行されます。

  • t (\t) すると、カーソルの乗っているテストケースを実行
  • T (\T) すると、そのスクリプト内の全テストケースを実行


注意点としては、

  • 実行中はvimのウィンドウからシェルに切り替わってしまう
  • テストケース単体実行と言っても、テストケース名の部分一致で見ている
    • test_fuga と test_fugafuga があって test_fuga 上で t した場合、test_fuga と test_fugafuga の両者が実行される
    • 要は、 ruby test/unit/hoge.rb -n /test_fuga/ が実行されるということ


ていうか、 -n /test_xxx/ で部分一致指定ができたのですね・・・知らなんだ・・・後方一致もできましたし。

完全一致にしたい場合は、~/.vimrc でカスタマイズしてやりましょう。

let g:rubytest_cmd_testcase = "ruby %p -n %c"


さよなら -n オプション!

    • -


はてブコメント見てたら、quickrun.vimというのもあるんですね。
スクリプト単位の実行なら、シェルに切り替わらないこちらが便利かもです。


参考:http://vim-users.jp/2009/05/hack7/


コメントありがとうございます!

    • -


さらに追記。
本日ずっとコケ続けていたscreenのインストールに成功したので、コードとテスト結果が同画面で閲覧でき便利になりました。

    • -


2011/08/22
さらにさらに追記。


railsのテストでrubytestを使っていたところ、次のエラーが出てテストが実行できない不具合に遭遇しました。

:!echo 'ruby integration/hogehoge_test.rb' && cd test && ruby integration/hogehoge_test.rb
ruby integration/hogehoge_test.rb
/usr/local/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:31:in `gem_original_require': no such file to load -- lib/piyopiyo (MissingSourceFile)
   ・
   ・
   ・

テストコード中で require 'lib/piyopiyo' しているところで、そんなのいないよと怒られる。でも実際にはいる。
rakeや、コンソールからの ruby test/integration/hogehoge_test.rb 実行なら落ちない。


よく見ると、rubytestが cd test してからテストを実行しているのがわかります。
rubytestのソースを確認すると、確かに cd しているところがありました。


自分の環境では cd しなくても不都合はないため、次のようにパッチをあてました。(リビジョンはこちら)

[admin@localhost plugin]$ diff ~/.vim/plugin/rubytest.vim.orig ~/.vim/plugin/rubytest.vim
64,67c64,69
<     if @% =~ '^test'
<       let cmd = substitute(cmd, '%p', s:EscapeBackSlash(strpart(@%,5)), '')
<       exe "!echo '" . cmd . "' && cd test && " . cmd
<     else
---
> 
>     " fix bug 'no such file to load', due to missing load path.
>     "if @% =~ '^test'
>     "  let cmd = substitute(cmd, '%p', s:EscapeBackSlash(strpart(@%,5)), '')
>     "  exe "!echo '" . cmd . "' && cd test && " . cmd
>     "else
70c72
<     end
---
>     "end


なお、ソースが新しければこの不具合は起こらないと思います。
おそらくこれ以前のものが該当するのでは、と。


アップデートしましょという話かもしれないガ━━(;゚Д゚)━━ン!!

In App Purchase (Apple アプリ内課金システム) のしくみ

iOS SDK Hacksに載っていた内容のまとめ。

販売コンテンツの幅

  • Non-Consumable (非消費型) プロダクト
    • 一度しか購入できない
    • この型に限り、Apple側で購入情報が管理される
    • 再度購入しようとした場合、無料でダウンロードできる
    • 別の端末にもリストアできる
      • 無償版から有償版へのアップグレード
      • 電子書籍等のダウンロード etc.
  • Consumable (消費型) プロダクト
    • Apple側では購入情報は管理されない
    • ダウンロードが消えてしまった場合は再購入
    • 別の端末に移せない
      • ゲームの武器のようなバーチャルアイテム etc.
  • Subcription (継続課金)
    • Apple側では購入情報は管理されない
    • ダウンロードが消えてしまった場合は再購入
    • 別の端末に移せない
    • 次回請求のタイミングはアプリデベロッパ側が管理 (自動決済はされない)
      • 月額・年額等、一定期間ごとの課金商品

特徴

  • 2009/10より、本体が無料のアプリでも利用可能になった
    • 有料・無料どちらにしても、Appleと有料販売契約(Paid Contract)を結ぶ必要がある
  • 課金プロダクトの価格帯は本体アプリと同じ
    • デベロッパが自由に設定できない
    • 無料にすることもできない
  • 決済にはトランザクションがかかる
    • 課金の失敗や二重課金が起こらないよう、一連の課金フローが保護されるということ
  • アプリを起動しない限り課金できない
    • 継続課金の場合も自動決済はされない。単に複数回の課金が可能というだけ
  • 課金用プロダクトもAppleの審査が必要
    • プロダクトごとのIDをふるため
    • プロダクトのデータ自体は審査しない。説明文とスクリーンショットが対象

Appleガイドライン

  • 課金プロダクトはデジタルコンテンツ、オンラインサービスに限る
    • 現実の物販やサービスは不可ということ
  • すべてのユーザが平等に購入権を得られなければならない
  • ポルノ、不快な文章や中傷、ギャンブル等の不適切なコンテンツは販売できない
    • ただし、ギャンブルは以下の条件を満たせば可
      • シミュレーションものであること
      • 仮想通貨が外部のコンテンツや別アプリの通貨との引換に対応していないこと

Appleの決済を使わないアプリ内課金

  • 原則できないと考えたほうがよい(App Store Review Guidelinesにもそのような記述がある)が、以下は可能 ※2010/10月現在
    • WEBサイトでの決済
      • iOS SDK Hacksには、UIWebView (アプリ内ブラウザ) を立ち上げてサイトを表示すれば決済できるように書いてあるが、2010/12月現在はmobile safariに切り替える方式でないとリジェクトされるという話も聞く。
        事例として載っているebi Readerも、現在はmobile safariに切り替わる仕様になっている模様。(iPhone版のみ確認)
    • PayPalを通しての決済

演算子の優先順位

演算子の優先順位を意識していないと予想外の結果になることがあるので注意しましょうというお話。

  • 和集合を代入したい場合
irb(main):001:0> hoge = nil || 'hoge'
=> "hoge"
irb(main):002:0> hoge = (nil || 'hoge')
=> "hoge"

結果は同じ。

  • 和集合を配列に追加したい場合
irb(main):003:0> hoge = []
=> []
irb(main):004:0> hoge << (nil || 'hoge')
=> ["hoge"]
irb(main):005:0> hoge = []
=> []
irb(main):006:0> hoge << nil || 'hoge'
=> [nil]
より << が強いので、カッコの有無で結果が変わってしまいます。


確かに、リファレンスを見ると = と << の優先順位が全然違いますね。

画像の非同期ロード

前回の内容とも関わるのですが、UITableViewCellの画像をWEBからロードする場合も、スクロールが非常に重くなることがあります。
前回同様、配列を使って回避できるかなと思ったのですが、うまく表示させることができませんでした。(もし方法があればご教示頂けると幸いです!)
そこで、画像は非同期通信でロードし、ロードできたものから順次表示されるようにしました。

環境
  • iPhoneSDK 3.1.3


方法はこちらのページを参考にさせて頂きました。
非同期通信で画像をロードする方法について - プログラミングノート
大まかな流れとしては、UIImageViewを継承したクラスを作成し、そのクラスに画像データのロードを行わせ、cell.imageViewにaddSubviewする形となります。


ただ、私の環境で試したところ、addSubviewだけでは表示されなかった(cell.imageView.imageが確保されていない感じ)ため、下記のようにデフォルト画像を明示的に指定しました。

// tableView:cellForRowAtIndexPath

UIAsyncImageView *ai = [[UIAsyncImageView alloc] initWithFrame:CGRectMake(0, 0, 50, 50)];
[ai loadImage:@"URL"];
[cell.imageView addSubview:ai];
cell.imageView.image = [UIImage imageNamed:@"NoPhoto.png"];   // デフォルト画像を明示的に指定


また、これだけですと、スクロールしてセルが再利用されたとき、新しい画像がロードされるまで古い画像が残っていて違和感がありました。
そこで、UIAsyncImageView.mにinitWithFrameを定義し、セル再利用時もデフォルト画像にリセットされるようにしました。

- (id)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
	
    if (self) {
	self.image = [UIImage imageNamed:@"NoPhoto.png"];   // セル再利用時にもこのメソッドが呼ばれるため、画像をリセット
    }
	
    return self;
}

JSONによるWEBアプリとiPhoneアプリのAPI連携

RailsアプリケーションでJSONデータを返すAPIを実装し、iPhoneアプリから受信する方法をご紹介します。

環境
  • Rails 1.2.3
  • json 0.4.1 [gem]
  • iPhoneSDK 3.1.3
  • json-framework 2.2.3

iPhoneアプリ

json-framworkというフレームワークを使ってJSONを扱うことができます。

  • json-framework をiPhoneSDKに組み込む
    • dmgをダウンロード・解凍
      • 今回は ~/Library 下に置くことを前提とします。
    • プロジェクト設定
      • 「ビルド」=>「追加SDK」の値に以下を記述します。
$HOME/Library/SDKs/JSON/$(PLATFORM_NAME).sdk
      • 「ビルド」=>「他のリンカフラグ」の値に以下を記述します。
-ObjC -ljson -all_load
  • JSONデータの受けかた
    • SDKをインポート
#import "JSON/JSON.h"
    • 受け皿の実装
// JSONデータのリクエスト
NSURL *jsonURL = [NSURL URLWithString:@"http://0.0.0.0:3000/users/show/1.json"];
NSMutableString *jsonData = [NSMutableString stringWithContentsOfURL:jsonURL
							    encoding:NSUTF8StringEncoding
							       error:nil];

// シングルクォーテーションはJSONでは扱えないため、ダブルクォーテーションに変換
[jsonData replaceOccurrencesOfString:@"'"
		          withString:@"\""
			     options:NSCaseInsensitiveSearch
			       range:NSMakeRange(0,[jsonData length])];

// result にデータを受けて出力
if (jsonData != nil) {
    id result = [jsonData JSONValue];   // JSONValue が外部メソッド
    NSLog(@"%@", result);
}

Railsアプリケーション

  • アクション名.json のリクエストを受けられるようにする
# config/routes.rb
map.connect ':controller/:action.:format'
map.connect ':controller/:action/:id.:format'   # users/show/1 のようにパラメータをパスで扱っている場合
  • アクションからJSONデータを返す
  render :text => @object.to_json
  • データの形式を調整する
    • Rails組み込みの to_json メソッドと json gem の to_json メソッドでは、以下のように挙動が変わります。
$ ruby script/console 
Loading development environment.
>> data = {"id" => 1}
=> {"id"=>1}
>> data.to_json
=> "{id: 1}"   # 属性がダブルクォートされていない
>> require "json"
=> ["JSON"]
>> data.to_json
=> "{\"id\":1}"   # 属性がダブルクォートされている
    • json-framework では属性をダブルクォートした値を扱いたいので、json gem の to_json を使うようにします。
require "json"
 ・・・
render :text => @object.to_json

UITableViewCellのパフォーマンス

UITableView の tableView:cellForRowAtIndexPath:indexPath からデータリクエストを飛ばしたりすると、パフォーマンスが著しく低下することがあります。
私はこちらのAPIの開発中に各セルからJSONデータをリクエストさせていたのですが、スクロールの際に引っかかるような動きがありました。
Rails(WEBアプリ)側のログを確認しながら動かしてみたところ、いちど画面から消えたセルであっても、再び表示されるときに再リクエストを飛ばしていることがわかりました。


ひとつの対処としては、予めデータを配列で取得してしまい、リクエストをその1回に抑えることです。
viewDidLoad 等のタイミングで配列arrayに取得し、tableView:cellForRowAtIndexPath:indexPath 内では [array objectAtIndex:[indexPath row]] として利用します。