Skip to main content

systemprompt_identifiers/
quota.rs

1//! [`QuotaSubjectKind`] — what a quota bucket is keyed by.
2//!
3//! The two principals are closed (`user`, `api_key`); every other kind is a
4//! scope dimension named by the gateway policy's `subject`, so the vocabulary
5//! is open and the database constrains only that a kind is present. A
6//! dimension name obeys [`ScopeDimension`]'s validation.
7//!
8//! Copyright (c) systemprompt.io — Business Source License 1.1.
9//! See <https://systemprompt.io> for licensing details.
10
11use std::fmt;
12use std::str::FromStr;
13
14use serde::de::Error as _;
15use serde::{Deserialize, Deserializer, Serialize, Serializer};
16
17use crate::ScopeDimension;
18use crate::error::IdValidationError;
19
20#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
21pub enum QuotaSubjectKind {
22    #[default]
23    User,
24    ApiKey,
25    Dimension(ScopeDimension),
26}
27
28impl QuotaSubjectKind {
29    #[must_use]
30    pub fn as_str(&self) -> &str {
31        match self {
32            Self::User => "user",
33            Self::ApiKey => "api_key",
34            Self::Dimension(name) => name.as_str(),
35        }
36    }
37
38    #[must_use]
39    pub const fn is_principal(&self) -> bool {
40        matches!(self, Self::User | Self::ApiKey)
41    }
42}
43
44impl fmt::Display for QuotaSubjectKind {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        f.write_str(self.as_str())
47    }
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
51#[error("quota subject kind is neither a principal nor a scope dimension: {0}")]
52pub struct InvalidQuotaSubjectKind(#[from] IdValidationError);
53
54impl FromStr for QuotaSubjectKind {
55    type Err = InvalidQuotaSubjectKind;
56
57    fn from_str(s: &str) -> Result<Self, Self::Err> {
58        match s {
59            "user" => Ok(Self::User),
60            "api_key" => Ok(Self::ApiKey),
61            other => Ok(Self::Dimension(ScopeDimension::try_new(other)?)),
62        }
63    }
64}
65
66impl Serialize for QuotaSubjectKind {
67    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
68        serializer.serialize_str(self.as_str())
69    }
70}
71
72impl<'de> Deserialize<'de> for QuotaSubjectKind {
73    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
74        let raw = String::deserialize(deserializer)?;
75        raw.parse().map_err(D::Error::custom)
76    }
77}