diff --git a/README.md b/README.md index 1cb81a6..05c7207 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,435 @@ # eventsourcingdb -The official Rust client SDK for [EventSourcingDB](https://www.eventsourcingdb.io/). +The official Rust client SDK for [EventSourcingDB](https://www.eventsourcingdb.io) – a purpose-built database for event sourcing. -# Project status +EventSourcingDB enables you to build and operate event-driven applications with native support for writing, reading, and observing events. This client SDK provides convenient access to its capabilities in Rust. -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: +For more information on EventSourcingDB, see its [official documentation](https://docs.eventsourcingdb.io/). -- 🚀 [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) -- 🚀 [Observing Events](https://docs.eventsourcingdb.io/client-sdks/compliance-criteria/#observing-events) -- 🚀 [Metadata and Discovery](https://docs.eventsourcingdb.io/client-sdks/compliance-criteria/#metadata-and-discovery) -- 🚀 [Testcontainers Support](https://docs.eventsourcingdb.io/client-sdks/compliance-criteria/#testcontainers-support) +This client SDK includes support for [Testcontainers](https://testcontainers.com/) to spin up EventSourcingDB instances in integration tests. For details, see [Using Testcontainers](#using-testcontainers). + +## Getting Started + +Install the client SDK: + +```shell +cargo add eventsourcingdb +``` + +Import the package and create an instance by providing the URL of your EventSourcingDB instance and the API token to use: + +```rust +use eventsourcingdb::client::Client; + +// ... + +let base_url: Url = "localhost:3000".parse().unwrap(); +let api_token = "secret"; +let client = Client::new(base_url, api_token); +``` + +Then call the `ping` function to check whether the instance is reachable. If it is not, the function will return an error: + +```rust +let result = client.ping().await; +if let Err(err) = result { + // handle error... +} +``` + +*Note that `ping` does not require authentication, so the call may succeed even if the API token is invalid.* + +If you want to verify the API token, call `verify_api_token`. If the token is invalid, the function will return an error: + +```rust +let result = client.verify_api_token().await; +if let Err(err) = result { + // handle error... +} +``` + +### Writing Events + +Call the `write_events` function and hand over a vector with one or more events. You do not have to provide all event fields – some are automatically added by the server. + +Specify `source`, `subject`, `type` (using `ty`), and `data` according to the [CloudEvents](https://docs.eventsourcingdb.io/fundamentals/cloud-events/) format. + +For `data` provide a JSON object using a `serde_json:Value`. + +The function returns the written events, including the fields added by the server: + +```rust +let event = EventCandidate::builder() + .source("https://library.eventsourcingdb.io".to_string()) + .subject("/books/42".to_string()) + .ty("io.eventsourcingdb.library.book-acquired") + .data(json!({ + "title": "2001 - A Space Odyssey", + "author": "Arthur C. Clarke", + "isbn": "978-0756906788", + })) + .build(); + +let result = client.write_events(vec![event.clone()], vec![]).await; +match result { + Ok(written_events) => // ... + Err(err) => // ... +} +``` + +#### Using the `IsSubjectPristine` precondition + +If you only want to write events in case a subject (such as `/books/42`) does not yet have any events, use the `IsSubjectPristine` precondition to create a precondition and pass it in a vector as the second argument: + +```rust +let result = client.write_events( + vec![event.clone()], + vec![Precondition::IsSubjectPristine { + subject: "/books/42".to_string(), + }], +).await; +match result { + Ok(written_events) => // ... + Err(err) => // ... +} +``` + +#### Using the `IsSubjectOnEventId` precondition + +If you only want to write events in case the last event of a subject (such as `/books/42`) has a specific ID (e.g., `0`), use the `IsSubjectOnEventId` precondition to create a precondition and pass it in a vector as the second argument: + +```rust +let result = client.write_events( + vec![event.clone()], + vec![Precondition::IsSubjectOnEventId { + subject: "/books/42".to_string(), + event_id: "0".to_string(), + }], +).await; +match result { + Ok(written_events) => // ... + Err(err) => // ... +} +``` + +*Note that according to the CloudEvents standard, event IDs must be of type string.* + +### Reading Events + +To read all events of a subject, call the `read_events` function with the subject and an options object. Set the `recursive` option to `false`. This ensures that only events of the given subject are returned, not events of nested subjects. + +The function returns a stream from which you can retrieve one event at a time: + +```rust +let result = client + .read_events("/books/42", Some( + ReadEventsOptions { + recursive: false, + ...Default::default(), + } + )) + .await; + +match result { + Err(err) => // ... + Ok(mut stream) => { + while let Some(event) = stream.next().await { + // ... + } + } +} +``` + +#### Reading From Subjects Recursively + +If you want to read not only all the events of a subject, but also the events of all nested subjects, set the `recursive` option to `true`: + +```rust +let result = client + .read_events("/books/42", Some( + ReadEventsOptions { + recursive: true, + ..Default::default(), + } + )) + .await; +``` + +This also allows you to read *all* events ever written. To do so, provide `/` as the subject and set `recursive` to `true`, since all subjects are nested under the root subject. + +#### Reading in Anti-Chronological Order + +By default, events are read in chronological order. To read in anti-chronological order, provide the `order` option and set it using the `Antichronological` ordering: + +```rust +let result = client + .read_events("/books/42", Some( + ReadEventsOptions { + recursive: false, + order: Some(Ordering::Antichronological) + ..Default::default(), + } + )) + .await; +``` + +*Note that you can also use the `Chronological` ordering to explicitly enforce the default order.* + +#### Specifying Bounds + +Sometimes you do not want to read all events, but only a range of events. For that, you can specify the `lower_bound` and `upper_bound` options – either one of them or even both at the same time. + +Specify the ID and whether to include or exclude it, for both the lower and upper bound: + +```rust +let result = client + .read_events("/books/42", Some( + ReadEventsOptions { + recursive: false, + lower_bound: Some(Bound { + bound_type: BoundType::Inclusive, + id: "100", + }), + upper_bound: Some(Bound { + bound_type: BoundType::Exclusive, + id: "200", + }), + ..Default::default(), + } + )) + .await; +``` + +#### Starting From the Latest Event of a Given Type + +To read starting from the latest event of a given type, provide the `from_latest_event` option and specify the subject, the type, and how to proceed if no such event exists. + +Possible options are `ReadNothing`, which skips reading entirely, or `ReadEverything`, which effectively behaves as if `from_latest_event` was not specified: + +```rust +let result = client + .read_events("/books/42", Some( + ReadEventsOptions { + recursive: false, + from_latest_event: Some( + FromLatestEventOptions { + subject: "/books/42", + ty: "io.eventsourcingdb.library.book-borrowed", + if_event_is_missing: ReadEventMissingStrategy::ReadEverything, + } + ) + ..Default::default(), + } + )) + .await; +``` + +*Note that `from_latest_event` and `lower_bound` can not be provided at the same time.* + +### Running EventQL Queries + +To run an EventQL query, call the `run_eventql_query` function and provide the query as argument. The function returns a stream. + +```rust +let result = client + .run_eventql_query("FROM e IN events PROJECT INTO e") + .await; + +match result { + Err(err) => // ... + Ok(mut stream) => { + while let Some(row) = stream.next().await { + // ... + } + } +} +``` + +*Note that each row returned by the stream is of type `serde_json::Value` and matches the projection specified in your query.* + +### Observing Events + +To observe all events of a subject, call the `observe_events` function with the subject and an options object. Set the `recursive` option to `false`. This ensures that only events of the given subject are returned, not events of nested subjects. + +The function returns a stream from which you can retrieve one event at a time: + +```rust +let result = client + .observe_events("/books/42", Some( + ObserveEventsOptions { + recursive: false, + from_latest_event: None, + lower_bound: None, + } + )) + .await; + +match result { + Err(err) => // ... + Ok(mut stream) => { + while let Some(event) = stream.next().await { + // ... + } + } +} +``` + +#### Observing From Subjects Recursively + +If you want to observe not only all the events of a subject, but also the events of all nested subjects, set the `recursive` option to `true`: + +```rust +let result = client + .observe_events("/books/42", Some( + ObserveEventsOptions { + recursive: true, + ..Default::default(), + } + )) + .await +``` + +This also allows you to observe *all* events ever written. To do so, provide `/` as the subject and set `recursive` to `true`, since all subjects are nested under the root subject. + +#### Specifying Bounds + +Sometimes you do not want to observe all events, but only a range of events. For that, you can specify the `lower_bound` option. + +Specify the ID and whether to include or exclude it: + +```rust +let result = client + .observe_events("/books/42", Some( + ObserveEventsOptions { + recursive: false, + lower_bound: Some(Bound { + bound_type: BoundType::Inclusive, + id: "100", + }), + ..Default::default(), + } + )) + .await +``` + +#### Starting From the Latest Event of a Given Type + +To observe starting from the latest event of a given type, provide the `from_latest_event` option and specify the subject, the type, and how to proceed if no such event exists. + +Possible options are `WaitForEvent`, which waits for an event of the given type to happen, or `ObserveEverything`, which effectively behaves as if `from_latest_event` was not specified: + +```rust +let result = client + .observe_events("/books/42", Some( + ObserveEventsOptions { + recursive: false, + from_latest_event: Some( + ObserveFromLatestEventOptions { + subject: "/books/42", + ty: "io.eventsourcingdb.library.book-borrowed", + if_event_is_missing: ObserveEventMissingStrategy::ObserveEverything, + } + ) + ..Default::default(), + } + )) + .await +``` + +*Note that `from_latest_event` and `lower_bound` can not be provided at the same time.* + +#### Aborting Observing + +The observe will automatically be canceled if the stream is dropped from scope. + +### Registering an Event Schema + +To register an event schema, call the `register_event_schema` function and hand over an event type and the desired schema: + +```rust +client.register_event_schema( + "io.eventsourcingdb.library.book-acquired", + &json!({ + "type": "object", + "properties": { + "title": { "type": "string" }, + "author": { "type": "string" }, + "isbn": { "type": "string" }, + }, + "required": [ + "title", + "author", + "isbn", + ], + "additionalProperties": false, + }).await;, +) +``` + +### Listing Subjects + +To list all subjects, call the `list_subjects` function with `/` as the base subject. The function returns a stream from which you can retrieve one subject at a time: + +```rust +let result = client.list_subjects(Some("/")).await; +match result { + Ok(subjects) => // ... + Err(err) => // ... +} +``` + +If you only want to list subjects within a specific branch, provide the desired base subject instead: + +```rust +let result := client.list_subjects("/books"); +``` + +### Listing Event Types + +To list all event types, call the `list_event_types` function. The function returns a stream from which you can retrieve one event type at a time: + +```rust +let result := client.list_event_types().await; +match result { + Ok(event_types) => // ... + Err(err) => // ... +} +``` +### Using Testcontainers + +Call the `Container::start_default()` function, get a client, and run your test code: + +```rust +let container = Container::start_default().await.unwrap(); +let client = container.get_client().await.unwrap(); +``` + +#### Configuring the Container Instance + +By default, `Container` uses the `latest` tag of the official EventSourcingDB Docker image. To change that use the provided builder and call the `with_image_tag` function. + +```rust +let container = Container::builder() + .with_image_tag("1.0.0") + .build() + .await.unwrap() +``` + +Similarly, you can configure the port to use and the API token. Call the `with_port` or the `with_api_token` function respectively: + +```rust +let container = Container::builder() + .with_port(4000) + .with_api_token("secret") + .build() + .await.unwrap() +``` + +#### Configuring the Client Manually + +In case you need to set up the client yourself, use the following functions to get details on the container: + +- `get_host()` returns the host name +- `get_mapped_port()` returns the port +- `get_base_url()` returns the full URL of the container +- `get_api_token()` returns the API token \ No newline at end of file diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 0000000..6865770 --- /dev/null +++ b/clippy.toml @@ -0,0 +1 @@ +doc-valid-idents = ["EventSourcingDB", "EventQL", "CloudEvents", ".."] diff --git a/examples/event_types.rs b/examples/event_types.rs new file mode 100644 index 0000000..cbd3efc --- /dev/null +++ b/examples/event_types.rs @@ -0,0 +1,21 @@ +use eventsourcingdb::{client::Client, container::Container}; +use futures::StreamExt; + +#[tokio::main] +async fn main() { + let db = Container::start_default().await.unwrap(); + let base_url = db.get_base_url().await.unwrap(); + let api_token = db.get_api_token(); + let client = Client::new(base_url, api_token); + + let result = client.list_event_types().await; + + match result { + Err(err) => panic!("{}", err), + Ok(mut event_types) => { + while let Some(Ok(event_type)) = event_types.next().await { + println!("{:?}", event_type) + } + } + } +} diff --git a/examples/listing_subjects.rs b/examples/listing_subjects.rs new file mode 100644 index 0000000..88ac2c0 --- /dev/null +++ b/examples/listing_subjects.rs @@ -0,0 +1,21 @@ +use eventsourcingdb::{client::Client, container::Container}; +use futures::StreamExt; + +#[tokio::main] +async fn main() { + let db = Container::start_default().await.unwrap(); + let base_url = db.get_base_url().await.unwrap(); + let api_token = db.get_api_token(); + let client = Client::new(base_url, api_token); + + let result = client.list_subjects(Some("/")).await; + + match result { + Err(err) => panic!("{}", err), + Ok(mut subjects) => { + while let Some(Ok(subject)) = subjects.next().await { + println!("{:?}", subject) + } + } + } +} diff --git a/examples/observing_events.rs b/examples/observing_events.rs new file mode 100644 index 0000000..23e143c --- /dev/null +++ b/examples/observing_events.rs @@ -0,0 +1,33 @@ +use eventsourcingdb::{ + client::{Client, request_options::ObserveEventsOptions}, + container::Container, +}; +use futures::StreamExt; + +#[tokio::main] +async fn main() { + let db = Container::start_default().await.unwrap(); + let base_url = db.get_base_url().await.unwrap(); + let api_token = db.get_api_token(); + let client = Client::new(base_url, api_token); + + let result = client + .observe_events( + "/books/42", + Some(ObserveEventsOptions { + recursive: false, + from_latest_event: None, + lower_bound: None, + }), + ) + .await; + + match result { + Err(err) => panic!("{}", err), + Ok(mut stream) => { + while let Some(Ok(event)) = stream.next().await { + println!("{:?}", event) + } + } + } +} diff --git a/examples/ping.rs b/examples/ping.rs new file mode 100644 index 0000000..abc4f6f --- /dev/null +++ b/examples/ping.rs @@ -0,0 +1,15 @@ +use eventsourcingdb::{client::Client, container::Container}; + +#[tokio::main] +async fn main() { + let db = Container::start_default().await.unwrap(); + let base_url = db.get_base_url().await.unwrap(); + let api_token = db.get_api_token(); + let client = Client::new(base_url, api_token); + + let result = client.ping().await; + if let Err(err) = result { + // handle error + panic!("{}", err) + } +} diff --git a/examples/reading_events.rs b/examples/reading_events.rs new file mode 100644 index 0000000..b77bdf6 --- /dev/null +++ b/examples/reading_events.rs @@ -0,0 +1,35 @@ +use eventsourcingdb::{ + client::{Client, request_options::ReadEventsOptions}, + container::Container, +}; +use futures::StreamExt; + +#[tokio::main] +async fn main() { + let db = Container::start_default().await.unwrap(); + let base_url = db.get_base_url().await.unwrap(); + let api_token = db.get_api_token(); + let client = Client::new(base_url, api_token); + + let result = client + .read_events( + "/books/42", + Some(ReadEventsOptions { + recursive: false, + from_latest_event: None, + order: None, + lower_bound: None, + upper_bound: None, + }), + ) + .await; + + match result { + Err(err) => panic!("{}", err), + Ok(mut stream) => { + while let Some(Ok(event)) = stream.next().await { + println!("{:?}", event) + } + } + } +} diff --git a/examples/registering_event_schema.rs b/examples/registering_event_schema.rs new file mode 100644 index 0000000..67a7c10 --- /dev/null +++ b/examples/registering_event_schema.rs @@ -0,0 +1,34 @@ +use eventsourcingdb::{client::Client, container::Container}; +use serde_json::json; + +#[tokio::main] +async fn main() { + let db = Container::start_default().await.unwrap(); + let base_url = db.get_base_url().await.unwrap(); + let api_token = db.get_api_token(); + let client = Client::new(base_url, api_token); + + let result = client + .register_event_schema( + "io.eventsourcingdb.library.book-acquired", + &json!({ + "type": "object", + "properties": { + "title": { "type": "string" }, + "author": { "type": "string" }, + "isbn": { "type": "string" }, + }, + "required": [ + "title", + "author", + "isbn", + ], + "additionalProperties": false, + }), + ) + .await; + + if let Err(err) = result { + panic!("{}", err) + } +} diff --git a/examples/running_eventql.rs b/examples/running_eventql.rs new file mode 100644 index 0000000..e4237e3 --- /dev/null +++ b/examples/running_eventql.rs @@ -0,0 +1,23 @@ +use eventsourcingdb::{client::Client, container::Container}; +use futures::StreamExt; + +#[tokio::main] +async fn main() { + let db = Container::start_default().await.unwrap(); + let base_url = db.get_base_url().await.unwrap(); + let api_token = db.get_api_token(); + let client = Client::new(base_url, api_token); + + let result = client + .run_eventql_query("FROM e IN events PROJECT INTO e") + .await; + + match result { + Err(err) => panic!("{}", err), + Ok(mut stream) => { + while let Some(Ok(row)) = stream.next().await { + println!("{:?}", row) + } + } + } +} diff --git a/examples/verify_api_token.rs b/examples/verify_api_token.rs new file mode 100644 index 0000000..46e8654 --- /dev/null +++ b/examples/verify_api_token.rs @@ -0,0 +1,15 @@ +use eventsourcingdb::{client::Client, container::Container}; + +#[tokio::main] +async fn main() { + let db = Container::start_default().await.unwrap(); + let base_url = db.get_base_url().await.unwrap(); + let api_token = db.get_api_token(); + let client = Client::new(base_url, api_token); + + let result = client.verify_api_token().await; + if let Err(err) = result { + // handle error + panic!("{}", err) + } +} diff --git a/examples/write_events.rs b/examples/write_events.rs new file mode 100644 index 0000000..48ef6d6 --- /dev/null +++ b/examples/write_events.rs @@ -0,0 +1,27 @@ +use eventsourcingdb::{client::Client, container::Container, event::EventCandidate}; +use serde_json::json; + +#[tokio::main] +async fn main() { + let db = Container::start_default().await.unwrap(); + let base_url = db.get_base_url().await.unwrap(); + let api_token = db.get_api_token(); + let client = Client::new(base_url, api_token); + + let event = EventCandidate::builder() + .source("https://library.eventsourcingdb.io".to_string()) + .subject("/books/42".to_string()) + .ty("io.eventsourcingdb.library.book-acquired") + .data(json!({ + "title": "2001 - A Space Odyssey", + "author": "Arthur C. Clarke", + "isbn": "978-0756906788", + })) + .build(); + + let result = client.write_events(vec![event.clone()], vec![]).await; + match result { + Ok(written_events) => println!("{:?}", written_events), + Err(err) => panic!("{}", err), + } +} diff --git a/src/client.rs b/src/client.rs index b739487..1871c7d 100644 --- a/src/client.rs +++ b/src/client.rs @@ -203,7 +203,7 @@ impl Client { pub async fn read_events<'a>( &self, subject: &'a str, - options: Option>, + options: Option>, ) -> Result>, ClientError> { let response = self .request_streaming(ReadEventsRequest { subject, options }) @@ -249,7 +249,7 @@ impl Client { pub async fn observe_events<'a>( &self, subject: &'a str, - options: Option>, + options: Option>, ) -> Result>, ClientError> { let response = self .request_streaming(ObserveEventsRequest { subject, options }) diff --git a/src/client/client_request/observe_events.rs b/src/client/client_request/observe_events.rs index 489f1b1..0afac56 100644 --- a/src/client/client_request/observe_events.rs +++ b/src/client/client_request/observe_events.rs @@ -1,9 +1,7 @@ use reqwest::Method; use serde::Serialize; -use crate::{ - client::request_options::ObserveEventsRequestOptions, error::ClientError, event::Event, -}; +use crate::{client::request_options::ObserveEventsOptions, error::ClientError, event::Event}; use super::{ClientRequest, StreamingRequest}; @@ -11,7 +9,7 @@ use super::{ClientRequest, StreamingRequest}; pub struct ObserveEventsRequest<'a> { pub subject: &'a str, #[serde(skip_serializing_if = "Option::is_none")] - pub options: Option>, + pub options: Option>, } impl ClientRequest for ObserveEventsRequest<'_> { diff --git a/src/client/client_request/read_events.rs b/src/client/client_request/read_events.rs index c0cba02..b377dfc 100644 --- a/src/client/client_request/read_events.rs +++ b/src/client/client_request/read_events.rs @@ -1,7 +1,7 @@ use reqwest::Method; use serde::Serialize; -use crate::{client::request_options::ReadEventsRequestOptions, error::ClientError, event::Event}; +use crate::{client::request_options::ReadEventsOptions, error::ClientError, event::Event}; use super::{ClientRequest, StreamingRequest}; @@ -9,7 +9,7 @@ use super::{ClientRequest, StreamingRequest}; pub struct ReadEventsRequest<'a> { pub subject: &'a str, #[serde(skip_serializing_if = "Option::is_none")] - pub options: Option>, + pub options: Option>, } impl ClientRequest for ReadEventsRequest<'_> { diff --git a/src/client/request_options.rs b/src/client/request_options.rs index f5f17af..4a6e453 100644 --- a/src/client/request_options.rs +++ b/src/client/request_options.rs @@ -5,10 +5,10 @@ use serde::Serialize; /// Options for reading events from the database #[derive(Debug, Default, Clone, Serialize)] #[serde(rename_all = "camelCase")] -pub struct ReadEventsRequestOptions<'a> { +pub struct ReadEventsOptions<'a> { /// Start reading events from this start event #[serde(skip_serializing_if = "Option::is_none")] - pub from_latest_event: Option>, + pub from_latest_event: Option>, /// Lower bound of events to read #[serde(skip_serializing_if = "Option::is_none")] pub lower_bound: Option>, @@ -25,10 +25,10 @@ pub struct ReadEventsRequestOptions<'a> { /// Options for observing events from the database #[derive(Debug, Default, Clone, Serialize)] #[serde(rename_all = "camelCase")] -pub struct ObserveEventsRequestOptions<'a> { +pub struct ObserveEventsOptions<'a> { /// Start reading events from this start event #[serde(skip_serializing_if = "Option::is_none")] - pub from_latest_event: Option>, + pub from_latest_event: Option>, /// Lower bound of events to read #[serde(skip_serializing_if = "Option::is_none")] pub lower_bound: Option>, @@ -67,25 +67,48 @@ pub struct Bound<'a> { pub id: &'a str, } -/// The strategy for handling missing events +/// The strategy for handling missing events while reading #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "kebab-case")] -pub enum EventMissingStrategy { +pub enum ReadEventMissingStrategy { /// Read all events if the required one is missing ReadEverything, /// Read no events if the required one is missing ReadNothing, } -/// Options for reading events from the start reading at +/// The strategy for handling missing events while observing +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "kebab-case")] +pub enum ObserveEventMissingStrategy { + /// Observe all events if the required one is missing + ObserveEverything, + /// Wait for the event until observing + WaitForEvent, +} + +/// Options for reading events from the latest event of certain type or subject #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] -pub struct FromLatestEventOptions<'a> { +pub struct ReadFromLatestEventOptions<'a> { /// The strategy for handling missing events - pub if_event_is_missing: EventMissingStrategy, + pub if_event_is_missing: ReadEventMissingStrategy, /// The subject the event should be on pub subject: &'a str, /// The type of the event to read from #[serde(rename = "type")] pub ty: &'a str, } + +/// Options for observe events from the latest event of certain type or subject +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ObserveFromLatestEventOptions<'a> { + /// The strategy for handling missing events + pub if_event_is_missing: ObserveEventMissingStrategy, + /// The subject the event should be on + pub subject: &'a str, + /// The type of the event to observe from + #[serde(rename = "type")] + pub ty: &'a str, +} diff --git a/src/lib.rs b/src/lib.rs index 653157c..340ed66 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,34 @@ -#![doc = include_str!("../README.md")] +//! # eventsourcingdb +//! +//! The official Rust client SDK for [EventSourcingDB](https://www.eventsourcingdb.io) – a purpose-built database for event sourcing. +//! EventSourcingDB enables you to build and operate event-driven applications with native support for writing, reading, and observing events. This client SDK provides convenient access to its capabilities in Rust. +//! For more information on EventSourcingDB, see its [official documentation](https://docs.eventsourcingdb.io/). +//! This client SDK includes support for [Testcontainers](https://testcontainers.com/) to spin up EventSourcingDB instances in integration tests. For details, see [Using Testcontainers](#using-testcontainers). +//! +//! ## Getting Started +//! +//! Install the client SDK: +//! +//! ```shell +//! cargo add eventsourcingdb +//! ``` +//! +//! Import the package and create an instance by providing the URL of your EventSourcingDB instance and the API token to use: +//! +//! ```rust +//! use eventsourcingdb::client::Client; +//! # use url::Url; +//! // ... +//! +//! let base_url: Url = "localhost:3000".parse().unwrap(); +//! let api_token = "secret"; +//! let client = Client::new(base_url, api_token); +//! ``` +//! +//! ## Examples +//! +//! Examples can be found in the [examples](https://github.com/thenativeweb/eventsourcingdb-client-rust/tree/main/examples) directory. +//! // There is a known bug in clippy: // https://github.com/rust-lang/rust-clippy/issues/12908 #![allow(clippy::needless_lifetimes)] diff --git a/tests/read_events.rs b/tests/read_events.rs index fbd7f67..0f773ba 100644 --- a/tests/read_events.rs +++ b/tests/read_events.rs @@ -3,7 +3,7 @@ mod utils; use eventsourcingdb::{ container::Container, request_options::{ - EventMissingStrategy, FromLatestEventOptions, Ordering, ReadEventsRequestOptions, + Ordering, ReadEventMissingStrategy, ReadEventsOptions, ReadFromLatestEventOptions, }, }; use futures::TryStreamExt; @@ -120,7 +120,7 @@ async fn read_recursive() { let events_stream = client .read_events( "/test", - Some(ReadEventsRequestOptions { + Some(ReadEventsOptions { recursive: true, ..Default::default() }), @@ -155,7 +155,7 @@ async fn read_not_recursive() { let events_stream = client .read_events( "/test", - Some(ReadEventsRequestOptions { + Some(ReadEventsOptions { recursive: false, ..Default::default() }), @@ -183,7 +183,7 @@ async fn read_chronological() { let events_stream = client .read_events( "/test", - Some(ReadEventsRequestOptions { + Some(ReadEventsOptions { order: Some(Ordering::Chronological), ..Default::default() }), @@ -211,7 +211,7 @@ async fn read_antichronological() { let events_stream = client .read_events( "/test", - Some(ReadEventsRequestOptions { + Some(ReadEventsOptions { order: Some(Ordering::Antichronological), ..Default::default() }), @@ -241,11 +241,11 @@ async fn read_everything_from_missing_latest_event() { let events_stream = client .read_events( "/test", - Some(ReadEventsRequestOptions { - from_latest_event: Some(FromLatestEventOptions { + Some(ReadEventsOptions { + from_latest_event: Some(ReadFromLatestEventOptions { subject: "/", ty: "io.eventsourcingdb.test.does-not-exist", - if_event_is_missing: EventMissingStrategy::ReadEverything, + if_event_is_missing: ReadEventMissingStrategy::ReadEverything, }), ..Default::default() }), @@ -273,11 +273,11 @@ async fn read_nothing_from_missing_latest_event() { let events_stream = client .read_events( "/test", - Some(ReadEventsRequestOptions { - from_latest_event: Some(FromLatestEventOptions { + Some(ReadEventsOptions { + from_latest_event: Some(ReadFromLatestEventOptions { subject: "/", ty: "io.eventsourcingdb.test.does-not-exist", - if_event_is_missing: EventMissingStrategy::ReadNothing, + if_event_is_missing: ReadEventMissingStrategy::ReadNothing, }), ..Default::default() }), @@ -316,11 +316,11 @@ async fn read_from_latest_event() { let events_stream = client .read_events( "/test", - Some(ReadEventsRequestOptions { - from_latest_event: Some(FromLatestEventOptions { + Some(ReadEventsOptions { + from_latest_event: Some(ReadFromLatestEventOptions { subject: "/marker", ty: "io.eventsourcingdb.test", - if_event_is_missing: EventMissingStrategy::ReadNothing, + if_event_is_missing: ReadEventMissingStrategy::ReadNothing, }), ..Default::default() }),