Sorcery + Request Specで undefined local variable or method `user_sessions_url'
Sorceryを使ったユーザー認証のあるRailsアプリケーションについて、
Request Specでユーザーログインしている状態としていない状態でテストをしようとしたら
undefined local variable or method
user_sessions_url'`
が出たのでそのあたりの話
Request Specでログインできるようにするためには、
RSpec.configure do |config| config.include Sorcery::TestHelpers::Rails::Request, type: :request end
をspec_helper.rbがrails_helper.rbに書けばいいらしい
これによって login_user(user)
が使えるようになるという怪情報を得た
ソース: Testing Rails · Sorcery/sorcery Wiki · GitHub
(注: 色々まちがっている?更新されていない?のでこのソースはあんまり信用しないほうが良さそう
仕方ないので原典にあたると、
sorcery/request.rb at 8d2d0a59bdeeb207428f9ba3073571f563ed01e6 · Sorcery/sorcery · GitHub
# Defaults - @user, 'secret', 'user_sessions_url' and http_method: POST def login_user(user = nil, password = 'secret', route = nil, http_method = :post)
routeを指定しないと user_sessions_url
が呼ばれて、これがタイトルのエラーを起こしていたらしい
じゃあ適当に
login_user(create(:user), 'password', login_path)
とかしてログインのPOSTの投げ先のパスを指定してやれば解決・・・
ではあるが、このヘルパーメソッドはログイン用のパスに認証情報とパスワードをPOSTしてくれるだけなので、
自前で書いてshared_examplesとかに入れておいたほうが何かと取り回し良さそうだなあとおもいましたまる
ユーザー認証にSorcery使ってるときのFactoryBotでactivation_state: 'active'にできない
表題の通り
ユーザー認証にSorceryを使っていて、(:user_activationを設定していて、)
authenticates_with_sorcery!
したUserモデルのFactoryBotで、
FactoryBot.define do factory :user do ... trait :activated do activation_state { :active } end ... end end
で、 FactoryBot.create(:user, :activated)
しても、
activation_status = "pending"
なUserが生成されてしまう。
Sorceryの実装見ていないけど、どうせUser.create
の挙動がオーバーライドされてるか, after_create
フックあたりで必ず
新規に作成されたUserの activation_state
を "pending"
にする実装があるんだろう。
結局、
trait :activated do after(:create) do |user| user.update!(activation_state: :active) end end
とした。
trait :activated do after(:create) do |user| user.activate! end end
でもいいけど。
AWS Lambdaに割り当てるメモリが少なすぎてスクレイピングに失敗していた
タイトルの通りです。
Lambda + Ruby2.5 + Mechanizeでのスクレイピングが50%ぐらいの確率で失敗していて、
原因がわからず彷徨っていたけどなんとなくメモリ増やしたら落ちなくなったのでメモ
ちなみに128MB -> 1024MBにしました
ユースケース的には、
毎日インスタの自社アカウントのフォロワー数を取得してslackに通知する、というプログラムを毎日定時にCloudWatchEventで動かしていたんですが,
(行儀がわるいのでInstagram Graph APIをつかえというのはナシで・・・)
class Fetcher INSTA_URL = 'https://www.instagram.com/****/?hl=ja'.freeze def initialize @agent = Mechanize.new @agent.user_agent_alias = 'Windows Mozilla' @insta_retry_count = 5 end # @abstract jsonldからもってくる # @return [String] def get_insta response = @agent.get(INSTA_URL) jsonld = response.at('script[type="application/ld+json"]').inner_text JSON.parse(jsonld, object_class: OpenStruct).mainEntityofPage.interactionStatistic.userInteractionCount.to_s rescue => e get_insta2 end # jsからもってくる # @abstract get_instaが失敗したときのためのフォールバック def get_insta2 response = @agent.get INSTA_URL js = response.search('script[type="text/javascript"]').find { |r| r.inner_text.include?('window._sharedData = ') }.inner_text matched = js.match(/"edge_followed_by":{"count":(\d+)}/) matched[1] rescue => e if @insta_retry_count >= 0 @insta_retry_count -= 1 get_insta else e.to_s end end end def lambda_handler(event:, context:) instance = Fetcher.new insta_count = instance.get_insta // 加工してslackに投げる処理 { statusCode: 200, body: JSON.generate('Hello!') } end
で undefined method
[]' for nil:NilClassを吐いていました
エラー箇所的には
matched[1]`のとこですね
元々のget_instaのみで(get_insta2とかいうふざけたメソッドなしで)実行していたときの失敗率と
get_insta2を追加してリトライを入れて実行したときの失敗率がほぼ変わらなかったし、
手元のdocker環境では失敗せず正常に動いていたので、環境特有のナニカ・・・うーん・・メモリかも・・・🤔と思って試しに
1024MB充ててみたら落ちなくなりましたとさ。おしまい。
パターンマッチ中の `..`
mdbookのコードを読んでいて、なんじゃこの記法は、となったコードがあったので、
理解してしまえばなんて無いことですが、しばらく悩んだし戒めのために書きます
簡略化して書くとこんなコードです
enum Enum { A(u8, u8, u8), B } macro_rules! some_macro { ($pat:pat) => { let e = Enum::A(0, 0, 0) match e { $pat => println!("\(^o^)/"), _ => panic!("panic!") } } } fn main() { some_macro!(Enum::A(..)); }
Enum::A(..)
の部分に引っかかりました。
Enum::Aは引数を3つとるのでは・・・?としばらく悩んだのですが、
マクロを展開してしまえば
let e = Enum::A(0, 0, 0) match e { Enum::A(..) => println!("\(^o^)/"), _ => panic!("panic!") }
ということですね。
パターンマッチ中はいちいち Enum::A(_, _, _)
などと書かなくても (..)
で省略できるんですね。
ちゃんとここに書いてありました。
let (.., x) = (0, 0, 0, 0, 0, 0, 0, 0, 1); println!("x: {}", x); // => x: 1 let (y, ..) = (1, 0, 0, 0, 0, 0, 0, 0, 0); println!("y: {}", y); // => y: 1 let (a, .., b) = (1, 0, 0, 0, 0, 0, 0, 0, 2); println!("a: {}, b: {}", a, b); // => a: 1, b: 2
と同じセマンティクスですね。
Rust便利ですね。
Rubyの *args
**args
で配列やハッシュで可変長の引数を取れるのとか、Ruby2.7のパターンマッチの*
と似てますね。似てないか。
ちなみに僕が悩んだ元のコードは
mdBook/summary.rs at a592da33bbf0adbe93d7d47fd016a8af462ed677 · rust-lang-nursery/mdBook · GitHub これです。
mdBookに変なPRを出した
mdBookという、gitbookのrust版みたいなOSSがあります。
マークダウンをつっこむとサイト(やpdfやepubその他レンダラを書けば何でも!)を出力してくれるものです。
主にドキュメントサイトを生成するのに使われていると思います。
Rustの公式ドキュメント系もmdBookでビルドされているものが多く、
たとえば、 Rust By Exampleもそうです。
さて、昨日mdbookのissueを眺めていたら、こんなissueを見つけました
『Rust By Exampleには「Ctrl とEnterを押したらコードが実行される」って書いてるけど、少なくとも僕の環境ではできなかったよ』
って内容です。
mdbookのデフォルトのテーマではそんな機能は提供されていないので、Ctrl + Enterでコードが実行されない、となると問題はRust By Exampleの方にあることになります。
それで、rust-by-exampleのリポジトリを掘っていたら、
たしかにCtrl + Enterでコード実行できるようにする、というコミットがありました。
ただこのコミットをよく見ると、gitbookについての実装なんですね。
というのも、rust by exampleは昔gitbookを使っていて、mdbookの機能が十分になったところでmdbookに乗り換えた、という歴史があるようです。
なので、昔、Ctrl+Enter はgitbookについて実装し、
// or if you prefer to use your keyboard, you can use the "Ctrl + Enter" shortcut
なんて書いて、mdbookに移行するときにこの機能を移すのを忘れてしまったようです。
ということは、正当な解決方法としては、
rust-by-exampleの方へ修正のコミットを出すのが筋でしょうが、
幸いrust-by-exampleではlatestのmdbookのデフォルトのテーマを使っているようです
ということは、mdbookのデフォルトテーマを修正すれば、
自動的にrust-by-exampleが修正されることになりますね。
ということで、
mdbookのデフォルトテーマに、Ctrl + Enterでコードを実行できるようにする修正をしてPRを投げておきました。
無事マージされるでしょうか。
P.S.
マージされました
Zero-SizedなオブジェクトをVecに入れてみる
Zero-Sizedなオブジェクトは、メンバを持たない構造体から作ることができます。
struct Blank; // struct Blank {} と同じ fn main() { println!("{}", std::mem::size_of::<Blank>()); // => 0 }
ちなみに、メンバを持たないenumもサイズは0です
enum BlankEnum; fn main() { println!("{}", std::mem::size_of::<BlankEnum>()); // => 0 }
(が、メンバがないのでenumからZero-Sizedのオブジェクトを作り出すことはできません)
ちなみに②、
上記の通りenumのサイズはメンバの"数"によってきまり、
メンバが1個なら0 byte,
1~256個なら1,
257~多分65536個なら2,
.
.
.
となります(なるはずです)
enum A { Member1, Member2, ... Member256 } enum B { Member1, Member2, ... Member256, Member257 } fn main() { println!("{}", std::mem::size_of::<A>()); // => 1 println!("{}", std::mem::size_of::<B>()); // => 2 }
(一応断っておくと、すべてがデータを持たないenumの場合の話です)
さて話は戻って、
Vec
からなる構造体なので、
ここにZeroSizedなオブジェクトを入れたらどうなるのだろうと、やってみました
ちなみに③、
0サイズのオブジェクトでもアロケーションはされている(と言ってよいのだろうか)ようです
struct Blank; fn main() { let obj_in_vain = Blank{}; println!("{:?}", &obj_in_vain as *const Blank); // => <なんらかのアドレスが印字される> }
さて、今度こそVecにZeroSizedなオブジェクトをいれていきます
struct Blank; fn main() { let mut blank_vec = Vec::<Blank>::new(); blank_vec.push(Blank{}); blank_vec.push(Blank{}); blank_vec.push(Blank{}); println!("length: {}", blank_vec.len()); // length: 3 println!("capacity: {}", blank_vec.capacity()); // capacity: 18446744073709551615 println!("ptr: {:?}", blank_vec.as_ptr()); // ptr: 0x1 }
長さは普通です。
キャパシティは18446744073709551615と std::usize::MAX_VALUEが返ってますね。
これはここの実装によりそうです。
そもそもサイズが0のものは理論上無限に詰め込んでよいはずなので、usizeの最大値が返るのはなんとなくわかる話です。
そしてポインタですが、 0x1
になっています。
空のオブジェクトはアロケーションされても、空のオブジェクトのVecはどこも指さないんですね。
勉強になりました。
ちなみに④、
なぜ 0x0
でなく 0x1
なのかというと、
参照はNonZeroだからのような気がします。
nullポインタ最適化ってやつのためでしょうか。
struct Blank; fn main() { println!("{}", std::mem::size_of::<usize>()); println!("{}", std::mem::size_of::<Option<usize>>()); println!("{}", std::mem::size_of::<&usize>()); println!("{}", std::mem::size_of::<Option<&usize>>()); // なんと増えない }
Rustおもしろいですね。
FlutterでFireRemoteConfigを使ってバージョンアップを強制する
FlutterとRemoteConfigを使って、
アプリをあるバージョン"以上"にする方法を紹介します
(正確には アプリがあるバージョン未満の場合にアップデートを促す方法、です)
まずFlutter側のRemoteConfigの設定を済ませます
以下のようにRemoteConfigの設定もしてしまいます
使うパッケージ
アプリのバージョン取得用 pub.dev
x.x.xの形式のバージョン同士を比較する用 pub.dev
実装
Flutterで実装する場合、iOSとAndroidでバージョンは基本的には同じだと思いますが、
審査待ち等何らかの事情で足並みが揃わない場合のためにiOSとAndroidで処理を分けています
import 'dart:io'; import 'package:package_info/package_info.dart'; import 'package:pub_semver/pub_semver.dart'; import 'package:firebase_remote_config/firebase_remote_config.dart'; Future<bool> _shouldUpdate() async { final packageInfo = await PackageInfo.fromPlatform(); final appVersionStr = packageInfo.version; final appVersion = Version.parse(appVersionStr); // 現在のアプリのバージョン // remoteConfigの初期化 final RemoteConfig remoteConfig = await RemoteConfig.instance; // 何らかの事情でRemoteConfigから最新の値を取ってこれなかった場合のフォールバック final defaultValues = <String, dynamic>{ 'android_required_semver': appVersionStr, 'ios_required_semver': appVersionStr }; await remoteConfig.setDefaults(defaultValues); await remoteConfig.fetch(); // デフォルトで12時間キャッシュされる await remoteConfig.activateFetched(); final remoteConfigAppVersionKey = Platform.isIOS ? 'ios_required_semver' : 'android_required_semver'; // iOSとAndroid以外のデバイスが存在しない世界線での実装 final requiredVersion = Version.parse(remoteConfig.getString(remoteConfigAppVersionKey)); return appVersion.compareTo(requiredVersion).isNegative; }
あとは適所で
if (await _shouldUpdate()) { // アップデートを促すモーダルを出す等の処理 }
など書けばOKです
なおストアのページを開かせる際には
が便利