23
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【🙇 懺悔 🙇】Qiitanグッズ欲しさに1日に33記事投稿した話またはQiita CLIとcargo scriptを布教する的な何か

Posted at

夏休み :island: ですね!皆さんは夏休みの宿題を...さっさと終わらせてしまう派ですか?コツコツ進める派...?それとも最終日にまとめて片付ける派でしょうか?

私はもちろん最終日まで溜め込む派です :sunglasses:

今日はこんな性分であるが故の筆者のやらかしを謝罪させていただきたく記事を書いております :bow:

TL;DR

Qiita Engineer Festa 2024 に参加していました

もう3週間も(!)前ですが、2024年6月10日から2024年7月17日の38日間、 Qiita Engineer Festa 2024 というQiitaの祭典が開かれ、多くのユーザー・企業がこぞって記事を出していました。

:qiitan: Qiitanグッズが必ず貰える記事投稿マラソン! :qiitan:

昨年度にはなかったイベントに、:runner: 記事投稿マラソン :runner: がありました。この企画では、Festa期間中に 38記事 投稿しきると、 Qiitan :qiitan: グッズが 必ず 貰えるそうです!

image.png

Qiita Engineer Festa オリジナルグッズセット

  • Qiitanぬいぐるみ
  • Qiitanストレスボール
  • Qiitanキーキャップ
  • 特設サイト内にQiita IDを掲載

かわいい!ほしい! 38記事投稿すれば必ず貰えるというなら、投稿しきってやろうじゃあないか、というわけで筆者の38(+α)日間の戦いが始まりました。

筆者は :crab: Rust好き :crab: なので、たまたまその時見かけた 100 Exercises To Learn Rust を題材として、記事を書き始めました!

やらかしたことと謝罪

マラソンに参加したのは良いですが、筆者は自分が宿題は溜め込んでから一気に終わらせる怠惰人間であることをすっかり忘れておりました。

最初の5記事までは順調に投稿していたのですが、以降最終日まで1記事も投稿せず、宿題、もとい記事投稿を溜めまくりました...

そして...ツールを活用し最終日に33記事(内19記事未完成)を一気にQiitaに投稿しました1...

Festa33記事投稿.png

...
...
...

大変申し訳ございませんでした...

グッズ欲しさに禁忌をやらかしてしまったわけです。本当は完走していないのに完走したフリをして、図々しくも完走者としてサイトに名前を載せてもらおうとした不届き者になってしまいました...

やらかすまでの経緯

なぜこんなことになってしまったか、反省を込めて以下に経緯を書いていこうと思います。

5記事投稿まで

投稿マラソンに記事を投稿しつつも、「薄っぺらい内容の記事でマラソン完走したらグッズ欲しさに参加している人と思われて嫌だ」「ちゃんとコミュニティに貢献した記事にしたい」と思っていた筆者は、クオリティを優先してしまい、記事執筆にかかる時間的コストを度外視して取り組みました。最終日とは真逆ですね。今思えばこれが間違いで、投稿しやすいペース・範囲・内容でサステナブルな継続的インテグレーション・継続的デリバリーを目指すべきだったのです。

1記事目から5記事目はかなり丁寧に取り組んでいました。5記事目 2は特にオタク語りが酷すぎて、Exercises To Learn Rustに全然関係ないUFCSの話とかをしちゃってます :sweat_smile:

6記事以降投稿できなくなった理由もクオリティのせいでした。「題材の『Exercises To Learn Rust』の問題を先に終わらせてから記事を書いた方が、問題間のつながりを意識して書けるのでクオリティが上がる」という判断をし、記事投稿自体の多少の遅延を許容してしまったのです。 だから今までの人生で宿題を溜め込まなかった夏休みはなかった というのに...わからない問題は飛ばすか答えを見て早く埋めちゃう方が偉いのに、それができないタイプだったわけです。

Qiita CLIとcargo scriptの活用

また、3記事目2までは新規記事をコピペで作成していたのですが、コピペ特有のミス(リンクを張り間違えるなど)がありそうで流石に苦しくなってきたため、Qiita CLIというツールから記事を投稿することとし、手元のエディタにてスクリプトを動かしてテンプレートから記事の骨組みを作成することにしました。

記事生成スクリプトに用いる言語の候補には色々ありますね。皆さんなら何を使いますか?bash?Python?Go...?...どれでもない?そうですよね、 最も楽にCLIツールを用意できる cargo script が良いに決まっています! :crab: 時代はRustです :crab:

長いですがスクリプトを載せちゃいます!(せっかく書いたので)

render.rs
#!/usr/bin/env -S cargo +nightly -q -Zscript run --release --manifest-path
---
[dependencies]
clap = { version = "4.5.7", features = ["derive"] }
handlebars = "5.1.2"
toml = "0.8.14"
serde = { version = "1.0.203", features = ["derive"] }
anyhow = "1.0.86"
tokio = { version = "1.38.0", features = ["full"] }
reqwest = "0.12.4"
dialoguer = "0.11.0"
---

use anyhow::Result;
use clap::Parser;
use std::path::PathBuf;
use serde::{Serialize, Deserialize};
use std::fs;
use handlebars::{Handlebars, no_escape};
use dialoguer::Confirm;

#[derive(Parser, Debug)]
struct Args {
    #[arg(short = 'f', long = "target-file")]
    target: PathBuf,

    #[arg(short = 't', long = "template", default_value = "./template.md")]
    template: PathBuf,

    #[arg(short, long)]
    output: Option<PathBuf>,
}

#[derive(Debug, Serialize, Deserialize)]
struct Prev {
    number: u32,
    title: String,
    id: String,
}

#[derive(Debug, Deserialize)]
struct Topic {
    title: String,
    path: String,
    jp_title: String,
}

impl Topic {
    async fn to_render(self) -> Result<TopicForRender> {
        let Self { title, path, jp_title } = self;

        let url = format!("https://raw.githubusercontent.com/mainmatter/100-exercises-to-learn-rust/main/exercises/{}/src/lib.rs", path);
        let librs = reqwest::get(&url)
            .await?
            .text()
            .await?
            .trim()
            .to_string();

        Ok(TopicForRender {
            title,
            path,
            jp_title,
            librs,
        })
    }
}

#[derive(Debug, Serialize)]
struct TopicForRender {
    title: String,
    path: String,
    jp_title: String,
    librs: String,
}

#[derive(Debug, Deserialize)]
struct Content {
    number: u32,
    title: String,
    prev: Prev,
    topics: Vec<Topic>,
}

impl Content {
    async fn to_render(self) -> Result<ContentForRender> {
        let Self { number, title, prev, topics: ts } = self;

        let mut topics = Vec::with_capacity(ts.len());
        for topic in ts {
            let res = topic.to_render().await?;
            topics.push(res);
        }

        Ok(ContentForRender {
            number,
            title,
            prev,
            topics,
        })
    }
}

#[derive(Debug, Serialize)]
struct ContentForRender {
    number: u32,
    title: String,
    prev: Prev,
    topics: Vec<TopicForRender>,
}

#[tokio::main]
async fn main() -> Result<()> {
    let Args {
        target,
        template,
        output,
    } = Args::parse();

    let template = fs::read_to_string(&template)?;
    let content = fs::read_to_string(&target)?;

    let content: Content = toml::from_str(&content)?;

    let content_for_render: ContentForRender = content.to_render().await?;

    let mut handlebars = Handlebars::new();
    handlebars.set_strict_mode(true);
    handlebars.register_escape_fn(no_escape);

    let res = handlebars.render_template(&template, &content_for_render)?;

    match output {
        Some(p) => {
            if p.exists() {
                println!("{} exists.", p.display());

                let conf = Confirm::new()
                    .with_prompt("Are you sure you want to overwrite this file?")
                    .interact()?;

                if !conf {
                    return Ok(());
                }
            }

            fs::write(p, res)?;
        },
        None => {
            println!("{}", res);
        }
    }

    Ok(())
}

このスクリプトは引数としてテンプレートファイル( template 変数にパスを格納)と記事のプランファイル( target 変数にパスを格納)を取ります。

template.md
---
title: "\U0001F980100 Exercises To Learn Rustに挑戦【{{number}}】 {{title}}"
tags:
  - Rust
private: false
updated_at: ''
id: null
organization_url_name: yumemi
slide: false
ignorePublish: false
---
前の記事

- [【0】 準備](https://qiita.com/namn1125/items/bd1b4cd028874189a3c1) ← 初回
- ...
- [【{{prev.number}}】 {{prev.title}}](https://qiita.com/namn1125/items/{{prev.id}}) ← 前回
- 【{{number}}】 {{title}} ← 今回

[100 Exercise To Learn Rust](https://rust-exercises.com/) 演習第{{number}}回になります!

今回の関連ページ

{{#each topics}}
- [{{this.title}}](https://rust-exercises.com/{{this.path}})
{{/each}}

{{#each topics}}
# [[{{this.path}}](https://github.com/mainmatter/100-exercises-to-learn-rust/blob/main/exercises/{{this.path}}/src/lib.rs)] {{this.jp_title}}

- [{{this.title}}](https://rust-exercises.com/{{this.path}})

問題はこちらです。

'''rust:lib.rs
{{this.librs}}
'''

<details><summary>テストを含めた全体</summary>

'''rust:lib.rs
{{this.librs}}
'''
</details>

## 解説

'''rust:lib.rs

'''

{{/each}}


では次の問題に行きましょう!

次の記事: (Comming soon...)
plan3.toml
number = 3
title = "ループ・オーバーフロー"

[prev]
number = 2
title = "if・パニック・演習"
id = "13870965e8273bc87bce"

[[topics]]
title = "2.6. Loops: while"
path = "02_basic_calculator/06_while"
jp_title = "while"

[[topics]]
title = "2.7. Loops: for"
path = "02_basic_calculator/07_for"
jp_title = "for"

[[topics]]
title = "2.8. Overflow and underflow"
path = "02_basic_calculator/08_overflow"
jp_title = "オーバーフロー"

プランファイルには以下の情報を記載しています。

  • 記事番号・タイトル
  • 前の記事の情報
  • 今回解く問題のタイトルとExercises To Learn Rustドキュメント内のリンク

Exercises To Learn Rustの問題を1記事あたり2~3個解く方針で進めており、このプランでは [[topics]] セクションが3つあります。各問題に設定されているプログラムをExercises To Learn RustのGitHubからreqwestで取得し、handlebarsによってテンプレートからレンダリングしています!

次のようにして実行することで、記事の骨組みが生成されます!

chmod 755 render.rs
./render.rs -- -f plans/plan3.toml --template ./template.md
出力された記事骨組み (流石に長いので折りたたみ)
---
title: "\U0001F980100 Exercises To Learn Rustに挑戦【3】 ループ・オーバーフロー"
tags:
  - Rust
private: false
updated_at: ''
id: null
organization_url_name: yumemi
slide: false
ignorePublish: false
---
前の記事

- [【0】 準備](https://qiita.com/namn1125/items/bd1b4cd028874189a3c1) ← 初回
- ...
- [【2】 if・パニック・演習](https://qiita.com/namn1125/items/13870965e8273bc87bce) ← 前回
- 【3】 ループ・オーバーフロー ← 今回

[100 Exercise To Learn Rust](https://rust-exercises.com/) 演習第3回になります!

今回の関連ページ

- [2.6. Loops: while](https://rust-exercises.com/02_basic_calculator/06_while)
- [2.7. Loops: for](https://rust-exercises.com/02_basic_calculator/07_for)
- [2.8. Overflow and underflow](https://rust-exercises.com/02_basic_calculator/08_overflow)

# [[02_basic_calculator/06_while](https://github.com/mainmatter/100-exercises-to-learn-rust/blob/main/exercises/02_basic_calculator/06_while/src/lib.rs)] while

- [2.6. Loops: while](https://rust-exercises.com/02_basic_calculator/06_while)

問題はこちらです。

'''rust:lib.rs
// Rewrite the factorial function using a `while` loop.
pub fn factorial(n: u32) -> u32 {
    // The `todo!()` macro is a placeholder that the compiler
    // interprets as "I'll get back to this later", thus
    // suppressing type errors.
    // It panics at runtime.
    todo!()
}

#[cfg(test)]
mod tests {
    use crate::factorial;

    #[test]
    fn first() {
        assert_eq!(factorial(0), 1);
    }

    #[test]
    fn second() {
        assert_eq!(factorial(1), 1);
    }

    #[test]
    fn third() {
        assert_eq!(factorial(2), 2);
    }

    #[test]
    fn fifth() {
        assert_eq!(factorial(5), 120);
    }
}
'''

<details><summary>テストを含めた全体</summary>

'''rust:lib.rs
// Rewrite the factorial function using a `while` loop.
pub fn factorial(n: u32) -> u32 {
    // The `todo!()` macro is a placeholder that the compiler
    // interprets as "I'll get back to this later", thus
    // suppressing type errors.
    // It panics at runtime.
    todo!()
}

#[cfg(test)]
mod tests {
    use crate::factorial;

    #[test]
    fn first() {
        assert_eq!(factorial(0), 1);
    }

    #[test]
    fn second() {
        assert_eq!(factorial(1), 1);
    }

    #[test]
    fn third() {
        assert_eq!(factorial(2), 2);
    }

    #[test]
    fn fifth() {
        assert_eq!(factorial(5), 120);
    }
}
'''
</details>

## 解説

'''rust:lib.rs

'''

# [[02_basic_calculator/07_for](https://github.com/mainmatter/100-exercises-to-learn-rust/blob/main/exercises/02_basic_calculator/07_for/src/lib.rs)] for

- [2.7. Loops: for](https://rust-exercises.com/02_basic_calculator/07_for)

問題はこちらです。

'''rust:lib.rs
// Rewrite the factorial function using a `for` loop.
pub fn factorial(n: u32) -> u32 {
    todo!()
}

#[cfg(test)]
mod tests {
    use crate::factorial;

    #[test]
    fn first() {
        assert_eq!(factorial(0), 1);
    }

    #[test]
    fn second() {
        assert_eq!(factorial(1), 1);
    }

    #[test]
    fn third() {
        assert_eq!(factorial(2), 2);
    }

    #[test]
    fn fifth() {
        assert_eq!(factorial(5), 120);
    }
}
'''

<details><summary>テストを含めた全体</summary>

'''rust:lib.rs
// Rewrite the factorial function using a `for` loop.
pub fn factorial(n: u32) -> u32 {
    todo!()
}

#[cfg(test)]
mod tests {
    use crate::factorial;

    #[test]
    fn first() {
        assert_eq!(factorial(0), 1);
    }

    #[test]
    fn second() {
        assert_eq!(factorial(1), 1);
    }

    #[test]
    fn third() {
        assert_eq!(factorial(2), 2);
    }

    #[test]
    fn fifth() {
        assert_eq!(factorial(5), 120);
    }
}
'''
</details>

## 解説

'''rust:lib.rs

'''

# [[02_basic_calculator/08_overflow](https://github.com/mainmatter/100-exercises-to-learn-rust/blob/main/exercises/02_basic_calculator/08_overflow/src/lib.rs)] オーバーフロー

- [2.8. Overflow and underflow](https://rust-exercises.com/02_basic_calculator/08_overflow)

問題はこちらです。

'''rust:lib.rs
// Customize the `dev` profile to wrap around on overflow.
// Check Cargo's documentation to find out the right syntax:
// https://doc.rust-lang.org/cargo/reference/profiles.html
//
// For reasons that we'll explain later, the customization needs to be done in the `Cargo.toml`
// at the root of the repository, not in the `Cargo.toml` of the exercise.

pub fn factorial(n: u32) -> u32 {
    let mut result = 1;
    for i in 1..=n {
        result *= i;
    }
    result
}

#[cfg(test)]
mod tests {
    use crate::factorial;

    #[test]
    fn twentieth() {
        // 20! is 2432902008176640000, which is too large to fit in a u32
        // With the default dev profile, this will panic when you run `cargo test`
        // We want it to wrap around instead
        assert_eq!(factorial(20), 2_192_834_560);
        //                           ☝️
        // A large number literal using underscores to improve readability!
    }

    #[test]
    fn first() {
        assert_eq!(factorial(0), 1);
    }

    #[test]
    fn second() {
        assert_eq!(factorial(1), 1);
    }

    #[test]
    fn third() {
        assert_eq!(factorial(2), 2);
    }

    #[test]
    fn fifth() {
        assert_eq!(factorial(5), 120);
    }
}
'''

<details><summary>テストを含めた全体</summary>

'''rust:lib.rs
// Customize the `dev` profile to wrap around on overflow.
// Check Cargo's documentation to find out the right syntax:
// https://doc.rust-lang.org/cargo/reference/profiles.html
//
// For reasons that we'll explain later, the customization needs to be done in the `Cargo.toml`
// at the root of the repository, not in the `Cargo.toml` of the exercise.

pub fn factorial(n: u32) -> u32 {
    let mut result = 1;
    for i in 1..=n {
        result *= i;
    }
    result
}

#[cfg(test)]
mod tests {
    use crate::factorial;

    #[test]
    fn twentieth() {
        // 20! is 2432902008176640000, which is too large to fit in a u32
        // With the default dev profile, this will panic when you run `cargo test`
        // We want it to wrap around instead
        assert_eq!(factorial(20), 2_192_834_560);
        //                           ☝️
        // A large number literal using underscores to improve readability!
    }

    #[test]
    fn first() {
        assert_eq!(factorial(0), 1);
    }

    #[test]
    fn second() {
        assert_eq!(factorial(1), 1);
    }

    #[test]
    fn third() {
        assert_eq!(factorial(2), 2);
    }

    #[test]
    fn fifth() {
        assert_eq!(factorial(5), 120);
    }
}
'''
</details>

## 解説

'''rust:lib.rs

'''



では次の問題に行きましょう!

次の記事: (Comming soon...)

Rustによるcargo scriptは、以下の部分で他言語によるスクリプトより便利です。是非使ってみてください!(VSCodeでRLSによる補完が効かないのが難点ですが...:sweat_smile:)

  • 宣言的に書ける
    • clap のお陰でコマンドライン引数が、 serde のお陰でプランファイルやスクリプトファイルの構造がそれぞれ宣言的に書けており、またRustの強い型付けによりバリデーション処理の記述を極力省けています。 derive マクロ最強!!!
  • WORA (Write Once Run Anywhere)
    • Cargo.toml の内容を直接スクリプト内に記述しているので、このスクリプトを別なマシンに持っていっても大体動きます!もし古くなってしまったとしても、当時のクレートを参照すれば良いので容易に修正できます!

生成スクリプトのお陰で一々問題をコピペしなくて良くなったので、かなり楽になりました!しかし今思えば記事生成スクリプトを用意したことも「問題さえ先に解いてしまえば記事はいつでも書ける」という意識に繋がり溜め込んでしまった要因になっていたかもしれません。(まぁ導入していなかったらなおさら完走は無理だった気がしますが...)

~ 7月11日ごろまで

アサインされたプロジェクトが忙しくなってきて、退勤後に問題をちまちま解いていました。 この間1記事も投稿していませんでした 。あと一週間切ったぞというタイミングの7月11日、ようやく最終問題が終わりました!記事はまだ全く書いていません!

7月13日からの三連休

寝る間も惜しんで?記事執筆とスプラトゥーンをやっていました。息抜きにゲームは大切ですからね。

もうこの時点で頭から取り組んでいては間に合わないことがわかっていたので、 いかに労力を掛けずに33記事書き上げるか のみに重点を置いていました。ゲームしながらたまり溜まった宿題に取り組む...完全に夏休み最終日近くの小中学生の気分でした...

次の完璧(笑)な順序で記事を書くことにしました。

  1. まず、新たなスクリプトを使い、前述した骨組みを作る「プランを自動生成する」 (自動生成の自動生成みたいな...)
  2. その後、骨組みを作り問題の「回答のみ」載せていく。この方法なら思考停止で作業できる
  3. 最後に、回答について解説を付けていく

「2の時点でとりあえず記事としての体裁は整う...!Qiita先生には叱られないはず...!」というわけで、連休中は2まではなんとしてでも終わらせることを目指しました...ソレデモツラカッタ...

そして最終日...👿卑劣な33記事同時投稿👿!

19記事までは解説まで付けられた(つまり、ハーフマラソンについてはちゃんと達成していました)のですが、残りの記事に仮置き1の文言を残したまま結局終わらせることができず、とりあえず グッズ欲しさに17日の21時ぐらいに一気に33記事投稿しました ...

投稿には、Qiita CLIのGitHub Actions を使用しました。ちまちま手で投稿していたのでは間に合わない! という思いから、 main ブランチに統合されたらQiita側にも反映されるようにしました。

天罰

未完成なのにも関わらずあたかも完走者のように振る舞おうとした...そんな筆者に天罰が下りました!

image.png

GitHub Actionsでの投稿では記事順が指定されないため、 ぐちゃぐちゃな順番で投稿されてしまったのです!!!!!

この瞬間、筆者のソウルジェムは濁りきり、魔女化しかけました...

「中身のない記事を不正に用意してグッズをもらう不届きなユーザーになってしまった...」

「Rustタグをフォローしているユーザーの方々のTLに、とんでもないノイズを流してしまった...」

「ああ...元々なんのために記事を書き始めたんだっけ...コミュニティに貢献するためではなかったのか...」

38日間寝る間も惜しんでスプラトゥーンをしながら記事執筆してきた努力が、ただ他人に迷惑をかけるだけの行為に成り下がり、そこには醜い物欲と承認欲求の獣がただ一人佇むだけとなった...


...というポエムは置いておいて、「完全に同一な時刻で投稿されてしまった」ために記事に正しい順序が付けられず 、タイムライン、マイページ、APIからの取得全てで 異なる順番で表示 されるようになってしまいました3...!

ページによって取得順が異なることより、 idを固定して並べ替えることも叶わず 、順番通りに記事を掲載できなくなってしまうという、天罰が下ったのでした...🥺

その後 改心と懺悔

魔法ってのはね、徹頭徹尾自分だけの望みを叶えるためのもんなんだよ

もう誰にも頼らない。誰にわかってもらう必要もない

17日を過ぎてからは、「どうせ誰も読みはしないだろう」と割り切って(読みやすさ・簡潔さよりも)クオリティを最優先して記事を書くことにしました!それが中途半端な記事を投稿してしまったことへの懺悔になるだろうという思いもありました。自己満足かもしれないですが、そう、まさしく自己満足のために記事を書き上げることにしました...それから完成まで3週間もかかるとは思いませんでしたが :sweat_smile:

とにかく、無事(?)に昨日38記事に関するまとめ記事を出すことができました! :cracker: :cracker:

Rustで勘違いしていたこと3選 🏄🌴 【100 Exercises To Learn Rust 🦀 完走記事 🏃】

まとめ・所感

結果的に、実に2ヶ月近く格闘していたわけです...その分得られたことも多かったです。

良いこともあった

マラソンに参加しても「いいね」をたくさんもらえるわけでもなく、承認欲求モンスターの自分はここまで話したように闇堕ちしてしまったわけですが、参加してよかったなぁと思ったこともありました。

それは、逆に「いいねを求めないからこそ自由に書けた」ことです。例えば第4回のUFCSの話のように、「ウケないだろうなぁ...」と思って眠らせておいたネタ達を、好きなタイミングで存分に振るえたのがよかったです。普段できないアウトプットを促進してくれる という点では、記事投稿マラソンはかなり良い執筆機会だったなと思います!

反省

今回の反省を活かし、また、自分の怠惰な性格は多分治せないので、冬に開かれるQiitaアドベントカレンダーでは、もし参加する場合イベント開始前に全記事書き上げておくことを誓います。今日から頑張ります!

あとスプラトゥーンはほどほどにします!

...というわけで、グッズ欲しさにズルをした筆者の顛末を話しました...どの口が言うという感じですが、読者の皆さんも夏休みの宿題は溜めないように気をつけましょう。ここまで読んでいただきありがとうございました!

  1. 未完成記事は、「早めに差し替えます!」という文言を入れて投稿していました... 2

  2. 0始まりなので記事番号は-1されています。 2

  3. この件について公式に問い合わせたところ、とても丁寧に調査していただきました、心よりお礼申し上げます :bow: ちなみに別な問い合わせフォームで聞いていて、その後に知ったのですがこの手の内容は Qiita Discussions で聞いた方が良いらしいですね。CLIのActionsについて改善案が思いつきそうな気がするので、もし思いついたら投稿してみようと思います!

23
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?