Sorcery + Request Specで undefined local variable or method `user_sessions_url'

Sorceryを使ったユーザー認証のあるRailsアプリケーションについて、
Request Specでユーザーログインしている状態としていない状態でテストをしようとしたら

undefined local variable or methoduser_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環境では失敗せず正常に動いていたので、環境特有のナニカ・・・うーん・・メモリかも・・・🤔と思って試しに
aws lambda メモリ設定
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(..));
}

play.rust-lang.org

Enum::A(..) の部分に引っかかりました。
Enum::Aは引数を3つとるのでは・・・?としばらく悩んだのですが、
マクロを展開してしまえば

        let e = Enum::A(0, 0, 0)
        match e {
            Enum::A(..) => println!("\(^o^)/"),
            _ => panic!("panic!")
        }

ということですね。
パターンマッチ中はいちいち Enum::A(_, _, _) などと書かなくても (..)で省略できるんですね。
ちゃんとここに書いてありました。

github.com

    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

play.rust-lang.org

と同じセマンティクスですね。

Rust便利ですね。

Ruby*args **argsで配列やハッシュで可変長の引数を取れるのとか、Ruby2.7のパターンマッチの*と似てますね。似てないか。

ちなみに僕が悩んだ元のコードは

mdBook/summary.rs at a592da33bbf0adbe93d7d47fd016a8af462ed677 · rust-lang-nursery/mdBook · GitHub これです。

mdBookに変なPRを出した

mdBookという、gitbookのrust版みたいなOSSがあります。

github.com

マークダウンをつっこむとサイト(やpdfやepubその他レンダラを書けば何でも!)を出力してくれるものです。
主にドキュメントサイトを生成するのに使われていると思います。

Rustの公式ドキュメント系もmdBookでビルドされているものが多く、
たとえば、 Rust By Exampleもそうです。

doc.rust-lang.org

さて、昨日mdbookのissueを眺めていたら、こんなissueを見つけました

github.com

『Rust By Exampleには「Ctrl とEnterを押したらコードが実行される」って書いてるけど、少なくとも僕の環境ではできなかったよ』
って内容です。

mdbookのデフォルトのテーマではそんな機能は提供されていないので、Ctrl + Enterでコードが実行されない、となると問題はRust By Exampleの方にあることになります。
それで、rust-by-exampleのリポジトリを掘っていたら、

github.com

たしかにCtrl + Enterでコード実行できるようにする、というコミットがありました。
ただこのコミットをよく見ると、gitbookについての実装なんですね。

というのも、rust by exampleは昔gitbookを使っていて、mdbookの機能が十分になったところでmdbookに乗り換えた、という歴史があるようです。

github.com

なので、昔、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を投げておきました。

github.com

無事マージされるでしょうか。

P.S.
マージされました

Zero-SizedなオブジェクトをVecに入れてみる

Zero-Sizedなオブジェクトは、メンバを持たない構造体から作ることができます。

struct Blank;
// struct Blank {} と同じ

fn main() {
  println!("{}", std::mem::size_of::<Blank>()); // => 0
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=502ca273485513e57f061fa22755f767

ちなみに、メンバを持たないenumもサイズは0です

enum BlankEnum;

fn main() {
  println!("{}", std::mem::size_of::<BlankEnum>()); // => 0
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=6a62ebde80162cdbad058521f5425e7e

(が、メンバがないので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
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=7d68970ccbe150fed081606e2b0a9e07

(一応断っておくと、すべてがデータを持たないenumの場合の話です)

さて話は戻って、
Vecは[T]の先頭へのポインタ*const T(≠ &[T])と キャパシティ(usize)と 長さ(usize)

からなる構造体なので、
ここにZeroSizedなオブジェクトを入れたらどうなるのだろうと、やってみました

ちなみに③、
0サイズのオブジェクトでもアロケーションはされている(と言ってよいのだろうか)ようです

struct Blank;

fn main() {
    let obj_in_vain = Blank{};
    println!("{:?}", &obj_in_vain as *const Blank); // => <なんらかのアドレスが印字される>
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=d94bddc4d0e323f764b3e41f42612e10

さて、今度こそ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
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=2edc3590213956f140927f7cab338755

長さは普通です。 キャパシティは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>>()); // なんと増えない
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=d33b62b9d79419061d65796878861116

Rustおもしろいですね。

FlutterでFireRemoteConfigを使ってバージョンアップを強制する

FlutterとRemoteConfigを使って、
アプリをあるバージョン"以上"にする方法を紹介します

(正確には アプリがあるバージョン未満の場合にアップデートを促す方法、です)

まずFlutter側のRemoteConfigの設定を済ませます

pub.dev

以下のようにRemoteConfigの設定もしてしまいます remote config

使うパッケージ

アプリのバージョン取得用 pub.dev

x.x.xの形式のバージョン同士を比較する用 pub.dev

実装

Flutterで実装する場合、iOSAndroidでバージョンは基本的には同じだと思いますが、
審査待ち等何らかの事情で足並みが揃わない場合のためにiOSAndroidで処理を分けています

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です

なおストアのページを開かせる際には

pub.dev

が便利