🦀
RustでHatena APIのClientを作った
公式ドキュメントにない言語で実装したので,知見として残しておきます.Basic認証で使うのであれば他の言語でも参考になるかと思います.
はてなブログのAPIを利用する方法
公式ドキュメントによるとOAuthかWSSEまたは,Basic認証が必要とのことでした.
はてなブログAtomPub を利用するために、クライアントは OAuth 認証、WSSE認証、Basic認証のいずれかを行う必要があります。
公式ドキュメントにはPerl,Ruby,Scalaの事例が載っています.
Basic認証をheaderに記載する
Basic認証をつけるには以下のようにはHeaderを追加します.
Authorization: Basic <credentials>
Rustで環境変数からuriとauthenticationを出力する
Clientに全部書き出すとテストができなくなりそうなので分けています.環境変数を読み取り,uriとauthenticationを出力します.
authenticationは先ほどの<credential>
に相当します.これは id:password
をBase64 Encodingしたものです.
In basic HTTP authentication, a request contains a header field in the form of Authorization: Basic <credentials>, where <credentials> is the Base64 encoding of ID and password joined by a single colon :
use base64::engine::general_purpose;
use base64::Engine;
use std::env;
use std::error::Error;
pub(crate) struct HatenaApiConfig {
user_name: String,
blog_domain: String,
api_key: String,
}
impl HatenaApiConfig {
pub(crate) fn new() -> Result<Self, Box<dyn Error>> {
// Get environment variables
let user_name: String = env::var("HATENA_USER_NAME")?;
let blog_domain: String = env::var("HATENA_BLOG_DOMAIN")?;
let api_key: String = env::var("HATENA_API_KEY")?;
// Return HatenaApiConfig instance
Ok(Self {
user_name,
blog_domain,
api_key,
})
}
// 今回はentryのfeedを取得したいので下記のuriを指定
pub(crate) fn api_url(&self) -> String {
format!(
"https://blog.hatena.ne.jp/{}/{}/atom/entry",
self.user_name, self.blog_domain
)
}
// ここで `id:pass` を Base64 Encode
pub(crate) fn authorization(&self) -> String {
general_purpose::STANDARD.encode(format!("{}:{}", self.user_name, self.api_key))
}
}
#[cfg(test)]
mod tests {
use super::*;
use dotenv::dotenv;
fn setup_env_vars() {
env::set_var("HATENA_USER_NAME", "test_user");
env::set_var("HATENA_BLOG_DOMAIN", "test_domain");
env::set_var("HATENA_API_KEY", "test_key");
}
fn clear_env_vars() {
env::remove_var("HATENA_USER_NAME");
env::remove_var("HATENA_BLOG_DOMAIN");
env::remove_var("HATENA_API_KEY");
}
#[test]
fn test_new() {
setup_env_vars();
let config = HatenaApiConfig::new().unwrap();
assert_eq!(config.user_name, "test_user");
assert_eq!(config.blog_domain, "test_domain");
assert_eq!(config.api_key, "test_key");
clear_env_vars();
}
#[test]
fn test_api_url() {
setup_env_vars();
let config = HatenaApiConfig::new().unwrap();
assert_eq!(
config.api_url(),
"https://blog.hatena.ne.jp/test_user/test_domain/atom/entry"
);
clear_env_vars();
}
#[test]
fn test_authorization() {
dotenv().ok();
let config = HatenaApiConfig::new().unwrap();
let user_name: String = env::var("HATENA_USER_NAME").unwrap();
let api_key: String = env::var("HATENA_API_KEY").unwrap();
// write assert_eq
let expected_auth: String =
general_purpose::STANDARD.encode(format!("{}:{}", user_name, api_key));
assert_eq!(config.authorization(), expected_auth);
}
}
Rust製 Hatena API Clientを構成
通常のHTTP RequestにAuthentication credential
を加えて完成です.
use crate::factory::hatena_api::HatenaApiConfig; // いい感じに調整してください
use reqwest::Client;
use std::error::Error;
pub struct HatenaApiClient {
config: HatenaApiConfig,
client: Client,
}
impl HatenaApiClient {
pub fn new(config: HatenaApiConfig) -> Self {
Self {
config,
client: Client::new(),
}
}
pub async fn get_entries(&self) -> Result<String, Box<dyn Error>> {
let res = self
.client
.get(&self.config.api_url())
.header(
"Authorization",
format!("Basic {}", self.config.authorization()),
)
.header("Content-Type", "application/xml")
.send()
.await?;
let body = res.text().await?;
Ok(body)
}
}
#[cfg(test)]
mod tests {
use super::*;
use dotenv::dotenv;
#[tokio::test]
async fn test_get_entries() {
dotenv().ok();
let config = HatenaApiConfig::new().unwrap();
let client = HatenaApiClient::new(config);
let entries = client.get_entries().await;
assert_eq!(entries.is_ok(), true);
let entries = entries.unwrap();
assert_eq!(entries.contains("<entry>"), true);
}
}
Test
テスト結果は以下になります.(Private Projectのため,一部文字列を変更しています)
cargo test
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.35s
Running unittests src/main.rs (target/debug/deps/***)
running 7 tests
...
test factory::hatena_api::tests::test_new ... ok
test factory::hatena_api::tests::test_api_url ... ok
test factory::hatena_api::tests::test_authorization ... ok
test api_client::hatena_api::tests::test_get_entries ... ok
test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.58s
Discussion