2023-01-02 / @syui

rust

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(&params)
        .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(&params)
        .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のコンパイラがあまりにうるさかったり、または依存関係の解消が面倒な場合にお使いください。

$ sudo pacman -S curl jq
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() + &lang;
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"] }