1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
use skyline_web::{WebSession};
use std::{collections::HashMap};
use crate::message::*;
use serde::{Serialize, Deserialize};

mod response;
mod message;
pub mod default_handlers;
mod unzipper;

/// progress data
#[derive(Serialize, Deserialize)]
pub struct Progress {
    pub title: String,
    pub info: String,
    /// an u32 in the range 0-100
    pub progress: f64
}

impl Progress {
    pub fn new(title: String, info: String, progress: f64) -> Self {
        return Progress { title: title, info: info, progress: progress.max(0.0).min(1.0) }
    }
}


/// An engine for streamlining the handling of backend requests by `skyline-web` applications.
pub struct RequestEngine {
    is_exit: bool,
    session: WebSession,
    handlers: HashMap<String, Handler>
}

struct Handler {
    pub call_name: String,
    pub arg_count: Option<usize>,
    pub callback:  Box<dyn Fn(&mut MessageContext) -> Result<String, String>>
}


impl RequestEngine {
    /// Creates a new RequestEngine, taking ownership of the session in the process.
    pub fn new(session: WebSession) -> Self {
        return RequestEngine{is_exit: false, session: session, handlers: HashMap::new()};
    }

    /// Registers a handler for requests with the given name.
    /// 
    /// # Arguments
    /// * `request_name` - the name of the request to listen for
    /// * `arg_count` - an optional number of arguments to expect. If `None` is supplied, this
    ///     argument does nothing. If `Some` is supplied, then the engine will validate that the
    ///     inbound request has the required arguments present before calling the registered 
    ///     handler. If the argument count is incorrect, the handler will not be called and an
    ///     error will be returned to the frontend instead.
    /// * `handler` - this is a closure or function, which takes a `MessageContext` and must return 
    ///     `Result<String, String>`. The returned value (`Ok` or `Err`) is then sent to the frontend
    ///     as an `accept()` or `reject()` on the original `Promise`. Note that the returned string 
    ///     can be populated with JSON data. Such JSON can then be used in the frontend via `JSON.parse()`
    ///     to retreive complex structures.
    /// 
    /// Example:
    /// ```
    /// engine.register("my_call_name", Some(3), |context| {
    ///     let args = context.arguments.unwrap();
    ///     return Ok(format!("args: {}, {}, {}", args[0], args[1], args[2]));
    /// })
    /// ```
    pub fn register<S: ToString>(
        &mut self, request_name: S, 
        arg_count: Option<usize>, 
        handler: impl Fn(&mut MessageContext)-> Result<String, String> + 'static) -> &mut Self {
        let name = request_name.to_string();
        self.handlers.insert(name.clone(), Handler { 
            call_name: name, 
            arg_count: arg_count, 
            callback: Box::new(handler)
        });
        return self;
    }

    /// Registers the "default" handlers for some common functionality. 
    /// This aligns with the `nx-request-api` NPM package's DefaultMessenger.
    /// Default calls:
    /// * `ping` 
    ///     - returns ok if the backend responded to the request
    /// * `read_file` 
    ///     - returns the file's contents as a string
    /// * `download_file` 
    ///     - downloads the given file to the given location
    /// * `delete_file` 
    ///     - deletes the given file
    /// * `write_file` 
    ///     - writes the given string to the given file location
    /// * `get_md5`
    ///     - returns the md5 checksum of the given file
    /// * `unzip`
    ///     - unzips the given file as to the given location
    /// * `file_exists`
    ///     - returns whether the given path exists and is a file
    /// * `dir_exists`
    ///     - returns whether the given path exists and is a directory
    /// * `list_all_files`
    ///     - returns a tree structure of the given directory, recursively
    /// * `list_dir`
    ///     - returns a list of the files and directories in the given path (non recursive)
    /// * `get_request`
    ///     - performs a GET request (using `smashnet`) and returns the body as a string
    /// * `exit_session`
    ///     - signals the engine to shutdown and the session to close, unblocking `start()`
    /// * `exit_application`
    ///     - closes the application entirely (you will return to the home menu)
    pub fn register_defaults(&mut self) -> &mut Self {
        default_handlers::register_defaults(self);
        return self;
    }

    /// Start the request engine. This will block and internally loop until `shutdown()` 
    /// has been called by a handler (such as with `exitSession()` in the 
    /// `DefaultMessenger`, or via `context.shutdown()` in a registered custom handler);
    pub fn start(&mut self) {
        while !self.is_exit {
            println!("listening");
            // block until we get a message from the frontend
            let msg = self.session.recv();
            let message = match serde_json::from_str::<Message>(&msg) {
                Ok(message) => {
                    message
                },
                Err(e) => {
                    let str = match &msg.len() {
                        0..=450 => msg.to_string(),
                        _ => format!("{} <truncated for performance> {}", &msg[0..299], &msg[(msg.len() - 100)..(msg.len() - 1)])
                    };
                    println!("Failed to deserialize message: {}\nError: {:?}", str, e);
                    continue;
                }
            };
            let call_name = message.call_name.clone();

            // try to handle the message
            match self.handlers.contains_key(&call_name) {
                true => {
                    println!("handling {}", call_name);
                    let mut ctx = MessageContext::build(message, &self.session);
                    // if an expected arg count was specified in the handler,
                    // we must ensure that this is reality. If not, respond with an error.
                    let handler = self.handlers.get(&call_name).unwrap();
                    if handler.arg_count.is_some() {
                        let count = handler.arg_count.unwrap();
                        // if the number of args is wrong, error out
                        match ctx.arguments {
                            Some(ref args) => {
                                if args.len() != count {
                                    let error = format!("Incorrect number of arguments were provided for {}", &call_name);
                                    ctx.return_error(error.as_ref());
                                    continue;
                                }
                            },
                            None => {
                                let error = format!("No arguments were provided for {}", &call_name);
                                ctx.return_error(error.as_ref());
                                continue;
                            }
                        }
                    }

                    // run the registered callback
                    let result = (handler.callback)(&mut ctx);

                    // if the callback signaled a shutdown, then 
                    // shutdown the engine and session
                    if ctx.is_shutdown() {
                        return;
                    } else {
                        match result {
                            Ok(res) => ctx.return_ok(&res),
                            Err(err) => ctx.return_error(&err)
                        }
                    }
                },
                false => println!("No handler was registered for {}", &message.call_name)
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use skyline_web::WebSession;
    use crate::{RequestEngine, response::Progress};
    

    #[test]
    fn can_construct() {
        let session = unsafe{std::mem::transmute::<&mut WebSession, WebSession>(&mut *std::ptr::null_mut() as &mut WebSession)};
        RequestEngine::new(session)
            .register_defaults()
            .register(
                "test",
                None,
                |context| context.send_progress(Progress{title: "Progress".to_owned(), info: "progress!".to_owned(), progress: 50}));
    }
}