1use crate::config::Config;
9use reqwest::Client;
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12
13pub fn build_client_from_config(config: &Config) -> KumihoClient {
20 let base_url = config.kumiho.api_url.clone();
21 let service_token = std::env::var("KUMIHO_SERVICE_TOKEN").unwrap_or_default();
22 KumihoClient::new(base_url, service_token)
23}
24
25pub fn slugify(name: &str) -> String {
27 name.trim()
28 .to_lowercase()
29 .chars()
30 .map(|c| {
31 if c.is_alphanumeric() || c == '-' {
32 c
33 } else {
34 '-'
35 }
36 })
37 .collect::<String>()
38 .split('-')
39 .filter(|s| !s.is_empty())
40 .collect::<Vec<_>>()
41 .join("-")
42}
43
44#[derive(Clone)]
46pub struct KumihoClient {
47 client: Client,
48 base_url: String,
49 service_token: String,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct ItemResponse {
56 pub kref: String,
57 pub name: String,
58 pub item_name: String,
59 pub kind: String,
60 #[serde(default)]
61 pub deprecated: bool,
62 pub created_at: Option<String>,
63 #[serde(default)]
64 pub metadata: HashMap<String, String>,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct RevisionResponse {
69 pub kref: String,
70 pub item_kref: String,
71 pub number: i32,
72 #[serde(default)]
73 pub latest: bool,
74 #[serde(default)]
75 pub tags: Vec<String>,
76 #[serde(default)]
77 pub metadata: HashMap<String, String>,
78 #[serde(default)]
79 pub deprecated: bool,
80 pub created_at: Option<String>,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct BatchRevisionsResponse {
85 pub revisions: Vec<RevisionResponse>,
86 pub not_found: Vec<String>,
87 pub requested_count: i32,
88 pub found_count: i32,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct SearchResult {
93 pub item: ItemResponse,
94 #[serde(default)]
95 pub score: f64,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct BundleMemberInfo {
102 pub item_kref: String,
103 pub added_at: Option<String>,
104 pub added_by: Option<String>,
105 pub added_in_revision: Option<i32>,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct BundleMembersResponse {
110 pub members: Vec<BundleMemberInfo>,
111 pub total_count: Option<i32>,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct ArtifactResponse {
118 pub kref: String,
119 pub name: String,
120 pub location: String,
121 pub revision_kref: String,
122 pub item_kref: Option<String>,
123 #[serde(default)]
124 pub deprecated: bool,
125 pub created_at: Option<String>,
126 #[serde(default)]
127 pub metadata: HashMap<String, String>,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct EdgeResponse {
134 pub source_kref: String,
135 pub target_kref: String,
136 pub edge_type: String,
137 pub created_at: Option<String>,
138 #[serde(default)]
139 pub metadata: Option<HashMap<String, String>>,
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct SpaceResponse {
146 pub path: String,
147 pub name: String,
148 pub parent_path: Option<String>,
149 pub created_at: Option<String>,
150}
151
152#[derive(Debug, thiserror::Error)]
155pub enum KumihoError {
156 #[error("Kumiho service unreachable: {0}")]
157 Unreachable(#[from] reqwest::Error),
158
159 #[error("Kumiho returned {status}: {body}")]
160 Api { status: u16, body: String },
161
162 #[error("Unexpected response: {0}")]
163 Decode(String),
164}
165
166pub type Result<T> = std::result::Result<T, KumihoError>;
167
168#[derive(Serialize)]
171struct CreateProjectBody {
172 name: String,
173 #[serde(skip_serializing_if = "Option::is_none")]
174 description: Option<String>,
175}
176
177#[derive(Serialize)]
178struct CreateSpaceBody {
179 parent_path: String,
180 name: String,
181}
182
183#[derive(Serialize)]
184struct CreateItemBody {
185 space_path: String,
186 item_name: String,
187 kind: String,
188 #[serde(skip_serializing_if = "HashMap::is_empty")]
189 metadata: HashMap<String, String>,
190}
191
192#[derive(Serialize)]
193struct CreateRevisionBody {
194 item_kref: String,
195 #[serde(skip_serializing_if = "HashMap::is_empty")]
196 metadata: HashMap<String, String>,
197}
198
199#[derive(Serialize)]
200struct CreateBundleBody {
201 space_path: String,
202 bundle_name: String,
203 #[serde(skip_serializing_if = "HashMap::is_empty")]
204 metadata: HashMap<String, String>,
205}
206
207#[derive(Serialize)]
208struct BundleMemberBody {
209 bundle_kref: String,
210 item_kref: String,
211 #[serde(skip_serializing_if = "Option::is_none")]
212 metadata: Option<HashMap<String, String>>,
213}
214
215#[derive(Serialize)]
216struct RemoveBundleMemberBody {
217 bundle_kref: String,
218 item_kref: String,
219}
220
221#[derive(Serialize)]
222struct CreateEdgeBody {
223 source_revision_kref: String,
224 target_revision_kref: String,
225 edge_type: String,
226 #[serde(skip_serializing_if = "HashMap::is_empty")]
227 metadata: HashMap<String, String>,
228}
229
230#[derive(Serialize)]
231struct CreateArtifactBody {
232 revision_kref: String,
233 name: String,
234 location: String,
235 #[serde(skip_serializing_if = "HashMap::is_empty")]
236 metadata: HashMap<String, String>,
237}
238
239impl KumihoClient {
240 pub fn new(base_url: String, service_token: String) -> Self {
244 let client = Client::builder()
245 .timeout(std::time::Duration::from_secs(20))
246 .connect_timeout(std::time::Duration::from_secs(5))
247 .pool_max_idle_per_host(32)
248 .build()
249 .unwrap_or_else(|_| Client::new());
250 Self {
251 client,
252 base_url: base_url.trim_end_matches('/').to_string(),
253 service_token,
254 }
255 }
256
257 pub fn client(&self) -> &Client {
259 &self.client
260 }
261
262 fn url(&self, path: &str) -> String {
265 format!("{}/api/v1{}", self.base_url, path)
266 }
267
268 async fn check_response(&self, resp: reqwest::Response) -> Result<reqwest::Response> {
269 let status = resp.status();
270 if status.is_success() {
271 Ok(resp)
272 } else {
273 let code = status.as_u16();
274 let body = resp.text().await.unwrap_or_default();
275 Err(KumihoError::Api { status: code, body })
276 }
277 }
278
279 pub async fn ensure_project(&self, project_name: &str) -> Result<()> {
283 let body = CreateProjectBody {
284 name: project_name.to_string(),
285 description: None,
286 };
287
288 let resp = self
289 .client
290 .post(self.url("/projects"))
291 .header("X-Kumiho-Token", &self.service_token)
292 .json(&body)
293 .send()
294 .await?;
295
296 let status = resp.status().as_u16();
297 if resp.status().is_success() || status == 409 {
298 Ok(())
299 } else {
300 let text = resp.text().await.unwrap_or_default();
301 Err(KumihoError::Api { status, body: text })
302 }
303 }
304
305 pub async fn ensure_space(&self, project: &str, space_name: &str) -> Result<()> {
309 let body = CreateSpaceBody {
310 parent_path: format!("/{project}"),
311 name: space_name.to_string(),
312 };
313
314 let resp = self
315 .client
316 .post(self.url("/spaces"))
317 .header("X-Kumiho-Token", &self.service_token)
318 .json(&body)
319 .send()
320 .await?;
321
322 let status = resp.status().as_u16();
323 if resp.status().is_success() || status == 409 {
325 Ok(())
326 } else {
327 let text = resp.text().await.unwrap_or_default();
328 Err(KumihoError::Api { status, body: text })
329 }
330 }
331
332 pub async fn ensure_child_space(
334 &self,
335 _project: &str,
336 parent_path: &str,
337 space_name: &str,
338 ) -> Result<()> {
339 let body = CreateSpaceBody {
340 parent_path: parent_path.to_string(),
341 name: space_name.to_string(),
342 };
343
344 let resp = self
345 .client
346 .post(self.url("/spaces"))
347 .header("X-Kumiho-Token", &self.service_token)
348 .json(&body)
349 .send()
350 .await?;
351
352 let status = resp.status().as_u16();
353 if resp.status().is_success() || status == 409 {
354 Ok(())
355 } else {
356 let text = resp.text().await.unwrap_or_default();
357 Err(KumihoError::Api { status, body: text })
358 }
359 }
360
361 pub async fn list_spaces(
363 &self,
364 parent_path: &str,
365 recursive: bool,
366 ) -> Result<Vec<SpaceResponse>> {
367 let resp = self
368 .client
369 .get(self.url("/spaces"))
370 .header("X-Kumiho-Token", &self.service_token)
371 .query(&[
372 ("parent_path", parent_path),
373 ("recursive", if recursive { "true" } else { "false" }),
374 ])
375 .send()
376 .await?;
377
378 let resp = self.check_response(resp).await?;
379 resp.json::<Vec<SpaceResponse>>()
380 .await
381 .map_err(|e| KumihoError::Decode(e.to_string()))
382 }
383
384 pub async fn list_items(
388 &self,
389 space_path: &str,
390 include_deprecated: bool,
391 ) -> Result<Vec<ItemResponse>> {
392 self.list_items_paged(space_path, include_deprecated, 100, 0)
393 .await
394 }
395
396 pub async fn list_items_paged(
398 &self,
399 space_path: &str,
400 include_deprecated: bool,
401 limit: u32,
402 offset: u32,
403 ) -> Result<Vec<ItemResponse>> {
404 let resp = self
405 .client
406 .get(self.url("/items"))
407 .header("X-Kumiho-Token", &self.service_token)
408 .query(&[
409 ("space_path", space_path),
410 (
411 "include_deprecated",
412 if include_deprecated { "true" } else { "false" },
413 ),
414 ("limit", &limit.to_string()),
415 ("offset", &offset.to_string()),
416 ])
417 .send()
418 .await?;
419
420 let resp = self.check_response(resp).await?;
421 resp.json::<Vec<ItemResponse>>()
422 .await
423 .map_err(|e| KumihoError::Decode(e.to_string()))
424 }
425
426 pub async fn list_items_filtered(
431 &self,
432 space_path: &str,
433 name_filter: &str,
434 include_deprecated: bool,
435 ) -> Result<Vec<ItemResponse>> {
436 let resp = self
437 .client
438 .get(self.url("/items"))
439 .header("X-Kumiho-Token", &self.service_token)
440 .query(&[
441 ("space_path", space_path),
442 ("name_filter", name_filter),
443 (
444 "include_deprecated",
445 if include_deprecated { "true" } else { "false" },
446 ),
447 ])
448 .send()
449 .await?;
450
451 let resp = self.check_response(resp).await?;
452 resp.json::<Vec<ItemResponse>>()
453 .await
454 .map_err(|e| KumihoError::Decode(e.to_string()))
455 }
456
457 pub async fn create_item(
459 &self,
460 space_path: &str,
461 item_name: &str,
462 kind: &str,
463 metadata: HashMap<String, String>,
464 ) -> Result<ItemResponse> {
465 let body = CreateItemBody {
466 space_path: space_path.to_string(),
467 item_name: item_name.to_string(),
468 kind: kind.to_string(),
469 metadata,
470 };
471
472 let resp = self
473 .client
474 .post(self.url("/items"))
475 .header("X-Kumiho-Token", &self.service_token)
476 .json(&body)
477 .send()
478 .await?;
479
480 let resp = self.check_response(resp).await?;
481 resp.json::<ItemResponse>()
482 .await
483 .map_err(|e| KumihoError::Decode(e.to_string()))
484 }
485
486 pub async fn deprecate_item(&self, kref: &str, deprecated: bool) -> Result<ItemResponse> {
488 let resp = self
489 .client
490 .post(self.url("/items/deprecate"))
491 .header("X-Kumiho-Token", &self.service_token)
492 .query(&[
493 ("kref", kref),
494 ("deprecated", if deprecated { "true" } else { "false" }),
495 ])
496 .send()
497 .await?;
498
499 let resp = self.check_response(resp).await?;
500 resp.json::<ItemResponse>()
501 .await
502 .map_err(|e| KumihoError::Decode(e.to_string()))
503 }
504
505 pub async fn delete_item(&self, kref: &str) -> Result<()> {
507 let resp = self
508 .client
509 .delete(self.url("/items/by-kref"))
510 .header("X-Kumiho-Token", &self.service_token)
511 .query(&[("kref", kref), ("force", "true")])
512 .send()
513 .await?;
514
515 let _ = self.check_response(resp).await?;
516 Ok(())
517 }
518
519 pub async fn search_items(
521 &self,
522 query: &str,
523 context: &str,
524 kind: &str,
525 include_deprecated: bool,
526 ) -> Result<Vec<SearchResult>> {
527 let resp = self
528 .client
529 .get(self.url("/items/fulltext-search"))
530 .header("X-Kumiho-Token", &self.service_token)
531 .query(&[
532 ("query", query),
533 ("context", context),
534 ("kind", kind),
535 (
536 "include_deprecated",
537 if include_deprecated { "true" } else { "false" },
538 ),
539 ])
540 .send()
541 .await?;
542
543 let resp = self.check_response(resp).await?;
544 resp.json::<Vec<SearchResult>>()
545 .await
546 .map_err(|e| KumihoError::Decode(e.to_string()))
547 }
548
549 pub async fn create_revision(
553 &self,
554 item_kref: &str,
555 metadata: HashMap<String, String>,
556 ) -> Result<RevisionResponse> {
557 let body = CreateRevisionBody {
558 item_kref: item_kref.to_string(),
559 metadata,
560 };
561
562 let resp = self
563 .client
564 .post(self.url("/revisions"))
565 .header("X-Kumiho-Token", &self.service_token)
566 .json(&body)
567 .send()
568 .await?;
569
570 let resp = self.check_response(resp).await?;
571 resp.json::<RevisionResponse>()
572 .await
573 .map_err(|e| KumihoError::Decode(e.to_string()))
574 }
575
576 pub async fn tag_revision(&self, revision_kref: &str, tag: &str) -> Result<()> {
578 let resp = self
579 .client
580 .post(self.url("/revisions/tags"))
581 .header("X-Kumiho-Token", &self.service_token)
582 .query(&[("kref", revision_kref)])
583 .json(&serde_json::json!({ "tag": tag }))
584 .send()
585 .await?;
586
587 let _ = self.check_response(resp).await?;
588 Ok(())
589 }
590
591 pub async fn get_revision_by_tag(
593 &self,
594 item_kref: &str,
595 tag: &str,
596 ) -> Result<RevisionResponse> {
597 let resp = self
598 .client
599 .get(self.url("/revisions/by-kref"))
600 .header("X-Kumiho-Token", &self.service_token)
601 .query(&[("kref", item_kref), ("t", tag)])
602 .send()
603 .await?;
604
605 let resp = self.check_response(resp).await?;
606 resp.json::<RevisionResponse>()
607 .await
608 .map_err(|e| KumihoError::Decode(e.to_string()))
609 }
610
611 pub async fn get_revision(&self, revision_kref: &str) -> Result<RevisionResponse> {
615 let resp = self
616 .client
617 .get(self.url("/revisions/by-kref"))
618 .header("X-Kumiho-Token", &self.service_token)
619 .query(&[("kref", revision_kref)])
620 .send()
621 .await?;
622
623 let resp = self.check_response(resp).await?;
624 resp.json::<RevisionResponse>()
625 .await
626 .map_err(|e| KumihoError::Decode(e.to_string()))
627 }
628
629 pub async fn get_latest_revision(&self, item_kref: &str) -> Result<RevisionResponse> {
631 let resp = self
632 .client
633 .get(self.url("/revisions/latest"))
634 .header("X-Kumiho-Token", &self.service_token)
635 .query(&[("item_kref", item_kref)])
636 .send()
637 .await?;
638
639 let resp = self.check_response(resp).await?;
640 resp.json::<RevisionResponse>()
641 .await
642 .map_err(|e| KumihoError::Decode(e.to_string()))
643 }
644
645 pub async fn get_published_or_latest(&self, item_kref: &str) -> Result<RevisionResponse> {
647 match self.get_revision_by_tag(item_kref, "published").await {
648 Ok(rev) => Ok(rev),
649 Err(_) => self.get_latest_revision(item_kref).await,
650 }
651 }
652
653 pub async fn batch_get_revisions(
657 &self,
658 item_krefs: &[String],
659 tag: &str,
660 ) -> Result<HashMap<String, RevisionResponse>> {
661 if item_krefs.is_empty() {
662 return Ok(HashMap::new());
663 }
664
665 let body = serde_json::json!({
666 "item_krefs": item_krefs,
667 "tag": tag,
668 "allow_partial": true,
669 });
670
671 let resp = self
672 .client
673 .post(self.url("/revisions/batch"))
674 .header("X-Kumiho-Token", &self.service_token)
675 .json(&body)
676 .send()
677 .await?;
678
679 let resp = self.check_response(resp).await?;
680 let batch: BatchRevisionsResponse = resp
681 .json()
682 .await
683 .map_err(|e| KumihoError::Decode(e.to_string()))?;
684
685 let mut map = HashMap::with_capacity(batch.revisions.len());
686 for rev in batch.revisions {
687 map.insert(rev.item_kref.clone(), rev);
688 }
689 Ok(map)
690 }
691
692 pub async fn list_skills(
696 &self,
697 project: &str,
698 include_deprecated: bool,
699 ) -> Result<Vec<ItemResponse>> {
700 let space_path = format!("/{project}/Skills");
701 self.list_items(&space_path, include_deprecated).await
702 }
703
704 pub async fn search_skills(
706 &self,
707 query: &str,
708 project: &str,
709 include_deprecated: bool,
710 ) -> Result<Vec<SearchResult>> {
711 self.search_items(query, project, "skill", include_deprecated)
712 .await
713 }
714
715 pub async fn create_skill(
717 &self,
718 project: &str,
719 name: &str,
720 metadata: HashMap<String, String>,
721 ) -> Result<(ItemResponse, RevisionResponse)> {
722 self.ensure_space(project, "Skills").await.ok();
723 let space_path = format!("/{project}/Skills");
724 let item = self
725 .create_item(&space_path, name, "skill", HashMap::new())
726 .await?;
727 let revision = self.create_revision(&item.kref, metadata).await?;
728 Ok((item, revision))
729 }
730
731 pub async fn deprecate_skill(&self, kref: &str, deprecated: bool) -> Result<ItemResponse> {
733 self.deprecate_item(kref, deprecated).await
734 }
735
736 pub async fn create_bundle(
740 &self,
741 space_path: &str,
742 bundle_name: &str,
743 metadata: HashMap<String, String>,
744 ) -> Result<ItemResponse> {
745 let body = CreateBundleBody {
746 space_path: space_path.to_string(),
747 bundle_name: bundle_name.to_string(),
748 metadata,
749 };
750
751 let resp = self
752 .client
753 .post(self.url("/bundles"))
754 .header("X-Kumiho-Token", &self.service_token)
755 .json(&body)
756 .send()
757 .await?;
758
759 let resp = self.check_response(resp).await?;
760 resp.json::<ItemResponse>()
761 .await
762 .map_err(|e| KumihoError::Decode(e.to_string()))
763 }
764
765 pub async fn get_bundle(&self, kref: &str) -> Result<ItemResponse> {
767 let resp = self
768 .client
769 .get(self.url("/bundles/by-kref"))
770 .header("X-Kumiho-Token", &self.service_token)
771 .query(&[("kref", kref)])
772 .send()
773 .await?;
774
775 let resp = self.check_response(resp).await?;
776 resp.json::<ItemResponse>()
777 .await
778 .map_err(|e| KumihoError::Decode(e.to_string()))
779 }
780
781 pub async fn delete_bundle(&self, kref: &str) -> Result<()> {
783 let resp = self
784 .client
785 .delete(self.url("/bundles/by-kref"))
786 .header("X-Kumiho-Token", &self.service_token)
787 .query(&[("kref", kref), ("force", "true")])
788 .send()
789 .await?;
790
791 let _ = self.check_response(resp).await?;
792 Ok(())
793 }
794
795 pub async fn add_bundle_member(
797 &self,
798 bundle_kref: &str,
799 item_kref: &str,
800 metadata: HashMap<String, String>,
801 ) -> Result<serde_json::Value> {
802 let body = BundleMemberBody {
803 bundle_kref: bundle_kref.to_string(),
804 item_kref: item_kref.to_string(),
805 metadata: if metadata.is_empty() {
806 None
807 } else {
808 Some(metadata)
809 },
810 };
811
812 let resp = self
813 .client
814 .post(self.url("/bundles/members/add"))
815 .header("X-Kumiho-Token", &self.service_token)
816 .json(&body)
817 .send()
818 .await?;
819
820 let resp = self.check_response(resp).await?;
821 resp.json::<serde_json::Value>()
822 .await
823 .map_err(|e| KumihoError::Decode(e.to_string()))
824 }
825
826 pub async fn remove_bundle_member(
828 &self,
829 bundle_kref: &str,
830 item_kref: &str,
831 ) -> Result<serde_json::Value> {
832 let body = RemoveBundleMemberBody {
833 bundle_kref: bundle_kref.to_string(),
834 item_kref: item_kref.to_string(),
835 };
836
837 let resp = self
838 .client
839 .post(self.url("/bundles/members/remove"))
840 .header("X-Kumiho-Token", &self.service_token)
841 .json(&body)
842 .send()
843 .await?;
844
845 let resp = self.check_response(resp).await?;
846 resp.json::<serde_json::Value>()
847 .await
848 .map_err(|e| KumihoError::Decode(e.to_string()))
849 }
850
851 pub async fn list_bundle_members(&self, bundle_kref: &str) -> Result<BundleMembersResponse> {
853 let resp = self
854 .client
855 .get(self.url("/bundles/members"))
856 .header("X-Kumiho-Token", &self.service_token)
857 .query(&[("bundle_kref", bundle_kref)])
858 .send()
859 .await?;
860
861 let resp = self.check_response(resp).await?;
862 resp.json::<BundleMembersResponse>()
863 .await
864 .map_err(|e| KumihoError::Decode(e.to_string()))
865 }
866
867 pub async fn create_edge(
871 &self,
872 source_kref: &str,
873 target_kref: &str,
874 edge_type: &str,
875 metadata: HashMap<String, String>,
876 ) -> Result<EdgeResponse> {
877 let body = CreateEdgeBody {
878 source_revision_kref: source_kref.to_string(),
879 target_revision_kref: target_kref.to_string(),
880 edge_type: edge_type.to_string(),
881 metadata,
882 };
883
884 let resp = self
885 .client
886 .post(self.url("/edges"))
887 .header("X-Kumiho-Token", &self.service_token)
888 .json(&body)
889 .send()
890 .await?;
891
892 let resp = self.check_response(resp).await?;
893 resp.json::<EdgeResponse>()
894 .await
895 .map_err(|e| KumihoError::Decode(e.to_string()))
896 }
897
898 pub async fn list_edges(
902 &self,
903 revision_kref: &str,
904 edge_type: Option<&str>,
905 direction: Option<&str>,
906 ) -> Result<Vec<EdgeResponse>> {
907 let dir_num = direction.map(|d| match d {
909 "outgoing" | "out" => "0",
910 "incoming" | "in" => "1",
911 "both" => "2",
912 other => other, });
914
915 let mut query_params: Vec<(&str, &str)> = vec![("kref", revision_kref)];
916 if let Some(et) = edge_type {
917 query_params.push(("edge_type", et));
918 }
919 if let Some(dir) = dir_num.as_deref() {
920 query_params.push(("direction", dir));
921 }
922
923 let resp = self
924 .client
925 .get(self.url("/edges"))
926 .header("X-Kumiho-Token", &self.service_token)
927 .query(&query_params)
928 .send()
929 .await?;
930
931 let resp = self.check_response(resp).await?;
932 resp.json::<Vec<EdgeResponse>>()
933 .await
934 .map_err(|e| KumihoError::Decode(e.to_string()))
935 }
936
937 pub async fn delete_edge(
939 &self,
940 source_kref: &str,
941 target_kref: &str,
942 edge_type: &str,
943 ) -> Result<()> {
944 let resp = self
945 .client
946 .delete(self.url("/edges"))
947 .header("X-Kumiho-Token", &self.service_token)
948 .query(&[
949 ("source_kref", source_kref),
950 ("target_kref", target_kref),
951 ("edge_type", edge_type),
952 ])
953 .send()
954 .await?;
955
956 let _ = self.check_response(resp).await?;
957 Ok(())
958 }
959
960 pub async fn create_artifact(
964 &self,
965 revision_kref: &str,
966 name: &str,
967 location: &str,
968 metadata: HashMap<String, String>,
969 ) -> Result<ArtifactResponse> {
970 let body = CreateArtifactBody {
971 revision_kref: revision_kref.to_string(),
972 name: name.to_string(),
973 location: location.to_string(),
974 metadata,
975 };
976
977 let resp = self
978 .client
979 .post(self.url("/artifacts"))
980 .header("X-Kumiho-Token", &self.service_token)
981 .json(&body)
982 .send()
983 .await?;
984
985 let resp = self.check_response(resp).await?;
986 resp.json::<ArtifactResponse>()
987 .await
988 .map_err(|e| KumihoError::Decode(e.to_string()))
989 }
990
991 pub async fn get_artifacts(&self, revision_kref: &str) -> Result<Vec<ArtifactResponse>> {
993 let resp = self
994 .client
995 .get(self.url("/artifacts"))
996 .header("X-Kumiho-Token", &self.service_token)
997 .query(&[("revision_kref", revision_kref)])
998 .send()
999 .await?;
1000
1001 let resp = self.check_response(resp).await?;
1002 resp.json::<Vec<ArtifactResponse>>()
1003 .await
1004 .map_err(|e| KumihoError::Decode(e.to_string()))
1005 }
1006
1007 pub async fn get_artifact_by_name(
1009 &self,
1010 revision_kref: &str,
1011 name: &str,
1012 ) -> Result<ArtifactResponse> {
1013 let resp = self
1014 .client
1015 .get(self.url("/artifacts/by-kref"))
1016 .header("X-Kumiho-Token", &self.service_token)
1017 .query(&[("revision_kref", revision_kref), ("name", name)])
1018 .send()
1019 .await?;
1020
1021 let resp = self.check_response(resp).await?;
1022 resp.json::<ArtifactResponse>()
1023 .await
1024 .map_err(|e| KumihoError::Decode(e.to_string()))
1025 }
1026
1027 pub async fn list_teams_in(
1031 &self,
1032 space_path: &str,
1033 include_deprecated: bool,
1034 ) -> Result<Vec<ItemResponse>> {
1035 self.list_items(space_path, include_deprecated).await
1036 }
1037
1038 pub async fn deprecate_team(&self, kref: &str, deprecated: bool) -> Result<()> {
1040 self.deprecate_item(kref, deprecated).await?;
1041 Ok(())
1042 }
1043}