Rustの勉強でCosenseのCLIを作る
Rustの勉強でCosenseのCLIを作る
公開学習よきtakker.icon
以前もこういうのがあって、何か名前ついていた気がしたけど、何だっけな
応援
がんばるmoeki.icon
curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh でインストール
code:rust-version.sh
rustc --version
rustc 1.79.0 (129f3b996 2024-06-10) (Homebrew)
できた
クレートはRubyでいうGemみたいな感じかな?
cargo add clap --features derive で clap をインストールしてみる
そのまえに cargo new しないとだめらしい
cargo new cosense
code:tree.sh
tree
.
├── Cargo.toml
└── src
└── main.rs
これ実行したらどうなるんだろ
rustc src/main.rs したら main バイナリが出来た
code:main.sh
./main
Hello, world!
おお、実行できた
clap をインストールした
code:_main.rs
use clap::Parser;
これはインポートみたいなやつか
code:_main.rs
これは謎。
claude.icon
この属性を構造体に付けると、その構造体に対してコマンドライン引数のパーシング機能が自動的に実装されます。
Args に色々生えるのか
code:_main.rs
struct Args;
fn main() {
let _ = Args::parse();
}
実行してみる
code:run.sh
cargo run -q -- --help
Usage: cosense
Options:
-h, --help Print help
プロジェクト名を受け取る
code:_main.rs
struct Args {
project: String,
}
https://scrapbox.io/api/pages/moeki を叩く
main function is not allowed to be async
main を async にしようとしたら怒られた
別の関数を作る
the trait serde::de::Deserialize<'_> is not implemented for CosensePages, which is required by Vec<CosensePages>: serde::de::DeserializeOwned
なんか怒られた
claude.icon
このエラーメッセージは、CosensePagesという構造体(または型)に対してDeserializeトレイトが実装されていないことを示しています。
トレイトはオブジェクトで言うインタフェースみたいなものか
code:warning.sh
projectName: String,
| ^^^^^^^^^^^ help: convert the identifier to snake case: project_name
snake case じゃないと怒られる
code:_main.rs
if response.status().is_success() {
let pages: CosensePages = response.json().await?;
println!("Project Name: {}", pages.project_name);
} else {
println!("Error: {}", response.status());
}
うーん出力されない。たぶん非同期処理周りがおかしい
結局 main も async にできたわ。 #[tokio::main] が足りてなかっただけだった
projectName を project_name にしちゃってたからデータを取得できてない
code:output.sh
cargo run -q -- moeki ✘ 1 master ◼
warning: structure field projectName should have a snake case name
--> src/main.rs:11:5
|
11 | projectName: String,
| ^^^^^^^^^^^ help: convert the identifier to snake case: project_name
|
= note: #[warn(non_snake_case)] on by default
Project Name: moeki
できた
code:_main.rs
project_name: String,
こんな感じで変換してくれるのか。便利
JSONをそのまま出力してほしいんだよな
serde_json::to_string_pretty(&pages) serde にこんな便利なものが
null になるかもしれないやつは image: Option<String>, で定義する
でけた
じゃあインタフェース考えるか(順番逆)
$ cosense json --skip 100 --limit 100 moeki
{ projectName: "moeki", ... }
引数にスラッシュがあったらページの情報を返す
$ cosense pages --skip 100 --limit 100 moeki
code:page-list.sh
ページ1
ページ2
$ cosense create moeki/ページ1
$ cosense page moeki/ページ1
$ cosense page --web moeki/ページ1
$ cosense project --web
$ cosense code moeki/ページ1/コード1
$ cosense table moeki/ページ1/コード1
コマンドとサブコマンドが必要なのでそれを定義する
なるほど、clapだと1階層しか無理か
cargo run -q -- json moeki
一旦これは出来た
次は cargo run -q -- json moeki/moeki
code:_main.rs
if resource.contains('/') {
project_json(resource).await?;
} else {
page_json(resource).await?;
}
こんなかんじか
ああ逆だ
Error: reqwest::Error { kind: Decode, source: Error("invalid type: null, expected u32", line: 1, column: 955) } ぜんぜんエラーの場所がわからない
とりあえず全部Optionにしちゃったよ
できた
次は --limit と --skip のフラグを受け取る
ついでに --pretty も受け取る
let mut url = format!("https://scrapbox.io/api/pages/{}", project);
mut でミュータブルにできるらしい
できた
次は cargo run -q -- pages moeki
the method join exists for struct Vec<(&str, String)>, but its trait bounds were not satisfied
なんだろうこれ
query_params.push(("limit", limit.to_string()));
文字列じゃないの含まれてた
Error: reqwest::Error { kind: Decode, source: Error("invalid type: map, expected a sequence", line: 1, column: 0) }
reqwestでエラー起きてるな
for page in pages { => for page in pages.pages {
次は cargo run -q -- create moeki/hoge hoge
できた
--url オプションでAPIのURLを吐き出すのをやろう。便利そう
code:help
cargo run -q -- --help
Usage: cosense <COMMAND>
Commands:
json
pages
create
page
code
table
icon
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
これ descripion 書くのどうやるんだろ
おお、コメント書いたら description になった。賢い
code:help
cargo run -q -- --help
Usage: cosense <COMMAND>
Commands:
json Get JSON data of project or page
pages Get page title list of project
create Create page with body on Browser
page Open page on Browser
code Get code of page
table Get table CSV of page
icon Get icon of page
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
それっぽくなった
あとはプライベートプロジェクトに対応しよう
code:ts
const response = await axios.get(url, { headers: {
Cookie: "connect.sid=hogehoge;"
}});
connect.sidをキーチェーンに保存する
これが使えるらしい
no method named set_password found for enum Result in the current scope
なんかエラーになった
よっしゃいけた
entry.expect("Failed to create keyring entry").set_password(&sid)
exceptが必要
だいぶ何が分からないかが網羅できてきたから一通りドキュメント見るか。
というか初めてバイナリで配布するけど他のOSで動くかわからん感覚すごいな
Homebrewで配布したいのでFomulaとやらを書いてみる。 うーん何もわからない
とりあえず Ruby の class を書いて Homebrew にプッシュすればいいらしいんだけどそのファイルをどこにおいておけばいいかわからない もしかしてこれ?
まずはGitHub ActionsとかでそれぞれのOSでコンパイルすればいいのかな
こんなのもやってるな
code:release.yml
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
name: Release for ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- os: ubuntu-latest
artifact_name: your-binary
asset_name: your-binary-linux-amd64
- os: windows-latest
artifact_name: your-binary.exe
asset_name: your-binary-windows-amd64.exe
- os: macos-latest
artifact_name: your-binary
asset_name: your-binary-macos-amd64
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
- name: Build
run: cargo build --release
- name: Upload binaries to release
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: target/release/${{ matrix.artifact_name }}
asset_name: ${{ matrix.asset_name }}
tag: ${{ github.ref }}
feature edition2024 is required
謎のエラー
version ならぬ edition というのがあるのか。なぜか2024にしてたけどまだ stable じゃない
エラー
code:error
The system library dbus-1 required by crate libdbus-sys was not found.
The file dbus-1.pc needs to be installed and the PKG_CONFIG_PATH environment variable must contain its parent directory.
The PKG_CONFIG_PATH environment variable is not set.
とりあえずUbuntuでだけlibbus-sysを入れてみる
今度はMacでエラー Resource not accessible by integration - https://docs.github.com/rest/releases/releases#create-a-release
ちがうなMacが一番早かっただけだ
使ってる action を読む
これが必要だった
code:yaml
permissions:
contents: write
できた!
これ嬉しい
https://gyazo.com/be4d210b4a3c9796cf191c998a131c05
この辺見つつ Fomula を書いていく
clone待ち
PRの description が結構いろいろ書いてある
code:pr
<!-- Use x to mark item done, or just click the checkboxes with device pointer --> - Have you built your formula locally with HOMEBREW_NO_INSTALL_FROM_API=1 brew install --build-from-source <formula>, where <formula> is the name of the formula you're submitting?
- Is your test running fine brew test <formula>, where <formula> is the name of the formula you're submitting?
- Does your build pass brew audit --strict <formula> (after doing HOMEBREW_NO_INSTALL_FROM_API=1 brew install --build-from-source <formula>)? If this is a new formula, does it pass brew audit --new <formula>?
-----
そしてぜんぜん気にしてなかったところいっぱいあった
HOMEBREW_NO_INSTALL_FROM_API=1 でローカルからインストールできるのか
やってみてる
なるほど、なんかしらんが Xcode が古くて怒られた。これ cs 使おうとした人みんな怒られるってこと?
FomulaのDescriptionは The とか A ではじまると怒られる
* GitHub repository not notable enough (<30 forks, <30 watchers and <75 stars)
なぬ?
おわた
Structをモジュール化してる
code:pages.rs
use serde::{Deserialize, Serialize};
use crate::cosense::models::page_in_project::PageInProject;
pub struct Pages {
project_name: String,
skip: Option<u32>,
limit: Option<u32>,
count: Option<u32>,
pages: Vec<PageInProject>,
}
impl Pages {
pub fn get_titles(&self) -> Vec<String> {
self.pages.iter().map(|page| page.get_title()).collect()
}
}
structをモジュール化すると属性が非公開になるのでinterfaceを用意してあげないといけない
Cursorでも use の補完効いてくれるな
Ruby と TypeScript しか触ったことないものからしても、ほぼ違和感感じるところないな
CLIで search が絶妙にうちにくいので find にした
CLIやっと分かってきた...subcommandを名詞にしてたからおかしいのか
一旦のインタフェース
code:help
cs help
Usage: cs <COMMAND>
Commands:
login Login to Cosense
switch Switch current project
ls List pages of project
view Open page on Browser
get Get resource of page
find Find pages
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
すごいyosider.icon