use reqwest::RequestBuilder;
use serde::{Deserialize, Serialize};
use url::Url;
use uuid::Uuid;
use crate::{send_and_translate_response, Deepgram};
use response::{
CreatedDistributionCredentials, DistributionCredentialsEntry, DistributionCredentialsList,
Message,
};
pub mod response;
#[derive(Debug, Clone)]
pub struct SelfHosted<'a>(&'a Deepgram);
impl Deepgram {
pub fn self_hosted(&self) -> SelfHosted<'_> {
self.into()
}
}
impl<'a> From<&'a Deepgram> for SelfHosted<'a> {
fn from(deepgram: &'a Deepgram) -> Self {
Self(deepgram)
}
}
pub const DEFAULT_PROVIDER: &str = "quay";
pub const DEFAULT_SCOPE: Scope = Scope::Products;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Scope {
Products,
Api,
Engine,
LicenseProxy,
Dgtools,
Billing,
Hotpepper,
MetricsServer,
Unknown(String),
}
impl Scope {
pub fn as_str(&self) -> &str {
match self {
Scope::Products => "self-hosted:products",
Scope::Api => "self-hosted:product:api",
Scope::Engine => "self-hosted:product:engine",
Scope::LicenseProxy => "self-hosted:product:license-proxy",
Scope::Dgtools => "self-hosted:product:dgtools",
Scope::Billing => "self-hosted:product:billing",
Scope::Hotpepper => "self-hosted:product:hotpepper",
Scope::MetricsServer => "self-hosted:product:metrics-server",
Scope::Unknown(scope) => scope,
}
}
}
impl AsRef<str> for Scope {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl From<&str> for Scope {
fn from(value: &str) -> Self {
match value {
"self-hosted:products" | "onprem:products" => Scope::Products,
"self-hosted:product:api" | "onprem:product:api" => Scope::Api,
"self-hosted:product:engine" | "onprem:product:engine" => Scope::Engine,
"self-hosted:product:license-proxy" | "onprem:product:license-proxy" => {
Scope::LicenseProxy
}
"self-hosted:product:dgtools" | "onprem:product:dgtools" => Scope::Dgtools,
"self-hosted:product:billing" | "onprem:product:billing" => Scope::Billing,
"self-hosted:product:hotpepper" | "onprem:product:hotpepper" => Scope::Hotpepper,
"self-hosted:product:metrics-server" | "onprem:product:metrics-server" => {
Scope::MetricsServer
}
other => Scope::Unknown(other.to_string()),
}
}
}
impl From<String> for Scope {
fn from(value: String) -> Self {
Scope::from(value.as_str())
}
}
impl Serialize for Scope {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for Scope {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
Ok(Scope::from(String::deserialize(deserializer)?))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[non_exhaustive]
pub struct CreateDistributionCredentials {
pub provider: String,
pub comment: String,
pub scopes: Vec<Scope>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<Vec<String>>,
}
impl CreateDistributionCredentials {
pub fn new(comment: impl Into<String>) -> Self {
Self {
provider: DEFAULT_PROVIDER.to_string(),
comment: comment.into(),
scopes: vec![DEFAULT_SCOPE],
tags: None,
}
}
pub fn comment(mut self, comment: impl Into<String>) -> Self {
self.comment = comment.into();
self
}
pub fn scopes<S: Into<Scope>>(mut self, scopes: impl IntoIterator<Item = S>) -> Self {
self.scopes = scopes.into_iter().map(Into::into).collect();
self
}
pub fn provider(mut self, provider: impl Into<String>) -> Self {
self.provider = provider.into();
self
}
pub fn tags<S: Into<String>>(mut self, tags: impl IntoIterator<Item = S>) -> Self {
self.tags = Some(tags.into_iter().map(Into::into).collect());
self
}
}
impl SelfHosted<'_> {
pub async fn list_distribution_credentials(
&self,
project_id: &str,
) -> crate::Result<DistributionCredentialsList> {
send_and_translate_response(self.0.client.get(self.credentials_url(project_id))).await
}
pub async fn get_distribution_credentials(
&self,
project_id: &str,
distribution_credentials_id: Uuid,
) -> crate::Result<DistributionCredentialsEntry> {
send_and_translate_response(
self.0
.client
.get(self.credentials_id_url(project_id, distribution_credentials_id)),
)
.await
}
pub async fn create_distribution_credentials(
&self,
project_id: &str,
request: &CreateDistributionCredentials,
) -> crate::Result<CreatedDistributionCredentials> {
send_and_translate_response(
self.create_distribution_credentials_request(project_id, request),
)
.await
}
fn create_distribution_credentials_request(
&self,
project_id: &str,
request: &CreateDistributionCredentials,
) -> RequestBuilder {
self.0
.client
.post(self.credentials_url(project_id))
.json(request)
}
pub async fn delete_distribution_credentials(
&self,
project_id: &str,
distribution_credentials_id: Uuid,
) -> crate::Result<Message> {
send_and_translate_response(
self.0
.client
.delete(self.credentials_id_url(project_id, distribution_credentials_id)),
)
.await
}
fn credentials_url(&self, project_id: &str) -> Url {
self.0
.base_url
.join(&format!(
"v1/projects/{project_id}/self-hosted/distribution/credentials"
))
.expect("base_url is checked to be a valid base_url when constructing Deepgram client")
}
fn credentials_id_url(&self, project_id: &str, distribution_credentials_id: Uuid) -> Url {
self.0
.base_url
.join(&format!(
"v1/projects/{project_id}/self-hosted/distribution/credentials/{distribution_credentials_id}"
))
.expect("base_url is checked to be a valid base_url when constructing Deepgram client")
}
}
#[cfg(test)]
mod tests {
use uuid::Uuid;
use super::{CreateDistributionCredentials, Scope, DEFAULT_PROVIDER, DEFAULT_SCOPE};
use crate::Deepgram;
const CREDENTIALS_ID: Uuid = Uuid::from_u128(0x8b36cfd0_472f_4a21_833f_2d6343c3a2f3);
fn body_json(request: &reqwest::Request) -> serde_json::Value {
let bytes = request
.body()
.expect("request has a body")
.as_bytes()
.expect("body is buffered");
serde_json::from_slice(bytes).expect("body is JSON")
}
#[test]
fn create_request_defaults() {
let request = CreateDistributionCredentials::new("primary cluster");
assert_eq!(request.provider, DEFAULT_PROVIDER);
assert_eq!(request.comment, "primary cluster");
assert_eq!(request.scopes, [DEFAULT_SCOPE]);
assert_eq!(request.tags, None);
}
#[test]
fn create_request_builder_replaces_scopes_and_sets_tags() {
let request = CreateDistributionCredentials::new("c")
.scopes([Scope::Api, Scope::Engine])
.tags(["prod", "us-east"])
.provider("quay")
.comment("renamed");
assert_eq!(request.scopes, [Scope::Api, Scope::Engine]);
assert_eq!(
request.tags,
Some(vec!["prod".to_string(), "us-east".to_string()])
);
assert_eq!(request.comment, "renamed");
}
#[test]
fn scopes_builder_also_accepts_wire_strings() {
let request = CreateDistributionCredentials::new("c").scopes([
"self-hosted:product:api",
"self-hosted:product:not-invented-yet",
]);
assert_eq!(
request.scopes,
[
Scope::Api,
Scope::Unknown("self-hosted:product:not-invented-yet".to_string())
]
);
}
#[test]
fn every_named_scope_round_trips_through_its_wire_string() {
let scopes = [
(Scope::Products, "self-hosted:products"),
(Scope::Api, "self-hosted:product:api"),
(Scope::Engine, "self-hosted:product:engine"),
(Scope::LicenseProxy, "self-hosted:product:license-proxy"),
(Scope::Dgtools, "self-hosted:product:dgtools"),
(Scope::Billing, "self-hosted:product:billing"),
(Scope::Hotpepper, "self-hosted:product:hotpepper"),
(Scope::MetricsServer, "self-hosted:product:metrics-server"),
];
for (scope, wire) in scopes {
assert_eq!(scope.as_str(), wire);
assert_eq!(scope.as_ref(), wire);
assert_eq!(Scope::from(wire), scope);
assert_eq!(Scope::from(wire.to_string()), scope);
assert_eq!(serde_json::to_value(&scope).unwrap(), wire);
assert_eq!(
serde_json::from_value::<Scope>(serde_json::json!(wire)).unwrap(),
scope
);
}
}
#[test]
fn onprem_aliases_resolve_to_the_named_variant() {
for (alias, scope) in [
("onprem:products", Scope::Products),
("onprem:product:api", Scope::Api),
("onprem:product:engine", Scope::Engine),
("onprem:product:license-proxy", Scope::LicenseProxy),
("onprem:product:dgtools", Scope::Dgtools),
("onprem:product:billing", Scope::Billing),
("onprem:product:hotpepper", Scope::Hotpepper),
("onprem:product:metrics-server", Scope::MetricsServer),
] {
let parsed: Scope = serde_json::from_value(serde_json::json!(alias)).unwrap();
assert_eq!(parsed, scope, "{alias}");
assert!(parsed.as_str().starts_with("self-hosted:"), "{alias}");
}
}
#[test]
fn unknown_scope_round_trips_verbatim() {
let scope: Scope =
serde_json::from_value(serde_json::json!("self-hosted:product:not-invented-yet"))
.unwrap();
assert_eq!(
scope,
Scope::Unknown("self-hosted:product:not-invented-yet".to_string())
);
assert_eq!(scope.as_str(), "self-hosted:product:not-invented-yet");
assert_eq!(
serde_json::to_value(&scope).unwrap(),
"self-hosted:product:not-invented-yet"
);
}
#[test]
fn create_sends_provider_comment_scopes_in_json_body_with_no_query() {
let dg = Deepgram::new("token").unwrap();
let request = CreateDistributionCredentials::new("my deployment");
let built = dg
.self_hosted()
.create_distribution_credentials_request("proj-123", &request)
.build()
.unwrap();
assert_eq!(built.method(), reqwest::Method::POST);
assert_eq!(
built.url().as_str(),
"https://api.deepgram.com/v1/projects/proj-123/self-hosted/distribution/credentials"
);
assert_eq!(built.url().query(), None, "no query parameters");
assert_eq!(
built.headers().get("content-type").unwrap(),
"application/json"
);
let body = body_json(&built);
assert_eq!(
body,
serde_json::json!({
"provider": "quay",
"comment": "my deployment",
"scopes": ["self-hosted:products"],
})
);
}
#[test]
fn create_sends_custom_scopes_and_tags_in_json_body() {
let dg = Deepgram::new("token").unwrap();
let request = CreateDistributionCredentials::new("engine only")
.scopes([Scope::Engine])
.tags(["staging"]);
let built = dg
.self_hosted()
.create_distribution_credentials_request("proj-123", &request)
.build()
.unwrap();
assert_eq!(built.url().query(), None);
assert_eq!(
body_json(&built),
serde_json::json!({
"provider": "quay",
"comment": "engine only",
"scopes": ["self-hosted:product:engine"],
"tags": ["staging"],
})
);
}
#[test]
fn credentials_urls() {
let dg = Deepgram::new("token").unwrap();
let self_hosted = dg.self_hosted();
assert_eq!(
self_hosted.credentials_url("proj-123").as_str(),
"https://api.deepgram.com/v1/projects/proj-123/self-hosted/distribution/credentials"
);
assert_eq!(
self_hosted
.credentials_id_url("proj-123", CREDENTIALS_ID)
.as_str(),
"https://api.deepgram.com/v1/projects/proj-123/self-hosted/distribution/credentials/8b36cfd0-472f-4a21-833f-2d6343c3a2f3"
);
}
#[test]
fn credentials_urls_honor_a_custom_base_url() {
let dg = Deepgram::with_base_url("http://localhost:8888/abc/").unwrap();
let self_hosted = dg.self_hosted();
assert_eq!(
self_hosted.credentials_url("proj-123").as_str(),
"http://localhost:8888/abc/v1/projects/proj-123/self-hosted/distribution/credentials"
);
assert_eq!(
self_hosted
.credentials_id_url("proj-123", CREDENTIALS_ID)
.as_str(),
"http://localhost:8888/abc/v1/projects/proj-123/self-hosted/distribution/credentials/8b36cfd0-472f-4a21-833f-2d6343c3a2f3"
);
let built = self_hosted
.create_distribution_credentials_request(
"proj-123",
&CreateDistributionCredentials::new("c"),
)
.build()
.unwrap();
assert_eq!(
built.url().as_str(),
"http://localhost:8888/abc/v1/projects/proj-123/self-hosted/distribution/credentials"
);
}
}