Rakuten miniを3000円で買って、みまもり端末にする

つくったもの

  • 学校にいるであろう時間帯は、アプリが何も使えない時計状態
    • でも親が本当に連絡を取りたいときに、電話はかかる(時計状態でも、着信画面は出る)
    • 動作確認してないが、たぶん地震速報とかそれ系もくる
  • スマホ使っていいよって時間帯は、限られたアプリだけ起動できる。

これなら「スマートフォン持ち込みなんて許さん、けど、みまもり端末に限って持ち込んでいいぞ」てきな学校を攻略できるんでは?そういう需要ありそうでは??

とか妄想してみたのであった。

つくりかた

  • 楽天ミニの中古デバイス
  • eSIM
  • 特定のアプリだけを表示するランチャーアプリ(自作する)
    • ManagedConfigurationを使って「お家モード」「学校モード」の切り替えができるように作るのがミソ

があればできる。

ランチャーアプリを作る

ランチャーと言っても、今回のユースケースでは任意のアプリを起動できる必要はない。

固定でアプリアイコンを配置してしまって、そのアイコンが押されたら特定のアプリ決め打ちでIntentを投げるだけのウルトラ適当実装で十分だ。

あとは、「お家モード」「学校モード」の2つのFragmentが切り替わるだけの1Activityのアプリを作ればいい。

ふつうにアプリを作る上ではあまり馴染みがないであろうRestrictionsManagerという機能を使い、MDMから流し込むことができる設定を定義・設定値の取得を行う。

Android Management APIでポリシーを作る

今回のアプリはAndroidバイス管理機能のKIOSKモードで動かす。(POSレジアプリとかでよく使われてるやつ)

ただ、大抵のMDMサービスを契約すると手間もお金もかかるので、簡易的なMDMサービスを自分で作ってしまうのがいいだろう。作り方は去年のアドベントカレンダーに結構細かく書いている。興味ある人は読んでくれ。

zenn.dev

で、MDMを作ってどういうポリシーを用意すればいいかというと、こんな感じ。「お家モード用のポリシー」「学校モード用のポリシー」の2つを用意し、お家モードで起動したいアプリは両方のポリシーにFORCE_INSTALLED指定で入れておく。

先の手順で作ったランチャーアプリはprivate appsとしてAndroid Management APIに登録し、以下のようにManaged Configurationを指定してインストール+KIOSKモードにする。

「お家モード用のポリシー」↓

  "applications": [
    {
      "packageName": "com.google.android.apps.maps",
      "installType": "FORCE_INSTALLED"
    },
    {
      "packageName": "com.google.android.apps.tachyon",
      "installType": "FORCE_INSTALLED"
    },
    {
      "packageName": "com.android.chrome",
      "installType": "FORCE_INSTALLED"
    },
    {
      "packageName": "dev.yusukeiwaki.mylauncher",
      "installType": "KIOSK",
      "managedConfiguration": {
        "mode": "home"
      }
    }
  ],

学校モードはmodeをschoolにしただけ。

      "packageName": "dev.yusukeiwaki.mylauncher",
      "installType": "KIOSK",
      "managedConfiguration": {
        "mode": "school"
      }
    }

あとは、適当な時間帯で、Android Management APIを叩いてポリシーを当てるようにすればいいだけ。IoTスイッチ的なものでポチッとできるようにするとさらに素敵かもしれない。

まとめ

Android Management APIがあれば専用端末作り放題で楽しい。

Rakuten miniは安くてコスパ最高

Ruby 2.7, 3.0, 3.1でもデータクラス (Data.define) が使えるようなライブラリdata_class_factoryを作った

まえおき

Ruby 3.2のデータクラスっていうやつがとてもハイセンス。実装した人自身が、dev.toでとても自慢げに記事を書いている。

dev.to

大昔に自分で初めて作ったライブラリが奇しくもデータクラスだったので、これは見逃せない。

yusukeiwaki.hatenablog.com

Ruby 3.2のデータクラスは、Structから機能を落とした版で、おそらく多くのRubyistたちが望んだものだろう。しかしながら当然のこと、これはRuby 3.1以前のバージョンでは使えない。

いっぽうで、そこそこ大きめのRailsアプリケーションを開発してると、Rubyのバージョンはそんなにホイホイ上げられるものではないので、Ruby 3.0や3.1止まりの会社も今はまだ多かろう。そんなわけで、kt_data_classのエッセンスは取り入れつつ、バックポートを目的としたライブラリを作ることにした。

1日で作りたい

kt_data_classを初めて作ったときは、たぶん1週間くらいかけて作ってたのだけど、今回のデータクラスのバックポートは、なる早で使いたい。

目標として「1日で仕上げる」ことを気に留めながら開発を進めた。

テスト駆動だが、今回はMinitest

スピード開発なので、迷わずテスト駆動で開発をすすめることにした。

kt_data_classのときは bundle gemのデフォルト値に従うままでRSpecでテストを書いていたが、Minitestの並列実行の速さに慣れてしまうとRSpecはとっても遅い。今回は速いMinitestを採用。

あと、bundle gemのデフォルトの構成だとtest_helperとか色々作られるが、よりシンプルな構成に変更。

Rakefileはこんなかんじ。 rake 単体でテストが流れるのは若干キモイので、 rake test でテストが流れるだけの設定にダイエット。

require 'bundler/gem_tasks'
require 'rake/testtask'

Rake::TestTask.new(:test) do |t|
  t.libs << 'test'
  t.libs << 'lib'
  t.test_files = FileList['test/**/*_test.rb']
end

個々のテストでは、test_helperを使わず、↓こんな感じのをテンプレートとしてこぴぺして使った。

require 'minitest/autorun'
require 'data_class_factory'

class XXXTest < Minitest::Test
  def test_zzzzzz

ここまでやることで、「テストファイル1つを見れば、単体テスト内容がすべてわかる」という状態にできる。あっちこっち見て回らなくて住むのでスピーディーにテストが書ける。

リファレンスとRuby本体の単体テストを大いに参考にした

Dataクラスについては、Rubyのリファレンスがとても充実している。

docs.ruby-lang.org

あとは、RubyにDataクラスが追加される際に書かれた単体試験(Add Data class implementation: Simple immutable value object by zverok · Pull Request #6353 · ruby/ruby · GitHub)も、ほぼそのまま流用することで、Dataクラスがどう振る舞うべきか、Ruby 3.2のDataクラスとの乖離が少ない形で実装が進められる。

dup, cloneと、Marshal.dump/loadの対応

Ruby本体の単体試験には以下のようなコードがある。

  def test_dup
    klass = Data.define(:foo, :bar)
    test = klass.new(foo: 1, bar: 2)
    assert_equal(klass.new(foo: 1, bar: 2), test.dup)
    assert_predicate(test.dup, :frozen?)
  end

  Klass = Data.define(:foo, :bar)

  def test_marshal
    test = Klass.new(foo: 1, bar: 2)
    loaded = Marshal.load(Marshal.dump(test))
    assert_equal(test, loaded)
    assert_not_same(test, loaded)
    assert_predicate(loaded, :frozen?)
  end

https://github.com/ruby/ruby/blob/v3_2_0/test/ruby/test_data.rb

シャローコピー(dupやclone)してもディープコピー(Marshal.dump/load)しても、freezeされた状態であれ、というもの。

class Data
  def initialize(**kwargs)
     ...いろいろ

     freeze
  end

こんな感じでinitializeの末尾でfreezeしておけばいいのだと思っていたが、これではdupやclone, Marshal.dump/loadいずれの操作時にも、freezeされていないインスタンスが生成されてしまった。

結論から言うと

  • dupやcloneでは #initialize_copy のオーバーライドが必要
  • Marshal.loadでは #marshal_dump #marshal_load のオーバーライドが必要
    • marshal_loadだけをオーバーライドしても動かない。両方オーバーライドが必要。

ということだった。kt_data_classのときはここまで考慮してなかったので、今回とても勉強になった。

github.com

.newの引数はキーワード引数だけにする

Structを踏襲してなのか、Dataクラスも「キーワード引数でもキーワード引数じゃなくてもいい」という仕様が健在だ。

Point = Data.define(:x, :y)

Point.new(3, 4) # OK
Point.new(x: 3, y: 4) # OK

ただ、これは完全に個人的意見だが、キーワード引数だけで十分だと思うし、そのほうが呼び出し側だけ読んで引数の意味がわかりやすくて良いと思う。

Feature #16122: Data: simple immutable value object - Ruby master - Ruby Issue Tracking Systemのディスカッションを見るに、どうもパターンマッチングとの整合性や、単純に書く量多くね?みたいな流れでキーワードじゃない引数もサポートするようにしたようだ。 また、New in Ruby 3.2 - Data.define - DEV Communityにも書かれているように、キーワードじゃない引数をサポートするための実装として、initializeではなく .newのほうで少々工夫が凝らされている。

もしキーワード引数だけの対応であれば、そんな工夫は一切いらない。

別にそれでよくね?

というわけで、バックポートDataクラスでは、一旦キーワード引数だけをサポートする形としてみた。

やっぱりpositional argumentもサポートしたほうが便利だなってなったら、今後追加するかもしれない。

で、一日でできたの?

思い立ったのが2023/3/4の10:53で

仕事終わりの夜に実装を進めて、データクラスが出来上がったのは 3/6の2:31 github.com

Gemを世に放ったのは3/8の10:28なので、まぁ全然1日では終わっていないw

そうはいっても、kt_data_classのときよりはだいぶスピーディーに開発したとは思う。(自画自賛w

まとめ

思い立って数日で、なんとか形にはした。

rubygems.org

そして、こういう既存機能ポーティングのライブラリ開発は作るたびに勉強になる。

自分で開発しているRuby用のライブラリに型情報を足す

仕事で忙殺され気味のため久々の更新。

playwright-ruby-clientにいきなり「型書け」ってissueが飛んできた。

github.com

個人的にはGitHub Copilotにどっぷり浸かっていて、「Rubyだったらべつに型いらんくね?」って考えなんだけど、playwright-ruby-clientに関してはユーザが触るコードは全部自動生成しているので、コード生成スクリプトにちょろっと追記したら手で型定義書かなくてもRBSも自動生成できるんじゃないか?と試してみた。

どういう状態にしないといけないか

RBSを"使う"側つまり、ライブラリを使ってコードを書く側の話はぐぐるとたくさんでてくるが、Gemを開発している人は何をやればいいのか?というのはそんなに情報がない。

唯一、この記事↓がまさにそれを紹介していた。

blog.kymmt.com

結論から言うと、ライブラリの開発者がやらないといけないことは至極単純で、 sig/your_library.rbs のファイルを同梱するだけ

.
├── CODE_OF_CONDUCT.md
├── Gemfile
├── Gemfile.lock
├── LICENSE.txt
├── README.md
├── Rakefile
├── bin
│   ├── console
│   └── setup
├── lib
│   ├── playwright
│   ├── playwright.rb
│   └── playwright_api
├── pkg
├── playwright.gemspec
├── sig ←★これを追加
│   └── playwright.rbs
└── spec

どんな型定義を書かないといけないのか

型定義はいきなり書けと言われて所見で書けるものではない。まずはTypeProfで雛形を作成する。

typeprof lib/playwright_api/*.rb -I lib/playwright -o sig/_playwright.rbs

これだけで割とそれっぽい型定義は作られる。

あとは、 sig/_playwright.rbsを参考に、 sig/playwright.rbs を頑張って作る。

さらにそれをTypeProfに食わせてみて、書いた型定義にミスが無いかを確認する。

typeprof sig/playwright.rbs lib/playwright_api/*.rb -I lib/playwright -o sig/_playwright.rbs 

rbs validateなどもあるが、typeprofだけでも結構いける。

「こんなときどう書けばいいの?」って疑問は、9割がた↓の記事をみて解決できた。pockeさん神記事です。

pocke.hatenablog.com

TypeProfの "unhandled exception" 問題

TypeProfに食わせるrbsファイルに不備があると、 "unhandled exception" っていうエラーが出て2回ハマった。

結論、以下の2つがハマりどころでした。

  • パラメータ名が "type" だとダメ
  • ArrayやHashは Array[untyped] Hash[untyped, untyped] って書かないとダメ

型を書いてみた結果...

RBSをサポートしてるぜって熱弁しているRubyMineをインストールして、対応前後の動きをみてみた。

もともとがこんな感じ。ほとんど何も補完が効かない。これならVSCodeSublime Textで十分、ってかんじ。

対応後はこんな感じ。「コード補完ほしい」「型書け」って言う人達にとってはまぁ嬉しいんだろうなという結果。

でもGitHub Copilotはもっとすごいよ...

型を書く書かないにかかわらず、Copilotはこのレベルで補完をガンガンしてくる↓

  • どちらかというと自分のライブラリのサンプルコードを世の中にバンバン出していくほうが、Copilotによる補完精度が上がって、ユーザにとってはありがたいんでは?
  • まだ人気のないライブラリに限ってはRBS書いたほうがよさそう

という所感に落ち着く。

まとめ

Gemを自分で作ってる人は sig/your_library.rbs を足すだけで「型書け」には答えられる。

蛇足: 「型書け」と言ったからには責任を!

GitHub Copilotにどっぷりな身としては、「なんとなくコードの補完効いてほしいよね〜」くらいのレベルで「型を書け」というのはRubyにおいては正しくないと思う。

Add type signatures (RBS) · Issue #239 · YusukeIwaki/playwright-ruby-client · GitHub

この手のissueを放り投げるのは、考えるきっかけとしてはありがたかったものの、「型書け」って人に言うからにはしっかり議論やフィードバックしてくれ!ってのが本音。