Skip to content

Commit 84cf990

Browse files
authored
Merge pull request #18 from PostHog/olly_reorg
chore: restructure lib
2 parents 962d1f1 + a43e428 commit 84cf990

File tree

7 files changed

+245
-225
lines changed

7 files changed

+245
-225
lines changed

Cargo.toml

+5-2
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@ edition = "2018"
1010
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1111

1212
[dependencies]
13-
reqwest = { version = "0.11.3", default-features = false, features = ["blocking", "rustls-tls"] }
13+
reqwest = { version = "0.11.3", default-features = false, features = [
14+
"blocking",
15+
"rustls-tls",
16+
] }
1417
serde = { version = "1.0.125", features = ["derive"] }
15-
chrono = {version = "0.4.19", features = ["serde"] }
18+
chrono = { version = "0.4.19", features = ["serde"] }
1619
serde_json = "1.0.64"
1720
semver = "1.0.24"
1821

src/client/blocking.rs

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
use reqwest::{blocking::Client as HttpClient, header::CONTENT_TYPE};
2+
3+
use crate::{event::InnerEvent, Error, Event, TIMEOUT};
4+
5+
use super::ClientOptions;
6+
7+
pub struct Client {
8+
options: ClientOptions,
9+
client: HttpClient,
10+
}
11+
12+
impl Client {
13+
pub fn capture(&self, event: Event) -> Result<(), Error> {
14+
let inner_event = InnerEvent::new(event, self.options.api_key.clone());
15+
let _res = self
16+
.client
17+
.post(self.options.api_endpoint.clone())
18+
.header(CONTENT_TYPE, "application/json")
19+
.body(serde_json::to_string(&inner_event).expect("unwrap here is safe"))
20+
.send()
21+
.map_err(|e| Error::Connection(e.to_string()))?;
22+
Ok(())
23+
}
24+
25+
pub fn capture_batch(&self, events: Vec<Event>) -> Result<(), Error> {
26+
for event in events {
27+
self.capture(event)?;
28+
}
29+
Ok(())
30+
}
31+
}
32+
33+
pub fn client<C: Into<ClientOptions>>(options: C) -> Client {
34+
let client = HttpClient::builder()
35+
.timeout(Some(*TIMEOUT))
36+
.build()
37+
.unwrap(); // Unwrap here is as safe as `HttpClient::new`
38+
Client {
39+
options: options.into(),
40+
client,
41+
}
42+
}

src/client/mod.rs

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
use crate::API_ENDPOINT;
2+
3+
mod blocking;
4+
5+
pub use blocking::client;
6+
pub use blocking::Client;
7+
8+
pub struct ClientOptions {
9+
api_endpoint: String,
10+
api_key: String,
11+
}
12+
13+
impl From<&str> for ClientOptions {
14+
fn from(api_key: &str) -> Self {
15+
ClientOptions {
16+
api_endpoint: API_ENDPOINT.to_string(),
17+
api_key: api_key.to_string(),
18+
}
19+
}
20+
}

src/error.rs

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
use std::fmt::{Display, Formatter};
2+
3+
impl Display for Error {
4+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
5+
match self {
6+
Error::Connection(msg) => write!(f, "Connection Error: {}", msg),
7+
Error::Serialization(msg) => write!(f, "Serialization Error: {}", msg),
8+
}
9+
}
10+
}
11+
12+
impl std::error::Error for Error {}
13+
14+
#[derive(Debug)]
15+
pub enum Error {
16+
Connection(String),
17+
Serialization(String),
18+
}

src/event.rs

+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
use std::collections::HashMap;
2+
3+
use chrono::NaiveDateTime;
4+
use semver::Version;
5+
use serde::Serialize;
6+
7+
use crate::Error;
8+
9+
#[derive(Serialize, Debug, PartialEq, Eq)]
10+
pub struct Event {
11+
event: String,
12+
properties: Properties,
13+
timestamp: Option<NaiveDateTime>,
14+
}
15+
16+
#[derive(Serialize, Debug, PartialEq, Eq)]
17+
pub struct Properties {
18+
distinct_id: String,
19+
props: HashMap<String, serde_json::Value>,
20+
}
21+
22+
impl Properties {
23+
fn new<S: Into<String>>(distinct_id: S) -> Self {
24+
Self {
25+
distinct_id: distinct_id.into(),
26+
props: Default::default(),
27+
}
28+
}
29+
}
30+
31+
impl Event {
32+
pub fn new<S: Into<String>>(event: S, distinct_id: S) -> Self {
33+
Self {
34+
event: event.into(),
35+
properties: Properties::new(distinct_id),
36+
timestamp: None,
37+
}
38+
}
39+
40+
/// Errors if `prop` fails to serialize
41+
pub fn insert_prop<K: Into<String>, P: Serialize>(
42+
&mut self,
43+
key: K,
44+
prop: P,
45+
) -> Result<(), Error> {
46+
let as_json =
47+
serde_json::to_value(prop).map_err(|e| Error::Serialization(e.to_string()))?;
48+
let _ = self.properties.props.insert(key.into(), as_json);
49+
Ok(())
50+
}
51+
}
52+
53+
// This exists so that the client doesn't have to specify the API key over and over
54+
#[derive(Serialize)]
55+
pub struct InnerEvent {
56+
api_key: String,
57+
event: String,
58+
properties: Properties,
59+
timestamp: Option<NaiveDateTime>,
60+
}
61+
62+
impl InnerEvent {
63+
pub fn new(event: Event, api_key: String) -> Self {
64+
let mut properties = event.properties;
65+
66+
// Add $lib_name and $lib_version to the properties
67+
properties.props.insert(
68+
"$lib_name".into(),
69+
serde_json::Value::String("posthog-rs".into()),
70+
);
71+
72+
let version_str = env!("CARGO_PKG_VERSION");
73+
properties.props.insert(
74+
"$lib_version".into(),
75+
serde_json::Value::String(version_str.into()),
76+
);
77+
78+
if let Ok(version) = version_str.parse::<Version>() {
79+
properties.props.insert(
80+
"$lib_version__major".into(),
81+
serde_json::Value::Number(version.major.into()),
82+
);
83+
properties.props.insert(
84+
"$lib_version__minor".into(),
85+
serde_json::Value::Number(version.minor.into()),
86+
);
87+
properties.props.insert(
88+
"$lib_version__patch".into(),
89+
serde_json::Value::Number(version.patch.into()),
90+
);
91+
}
92+
93+
Self {
94+
api_key,
95+
event: event.event,
96+
properties,
97+
timestamp: event.timestamp,
98+
}
99+
}
100+
}
101+
102+
#[cfg(test)]
103+
pub mod tests {
104+
use crate::{event::InnerEvent, Event};
105+
106+
#[test]
107+
fn inner_event_adds_lib_properties_correctly() {
108+
// Arrange
109+
let mut event = Event::new("unit test event", "1234");
110+
event.insert_prop("key1", "value1").unwrap();
111+
let api_key = "test_api_key".to_string();
112+
113+
// Act
114+
let inner_event = InnerEvent::new(event, api_key);
115+
116+
// Assert
117+
let props = &inner_event.properties.props;
118+
assert_eq!(
119+
props.get("$lib_name"),
120+
Some(&serde_json::Value::String("posthog-rs".to_string()))
121+
);
122+
}
123+
}

0 commit comments

Comments
 (0)