diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..8191475 --- /dev/null +++ b/src/client.rs @@ -0,0 +1,43 @@ +use reqwest::blocking::Client as HttpClient; +use reqwest::header::CONTENT_TYPE; + +use crate::client_options::ClientOptions; +use crate::error::Error; +use crate::event::{Event, InnerEvent}; + +pub struct Client { + options: ClientOptions, + http_client: HttpClient, +} + +impl Client { + pub(crate) fn new(options: ClientOptions) -> Self { + let http_client = HttpClient::builder() + .timeout(Some(options.timeout)) + .build() + .unwrap(); // Unwrap here is as safe as `HttpClient::new` + Client { + options, + http_client, + } + } + + pub fn capture(&self, event: Event) -> Result<(), Error> { + let inner_event = InnerEvent::new(event, self.options.api_key.clone()); + let _res = self + .http_client + .post(self.options.api_endpoint.clone()) + .header(CONTENT_TYPE, "application/json") + .body(serde_json::to_string(&inner_event).expect("unwrap here is safe")) + .send() + .map_err(|e| Error::Connection(e.to_string()))?; + Ok(()) + } + + pub fn capture_batch(&self, events: Vec) -> Result<(), Error> { + for event in events { + self.capture(event)?; + } + Ok(()) + } +} diff --git a/src/client_options.rs b/src/client_options.rs new file mode 100644 index 0000000..f9dee95 --- /dev/null +++ b/src/client_options.rs @@ -0,0 +1,42 @@ +use std::time::Duration; + +use crate::client::Client; + +const API_ENDPOINT: &str = "https://app.posthog.com/capture/"; +const TIMEOUT: Duration = Duration::from_millis(800); // This should be specified by the user + +pub struct ClientOptions { + pub(crate) api_endpoint: String, + pub(crate) api_key: String, + pub(crate) timeout: Duration, +} + +impl ClientOptions { + pub fn new(api_key: impl ToString) -> ClientOptions { + ClientOptions { + api_endpoint: API_ENDPOINT.to_string(), + api_key: api_key.to_string(), + timeout: TIMEOUT, + } + } + + pub fn api_endpoint(&mut self, api_endpoint: impl ToString) -> &mut Self { + self.api_endpoint = api_endpoint.to_string(); + self + } + + pub fn timeout(&mut self, timeout: Duration) -> &mut Self { + self.timeout = timeout; + self + } + + pub fn build(self) -> Client { + Client::new(self) + } +} + +impl From<&str> for ClientOptions { + fn from(api_key: &str) -> Self { + ClientOptions::new(api_key) + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..6474bd4 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,18 @@ +use std::fmt::{Display, Formatter}; + +#[derive(Debug)] +pub enum Error { + Connection(String), + Serialization(String), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Error::Connection(msg) => write!(f, "Connection Error: {}", msg), + Error::Serialization(msg) => write!(f, "Serialization Error: {}", msg), + } + } +} + +impl std::error::Error for Error {} diff --git a/src/event.rs b/src/event.rs new file mode 100644 index 0000000..42efb96 --- /dev/null +++ b/src/event.rs @@ -0,0 +1,69 @@ +use chrono::NaiveDateTime; +use serde::Serialize; +use std::collections::HashMap; + +use crate::error::Error; + +// This exists so that the client doesn't have to specify the API key over and over +#[derive(Serialize)] +pub(crate) struct InnerEvent { + api_key: String, + event: String, + properties: Properties, + timestamp: Option, +} + +impl InnerEvent { + pub(crate) fn new(event: Event, api_key: String) -> Self { + Self { + api_key, + event: event.event, + properties: event.properties, + timestamp: event.timestamp, + } + } +} + +#[derive(Serialize, Debug, PartialEq, Eq)] +pub struct Event { + event: String, + properties: Properties, + timestamp: Option, +} + +#[derive(Serialize, Debug, PartialEq, Eq)] +pub struct Properties { + distinct_id: String, + props: HashMap, +} + +impl Properties { + fn new>(distinct_id: S) -> Self { + Self { + distinct_id: distinct_id.into(), + props: Default::default(), + } + } +} + +impl Event { + pub fn new>(event: S, distinct_id: S) -> Self { + Self { + event: event.into(), + properties: Properties::new(distinct_id), + timestamp: None, + } + } + + /// Errors if `prop` fails to serialize + pub fn insert_prop, P: Serialize>( + &mut self, + key: K, + prop: P, + ) -> Result<(), Error> { + let as_json = + serde_json::to_value(prop).map_err(|e| Error::Serialization(e.to_string()))?; + let _ = self.properties.props.insert(key.into(), as_json); + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 98630fa..b527fea 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,160 +1,13 @@ -use std::collections::HashMap; -use std::fmt::{Display, Formatter}; -use chrono::{NaiveDateTime}; -use reqwest::blocking::Client as HttpClient; -use reqwest::header::CONTENT_TYPE; -use serde::{Serialize}; -use std::time::Duration; +mod client; +mod client_options; +mod error; +mod event; -extern crate serde_json; - -const API_ENDPOINT: &str = "https://app.posthog.com/capture/"; -const TIMEOUT: &Duration = &Duration::from_millis(800); // This should be specified by the user +pub use client::Client; +pub use client_options::ClientOptions; +pub use error::Error; +pub use event::{Event, Properties}; pub fn client>(options: C) -> Client { - let client = HttpClient::builder().timeout(Some(TIMEOUT.clone())).build().unwrap(); // Unwrap here is as safe as `HttpClient::new` - Client { - options: options.into(), - client, - } -} - -impl Display for Error { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - Error::Connection(msg) => write!(f, "Connection Error: {}", msg), - Error::Serialization(msg) => write!(f, "Serialization Error: {}", msg) - } - } -} - -impl std::error::Error for Error { - -} - -#[derive(Debug)] -pub enum Error { - Connection(String), - Serialization(String) -} - -pub struct ClientOptions { - api_endpoint: String, - api_key: String, -} - -impl From<&str> for ClientOptions { - fn from(api_key: &str) -> Self { - ClientOptions { - api_endpoint: API_ENDPOINT.to_string(), - api_key: api_key.to_string(), - } - } -} - -pub struct Client { - options: ClientOptions, - client: HttpClient, -} - -impl Client { - pub fn capture(&self, event: Event) -> Result<(), Error> { - let inner_event = InnerEvent::new(event, self.options.api_key.clone()); - let _res = self.client.post(self.options.api_endpoint.clone()) - .header(CONTENT_TYPE, "application/json") - .body(serde_json::to_string(&inner_event).expect("unwrap here is safe")) - .send() - .map_err(|e| Error::Connection(e.to_string()))?; - Ok(()) - } - - pub fn capture_batch(&self, events: Vec) -> Result<(), Error> { - for event in events { - self.capture(event)?; - } - Ok(()) - } -} - -// This exists so that the client doesn't have to specify the API key over and over -#[derive(Serialize)] -struct InnerEvent { - api_key: String, - event: String, - properties: Properties, - timestamp: Option, + options.into().build() } - -impl InnerEvent { - fn new(event: Event, api_key: String) -> Self { - Self { - api_key, - event: event.event, - properties: event.properties, - timestamp: event.timestamp, - } - } -} - - -#[derive(Serialize, Debug, PartialEq, Eq)] -pub struct Event { - event: String, - properties: Properties, - timestamp: Option, -} - -#[derive(Serialize, Debug, PartialEq, Eq)] -pub struct Properties { - distinct_id: String, - props: HashMap, -} - -impl Properties { - fn new>(distinct_id: S) -> Self { - Self { - distinct_id: distinct_id.into(), - props: Default::default() - } - } -} - -impl Event { - pub fn new>(event: S, distinct_id: S) -> Self { - Self { - event: event.into(), - properties: Properties::new(distinct_id), - timestamp: None - } - } - - /// Errors if `prop` fails to serialize - pub fn insert_prop, P: Serialize>(&mut self, key: K, prop: P) -> Result<(), Error> { - let as_json = serde_json::to_value(prop).map_err(|e| Error::Serialization(e.to_string()))?; - let _ = self.properties.props.insert(key.into(), as_json); - Ok(()) - } -} - - -#[cfg(test)] -pub mod tests { - use super::*; - use chrono::{Utc}; - - #[test] - fn get_client() { - let client = crate::client(env!("POSTHOG_API_KEY")); - - let mut child_map = HashMap::new(); - child_map.insert("child_key1", "child_value1"); - - - let mut event = Event::new("test", "1234"); - event.insert_prop("key1", "value1").unwrap(); - event.insert_prop("key2", vec!["a", "b"]).unwrap(); - event.insert_prop("key3", child_map).unwrap(); - - client.capture(event).unwrap(); - } -} \ No newline at end of file diff --git a/tests/basic.rs b/tests/basic.rs new file mode 100644 index 0000000..8d1e418 --- /dev/null +++ b/tests/basic.rs @@ -0,0 +1,17 @@ +use posthog_rs::Event; +use std::collections::HashMap; + +#[test] +fn get_client() { + let client = posthog_rs::client(env!("POSTHOG_API_KEY")); + + let mut child_map = HashMap::new(); + child_map.insert("child_key1", "child_value1"); + + let mut event = Event::new("test", "1234"); + event.insert_prop("key1", "value1").unwrap(); + event.insert_prop("key2", vec!["a", "b"]).unwrap(); + event.insert_prop("key3", child_map).unwrap(); + + client.capture(event).unwrap(); +}