diff --git a/Cargo.lock b/Cargo.lock index aa963b7..f2f3425 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -216,11 +216,36 @@ checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-link", ] +[[package]] +name = "cloudevents-sdk" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d48721bdd20fbaa008c5180469dc76363776cb68ace437064e916044dd2d92a" +dependencies = [ + "async-trait", + "base64 0.22.1", + "bitflags 2.9.0", + "bytes", + "chrono", + "delegate-attr", + "hostname", + "http", + "reqwest", + "serde", + "serde_json", + "snafu", + "url", + "uuid", + "web-sys", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -288,6 +313,17 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +[[package]] +name = "delegate-attr" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51aac4c99b2e6775164b412ea33ae8441b2fde2dbf05a20bc0052a63d08c475b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "deranged" version = "0.4.0" @@ -378,8 +414,12 @@ dependencies = [ name = "eventsourcingdb-client-rust" version = "0.1.0" dependencies = [ + "chrono", + "cloudevents-sdk", "eventsourcingdb-client-rust", "reqwest", + "serde", + "serde_json", "testcontainers", "thiserror 2.0.12", "tokio", @@ -654,6 +694,17 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "hostname" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65" +dependencies = [ + "cfg-if", + "libc", + "windows-link", +] + [[package]] name = "http" version = "1.3.1" @@ -1814,6 +1865,27 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +[[package]] +name = "snafu" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "223891c85e2a29c3fe8fb900c1fae5e69c2e42415e3177752e8718475efa5019" +dependencies = [ + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c3c6b7927ffe7ecaa769ee0e3994da3b8cafc8f444578982c83ecb161af917" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "socket2" version = "0.5.9" @@ -2257,6 +2329,17 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "uuid" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" +dependencies = [ + "getrandom 0.3.2", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index 351b80c..1a22ad5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,16 +5,23 @@ edition = "2024" [features] default = [] +cloudevents = ["dep:cloudevents-sdk"] testcontainer = ["dep:testcontainers"] [dependencies] -thiserror = "2.0.12" +chrono = { version = "0.4.41", features = ["serde"] } +cloudevents-sdk = { version = "0.8.0", features = ["reqwest"], optional = true } url = "2.5.4" -testcontainers = { version = "0.24.0", features = ["http_wait"], optional = true } +reqwest = { version = "0.12.15", features = ["json"] } +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.140" +testcontainers = { version = "0.24.0", features = [ + "http_wait", +], optional = true } +thiserror = "2.0.12" [dev-dependencies] -eventsourcingdb-client-rust = {path = ".", features = ["testcontainer"]} -reqwest = "0.12.15" +eventsourcingdb-client-rust = { path = ".", features = ["testcontainer"] } testcontainers = { version = "0.24.0", features = ["http_wait"] } tokio = { version = "1.44.2", features = ["full"] } tokio-test = "0.4.4" diff --git a/README.md b/README.md index c72ad36..7e04257 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ The official Rust client SDK for [EventSourcingDB](https://www.eventsourcingdb.i This is a work in progress and not yet ready for production use. Based on the [compliance criteria](https://docs.eventsourcingdb.io/client-sdks/compliance-criteria/) the SDK covers these criteria: -- ❌ [Essentials](https://docs.eventsourcingdb.io/client-sdks/compliance-criteria/#essentials) +- 🚀 [Essentials](https://docs.eventsourcingdb.io/client-sdks/compliance-criteria/#essentials) - ❌ [Writing Events](https://docs.eventsourcingdb.io/client-sdks/compliance-criteria/#writing-events) - ❌ [Reading Events](https://docs.eventsourcingdb.io/client-sdks/compliance-criteria/#reading-events) - ❌ [Using EventQL](https://docs.eventsourcingdb.io/client-sdks/compliance-criteria/#using-eventql) diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..3d9211f --- /dev/null +++ b/src/client.rs @@ -0,0 +1,152 @@ +//! Client for the [EventsourcingDB](https://www.eventsourcingdb.io/) API. +//! +//! To use the client, create it with the base URL and API token of your [EventsourcingDB](https://www.eventsourcingdb.io/) instance. +//! ``` +//! # tokio_test::block_on(async { +//! # let container = eventsourcingdb_client_rust::container::Container::start_default().await.unwrap(); +//! let db_url = "http://localhost:3000/"; +//! let api_token = "secrettoken"; +//! # let db_url = container.get_base_url().await.unwrap(); +//! # let api_token = container.get_api_token(); +//! let client = eventsourcingdb_client_rust::client::Client::new(db_url, api_token); +//! client.ping().await.expect("Failed to ping"); +//! client.verify_api_token().await.expect("Failed to verify API token"); +//! # }) +//! ``` +//! +//! With the code above you can verify that the DB is reachable and that the API token is valid. +//! If this works, it means that the client is correctly configured and you can use it to make requests to the DB. + +mod client_request; + +use client_request::{ClientRequest, PingRequest, VerifyApiTokenRequest}; + +use reqwest; +use url::Url; + +use crate::error::ClientError; + +/// Client for an [EventsourcingDB](https://www.eventsourcingdb.io/) instance. +#[derive(Debug)] +pub struct Client { + base_url: Url, + api_token: String, + client: reqwest::Client, +} + +impl Client { + /// Creates a new client instance based on the base URL and API token + pub fn new(base_url: Url, api_token: impl Into) -> Self { + Client { + base_url, + api_token: api_token.into(), + client: reqwest::Client::new(), + } + } + + /// Get the base URL of the client to use for API calls + /// ``` + /// # use url::Url; + /// # use eventsourcingdb_client_rust::client::Client; + /// # let client = Client::new("http://localhost:8080/".parse().unwrap(), "token"); + /// let base_url = client.get_base_url(); + /// # assert_eq!(base_url.as_str(), "http://localhost:8080/"); + /// ``` + #[must_use] + pub fn get_base_url(&self) -> &Url { + &self.base_url + } + + /// Get the API token of the client to use for API calls + /// ``` + /// # use eventsourcingdb_client_rust::client::Client; + /// # use url::Url; + /// # let client = Client::new("http://localhost:8080/".parse().unwrap(), "secrettoken"); + /// let api_token = client.get_api_token(); + /// # assert_eq!(api_token, "secrettoken"); + /// ``` + #[must_use] + pub fn get_api_token(&self) -> &str { + &self.api_token + } + + /// Utility function to request an endpoint of the API. + /// + /// # Errors + /// This function will return an error if the request fails or if the URL is invalid. + async fn request(&self, endpoint: R) -> Result { + let url = self + .base_url + .join(endpoint.url_path()) + .map_err(ClientError::URLParseError)?; + + let request = match endpoint.method() { + reqwest::Method::GET => self.client.get(url), + reqwest::Method::POST => self.client.post(url), + _ => return Err(ClientError::InvalidRequestMethod), + } + .header("Authorization", format!("Bearer {}", self.api_token)); + let request = if let Some(body) = endpoint.body() { + request + .header("Content-Type", "application/json") + .json(&body?) + } else { + request + }; + + let response = request.send().await?; + + if response.status().is_success() { + let result = response.json().await?; + endpoint.validate_response(&result)?; + Ok(result) + } else { + Err(ClientError::DBError( + response.status(), + response.text().await.unwrap_or_default(), + )) + } + } + + /// Pings the DB instance to check if it is reachable. + /// + /// ``` + /// # tokio_test::block_on(async { + /// # let container = eventsourcingdb_client_rust::container::Container::start_default().await.unwrap(); + /// let db_url = "http://localhost:3000/"; + /// let api_token = "secrettoken"; + /// # let db_url = container.get_base_url().await.unwrap(); + /// # let api_token = container.get_api_token(); + /// let client = eventsourcingdb_client_rust::client::Client::new(db_url, api_token); + /// client.ping().await.expect("Failed to ping"); + /// # }) + /// ``` + /// + /// # Errors + /// This function will return an error if the request fails or if the URL is invalid. + pub async fn ping(&self) -> Result<(), ClientError> { + let _ = self.request(PingRequest).await?; + Ok(()) + } + + /// Verifies the API token by sending a request to the DB instance. + /// + /// ``` + /// # tokio_test::block_on(async { + /// # let container = eventsourcingdb_client_rust::container::Container::start_default().await.unwrap(); + /// let db_url = "http://localhost:3000/"; + /// let api_token = "secrettoken"; + /// # let db_url = container.get_base_url().await.unwrap(); + /// # let api_token = container.get_api_token(); + /// let client = eventsourcingdb_client_rust::client::Client::new(db_url, api_token); + /// client.verify_api_token().await.expect("Failed to ping"); + /// # }) + /// ``` + /// + /// # Errors + /// This function will return an error if the request fails or if the URL is invalid. + pub async fn verify_api_token(&self) -> Result<(), ClientError> { + let _ = self.request(VerifyApiTokenRequest).await?; + Ok(()) + } +} diff --git a/src/client/client_request.rs b/src/client/client_request.rs new file mode 100644 index 0000000..cc389a9 --- /dev/null +++ b/src/client/client_request.rs @@ -0,0 +1,65 @@ +//! This is a purely internal module to represent client requests to the database. + +use reqwest::Method; +use serde_json::Value; + +use crate::{error::ClientError, event::ManagementEvent}; + +/// Represents a request to the database client +pub trait ClientRequest { + const URL_PATH: &'static str; + const METHOD: Method; + type Response: serde::de::DeserializeOwned; + + /// Returns the URL path for the request + fn url_path(&self) -> &'static str { + Self::URL_PATH + } + + /// Returns the http method type for the request + fn method(&self) -> Method { + Self::METHOD + } + + /// Returns the body for the request + fn body(&self) -> Option> { + None + } + + /// Validate the response from the database + fn validate_response(&self, _response: &Self::Response) -> Result<(), ClientError> { + Ok(()) + } +} + +/// Ping the Database instance +#[derive(Debug, Clone, Copy)] +pub struct PingRequest; + +impl ClientRequest for PingRequest { + const URL_PATH: &'static str = "/api/v1/ping"; + const METHOD: Method = Method::GET; + type Response = ManagementEvent; + + fn validate_response(&self, response: &Self::Response) -> Result<(), ClientError> { + (response.ty() == "io.eventsourcingdb.api.ping-received") + .then_some(()) + .ok_or(ClientError::PingFailed) + } +} + +/// Verify the API token +#[derive(Debug, Clone, Copy)] +pub struct VerifyApiTokenRequest; + +impl ClientRequest for VerifyApiTokenRequest { + const URL_PATH: &'static str = "/api/v1/verify-api-token"; + const METHOD: Method = Method::POST; + type Response = ManagementEvent; + + fn validate_response(&self, response: &Self::Response) -> Result<(), ClientError> { + (response.ty() == "io.eventsourcingdb.api.api-token-verified") + .then_some(()) + .ok_or(ClientError::APITokenInvalid) + } +} diff --git a/src/container.rs b/src/container.rs index 3dae1cb..b5833ea 100644 --- a/src/container.rs +++ b/src/container.rs @@ -32,7 +32,7 @@ //! //! ## Stopping the container //! The container will be stopped automatically when it is dropped. -//! You can also stop it manually by calling the [Container::stop] method. +//! You can also stop it manually by calling the [`Container::stop`] method. use testcontainers::{ ContainerAsync, GenericImage, core::{ContainerPort, ImageExt, WaitFor, wait::HttpWaitStrategy}, @@ -40,11 +40,11 @@ use testcontainers::{ }; use url::{Host, Url}; -use crate::error::ContainerError; +use crate::{client::Client, error::ContainerError}; /// Builder for the [Container]. /// -/// **You should not use this directly**, but use the [Container::builder] method instead. +/// **You should not use this directly**, but use the [`Container::builder`] method instead. /// /// By default this container is the same as running this: /// ``` @@ -94,7 +94,7 @@ impl ContainerBuilder { /// /// This is the port that will be exposed from the container to the host. /// It will be mapped to a random port on the host that you can connect to. - /// To find that port, use the [Container::get_mapped_port] method. + /// To find that port, use the [`Container::get_mapped_port`] method. #[must_use] pub fn with_port(mut self, port: impl Into) -> Self { self.internal_port = port.into(); @@ -141,8 +141,8 @@ impl ContainerBuilder { /// Aside from managing the container, this struct also provides methods to get the data needed to connect to /// the database or even a fully configured client. /// -/// You'll most likely want to use the [Container::start_default] method to create a new container instance for your tests. -/// For more details, see the [crate::container] module documentation. +/// You'll most likely want to use the [`Container::start_default`] method to create a new container instance for your tests. +/// For more details, see the [`crate::container`] module documentation. /// ``` /// # use eventsourcingdb_client_rust::container::Container; /// # tokio_test::block_on(async { @@ -159,8 +159,8 @@ pub struct Container { impl Container { /// Create a new container builder instance to configure the container. - /// The returned builder starts with the default settings and is the same as calling [ContainerBuilder::default]. - /// This is the recommended way to create a new [ContainerBuilder] instance. + /// The returned builder starts with the default settings and is the same as calling [`ContainerBuilder::default`]. + /// This is the recommended way to create a new [`ContainerBuilder`] instance. #[must_use] pub fn builder() -> ContainerBuilder { ContainerBuilder::default() @@ -168,11 +168,11 @@ impl Container { /// Shortcut method to start the container with default settings. /// - /// This is the same as calling [Container::builder] and then [ContainerBuilder::start]. + /// This is the same as calling [`Container::builder`] and then [`ContainerBuilder::start`]. /// In most cases this will create a contaienr with the latest image tag and a working configuration. /// /// # Errors - /// This functions returns the errors of `ContainerBuilder::start()` + /// This functions returns the errors of [`ContainerBuilder::start()`] pub async fn start_default() -> Result { Self::builder().start().await } @@ -189,7 +189,7 @@ impl Container { /// Get the mapped port for the database. /// - /// This is the port that you can use to connect to the database. This will be a random port that is mapped to the internal port configured via [ContainerBuilder::with_port]. + /// This is the port that you can use to connect to the database. This will be a random port that is mapped to the internal port configured via [`ContainerBuilder::with_port`]. /// /// # Errors /// This function will return an error if the container is not running (e.g. because it crashed) or if the host could not be retrieved @@ -231,13 +231,12 @@ impl Container { Ok(()) } - // TODO!: Uncomment this when the client is available - // /// Get a new client instance for the database container - // /// - // /// # Errors - // /// This function will return an error if the container is not running (e.g. because it crashed) or if the host could not be retrieved - // pub async fn get_client(&self) -> Result { - // let base_url = self.get_base_url().await?; - // Ok(Client::new(base_url, self.api_token.clone())) - // } + /// Get a new client instance for the database container + /// + /// # Errors + /// This function will return an error if the container is not running (e.g. because it crashed) or if the host could not be retrieved + pub async fn get_client(&self) -> Result { + let base_url = self.get_base_url().await?; + Ok(Client::new(base_url, self.api_token.clone())) + } } diff --git a/src/error.rs b/src/error.rs index 4323c12..89b61b9 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,8 +1,41 @@ //! This module contains all error types of the SDK. -/// Error type for the [crate::container] feature. +use reqwest::{self, StatusCode}; +use thiserror::Error; + +/// Error type for the client +#[derive(Debug, Error)] +pub enum ClientError { + /// The provided request method is invalid + #[error("The provided request method is invalid")] + InvalidRequestMethod, + /// The provided API token is invalid + #[error("The provided API token is invalid")] + APITokenInvalid, + /// There was a generic problem with pinging the DB + #[error("Pinging the DB failed")] + PingFailed, + /// There was a problem making a request to the DB + #[error("The request failed with error: {0}")] + ReqwestError(#[from] reqwest::Error), + /// There was a problem parsing the URL + #[error("The URL is invalid: {0}")] + URLParseError(#[from] url::ParseError), + /// There was a problem with the JSON serialization + #[error("The JSON serialization failed: {0}")] + SerdeJsonError(#[from] serde_json::Error), + /// The DB returned an error+ + #[error("The DB returned an error: {0}")] + DBError(StatusCode, String), + /// There was a problem with the `cloudevents` message + #[cfg(feature = "cloudevents")] + #[error("The CloudEvents message is invalid: {0}")] + CloudeventsMessageError(#[from] cloudevents::message::Error), +} + +/// Error type for the [`crate::container`] feature. #[cfg(feature = "testcontainer")] -#[derive(Debug, thiserror::Error)] +#[derive(Debug, Error)] pub enum ContainerError { /// This error is returned when anything goes wrong with the testcontainers crate. /// If you experience this error, a likely cause is that your docker daemon is not running. diff --git a/src/event.rs b/src/event.rs new file mode 100644 index 0000000..09f88e3 --- /dev/null +++ b/src/event.rs @@ -0,0 +1,5 @@ +//! This module holds all event types that are send between the client and the database. + +mod management_event; + +pub use management_event::ManagementEvent; diff --git a/src/event/management_event.rs b/src/event/management_event.rs new file mode 100644 index 0000000..f47893c --- /dev/null +++ b/src/event/management_event.rs @@ -0,0 +1,86 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use chrono::{DateTime, Utc}; + +/// Represents a management event that has been received from the DB. +/// +/// For management requests like [`crate::client::Client::ping`] and [`crate::client::Client::verify_api_token`] the DB will send a management event. +/// +/// Compared to a normal Event, this does not contain the following fields: +/// - hash +/// - predecessorhash +/// - traceinfo +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ManagementEvent { + data: Value, + datacontenttype: String, + id: String, + source: String, + specversion: String, + subject: String, + time: DateTime, + r#type: String, +} + +impl ManagementEvent { + /// Get the data of an event. + #[must_use] + pub fn data(&self) -> &Value { + &self.data + } + /// Get the data content type of an event. + #[must_use] + pub fn datacontenttype(&self) -> &str { + &self.datacontenttype + } + /// Get the ID of an event. + /// In eventsourcingdb, this is the sequence number of the event. + #[must_use] + pub fn id(&self) -> &str { + &self.id + } + /// Get the source of an event. + #[must_use] + pub fn source(&self) -> &str { + &self.source + } + /// Get the spec version of an event. + /// This is always `1.0`. + #[must_use] + pub fn specversion(&self) -> &str { + &self.specversion + } + /// Get the subject of an event. + #[must_use] + pub fn subject(&self) -> &str { + &self.subject + } + /// Get the time of an event. + #[must_use] + pub fn time(&self) -> &DateTime { + &self.time + } + /// Get the type of an event. + /// + /// This method is called `ty` to avoid conflicts with the `type` keyword in Rust. + #[must_use] + pub fn ty(&self) -> &str { + &self.r#type + } +} + +/// Optionally implement compatibility with the [cloudevents] crate. +#[cfg(feature = "cloudevents")] +impl From for cloudevents::Event { + fn from(event: ManagementEvent) -> Self { + cloudevents::EventBuilderV10::new() + .source(event.source) + .subject(event.subject) + .ty(event.r#type) + .id(event.id) + .time(event.time.to_string()) + .data(event.datacontenttype, event.data) + .build() + .expect("Failed to build cloudevent") + } +} diff --git a/src/lib.rs b/src/lib.rs index 4d57075..344f1be 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,4 @@ #![doc = include_str!("../README.md")] - #![deny( ambiguous_negative_literals, clippy::pedantic, @@ -15,6 +14,8 @@ warnings )] -pub mod error; +pub mod client; #[cfg(feature = "testcontainer")] pub mod container; +pub mod error; +pub mod event; diff --git a/tests/essentials.rs b/tests/essentials.rs new file mode 100644 index 0000000..49b2615 --- /dev/null +++ b/tests/essentials.rs @@ -0,0 +1,38 @@ +use eventsourcingdb_client_rust::{client::Client, container::Container}; + +#[tokio::test] +async fn ping() { + let container = Container::start_default().await.unwrap(); + let client = container.get_client().await.unwrap(); + client.ping().await.expect("Failed to ping"); +} + +#[tokio::test] +async fn ping_unavailable_server_errors() { + let client = Client::new("http://localhost:12345".parse().unwrap(), "secrettoken"); + let result = client.ping().await; + assert!(result.is_err(), "Expected an error, but got: {:?}", result); +} + +#[tokio::test] +async fn verify_api_token() { + let container = Container::start_default().await.unwrap(); + let client = container.get_client().await.unwrap(); + client.verify_api_token().await.expect("Failed to verify API token"); +} + +#[tokio::test] +async fn verify_api_token_unavailable_server_errors() { + let client = Client::new("http://localhost:12345".parse().unwrap(), "secrettoken"); + let result = client.verify_api_token().await; + assert!(result.is_err(), "Expected an error, but got: {:?}", result); +} + +#[tokio::test] +async fn verify_api_token_invalid_token_errors() { + let container = Container::start_default().await.unwrap(); + let client = container.get_client().await.unwrap(); + let invalid_client = Client::new(client.get_base_url().clone(), "invalid_token"); + let result = invalid_client.verify_api_token().await; + assert!(result.is_err(), "Expected an error, but got: {:?}", result); +} diff --git a/tests/testcontainer.rs b/tests/testcontainer.rs index b6f62bf..9ef8208 100644 --- a/tests/testcontainer.rs +++ b/tests/testcontainer.rs @@ -25,14 +25,13 @@ async fn db_is_reachable() { reqwest::Client::new().get(ping_url).send().await.unwrap(); } -// TODO!: Uncomment this test when the client is available -// #[tokio::test] -// async fn generate_client() { -// let c = Container::start_default().await.unwrap(); -// let generated_client = c.get_client().await.unwrap(); -// let base_url = c.get_base_url().await.unwrap(); -// let api_token = c.get_api_token(); -// let client = eventsourcingdb_client_rust::client::Client::new(base_url, api_token); -// assert_eq!(client.get_base_url(), generated_client.get_base_url()); -// assert_eq!(client.get_api_token(), generated_client.get_api_token()); -// } +#[tokio::test] +async fn generate_client() { + let c = Container::start_default().await.unwrap(); + let generated_client = c.get_client().await.unwrap(); + let base_url = c.get_base_url().await.unwrap(); + let api_token = c.get_api_token(); + let client = eventsourcingdb_client_rust::client::Client::new(base_url, api_token); + assert_eq!(client.get_base_url(), generated_client.get_base_url()); + assert_eq!(client.get_api_token(), generated_client.get_api_token()); +}