1use std::time::Duration;
2use tonic::transport::{Channel, Endpoint};
3
4use crate::anytype::ClientCommandsClient;
5use crate::auth::{
6 create_session_token_from_account_key_with_policy,
7 create_session_token_from_app_key_with_policy,
8};
9use crate::deadline::{GrpcDeadlineService, GrpcTimeoutPolicy};
10use crate::error::AnytypeGrpcError;
11
12const ANYTYPE_GRPC_ENDPOINT_ENV: &str = "ANYTYPE_GRPC_ENDPOINT";
14const ANYTYPE_GRPC_ENDPOINT: &str = "http://127.0.0.1:31010"; pub fn default_grpc_endpoint() -> String {
18 std::env::var(ANYTYPE_GRPC_ENDPOINT_ENV).unwrap_or_else(|_| ANYTYPE_GRPC_ENDPOINT.to_string())
19}
20
21#[derive(Clone)]
23pub struct AnytypeGrpcConfig {
24 endpoint: String,
25 grpc_timeouts: Option<GrpcTimeoutPolicy>,
26}
27
28impl std::fmt::Debug for AnytypeGrpcConfig {
29 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 formatter
31 .debug_struct("AnytypeGrpcConfig")
32 .field("endpoint", &"redacted")
33 .field("grpc_timeouts", &self.grpc_timeouts)
34 .finish()
35 }
36}
37
38impl Default for AnytypeGrpcConfig {
39 fn default() -> Self {
40 Self {
41 endpoint: default_grpc_endpoint(),
42 grpc_timeouts: None,
43 }
44 }
45}
46
47impl AnytypeGrpcConfig {
48 pub fn new(endpoint: impl Into<String>) -> Self {
49 Self {
50 endpoint: endpoint.into(),
51 grpc_timeouts: None,
52 }
53 }
54
55 pub fn endpoint(&self) -> &str {
56 &self.endpoint
57 }
58
59 #[must_use]
64 pub fn grpc_timeouts(mut self, policy: GrpcTimeoutPolicy) -> Self {
65 self.grpc_timeouts = Some(policy);
66 self
67 }
68
69 pub fn resolved_grpc_timeouts(
71 &self,
72 ) -> Result<GrpcTimeoutPolicy, crate::deadline::GrpcTimeoutConfigError> {
73 GrpcTimeoutPolicy::resolve(self.grpc_timeouts)
74 }
75}
76
77pub type AnytypeGrpcService = GrpcDeadlineService<Channel>;
79
80#[derive(Clone)]
82pub struct AnytypeGrpcClient {
83 channel: Channel,
84 token: String,
85 endpoint: String,
86 grpc_timeouts: GrpcTimeoutPolicy,
87}
88
89impl std::fmt::Debug for AnytypeGrpcClient {
90 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 formatter
92 .debug_struct("AnytypeGrpcClient")
93 .field("channel", &"redacted")
94 .field("token_configured", &!self.token.is_empty())
95 .field("endpoint", &"redacted")
96 .field("grpc_timeouts", &self.grpc_timeouts)
97 .finish()
98 }
99}
100
101impl AnytypeGrpcClient {
102 pub fn get_endpoint(&self) -> &str {
104 &self.endpoint
105 }
106
107 pub async fn connect_channel(config: &AnytypeGrpcConfig) -> Result<Channel, AnytypeGrpcError> {
112 let _ = config.resolved_grpc_timeouts()?;
115 let endpoint = Endpoint::from_shared(config.endpoint.clone())?
116 .connect_timeout(Duration::from_secs(30))
117 .tcp_keepalive(Some(Duration::from_secs(60)))
118 .http2_keep_alive_interval(Duration::from_secs(30))
119 .keep_alive_timeout(Duration::from_secs(10))
120 .keep_alive_while_idle(true);
121 Ok(endpoint.connect().await?)
122 }
123
124 pub async fn from_account_key(
127 config: &AnytypeGrpcConfig,
128 account_key: impl AsRef<str>,
129 ) -> Result<Self, AnytypeGrpcError> {
130 let grpc_timeouts = config.resolved_grpc_timeouts()?;
131 let channel = Self::connect_channel(config).await?;
132 let token = create_session_token_from_account_key_with_policy(
133 channel.clone(),
134 account_key,
135 grpc_timeouts,
136 )
137 .await?;
138 Ok(Self {
139 channel,
140 token,
141 endpoint: config.endpoint.clone(),
142 grpc_timeouts,
143 })
144 }
145
146 pub async fn from_app_key(
148 config: &AnytypeGrpcConfig,
149 app_key: impl AsRef<str>,
150 ) -> Result<Self, AnytypeGrpcError> {
151 let grpc_timeouts = config.resolved_grpc_timeouts()?;
152 let channel = Self::connect_channel(config).await?;
153 let token =
154 create_session_token_from_app_key_with_policy(channel.clone(), app_key, grpc_timeouts)
155 .await?;
156 Ok(Self {
157 channel,
158 token,
159 endpoint: config.endpoint.clone(),
160 grpc_timeouts,
161 })
162 }
163
164 pub async fn from_token(
165 config: &AnytypeGrpcConfig,
166 token: impl Into<String>,
167 ) -> Result<Self, AnytypeGrpcError> {
168 let grpc_timeouts = config.resolved_grpc_timeouts()?;
169 let channel = Self::connect_channel(config).await?;
170 Ok(Self {
171 channel,
172 token: token.into(),
173 endpoint: config.endpoint.clone(),
174 grpc_timeouts,
175 })
176 }
177
178 pub fn client_commands(&self) -> ClientCommandsClient<AnytypeGrpcService> {
184 ClientCommandsClient::new(self.deadline_channel())
185 }
186
187 pub fn token(&self) -> &str {
188 &self.token
189 }
190
191 #[must_use]
196 pub fn channel(&self) -> Channel {
197 self.channel.clone()
198 }
199
200 #[must_use]
202 pub fn deadline_channel(&self) -> AnytypeGrpcService {
203 GrpcDeadlineService::new_resolved(self.channel.clone(), self.grpc_timeouts)
204 }
205
206 #[must_use]
208 pub const fn grpc_timeouts(&self) -> GrpcTimeoutPolicy {
209 self.grpc_timeouts
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[tokio::test]
218 async fn debug_output_redacts_endpoint_token_and_transport() {
219 let endpoint = "http://ENDPOINT_SECRET.invalid";
220 let config = AnytypeGrpcConfig::new(endpoint);
221 let config_debug = format!("{config:?}");
222 assert!(!config_debug.contains("ENDPOINT_SECRET"));
223
224 let channel = Endpoint::from_static("http://CHANNEL_SECRET.invalid").connect_lazy();
225 let client = AnytypeGrpcClient {
226 channel,
227 token: "TOKEN_SECRET".to_owned(),
228 endpoint: endpoint.to_owned(),
229 grpc_timeouts: GrpcTimeoutPolicy::default(),
230 };
231 let client_debug = format!("{client:?}");
232 for secret in ["ENDPOINT_SECRET", "CHANNEL_SECRET", "TOKEN_SECRET"] {
233 assert!(!client_debug.contains(secret));
234 }
235 }
236}