1use std::fmt;
2use tonic::transport::Channel;
3
4pub mod proto {
5 tonic::include_proto!("lores.panda.v2");
6}
7
8use proto::{GetNodeRequest, InfoRequest, OperationEvent, PublishRequest, SubscribeRequest, panda_client::PandaClient as TonicPandaClient};
9use tonic::{Code, Response, Status, Streaming};
10
11#[derive(Clone, PartialEq, Eq)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14pub struct OperationId(pub Vec<u8>);
15
16impl OperationId {
17 pub fn to_hex(&self) -> String {
18 hex::encode(&self.0)
19 }
20
21 pub fn into_non_empty(self) -> Option<Vec<u8>> {
23 if self.0.is_empty() { None } else { Some(self.0) }
24 }
25}
26
27impl fmt::Debug for OperationId {
28 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29 write!(f, "OperationId({})", self.to_hex())
30 }
31}
32
33impl fmt::Display for OperationId {
34 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35 f.write_str(&self.to_hex())
36 }
37}
38
39#[derive(Clone, PartialEq, Eq)]
41#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
42pub struct NodeId(pub Vec<u8>);
43
44impl NodeId {
45 pub fn to_hex(&self) -> String {
46 hex::encode(&self.0)
47 }
48
49 pub fn into_non_empty(self) -> Option<Vec<u8>> {
51 if self.0.is_empty() { None } else { Some(self.0) }
52 }
53}
54
55impl fmt::Debug for NodeId {
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57 write!(f, "NodeId({})", self.to_hex())
58 }
59}
60
61impl fmt::Display for NodeId {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 f.write_str(&self.to_hex())
64 }
65}
66
67#[derive(Clone, PartialEq, Eq)]
69#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
70pub struct RegionId(pub Vec<u8>);
71
72impl RegionId {
73 pub fn to_hex(&self) -> String {
74 hex::encode(&self.0)
75 }
76}
77
78impl fmt::Debug for RegionId {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 write!(f, "RegionId({})", self.to_hex())
81 }
82}
83
84impl fmt::Display for RegionId {
85 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86 f.write_str(&self.to_hex())
87 }
88}
89
90#[derive(Debug, Clone)]
92pub struct NodeInfo {
93 pub node_id: String,
94 pub name: Option<String>,
95 pub domain_on_internet: Option<String>,
96}
97
98#[derive(Debug, Clone)]
100pub struct InfoResult {
101 pub node_id: NodeId,
102 pub region: RegionInfo,
103}
104
105#[derive(Debug, Clone)]
107pub struct RegionInfo {
108 pub region_id: RegionId,
109 pub slug: Option<String>,
110 pub name: Option<String>,
111}
112
113#[derive(Debug, Clone)]
116pub struct PublishResult {
117 pub operation_id: OperationId,
118 pub node_id: NodeId,
119}
120
121#[derive(Debug)]
123pub enum PandaError {
124 RegionNotBound(String),
128 Rpc(Status),
130}
131
132impl std::fmt::Display for PandaError {
133 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134 match self {
135 PandaError::RegionNotBound(msg) => write!(f, "{msg}"),
136 PandaError::Rpc(s) => write!(f, "RPC error: {s}"),
137 }
138 }
139}
140
141impl std::error::Error for PandaError {}
142
143impl From<Status> for PandaError {
144 fn from(s: Status) -> Self {
145 if s.code() == Code::NotFound {
146 PandaError::RegionNotBound(s.message().to_string())
147 } else {
148 PandaError::Rpc(s)
149 }
150 }
151}
152
153#[derive(Debug)]
155pub enum GetNodeError {
156 NodeNotFound(String),
158 Other(PandaError),
160}
161
162impl std::fmt::Display for GetNodeError {
163 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164 match self {
165 GetNodeError::NodeNotFound(msg) => write!(f, "{msg}"),
166 GetNodeError::Other(e) => write!(f, "{e}"),
167 }
168 }
169}
170
171impl std::error::Error for GetNodeError {}
172
173pub struct PandaClient {
175 inner: TonicPandaClient<Channel>,
176}
177
178impl PandaClient {
179 pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
188 where
189 D: TryInto<tonic::transport::Endpoint>,
190 D::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
191 {
192 let inner = TonicPandaClient::connect(dst).await?;
193 Ok(Self { inner })
194 }
195
196 pub fn connect_lazy<D>(dst: D) -> Result<Self, tonic::transport::Error>
200 where
201 D: TryInto<tonic::transport::Endpoint>,
202 D::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
203 {
204 let endpoint = tonic::transport::Endpoint::new(dst)?;
205 let inner = TonicPandaClient::new(endpoint.connect_lazy());
206 Ok(Self { inner })
207 }
208
209 pub async fn publish(
218 &mut self,
219 app_id: impl Into<String>,
220 instance_id: impl Into<String>,
221 payload: impl Into<Vec<u8>>,
222 idempotency_key: Option<Vec<u8>>,
223 ) -> Result<PublishResult, PandaError> {
224 let request = PublishRequest {
225 app_id: app_id.into(),
226 instance_id: instance_id.into(),
227 payload: payload.into(),
228 idempotency_key: idempotency_key.unwrap_or_default(),
229 };
230 self.inner
231 .publish(request)
232 .await
233 .map(|r| {
234 let r = r.into_inner();
235 PublishResult {
236 operation_id: OperationId(r.operation_id),
237 node_id: NodeId(r.node_id),
238 }
239 })
240 .map_err(PandaError::from)
241 }
242
243 pub async fn subscribe(
248 &mut self,
249 app_id: impl Into<String>,
250 instance_id: impl Into<String>,
251 ) -> Result<Response<Streaming<OperationEvent>>, PandaError> {
252 let request = SubscribeRequest {
253 app_id: app_id.into(),
254 instance_id: instance_id.into(),
255 };
256 self.inner.subscribe(request).await.map_err(PandaError::from)
257 }
258
259 pub async fn info(&mut self, app_id: impl Into<String>, instance_id: impl Into<String>) -> Result<InfoResult, PandaError> {
261 self.inner
262 .info(InfoRequest {
263 app_id: app_id.into(),
264 instance_id: instance_id.into(),
265 })
266 .await
267 .map_err(PandaError::from)
268 .and_then(|r| {
269 let r = r.into_inner();
270 let region = r
271 .region
272 .ok_or_else(|| PandaError::Rpc(tonic::Status::internal("server returned info response without region")))?;
273 Ok(InfoResult {
274 node_id: NodeId(r.node_id),
275 region: RegionInfo {
276 region_id: RegionId(region.region_id),
277 slug: region.slug,
278 name: region.name,
279 },
280 })
281 })
282 }
283
284 pub async fn get_node(
286 &mut self,
287 app_id: impl Into<String>,
288 instance_id: impl Into<String>,
289 node_id: impl Into<String>,
290 ) -> Result<NodeInfo, GetNodeError> {
291 self.inner
292 .get_node(GetNodeRequest {
293 app_id: app_id.into(),
294 instance_id: instance_id.into(),
295 node_id: node_id.into(),
296 })
297 .await
298 .map(|r| {
299 let r = r.into_inner();
300 NodeInfo {
301 node_id: r.node_id,
302 name: r.name,
303 domain_on_internet: r.domain_on_internet,
304 }
305 })
306 .map_err(|s| match s.code() {
307 Code::NotFound => GetNodeError::NodeNotFound(s.message().to_string()),
308 _ => GetNodeError::Other(PandaError::from(s)),
309 })
310 }
311}