diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 4292578..152dc46 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -19,5 +19,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" futures = { version = "0.3", default-features=false} serde_repr = "0.1" +reqwest = "0.11.24" +url = "2.5.0" rand = "0.8.5" lazy_static = "1.4.0" diff --git a/backend/src/cache_client.rs b/backend/src/cache_client.rs new file mode 100644 index 0000000..fca0d7c --- /dev/null +++ b/backend/src/cache_client.rs @@ -0,0 +1,118 @@ +use crate::kube::Kube; +use crate::recipe::Recipe; +use thiserror::Error; +use reqwest::Url; +use uuid::Uuid; + +#[derive(Debug, Error)] +/// Errors while parsing the environment variable for the endpoint URL. +pub enum EnvVarParseError { + #[error("Error while getting environment variable. Have you included an equals sign?")] + /// Used when the environment variable hasn't been set properly (such as a missing equals sign). + EnvironmentError(#[from] std::env::VarError), + #[error("Failed to parse URL, is it valid?")] + /// Used when the URL can't be parsed, usually meaning that it's not been written correctly. + UrlParseError(#[from] url::ParseError) +} + +#[derive(Debug, Error)] +/// Errors while communicating with the REST cache server. +pub enum RestError { + #[error("Error while trying to reach REST server.")] + /// Failed to establish connection to the REST server, or got another error (e.g. 404). + ReqwestError(#[from] reqwest::Error), + #[error("Could not parse JSON.")] + /// Couldn't parse the JSON, usually because something else went wrong when trying to establish a connection to the server. + SerdeError(#[from] serde_json::Error), +} + +/// A client which interacts with the cache server. +#[derive(Debug)] +pub struct CacheClient { + /// The endpoint URL. + url: Url, +} +impl CacheClient { + /// Create a new client using an environment variable. + /// + /// # Errors + /// Will error when the environment variable isn't defined correctly, or if the URL couldn't be parsed. + pub fn new() -> Result { + let url_string = std::env::var("ENDPOINT")?; + Ok(CacheClient { + url: Url::parse(&url_string)?, + }) + } + /// Create a new client using a given endpoint URL. + /// + /// # Errors + /// + /// Will error if the URL couldn't be parsed. + pub fn from_url(url: &str) -> Result { + Ok(CacheClient { + url: Url::parse(url)?, + }) + } + /// Gets all the Kubes from the cache server. + /// + /// # Errors + /// + /// If the REST server connection fails, or if the output is an error (like 404), then this will return an Err. + pub async fn get_kubes(&self) -> Result, RestError> { + let res = reqwest::get(self.url.join("kubes").unwrap()).await?.text().await?; + Ok(serde_json::from_str(&res)?) + } + /// Gets all the recipes from the cache server. + /// + /// # Errors + /// + /// If the REST server connection fails, or if the output is an error (like 404), then this will return an Err. + pub async fn get_recipes(&self) -> Result, RestError> { + let res = reqwest::get(self.url.join("kubeRecipes").unwrap()).await?.text().await?; + Ok(serde_json::from_str(&res)?) + } + /// Gets a Kube by its ID from the cache server. + /// + /// # Errors + /// + /// If the REST server connection fails, or if the output is an error (like 404), then this will return an Err. + pub async fn get_kube_by_id(&self, id: Uuid) -> Result { + let res = reqwest::get(self.url.join(format!("kubeById/{}", id).as_str()).unwrap()).await?.text().await?; + Ok(serde_json::from_str(&res)?) + } + /// Gets a recipe by its ID from the cache server. + /// + /// # Errors + /// + /// If the REST server connection fails, or if the output is an error (like 404), then this will return an Err. + pub async fn get_recipe_by_id(&self, id1: Uuid, id2: Uuid) -> Result { + let res = reqwest::get(self.url.join(format!("kubeRecipeByIds/{}/{}", id1, id2).as_str()).unwrap()).await?.text().await?; + Ok(serde_json::from_str(&res)?) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn try_get_kubes() { + let client = CacheClient::from_url("https://hack.djpiper28.co.uk/cache/").unwrap(); + let mut kubes = client.get_kubes().await.unwrap(); + let control_kubes_string = String::from( +"[{\"name\":\"hydrogen\",\"id\":\"cdede93f-d0d7-4b4a-9fde-2a909444c58d\"},{\"name\":\"oxygen\",\"id\":\"8ddcf7ad-61f6-47ff-a49c-4abcec21d6a1\"},{\"name\":\"nitrogen\",\"id\":\"59a64f5b-bcf4-4d2d-bb7f-bc4eceaf41e5\"},{\"name\":\"calcium\",\"id\":\"2b006956-063d-4ca2-b75d-f6e5d67455c9\"},{\"name\":\"iron\",\"id\":\"5e930e14-4597-49b3-95fa-3e6dcc40b1ae\"},{\"name\":\"aluminium\",\"id\":\"d076033a-c583-4d38-8094-249a7fe2972b\"},{\"name\":\"uranium\",\"id\":\"82ac0ed4-62e3-4c5e-af3e-024c38285227\"},{\"name\":\"sodium\",\"id\":\"1c7fda1b-af90-411d-8162-8fd04c4890d3\"},{\"name\":\"chlorine\",\"id\":\"061f6efd-0067-4d71-92b8-a0b58562b906\"},{\"name\":\"light\",\"id\":\"e38ba705-58c1-469d-8eff-7cc01b94fd46\"},{\"name\":\"time\",\"id\":\"6991989a-f347-48eb-8c67-ade4cdc010d0\"},{\"name\":\"silicon\",\"id\":\"72650f96-011b-404d-aba0-2c8aa2f17aeb\"},{\"name\":\"water\",\"id\":\"8cf89e77-bf8b-4cf4-9941-46b853df4480\"},{\"name\":\"tap water\",\"id\":\"5ca230bc-135e-4be8-8be3-7c7c1b3e5484\"},{\"name\":\"salt\",\"id\":\"540710d4-5d7d-42f6-b9b7-aad181affbcf\"},{\"name\":\"sea water\",\"id\":\"74102256-00b5-42aa-9665-1bc81f13c18b\"},{\"name\":\"air\",\"id\":\"88bb179c-0d91-4322-a4e5-d3862bf83a31\"},{\"name\":\"rust\",\"id\":\"ad92587b-1643-469e-8357-1d6ec5ab6380\"},{\"name\":\"feldspar\",\"id\":\"2adfa430-faa4-4ecd-95e1-c6cc8c85f3b5\"},{\"name\":\"sand\",\"id\":\"1db355de-1404-4e7c-bf8b-a6b3355b9dc4\"},{\"name\":\"dirt\",\"id\":\"649e8325-530b-4abb-8fe0-5b79b395e84f\"},{\"name\":\"beach\",\"id\":\"03f9f164-be03-4de3-b5b4-c7549c1ef9e4\"},{\"name\":\"earth\",\"id\":\"66744f80-ccec-4c8b-9025-9edbb75df0a6\"},{\"name\":\"life\",\"id\":\"abd2f9a5-34cd-4b3f-941f-8cf934f6b967\"},{\"name\":\"age\",\"id\":\"bda3c6c5-3b79-418d-8d24-cc533a509065\"},{\"name\":\"energy\",\"id\":\"b4eba917-2179-4cd4-a64e-316fe005f11e\"},{\"name\":\"rock\",\"id\":\"3c545935-1382-4c3d-9771-1fa8492f1b77\"},{\"name\":\"fire\",\"id\":\"1e2e14df-f36b-43a4-9a2a-5112f84abb52\"},{\"name\":\"glass\",\"id\":\"12ec3f8d-0986-42d7-acc2-e6af8ba1842a\"}]" + ); + let mut control_kubes: Vec = serde_json::from_str(&control_kubes_string).unwrap(); + kubes.sort(); + control_kubes.sort(); + assert_eq!(control_kubes, kubes) + } + #[tokio::test] + async fn try_kube_by_id() { + let client = CacheClient::from_url("https://hack.djpiper28.co.uk/cache/").unwrap(); + let kube = client.get_kube_by_id(Uuid::parse_str("5e930e14-4597-49b3-95fa-3e6dcc40b1ae").unwrap()).await.unwrap(); + let expected_string = String::from("{\"name\":\"iron\",\"id\":\"5e930e14-4597-49b3-95fa-3e6dcc40b1ae\"}"); + let expected_kube: Kube = serde_json::from_str(&expected_string).unwrap(); + assert_eq!(expected_kube, kube); + } +} diff --git a/backend/src/grid.rs b/backend/src/grid.rs index 4c9e115..64ced36 100644 --- a/backend/src/grid.rs +++ b/backend/src/grid.rs @@ -7,6 +7,7 @@ fn distance([a, b]: Coordinate, [x, y]: Coordinate) -> u64 { (a.abs_diff(x)).max(b.abs_diff(y)) } +#[derive(Debug, Clone, Copy)] /// The direction in which to grow. For example, going from 3 to 9 would give a `GrowDirection::Expand`. enum GrowDirection { Shrink, @@ -56,6 +57,10 @@ impl Grid { pub fn insert(&mut self, space: Space) { self.spaces.insert(space.coordinate, space); } + /// Removes the space at the given coordinate. If there is no recorded space there, then this returns [`std::option::Option::None`]. + pub fn remove(&mut self, coordinate: Coordinate) -> Option { + self.spaces.remove(&coordinate) + } /// Checks that a coordinate is not beyond the bounds of the grid. pub fn in_bounds(&self, coordinate: [u64; 2]) -> bool { coordinate[0] < self.width && coordinate[1] < self.height @@ -67,7 +72,7 @@ impl Grid { /// ```rust /// use cosmic_kube::grid::Grid; /// use cosmic_kube::space::{ Space, SpaceKind }; - /// + /// /// let grid = Grid::from_spaces( /// vec![ /// Space::new([0, 2], SpaceKind::EmptySpace), @@ -83,6 +88,9 @@ impl Grid { self.spaces.get(&coordinate) } + /// Gets the neighbours that are *n* squares away. + /// + /// In other words, this will look at all the rings which are 1, 2, …, n squares away from the coordinate. It will return any squares found in these rings. pub fn get_neighbours_n_away(&self, coordinate: Coordinate, n: u64) -> Vec<&Space> { let mut coords: Vec = Vec::new(); let mut stack: Vec = self.neighbour_coords_in_bounds(coordinate); @@ -97,18 +105,18 @@ impl Grid { .filter(|c| distance(coordinate, **c) <= n && distance(coordinate, **c) > distance(coord, **c) ) - ) + ); } let mut to_return: Vec<&Space> = Vec::new(); for coord in coords { - match self.spaces.get(&coord) { - Some(space) => to_return.push(space), - None => (), + if let Some(space) = self.spaces.get(&coord) { + to_return.push(space) } } to_return } + /// Gets all the neighbours (directly adjacent squares incl. diagonally) of the coordinate if they're in bounds. fn neighbour_coords_in_bounds(&self, coordinate: Coordinate) -> Vec { let coordinates: [[Option; 2]; 8] = [ [coordinate[0].checked_add(1), Some(coordinate[1])], @@ -129,7 +137,7 @@ impl Grid { .collect::>() } - /// Returns neighbours which are in the grid *and* which aren't [`crate::space::SpaceKind::EmptySpace`]s. + /// Returns neighbours (adjacent incl. diagonally) which are in the grid *and* which aren't [`crate::space::SpaceKind::EmptySpace`]s. pub fn get_nonempty_neighbours(&self, coordinate: Coordinate) -> Vec<&Space> { self.neighbour_coords_in_bounds(coordinate).iter() .map(|coord| self.get_space(*coord)) @@ -206,13 +214,27 @@ impl Grid { } /// This will expand the grid size and change the coordinates of the respective kubes. - /// + /// /// The change in size can be thought of as "rings of squares" to be added around the outside of the grid. So if the grid used to be a 2×2 grid, adding a ring of squares around the outside will give a 4×4 square. + /// + ///
Warning! This function may use a lot of memory! When resizing the grid, the program needs to copy all of the grid's contents to a new `Vec`. If the grid is densely populated then take care when calling it.
+ /// + /// # Errors + /// + /// Will fail if the resize request is invalid, if the new grid will be too big, (bigger than 2^64 - 1). pub fn expand_grid(&mut self, rings_to_add: u64) -> Result<(), ResizeError> { self.change_grid_by_rings(rings_to_add, GrowDirection::Expand) } /// Like [`crate::grid::Grid::expand_grid`], but instead of expanding, this shrinks. + /// + ///
Warning! This function may use a lot of memory! When resizing the grid, the program needs to copy all of the grid's contents to a new `Vec`. If the grid is densely populated then take care when calling it.
+ /// + /// # Errors + /// + /// Will fail if the resize request is invalid. + /// - If the new grid will be too small, (smaller than 1). + /// - If there is a Kube near the edge such that it would be "cut off" when the rings are removed. pub fn shrink_grid(&mut self, rings_to_shrink_by: u64) -> Result<(), ResizeError> { self.change_grid_by_rings(rings_to_shrink_by, GrowDirection::Shrink) } diff --git a/backend/src/kube.rs b/backend/src/kube.rs index e5f755b..fc6ffd8 100644 --- a/backend/src/kube.rs +++ b/backend/src/kube.rs @@ -1,41 +1,48 @@ +use std::str::FromStr; + use uuid::Uuid; use serde::{Deserialize, Serialize}; -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Serialize, Deserialize)] -pub struct KubeId { - uuid: Uuid, -} - -impl KubeId { - pub fn new(name: &str) -> Self { - let mut name = name.to_string(); - name.push_str("kube"); - KubeId { - uuid: Uuid::new_v5(&Uuid::NAMESPACE_DNS, name.as_bytes()), - } - } - - pub fn as_u128(&self) -> u128 { - self.uuid.as_u128() - } - - pub fn uuid(&self) -> Uuid { - self.uuid - } -} - #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub struct Kube { - pub id: KubeId, + pub id: Uuid, pub name: String, } impl Kube { pub fn new(name: String) -> Kube { + let mut name_uuid = name.clone(); + name_uuid.push_str("kube"); Kube { - id: KubeId::new(name.as_str()), + id: Uuid::new_v5(&Uuid::NAMESPACE_DNS, name_uuid.as_bytes()), name, } } + pub fn from_name_uuid(name: &str, uuid: &str) -> Result::Err> { + Ok(Kube { + id: Uuid::from_str(uuid)?, + name: name.to_string() + }) + } } // we should have a placeholder ''loading'' cube we can send over if api is slow + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn parse_kubes() { + let input_json = String::from( +"[{\"name\":\"hydrogen\",\"id\":\"153227c6-c069-4748-b5aa-aafac8abef00\"},{\"name\":\"oxygen\",\"id\":\"3ffe4c9e-5d35-42c9-a70e-1d80c544bdbb\"},{\"name\":\"nitrogen\",\"id\":\"bb155bf1-7200-45e7-b126-f2882f7aecaa\"}]" + ); + let mut expected: Vec = vec![ + Kube::from_name_uuid("hydrogen", "153227c6-c069-4748-b5aa-aafac8abef00").unwrap(), + Kube::from_name_uuid("oxygen", "3ffe4c9e-5d35-42c9-a70e-1d80c544bdbb").unwrap(), + Kube::from_name_uuid("nitrogen", "bb155bf1-7200-45e7-b126-f2882f7aecaa").unwrap(), + ]; + let mut kubes: Vec = serde_json::from_str(&input_json).unwrap(); + expected.sort(); + kubes.sort(); + assert_eq!(expected, kubes); + } +} diff --git a/backend/src/lib.rs b/backend/src/lib.rs index 21ca423..b48b270 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -8,6 +8,8 @@ pub mod kube; pub mod player; pub mod llm; pub mod local_grid; +pub mod cache_client; +pub mod recipe; #[macro_use] extern crate lazy_static; diff --git a/backend/src/llm.rs b/backend/src/llm.rs deleted file mode 100644 index c94aecb..0000000 --- a/backend/src/llm.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::kube::Kube; -use async_trait::async_trait; - -/// Trait for interacting with an LLM. -#[async_trait] -pub trait LLM { - /// Send a query to the LLM and get a [`std::string::String`] response. - async fn query(input: &str) -> String; - /// Ask the LLM to combine the given Kubes and return a new Kube. - async fn combine(&self, kubes: &[Kube]) -> Kube; -} - -/// A fake LLM that functions very basically, not processing the input in any meaningful way. This is most useful for testing functionality of other features which use LLMs. -pub struct FakeLLM { -} -impl FakeLLM { - fn new() -> FakeLLM { - FakeLLM { } - } -} - -#[async_trait] -impl LLM for FakeLLM { - async fn query(input: &str) -> String { - format!("This is a response to: {input}") - } - async fn combine(&self, kubes: &[Kube]) -> Kube { - let mut new_string = String::new(); - for kube in kubes { - new_string.push_str(kube.name.as_str()); - } - Kube::new(new_string) - } -} - -#[cfg(test)] -mod tests { - use super::*; - #[tokio::test] - async fn fake_combine_test() { - let kubes = vec![ - Kube::new(String::from("water")), - Kube::new(String::from("glass")), - ]; - let fake_llm = FakeLLM::new(); - let response_kube = fake_llm.combine(&kubes).await; - assert_eq!( - String::from("waterglass"), - response_kube.name, - ); - } -} diff --git a/backend/src/recipe.rs b/backend/src/recipe.rs new file mode 100644 index 0000000..9c333e3 --- /dev/null +++ b/backend/src/recipe.rs @@ -0,0 +1,12 @@ +use serde::{Deserialize, Serialize}; +use uuid::Uuid; +use crate::kube::Kube; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Recipe { + id: Uuid, + output_id: Uuid, + output_kube: Kube, + kube1_id: Uuid, + kube2_id: Uuid, +} \ No newline at end of file diff --git a/backend/src/ws.rs b/backend/src/ws.rs index d9152e8..219964a 100644 --- a/backend/src/ws.rs +++ b/backend/src/ws.rs @@ -58,7 +58,7 @@ pub async fn client_connection(ws: WebSocket, clients: Clients) { // ->recieve client game info <- send back client game state -// wwwwwwwwwwwwwwwwwwwww i am so tired +// wwwwwwwwwwwwwwwwwwwww i am so tired async fn client_msg(client_id: &str, msg: Message, clients: &Clients) { println!("received message from {}: {:?}", client_id, msg); //debug