1use super::{PlmRequest, PlmResponse, PlmTransport};
24use serde::Deserialize;
25use serde_json::{json, Value};
26use std::cell::RefCell;
27use std::rc::Rc;
28
29#[cfg(not(target_arch = "wasm32"))]
33pub(crate) use super::backend::open_session;
34
35pub const CLIENT_VERSION: &str = env!("CARGO_PKG_VERSION");
37
38#[derive(Debug, Clone, PartialEq)]
41pub enum PlmError {
42 Unreachable(String),
45 SignIn(String),
47 Forbidden(String),
49 NotFound(String),
51 Conflict(String),
55 TooLarge(String),
57 Throttled(String),
59 Refused { status: u16, message: String },
61 Malformed(String),
64 Version { server: String, min_client: String, client: String, reason: String },
68}
69
70impl PlmError {
71 pub fn status(&self) -> Option<u16> {
73 match self {
74 PlmError::SignIn(_) => Some(401),
75 PlmError::Forbidden(_) => Some(403),
76 PlmError::NotFound(_) => Some(404),
77 PlmError::Conflict(_) => Some(409),
78 PlmError::TooLarge(_) => Some(413),
79 PlmError::Throttled(_) => Some(429),
80 PlmError::Refused { status, .. } => Some(*status),
81 PlmError::Unreachable(_) | PlmError::Malformed(_) | PlmError::Version { .. } => None,
82 }
83 }
84
85 pub fn from_response(response: &PlmResponse) -> Self {
88 let sentence = server_sentence(response);
89 let or = |fallback: &str| if sentence.is_empty() { fallback.to_string() } else { sentence.clone() };
90 match response.status {
91 401 => PlmError::SignIn(or("sign in again")),
92 403 => PlmError::Forbidden(or("you may not do that")),
93 404 => PlmError::NotFound(or("not found")),
94 409 => PlmError::Conflict(or("refused: the revision is locked or not editable")),
95 413 => PlmError::TooLarge(or("too large for the server's limit")),
96 429 => PlmError::Throttled(or("too many sign-in attempts; wait and try again")),
97 status => PlmError::Refused { status, message: or(&format!("the server answered {status}")) },
98 }
99 }
100}
101
102impl std::fmt::Display for PlmError {
103 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104 match self {
105 PlmError::Unreachable(why) => write!(f, "the PLM server did not answer: {why}"),
106 PlmError::SignIn(s) => write!(f, "sign in again: {s}"),
107 PlmError::Forbidden(s)
108 | PlmError::NotFound(s)
109 | PlmError::Conflict(s)
110 | PlmError::TooLarge(s)
111 | PlmError::Throttled(s) => f.write_str(s),
112 PlmError::Refused { message, .. } => f.write_str(message),
113 PlmError::Malformed(why) => write!(f, "the PLM server's answer was not understood: {why}"),
114 PlmError::Version { server, min_client, client, reason } => write!(
115 f,
116 "this CAD app ({client}) cannot use this PLM server ({server}, which serves CAD {min_client} and later): {reason}."
117 ),
118 }
119 }
120}
121
122fn credential_headers(credential: &Credential, method: &str) -> Vec<(String, String)> {
125 let mut headers = vec![("Accept".to_string(), "application/json".to_string())];
126 match credential {
127 Credential::Anonymous => {}
128 Credential::Token(token) => headers.push(("Authorization".into(), format!("Bearer {token}"))),
129 Credential::Session { cookie, csrf } => {
130 if let Some(cookie) = cookie {
131 headers.push(("Cookie".into(), cookie.clone()));
132 }
133 if method != "GET" {
134 if let Some(csrf) = csrf {
135 headers.push(("X-CSRF-Token".into(), csrf.clone()));
136 }
137 }
138 }
139 }
140 headers
141}
142
143fn server_sentence(response: &PlmResponse) -> String {
144 if let Ok(v) = serde_json::from_slice::<Value>(&response.body) {
145 if let Some(s) = v.get("error").and_then(Value::as_str) {
146 return s.to_string();
147 }
148 }
149 String::from_utf8_lossy(&response.body).trim().chars().take(500).collect()
150}
151
152#[derive(Debug, Clone, Default, PartialEq, Deserialize)]
155#[serde(default)]
156pub struct Me {
157 pub id: String,
158 pub username: String,
159 pub display_name: String,
160 pub groups: Vec<String>,
161 pub via: Option<String>,
163 pub csrf: Option<String>,
165 pub server_version: Option<String>,
167 pub min_client_version: Option<String>,
169 #[serde(default)]
173 pub features: Vec<String>,
174 pub can_author: Option<bool>,
177}
178
179impl Me {
180 pub fn has(&self, feature: &str) -> bool {
182 self.features.iter().any(|f| f == feature)
183 }
184}
185
186#[derive(Debug, Clone, PartialEq)]
188pub enum Credential {
189 Anonymous,
190 Token(String),
192 Session { cookie: Option<String>, csrf: Option<String> },
196}
197
198#[derive(Debug, Clone, PartialEq, Deserialize)]
201pub struct DesktopLink {
202 pub link_token: String,
203 pub poll_secret: String,
204 pub code: String,
206 pub path: String,
208 pub expires_in: u64,
209 pub interval: u64,
210}
211
212#[derive(Debug, Clone, PartialEq)]
214pub enum LinkPoll {
215 Pending,
216 Denied,
217 Approved(String),
219 Gone,
221}
222
223#[derive(Debug, Clone, Default, PartialEq, Deserialize)]
226#[serde(default)]
227pub struct StoreIndex {
228 pub seq: u64,
229 pub entries: Vec<IndexEntry>,
230}
231
232#[derive(Debug, Clone, Default, PartialEq, Deserialize)]
233#[serde(default)]
234pub struct IndexEntry {
235 pub key: String,
236 pub part_id: String,
237 pub part_number: String,
238 pub part_name: String,
239 pub revision_id: String,
240 pub revision_label: String,
241 pub lifecycle: String,
242 pub editable: bool,
243 pub content_hash: String,
244 pub size: u64,
245 pub modified: i64,
246 pub locked_by: Option<String>,
247 pub locked_by_me: bool,
248 pub document_class: Option<String>,
250}
251
252#[derive(Debug, Clone, Default, PartialEq, Deserialize)]
254#[serde(default)]
255pub struct PutAck {
256 pub seq: u64,
257 pub content_hash: String,
258 pub size: u64,
259 pub uses_refused: Option<String>,
262}
263
264#[derive(Debug, Clone, Default, PartialEq, Deserialize)]
266#[serde(default)]
267pub struct Changes {
268 pub seq: u64,
269 pub stale: bool,
272 pub keys: Vec<String>,
273}
274
275pub struct PlmClient {
277 transport: Rc<dyn PlmTransport>,
278 credential: RefCell<Credential>,
279 me: RefCell<Option<Me>>,
280 sent: std::cell::Cell<u64>,
283 by_route: RefCell<std::collections::BTreeMap<String, u64>>,
285}
286
287pub fn route_of(method: &str, path: &str) -> String {
291 let path = path.split('?').next().unwrap_or(path);
292 let segments: Vec<&str> = path.split('/').map(|s| if s.chars().any(|c| c.is_ascii_digit()) { "*" } else { s }).collect();
293 format!("{method} {}", segments.join("/"))
294}
295
296impl PlmClient {
297 pub fn new(transport: Rc<dyn PlmTransport>) -> Self {
298 Self {
299 transport,
300 credential: RefCell::new(Credential::Anonymous),
301 me: RefCell::new(None),
302 sent: std::cell::Cell::new(0),
303 by_route: RefCell::new(Default::default()),
304 }
305 }
306
307 pub fn requests_sent(&self) -> u64 {
309 self.sent.get()
310 }
311
312
313 pub fn requests_by_route(&self) -> std::collections::BTreeMap<String, u64> {
315 self.by_route.borrow().clone()
316 }
317
318 fn count(&self, method: &str, path: &str) {
319 self.sent.set(self.sent.get() + 1);
320 *self.by_route.borrow_mut().entry(route_of(method, path)).or_default() += 1;
321 }
322
323 pub fn me(&self) -> Option<Me> {
326 self.me.borrow().clone()
327 }
328
329 pub fn can_author(&self) -> Option<bool> {
332 self.me.borrow().as_ref().and_then(|me| me.can_author)
333 }
334
335 pub fn credential(&self) -> Credential {
336 self.credential.borrow().clone()
337 }
338
339 pub fn replace_token(&self, token: &str) {
344 *self.credential.borrow_mut() = Credential::Token(token.trim().to_string());
345 }
346
347 pub fn forget(&self) {
350 *self.credential.borrow_mut() = Credential::Anonymous;
351 *self.me.borrow_mut() = None;
352 }
353
354 pub async fn raw(&self, method: &'static str, path: &str, body: Option<Vec<u8>>) -> Result<PlmResponse, PlmError> {
357 let credential = self.credential.borrow().clone();
358 self.send_as(&credential, method, path, body).await
359 }
360
361 async fn send_as(
362 &self,
363 credential: &Credential,
364 method: &'static str,
365 path: &str,
366 body: Option<Vec<u8>>,
367 ) -> Result<PlmResponse, PlmError> {
368 let mut headers = credential_headers(credential, method);
369 if body.as_deref().is_some_and(|b| serde_json::from_slice::<Value>(b).is_ok()) {
370 headers.push(("Content-Type".into(), "application/json".into()));
371 }
372 self.count(method, path);
373 let request = PlmRequest { method, path: path.to_string(), headers, body: body.unwrap_or_default() };
374 self.transport.send(request).await.map_err(PlmError::Unreachable)
375 }
376
377 pub(crate) async fn call_typed(
381 &self,
382 method: &'static str,
383 path: &str,
384 body: Vec<u8>,
385 content_type: &str,
386 ) -> Result<PlmResponse, PlmError> {
387 let credential = self.credential.borrow().clone();
388 let mut headers = credential_headers(&credential, method);
389 headers.push(("Content-Type".into(), content_type.to_string()));
390 self.count(method, path);
391 let request = PlmRequest { method, path: path.to_string(), headers, body };
392 let response = self.transport.send(request).await.map_err(PlmError::Unreachable)?;
393 if (200..300).contains(&response.status) {
394 Ok(response)
395 } else {
396 Err(PlmError::from_response(&response))
397 }
398 }
399
400 pub(crate) async fn call(&self, method: &'static str, path: &str, body: Option<Vec<u8>>) -> Result<PlmResponse, PlmError> {
402 let response = self.raw(method, path, body).await?;
403 if (200..300).contains(&response.status) {
404 Ok(response)
405 } else {
406 Err(PlmError::from_response(&response))
407 }
408 }
409
410 async fn call_json<T: for<'de> Deserialize<'de>>(
411 &self,
412 method: &'static str,
413 path: &str,
414 body: Option<Value>,
415 ) -> Result<T, PlmError> {
416 let response = self.call(method, path, body.map(|b| serde_json::to_vec(&b).unwrap_or_default())).await?;
417 parse(&response, path)
418 }
419
420 pub async fn sign_in_with_token(&self, token: &str) -> Result<Me, PlmError> {
425 let token = token.trim();
426 if token.is_empty() {
427 return Err(PlmError::SignIn("paste a token (it starts with plm_)".into()));
428 }
429 self.adopt(Credential::Token(token.to_string())).await
430 }
431
432 pub async fn sign_in_with_browser_session(&self) -> Result<Me, PlmError> {
435 self.adopt(Credential::Session { cookie: None, csrf: None }).await
436 }
437
438 pub async fn sign_in_with_password(&self, username: &str, password: &str) -> Result<Me, PlmError> {
440 let cookie = self.log_in(username, password).await?;
441 self.adopt(Credential::Session { cookie, csrf: None }).await
442 }
443
444 pub async fn sign_in_for_token(&self, username: &str, password: &str, token_name: &str) -> Result<(Me, String), PlmError> {
449 let cookie = self.log_in(username, password).await?;
450 let session = Credential::Session { cookie, csrf: None };
451 let me: Me = parse(&self.checked(&session, "GET", "/api/me", None).await?, "/api/me")?;
452 let session = Credential::Session { cookie: session_cookie(&session), csrf: me.csrf.clone() };
453 if let Err(refused) = check_version(&me) {
456 let _ = self.send_as(&session, "POST", "/api/logout", Some(b"{}".to_vec())).await;
457 return Err(refused);
458 }
459 let body = serde_json::to_vec(&json!({ "name": token_name, "scope": "full" })).unwrap_or_default();
460 let minted: Value = parse(&self.checked(&session, "POST", "/api/tokens", Some(body)).await?, "/api/tokens")?;
461 let token = minted
462 .get("token")
463 .and_then(Value::as_str)
464 .ok_or_else(|| PlmError::Malformed("/api/tokens answered without a token".into()))?
465 .to_string();
466 let _ = self.send_as(&session, "POST", "/api/logout", Some(b"{}".to_vec())).await;
468 let me = self.sign_in_with_token(&token).await?;
469 Ok((me, token))
470 }
471
472 pub async fn register_desktop_link(&self, host: &str, os: &str) -> Result<DesktopLink, PlmError> {
475 let body = serde_json::to_vec(&json!({ "host": host, "os": os, "app_version": CLIENT_VERSION })).unwrap_or_default();
476 let path = "/api/desktop/link";
477 parse(&self.checked(&Credential::Anonymous, "POST", path, Some(body)).await?, path)
478 }
479
480 pub async fn poll_desktop_link(&self, poll_secret: &str) -> Result<LinkPoll, PlmError> {
484 let body = serde_json::to_vec(&json!({ "poll_secret": poll_secret })).unwrap_or_default();
485 let path = "/api/desktop/link/poll";
486 let response = self.send_as(&Credential::Anonymous, "POST", path, Some(body)).await?;
487 if response.status == 404 {
488 return Ok(LinkPoll::Gone);
489 }
490 if !(200..300).contains(&response.status) {
491 return Err(PlmError::from_response(&response));
492 }
493 let answer: Value = parse(&response, path)?;
494 match answer.get("status").and_then(Value::as_str) {
495 Some("pending") => Ok(LinkPoll::Pending),
496 Some("denied") => Ok(LinkPoll::Denied),
497 Some("approved") => match answer.get("token").and_then(Value::as_str) {
498 Some(token) => Ok(LinkPoll::Approved(token.to_string())),
499 None => Err(PlmError::Malformed(format!("{path} approved without a token"))),
500 },
501 other => Err(PlmError::Malformed(format!("{path} answered status {other:?}"))),
502 }
503 }
504
505 async fn log_in(&self, username: &str, password: &str) -> Result<Option<String>, PlmError> {
509 let body = serde_json::to_vec(&json!({ "username": username, "password": password })).unwrap_or_default();
510 let response = self.checked(&Credential::Anonymous, "POST", "/api/login", Some(body)).await?;
511 Ok(response
512 .headers
513 .iter()
514 .find(|(k, _)| k.eq_ignore_ascii_case("set-cookie"))
515 .and_then(|(_, v)| v.split(';').next())
516 .map(|c| c.trim().to_string()))
517 }
518
519 async fn checked(&self, credential: &Credential, method: &'static str, path: &str, body: Option<Vec<u8>>) -> Result<PlmResponse, PlmError> {
521 let response = self.send_as(credential, method, path, body).await?;
522 if (200..300).contains(&response.status) {
523 Ok(response)
524 } else {
525 Err(PlmError::from_response(&response))
526 }
527 }
528
529 async fn adopt(&self, credential: Credential) -> Result<Me, PlmError> {
532 let me: Me = parse(&self.checked(&credential, "GET", "/api/me", None).await?, "/api/me")?;
533 check_version(&me)?;
534 let credential = match credential {
535 Credential::Session { cookie, .. } => Credential::Session { cookie, csrf: me.csrf.clone() },
536 other => other,
537 };
538 *self.credential.borrow_mut() = credential;
539 *self.me.borrow_mut() = Some(me.clone());
540 Ok(me)
541 }
542
543 pub async fn index(&self) -> Result<StoreIndex, PlmError> {
547 self.call_json("GET", "/api/store/index", None).await
548 }
549
550 pub async fn index_rows(&self, keys: &[String]) -> Result<StoreIndex, PlmError> {
553 self.call_json("GET", &format!("/api/store/index?keys={}", keys.iter().map(|k| crate::plm::identity::segment(k)).collect::<Vec<_>>().join(",")), None).await
554 }
555
556 pub async fn get_document(&self, key: &str) -> Result<Option<Vec<u8>>, PlmError> {
560 let response = self.call("GET", &doc_path(key), None).await?;
561 Ok(if response.status == 204 { None } else { Some(response.body) })
562 }
563
564 pub async fn put_document(&self, key: &str, bytes: Vec<u8>) -> Result<PutAck, PlmError> {
567 let path = doc_path(key);
568 let response = self.call("PUT", &path, Some(bytes)).await?;
569 parse(&response, &path)
570 }
571
572 pub async fn put_document_if(&self, key: &str, bytes: Vec<u8>, expected: &str) -> Result<PutAck, PlmError> {
577 let path = doc_path(key);
578 let credential = self.credential.borrow().clone();
579 let mut headers = credential_headers(&credential, "PUT");
580 headers.push(("Content-Type".into(), "application/json".into()));
581 headers.push(("If-Match".into(), format!("\"{expected}\"")));
582 self.count("PUT", &path);
583 let request = PlmRequest { method: "PUT", path: path.clone(), headers, body: bytes };
584 let response = self.transport.send(request).await.map_err(PlmError::Unreachable)?;
585 if !(200..300).contains(&response.status) {
586 return Err(PlmError::from_response(&response));
587 }
588 parse(&response, &path)
589 }
590
591 pub async fn delete_document(&self, key: &str) -> Result<(), PlmError> {
593 self.call("DELETE", &doc_path(key), None).await.map(|_| ())
594 }
595
596 pub async fn get_preference(&self, name: &str) -> Result<Option<String>, PlmError> {
603 match self.call("GET", &preference_path(name), None).await {
604 Ok(response) if response.status == 204 => Ok(None),
605 Ok(response) => String::from_utf8(response.body)
606 .map(Some)
607 .map_err(|_| PlmError::Malformed(format!("preference {name} is not UTF-8"))),
608 Err(PlmError::NotFound(_)) => Ok(None),
609 Err(e) => Err(e),
610 }
611 }
612
613 pub async fn put_preference(&self, name: &str, value: String) -> Result<(), PlmError> {
615 self.call("PUT", &preference_path(name), Some(value.into_bytes())).await.map(|_| ())
616 }
617
618 pub async fn delete_preference(&self, name: &str) -> Result<(), PlmError> {
620 self.call("DELETE", &preference_path(name), None).await.map(|_| ())
621 }
622
623 pub async fn changes(&self, since: u64) -> Result<Changes, PlmError> {
625 self.call_json("GET", &format!("/api/store/changes?since={since}"), None).await
626 }
627}
628
629#[derive(Debug, Clone, PartialEq)]
631pub enum FeedUpdate {
632 Quiet,
634 Keys(Vec<String>),
636 Reindexed(StoreIndex),
639}
640
641#[derive(Debug, Clone, Default)]
645pub struct ChangeFeed {
646 since: Option<u64>,
647}
648
649impl ChangeFeed {
650 pub fn new() -> Self {
651 Self::default()
652 }
653
654 pub fn starting_at(seq: u64) -> Self {
656 Self { since: Some(seq) }
657 }
658
659 pub fn position(&self) -> Option<u64> {
660 self.since
661 }
662
663 pub async fn poll(&mut self, client: &PlmClient) -> Result<FeedUpdate, PlmError> {
664 let Some(since) = self.since else {
665 let index = client.index().await?;
666 self.since = Some(index.seq);
667 return Ok(FeedUpdate::Reindexed(index));
668 };
669 let changes = client.changes(since).await?;
670 if changes.stale {
671 let index = client.index().await?;
672 self.since = Some(index.seq);
673 return Ok(FeedUpdate::Reindexed(index));
674 }
675 self.since = Some(changes.seq.max(since));
676 let mut keys = Vec::new();
677 for key in changes.keys {
678 if !keys.contains(&key) {
679 keys.push(key);
680 }
681 }
682 Ok(if keys.is_empty() { FeedUpdate::Quiet } else { FeedUpdate::Keys(keys) })
683 }
684}
685
686pub fn check_version(me: &Me) -> Result<(), PlmError> {
693 check_version_of(me, CLIENT_VERSION)
694}
695
696fn check_version_of(me: &Me, client: &str) -> Result<(), PlmError> {
697 let server = me.server_version.as_deref().unwrap_or("a version it does not report");
698 let refuse = |min: &str, reason: String| PlmError::Version {
699 server: server.to_string(),
700 min_client: min.to_string(),
701 client: client.to_string(),
702 reason,
703 };
704 let Some(min) = me.min_client_version.as_deref() else {
705 return Err(refuse("?", "the server does not say which CAD versions it serves; it is older than this app".into()));
706 };
707 if !serves(min, client) {
708 return Err(refuse(min, format!("update this CAD app to {min} or later")));
709 }
710 Ok(())
711}
712
713pub fn parse_version(version: &str) -> Option<(u64, u64, u64)> {
716 let core = version.trim().split(['-', '+']).next()?;
717 let mut fields = core.split('.').map(|f| f.parse::<u64>().ok());
718 let triple = (fields.next()??, fields.next()??, fields.next()??);
719 fields.next().is_none().then_some(triple)
720}
721
722pub fn serves(min_client_version: &str, client_version: &str) -> bool {
725 match (parse_version(min_client_version), parse_version(client_version)) {
726 (Some(min), Some(client)) => client >= min,
727 _ => false,
728 }
729}
730
731fn preference_path(name: &str) -> String {
732 format!("/api/me/preferences/{name}")
733}
734
735fn doc_path(key: &str) -> String {
736 format!("/api/store/doc/{}", key.trim_start_matches('/').replace('%', "%25"))
737}
738
739fn session_cookie(credential: &Credential) -> Option<String> {
740 match credential {
741 Credential::Session { cookie, .. } => cookie.clone(),
742 _ => None,
743 }
744}
745
746fn parse<T: for<'de> Deserialize<'de>>(response: &PlmResponse, what: &str) -> Result<T, PlmError> {
747 serde_json::from_slice(&response.body).map_err(|e| PlmError::Malformed(format!("{what}: {e}")))
748}
749