+++ date = "2023-01-02" tags = ["rust"] title = "rustでdeeplのcliを作った" slug = "rust-json-request" +++ deeplは翻訳サービスです。 https://www.deepl.com/ja/docs-api 今回は、主にrustのjson, requestのexampleを載せる趣旨で書きます。 ### cargo 面倒なので最初に`cargo.toml`に書きそうなpkgを載せておきます。 ```rust:Cargo.toml 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 ```json:~/.config/msr/deepl.json { "translations": [ { "detected_source_language": "JA", "text": "I'm not sure if I should use reqwest for response as well." } ] } ``` `serde_json`を使います。 ```rust:src/main.rs use serde::{Deserialize, Serialize}; use std::fs; #[derive(Serialize, Deserialize, Debug)] #[serde(tag = "type")] struct DeepData { translations: Vec, } #[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`を使います。 ```rust:src/main.rs #[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として動きます。 ```sh $ mkdir -p ~/.config/msr $ export api="xxx" $ trans-rs a $api $ trans-rs tt "テスト" -l en $ trans-rs tt "test" -l ja ``` ```rust:src/data.rs 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 { 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() } } ``` ```rust:src/main.rs 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, } #[derive(Serialize, Deserialize, Debug)] struct Translation { text: String, detected_source_language : String, } fn main() { let args: Vec = 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のコンパイラがあまりにうるさかったり、または依存関係の解消が面倒な場合にお使いください。 ```sh $ sudo pacman -S curl jq ``` ```rust:src/main.rs 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 ```rust:Cargo.toml reqwest = { version = "*", features = ["json"] } futures = "0.3.5" tokio = { version = "*", features = ["full"] } ```