osrs_cli/
lib.rs

1mod commands;
2mod config;
3mod error;
4mod utils;
5
6use crate::{
7    commands::{
8        CalcCommand, Command, CommandType, ConfigCommand, HiscoreCommand,
9        PingCommand, PriceCommand, WikiCommand,
10    },
11    utils::context::CommandContext,
12};
13use std::io::Write;
14use structopt::StructOpt;
15
16/// All top-level CLI commands.
17#[derive(Debug, StructOpt)]
18enum OsrsCommandType {
19    Calc(CalcCommand),
20    #[structopt(visible_alias = "cfg")]
21    Config(ConfigCommand),
22    #[structopt(visible_alias = "hs")]
23    Hiscore(HiscoreCommand),
24    Ping(PingCommand),
25    #[structopt(visible_alias = "ge")]
26    Price(PriceCommand),
27    Wiki(WikiCommand),
28}
29
30impl<O: Write> CommandType<O> for OsrsCommandType {
31    fn command(&self) -> &dyn Command<O> {
32        match &self {
33            Self::Calc(cmd) => cmd,
34            Self::Config(cmd) => cmd,
35            Self::Hiscore(cmd) => cmd,
36            Self::Ping(cmd) => cmd,
37            Self::Price(cmd) => cmd,
38            Self::Wiki(cmd) => cmd,
39        }
40    }
41}
42
43/// Oldschool RuneScape CLI.
44/// Bugs/suggestions: https://github.com/LucasPickering/osrs-cli/issues
45#[derive(Debug, StructOpt)]
46pub struct OsrsOptions {
47    #[structopt(subcommand)]
48    cmd: OsrsCommandType,
49}
50
51impl OsrsOptions {
52    /// Execute the command defined by this options object. This is the main
53    /// entrypoint to the program. Callers can customize how the these options
54    /// are parsed, then call this function to execute.
55    ///
56    /// The type `O` defines how command output will be written. For native
57    /// environments, this will be stdout, whereas for the browser it will be
58    /// a string buffer (which presumably gets written to the DOM).
59    pub async fn run<O: Write>(self, output: O) -> anyhow::Result<()> {
60        let context = CommandContext::load(output)?;
61        self.cmd.command().execute(context).await
62    }
63}
64
65/// Public WebAssembly API
66#[cfg(target_family = "wasm")]
67mod wasm {
68    use super::*;
69    use crate::error::OsrsError;
70    use wasm_bindgen::prelude::*;
71
72    /// Initialization. This function gets called when the wasm module is loaded
73    #[wasm_bindgen(start)]
74    pub fn start() {
75        std::panic::set_hook(Box::new(console_error_panic_hook::hook));
76    }
77
78    /// Wasm entrypoint, parses and executes arguments. Command should be a list
79    /// of arguments, where each one is a string. You *must* pass the binary
80    /// name as the first argument.
81    ///
82    /// We can't take in Vec<String> because wasm_bindgen.
83    /// Clean up after https://github.com/rustwasm/wasm-bindgen/issues/168
84    #[wasm_bindgen(js_name = runCommand)]
85    pub async fn run_command(command: Vec<JsValue>) -> String {
86        // Replace this with a try block after is stable
87        // https://github.com/rust-lang/rust/issues/31436
88        async fn helper(command: Vec<JsValue>) -> anyhow::Result<String> {
89            // Convert each arg to a string
90            let args: Vec<String> = command
91                .into_iter()
92                .map::<Result<String, OsrsError>, _>(|value| {
93                    value.as_string().ok_or(OsrsError::ExpectedString)
94                })
95                // Pull all results into one
96                .collect::<Result<_, _>>()?;
97
98            // Write all output to a buffer, which we'll return to JS
99            // TODO figure out how to stream output back to JS
100            let mut output = Vec::new();
101            let options = OsrsOptions::from_iter_safe(args)?;
102            options.run(&mut output).await?;
103            Ok(String::from_utf8(output)?)
104        }
105
106        match helper(command).await {
107            Ok(output) => output,
108            // TODO return error here instead and make the caller print it
109            Err(err) => format!("{}\n", err),
110        }
111    }
112}