はじめに
こちらに触発されて、同じようなものをrustで作ってみました。
awsの利用料金をslackに通知するアプリケーションです。
https://github.com/pokotyan/aws_cost_rs
こんな感じでコマンドを実行すると、
$ cargo run -- cost
ディレクトリ構成
こんな感じのモノレポの構成になっています。
rustのパッケージマネージャであるcargoにはワークスペースという機能があり、モノレポの構成を簡単に作れます。
.
├── Cargo.lock
├── Cargo.toml
├── README.md
├── cli
│ ├── Cargo.toml
│ └── src
│ ├── get_cost
│ └── main.rs
├── infra
│ ├── Cargo.toml
│ └── src
│ ├── aws
│ └── lib.rs
├── module
│ ├── Cargo.toml
│ └── src
│ ├── lib.rs
│ └── slack_module.rs
├── presenter
│ ├── Cargo.toml
│ └── src
│ ├── lib.rs
│ └── slack_presenter.rs
└── server
├── Cargo.toml
└── src
├── main.rs
└── router.rs
それぞれのパッケージの役割は以下のような感じです。
-
cli
コマンド実行のエントリーポイント -
infra
リポジトリ。awsのsdkを使って料金情報を取ってくる -
module
モジュール置き場。webhookを使ってslackへメッセージを送信するモジュールを置いてる。 -
presenter
sdkから取得した料金情報をslack用の構造体に整形するコードを置いてる。 -
server
使ってない。今後、APIでも料金情報を取得したいとなったら、ここにコードを書く。
依存関係
依存関係はこんな感じになっています。
エントリーポイントとなるcliのところで各クレートを利用しており、他のinfra, module, presenterはどこの層にも依存しない形になっています。
[dependencies]
presenter = {path = "../presenter"}
infra = {path = "../infra"}
module = {path = "../module"}
structopt = "0.3.14"
tokio = { version = "0.2", features = ["full"] }
async-trait = "0.1.31"
[dependencies]
tokio = { version = "0.2", features = ["full"] }
dotenv = "0.15.0"
rusoto_core = "0.43.0"
rusoto_credential = "0.43.0"
rusoto_ce = "0.43.0"
futures = "0.3.5"
anyhow = "1.0"
chrono = "0.4"
async-trait = "0.1.31"
serde = "1.0.110"
serde_json = "1.0.53"
[dependencies]
slack-hook = "0.7"
[dependencies]
anyhow = "1.0"
rusoto_ce = "0.43.0"
slack-hook = "0.7"
DI
cliのところでinfra, module, presenterを利用していますが、infra(AwsRepository)とmodule(SlackModule)についてはDIをしています。
pub struct GetCost<T, U>
where
T: AwsRepository,
U: SlackModule,
{
aws_repository: T,
slack: U,
}
#[async_trait]
pub trait UseCase {
async fn new() -> Self;
async fn run(&self, start: Option<String>, end: Option<String>, channel: Option<String>);
}
#[async_trait]
impl<T: Sync + Send + AwsRepository, U: Sync + Send + SlackModule> UseCase for GetCost<T, U> {
async fn new() -> Self {
let aws_repository = AwsRepository::new().await;
let slack = SlackModule::new();
GetCost {
aws_repository,
slack,
}
}
async fn run(&self, start: Option<String>, end: Option<String>, channel: Option<String>) {
// slackへaws料金を送信するコード。。。
}
}
GetCostという構造体は、
- awsの料金情報を取得するためのAwsRepository
- slackへ情報を送信するためのSlackModule
の二つをプロパティを持っています。どちらとも具象的な処理には依存しておらず、traitに依存するようになっています。
なので、traitさえ満たしていればAwsRepository、SlackModuleは別の実装に切り替えることができる状態です。
そのため、依存関係の向きとしてはCargo.tomlのdependencies的にはこの方向になってますが、
cli → infra
cli → module
DIを行ったことにより、こんな感じになり依存関係が逆転している感じになります。
(cli → trait) ← infra
(cli → trait) ← module
presenterについてはDIをしませんでした。slack用にデータを整形するためのコードとなるので、slackの仕様にどっぷり浸かったコードになってしまいます。そのためどう抽象化すればいいかよくわからず、traitは定義せずそのまま処理を呼ぶようにしました。
実装を入れ替えてみる
では、SlackModuleの実装を slackへ料金情報を送信する
から ターミナルに料金情報を標準出力する
に切り替えてみます。
まず、現在のslackのモジュールはこうなってます。
use slack_hook::{Attachment, PayloadBuilder, Slack};
use std::env;
pub struct Client {
slack: Slack,
}
pub trait SlackModule {
fn new() -> Self;
fn send(&self, channel: String, user_name: String, text: String, attachments: Vec<Attachment>);
}
impl SlackModule for Client {
fn new() -> Self {
let webhook_url = env::var("SLACK_WEBHOOK_URL")
.unwrap_or_else(|_| panic!("SLACK_WEBHOOK_URL is not found."));
Client {
slack: Slack::new(webhook_url.as_str()).unwrap(),
}
}
fn send(&self, channel: String, user_name: String, text: String, attachments: Vec<Attachment>) {
let p = PayloadBuilder::new()
.channel(channel)
.username(user_name)
.text(text)
.attachments(attachments)
.build()
.unwrap();
let _ = self.slack.send(&p);
}
}
traitはこれです。
pub trait SlackModule {
fn new() -> Self;
fn send(&self, channel: String, user_name: String, text: String, attachments: Vec<Attachment>);
}
なので、このtraitを満たすようにして ターミナルに料金情報を標準出力する
実装をつくります。
use crate::slack_module::SlackModule;
use slack_hook::Attachment;
pub struct MockClient {}
impl SlackModule for MockClient {
fn new() -> Self {
MockClient {}
}
fn send(&self, channel: String, user_name: String, text: String, attachments: Vec<Attachment>) {
println!(
"slackへメッセージを送信: channel: {}, user_name: {}, text: {}, attachments: {:?}",
channel, user_name, text, attachments
);
}
}
別の実装ができたので、利用する側を切り替えます。
現在のslackへ送信する処理はこんな感じ(抜粋)になっており、通常のClientを利用するようになってます。
use module::slack::Client;
let get_cost: GetCost<AWS, Client> = UseCase::new().await;
get_cost.run(start, end, channel).await;
これをMockのClientを利用するように変更します。
use module::slack::MockClient;
let get_cost: GetCost<AWS, MockClient> = UseCase::new().await;
get_cost.run(start, end, channel).await;
標準出力に料金情報が表示され、実装が切り替わっていることが分かります。
最後に
rustは難しいですがやってて楽しいですね!