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}
175
176impl Me {
177 pub fn has(&self, feature: &str) -> bool {
179 self.features.iter().any(|f| f == feature)
180 }
181}
182
183#[derive(Debug, Clone, PartialEq)]
185pub enum Credential {
186 Anonymous,
187 Token(String),
189 Session { cookie: Option<String>, csrf: Option<String> },
193}
194
195#[derive(Debug, Clone, Default, PartialEq, Deserialize)]
198#[serde(default)]
199pub struct StoreIndex {
200 pub seq: u64,
201 pub entries: Vec<IndexEntry>,
202}
203
204#[derive(Debug, Clone, Default, PartialEq, Deserialize)]
205#[serde(default)]
206pub struct IndexEntry {
207 pub key: String,
208 pub part_id: String,
209 pub part_number: String,
210 pub part_name: String,
211 pub revision_id: String,
212 pub revision_label: String,
213 pub lifecycle: String,
214 pub editable: bool,
215 pub content_hash: String,
216 pub size: u64,
217 pub modified: i64,
218 pub locked_by: Option<String>,
219 pub locked_by_me: bool,
220 pub document_class: Option<String>,
222}
223
224#[derive(Debug, Clone, Default, PartialEq, Deserialize)]
226#[serde(default)]
227pub struct PutAck {
228 pub seq: u64,
229 pub content_hash: String,
230 pub size: u64,
231}
232
233#[derive(Debug, Clone, Default, PartialEq, Deserialize)]
235#[serde(default)]
236pub struct Changes {
237 pub seq: u64,
238 pub stale: bool,
241 pub keys: Vec<String>,
242}
243
244pub struct PlmClient {
246 transport: Rc<dyn PlmTransport>,
247 credential: RefCell<Credential>,
248 me: RefCell<Option<Me>>,
249}
250
251impl PlmClient {
252 pub fn new(transport: Rc<dyn PlmTransport>) -> Self {
253 Self { transport, credential: RefCell::new(Credential::Anonymous), me: RefCell::new(None) }
254 }
255
256 pub fn me(&self) -> Option<Me> {
259 self.me.borrow().clone()
260 }
261
262 pub fn credential(&self) -> Credential {
263 self.credential.borrow().clone()
264 }
265
266 pub fn forget(&self) {
269 *self.credential.borrow_mut() = Credential::Anonymous;
270 *self.me.borrow_mut() = None;
271 }
272
273 pub async fn raw(&self, method: &'static str, path: &str, body: Option<Vec<u8>>) -> Result<PlmResponse, PlmError> {
276 let credential = self.credential.borrow().clone();
277 self.send_as(&credential, method, path, body).await
278 }
279
280 async fn send_as(
281 &self,
282 credential: &Credential,
283 method: &'static str,
284 path: &str,
285 body: Option<Vec<u8>>,
286 ) -> Result<PlmResponse, PlmError> {
287 let mut headers = credential_headers(credential, method);
288 if body.as_deref().is_some_and(|b| serde_json::from_slice::<Value>(b).is_ok()) {
289 headers.push(("Content-Type".into(), "application/json".into()));
290 }
291 let request = PlmRequest { method, path: path.to_string(), headers, body: body.unwrap_or_default() };
292 self.transport.send(request).await.map_err(PlmError::Unreachable)
293 }
294
295 pub(crate) async fn call_typed(
299 &self,
300 method: &'static str,
301 path: &str,
302 body: Vec<u8>,
303 content_type: &str,
304 ) -> Result<PlmResponse, PlmError> {
305 let credential = self.credential.borrow().clone();
306 let mut headers = credential_headers(&credential, method);
307 headers.push(("Content-Type".into(), content_type.to_string()));
308 let request = PlmRequest { method, path: path.to_string(), headers, body };
309 let response = self.transport.send(request).await.map_err(PlmError::Unreachable)?;
310 if (200..300).contains(&response.status) {
311 Ok(response)
312 } else {
313 Err(PlmError::from_response(&response))
314 }
315 }
316
317 pub(crate) async fn call(&self, method: &'static str, path: &str, body: Option<Vec<u8>>) -> Result<PlmResponse, PlmError> {
319 let response = self.raw(method, path, body).await?;
320 if (200..300).contains(&response.status) {
321 Ok(response)
322 } else {
323 Err(PlmError::from_response(&response))
324 }
325 }
326
327 async fn call_json<T: for<'de> Deserialize<'de>>(
328 &self,
329 method: &'static str,
330 path: &str,
331 body: Option<Value>,
332 ) -> Result<T, PlmError> {
333 let response = self.call(method, path, body.map(|b| serde_json::to_vec(&b).unwrap_or_default())).await?;
334 parse(&response, path)
335 }
336
337 pub async fn sign_in_with_token(&self, token: &str) -> Result<Me, PlmError> {
342 let token = token.trim();
343 if token.is_empty() {
344 return Err(PlmError::SignIn("paste a token (it starts with plm_)".into()));
345 }
346 self.adopt(Credential::Token(token.to_string())).await
347 }
348
349 pub async fn sign_in_with_browser_session(&self) -> Result<Me, PlmError> {
352 self.adopt(Credential::Session { cookie: None, csrf: None }).await
353 }
354
355 pub async fn sign_in_with_password(&self, username: &str, password: &str) -> Result<Me, PlmError> {
357 let cookie = self.log_in(username, password).await?;
358 self.adopt(Credential::Session { cookie, csrf: None }).await
359 }
360
361 pub async fn sign_in_for_token(&self, username: &str, password: &str, token_name: &str) -> Result<(Me, String), PlmError> {
366 let cookie = self.log_in(username, password).await?;
367 let session = Credential::Session { cookie, csrf: None };
368 let me: Me = parse(&self.checked(&session, "GET", "/api/me", None).await?, "/api/me")?;
369 let session = Credential::Session { cookie: session_cookie(&session), csrf: me.csrf.clone() };
370 if let Err(refused) = check_version(&me) {
373 let _ = self.send_as(&session, "POST", "/api/logout", Some(b"{}".to_vec())).await;
374 return Err(refused);
375 }
376 let body = serde_json::to_vec(&json!({ "name": token_name, "scope": "full" })).unwrap_or_default();
377 let minted: Value = parse(&self.checked(&session, "POST", "/api/tokens", Some(body)).await?, "/api/tokens")?;
378 let token = minted
379 .get("token")
380 .and_then(Value::as_str)
381 .ok_or_else(|| PlmError::Malformed("/api/tokens answered without a token".into()))?
382 .to_string();
383 let _ = self.send_as(&session, "POST", "/api/logout", Some(b"{}".to_vec())).await;
385 let me = self.sign_in_with_token(&token).await?;
386 Ok((me, token))
387 }
388
389 async fn log_in(&self, username: &str, password: &str) -> Result<Option<String>, PlmError> {
393 let body = serde_json::to_vec(&json!({ "username": username, "password": password })).unwrap_or_default();
394 let response = self.checked(&Credential::Anonymous, "POST", "/api/login", Some(body)).await?;
395 Ok(response
396 .headers
397 .iter()
398 .find(|(k, _)| k.eq_ignore_ascii_case("set-cookie"))
399 .and_then(|(_, v)| v.split(';').next())
400 .map(|c| c.trim().to_string()))
401 }
402
403 async fn checked(&self, credential: &Credential, method: &'static str, path: &str, body: Option<Vec<u8>>) -> Result<PlmResponse, PlmError> {
405 let response = self.send_as(credential, method, path, body).await?;
406 if (200..300).contains(&response.status) {
407 Ok(response)
408 } else {
409 Err(PlmError::from_response(&response))
410 }
411 }
412
413 async fn adopt(&self, credential: Credential) -> Result<Me, PlmError> {
416 let me: Me = parse(&self.checked(&credential, "GET", "/api/me", None).await?, "/api/me")?;
417 check_version(&me)?;
418 let credential = match credential {
419 Credential::Session { cookie, .. } => Credential::Session { cookie, csrf: me.csrf.clone() },
420 other => other,
421 };
422 *self.credential.borrow_mut() = credential;
423 *self.me.borrow_mut() = Some(me.clone());
424 Ok(me)
425 }
426
427 pub async fn index(&self) -> Result<StoreIndex, PlmError> {
431 self.call_json("GET", "/api/store/index", None).await
432 }
433
434 pub async fn index_rows(&self, keys: &[String]) -> Result<StoreIndex, PlmError> {
437 self.call_json("GET", &format!("/api/store/index?keys={}", keys.iter().map(|k| crate::plm::identity::segment(k)).collect::<Vec<_>>().join(",")), None).await
438 }
439
440 pub async fn get_document(&self, key: &str) -> Result<Option<Vec<u8>>, PlmError> {
444 let response = self.call("GET", &doc_path(key), None).await?;
445 Ok(if response.status == 204 { None } else { Some(response.body) })
446 }
447
448 pub async fn put_document(&self, key: &str, bytes: Vec<u8>) -> Result<PutAck, PlmError> {
451 let path = doc_path(key);
452 let response = self.call("PUT", &path, Some(bytes)).await?;
453 parse(&response, &path)
454 }
455
456 pub async fn delete_document(&self, key: &str) -> Result<(), PlmError> {
458 self.call("DELETE", &doc_path(key), None).await.map(|_| ())
459 }
460
461 pub async fn get_preference(&self, name: &str) -> Result<Option<String>, PlmError> {
468 match self.call("GET", &preference_path(name), None).await {
469 Ok(response) if response.status == 204 => Ok(None),
470 Ok(response) => String::from_utf8(response.body)
471 .map(Some)
472 .map_err(|_| PlmError::Malformed(format!("preference {name} is not UTF-8"))),
473 Err(PlmError::NotFound(_)) => Ok(None),
474 Err(e) => Err(e),
475 }
476 }
477
478 pub async fn put_preference(&self, name: &str, value: String) -> Result<(), PlmError> {
480 self.call("PUT", &preference_path(name), Some(value.into_bytes())).await.map(|_| ())
481 }
482
483 pub async fn delete_preference(&self, name: &str) -> Result<(), PlmError> {
485 self.call("DELETE", &preference_path(name), None).await.map(|_| ())
486 }
487
488 pub async fn changes(&self, since: u64) -> Result<Changes, PlmError> {
490 self.call_json("GET", &format!("/api/store/changes?since={since}"), None).await
491 }
492}
493
494#[derive(Debug, Clone, PartialEq)]
496pub enum FeedUpdate {
497 Quiet,
499 Keys(Vec<String>),
501 Reindexed(StoreIndex),
504}
505
506#[derive(Debug, Clone, Default)]
510pub struct ChangeFeed {
511 since: Option<u64>,
512}
513
514impl ChangeFeed {
515 pub fn new() -> Self {
516 Self::default()
517 }
518
519 pub fn starting_at(seq: u64) -> Self {
521 Self { since: Some(seq) }
522 }
523
524 pub fn position(&self) -> Option<u64> {
525 self.since
526 }
527
528 pub async fn poll(&mut self, client: &PlmClient) -> Result<FeedUpdate, PlmError> {
529 let Some(since) = self.since else {
530 let index = client.index().await?;
531 self.since = Some(index.seq);
532 return Ok(FeedUpdate::Reindexed(index));
533 };
534 let changes = client.changes(since).await?;
535 if changes.stale {
536 let index = client.index().await?;
537 self.since = Some(index.seq);
538 return Ok(FeedUpdate::Reindexed(index));
539 }
540 self.since = Some(changes.seq.max(since));
541 let mut keys = Vec::new();
542 for key in changes.keys {
543 if !keys.contains(&key) {
544 keys.push(key);
545 }
546 }
547 Ok(if keys.is_empty() { FeedUpdate::Quiet } else { FeedUpdate::Keys(keys) })
548 }
549}
550
551pub fn check_version(me: &Me) -> Result<(), PlmError> {
558 check_version_of(me, CLIENT_VERSION)
559}
560
561fn check_version_of(me: &Me, client: &str) -> Result<(), PlmError> {
562 let server = me.server_version.as_deref().unwrap_or("a version it does not report");
563 let refuse = |min: &str, reason: String| PlmError::Version {
564 server: server.to_string(),
565 min_client: min.to_string(),
566 client: client.to_string(),
567 reason,
568 };
569 let Some(min) = me.min_client_version.as_deref() else {
570 return Err(refuse("?", "the server does not say which CAD versions it serves; it is older than this app".into()));
571 };
572 if !serves(min, client) {
573 return Err(refuse(min, format!("update this CAD app to {min} or later")));
574 }
575 Ok(())
576}
577
578pub fn parse_version(version: &str) -> Option<(u64, u64, u64)> {
581 let core = version.trim().split(['-', '+']).next()?;
582 let mut fields = core.split('.').map(|f| f.parse::<u64>().ok());
583 let triple = (fields.next()??, fields.next()??, fields.next()??);
584 fields.next().is_none().then_some(triple)
585}
586
587pub fn serves(min_client_version: &str, client_version: &str) -> bool {
590 match (parse_version(min_client_version), parse_version(client_version)) {
591 (Some(min), Some(client)) => client >= min,
592 _ => false,
593 }
594}
595
596fn preference_path(name: &str) -> String {
597 format!("/api/me/preferences/{name}")
598}
599
600fn doc_path(key: &str) -> String {
601 format!("/api/store/doc/{}", key.trim_start_matches('/').replace('%', "%25"))
602}
603
604fn session_cookie(credential: &Credential) -> Option<String> {
605 match credential {
606 Credential::Session { cookie, .. } => cookie.clone(),
607 _ => None,
608 }
609}
610
611fn parse<T: for<'de> Deserialize<'de>>(response: &PlmResponse, what: &str) -> Result<T, PlmError> {
612 serde_json::from_slice(&response.body).map_err(|e| PlmError::Malformed(format!("{what}: {e}")))
613}
614