1use crate::http::query_component;
30use crate::plm::client::PlmClient;
31use crate::plm::content_hash;
32use serde_json::Value;
33
34#[derive(Clone, Debug, Default, PartialEq, serde::Deserialize)]
37pub struct ImportedRow {
38 pub id: String,
39 #[serde(default)]
40 pub number: String,
41 #[serde(default)]
42 pub latest_revision_id: String,
43 #[serde(default)]
46 pub latest_origin: Option<String>,
47 #[serde(default)]
49 pub latest_content_hash: String,
50 #[serde(default)]
52 pub latest_state: String,
53}
54
55#[derive(Clone, Debug, PartialEq, Eq)]
57pub enum Action {
58 Mint,
60 Finish { part: String, revision: String },
63 Revise { part: String },
65 Skip { part: String, reason: String },
67}
68
69pub fn decide(existing: Option<&ImportedRow>, hash: &str) -> Action {
72 let Some(row) = existing else {
73 return Action::Mint;
74 };
75 let skip = |reason: String| Action::Skip { part: row.id.clone(), reason };
76 match row.latest_origin.as_deref() {
77 Some("imported") => {}
78 Some(origin) => {
79 return skip(format!(
80 "{} was revised by hand (its latest revision is {origin}) — the library is not written over it",
81 row.number
82 ))
83 }
84 None => {
85 return Action::Finish { part: row.id.clone(), revision: row.latest_revision_id.clone() };
86 }
87 }
88 if row.latest_state == "draft" {
89 return Action::Finish { part: row.id.clone(), revision: row.latest_revision_id.clone() };
90 }
91 if row.latest_content_hash == hash {
92 return skip(format!("{} is unchanged", row.number));
93 }
94 Action::Revise { part: row.id.clone() }
95}
96
97#[derive(Clone, Debug, PartialEq, Eq)]
99pub enum Outcome {
100 Written { part: String, revision: String, created_part: bool },
102 Skipped { part: String, reason: String },
103}
104
105fn body(value: Value) -> Option<Vec<u8>> {
106 Some(serde_json::to_vec(&value).unwrap_or_default())
107}
108
109async fn call(client: &PlmClient, method: &'static str, path: &str, payload: Option<Vec<u8>>) -> Result<Value, String> {
110 let response = client.call(method, path, payload).await.map_err(|error| error.to_string())?;
111 Ok(serde_json::from_slice(&response.body).unwrap_or(Value::Null))
112}
113
114pub async fn find(client: &PlmClient, part_type: &str, library_id: &str) -> Result<Option<ImportedRow>, String> {
116 let page = call(
117 client,
118 "GET",
119 &format!("/api/parts?limit=2&part_type={}&external_ref={}", query_component(part_type), query_component(library_id)),
120 None,
121 )
122 .await?;
123 let rows = page["parts"].as_array().cloned().unwrap_or_default();
124 Ok(rows.into_iter().next().and_then(|row| serde_json::from_value(row).ok()))
125}
126
127async fn write_and_release(client: &PlmClient, part: &str, revision: &str, document: &[u8]) -> Result<(), String> {
130 let base = crate::plm::identity::revision_path(&part, &revision);
131 call(client, "POST", &format!("{base}/checkout"), body(serde_json::json!({ "client_id": "brep-app kicad" }))).await?;
132 let written = call(client, "PUT", &super::identity::doc_path(&super::identity::document_key(part, revision)), Some(document.to_vec())).await;
133 call(client, "POST", &format!("{base}/checkin"), body(serde_json::json!({}))).await?;
134 written?;
135 call(client, "POST", &format!("{base}/state"), body(serde_json::json!({ "to": "released" }))).await?;
136 Ok(())
137}
138
139pub async fn publish_part(
144 client: &PlmClient,
145 part_type: &str,
146 library_id: &str,
147 name: &str,
148 document: &[u8],
149) -> Result<Outcome, String> {
150 let hash = content_hash(document);
151 for attempt in 0..2 {
152 let existing = find(client, part_type, library_id).await?;
153 match decide(existing.as_ref(), &hash) {
154 Action::Skip { part, reason } => return Ok(Outcome::Skipped { part, reason }),
155 Action::Finish { part, revision } => {
156 write_and_release(client, &part, &revision, document).await?;
157 return Ok(Outcome::Written { part, revision, created_part: false });
158 }
159 Action::Revise { part } => {
160 let revision = call(
161 client,
162 "POST",
163 &format!("{}/revisions", crate::plm::identity::part_path(&part)),
164 body(serde_json::json!({ "origin": "imported" })),
165 )
166 .await?;
167 let revision = revision["id"].as_str().unwrap_or_default().to_string();
168 write_and_release(client, &part, &revision, document).await?;
169 return Ok(Outcome::Written { part, revision, created_part: false });
170 }
171 Action::Mint => {
172 let request = serde_json::json!({
173 "part_type": part_type,
174 "name": name,
175 "external_ref": library_id,
176 "origin": "imported",
177 "bulk": true,
180 });
181 match client.call("POST", "/api/parts", body(request)).await {
182 Ok(response) => {
183 let created: Value = serde_json::from_slice(&response.body).unwrap_or(Value::Null);
184 let part = created["id"].as_str().unwrap_or_default().to_string();
185 let revision = created["revisions"][0]["id"].as_str().unwrap_or_default().to_string();
186 write_and_release(client, &part, &revision, document).await?;
187 return Ok(Outcome::Written { part, revision, created_part: true });
188 }
189 Err(error) if error.status() == Some(409) && attempt == 0 => continue,
191 Err(error) => return Err(error.to_string()),
192 }
193 }
194 }
195 }
196 Err(format!("'{library_id}' could not be minted or found"))
197}
198
199#[derive(Clone, Debug, Default, PartialEq, Eq)]
201pub struct LibraryReport {
202 pub written: usize,
203 pub created: usize,
204 pub skipped: usize,
205 pub failed: Vec<(String, String)>,
209}
210
211pub async fn publish_library(
220 client: &PlmClient,
221 store: &dyn crate::store::ModelStore,
222 part_type: &str,
223 parts: &[(String, String)],
224 progress: &mut dyn FnMut(&LibraryReport) -> bool,
225) -> LibraryReport {
226 let mut report = LibraryReport::default();
227 for (library_id, document) in parts {
228 let outcome = match store.read(document) {
229 None => Err(format!("its document '{document}' is not in the store")),
230 Some(bytes) => {
231 let name = library_id.rsplit(':').next().unwrap_or(library_id);
232 publish_part(client, part_type, library_id, name, bytes.as_bytes()).await
233 }
234 };
235 match outcome {
236 Ok(Outcome::Written { created_part, .. }) => {
237 report.written += 1;
238 report.created += usize::from(created_part);
239 }
240 Ok(Outcome::Skipped { .. }) => report.skipped += 1,
241 Err(sentence) => report.failed.push((library_id.clone(), sentence)),
242 }
243 if !progress(&report) {
244 break;
245 }
246 }
247 report
248}
249