1use crate::{AnnotationSet, Client, Dataset, Error, Progress, Sample, client};
5use chrono::{DateTime, Utc};
6use log::trace;
7use reqwest::multipart::{Form, Part};
8use serde::{Deserialize, Deserializer, Serialize};
9use std::{collections::HashMap, fmt::Display, path::PathBuf, str::FromStr};
10
11#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
41#[serde(untagged)]
42pub enum Parameter {
43 Integer(i64),
45 Real(f64),
47 Boolean(bool),
49 String(String),
51 Array(Vec<Parameter>),
53 Object(HashMap<String, Parameter>),
55}
56
57#[derive(Deserialize)]
58pub struct LoginResult {
59 pub(crate) token: String,
60}
61
62macro_rules! typeid {
71 ($(#[$meta:meta])* $name:ident, $prefix:literal) => {
72 $(#[$meta])*
73 #[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
74 pub struct $name(u64);
75
76 impl Display for $name {
77 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
78 write!(f, concat!($prefix, "-{:x}"), self.0)
79 }
80 }
81
82 impl From<u64> for $name {
83 fn from(id: u64) -> Self {
84 $name(id)
85 }
86 }
87
88 impl From<$name> for u64 {
89 fn from(val: $name) -> Self {
90 val.0
91 }
92 }
93
94 impl $name {
95 pub fn value(&self) -> u64 {
97 self.0
98 }
99 }
100
101 impl TryFrom<&str> for $name {
102 type Error = Error;
103
104 fn try_from(s: &str) -> Result<Self, Self::Error> {
105 $name::from_str(s)
106 }
107 }
108
109 impl TryFrom<String> for $name {
110 type Error = Error;
111
112 fn try_from(s: String) -> Result<Self, Self::Error> {
113 $name::from_str(&s)
114 }
115 }
116
117 impl FromStr for $name {
118 type Err = Error;
119
120 fn from_str(s: &str) -> Result<Self, Self::Err> {
121 let hex_part =
122 s.strip_prefix(concat!($prefix, "-")).ok_or_else(|| {
123 Error::InvalidParameters(format!(
124 "{} must start with '{}-' prefix",
125 stringify!($name),
126 $prefix
127 ))
128 })?;
129 let id = u64::from_str_radix(hex_part, 16)?;
130 Ok($name(id))
131 }
132 }
133 };
134}
135
136typeid!(
137 OrganizationID,
157 "org"
158);
159
160#[derive(Deserialize, Clone, Debug)]
181pub struct Organization {
182 id: OrganizationID,
183 name: String,
184 #[serde(rename = "latest_credit")]
185 credits: i64,
186}
187
188impl Display for Organization {
189 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
190 write!(f, "{}", self.name())
191 }
192}
193
194impl Organization {
195 pub fn id(&self) -> OrganizationID {
196 self.id
197 }
198
199 pub fn name(&self) -> &str {
200 &self.name
201 }
202
203 pub fn credits(&self) -> i64 {
204 self.credits
205 }
206}
207
208#[derive(Deserialize, Clone, Debug)]
214pub struct UsageSummary {
215 #[serde(default)]
216 credits: f64,
217 #[serde(default)]
218 funds: f64,
219 #[serde(default, rename = "total_funds_and_credits")]
220 total: f64,
221}
222
223impl UsageSummary {
224 pub fn credits(&self) -> f64 {
225 self.credits
226 }
227
228 pub fn funds(&self) -> f64 {
229 self.funds
230 }
231
232 pub fn total(&self) -> f64 {
233 self.total
234 }
235}
236
237typeid!(
238 ProjectID,
259 "p"
260);
261
262typeid!(
263 ExperimentID,
284 "exp"
285);
286
287typeid!(
288 TrainingSessionID,
309 "t"
310);
311
312typeid!(
313 ValidationSessionID,
333 "v"
334);
335
336typeid!(
337 SnapshotID,
353 "ss"
354);
355
356typeid!(
357 TaskID,
373 "task"
374);
375
376typeid!(
377 DatasetID,
398 "ds"
399);
400
401typeid!(
402 AnnotationSetID,
418 "as"
419);
420
421typeid!(
422 SampleID,
438 "s"
439);
440
441typeid!(
442 AppId,
448 "app"
449);
450
451typeid!(
452 ImageId,
458 "im"
459);
460
461typeid!(
462 SequenceId,
468 "se"
469);
470
471#[derive(Deserialize, Clone, Debug)]
475pub struct Project {
476 id: ProjectID,
477 name: String,
478 description: String,
479}
480
481impl Display for Project {
482 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
483 write!(f, "{} {}", self.id(), self.name())
484 }
485}
486
487impl Project {
488 pub fn id(&self) -> ProjectID {
489 self.id
490 }
491
492 pub fn name(&self) -> &str {
493 &self.name
494 }
495
496 pub fn description(&self) -> &str {
497 &self.description
498 }
499
500 pub async fn datasets(
501 &self,
502 client: &client::Client,
503 name: Option<&str>,
504 ) -> Result<Vec<Dataset>, Error> {
505 client.datasets(self.id, name).await
506 }
507
508 pub async fn experiments(
509 &self,
510 client: &client::Client,
511 name: Option<&str>,
512 ) -> Result<Vec<Experiment>, Error> {
513 client.experiments(self.id, name).await
514 }
515}
516
517#[derive(Deserialize, Debug)]
518pub struct SamplesCountResult {
519 pub total: u64,
520}
521
522#[derive(Serialize, Clone, Debug)]
523pub struct SamplesListParams {
524 pub dataset_id: DatasetID,
525 #[serde(skip_serializing_if = "Option::is_none")]
526 pub annotation_set_id: Option<AnnotationSetID>,
527 #[serde(skip_serializing_if = "Option::is_none")]
528 pub continue_token: Option<String>,
529 #[serde(skip_serializing_if = "Vec::is_empty")]
530 pub types: Vec<String>,
531 #[serde(skip_serializing_if = "Vec::is_empty")]
532 pub group_names: Vec<String>,
533}
534
535#[derive(Deserialize, Debug)]
536pub struct SamplesListResult {
537 pub samples: Vec<Sample>,
538 pub continue_token: Option<String>,
539}
540
541#[derive(Serialize, Clone, Debug)]
543pub struct SampleDimensionUpdate {
544 pub id: SampleID,
545 pub width: u32,
546 pub height: u32,
547}
548
549#[derive(Serialize, Clone, Debug)]
551pub struct SamplesUpdateDimensionsParams {
552 pub dataset_id: DatasetID,
553 pub samples: Vec<SampleDimensionUpdate>,
554}
555
556#[derive(Deserialize, Debug)]
558pub struct SamplesUpdateDimensionsResult {
559 pub updated: u64,
560}
561
562#[derive(Serialize, Clone, Debug)]
567pub struct SamplesPopulateParams {
568 pub dataset_id: DatasetID,
569 #[serde(skip_serializing_if = "Option::is_none")]
570 pub annotation_set_id: Option<AnnotationSetID>,
571 #[serde(skip_serializing_if = "Option::is_none")]
572 pub presigned_urls: Option<bool>,
573 pub samples: Vec<Sample>,
574}
575
576#[derive(Deserialize, Debug, Clone)]
582pub struct SamplesPopulateResult {
583 pub uuid: String,
585 pub urls: Vec<PresignedUrl>,
587}
588
589#[derive(Deserialize, Debug, Clone)]
591pub struct PresignedUrl {
592 pub filename: String,
594 pub key: String,
596 pub url: String,
598}
599
600#[derive(Serialize, Clone, Debug)]
613pub struct ServerAnnotation {
614 #[serde(skip_serializing_if = "Option::is_none")]
616 pub label_id: Option<u64>,
617 #[serde(skip_serializing_if = "Option::is_none")]
619 pub label_index: Option<u64>,
620 #[serde(skip_serializing_if = "Option::is_none")]
622 pub label_name: Option<String>,
623 #[serde(rename = "type")]
625 pub annotation_type: String,
626 pub x: f64,
628 pub y: f64,
630 pub w: f64,
632 pub h: f64,
634 pub score: f64,
636 #[serde(skip_serializing_if = "String::is_empty")]
638 pub polygon: String,
639 pub image_id: u64,
641 pub annotation_set_id: u64,
643 #[serde(skip_serializing_if = "Option::is_none")]
645 pub object_reference: Option<String>,
646}
647
648#[derive(Serialize, Debug)]
650pub struct AnnotationAddBulkParams {
651 pub annotation_set_id: u64,
652 pub annotations: Vec<ServerAnnotation>,
653}
654
655#[derive(Serialize, Debug)]
657pub struct AnnotationBulkDeleteParams {
658 pub annotation_set_id: u64,
659 pub annotation_types: Vec<String>,
660 #[serde(skip_serializing_if = "Vec::is_empty")]
662 pub image_ids: Vec<u64>,
663 #[serde(skip_serializing_if = "Option::is_none")]
665 pub delete_all: Option<bool>,
666}
667
668#[derive(Deserialize)]
669pub struct Snapshot {
670 id: SnapshotID,
671 description: String,
672 status: String,
673 path: String,
674 #[serde(rename = "date")]
675 created: DateTime<Utc>,
676}
677
678impl Display for Snapshot {
679 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
680 write!(f, "{} {}", self.id, self.description)
681 }
682}
683
684impl Snapshot {
685 pub fn id(&self) -> SnapshotID {
686 self.id
687 }
688
689 pub fn description(&self) -> &str {
690 &self.description
691 }
692
693 pub fn status(&self) -> &str {
694 &self.status
695 }
696
697 pub fn path(&self) -> &str {
698 &self.path
699 }
700
701 pub fn created(&self) -> &DateTime<Utc> {
702 &self.created
703 }
704}
705
706#[derive(Serialize, Debug)]
707pub struct SnapshotRestore {
708 pub project_id: ProjectID,
709 pub snapshot_id: SnapshotID,
710 pub fps: u64,
711 #[serde(rename = "enabled_topics", skip_serializing_if = "Vec::is_empty")]
712 pub topics: Vec<String>,
713 #[serde(rename = "label_names", skip_serializing_if = "Vec::is_empty")]
714 pub autolabel: Vec<String>,
715 #[serde(rename = "depth_gen")]
716 pub autodepth: bool,
717 pub agtg_pipeline: bool,
718 #[serde(skip_serializing_if = "Option::is_none")]
719 pub dataset_name: Option<String>,
720 #[serde(skip_serializing_if = "Option::is_none")]
721 pub dataset_description: Option<String>,
722}
723
724#[derive(Deserialize, Debug)]
725pub struct SnapshotRestoreResult {
726 pub id: SnapshotID,
727 pub description: String,
728 pub dataset_name: String,
729 pub dataset_id: DatasetID,
730 pub annotation_set_id: AnnotationSetID,
731 #[serde(default)]
732 pub task_id: Option<TaskID>,
733 #[serde(default)]
737 pub date: Option<DateTime<Utc>>,
738}
739
740#[derive(Serialize, Debug)]
745pub struct SnapshotCreateFromDataset {
746 pub description: String,
748 pub dataset_id: DatasetID,
750 pub annotation_set_id: AnnotationSetID,
752}
753
754#[derive(Deserialize, Debug)]
758pub struct SnapshotFromDatasetResult {
759 #[serde(alias = "snapshot_id")]
761 pub id: SnapshotID,
762 #[serde(default)]
764 pub task_id: Option<TaskID>,
765}
766
767#[derive(Deserialize)]
768pub struct Experiment {
769 id: ExperimentID,
770 project_id: ProjectID,
771 name: String,
772 description: String,
773}
774
775impl Display for Experiment {
776 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
777 write!(f, "{} {}", self.id, self.name)
778 }
779}
780
781impl Experiment {
782 pub fn id(&self) -> ExperimentID {
783 self.id
784 }
785
786 pub fn project_id(&self) -> ProjectID {
787 self.project_id
788 }
789
790 pub fn name(&self) -> &str {
791 &self.name
792 }
793
794 pub fn description(&self) -> &str {
795 &self.description
796 }
797
798 pub async fn project(&self, client: &client::Client) -> Result<Project, Error> {
799 client.project(self.project_id).await
800 }
801
802 pub async fn training_sessions(
803 &self,
804 client: &client::Client,
805 name: Option<&str>,
806 ) -> Result<Vec<TrainingSession>, Error> {
807 client.training_sessions(self.id, name).await
808 }
809}
810
811#[derive(Serialize, Debug)]
812pub struct PublishMetrics {
813 #[serde(rename = "trainer_session_id", skip_serializing_if = "Option::is_none")]
814 pub trainer_session_id: Option<TrainingSessionID>,
815 #[serde(
816 rename = "validate_session_id",
817 skip_serializing_if = "Option::is_none"
818 )]
819 pub validate_session_id: Option<ValidationSessionID>,
820 pub metrics: HashMap<String, Parameter>,
821}
822
823#[derive(Deserialize)]
824struct TrainingSessionParams {
825 model_params: HashMap<String, Parameter>,
826 dataset_params: DatasetParams,
827}
828
829#[derive(Deserialize)]
830pub struct TrainingSession {
831 id: TrainingSessionID,
832 #[serde(rename = "trainer_id")]
833 experiment_id: ExperimentID,
834 model: String,
835 name: String,
836 description: String,
837 params: TrainingSessionParams,
838 #[serde(rename = "docker_task")]
839 task: Task,
840}
841
842impl Display for TrainingSession {
843 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
844 write!(f, "{} {}", self.id, self.name())
845 }
846}
847
848impl TrainingSession {
849 pub fn id(&self) -> TrainingSessionID {
850 self.id
851 }
852
853 pub fn name(&self) -> &str {
854 &self.name
855 }
856
857 pub fn description(&self) -> &str {
858 &self.description
859 }
860
861 pub fn model(&self) -> &str {
862 &self.model
863 }
864
865 pub fn experiment_id(&self) -> ExperimentID {
866 self.experiment_id
867 }
868
869 pub fn task(&self) -> Task {
870 self.task.clone()
871 }
872
873 pub fn model_params(&self) -> &HashMap<String, Parameter> {
874 &self.params.model_params
875 }
876
877 pub fn dataset_params(&self) -> &DatasetParams {
878 &self.params.dataset_params
879 }
880
881 pub fn train_group(&self) -> &str {
882 &self.params.dataset_params.train_group
883 }
884
885 pub fn val_group(&self) -> &str {
886 &self.params.dataset_params.val_group
887 }
888
889 pub async fn experiment(&self, client: &client::Client) -> Result<Experiment, Error> {
890 client.experiment(self.experiment_id).await
891 }
892
893 pub async fn dataset(&self, client: &client::Client) -> Result<Dataset, Error> {
894 client.dataset(self.params.dataset_params.dataset_id).await
895 }
896
897 pub async fn annotation_set(&self, client: &client::Client) -> Result<AnnotationSet, Error> {
898 client
899 .annotation_set(self.params.dataset_params.annotation_set_id)
900 .await
901 }
902
903 pub async fn artifacts(&self, client: &client::Client) -> Result<Vec<Artifact>, Error> {
904 client.artifacts(self.id).await
905 }
906
907 pub async fn metrics(
908 &self,
909 client: &client::Client,
910 ) -> Result<HashMap<String, Parameter>, Error> {
911 #[derive(Deserialize)]
912 #[serde(untagged, deny_unknown_fields, expecting = "map, empty map or string")]
913 enum Response {
914 Empty {},
915 Map(HashMap<String, Parameter>),
916 String(String),
917 }
918
919 let params = HashMap::from([("trainer_session_id", self.id().value())]);
920 let resp: Response = client
921 .rpc("trainer.session.metrics".to_owned(), Some(params))
922 .await?;
923
924 Ok(match resp {
925 Response::String(metrics) => serde_json::from_str(&metrics)?,
926 Response::Map(metrics) => metrics,
927 Response::Empty {} => HashMap::new(),
928 })
929 }
930
931 pub async fn set_metrics(
932 &self,
933 client: &client::Client,
934 metrics: HashMap<String, Parameter>,
935 ) -> Result<(), Error> {
936 let metrics = PublishMetrics {
937 trainer_session_id: Some(self.id()),
938 validate_session_id: None,
939 metrics,
940 };
941
942 let _: String = client
943 .rpc("trainer.session.metrics".to_owned(), Some(metrics))
944 .await?;
945
946 Ok(())
947 }
948
949 pub async fn download_artifact(
951 &self,
952 client: &client::Client,
953 filename: &str,
954 ) -> Result<Vec<u8>, Error> {
955 client
956 .fetch(&format!(
957 "download_model?training_session_id={}&file={}",
958 self.id().value(),
959 filename
960 ))
961 .await
962 }
963
964 pub async fn upload_artifact(
968 &self,
969 client: &client::Client,
970 filename: &str,
971 path: PathBuf,
972 ) -> Result<(), Error> {
973 self.upload(client, &[(format!("artifacts/{}", filename), path)])
974 .await
975 }
976
977 pub async fn download_checkpoint(
979 &self,
980 client: &client::Client,
981 filename: &str,
982 ) -> Result<Vec<u8>, Error> {
983 client
984 .fetch(&format!(
985 "download_checkpoint?folder=checkpoints&training_session_id={}&file={}",
986 self.id().value(),
987 filename
988 ))
989 .await
990 }
991
992 pub async fn upload_checkpoint(
996 &self,
997 client: &client::Client,
998 filename: &str,
999 path: PathBuf,
1000 ) -> Result<(), Error> {
1001 self.upload(client, &[(format!("checkpoints/{}", filename), path)])
1002 .await
1003 }
1004
1005 pub async fn download(&self, client: &client::Client, filename: &str) -> Result<String, Error> {
1009 #[derive(Serialize)]
1010 struct DownloadRequest {
1011 session_id: TrainingSessionID,
1012 file_path: String,
1013 }
1014
1015 let params = DownloadRequest {
1016 session_id: self.id(),
1017 file_path: filename.to_string(),
1018 };
1019
1020 client
1021 .rpc("trainer.download.file".to_owned(), Some(params))
1022 .await
1023 }
1024
1025 pub async fn upload(
1026 &self,
1027 client: &client::Client,
1028 files: &[(String, PathBuf)],
1029 ) -> Result<(), Error> {
1030 let mut parts = Form::new().part(
1031 "params",
1032 Part::text(format!("{{ \"session_id\": {} }}", self.id().value())),
1033 );
1034
1035 for (name, path) in files {
1036 let file_part = Part::file(path).await?.file_name(name.to_owned());
1037 parts = parts.part("file", file_part);
1038 }
1039
1040 let result = client.post_multipart("trainer.upload.files", parts).await?;
1041 trace!("TrainingSession::upload: {:?}", result);
1042 Ok(())
1043 }
1044}
1045
1046#[derive(Deserialize, Clone, Debug)]
1047pub struct ValidationSession {
1048 id: ValidationSessionID,
1049 description: String,
1050 dataset_id: DatasetID,
1051 experiment_id: ExperimentID,
1052 training_session_id: TrainingSessionID,
1053 #[serde(rename = "gt_annotation_set_id")]
1054 annotation_set_id: AnnotationSetID,
1055 #[serde(deserialize_with = "validation_session_params")]
1056 params: HashMap<String, Parameter>,
1057 #[serde(rename = "docker_task")]
1058 task: Task,
1059}
1060
1061fn validation_session_params<'de, D>(
1062 deserializer: D,
1063) -> Result<HashMap<String, Parameter>, D::Error>
1064where
1065 D: Deserializer<'de>,
1066{
1067 #[derive(Deserialize)]
1068 struct ModelParams {
1069 validation: Option<HashMap<String, Parameter>>,
1070 }
1071
1072 #[derive(Deserialize)]
1073 struct ValidateParams {
1074 model: String,
1075 }
1076
1077 #[derive(Deserialize)]
1078 struct Params {
1079 model_params: ModelParams,
1080 validate_params: ValidateParams,
1081 }
1082
1083 let params = Params::deserialize(deserializer)?;
1084 let params = match params.model_params.validation {
1085 Some(mut map) => {
1086 map.insert(
1087 "model".to_string(),
1088 Parameter::String(params.validate_params.model),
1089 );
1090 map
1091 }
1092 None => HashMap::from([(
1093 "model".to_string(),
1094 Parameter::String(params.validate_params.model),
1095 )]),
1096 };
1097
1098 Ok(params)
1099}
1100
1101impl ValidationSession {
1102 pub fn id(&self) -> ValidationSessionID {
1103 self.id
1104 }
1105
1106 pub fn name(&self) -> &str {
1107 self.task.name()
1108 }
1109
1110 pub fn description(&self) -> &str {
1111 &self.description
1112 }
1113
1114 pub fn dataset_id(&self) -> DatasetID {
1115 self.dataset_id
1116 }
1117
1118 pub fn experiment_id(&self) -> ExperimentID {
1119 self.experiment_id
1120 }
1121
1122 pub fn training_session_id(&self) -> TrainingSessionID {
1123 self.training_session_id
1124 }
1125
1126 pub fn annotation_set_id(&self) -> AnnotationSetID {
1127 self.annotation_set_id
1128 }
1129
1130 pub fn params(&self) -> &HashMap<String, Parameter> {
1131 &self.params
1132 }
1133
1134 pub fn task(&self) -> &Task {
1135 &self.task
1136 }
1137
1138 pub async fn metrics(
1139 &self,
1140 client: &client::Client,
1141 ) -> Result<HashMap<String, Parameter>, Error> {
1142 #[derive(Deserialize)]
1143 #[serde(untagged, deny_unknown_fields, expecting = "map, empty map or string")]
1144 enum Response {
1145 Empty {},
1146 Map(HashMap<String, Parameter>),
1147 String(String),
1148 }
1149
1150 let params = HashMap::from([("validate_session_id", self.id().value())]);
1151 let resp: Response = client
1152 .rpc("validate.session.metrics".to_owned(), Some(params))
1153 .await?;
1154
1155 Ok(match resp {
1156 Response::String(metrics) => serde_json::from_str(&metrics)?,
1157 Response::Map(metrics) => metrics,
1158 Response::Empty {} => HashMap::new(),
1159 })
1160 }
1161
1162 pub async fn set_metrics(
1163 &self,
1164 client: &client::Client,
1165 metrics: HashMap<String, Parameter>,
1166 ) -> Result<(), Error> {
1167 let metrics = PublishMetrics {
1168 trainer_session_id: None,
1169 validate_session_id: Some(self.id()),
1170 metrics,
1171 };
1172
1173 let _: String = client
1174 .rpc("validate.session.metrics".to_owned(), Some(metrics))
1175 .await?;
1176
1177 Ok(())
1178 }
1179
1180 pub async fn upload_data(
1205 &self,
1206 client: &client::Client,
1207 files: &[(String, std::path::PathBuf)],
1208 folder: Option<&str>,
1209 progress: Option<tokio::sync::mpsc::Sender<Progress>>,
1210 ) -> Result<(), Error> {
1211 use futures::StreamExt;
1212 use std::sync::{
1213 Arc,
1214 atomic::{AtomicUsize, Ordering},
1215 };
1216 use tokio_util::io::ReaderStream;
1217
1218 let mut total: usize = 0;
1220 let mut file_meta = Vec::with_capacity(files.len());
1221 for (name, path) in files {
1222 let f = tokio::fs::File::open(path).await?;
1223 let len = f.metadata().await?.len() as usize;
1224 total += len;
1225 file_meta.push((name.clone(), f, len));
1226 }
1227
1228 let sent = Arc::new(AtomicUsize::new(0));
1230
1231 let mut form = Form::new().text("session_id", self.id().value().to_string());
1232 if let Some(folder) = folder.filter(|s| !s.is_empty()) {
1233 form = form.text("folder", folder.to_owned());
1234 }
1235
1236 for (name, file, len) in file_meta {
1237 let reader_stream = ReaderStream::new(file);
1238 let sent_clone = sent.clone();
1239 let progress_clone = progress.clone();
1240 let progress_stream = reader_stream.inspect(move |chunk_result| {
1241 if let Ok(chunk) = chunk_result {
1242 let current =
1243 sent_clone.fetch_add(chunk.len(), Ordering::Relaxed) + chunk.len();
1244 if let Some(tx) = &progress_clone {
1249 let _ = tx.try_send(Progress {
1250 current,
1251 total,
1252 status: None,
1253 });
1254 }
1255 }
1256 });
1257 let body = reqwest::Body::wrap_stream(progress_stream);
1258 let part = Part::stream_with_length(body, len as u64).file_name(name);
1259 form = form.part("file", part);
1260 }
1261
1262 let result = match client.post_multipart("val.data.upload", form).await {
1263 Ok(_) => Ok(()),
1264 Err(Error::RpcError(code, msg)) => {
1265 Err(client::map_rpc_error("val.data.upload", code, msg, None))
1266 }
1267 Err(e) => Err(e),
1268 };
1269
1270 if result.is_ok()
1275 && let Some(tx) = progress
1276 {
1277 let _ = tx
1278 .send(Progress {
1279 current: total,
1280 total,
1281 status: None,
1282 })
1283 .await;
1284 }
1285 result
1286 }
1287
1288 pub async fn download_data(
1308 &self,
1309 client: &client::Client,
1310 filename: &str,
1311 output_path: &std::path::Path,
1312 progress: Option<tokio::sync::mpsc::Sender<Progress>>,
1313 ) -> Result<(), Error> {
1314 let req = client::ValDataDownloadRequest {
1315 session_id: self.id().value(),
1316 filename: filename.to_owned(),
1317 };
1318 match client
1319 .rpc_download("val.data.download", &req, output_path, progress)
1320 .await
1321 {
1322 Ok(()) => Ok(()),
1323 Err(Error::RpcError(code, msg)) => {
1324 Err(client::map_rpc_error("val.data.download", code, msg, None))
1325 }
1326 Err(e) => Err(e),
1327 }
1328 }
1329
1330 pub async fn data_list(&self, client: &client::Client) -> Result<Vec<String>, Error> {
1345 let req = client::ValDataListRequest {
1346 session_id: self.id().value(),
1347 };
1348 match client.rpc("val.data.list".to_owned(), Some(&req)).await {
1349 Ok(r) => Ok(r),
1350 Err(Error::RpcError(code, msg)) => {
1351 Err(client::map_rpc_error("val.data.list", code, msg, None))
1352 }
1353 Err(e) => Err(e),
1354 }
1355 }
1356}
1357
1358#[derive(Debug, Clone)]
1377pub struct StartValidationRequest {
1378 pub project_id: ProjectID,
1379 pub name: String,
1380 pub training_session_id: TrainingSessionID,
1381 pub model_file: String,
1382 pub val_type: String,
1383 pub params: HashMap<String, Parameter>,
1384 pub is_local: bool,
1385 pub is_kubernetes: bool,
1386 pub description: Option<String>,
1387 pub dataset_id: Option<DatasetID>,
1388 pub annotation_set_id: Option<AnnotationSetID>,
1389 pub snapshot_id: Option<SnapshotID>,
1390}
1391
1392#[derive(Deserialize, Debug, Clone)]
1407pub struct NewValidationSession {
1408 #[serde(rename = "id")]
1409 pub task_id: TaskID,
1410 #[serde(rename = "val_session_id", default)]
1411 pub session_id: Option<ValidationSessionID>,
1412}
1413
1414impl Display for NewValidationSession {
1415 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1416 match self.session_id {
1417 Some(id) => write!(f, "task {} session {}", self.task_id, id),
1418 None => write!(f, "task {} (no session)", self.task_id),
1419 }
1420 }
1421}
1422
1423#[derive(Deserialize, Clone, Debug)]
1424pub struct DatasetParams {
1425 dataset_id: DatasetID,
1426 annotation_set_id: AnnotationSetID,
1427 #[serde(rename = "train_group_name")]
1428 train_group: String,
1429 #[serde(rename = "val_group_name")]
1430 val_group: String,
1431}
1432
1433impl DatasetParams {
1434 pub fn dataset_id(&self) -> DatasetID {
1435 self.dataset_id
1436 }
1437
1438 pub fn annotation_set_id(&self) -> AnnotationSetID {
1439 self.annotation_set_id
1440 }
1441
1442 pub fn train_group(&self) -> &str {
1443 &self.train_group
1444 }
1445
1446 pub fn val_group(&self) -> &str {
1447 &self.val_group
1448 }
1449}
1450
1451#[derive(Serialize, Debug, Clone)]
1452pub struct TasksListParams {
1453 #[serde(skip_serializing_if = "Option::is_none")]
1454 pub continue_token: Option<String>,
1455 #[serde(skip_serializing_if = "Option::is_none")]
1456 pub types: Option<Vec<String>>,
1457 #[serde(rename = "manage_types", skip_serializing_if = "Option::is_none")]
1458 pub manager: Option<Vec<String>>,
1459 #[serde(skip_serializing_if = "Option::is_none")]
1460 pub status: Option<Vec<String>>,
1461}
1462
1463#[derive(Debug, Clone, Serialize, Deserialize)]
1469pub struct TaskDataList {
1470 pub server: String,
1471 #[serde(rename = "organization_uid")]
1472 pub organization_uid: String,
1473 #[serde(default)]
1474 pub traces: Vec<String>,
1475 #[serde(default)]
1476 pub data: std::collections::HashMap<String, Vec<String>>,
1477}
1478
1479#[derive(Debug, Clone, Serialize, Deserialize)]
1484pub struct Job {
1485 #[serde(default)]
1487 pub code: String,
1488 #[serde(default)]
1490 pub title: String,
1491 #[serde(default)]
1493 pub job_name: String,
1494 #[serde(default)]
1496 pub job_id: String,
1497 #[serde(default)]
1499 pub state: String,
1500 #[serde(default)]
1502 pub launch: Option<DateTime<Utc>>,
1503 pub task_id: i64,
1508}
1509
1510impl Job {
1511 pub fn task_id(&self) -> TaskID {
1517 TaskID::from(self.task_id.max(0) as u64)
1518 }
1519}
1520
1521#[derive(Deserialize, Debug, Clone)]
1522pub struct TasksListResult {
1523 pub tasks: Vec<Task>,
1524 pub continue_token: Option<String>,
1525}
1526
1527#[derive(Deserialize, Debug, Clone)]
1528pub struct Task {
1529 id: TaskID,
1530 name: String,
1531 #[serde(rename = "type")]
1532 workflow: String,
1533 status: String,
1534 #[serde(rename = "manage_type")]
1535 manager: Option<String>,
1536 #[serde(rename = "instance_type")]
1537 instance: String,
1538 #[serde(rename = "date")]
1539 created: DateTime<Utc>,
1540}
1541
1542impl Task {
1543 pub fn id(&self) -> TaskID {
1544 self.id
1545 }
1546
1547 pub fn name(&self) -> &str {
1548 &self.name
1549 }
1550
1551 pub fn workflow(&self) -> &str {
1552 &self.workflow
1553 }
1554
1555 pub fn status(&self) -> &str {
1556 &self.status
1557 }
1558
1559 pub fn manager(&self) -> Option<&str> {
1560 self.manager.as_deref()
1561 }
1562
1563 pub fn instance(&self) -> &str {
1564 &self.instance
1565 }
1566
1567 pub fn created(&self) -> &DateTime<Utc> {
1568 &self.created
1569 }
1570}
1571
1572impl Display for Task {
1573 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1574 write!(
1575 f,
1576 "{} [{:?} {}] {}",
1577 self.id,
1578 self.manager(),
1579 self.workflow(),
1580 self.name()
1581 )
1582 }
1583}
1584
1585#[derive(Deserialize, Debug, Clone)]
1586pub struct TaskInfo {
1587 id: TaskID,
1588 project_id: Option<ProjectID>,
1589 #[serde(rename = "task_description", alias = "description", default)]
1590 description: String,
1591 #[serde(rename = "type")]
1592 workflow: String,
1593 status: Option<String>,
1594 #[serde(default)]
1595 progress: TaskProgress,
1596 #[serde(
1597 rename = "created_date",
1598 alias = "created",
1599 default = "default_datetime_utc"
1600 )]
1601 created: DateTime<Utc>,
1602 #[serde(
1603 rename = "end_date",
1604 alias = "completed",
1605 default = "default_datetime_utc"
1606 )]
1607 completed: DateTime<Utc>,
1608}
1609
1610fn default_datetime_utc() -> DateTime<Utc> {
1611 DateTime::UNIX_EPOCH
1612}
1613
1614impl Display for TaskInfo {
1615 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1616 write!(f, "{} {}: {}", self.id, self.workflow(), self.description())
1617 }
1618}
1619
1620impl TaskInfo {
1621 pub fn id(&self) -> TaskID {
1622 self.id
1623 }
1624
1625 pub fn project_id(&self) -> Option<ProjectID> {
1626 self.project_id
1627 }
1628
1629 pub fn description(&self) -> &str {
1630 &self.description
1631 }
1632
1633 pub fn workflow(&self) -> &str {
1634 &self.workflow
1635 }
1636
1637 pub fn status(&self) -> &Option<String> {
1638 &self.status
1639 }
1640
1641 pub async fn set_status(&mut self, client: &Client, status: &str) -> Result<(), Error> {
1642 let t = client.task_status(self.id(), status).await?;
1643 self.status = Some(t.status);
1644 Ok(())
1645 }
1646
1647 pub fn stages(&self) -> HashMap<String, Stage> {
1648 match &self.progress.stages {
1649 Some(stages) => stages.clone(),
1650 None => HashMap::new(),
1651 }
1652 }
1653
1654 pub async fn update_stage(
1655 &mut self,
1656 client: &Client,
1657 stage: &str,
1658 status: &str,
1659 message: &str,
1660 percentage: u8,
1661 ) -> Result<(), Error> {
1662 client
1663 .update_stage(self.id(), stage, status, message, percentage)
1664 .await?;
1665 let t = client.task_info(self.id()).await?;
1666 self.progress.stages = Some(t.progress.stages.unwrap_or_default());
1667 Ok(())
1668 }
1669
1670 pub async fn set_stages(
1671 &mut self,
1672 client: &Client,
1673 stages: &[(&str, &str)],
1674 ) -> Result<(), Error> {
1675 client.set_stages(self.id(), stages).await?;
1676 let t = client.task_info(self.id()).await?;
1677 self.progress.stages = Some(t.progress.stages.unwrap_or_default());
1678 Ok(())
1679 }
1680
1681 pub async fn data_list(&self, client: &client::Client) -> Result<TaskDataList, Error> {
1697 let req = client::TaskDataListRequest {
1698 task_id: self.id().value(),
1699 };
1700 match client.rpc("task.data.list".to_owned(), Some(&req)).await {
1701 Ok(r) => Ok(r),
1702 Err(Error::RpcError(code, msg)) => Err(client::map_rpc_error(
1703 "task.data.list",
1704 code,
1705 msg,
1706 Some(self.id()),
1707 )),
1708 Err(e) => Err(e),
1709 }
1710 }
1711
1712 pub async fn upload_data(
1733 &self,
1734 client: &client::Client,
1735 path: &std::path::Path,
1736 folder: Option<&str>,
1737 progress: Option<tokio::sync::mpsc::Sender<Progress>>,
1738 ) -> Result<(), Error> {
1739 use futures::StreamExt;
1740 use std::sync::{
1741 Arc,
1742 atomic::{AtomicUsize, Ordering},
1743 };
1744 use tokio_util::io::ReaderStream;
1745
1746 let file_name = path
1747 .file_name()
1748 .and_then(|s| s.to_str())
1749 .ok_or_else(|| Error::InvalidParameters("path must have a UTF-8 filename".into()))?
1750 .to_owned();
1751
1752 let file = tokio::fs::File::open(path).await?;
1753 let total = file.metadata().await?.len() as usize;
1754 let sent = Arc::new(AtomicUsize::new(0));
1755
1756 let reader_stream = ReaderStream::new(file);
1757 let sent_clone = sent.clone();
1758 let progress_clone = progress.clone();
1759 let progress_stream = reader_stream.inspect(move |chunk_result| {
1760 if let Ok(chunk) = chunk_result {
1761 let current = sent_clone.fetch_add(chunk.len(), Ordering::Relaxed) + chunk.len();
1762 if let Some(tx) = &progress_clone {
1768 let _ = tx.try_send(Progress {
1769 current,
1770 total,
1771 status: None,
1772 });
1773 }
1774 }
1775 });
1776
1777 let body = reqwest::Body::wrap_stream(progress_stream);
1778 let file_part = Part::stream_with_length(body, total as u64).file_name(file_name);
1779
1780 let mut form = Form::new().text("task_id", self.id().value().to_string());
1781 if let Some(folder) = folder.filter(|s| !s.is_empty()) {
1782 form = form.text("folder", folder.to_owned());
1783 }
1784 form = form.part("file", file_part);
1785
1786 let result = match client.post_multipart("task.data.upload", form).await {
1787 Ok(_) => Ok(()),
1788 Err(Error::RpcError(code, msg)) => Err(client::map_rpc_error(
1789 "task.data.upload",
1790 code,
1791 msg,
1792 Some(self.id()),
1793 )),
1794 Err(e) => Err(e),
1795 };
1796
1797 if result.is_ok()
1801 && let Some(tx) = progress
1802 {
1803 let _ = tx
1804 .send(Progress {
1805 current: total,
1806 total,
1807 status: None,
1808 })
1809 .await;
1810 }
1811 result
1812 }
1813
1814 pub async fn download_data(
1843 &self,
1844 client: &client::Client,
1845 file: &str,
1846 folder: Option<&str>,
1847 output_path: &std::path::Path,
1848 progress: Option<tokio::sync::mpsc::Sender<Progress>>,
1849 ) -> Result<(), Error> {
1850 let folder = folder.unwrap_or("").to_owned();
1851 let req = client::TaskDataDownloadRequest {
1852 task_id: self.id().value(),
1853 folder,
1854 file: file.to_owned(),
1855 };
1856 match client
1857 .rpc_download("task.data.download", &req, output_path, progress)
1858 .await
1859 {
1860 Ok(()) => Ok(()),
1861 Err(Error::RpcError(code, msg)) => Err(client::map_rpc_error(
1862 "task.data.download",
1863 code,
1864 msg,
1865 Some(self.id()),
1866 )),
1867 Err(e) => Err(e),
1868 }
1869 }
1870
1871 pub async fn add_chart(
1899 &self,
1900 client: &client::Client,
1901 group: &str,
1902 name: &str,
1903 data: Parameter,
1904 params: Option<Parameter>,
1905 ) -> Result<(), Error> {
1906 client::validate_chart_args(group, name)?;
1907 let req = client::TaskChartAddRequest {
1908 task_id: self.id().value(),
1909 group_name: group.to_owned(),
1910 chart_name: name.to_owned(),
1911 params,
1912 data,
1913 };
1914 let _resp: serde_json::Value =
1915 match client.rpc("task.chart.add".to_owned(), Some(&req)).await {
1916 Ok(r) => r,
1917 Err(Error::RpcError(code, msg)) => {
1918 return Err(client::map_rpc_error(
1919 "task.chart.add",
1920 code,
1921 msg,
1922 Some(self.id()),
1923 ));
1924 }
1925 Err(e) => return Err(e),
1926 };
1927 Ok(())
1928 }
1929
1930 pub async fn list_charts(
1947 &self,
1948 client: &client::Client,
1949 group: Option<&str>,
1950 ) -> Result<TaskDataList, Error> {
1951 let req = client::TaskChartListRequest {
1952 task_id: self.id().value(),
1953 group_name: group.unwrap_or("").to_owned(),
1954 };
1955 match client.rpc("task.chart.list".to_owned(), Some(&req)).await {
1956 Ok(r) => Ok(r),
1957 Err(Error::RpcError(code, msg)) => Err(client::map_rpc_error(
1958 "task.chart.list",
1959 code,
1960 msg,
1961 Some(self.id()),
1962 )),
1963 Err(e) => Err(e),
1964 }
1965 }
1966
1967 pub async fn get_chart(
1986 &self,
1987 client: &client::Client,
1988 group: &str,
1989 name: &str,
1990 ) -> Result<Parameter, Error> {
1991 client::validate_chart_args(group, name)?;
1992 let req = client::TaskChartGetRequest {
1993 task_id: self.id().value(),
1994 group_name: group.to_owned(),
1995 chart_name: name.to_owned(),
1996 };
1997 match client.rpc("task.chart.get".to_owned(), Some(&req)).await {
1998 Ok(r) => Ok(r),
1999 Err(Error::RpcError(code, msg)) => Err(client::map_rpc_error(
2000 "task.chart.get",
2001 code,
2002 msg,
2003 Some(self.id()),
2004 )),
2005 Err(e) => Err(e),
2006 }
2007 }
2008
2009 pub fn created(&self) -> &DateTime<Utc> {
2010 &self.created
2011 }
2012
2013 pub fn completed(&self) -> &DateTime<Utc> {
2014 &self.completed
2015 }
2016}
2017
2018#[derive(Deserialize, Debug, Default, Clone)]
2019pub struct TaskProgress {
2020 stages: Option<HashMap<String, Stage>>,
2021}
2022
2023#[derive(Serialize, Debug, Clone)]
2024pub struct TaskStatus {
2025 #[serde(rename = "docker_task_id")]
2026 pub task_id: TaskID,
2027 pub status: String,
2028}
2029
2030#[derive(Serialize, Deserialize, Debug, Clone)]
2031pub struct Stage {
2032 #[serde(rename = "docker_task_id", skip_serializing_if = "Option::is_none")]
2033 task_id: Option<TaskID>,
2034 stage: String,
2035 #[serde(skip_serializing_if = "Option::is_none")]
2036 status: Option<String>,
2037 #[serde(skip_serializing_if = "Option::is_none")]
2038 description: Option<String>,
2039 #[serde(skip_serializing_if = "Option::is_none")]
2040 message: Option<String>,
2041 percentage: u8,
2042}
2043
2044impl Stage {
2045 pub fn new(
2046 task_id: Option<TaskID>,
2047 stage: String,
2048 status: Option<String>,
2049 message: Option<String>,
2050 percentage: u8,
2051 ) -> Self {
2052 Stage {
2053 task_id,
2054 stage,
2055 status,
2056 description: None,
2057 message,
2058 percentage,
2059 }
2060 }
2061
2062 pub fn task_id(&self) -> &Option<TaskID> {
2063 &self.task_id
2064 }
2065
2066 pub fn stage(&self) -> &str {
2067 &self.stage
2068 }
2069
2070 pub fn status(&self) -> &Option<String> {
2071 &self.status
2072 }
2073
2074 pub fn description(&self) -> &Option<String> {
2075 &self.description
2076 }
2077
2078 pub fn message(&self) -> &Option<String> {
2079 &self.message
2080 }
2081
2082 pub fn percentage(&self) -> u8 {
2083 self.percentage
2084 }
2085}
2086
2087#[derive(Serialize, Debug)]
2088pub struct TaskStages {
2089 #[serde(rename = "docker_task_id")]
2090 pub task_id: TaskID,
2091 #[serde(skip_serializing_if = "Vec::is_empty")]
2092 pub stages: Vec<HashMap<String, String>>,
2093}
2094
2095#[derive(Deserialize, Debug)]
2096pub struct Artifact {
2097 name: String,
2098 #[serde(rename = "modelType")]
2099 model_type: String,
2100}
2101
2102impl Artifact {
2103 pub fn name(&self) -> &str {
2104 &self.name
2105 }
2106
2107 pub fn model_type(&self) -> &str {
2108 &self.model_type
2109 }
2110}
2111
2112#[cfg(test)]
2113mod tests {
2114 use super::*;
2115
2116 #[test]
2118 fn test_organization_id_from_u64() {
2119 let id = OrganizationID::from(12345);
2120 assert_eq!(id.value(), 12345);
2121 }
2122
2123 #[test]
2124 fn test_organization_id_display() {
2125 let id = OrganizationID::from(0xabc123);
2126 assert_eq!(format!("{}", id), "org-abc123");
2127 }
2128
2129 #[test]
2130 fn test_organization_id_try_from_str_valid() {
2131 let id = OrganizationID::try_from("org-abc123").unwrap();
2132 assert_eq!(id.value(), 0xabc123);
2133 }
2134
2135 #[test]
2136 fn test_organization_id_try_from_str_invalid_prefix() {
2137 let result = OrganizationID::try_from("invalid-abc123");
2138 assert!(result.is_err());
2139 match result {
2140 Err(Error::InvalidParameters(msg)) => {
2141 assert!(msg.contains("must start with 'org-'"));
2142 }
2143 _ => panic!("Expected InvalidParameters error"),
2144 }
2145 }
2146
2147 #[test]
2148 fn test_organization_id_try_from_str_invalid_hex() {
2149 let result = OrganizationID::try_from("org-xyz");
2150 assert!(result.is_err());
2151 }
2152
2153 #[test]
2154 fn test_organization_id_try_from_str_empty() {
2155 let result = OrganizationID::try_from("org-");
2156 assert!(result.is_err());
2157 }
2158
2159 #[test]
2160 fn test_organization_id_into_u64() {
2161 let id = OrganizationID::from(54321);
2162 let value: u64 = id.into();
2163 assert_eq!(value, 54321);
2164 }
2165
2166 #[test]
2168 fn test_usage_summary_deserialize_and_accessors() {
2169 let usage: UsageSummary = serde_json::from_str(
2170 r#"{"credits": 12.5, "funds": 49092.92, "total_funds_and_credits": 49105.42}"#,
2171 )
2172 .unwrap();
2173 assert_eq!(usage.credits(), 12.5);
2174 assert_eq!(usage.funds(), 49092.92);
2175 assert_eq!(usage.total(), 49105.42);
2176 }
2177
2178 #[test]
2179 fn test_usage_summary_defaults_for_missing_fields() {
2180 let usage: UsageSummary = serde_json::from_str("{}").unwrap();
2184 assert_eq!(usage.credits(), 0.0);
2185 assert_eq!(usage.funds(), 0.0);
2186 assert_eq!(usage.total(), 0.0);
2187 }
2188
2189 #[test]
2191 fn test_project_id_from_u64() {
2192 let id = ProjectID::from(78910);
2193 assert_eq!(id.value(), 78910);
2194 }
2195
2196 #[test]
2197 fn test_project_id_display() {
2198 let id = ProjectID::from(0xdef456);
2199 assert_eq!(format!("{}", id), "p-def456");
2200 }
2201
2202 #[test]
2203 fn test_project_id_from_str_valid() {
2204 let id = ProjectID::from_str("p-def456").unwrap();
2205 assert_eq!(id.value(), 0xdef456);
2206 }
2207
2208 #[test]
2209 fn test_project_id_try_from_str_valid() {
2210 let id = ProjectID::try_from("p-123abc").unwrap();
2211 assert_eq!(id.value(), 0x123abc);
2212 }
2213
2214 #[test]
2215 fn test_project_id_try_from_string_valid() {
2216 let id = ProjectID::try_from("p-456def".to_string()).unwrap();
2217 assert_eq!(id.value(), 0x456def);
2218 }
2219
2220 #[test]
2221 fn test_project_id_from_str_invalid_prefix() {
2222 let result = ProjectID::from_str("proj-123");
2223 assert!(result.is_err());
2224 match result {
2225 Err(Error::InvalidParameters(msg)) => {
2226 assert!(msg.contains("must start with 'p-'"));
2227 }
2228 _ => panic!("Expected InvalidParameters error"),
2229 }
2230 }
2231
2232 #[test]
2233 fn test_project_id_from_str_invalid_hex() {
2234 let result = ProjectID::from_str("p-notahex");
2235 assert!(result.is_err());
2236 }
2237
2238 #[test]
2239 fn test_project_id_into_u64() {
2240 let id = ProjectID::from(99999);
2241 let value: u64 = id.into();
2242 assert_eq!(value, 99999);
2243 }
2244
2245 #[test]
2247 fn test_experiment_id_from_u64() {
2248 let id = ExperimentID::from(1193046);
2249 assert_eq!(id.value(), 1193046);
2250 }
2251
2252 #[test]
2253 fn test_experiment_id_display() {
2254 let id = ExperimentID::from(0x123abc);
2255 assert_eq!(format!("{}", id), "exp-123abc");
2256 }
2257
2258 #[test]
2259 fn test_experiment_id_from_str_valid() {
2260 let id = ExperimentID::from_str("exp-456def").unwrap();
2261 assert_eq!(id.value(), 0x456def);
2262 }
2263
2264 #[test]
2265 fn test_experiment_id_try_from_str_valid() {
2266 let id = ExperimentID::try_from("exp-789abc").unwrap();
2267 assert_eq!(id.value(), 0x789abc);
2268 }
2269
2270 #[test]
2271 fn test_experiment_id_try_from_string_valid() {
2272 let id = ExperimentID::try_from("exp-fedcba".to_string()).unwrap();
2273 assert_eq!(id.value(), 0xfedcba);
2274 }
2275
2276 #[test]
2277 fn test_experiment_id_from_str_invalid_prefix() {
2278 let result = ExperimentID::from_str("experiment-123");
2279 assert!(result.is_err());
2280 match result {
2281 Err(Error::InvalidParameters(msg)) => {
2282 assert!(msg.contains("must start with 'exp-'"));
2283 }
2284 _ => panic!("Expected InvalidParameters error"),
2285 }
2286 }
2287
2288 #[test]
2289 fn test_experiment_id_from_str_invalid_hex() {
2290 let result = ExperimentID::from_str("exp-zzz");
2291 assert!(result.is_err());
2292 }
2293
2294 #[test]
2295 fn test_experiment_id_into_u64() {
2296 let id = ExperimentID::from(777777);
2297 let value: u64 = id.into();
2298 assert_eq!(value, 777777);
2299 }
2300
2301 #[test]
2303 fn test_training_session_id_from_u64() {
2304 let id = TrainingSessionID::from(7901234);
2305 assert_eq!(id.value(), 7901234);
2306 }
2307
2308 #[test]
2309 fn test_training_session_id_display() {
2310 let id = TrainingSessionID::from(0xabc123);
2311 assert_eq!(format!("{}", id), "t-abc123");
2312 }
2313
2314 #[test]
2315 fn test_training_session_id_from_str_valid() {
2316 let id = TrainingSessionID::from_str("t-abc123").unwrap();
2317 assert_eq!(id.value(), 0xabc123);
2318 }
2319
2320 #[test]
2321 fn test_training_session_id_try_from_str_valid() {
2322 let id = TrainingSessionID::try_from("t-deadbeef").unwrap();
2323 assert_eq!(id.value(), 0xdeadbeef);
2324 }
2325
2326 #[test]
2327 fn test_training_session_id_try_from_string_valid() {
2328 let id = TrainingSessionID::try_from("t-cafebabe".to_string()).unwrap();
2329 assert_eq!(id.value(), 0xcafebabe);
2330 }
2331
2332 #[test]
2333 fn test_training_session_id_from_str_invalid_prefix() {
2334 let result = TrainingSessionID::from_str("training-123");
2335 assert!(result.is_err());
2336 match result {
2337 Err(Error::InvalidParameters(msg)) => {
2338 assert!(msg.contains("must start with 't-'"));
2339 }
2340 _ => panic!("Expected InvalidParameters error"),
2341 }
2342 }
2343
2344 #[test]
2345 fn test_training_session_id_from_str_invalid_hex() {
2346 let result = TrainingSessionID::from_str("t-qqq");
2347 assert!(result.is_err());
2348 }
2349
2350 #[test]
2351 fn test_training_session_id_into_u64() {
2352 let id = TrainingSessionID::from(123456);
2353 let value: u64 = id.into();
2354 assert_eq!(value, 123456);
2355 }
2356
2357 #[test]
2359 fn test_validation_session_id_from_u64() {
2360 let id = ValidationSessionID::from(3456789);
2361 assert_eq!(id.value(), 3456789);
2362 }
2363
2364 #[test]
2365 fn test_validation_session_id_display() {
2366 let id = ValidationSessionID::from(0x34c985);
2367 assert_eq!(format!("{}", id), "v-34c985");
2368 }
2369
2370 #[test]
2371 fn test_validation_session_id_try_from_str_valid() {
2372 let id = ValidationSessionID::try_from("v-deadbeef").unwrap();
2373 assert_eq!(id.value(), 0xdeadbeef);
2374 }
2375
2376 #[test]
2377 fn test_validation_session_id_try_from_string_valid() {
2378 let id = ValidationSessionID::try_from("v-12345678".to_string()).unwrap();
2379 assert_eq!(id.value(), 0x12345678);
2380 }
2381
2382 #[test]
2383 fn test_validation_session_id_try_from_str_invalid_prefix() {
2384 let result = ValidationSessionID::try_from("validation-123");
2385 assert!(result.is_err());
2386 match result {
2387 Err(Error::InvalidParameters(msg)) => {
2388 assert!(msg.contains("must start with 'v-'"));
2389 }
2390 _ => panic!("Expected InvalidParameters error"),
2391 }
2392 }
2393
2394 #[test]
2395 fn test_validation_session_id_try_from_str_invalid_hex() {
2396 let result = ValidationSessionID::try_from("v-xyz");
2397 assert!(result.is_err());
2398 }
2399
2400 #[test]
2401 fn test_validation_session_id_into_u64() {
2402 let id = ValidationSessionID::from(987654);
2403 let value: u64 = id.into();
2404 assert_eq!(value, 987654);
2405 }
2406
2407 #[test]
2409 fn test_snapshot_id_from_u64() {
2410 let id = SnapshotID::from(111222);
2411 assert_eq!(id.value(), 111222);
2412 }
2413
2414 #[test]
2415 fn test_snapshot_id_display() {
2416 let id = SnapshotID::from(0xaabbcc);
2417 assert_eq!(format!("{}", id), "ss-aabbcc");
2418 }
2419
2420 #[test]
2421 fn test_snapshot_id_try_from_str_valid() {
2422 let id = SnapshotID::try_from("ss-aabbcc").unwrap();
2423 assert_eq!(id.value(), 0xaabbcc);
2424 }
2425
2426 #[test]
2427 fn test_snapshot_id_try_from_str_invalid_prefix() {
2428 let result = SnapshotID::try_from("snapshot-123");
2429 assert!(result.is_err());
2430 match result {
2431 Err(Error::InvalidParameters(msg)) => {
2432 assert!(msg.contains("must start with 'ss-'"));
2433 }
2434 _ => panic!("Expected InvalidParameters error"),
2435 }
2436 }
2437
2438 #[test]
2439 fn test_snapshot_id_try_from_str_invalid_hex() {
2440 let result = SnapshotID::try_from("ss-ggg");
2441 assert!(result.is_err());
2442 }
2443
2444 #[test]
2445 fn test_snapshot_id_into_u64() {
2446 let id = SnapshotID::from(333444);
2447 let value: u64 = id.into();
2448 assert_eq!(value, 333444);
2449 }
2450
2451 #[test]
2453 fn test_task_id_from_u64() {
2454 let id = TaskID::from(555666);
2455 assert_eq!(id.value(), 555666);
2456 }
2457
2458 #[test]
2459 fn test_task_id_display() {
2460 let id = TaskID::from(0x123456);
2461 assert_eq!(format!("{}", id), "task-123456");
2462 }
2463
2464 #[test]
2465 fn test_task_id_from_str_valid() {
2466 let id = TaskID::from_str("task-123456").unwrap();
2467 assert_eq!(id.value(), 0x123456);
2468 }
2469
2470 #[test]
2471 fn test_task_id_try_from_str_valid() {
2472 let id = TaskID::try_from("task-abcdef").unwrap();
2473 assert_eq!(id.value(), 0xabcdef);
2474 }
2475
2476 #[test]
2477 fn test_task_id_try_from_string_valid() {
2478 let id = TaskID::try_from("task-fedcba".to_string()).unwrap();
2479 assert_eq!(id.value(), 0xfedcba);
2480 }
2481
2482 #[test]
2483 fn test_task_id_from_str_invalid_prefix() {
2484 let result = TaskID::from_str("t-123");
2485 assert!(result.is_err());
2486 match result {
2487 Err(Error::InvalidParameters(msg)) => {
2488 assert!(msg.contains("must start with 'task-'"));
2489 }
2490 _ => panic!("Expected InvalidParameters error"),
2491 }
2492 }
2493
2494 #[test]
2495 fn test_task_id_from_str_invalid_hex() {
2496 let result = TaskID::from_str("task-zzz");
2497 assert!(result.is_err());
2498 }
2499
2500 #[test]
2501 fn test_task_id_into_u64() {
2502 let id = TaskID::from(777888);
2503 let value: u64 = id.into();
2504 assert_eq!(value, 777888);
2505 }
2506
2507 #[test]
2509 fn test_dataset_id_from_u64() {
2510 let id = DatasetID::from(1193046);
2511 assert_eq!(id.value(), 1193046);
2512 }
2513
2514 #[test]
2515 fn test_dataset_id_display() {
2516 let id = DatasetID::from(0x123abc);
2517 assert_eq!(format!("{}", id), "ds-123abc");
2518 }
2519
2520 #[test]
2521 fn test_dataset_id_from_str_valid() {
2522 let id = DatasetID::from_str("ds-456def").unwrap();
2523 assert_eq!(id.value(), 0x456def);
2524 }
2525
2526 #[test]
2527 fn test_dataset_id_try_from_str_valid() {
2528 let id = DatasetID::try_from("ds-789abc").unwrap();
2529 assert_eq!(id.value(), 0x789abc);
2530 }
2531
2532 #[test]
2533 fn test_dataset_id_try_from_string_valid() {
2534 let id = DatasetID::try_from("ds-fedcba".to_string()).unwrap();
2535 assert_eq!(id.value(), 0xfedcba);
2536 }
2537
2538 #[test]
2539 fn test_dataset_id_from_str_invalid_prefix() {
2540 let result = DatasetID::from_str("dataset-123");
2541 assert!(result.is_err());
2542 match result {
2543 Err(Error::InvalidParameters(msg)) => {
2544 assert!(msg.contains("must start with 'ds-'"));
2545 }
2546 _ => panic!("Expected InvalidParameters error"),
2547 }
2548 }
2549
2550 #[test]
2551 fn test_dataset_id_from_str_invalid_hex() {
2552 let result = DatasetID::from_str("ds-zzz");
2553 assert!(result.is_err());
2554 }
2555
2556 #[test]
2557 fn test_dataset_id_into_u64() {
2558 let id = DatasetID::from(111111);
2559 let value: u64 = id.into();
2560 assert_eq!(value, 111111);
2561 }
2562
2563 #[test]
2565 fn test_annotation_set_id_from_u64() {
2566 let id = AnnotationSetID::from(222333);
2567 assert_eq!(id.value(), 222333);
2568 }
2569
2570 #[test]
2571 fn test_annotation_set_id_display() {
2572 let id = AnnotationSetID::from(0xabcdef);
2573 assert_eq!(format!("{}", id), "as-abcdef");
2574 }
2575
2576 #[test]
2577 fn test_annotation_set_id_from_str_valid() {
2578 let id = AnnotationSetID::from_str("as-abcdef").unwrap();
2579 assert_eq!(id.value(), 0xabcdef);
2580 }
2581
2582 #[test]
2583 fn test_annotation_set_id_try_from_str_valid() {
2584 let id = AnnotationSetID::try_from("as-123456").unwrap();
2585 assert_eq!(id.value(), 0x123456);
2586 }
2587
2588 #[test]
2589 fn test_annotation_set_id_try_from_string_valid() {
2590 let id = AnnotationSetID::try_from("as-fedcba".to_string()).unwrap();
2591 assert_eq!(id.value(), 0xfedcba);
2592 }
2593
2594 #[test]
2595 fn test_annotation_set_id_from_str_invalid_prefix() {
2596 let result = AnnotationSetID::from_str("annotation-123");
2597 assert!(result.is_err());
2598 match result {
2599 Err(Error::InvalidParameters(msg)) => {
2600 assert!(msg.contains("must start with 'as-'"));
2601 }
2602 _ => panic!("Expected InvalidParameters error"),
2603 }
2604 }
2605
2606 #[test]
2607 fn test_annotation_set_id_from_str_invalid_hex() {
2608 let result = AnnotationSetID::from_str("as-zzz");
2609 assert!(result.is_err());
2610 }
2611
2612 #[test]
2613 fn test_annotation_set_id_into_u64() {
2614 let id = AnnotationSetID::from(444555);
2615 let value: u64 = id.into();
2616 assert_eq!(value, 444555);
2617 }
2618
2619 #[test]
2621 fn test_sample_id_from_u64() {
2622 let id = SampleID::from(666777);
2623 assert_eq!(id.value(), 666777);
2624 }
2625
2626 #[test]
2627 fn test_sample_id_display() {
2628 let id = SampleID::from(0x987654);
2629 assert_eq!(format!("{}", id), "s-987654");
2630 }
2631
2632 #[test]
2633 fn test_sample_id_try_from_str_valid() {
2634 let id = SampleID::try_from("s-987654").unwrap();
2635 assert_eq!(id.value(), 0x987654);
2636 }
2637
2638 #[test]
2639 fn test_sample_id_try_from_str_invalid_prefix() {
2640 let result = SampleID::try_from("sample-123");
2641 assert!(result.is_err());
2642 match result {
2643 Err(Error::InvalidParameters(msg)) => {
2644 assert!(msg.contains("must start with 's-'"));
2645 }
2646 _ => panic!("Expected InvalidParameters error"),
2647 }
2648 }
2649
2650 #[test]
2651 fn test_sample_id_try_from_str_invalid_hex() {
2652 let result = SampleID::try_from("s-zzz");
2653 assert!(result.is_err());
2654 }
2655
2656 #[test]
2657 fn test_sample_id_into_u64() {
2658 let id = SampleID::from(888999);
2659 let value: u64 = id.into();
2660 assert_eq!(value, 888999);
2661 }
2662
2663 #[test]
2665 fn test_app_id_from_u64() {
2666 let id = AppId::from(123123);
2667 assert_eq!(id.value(), 123123);
2668 }
2669
2670 #[test]
2671 fn test_app_id_display() {
2672 let id = AppId::from(0x456789);
2673 assert_eq!(format!("{}", id), "app-456789");
2674 }
2675
2676 #[test]
2677 fn test_app_id_try_from_str_valid() {
2678 let id = AppId::try_from("app-456789").unwrap();
2679 assert_eq!(id.value(), 0x456789);
2680 }
2681
2682 #[test]
2683 fn test_app_id_try_from_str_invalid_prefix() {
2684 let result = AppId::try_from("application-123");
2685 assert!(result.is_err());
2686 match result {
2687 Err(Error::InvalidParameters(msg)) => {
2688 assert!(msg.contains("must start with 'app-'"));
2689 }
2690 _ => panic!("Expected InvalidParameters error"),
2691 }
2692 }
2693
2694 #[test]
2695 fn test_app_id_try_from_str_invalid_hex() {
2696 let result = AppId::try_from("app-zzz");
2697 assert!(result.is_err());
2698 }
2699
2700 #[test]
2701 fn test_app_id_into_u64() {
2702 let id = AppId::from(321321);
2703 let value: u64 = id.into();
2704 assert_eq!(value, 321321);
2705 }
2706
2707 #[test]
2709 fn test_image_id_from_u64() {
2710 let id = ImageId::from(789789);
2711 assert_eq!(id.value(), 789789);
2712 }
2713
2714 #[test]
2715 fn test_image_id_display() {
2716 let id = ImageId::from(0xabcd1234);
2717 assert_eq!(format!("{}", id), "im-abcd1234");
2718 }
2719
2720 #[test]
2721 fn test_image_id_try_from_str_valid() {
2722 let id = ImageId::try_from("im-abcd1234").unwrap();
2723 assert_eq!(id.value(), 0xabcd1234);
2724 }
2725
2726 #[test]
2727 fn test_image_id_try_from_str_invalid_prefix() {
2728 let result = ImageId::try_from("image-123");
2729 assert!(result.is_err());
2730 match result {
2731 Err(Error::InvalidParameters(msg)) => {
2732 assert!(msg.contains("must start with 'im-'"));
2733 }
2734 _ => panic!("Expected InvalidParameters error"),
2735 }
2736 }
2737
2738 #[test]
2739 fn test_image_id_try_from_str_invalid_hex() {
2740 let result = ImageId::try_from("im-zzz");
2741 assert!(result.is_err());
2742 }
2743
2744 #[test]
2745 fn test_image_id_into_u64() {
2746 let id = ImageId::from(987987);
2747 let value: u64 = id.into();
2748 assert_eq!(value, 987987);
2749 }
2750
2751 #[test]
2753 fn test_id_types_equality() {
2754 let id1 = ProjectID::from(12345);
2755 let id2 = ProjectID::from(12345);
2756 let id3 = ProjectID::from(54321);
2757
2758 assert_eq!(id1, id2);
2759 assert_ne!(id1, id3);
2760 }
2761
2762 #[test]
2763 fn test_id_types_hash() {
2764 use std::collections::HashSet;
2765
2766 let mut set = HashSet::new();
2767 set.insert(DatasetID::from(100));
2768 set.insert(DatasetID::from(200));
2769 set.insert(DatasetID::from(100)); assert_eq!(set.len(), 2);
2772 assert!(set.contains(&DatasetID::from(100)));
2773 assert!(set.contains(&DatasetID::from(200)));
2774 }
2775
2776 #[test]
2777 fn test_id_types_copy_clone() {
2778 let id1 = ExperimentID::from(999);
2779 let id2 = id1; let id3 = id1; assert_eq!(id1, id2);
2783 assert_eq!(id1, id3);
2784 }
2785
2786 #[test]
2788 fn test_id_zero_value() {
2789 let id = ProjectID::from(0);
2790 assert_eq!(format!("{}", id), "p-0");
2791 assert_eq!(id.value(), 0);
2792 }
2793
2794 #[test]
2795 fn test_id_max_value() {
2796 let id = ProjectID::from(u64::MAX);
2797 assert_eq!(format!("{}", id), "p-ffffffffffffffff");
2798 assert_eq!(id.value(), u64::MAX);
2799 }
2800
2801 #[test]
2802 fn test_id_round_trip_conversion() {
2803 let original = 0xdeadbeef_u64;
2804 let id = TrainingSessionID::from(original);
2805 let back: u64 = id.into();
2806 assert_eq!(original, back);
2807 }
2808
2809 #[test]
2810 fn test_id_case_insensitive_hex() {
2811 let id1 = DatasetID::from_str("ds-ABCDEF").unwrap();
2813 let id2 = DatasetID::from_str("ds-abcdef").unwrap();
2814 assert_eq!(id1.value(), id2.value());
2815 }
2816
2817 #[test]
2818 fn test_id_with_leading_zeros() {
2819 let id = ProjectID::from_str("p-00001234").unwrap();
2820 assert_eq!(id.value(), 0x1234);
2821 }
2822
2823 #[test]
2825 fn test_parameter_integer() {
2826 let param = Parameter::Integer(42);
2827 match param {
2828 Parameter::Integer(val) => assert_eq!(val, 42),
2829 _ => panic!("Expected Integer variant"),
2830 }
2831 }
2832
2833 #[test]
2834 fn test_parameter_real() {
2835 let param = Parameter::Real(2.5);
2836 match param {
2837 Parameter::Real(val) => assert_eq!(val, 2.5),
2838 _ => panic!("Expected Real variant"),
2839 }
2840 }
2841
2842 #[test]
2843 fn test_parameter_boolean() {
2844 let param = Parameter::Boolean(true);
2845 match param {
2846 Parameter::Boolean(val) => assert!(val),
2847 _ => panic!("Expected Boolean variant"),
2848 }
2849 }
2850
2851 #[test]
2852 fn test_parameter_string() {
2853 let param = Parameter::String("test".to_string());
2854 match param {
2855 Parameter::String(val) => assert_eq!(val, "test"),
2856 _ => panic!("Expected String variant"),
2857 }
2858 }
2859
2860 #[test]
2861 fn test_parameter_array() {
2862 let param = Parameter::Array(vec![
2863 Parameter::Integer(1),
2864 Parameter::Integer(2),
2865 Parameter::Integer(3),
2866 ]);
2867 match param {
2868 Parameter::Array(arr) => assert_eq!(arr.len(), 3),
2869 _ => panic!("Expected Array variant"),
2870 }
2871 }
2872
2873 #[test]
2874 fn test_parameter_object() {
2875 let mut map = HashMap::new();
2876 map.insert("key".to_string(), Parameter::Integer(100));
2877 let param = Parameter::Object(map);
2878 match param {
2879 Parameter::Object(obj) => {
2880 assert_eq!(obj.len(), 1);
2881 assert!(obj.contains_key("key"));
2882 }
2883 _ => panic!("Expected Object variant"),
2884 }
2885 }
2886
2887 #[test]
2888 fn test_parameter_clone() {
2889 let param1 = Parameter::Integer(42);
2890 let param2 = param1.clone();
2891 assert_eq!(param1, param2);
2892 }
2893
2894 #[test]
2895 fn test_parameter_nested() {
2896 let inner_array = Parameter::Array(vec![Parameter::Integer(1), Parameter::Integer(2)]);
2897 let outer_array = Parameter::Array(vec![inner_array.clone(), inner_array]);
2898
2899 match outer_array {
2900 Parameter::Array(arr) => {
2901 assert_eq!(arr.len(), 2);
2902 }
2903 _ => panic!("Expected Array variant"),
2904 }
2905 }
2906
2907 macro_rules! test_typeid_conversions {
2910 ($test_name:ident, $type:ty, $prefix:literal, $wrong_prefix:literal) => {
2911 #[test]
2912 fn $test_name() {
2913 let id = <$type>::from(0xabc123);
2915 assert_eq!(id.value(), 0xabc123);
2916
2917 assert_eq!(format!("{}", id), concat!($prefix, "-abc123"));
2919
2920 let id: $type = concat!($prefix, "-abc123").parse().unwrap();
2922 assert_eq!(id.value(), 0xabc123);
2923
2924 assert!(concat!($wrong_prefix, "-abc").parse::<$type>().is_err());
2926
2927 assert!("abc123".parse::<$type>().is_err());
2929
2930 assert!(concat!($prefix, "-xyz").parse::<$type>().is_err());
2932
2933 let id = <$type>::try_from(concat!($prefix, "-abc123")).unwrap();
2935 assert_eq!(id.value(), 0xabc123);
2936
2937 let id = <$type>::try_from(concat!($prefix, "-abc123").to_string()).unwrap();
2939 assert_eq!(id.value(), 0xabc123);
2940
2941 let id = <$type>::from(0xabc123);
2943 let json = serde_json::to_string(&id).unwrap();
2944 let parsed: $type = serde_json::from_str(&json).unwrap();
2945 assert_eq!(id, parsed);
2946
2947 let id = <$type>::from(0xabc123);
2949 let val: u64 = id.into();
2950 assert_eq!(val, 0xabc123);
2951 }
2952 };
2953 }
2954
2955 test_typeid_conversions!(test_organization_id_conversions, OrganizationID, "org", "p");
2956 test_typeid_conversions!(test_project_id_conversions, ProjectID, "p", "org");
2957 test_typeid_conversions!(test_experiment_id_conversions, ExperimentID, "exp", "p");
2958 test_typeid_conversions!(
2959 test_training_session_id_conversions,
2960 TrainingSessionID,
2961 "t",
2962 "v"
2963 );
2964 test_typeid_conversions!(
2965 test_validation_session_id_conversions,
2966 ValidationSessionID,
2967 "v",
2968 "t"
2969 );
2970 test_typeid_conversions!(test_snapshot_id_conversions, SnapshotID, "ss", "ds");
2971 test_typeid_conversions!(test_task_id_conversions, TaskID, "task", "t");
2972 test_typeid_conversions!(test_dataset_id_conversions, DatasetID, "ds", "ss");
2973 test_typeid_conversions!(
2974 test_annotation_set_id_conversions,
2975 AnnotationSetID,
2976 "as",
2977 "ds"
2978 );
2979 test_typeid_conversions!(test_sample_id_conversions, SampleID, "s", "p");
2980 test_typeid_conversions!(test_app_id_conversions, AppId, "app", "p");
2981 test_typeid_conversions!(test_image_id_conversions, ImageId, "im", "se");
2982 test_typeid_conversions!(test_sequence_id_conversions, SequenceId, "se", "im");
2983}
2984
2985#[cfg(test)]
2986mod tests_task_data_list {
2987 use super::*;
2988
2989 #[test]
2990 fn task_data_list_deserializes_from_server_shape() {
2991 let json = r#"{
2992 "server": "test.edgefirst.studio",
2993 "organization_uid": "org-abc123",
2994 "traces": ["trace/imx95.json"],
2995 "data": {
2996 "predictions": ["predictions.parquet"],
2997 "trace": ["imx95.json"]
2998 }
2999 }"#;
3000 let parsed: TaskDataList = serde_json::from_str(json).unwrap();
3001 assert_eq!(parsed.server, "test.edgefirst.studio");
3002 assert_eq!(parsed.organization_uid, "org-abc123");
3003 assert_eq!(parsed.traces, vec!["trace/imx95.json"]);
3004 assert_eq!(
3005 parsed.data.get("predictions").unwrap(),
3006 &vec!["predictions.parquet".to_string()]
3007 );
3008 }
3009}
3010
3011#[cfg(test)]
3012mod tests_upload_data {
3013 #[test]
3017 fn folder_empty_string_is_normalised() {
3018 let folder: Option<&str> = Some("");
3019 assert!(folder.filter(|s| !s.is_empty()).is_none());
3020
3021 let folder_real: Option<&str> = Some("predictions");
3022 assert!(folder_real.filter(|s| !s.is_empty()).is_some());
3023 }
3024}
3025
3026#[cfg(test)]
3027mod tests_job_struct {
3028 use super::*;
3029
3030 #[test]
3031 fn job_deserializes_with_all_fields() {
3032 let json = r#"{
3033 "code": "edgefirst-validator:2.9.5",
3034 "title": "EdgeFirst Validator",
3035 "job_name": "smoke-test",
3036 "job_id": "aws-batch-abc",
3037 "state": "RUNNING",
3038 "launch": "2026-05-14T15:00:00Z",
3039 "task_id": 6789
3040 }"#;
3041 let job: Job = serde_json::from_str(json).unwrap();
3042 assert_eq!(job.code, "edgefirst-validator:2.9.5");
3043 assert_eq!(job.title, "EdgeFirst Validator");
3044 assert_eq!(job.job_name, "smoke-test");
3045 assert_eq!(job.job_id, "aws-batch-abc");
3046 assert_eq!(job.state, "RUNNING");
3047 assert!(job.launch.is_some());
3048 assert_eq!(job.task_id, 6789);
3049 }
3050
3051 #[test]
3052 fn job_tolerates_missing_optional_fields() {
3053 let json = r#"{ "task_id": 42 }"#;
3057 let job: Job = serde_json::from_str(json).unwrap();
3058 assert_eq!(job.task_id, 42);
3059 assert!(job.code.is_empty());
3060 assert!(job.title.is_empty());
3061 assert!(job.job_name.is_empty());
3062 assert!(job.job_id.is_empty());
3063 assert!(job.state.is_empty());
3064 assert!(job.launch.is_none());
3065 }
3066
3067 #[test]
3068 fn job_task_id_accessor_saturates_negative_to_zero() {
3069 let job = Job {
3074 code: String::new(),
3075 title: String::new(),
3076 job_name: String::new(),
3077 job_id: String::new(),
3078 state: String::new(),
3079 launch: None,
3080 task_id: -1,
3081 };
3082 assert_eq!(job.task_id().value(), 0);
3083 }
3084
3085 #[test]
3086 fn job_task_id_accessor_passes_through_positive_values() {
3087 let job = Job {
3088 code: String::new(),
3089 title: String::new(),
3090 job_name: String::new(),
3091 job_id: String::new(),
3092 state: String::new(),
3093 launch: None,
3094 task_id: 12345,
3095 };
3096 assert_eq!(job.task_id().value(), 12345);
3097 }
3098
3099 #[test]
3100 fn job_ignores_unknown_fields() {
3101 let json = r#"{
3105 "code": "x",
3106 "task_id": 1,
3107 "docker_task": { "image": "x" },
3108 "aws_region": "us-east-1",
3109 "tags": ["a", "b"]
3110 }"#;
3111 let job: Job = serde_json::from_str(json).unwrap();
3112 assert_eq!(job.task_id, 1);
3113 }
3114}
3115
3116#[cfg(test)]
3117mod tests_task_info_schema_tolerance {
3118 use super::*;
3119
3120 #[test]
3125 fn task_info_accepts_task_description_field() {
3126 let json = r#"{
3128 "id": 6699,
3129 "type": "edgefirst-validator:2.9.5",
3130 "task_description": "Profiler run for IMX95",
3131 "status": "running"
3132 }"#;
3133 let info: TaskInfo = serde_json::from_str(json).unwrap();
3134 assert_eq!(info.description(), "Profiler run for IMX95");
3135 }
3136
3137 #[test]
3138 fn task_info_accepts_legacy_description_field() {
3139 let json = r#"{
3141 "id": 6699,
3142 "type": "edgefirst-validator:2.9.5",
3143 "description": "Legacy description"
3144 }"#;
3145 let info: TaskInfo = serde_json::from_str(json).unwrap();
3146 assert_eq!(info.description(), "Legacy description");
3147 }
3148
3149 #[test]
3150 fn task_info_tolerates_missing_description() {
3151 let json = r#"{
3153 "id": 6699,
3154 "type": "x"
3155 }"#;
3156 let info: TaskInfo = serde_json::from_str(json).unwrap();
3157 assert!(info.description().is_empty());
3158 }
3159
3160 #[test]
3161 fn task_info_tolerates_missing_dates_via_default() {
3162 let json = r#"{
3164 "id": 6699,
3165 "type": "x"
3166 }"#;
3167 let info: TaskInfo = serde_json::from_str(json).unwrap();
3168 assert_eq!(info.id().value(), 6699);
3170 }
3171
3172 #[test]
3173 fn task_info_status_accessor_returns_option() {
3174 let json = r#"{
3175 "id": 1,
3176 "type": "x"
3177 }"#;
3178 let info: TaskInfo = serde_json::from_str(json).unwrap();
3179 assert!(info.status().is_none());
3180 }
3181
3182 #[test]
3183 fn task_info_stages_returns_empty_map_when_unset() {
3184 let json = r#"{
3185 "id": 1,
3186 "type": "x"
3187 }"#;
3188 let info: TaskInfo = serde_json::from_str(json).unwrap();
3189 let stages = info.stages();
3190 assert!(stages.is_empty());
3191 }
3192}
3193
3194#[cfg(test)]
3195mod tests_stage_struct {
3196 use super::*;
3197
3198 #[test]
3199 fn stage_new_sets_only_supplied_fields() {
3200 let stage = Stage::new(
3201 None,
3202 "download".into(),
3203 Some("running".into()),
3204 Some("fetching".into()),
3205 42,
3206 );
3207 assert!(stage.task_id().is_none());
3208 assert_eq!(stage.stage(), "download");
3209 assert_eq!(stage.status().as_deref(), Some("running"));
3210 assert_eq!(stage.message().as_deref(), Some("fetching"));
3211 assert_eq!(stage.percentage(), 42);
3212 assert!(stage.description().is_none());
3214 }
3215
3216 #[test]
3217 fn stage_serializes_without_optional_none_fields() {
3218 let stage = Stage::new(None, "init".into(), None, None, 0);
3220 let json = serde_json::to_value(&stage).unwrap();
3221 assert!(json.get("status").is_none(), "got: {json}");
3222 assert!(json.get("message").is_none(), "got: {json}");
3223 assert!(json.get("docker_task_id").is_none(), "got: {json}");
3224 assert_eq!(json["stage"], "init");
3226 assert_eq!(json["percentage"], 0);
3227 }
3228
3229 #[test]
3230 fn stage_serializes_task_id_when_present() {
3231 let task_id = TaskID::from(0xdeadu64);
3232 let stage = Stage::new(Some(task_id), "x".into(), None, None, 0);
3233 let json = serde_json::to_value(&stage).unwrap();
3234 assert!(json.get("docker_task_id").is_some());
3237 }
3238
3239 #[test]
3240 fn stage_round_trips_through_json() {
3241 let stage = Stage::new(
3242 None,
3243 "train".into(),
3244 Some("done".into()),
3245 Some("epoch 100".into()),
3246 100,
3247 );
3248 let s = serde_json::to_string(&stage).unwrap();
3249 let back: Stage = serde_json::from_str(&s).unwrap();
3250 assert_eq!(back.stage(), "train");
3251 assert_eq!(back.status().as_deref(), Some("done"));
3252 assert_eq!(back.message().as_deref(), Some("epoch 100"));
3253 assert_eq!(back.percentage(), 100);
3254 }
3255}
3256
3257#[cfg(test)]
3258mod tests_task_data_list_extra {
3259 use super::*;
3260
3261 #[test]
3262 fn task_data_list_with_empty_data_map() {
3263 let json = r#"{
3264 "server": "studio",
3265 "organization_uid": "org-1",
3266 "traces": [],
3267 "data": {}
3268 }"#;
3269 let parsed: TaskDataList = serde_json::from_str(json).unwrap();
3270 assert!(parsed.traces.is_empty());
3271 assert!(parsed.data.is_empty());
3272 }
3273
3274 #[test]
3275 fn task_data_list_multiple_folders() {
3276 let json = r#"{
3277 "server": "studio",
3278 "organization_uid": "org-1",
3279 "traces": ["t1", "t2"],
3280 "data": {
3281 "predictions": ["a.parquet", "b.parquet"],
3282 "metrics": ["loss.json"]
3283 }
3284 }"#;
3285 let parsed: TaskDataList = serde_json::from_str(json).unwrap();
3286 assert_eq!(parsed.traces.len(), 2);
3287 assert_eq!(parsed.data.len(), 2);
3288 assert_eq!(parsed.data["predictions"].len(), 2);
3289 }
3290}
3291
3292#[cfg(test)]
3293mod tests_artifact_struct {
3294 use super::*;
3295
3296 #[test]
3297 fn artifact_accessors_return_strs() {
3298 let json = r#"{ "name": "best.onnx", "modelType": "yolo" }"#;
3301 let a: Artifact = serde_json::from_str(json).unwrap();
3302 assert_eq!(a.name(), "best.onnx");
3303 assert_eq!(a.model_type(), "yolo");
3304 }
3305}
3306
3307#[cfg(test)]
3308mod tests_task_status_serialize {
3309 use super::*;
3310
3311 #[test]
3312 fn task_status_uses_docker_task_id_wire_field() {
3313 let s = TaskStatus {
3314 task_id: TaskID::from(0x1a2bu64),
3315 status: "training".into(),
3316 };
3317 let json = serde_json::to_value(&s).unwrap();
3318 assert!(json.get("docker_task_id").is_some(), "got: {json}");
3320 assert_eq!(json["status"], "training");
3321 }
3322}
3323
3324#[cfg(test)]
3325mod tests_task_stages_serialize {
3326 use super::*;
3327
3328 #[test]
3329 fn task_stages_omits_empty_vec() {
3330 let stages = TaskStages {
3331 task_id: TaskID::from(1u64),
3332 stages: Vec::new(),
3333 };
3334 let json = serde_json::to_value(&stages).unwrap();
3335 assert!(json.get("stages").is_none(), "got: {json}");
3337 }
3338
3339 #[test]
3340 fn task_stages_serializes_non_empty_vec() {
3341 let stages = TaskStages {
3342 task_id: TaskID::from(1u64),
3343 stages: vec![std::collections::HashMap::from([(
3344 "stage".to_string(),
3345 "download".to_string(),
3346 )])],
3347 };
3348 let json = serde_json::to_value(&stages).unwrap();
3349 assert_eq!(json["stages"][0]["stage"], "download");
3350 }
3351}