diff --git a/.code-samples.meilisearch.yaml b/.code-samples.meilisearch.yaml index f793d026..f072d5aa 100644 --- a/.code-samples.meilisearch.yaml +++ b/.code-samples.meilisearch.yaml @@ -1823,3 +1823,23 @@ reset_localized_attribute_settings_1: |- .reset_localized_attributes() .await .unwrap(); +get_network_1: |- + let network: Network = client.get_network().await.unwrap(); +update_network_1: |- + client.update_network( + NetworkUpdate::new() + .with_self("ms-00") + .with_remotes(&[ + Remote { + name: "ms-00".to_string(), + url: "http://INSTANCE_URL".to_string(), + search_api_key: Some("INSTANCE_API_KEY".to_string()), + }, + Remote { + name: "ms-01".to_string(), + url: "http://ANOTHER_INSTANCE_URL".to_string(), + search_api_key: Some("ANOTHER_INSTANCE_API_KEY".to_string()), + }, + ]), + ) + .await.unwrap(); diff --git a/src/client.rs b/src/client.rs index 3e5068c9..1dfb22c6 100644 --- a/src/client.rs +++ b/src/client.rs @@ -8,6 +8,7 @@ use crate::{ errors::*, indexes::*, key::{Key, KeyBuilder, KeyUpdater, KeysQuery, KeysResults}, + network::{Network, NetworkUpdate}, request::*, search::*, task_info::TaskInfo, @@ -1107,6 +1108,76 @@ impl Client { Ok(tasks) } + /// Get the network configuration (sharding). + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, features::ExperimentalFeatures}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # ExperimentalFeatures::new(&client).set_network(true).update().await.unwrap(); + /// let network = client.get_network().await.unwrap(); + /// # }); + /// ``` + pub async fn get_network(&self) -> Result { + let network = self + .http_client + .request::<(), (), Network>( + &format!("{}/network", self.host), + Method::Get { query: () }, + 200, + ) + .await?; + + Ok(network) + } + + /// Update the network configuration (sharding). + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, features::ExperimentalFeatures, network::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # ExperimentalFeatures::new(&client).set_network(true).update().await.unwrap(); + /// let network = client.update_network( + /// NetworkUpdate::new() + /// // .reset_self() + /// // .reset_remotes() + /// // .delete_remotes(&["ms-00"]) + /// .with_self("ms-00") + /// .with_remotes(&[Remote { + /// name: "ms-00".to_string(), + /// url: "http://localhost:7700".to_string(), + /// search_api_key: Some("secret".to_string()), + /// }]), + /// ) + /// .await.unwrap(); + /// # }); + /// ``` + pub async fn update_network(&self, network_update: &NetworkUpdate) -> Result { + self.http_client + .request::<(), &NetworkUpdate, Network>( + &format!("{}/network", self.host), + Method::Patch { + body: network_update, + query: (), + }, + 200, + ) + .await + } + /// Generates a new tenant token. /// /// # Example @@ -1205,7 +1276,10 @@ mod tests { use meilisearch_test_macro::meilisearch_test; - use crate::{client::*, key::Action, reqwest::qualified_version}; + use crate::{ + client::*, features::ExperimentalFeatures, key::Action, network::Remote, + reqwest::qualified_version, + }; #[derive(Debug, Serialize, Deserialize, PartialEq)] struct Document { @@ -1355,6 +1429,80 @@ mod tests { assert_eq!(tasks.limit, 20); } + async fn enable_network(client: &Client) { + ExperimentalFeatures::new(client) + .set_network(true) + .update() + .await + .unwrap(); + } + + #[meilisearch_test] + async fn test_get_network(client: Client) { + enable_network(&client).await; + + let network = client.get_network().await.unwrap(); + assert!(matches!( + network, + Network { + self_: _, + remotes: _, + } + )) + } + + #[meilisearch_test] + async fn test_update_network_self(client: Client) { + enable_network(&client).await; + + client + .update_network(NetworkUpdate::new().reset_self()) + .await + .unwrap(); + + let network = client + .update_network(NetworkUpdate::new().with_self("ms-00")) + .await + .unwrap(); + + assert_eq!(network.self_, Some("ms-00".to_string())); + } + + #[meilisearch_test] + async fn test_update_network_remotes(client: Client) { + enable_network(&client).await; + + client + .update_network(NetworkUpdate::new().reset_remotes()) + .await + .unwrap(); + + let network = client + .update_network(NetworkUpdate::new().with_remotes(&[Remote { + name: "ms-00".to_string(), + url: "http://localhost:7700".to_string(), + search_api_key: Some("secret".to_string()), + }])) + .await + .unwrap(); + + assert_eq!( + network.remotes, + vec![Remote { + name: "ms-00".to_string(), + url: "http://localhost:7700".to_string(), + search_api_key: Some("secret".to_string()), + }] + ); + + let network = client + .update_network(NetworkUpdate::new().delete_remotes(&["ms-00"])) + .await + .unwrap(); + + assert_eq!(network.remotes, vec![]); + } + #[meilisearch_test] async fn test_get_keys(client: Client) { let keys = client.get_keys().await.unwrap(); diff --git a/src/lib.rs b/src/lib.rs index c0e4a74a..882043b7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -244,6 +244,8 @@ pub mod features; pub mod indexes; /// Module containing the [`Key`](key::Key) struct. pub mod key; +// Module containing the [`Network`](network::Network) struct. +pub mod network; pub mod request; /// Module related to search queries and results. pub mod search; diff --git a/src/network.rs b/src/network.rs new file mode 100644 index 00000000..7075a71c --- /dev/null +++ b/src/network.rs @@ -0,0 +1,304 @@ +use std::collections::{BTreeMap, HashMap}; + +use serde::{Deserialize, Deserializer, Serialize}; + +#[derive(PartialEq, Debug, Serialize, Deserialize, Clone)] +pub struct Network { + #[serde(rename = "self")] + pub self_: Option, + #[serde(deserialize_with = "network_deserializer")] + pub remotes: Vec, +} + +fn network_deserializer<'de, D>(d: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let map: BTreeMap> = Deserialize::deserialize(d)?; + + Ok(map + .into_iter() + .map(|(name, remote)| Remote { + name, + url: remote.get("url").cloned().unwrap_or_default(), + search_api_key: remote.get("searchApiKey").cloned(), + }) + .collect()) +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Remote { + #[serde(skip_serializing, skip_deserializing)] + pub name: String, + + pub url: String, + pub search_api_key: Option, +} + +#[derive(Serialize, Default)] +pub struct NetworkUpdate { + #[serde(rename = "self", skip_serializing_if = "Option::is_none")] + self_: Option>, + remotes: Option>>, +} + +impl NetworkUpdate { + #[must_use] + pub fn new() -> Self { + NetworkUpdate { + self_: None, + remotes: Some(BTreeMap::new()), + } + } + + pub fn reset_self(&mut self) -> &mut Self { + self.self_ = Some(None); + self + } + + pub fn reset_remotes(&mut self) -> &mut Self { + self.remotes = None; + self + } + + pub fn with_self(&mut self, new_self: &str) -> &mut Self { + self.self_ = Some(Some(new_self.to_string())); + self + } + + pub fn with_remotes(&mut self, new_remotes: &[Remote]) -> &mut Self { + if self.remotes.is_none() { + self.remotes = Some(BTreeMap::new()); + } + + self.remotes.as_mut().unwrap().extend( + new_remotes + .iter() + .map(|new_remote| (new_remote.name.clone(), Some(new_remote.clone()))), + ); + + self + } + + pub fn delete_remotes(&mut self, remotes_to_delete: &[&str]) -> &mut Self { + if self.remotes.is_none() { + self.remotes = Some(BTreeMap::new()); + } + + self.remotes.as_mut().unwrap().extend( + remotes_to_delete + .iter() + .map(|remote_name| (remote_name.to_string(), None)), + ); + + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_deserialize_network() { + let example_json = r###" + { + "self": "ms-00", + "remotes": { + "ms-00": { + "url": "http://ms-1235.example.meilisearch.io", + "searchApiKey": "Ecd1SDDi4pqdJD6qYLxD3y7VZAEb4d9j6LJgt4d6xas" + }, + "ms-01": { + "url": "http://ms-4242.example.meilisearch.io", + "searchApiKey": "hrVu-OMcjPGElK7692K7bwriBoGyHXTMvB5NmZkMKqQ" + } + } + } + "###; + + let actual_remotes: Network = serde_json::from_str(example_json).unwrap(); + + let expected_remotes = Network { + self_: Some("ms-00".to_string()), + remotes: vec![ + Remote { + name: "ms-00".to_string(), + url: "http://ms-1235.example.meilisearch.io".to_string(), + search_api_key: Some("Ecd1SDDi4pqdJD6qYLxD3y7VZAEb4d9j6LJgt4d6xas".to_string()), + }, + Remote { + name: "ms-01".to_string(), + url: "http://ms-4242.example.meilisearch.io".to_string(), + search_api_key: Some("hrVu-OMcjPGElK7692K7bwriBoGyHXTMvB5NmZkMKqQ".to_string()), + }, + ], + }; + + assert_eq!(actual_remotes, expected_remotes); + } + + #[test] + fn test_serialize_network_update_reset_all() { + let mut expected_reset_self_json = r###" + { + "self": null, + "remotes": null + } + "### + .to_string(); + expected_reset_self_json.retain(|c| !c.is_whitespace()); + + let mut reset_self_json = + serde_json::to_string(NetworkUpdate::new().reset_self().reset_remotes()).unwrap(); + reset_self_json.retain(|c| !c.is_whitespace()); + + assert_eq!(expected_reset_self_json, reset_self_json); + } + + #[test] + fn test_serialize_network_update_reset_self() { + let mut expected_reset_self_json = r###" + { + "self": null, + "remotes": {} + } + "### + .to_string(); + expected_reset_self_json.retain(|c| !c.is_whitespace()); + + let mut reset_self_json = serde_json::to_string(NetworkUpdate::new().reset_self()).unwrap(); + reset_self_json.retain(|c| !c.is_whitespace()); + + assert_eq!(expected_reset_self_json, reset_self_json); + } + + #[test] + fn test_serialize_network_update_with_self() { + let mut expected_with_self_json = r###" + { + "self": "ms-00", + "remotes": {} + } + "### + .to_string(); + expected_with_self_json.retain(|c| !c.is_whitespace()); + + let mut with_self_json = + serde_json::to_string(NetworkUpdate::new().with_self("ms-00")).unwrap(); + with_self_json.retain(|c| !c.is_whitespace()); + + assert_eq!(expected_with_self_json, with_self_json); + } + + #[test] + fn test_serialize_network_update_reset_remotes() { + let mut expected_reset_remotes_json = r###" + { + "remotes": null + } + "### + .to_string(); + expected_reset_remotes_json.retain(|c| !c.is_whitespace()); + + let mut reset_remotes_json = + serde_json::to_string(NetworkUpdate::new().reset_remotes()).unwrap(); + reset_remotes_json.retain(|c| !c.is_whitespace()); + + assert_eq!(expected_reset_remotes_json, reset_remotes_json); + } + + #[test] + fn test_serialize_network_update_add_remotes() { + let mut expected_with_remotes_json = r###" + { + "remotes": { + "ms-00": { + "url": "http://localhost:7700", + "searchApiKey": "hello_world" + }, + "ms-01": { + "url": "http://localhost:7701", + "searchApiKey": "another_key" + } + } + } + "### + .to_string(); + expected_with_remotes_json.retain(|c| !c.is_whitespace()); + + let mut with_remotes_json = serde_json::to_string(NetworkUpdate::new().with_remotes(&[ + Remote { + name: "ms-00".to_string(), + url: "http://localhost:7700".to_string(), + search_api_key: Some("hello_world".to_string()), + }, + Remote { + name: "ms-01".to_string(), + url: "http://localhost:7701".to_string(), + search_api_key: Some("another_key".to_string()), + }, + ])) + .unwrap(); + with_remotes_json.retain(|c| !c.is_whitespace()); + + assert_eq!(expected_with_remotes_json, with_remotes_json); + } + + #[test] + fn test_serialize_network_update_delete_remotes() { + let mut expected_with_remotes_json = r###" + { + "remotes": { + "ms-00": null, + "ms-01": null + } + } + "### + .to_string(); + expected_with_remotes_json.retain(|c| !c.is_whitespace()); + + let mut with_remotes_json = + serde_json::to_string(NetworkUpdate::new().delete_remotes(&["ms-00", "ms-01"])) + .unwrap(); + with_remotes_json.retain(|c| !c.is_whitespace()); + + assert_eq!(expected_with_remotes_json, with_remotes_json); + } + + #[test] + fn test_serialize_network_update_operation_override() { + let mut expected_overridden_json = r###" + { + "remotes": { + "ms-00": null, + "ms-01": null + } + } + "### + .to_string(); + expected_overridden_json.retain(|c| !c.is_whitespace()); + + let mut overriden_json = serde_json::to_string( + NetworkUpdate::new() + .with_remotes(&[ + Remote { + name: "ms-00".to_string(), + url: "http://localhost:7700".to_string(), + search_api_key: Some("hello_world".to_string()), + }, + Remote { + name: "ms-01".to_string(), + url: "http://localhost:7701".to_string(), + search_api_key: Some("another_key".to_string()), + }, + ]) + .delete_remotes(&["ms-00", "ms-01"]), + ) + .unwrap(); + overriden_json.retain(|c| !c.is_whitespace()); + + assert_eq!(expected_overridden_json, overriden_json); + } +}