use std::sync::Arc;
use jiff::SignedDuration;
use serde::{Deserialize, Serialize};
use tollgate_core::{
AccountId, AccountSnapshot, AccountStatus, CapacityClass, CostUnits, FencingToken, Generation,
LeaseId, UsageEvent,
};
pub const API_PREFIX: &str = "/v1";
pub const MAX_USAGE_EVENT_BYTES: usize = 410;
pub const MAX_INGEST_BODY_BYTES: usize = 2 * 1024 * 1024;
pub const MAX_INGEST_REPORT_BYTES: usize = 134;
pub const MAX_SNAPSHOT_BODY_BYTES: usize = 4 * 1024 * 1024;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeysResponse {
pub revision: u64,
pub as_of: jiff::Timestamp,
pub keys: Vec<crate::CredentialRecord>,
#[serde(deserialize_with = "crate::credentials::required_option")]
pub next_after: Option<tollgate_core::KeyId>,
}
pub const MAX_KEY_RECORD_BYTES: usize = 216;
pub const MAX_KEYS_BODY_BYTES: usize = crate::MAX_KEY_PAGE_LIMIT * (MAX_KEY_RECORD_BYTES + 1) + 133;
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct LeaseTtl {
ttl_seconds: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
ttl: Option<SignedDuration>,
}
impl TryFrom<SignedDuration> for LeaseTtl {
type Error = crate::AllocateError;
fn try_from(ttl: SignedDuration) -> Result<Self, Self::Error> {
if ttl <= SignedDuration::ZERO {
return Err(crate::AllocateError::InvalidTtl);
}
Ok(match (u32::try_from(ttl.as_secs()), ttl.subsec_nanos()) {
(Ok(ttl_seconds), 0) => Self {
ttl_seconds,
ttl: None,
},
_ => Self {
ttl_seconds: 0,
ttl: Some(ttl),
},
})
}
}
impl LeaseTtl {
pub fn duration(self) -> Result<SignedDuration, crate::AllocateError> {
let ttl = match (self.ttl_seconds, self.ttl) {
(0, Some(ttl)) => ttl,
(seconds, None) => SignedDuration::from_secs(i64::from(seconds)),
_ => return Err(crate::AllocateError::InvalidTtl),
};
if ttl <= SignedDuration::ZERO {
return Err(crate::AllocateError::InvalidTtl);
}
Ok(ttl)
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct AcquireRequest {
pub account_id: AccountId,
pub requested: CostUnits,
#[serde(flatten)]
pub ttl: LeaseTtl,
}
pub type AcquireResponse = crate::Allocation;
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct ReleaseRequest {
pub lease_id: LeaseId,
pub fencing_token: FencingToken,
pub unspent: CostUnits,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct ConsolidateRequest {
pub lease_id: LeaseId,
pub fencing_token: FencingToken,
pub unspent: CostUnits,
pub requested: CostUnits,
#[serde(default, skip_serializing_if = "no_demand")]
pub needed: CostUnits,
#[serde(flatten)]
pub ttl: LeaseTtl,
}
pub type ConsolidateResponse = crate::Allocation;
#[allow(
clippy::trivially_copy_pass_by_ref,
reason = "serde's skip_serializing_if passes the field by reference"
)]
fn no_demand(units: &CostUnits) -> bool {
units.is_zero()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IngestRequest {
pub events: Vec<UsageEvent>,
}
#[derive(Debug, Serialize)]
pub struct IngestRequestRef<'a> {
pub events: &'a [UsageEvent],
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateAccountRequest {
pub account_id: AccountId,
pub initial_balance: CostUnits,
pub status: AccountStatus,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct DepositRequest {
pub units: CostUnits,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct SetStatusRequest {
pub status: AccountStatus,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct SetCapacityClassRequest {
pub capacity_class: CapacityClass,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct SetStatusResponse {
pub republished: usize,
pub unreadable: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PublishSnapshotRequest {
pub snapshot: Arc<AccountSnapshot>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PrincipalsResponse {
pub principals: Vec<tollgate_core::Principal>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Problem {
pub status: u16,
pub code: String,
pub title: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub generation: Option<Generation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub balance_exhaustion: Option<tollgate_core::BalanceExhaustion>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub balance_shortfall: Option<tollgate_core::BalanceShortfall>,
}
#[cfg(test)]
mod tests {
use super::*;
use jiff::Timestamp;
use tollgate_core::PolicyRevision;
use tollgate_core::RequestId;
use tollgate_core::UsageSource;
#[test]
fn consolidation_demand_is_invisible_until_used() {
let request = |needed| ConsolidateRequest {
lease_id: LeaseId(1),
fencing_token: FencingToken(1),
unspent: CostUnits(30),
requested: CostUnits(1_000),
needed: CostUnits(needed),
ttl: LeaseTtl::try_from(SignedDuration::from_secs(60)).unwrap(),
};
let idle = serde_json::to_value(request(0)).unwrap();
assert!(idle.get("needed").is_none(), "{idle}");
let old: ConsolidateRequest = serde_json::from_value(idle).unwrap();
assert_eq!(old.needed, CostUnits::ZERO);
let demand = serde_json::to_value(request(51)).unwrap();
assert_eq!(demand["needed"], 51);
let parsed: ConsolidateRequest = serde_json::from_value(demand).unwrap();
assert_eq!(parsed.needed, CostUnits(51));
}
fn event(seq: u128) -> UsageEvent {
UsageEvent::new(
RequestId(seq),
AccountId(7),
UsageSource::Leased {
lease_id: LeaseId(11),
fencing_token: FencingToken(3),
},
CostUnits(64),
Timestamp::from_second(1_755_600_000).unwrap(),
PolicyRevision::UNSTATED,
None,
)
}
#[test]
fn borrowed_and_owned_ingest_requests_serialize_identically() {
for count in [0, 1, 256] {
let events: Vec<UsageEvent> = (0..count).map(event).collect();
let borrowed = serde_json::to_string(&IngestRequestRef { events: &events }).unwrap();
let owned = serde_json::to_string(&IngestRequest {
events: events.clone(),
})
.unwrap();
assert_eq!(
borrowed, owned,
"the two ingest forms disagree at {count} events, so the wire \
format depends on which one the caller happened to use"
);
}
}
#[test]
fn a_borrowed_request_deserializes_as_the_owned_one() {
let events: Vec<UsageEvent> = (0..3).map(event).collect();
let body = serde_json::to_string(&IngestRequestRef { events: &events }).unwrap();
let received: IngestRequest = serde_json::from_str(&body).unwrap();
assert_eq!(received.events, events);
}
#[test]
fn an_empty_batch_stays_an_empty_list() {
let body = serde_json::to_string(&IngestRequestRef { events: &[] }).unwrap();
assert_eq!(body, r#"{"events":[]}"#);
let received: IngestRequest = serde_json::from_str(&body).unwrap();
assert!(received.events.is_empty());
}
#[test]
fn high_bit_ids_are_portable_text_in_an_untyped_json_consumer() {
let high = (1u128 << 127) | 0x2a;
let mut event = event(high);
event.account_id = AccountId(high + 1);
event.source = UsageSource::Leased {
lease_id: LeaseId(high + 2),
fencing_token: FencingToken(3),
};
let events = [event];
let value = serde_json::to_value(IngestRequestRef { events: &events }).unwrap();
let event = &value["events"][0];
let leased = &event["source"]["Leased"];
for (field, node, expected) in [
("request_id", event, high),
("account_id", event, high + 1),
("lease_id", leased, high + 2),
] {
let text = node[field]
.as_str()
.unwrap_or_else(|| panic!("{field} must be JSON text, got {}", node[field]));
assert_eq!(text.len(), 32);
assert_eq!(u128::from_str_radix(text, 16).unwrap(), expected);
}
let received: IngestRequest = serde_json::from_value(value).unwrap();
assert_eq!(received.events, events);
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct AccountResponse {
pub account_id: tollgate_core::AccountId,
pub as_of: jiff::Timestamp,
pub status: tollgate_core::AccountStatus,
pub capacity_class: tollgate_core::CapacityClass,
pub budget: Option<tollgate_core::BudgetSchedule>,
pub period_start: jiff::Timestamp,
pub balance: tollgate_core::CostUnits,
pub outstanding_lease_grants: tollgate_core::CostUnits,
pub settled_usage: tollgate_core::CostUnits,
pub expired_allowance: tollgate_core::CostUnits,
pub settlement_loss: tollgate_core::CostUnits,
pub deposited: tollgate_core::CostUnits,
pub overage_recorded: tollgate_core::CostUnits,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SetBudgetRequest {
#[serde(deserialize_with = "required_budget")]
pub budget: Option<tollgate_core::BudgetSchedule>,
}
fn required_budget<'de, D>(
deserializer: D,
) -> Result<Option<tollgate_core::BudgetSchedule>, D::Error>
where
D: serde::Deserializer<'de>,
{
Option::deserialize(deserializer)
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct SetBudgetResponse {
pub previous: Option<tollgate_core::BudgetSchedule>,
pub current: Option<tollgate_core::BudgetSchedule>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct IssueKeyRequest {
pub key_id: tollgate_core::KeyId,
pub max_active_keys: std::num::NonZeroUsize,
#[serde(default)]
pub not_after: Option<jiff::Timestamp>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IssuedKeyResponse {
pub key_id: tollgate_core::KeyId,
pub secret: String,
pub not_after: Option<jiff::Timestamp>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct AccountKeyResponse {
pub key_id: tollgate_core::KeyId,
pub not_after: Option<jiff::Timestamp>,
pub revoked_at: Option<jiff::Timestamp>,
pub live: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccountKeysResponse {
pub as_of: jiff::Timestamp,
pub keys: Vec<AccountKeyResponse>,
pub next_after: Option<tollgate_core::KeyId>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct RevokeKeyResponse {
pub key_id: tollgate_core::KeyId,
pub retired: bool,
}