pebbles 0.0.1

A unfinished webautomation framework for working with Firefox and the geckodriver.
Documentation
use crate::result::{PebblesError, PebblesErrorDetails};
use reqwest::blocking::{Client, Response};
use serde_json::Value;
use std::net::{SocketAddr, TcpListener, TcpStream};
use std::process::{Child, Command, Stdio};

pub enum RequestType {
    Post,
    Get,
    Delete,
}

pub struct Service {
    port: u16,
    session_id: String,
    client: Client,
    process: Child,
}

impl Service {
    fn find_free_port() -> u16 {
        let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind to a socket");
        let port = listener.local_addr().unwrap().port();
        drop(listener);
        port
    }

    fn launch_gecko(command: &str, port: &u16) -> Child {
        let process = Command::new(command)
            .arg("--port")
            .arg(port.to_string())
            .stdout(Stdio::null())
            .spawn()
            .expect("Failed to launch process");
        process
    }

    fn accepting_connections(port: &u16) -> bool {
        let address: SocketAddr = format!("127.0.0.1:{}", port.to_string()).parse().unwrap();
        match TcpStream::connect_timeout(&address, std::time::Duration::from_secs(1)) {
            Ok(_) => true,
            Err(_) => false,
        }
    }

    fn launch_firefox(
        client: &Client,
        port: &u16,
        capabilities: &Value,
    ) -> Result<String, PebblesError> {
        let response = client
            .post(format!("http://localhost:{}/session", port.to_string()))
            .header("Content-Type", "application/json")
            .body(match serde_json::to_string(&capabilities) {
                Ok(json_string) => json_string,
                Err(_) => {
                    return Err(PebblesError::NewSessionError(PebblesErrorDetails::new(
                        "Capabilities not in Valid json Format",
                    )))
                }
            })
            .send()?;

        if !response.status().is_success() {
            return Err(PebblesError::NewSessionError(PebblesErrorDetails::new(
                format!(
                    "Session Start Request Failed: /
                        Status Code: {}",
                    response.status().as_u16()
                ),
            )));
        }
        let response_json: Value = response.json::<Value>()?;
        match response_json["value"]["sessionId"].as_str() {
            Some(session_id) => Ok(session_id.to_string()),
            None => Err(PebblesError::NewSessionError(PebblesErrorDetails::new(
                "Faild not load session ID from response json",
            ))),
        }
    }

    pub fn new(capabilities: &Value, geckodriver_cmd: &str) -> Result<Service, PebblesError> {
        let port = Self::find_free_port();
        let mut process = Self::launch_gecko(geckodriver_cmd, &port);

        while !Self::accepting_connections(&port) {
            // Check if the process is still running
            if let Some(_) = process.try_wait().expect("Failed to get process status") {
                return Err(PebblesError::NewSessionError(PebblesErrorDetails::new(
                    "Process has Exited Early",
                )));
            }
        }

        let client = Client::new();
        let session_id = Self::launch_firefox(&client, &port, &capabilities)?;

        //Return the service
        Ok(Service {
            port: port,
            session_id: session_id,
            client: client,
            process: process,
        })
    }

    fn request(
        &self,
        method: RequestType,
        path: &str,
        body: Option<&Value>,
    ) -> Result<Response, PebblesError> {
        let url = format!(
            "http://localhost:{}/session/{}{}",
            self.port.to_string(),
            &self.session_id,
            path
        );
        let mut request = match method {
            RequestType::Post => self.client.post(url),
            RequestType::Get => self.client.get(url),
            RequestType::Delete => self.client.delete(url),
        };
        request = request.header("Content-Type", "application/json");
        if let Some(body) = body {
            request = request.body(match serde_json::to_string(body) {
                Ok(json_string) => json_string,
                Err(_) => {
                    return Err(PebblesError::RequestError(PebblesErrorDetails::new(
                        "Unable to format body as String",
                    )))
                }
            });
        }
        let response = request.send()?;
        if !response.status().is_success() {
            return Err(PebblesError::RequestError(PebblesErrorDetails::new(
                format!(
                    "Request Failed: Status /
                    Code: {}/
                    Body: {}",
                    response.status().as_u16(),
                    match response.text() {Ok(text) => text, Err(_) => "None".to_string()},
                ),
            )));
        }
        Ok(response)
    }

    pub fn post(&self, path: &str, body: &Value) -> Result<Response, PebblesError> {
        return self.request(RequestType::Post, path, Some(body));
    }

    pub fn get(&self, path: &str) -> Result<Response, PebblesError> {
        return self.request(RequestType::Get, path, None);
    }

    pub fn delete(&self, path: &str) -> Result<Response, PebblesError> {
        return self.request(RequestType::Delete, path, None);
    }
}

impl Drop for Service {
    fn drop(&mut self) {
        let _ = self.delete("");
        match self.process.kill() {
            Ok(_) => println!("Process killed successfully"),
            Err(e) => println!("Failed to kill process: {}", e),
        }
    }
}