Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: restructure lib #18

Merged
merged 3 commits into from
Feb 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
reqwest = { version = "0.11.3", default-features = false, features = ["blocking", "rustls-tls"] }
reqwest = { version = "0.11.3", default-features = false, features = [
"blocking",
"rustls-tls",
] }
serde = { version = "1.0.125", features = ["derive"] }
chrono = {version = "0.4.19", features = ["serde"] }
chrono = { version = "0.4.19", features = ["serde"] }
serde_json = "1.0.64"
semver = "1.0.24"

Expand Down
42 changes: 42 additions & 0 deletions src/client/blocking.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use reqwest::{blocking::Client as HttpClient, header::CONTENT_TYPE};

use crate::{event::InnerEvent, Error, Event, TIMEOUT};

use super::ClientOptions;

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<Event>) -> Result<(), Error> {
for event in events {
self.capture(event)?;
}
Ok(())
}
}

pub fn client<C: Into<ClientOptions>>(options: C) -> Client {
let client = HttpClient::builder()
.timeout(Some(*TIMEOUT))
.build()
.unwrap(); // Unwrap here is as safe as `HttpClient::new`
Client {
options: options.into(),
client,
}
}
20 changes: 20 additions & 0 deletions src/client/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use crate::API_ENDPOINT;

mod blocking;

pub use blocking::client;
pub use blocking::Client;

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(),
}
}
}
18 changes: 18 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use std::fmt::{Display, Formatter};

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),
}
123 changes: 123 additions & 0 deletions src/event.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
use std::collections::HashMap;

use chrono::NaiveDateTime;
use semver::Version;
use serde::Serialize;

use crate::Error;

#[derive(Serialize, Debug, PartialEq, Eq)]
pub struct Event {
event: String,
properties: Properties,
timestamp: Option<NaiveDateTime>,
}

#[derive(Serialize, Debug, PartialEq, Eq)]
pub struct Properties {
distinct_id: String,
props: HashMap<String, serde_json::Value>,
}

impl Properties {
fn new<S: Into<String>>(distinct_id: S) -> Self {
Self {
distinct_id: distinct_id.into(),
props: Default::default(),
}
}
}

impl Event {
pub fn new<S: Into<String>>(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<K: Into<String>, 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(())
}
}

// This exists so that the client doesn't have to specify the API key over and over
#[derive(Serialize)]
pub struct InnerEvent {
api_key: String,
event: String,
properties: Properties,
timestamp: Option<NaiveDateTime>,
}

impl InnerEvent {
pub fn new(event: Event, api_key: String) -> Self {
let mut properties = event.properties;

// Add $lib_name and $lib_version to the properties
properties.props.insert(
"$lib_name".into(),
serde_json::Value::String("posthog-rs".into()),
);

let version_str = env!("CARGO_PKG_VERSION");
properties.props.insert(
"$lib_version".into(),
serde_json::Value::String(version_str.into()),
);

if let Ok(version) = version_str.parse::<Version>() {
properties.props.insert(
"$lib_version__major".into(),
serde_json::Value::Number(version.major.into()),
);
properties.props.insert(
"$lib_version__minor".into(),
serde_json::Value::Number(version.minor.into()),
);
properties.props.insert(
"$lib_version__patch".into(),
serde_json::Value::Number(version.patch.into()),
);
}

Self {
api_key,
event: event.event,
properties,
timestamp: event.timestamp,
}
}
}

#[cfg(test)]
pub mod tests {
use crate::{event::InnerEvent, Event};

#[test]
fn inner_event_adds_lib_properties_correctly() {
// Arrange
let mut event = Event::new("unit test event", "1234");
event.insert_prop("key1", "value1").unwrap();
let api_key = "test_api_key".to_string();

// Act
let inner_event = InnerEvent::new(event, api_key);

// Assert
let props = &inner_event.properties.props;
assert_eq!(
props.get("$lib_name"),
Some(&serde_json::Value::String("posthog-rs".to_string()))
);
}
}
Loading