wip: dev-1126-v2

This commit is contained in:
aecsocket
2026-08-17 07:28:50 +00:00
parent 248c7bb217
commit 3326c6d973
14 changed files with 617 additions and 0 deletions
+82
View File
@@ -0,0 +1,82 @@
#![doc = include_str!("../README.md")]
#![allow(missing_docs, reason = "these are Aditude types")]
#[cfg(feature = "mock")]
pub mod mock;
pub mod v1;
pub mod v2;
use std::borrow::Cow;
use chrono::{DateTime, Duration, Utc};
use secrecy::SecretString;
/// [Aditude](https://www.aditude.com/) client.
#[derive(Debug)]
pub struct Client {
http: reqwest::Client,
pub api_url: Cow<'static, str>,
pub api_key: SecretString,
#[cfg(feature = "mock")]
pub mock: arc_swap::ArcSwapOption<mock::AditudeMock>,
}
impl Client {
/// Creates a new Aditude client with a [`reqwest::Client`].
#[must_use]
pub fn from_client(
http: reqwest::Client,
api_url: impl Into<Cow<'static, str>>,
api_key: impl Into<SecretString>,
) -> Self {
Self {
http,
api_url: api_url.into(),
api_key: api_key.into(),
#[cfg(feature = "mock")]
mock: arc_swap::ArcSwapOption::empty(),
}
}
/// Creates a new Aditude client.
#[must_use]
pub fn new(
api_url: impl Into<Cow<'static, str>>,
api_key: impl Into<SecretString>,
) -> Self {
Self::from_client(reqwest::Client::new(), api_url, api_key)
}
/// Creates an Aditude client which mocks responses.
#[cfg(feature = "mock")]
#[must_use]
pub fn from_mock(mock: mock::AditudeMock) -> Self {
Self {
http: reqwest::Client::new(),
api_url: "".into(),
api_key: SecretString::from(String::new()),
mock: arc_swap::ArcSwapOption::from_pointee(mock),
}
}
/// Sets the mock responses that this client will output.
#[cfg(feature = "mock")]
pub fn set_mock(&self, mock: mock::AditudeMock) {
self.mock.store(Some(std::sync::Arc::new(mock)));
}
}
/// Returns the start and end of what Aditude considers the "Yesterday" time
/// range.
#[must_use]
pub fn yesterday(now: DateTime<Utc>) -> (DateTime<Utc>, DateTime<Utc>) {
let start = DateTime::<Utc>::from_naive_utc_and_offset(
(now - Duration::days(1))
.date_naive()
.and_hms_nano_opt(0, 0, 0, 0)
.unwrap_or_default(),
Utc,
);
let end = start + Duration::days(1);
(start, end)
}
+12
View File
@@ -0,0 +1,12 @@
//! See [`AditudeMock`].
use crate::{v1, v2};
/// Mock data returned by [`crate::Aditude`].
#[derive(Debug, Clone, Default)]
pub struct AditudeMock {
/// Response for [`crate::Aditude::get_metrics_v1`].
pub metrics_v1_response: Option<Vec<v1::MetricsResponse>>,
/// Response for [`crate::Aditude::get_metrics_v2`].
pub metrics_v2_response: Option<v2::Metrics>,
}
+139
View File
@@ -0,0 +1,139 @@
use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use secrecy::ExposeSecret;
use serde::{Deserialize, Serialize};
use crate::Client;
impl Client {
/// Fetches insights metrics from the API.
///
/// # Errors
///
/// Errors if the request could not be completed.
pub async fn get_metrics_v1(
&self,
req: GetMetrics<'_>,
) -> reqwest::Result<Vec<MetricsResponse>> {
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Body<'a> {
pub metrics: &'a [MetricKind],
pub range: &'static str,
pub interval: Interval,
#[serde(with = "chrono::serde::ts_milliseconds_option")]
pub start_time: Option<DateTime<Utc>>,
#[serde(with = "chrono::serde::ts_milliseconds_option")]
pub end_time: Option<DateTime<Utc>>,
}
#[cfg(feature = "mock")]
if let Some(mock) = &*self.mock.load() {
return Ok(mock.get_metrics_v1());
}
let body = Body {
metrics: req.metrics,
range: match req.range {
Range::Yesterday => "Yesterday",
Range::Custom { .. } => "custom",
},
interval: req.interval,
start_time: if let Range::Custom { start, .. } = req.range {
Some(start)
} else {
None
},
end_time: if let Range::Custom { end, .. } = req.range {
Some(end)
} else {
None
},
};
self.http
.post(format!("{}/public/insights/metrics", self.api_url))
.bearer_auth(self.api_key.expose_secret())
.json(&body)
.send()
.await?
.error_for_status()?
.json::<Vec<MetricsResponse>>()
.await
}
}
#[derive(Debug, Clone)]
pub struct GetMetrics<'a> {
pub metrics: &'a [MetricKind],
pub range: Range,
pub interval: Interval,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Range {
#[serde(rename = "Yesterday")]
Yesterday,
#[serde(rename = "custom", rename_all = "camelCase")]
Custom {
#[serde(with = "chrono::serde::ts_milliseconds")]
start: DateTime<Utc>,
#[serde(with = "chrono::serde::ts_milliseconds")]
end: DateTime<Utc>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Interval {
#[serde(rename = "1d")]
OneDay,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum MetricKind {
#[serde(rename = "METRIC_IMPRESSIONS")]
Impressions,
#[serde(rename = "METRIC_REVENUE")]
Revenue,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MetricsResponse {
pub points_list: Vec<Point>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Point {
pub metric: Metric,
pub time: Time,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Metric {
#[serde(with = "rust_decimal::serde::float_option", default)]
pub revenue: Option<Decimal>,
pub impressions: Option<u128>,
#[serde(with = "rust_decimal::serde::float_option", default)]
pub cpm: Option<Decimal>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Time {
pub seconds: u64,
pub nanos: u64,
}
#[cfg(feature = "mock")]
const _: () = {
use crate::{mock::AditudeMock, v1};
impl AditudeMock {
pub(crate) fn get_metrics_v1(&self) -> Vec<v1::MetricsResponse> {
self.metrics_v1_response
.as_ref()
.expect("missing mock `metrics_v1_response`")
.clone()
}
}
};
+114
View File
@@ -0,0 +1,114 @@
pub use crate::v1::{Interval, Range};
use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use secrecy::ExposeSecret;
use serde::{Deserialize, Serialize};
use crate::Client;
impl Client {
/// Fetches insights metrics from the API.
///
/// # Errors
///
/// Errors if the request could not be completed.
pub async fn get_metrics_v2(
&self,
req: GetMetrics<'_>,
) -> reqwest::Result<Metrics> {
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Body<'a> {
pub metrics: &'a [MetricKind],
pub range: &'static str,
pub interval: Interval,
#[serde(with = "chrono::serde::ts_milliseconds_option")]
pub start_time: Option<DateTime<Utc>>,
#[serde(with = "chrono::serde::ts_milliseconds_option")]
pub end_time: Option<DateTime<Utc>>,
}
#[cfg(feature = "mock")]
if let Some(mock) = &*self.mock.load() {
return Ok(mock.get_metrics_v2());
}
let body = Body {
metrics: req.metrics,
range: match req.range {
Range::Yesterday => "Yesterday",
Range::Custom { .. } => "custom",
},
interval: req.interval,
start_time: if let Range::Custom { start, .. } = req.range {
Some(start)
} else {
None
},
end_time: if let Range::Custom { end, .. } = req.range {
Some(end)
} else {
None
},
};
self.http
.post(format!("{}/public/insights/metrics/v2", self.api_url))
.bearer_auth(self.api_key.expose_secret())
.json(&body)
.send()
.await?
.error_for_status()?
.json::<Metrics>()
.await
}
}
#[derive(Debug, Clone)]
pub struct GetMetrics<'a> {
pub metrics: &'a [MetricKind],
pub range: Range,
pub interval: Interval,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum MetricKind {
#[serde(rename = "IMPRESSIONS")]
Impressions,
#[serde(rename = "REVENUE")]
Revenue,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Metrics {
pub responses: Vec<Response>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Response {
pub rows: Vec<Row>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Row {
#[serde(rename = "IMPRESSIONS")]
pub impressions: Option<u128>,
#[serde(rename = "REVENUE")]
pub revenue: Option<Decimal>,
#[serde(rename = "_TIME", with = "chrono::serde::ts_milliseconds")]
pub time: DateTime<Utc>,
}
#[cfg(feature = "mock")]
const _: () = {
use crate::{mock::AditudeMock, v2};
impl AditudeMock {
pub(crate) fn get_metrics_v2(&self) -> v2::Metrics {
self.metrics_v2_response
.as_ref()
.expect("missing mock `metrics_v2_response`")
.clone()
}
}
};