rustでdeeplのcliを作った
deeplは翻訳サービスです。
https://www.deepl.com/ja/docs-api
今回は、主にrustのjson, requestのexampleを載せる趣旨で書きます。
cargo
面倒なので最初にcargo.toml
に書きそうなpkgを載せておきます。
seahorse = "*"
dotenv = "0.15"
serde_derive = "1.0"
serde_json = "1.0"
serde = "*"
config = { git = "https://github.com/mehcode/config-rs", branch = "master" }
shellexpand = "*"
toml = "*"
reqwest = "*"
tokio = { version = "1", features = ["full"] }
json
{
"translations": [
{
"detected_source_language": "JA",
"text": "I'm not sure if I should use reqwest for response as well."
}
]
}
serde_json
を使います。
use serde::{Deserialize, Serialize};
use std::fs;
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "type")]
struct DeepData {
translations: Vec<Translation>,
}
#[derive(Serialize, Deserialize, Debug)]
struct Translation {
text: String,
detected_source_language : String,
}
fn main() {
let l = shellexpand::tilde("~") + "/.config/msr/deepl.json";
let l = l.to_string();
let o = fs::read_to_string(&l).expect("could not read file");
let p: DeepData = serde_json::from_str(&o).unwrap();
let o = &p.translations[0].text;
println!("{}", o);
}
reqwest
reqwest
を使います。
#[tokio::main]
async fn deepl(message: String,lang: String) -> reqwest::Result<()> {
let data = Deeps::new().unwrap();
let data = Deeps {
api: data.api,
};
let api = "DeepL-Auth-Key ".to_owned() + &data.api;
let mut params = HashMap::new();
params.insert("text", &message);
params.insert("target_lang", &lang);
let client = reqwest::Client::new();
let res = client
.post("https://api-free.deepl.com/v2/translate")
.header(AUTHORIZATION, api)
.header(CONTENT_TYPE, "json")
.form(¶ms)
.send()
.await?
.text()
.await?;
let p: DeepData = serde_json::from_str(&res).unwrap();
let o = &p.translations[0].text;
//println!("{}", res);
println!("{}", o);
Ok(())
}
cli:trans-rs
以下は全体のcliの作りです。cli-toolとして動きます。
$ mkdir -p ~/.config/msr
$ export api="xxx"
$ trans-rs a $api
$ trans-rs tt "テスト" -l en
$ trans-rs tt "test" -l ja
use config::{Config, ConfigError, File};
use serde_derive::Deserialize;
#[derive(Debug, Deserialize)]
#[allow(unused)]
pub struct Deep {
pub api: String,
}
impl Deep {
pub fn new() -> Result<Self, ConfigError> {
let d = shellexpand::tilde("~") + "/.config/msr/deepl.toml";
let s = Config::builder()
.add_source(File::with_name(&d))
.add_source(config::Environment::with_prefix("APP"))
.build()?;
s.try_deserialize()
}
}
pub mod data;
use std::env;
use std::fs;
use std::io::prelude::*;
use data::Deep as Deeps;
use seahorse::{App, Command, Context, Flag, FlagType};
use serde::{Deserialize, Serialize};
use reqwest::header::AUTHORIZATION;
use reqwest::header::CONTENT_TYPE;
use std::collections::HashMap;
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "type")]
struct DeepData {
translations: Vec<Translation>,
}
#[derive(Serialize, Deserialize, Debug)]
struct Translation {
text: String,
detected_source_language : String,
}
fn main() {
let args: Vec<String> = env::args().collect();
let app = App::new(env!("CARGO_PKG_NAME"))
.author(env!("CARGO_PKG_AUTHORS"))
.description(env!("CARGO_PKG_DESCRIPTION"))
.version(env!("CARGO_PKG_VERSION"))
.usage("trans-rs [option] [x]")
.command(
Command::new("translate")
.usage("trans-rs tt {}")
.description("translate message, ex: $ trans-rs tt $text -l en")
.alias("tt")
.action(tt)
.flag(
Flag::new("lang", FlagType::String)
.description("Lang flag")
.alias("l"),
)
)
.command(
Command::new("api")
.usage("trans-rs a {}")
.description("api change, ex : $ msr a $api")
.alias("a")
.action(a),
)
;
app.run(args);
}
#[tokio::main]
async fn deepl(message: String,lang: String) -> reqwest::Result<()> {
let data = Deeps::new().unwrap();
let data = Deeps {
api: data.api,
};
let api = "DeepL-Auth-Key ".to_owned() + &data.api;
let mut params = HashMap::new();
params.insert("text", &message);
params.insert("target_lang", &lang);
let client = reqwest::Client::new();
let res = client
.post("https://api-free.deepl.com/v2/translate")
.header(AUTHORIZATION, api)
.header(CONTENT_TYPE, "json")
.form(¶ms)
.send()
.await?
.text()
.await?;
let p: DeepData = serde_json::from_str(&res).unwrap();
let o = &p.translations[0].text;
//println!("{}", res);
println!("{}", o);
Ok(())
}
#[allow(unused_must_use)]
fn tt(c: &Context) {
let m = c.args[0].to_string();
if let Ok(lang) = c.string_flag("lang") {
deepl(m,lang.to_string());
} else {
let lang = "ja";
deepl(m,lang.to_string());
}
}
#[allow(unused_must_use)]
fn a(c: &Context) {
let api = c.args[0].to_string();
let o = "api='".to_owned() + &api.to_string() + &"'".to_owned();
let o = o.to_string();
let l = shellexpand::tilde("~") + "/.config/msr/deepl.toml";
let l = l.to_string();
let mut l = fs::File::create(l).unwrap();
if o != "" {
l.write_all(&o.as_bytes()).unwrap();
}
println!("{:#?}", l);
}
shell-command
rustでのjsonやrequestの処理について、shell-commandを使う方法もあります。
rustのコンパイラがあまりにうるさかったり、または依存関係の解消が面倒な場合にお使いください。
use std::process::Command;
let api = "Authorization: DeepL-Auth-Key ".to_owned() + &data.api;
let txt = "text=".to_owned() + &message.to_string();
let lang = "target_lang=".to_owned() + ⟨
let output = Command::new("curl").arg("-X").arg("POST").arg("https://api-free.deepl.com/v2/translate")
.arg("-H").arg(api)
.arg("-d").arg(txt)
.arg("-d").arg(lang)
.output().expect("curl");
//let p: DeepData = serde_json::from_str(&o)?;
//let o = &p.translations[0].text;
let o = String::from_utf8_lossy(&output.stdout);
let o = o.to_string();
let l = shellexpand::tilde("~") + "/.config/msr/deepl.json";
let l = l.to_string();
let mut l = fs::File::create(l).unwrap();
if o != "" {
l.write_all(&o.as_bytes()).unwrap();
}
let l = shellexpand::tilde("~") + "/.config/msr/deepl.json";
let l = l.to_string();
let output = Command::new("jq").arg("-r")
.arg(".translations|.[]|.text")
.arg(l)
.output().expect("jq");
let o = String::from_utf8_lossy(&output.stdout);
let o = o.to_string();
features
reqwest + tokio + featuresを使う場合、cargo.tomlに以下のようなfeaturesのsupportを追加してください。
.await? doesn’t have a size known at compile-time
reqwest = { version = "*", features = ["json"] }
futures = "0.3.5"
tokio = { version = "*", features = ["full"] }