📰 Topics

今週はレポートの半分がRustでCLIツールを作って学んだことです。
給電とHDMI出力を兼ねた高性能なUSB TYpe-Cハブもオススメ。

Table of Contents

書いたこと

なし。

学んだこと

【Rust】CLIを作ってみた

RustでAtLrusというCLIツールを作ってみました。
READMEに説明もなく、バージョニングもしていませんが以下がリポジトリです。

開発を通していくつかのことを学びました。

引数のパース

structoptを使います。

構造体にコマンドライン引数の仕様を宣言すると、Args::from_args()すればOK。

use std::path::PathBuf;
use structopt::StructOpt;

// ... 中略

#[derive(Debug, StructOpt)]
struct Args {
    /// Input parameter json file
    #[structopt(parse(from_os_str))]
    input: PathBuf,
}

#[tokio::main]
async fn main() -> Result<()> {
    // ... 中略

    let args: Args = Args::from_args();
    let json_str = fs::read_to_string(&args.input)?;

    // ... 中略
}

今回のツールは入力のjsonファイルパスだけですが、引数が増えると便利です。

JSONのパース

serde-rs/jsonを使います。

別途serdeのインストールも必要です。

serdeのSerialize/Deserializeを使うため、Cargo.tomlのfeatures指定が必要です。

serde_json = "1.0.57"
serde = { version = "1.0.115", features = ["derive"] }

JSONの構造を#[derive(Deserialize)]のついたstructで定義します。
serde_json::from_strを使って指定した型に変換できます。

use serde::Deserialize;

// ... 中略

#[derive(Deserialize, Debug)]
struct Group {
    slug: String,
    emails: Vec<String>,
}

#[derive(Deserialize, Debug)]
struct CreateGroupsOperation {
    workspace_uuid: String,
    group_names: Vec<String>,
}

#[derive(Deserialize, Debug)]
struct InviteMembersOperation {
    /// Ex: tadashi-aikawa/x-viewer
    repository: String,
    /// Ex: read, write
    permission: String,
    emails: Vec<String>,
}

#[derive(Deserialize, Debug)]
struct AddGroupMembersOperation {
    workspace_uuid: String,
    groups: Vec<Group>,
}

#[derive(Deserialize, Debug)]
struct Operation {
    create_groups: Option<CreateGroupsOperation>,
    invite_members: Option<InviteMembersOperation>,
    add_group_members: Option<AddGroupMembersOperation>,
}

#[tokio::main]
async fn main() -> Result<()> {
    // ... 中略

    let args: Args = Args::from_args();
    let json_str = fs::read_to_string(&args.input)?;
    let operation = serde_json::from_str::<Operation>(&json_str)?;

    if let Some(op) = operation.create_groups {
        info!(">>>>>>>>>> Create groups");
        do_create_groups(&op).await
    }

    // ... 中略
}

APIにリクエストしてレスポンスJSONを取得

reqwestを使います。

JSONを扱うのでCargo.tomlのfeatures指定をします。

reqwest = { version = "0.10.7", features = ["json"] }

reqwest::Client = reqwest::Client::new()で作成したクライアントを使います。
メソッドチェーンで爽やかに書けますね😁

先ほどのserdeを使った構造体を指定して.jsonとすればレスポンスJSONを変換できます。
res.json::<PostGroupsResponse>(...)の部分です。

use std::env;

use anyhow::Result;
use serde::Deserialize;
use std::collections::HashMap;

const URL: &str = "https://api.bitbucket.org/1.0";

lazy_static! {
    static ref CLIENT: reqwest::Client = reqwest::Client::new();
    static ref USER_NAME: String =
        env::var("ATLRUS_USER_NAME").expect("You must specify ATLRUS_USER_NAME");
    static ref APP_PASSWORD: String =
        env::var("ATLRUS_APP_PASSWORD").expect("You must specify ATLRUS_APP_PASSWORD");
}

/// Actually.. there are more properties.
#[derive(Deserialize, Debug)]
pub struct PostGroupsResponse {
    pub name: String,
    pub slug: String,
}

/// Create a group in specified workspace.
pub async fn post_groups(workspaces_uuid: &str, group_name: &str) -> Result<PostGroupsResponse> {
    let url = format!(
        "{base_url}/groups/{workspace}",
        base_url = URL,
        workspace = workspaces_uuid,
    );

    let mut params = HashMap::new();
    params.insert("name", group_name);

    let res = CLIENT
        .post(&url)
        .basic_auth(USER_NAME.to_string(), Some(APP_PASSWORD.to_string()))
        .form(&params)
        .send()
        .await?;

    match res.status() {
        s if s.is_client_error() => bail!("Client error: {}. detail: {}", s, res.text().await?),
        s if s.is_server_error() => bail!("Server error: {}. detail: {}", s, res.text().await?),
        _ => Ok(res.json::<PostGroupsResponse>().await?),
    }
}

async/awaitで非同期処理

上記コードを見てみましょう。
async/awaitで非同期処理を実現しています。

JavaScriptで使うPromiseに似た概念として、RustではFutureがあります。
async関数は常にFutureトレイトを返し、awaitFuture::pollPoll::Readyを返すまで待ちます。

JavaScriptとの違いはawaitの位置です。
await futureImplementedではなくfutureImplemented.awaitのように書きます。

複数の非同期処理をメソッドチェーンするケースではクールなコードになります😎
JavaScriptだと

const hoge = (await (await aaa).bbb).ccc;

これがRustだと

let hoge = aaa.await.bbb.await.ccc;

非同期処理は失敗を含むケースが多いので、実際にはawait?を使いますけど😜
reqwestのレスポンスはtext()json::<T>()も非同期処理なのでawai?tを使います。

asyncなmain関数

mainをasync関数にする場合は非同期ランタイムが必要です。
拘りはなかったのでtokioを使いました。

#[tokio::main]を使いたいのでCargo.tomlにfeaturesを指定します。

tokio = { version = "0.2.22", features = ["macros"] }

あとはmain関数に#[tokio::main]を付けるだけ。
よく見ると、先ほどまでのコードサンプルにも付いていますよ👍

エラー処理の統一

エラー型ライブラリごとに異なります。
都度変換するのは辛すぎるため、anyhowを使います。

これも先ほどのコード例に登場しています。
use anyhow::Result;と宣言するだけ。

これで、Resultstd::error::Errorトレイトを実装するエラーをすべて同様に扱えます。
つまりhoge?構文をすべて受け入れてくれるわけです。太っ腹🍜

他にもbailマクロを使っています。
bail!("...")と書くだけでanyhowが扱えるエラーを作成できます。

    match res.status() {
        s if s.is_client_error() => bail!("Client error: {}. detail: {}", s, res.text().await?),
        s if s.is_server_error() => bail!("Server error: {}. detail: {}", s, res.text().await?),
        _ => Ok(res.json::<PostGroupsResponse>().await?),
    }

今回は使いませんでしたが、条件を満たさない場合にエラーを返すensureマクロも便利だと思いました。

遅延初期化でシングルトン

コンパイル時には確定しないけど、一度だけの初期化処理を書きたい..
そんなときにlazy_staticです👏

先ほどのコードにもしれっと入れてました。
lazy_static! { ... }の中に書くだけでお手軽!

#[macro_use]
extern crate lazy_static;

lazy_static! {
    static ref CLIENT: reqwest::Client = reqwest::Client::new();
    static ref USER_NAME: String =
        env::var("ATLRUS_USER_NAME").expect("You must specify ATLRUS_USER_NAME");
    static ref APP_PASSWORD: String =
        env::var("ATLRUS_APP_PASSWORD").expect("You must specify ATLRUS_APP_PASSWORD");
}

ロガーでログを吐く

いつまでもprint!だとつらいのでロガーを入れます。
logを使います。

logの実装は切り離されているため、以下のいずれかを使います。
社内勉強会でも話題に上がったenv_loggerにします。

env_logger::init()で初期化したらinfo!error!と呼び出すだけです。ラクチン😄
以下はデフォルトログレベルをINFOにするためenv_logger::from_envを使っていますが。

#[macro_use]
extern crate log;

// ... 中略

#[tokio::main]
async fn main() -> Result<()> {
    env_logger::from_env(Env::default().default_filter_or("info")).init();

    // ... 中略
}

// ... 中略

async fn do_create_groups(op: &CreateGroupsOperation) {
    for group_name in op.group_names.iter() {
        match bitbucket::v1api::post_groups(&op.workspace_uuid, &group_name).await {
            Ok(group) => info!("Create a new group, {}!!", group.name),
            Err(err) => {
                error!("Fail to create a new group, {}..", group_name);
                error!("{}", err)
            }
        }
    }
}

// ... 中略

あとがき

結構なボリュームになったので、後ほど1つの記事として切り出すかもしれません。
for文を使わずにasync/awaitする方法とかもっとオシャレにできそうな気はしてます..。

そして、予想以上にJavaScriptの非同期処理と似てて親しみが沸きました。

読んだこと/聴いたこと

Rustの非同期プログラミングをマスターする

Rustの非同期処理と歴史について、とても丁寧で分かりやすく説明されています。

駆け出しRustエンジニアとしてはすべてを理解できませんが、歴史的な経緯はTypeScript/JavaScriptのPromise -> async/awaitに近い印象を受けました。

開発体験を変える! Chrome DevTools Tips 7選

Chromeでデバッグするときの便利な手法が紹介されています。

Exceptionの発生箇所で自動停止は知らなかった..。

TodoistにBoards機能が追加

ベータ版ですがカンバンボード機能が投入されました。
テスター募集されているので気になる方は是非💪

カンバンの列はセクションと一致します。
また、Board表示とList表示はいつでも切り替えられます。

私はTodoリストにステータスを求めないので、恐らく使わないと思います。
ステータス管理が必要になる場合は細分化が足りないと思っているため。

なぜTaskChute Cloudの時間管理が窮屈ではないのか?

分単位のタスク管理は窮屈と思えるでしょう。私にもそんな時代がありました。
この記事はその疑問にしっかり言語された回答を教えてくれます。

導入部の文がすべてを物語っています。

休日にこのような”何もしない”などという拷問を甘んじて受け入れる人はまず居ないでしょう。結局のところ、何も決めず、何もしないと事前に決めたところで、いざそのときになったら何かしらを選択せずにはいられません。

是非、記事を最後まで読んでみて下さい😁

世界一のコーチですら「素直じゃない人は放っておけばいい」と思っていた。

コーチングは受ける側の問題であるという少し過激な記事です。
とはいえ、私も『他人は変えられない』を信じているタマなので同感です。

コーチングや他人を変えることを仕事にしている場合はその限りでもないと思いますが..。

試したこと

IDEAでdatabaseと連携

DBeaver使っているからいらないかな..と思っていましたが気になったので試してみました。

私がDBeaverで必要な機能はほとんど実装されている印象..。

  • ER図の表示/ジャンプ
  • SQLフォーマット
  • 補完
  • SQLエディタ
  • View
  • Windowsを独立させて複数画面使用

それに加えてJetBrains製品ならではの特徴もあります。
※ 対象リソースに接続する必要があります

  • ソースコード(jsファイルなど)内のSQL文でも補完/検査
  • ソースコードとの連携によるテーブルなどの呼び出し履歴
  • Vim操作が可能..そうIdeaVimが使えるからね!!

知らないだけで他にもメリットはあるでしょう。
個人的にはIdeaVimの力でVim操作できることが盲点であり、感動しましたw

調べたこと

【Git】git diffの行末に『^M』が表示される

WindowsでCR改行の差分が表示されるためでした。
core.whitespace = cr-at-eolを指定すると解消されます。

以下にもまとめました。

【TypeScript】sqliteパッケージをv4にアップデートしたらビルドできない。

v3からv4にメジャーアップデートしたら当たり前のようにビルドできなくなりました。

影響があったのは以下4点です。

  • db.allの型引数をTからT[]に変わった
  • db.getundefinedを返すようになった
  • ドライバとwrapperが分離された

明示的にsqlite3をドライバとしてインストールする必要があります。

npm i sqlite3

READMEにあるとおり、初期化の方法も変わっています。

import sqlite3 from 'sqlite3'
import { open } from 'sqlite'

(async () => {
  const db = await open({
    filename: "./db_system/database.sqlite",
    driver: sqlite3.Database,
  });
})();

なお、importをimport sqlite from 'sqlite'にしてawait sqlite.open(...)ではなぜか動きませんでした..。

【Git】Gitの管理化では改行コードをLFにしたい

.gitattributesで改行コードにLFを強制します。
これならばautocrlfの設定にかかわらずLF改行を強制させることができます。

* text=auto eol=lf

CRLFが必要なファイルは別途設定します。

*.bat text eol=crlf

【Rust】Optionの値が存在する場合のみ非同期処理を実行したい

非同期処理ではmapによるメソッドチェーンが難しそうだったため、こう書いていました。

if operation.create_groups.is_some() {
    do_create_group(&operation.create_groups.unwrap()).await
}

しかし、これはかなり冗長です。
operation.create_groupsがSomeと分かった時点で、中身だけが欲しい..。

let式とパターンマッチでシンプルにかけました。GOOD😄

if let Some(op) = operation.create_groups {
    do_create_group(&op).await
}

整備したこと

【ターミナル】delta

Rustで書かれたオシャレなdiff CLIのdeltaを導入しました。

Windowsでも動きます。さらにはcmd.exeでも実用的に表示されます!

USB Type-C で給電とHDMI出力を兼ねる

ノートパソコンのUSB Type-Cポートが動かなくなってしまい、USB Type-C接続の2560x1440ディスプレイが使えなくなりました。
もう1つのType-CポートはACアダプタからの給電だったので使うわけにはいかず..。

USBと給電の知識

USB PDという概念をはじめて知りました。

製品を探す

そこで以下の要件を満たす製品を探しました。

  • 経由してPCに給電できる (PCに給電が必要)
  • 60Hzで2560x1440画面出力できる (30Hzではカクカク)
  • 給電可能でWi-fiルータに接続できる (USB-A)

特に60Hzの画面出力は対応製品が少なく、最終的に以下製品を購入しました。
お値段はそれなりですが、製品説明も丁寧で信頼できそうだったからです。

Club 3D CSV-1592 は、USB タイプ C 7-in-1 ハブ to HDMI 4K60Hz /SD-TF カードスロット / 2x USB タイプ A / USB タイプ C PD / RJ45のハブです。USB タイプ Cホストから映像・音楽の視聴、周辺デバイスへの充電、データ通信が行えます。HDMIの最大解像度を4K60Hzで使用するためのは、ご使用のホストがDP 1.4 Alt mode をサポートしている必要があります。

HDMIの最大解像度を4K60Hzとあるので画面出力はできそう。
30Hzはカクカクで耐えられませんからね😜

アップストリームは、ホスト接続用のUSB タイプ-C オス コネクタです。ダウンストリームは HDMI メスコネクタ、USB タイプ-A 3.0が二つ(一つはBC 1.2充電機能付き)、PD充電およびデータ通信可能なUSB タイプ-Cメスコネクタ、Micro SD カードスロット1基、SD カードスロット1基、RJ45ギガイーサネットです。サイズは、13.6 x 3.4 x 1.4 cmです。重さは69gです。

USB タイプ-A 3.0が二つ(一つはBC 1.2充電機能付き)とあるのでWi-fiルータにも接続できそうです。

USB タイプ-Aの帯域は5Gbpsで、USBタイプ-Aの二つのポートのうち一つが、最大7.5W([email protected])充電のBC1.2をサポートしています。USB タイプ-C PDは、データ通信と充電が行え、100W(20V/5A)まで充電できます。データ通信はできません。SD/Micro SDは、セキュアデジタルv3.0 UHS-I をサポートサポートします。RJ45ギガビットイーサーネットは、10Mbps/100Mbps/1000Mbpsをサポートします。

USB タイプ-C PDは、データ通信と充電が行え、100W(20V/5A)まで充電できますとあるので給電もいけます。

実際どうだったか?

今のところ、予想通り動いています👍
しばらく使ってみないと分かりませんが、一旦画面が蘇って満足😁

ちなみに接続先のディスプレイは以下です。


今週のリリース

Togowl v2.13.0 ~ 2.13.2

タスク、エントリ、プロジェクトなどをリロードするボタンを追加

通信エラーやデータ不整合が発生したとき、アプリケーションのリロードなしに復帰できます。
また、タスクのロード中は常にインジケーターブロックを表示するようにしました。

その他

遂にRustで動くモノを作れたことは大きな一歩だと思っています。

家のメインディスプレイが使えなくなったときは予想以上に絶望を感じました。
普段当たり前にある環境..そのことに感謝する気持ちを忘れないようにと思いましたね

そして日本FALCOMの軌跡シリーズ最新作..創の軌跡がいよいよ今週発売です!

Togowlの背景画像1にも設定してテンションを上げつつ、木金は休みをとったので準備もOK。
今週/来週は寂しいレポートになると思いますがご了承ください😅


  1. Togowlは任意の背景画像を設定可能であり、リソースに利用しているわけではありません ↩︎