edgefirst_client/
api.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright © 2025 Au-Zone Technologies. All Rights Reserved.
3
4use crate::{AnnotationSet, Client, Dataset, Error, 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/// Generic parameter value used in API requests and configuration.
12///
13/// This enum represents various data types that can be passed as parameters
14/// to EdgeFirst Studio API calls or stored in configuration files.
15///
16/// # Examples
17///
18/// ```rust
19/// use edgefirst_client::Parameter;
20/// use std::collections::HashMap;
21///
22/// // Different parameter types
23/// let int_param = Parameter::Integer(42);
24/// let float_param = Parameter::Real(3.14);
25/// let bool_param = Parameter::Boolean(true);
26/// let string_param = Parameter::String("model_name".to_string());
27///
28/// // Complex nested parameters
29/// let array_param = Parameter::Array(vec![
30///     Parameter::Integer(1),
31///     Parameter::Integer(2),
32///     Parameter::Integer(3),
33/// ]);
34///
35/// let mut config = HashMap::new();
36/// config.insert("learning_rate".to_string(), Parameter::Real(0.001));
37/// config.insert("epochs".to_string(), Parameter::Integer(100));
38/// let object_param = Parameter::Object(config);
39/// ```
40#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
41#[serde(untagged)]
42pub enum Parameter {
43    /// 64-bit signed integer value.
44    Integer(i64),
45    /// 64-bit floating-point value.
46    Real(f64),
47    /// Boolean true/false value.
48    Boolean(bool),
49    /// UTF-8 string value.
50    String(String),
51    /// Array of nested parameter values.
52    Array(Vec<Parameter>),
53    /// Object/map with string keys and parameter values.
54    Object(HashMap<String, Parameter>),
55}
56
57#[derive(Deserialize)]
58pub struct LoginResult {
59    pub(crate) token: String,
60}
61
62/// Unique identifier for an organization in EdgeFirst Studio.
63///
64/// Organizations are the top-level containers for users, projects, and
65/// resources in EdgeFirst Studio. Each organization has a unique ID that is
66/// displayed in hexadecimal format with an "org-" prefix (e.g., "org-abc123").
67///
68/// # Examples
69///
70/// ```rust
71/// use edgefirst_client::OrganizationID;
72///
73/// // Create from u64
74/// let org_id = OrganizationID::from(12345);
75/// println!("{}", org_id); // Displays: org-3039
76///
77/// // Parse from string
78/// let org_id: OrganizationID = "org-abc123".try_into().unwrap();
79/// assert_eq!(org_id.value(), 0xabc123);
80/// ```
81#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
82pub struct OrganizationID(u64);
83
84impl Display for OrganizationID {
85    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
86        write!(f, "org-{:x}", self.0)
87    }
88}
89
90impl From<u64> for OrganizationID {
91    fn from(id: u64) -> Self {
92        OrganizationID(id)
93    }
94}
95
96impl From<OrganizationID> for u64 {
97    fn from(val: OrganizationID) -> Self {
98        val.0
99    }
100}
101
102impl OrganizationID {
103    pub fn value(&self) -> u64 {
104        self.0
105    }
106}
107
108impl TryFrom<&str> for OrganizationID {
109    type Error = Error;
110
111    fn try_from(s: &str) -> Result<Self, Self::Error> {
112        let hex_part = s.strip_prefix("org-").ok_or_else(|| {
113            Error::InvalidParameters("Organization ID must start with 'org-' prefix".to_string())
114        })?;
115        let id = u64::from_str_radix(hex_part, 16)?;
116        Ok(OrganizationID(id))
117    }
118}
119
120/// Organization information and metadata.
121///
122/// Each user belongs to an organization which contains projects, datasets,
123/// and other resources. Organizations provide isolated workspaces for teams
124/// and manage resource quotas and billing.
125///
126/// # Examples
127///
128/// ```no_run
129/// use edgefirst_client::{Client, Organization};
130///
131/// # async fn example() -> Result<(), edgefirst_client::Error> {
132/// # let client = Client::new()?;
133/// // Access organization details
134/// let org: Organization = client.organization().await?;
135/// println!("Organization: {} (ID: {})", org.name(), org.id());
136/// println!("Available credits: {}", org.credits());
137/// # Ok(())
138/// # }
139/// ```
140#[derive(Deserialize, Clone, Debug)]
141pub struct Organization {
142    id: OrganizationID,
143    name: String,
144    #[serde(rename = "latest_credit")]
145    credits: i64,
146}
147
148impl Display for Organization {
149    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
150        write!(f, "{}", self.name())
151    }
152}
153
154impl Organization {
155    pub fn id(&self) -> OrganizationID {
156        self.id
157    }
158
159    pub fn name(&self) -> &str {
160        &self.name
161    }
162
163    pub fn credits(&self) -> i64 {
164        self.credits
165    }
166}
167
168/// Unique identifier for a project within EdgeFirst Studio.
169///
170/// Projects contain datasets, experiments, and models within an organization.
171/// Each project has a unique ID displayed in hexadecimal format with a "p-"
172/// prefix (e.g., "p-def456").
173///
174/// # Examples
175///
176/// ```rust
177/// use edgefirst_client::ProjectID;
178/// use std::str::FromStr;
179///
180/// // Create from u64
181/// let project_id = ProjectID::from(78910);
182/// println!("{}", project_id); // Displays: p-1343e
183///
184/// // Parse from string
185/// let project_id = ProjectID::from_str("p-def456").unwrap();
186/// assert_eq!(project_id.value(), 0xdef456);
187/// ```
188#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
189pub struct ProjectID(u64);
190
191impl Display for ProjectID {
192    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
193        write!(f, "p-{:x}", self.0)
194    }
195}
196
197impl From<u64> for ProjectID {
198    fn from(id: u64) -> Self {
199        ProjectID(id)
200    }
201}
202
203impl From<ProjectID> for u64 {
204    fn from(val: ProjectID) -> Self {
205        val.0
206    }
207}
208
209impl ProjectID {
210    pub fn value(&self) -> u64 {
211        self.0
212    }
213}
214
215impl TryFrom<&str> for ProjectID {
216    type Error = Error;
217
218    fn try_from(s: &str) -> Result<Self, Self::Error> {
219        ProjectID::from_str(s)
220    }
221}
222
223impl TryFrom<String> for ProjectID {
224    type Error = Error;
225
226    fn try_from(s: String) -> Result<Self, Self::Error> {
227        ProjectID::from_str(&s)
228    }
229}
230
231impl FromStr for ProjectID {
232    type Err = Error;
233
234    fn from_str(s: &str) -> Result<Self, Self::Err> {
235        let hex_part = s.strip_prefix("p-").ok_or_else(|| {
236            Error::InvalidParameters("Project ID must start with 'p-' prefix".to_string())
237        })?;
238        let id = u64::from_str_radix(hex_part, 16)?;
239        Ok(ProjectID(id))
240    }
241}
242
243/// Unique identifier for an experiment within a project.
244///
245/// Experiments represent individual machine learning experiments with specific
246/// configurations, datasets, and results. Each experiment has a unique ID
247/// displayed in hexadecimal format with an "exp-" prefix (e.g., "exp-123abc").
248///
249/// # Examples
250///
251/// ```rust
252/// use edgefirst_client::ExperimentID;
253/// use std::str::FromStr;
254///
255/// // Create from u64
256/// let exp_id = ExperimentID::from(1193046);
257/// println!("{}", exp_id); // Displays: exp-123abc
258///
259/// // Parse from string
260/// let exp_id = ExperimentID::from_str("exp-456def").unwrap();
261/// assert_eq!(exp_id.value(), 0x456def);
262/// ```
263#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
264pub struct ExperimentID(u64);
265
266impl Display for ExperimentID {
267    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
268        write!(f, "exp-{:x}", self.0)
269    }
270}
271
272impl From<u64> for ExperimentID {
273    fn from(id: u64) -> Self {
274        ExperimentID(id)
275    }
276}
277
278impl From<ExperimentID> for u64 {
279    fn from(val: ExperimentID) -> Self {
280        val.0
281    }
282}
283
284impl ExperimentID {
285    pub fn value(&self) -> u64 {
286        self.0
287    }
288}
289
290impl TryFrom<&str> for ExperimentID {
291    type Error = Error;
292
293    fn try_from(s: &str) -> Result<Self, Self::Error> {
294        ExperimentID::from_str(s)
295    }
296}
297
298impl TryFrom<String> for ExperimentID {
299    type Error = Error;
300
301    fn try_from(s: String) -> Result<Self, Self::Error> {
302        ExperimentID::from_str(&s)
303    }
304}
305
306impl FromStr for ExperimentID {
307    type Err = Error;
308
309    fn from_str(s: &str) -> Result<Self, Self::Err> {
310        let hex_part = s.strip_prefix("exp-").ok_or_else(|| {
311            Error::InvalidParameters("Experiment ID must start with 'exp-' prefix".to_string())
312        })?;
313        let id = u64::from_str_radix(hex_part, 16)?;
314        Ok(ExperimentID(id))
315    }
316}
317
318/// Unique identifier for a training session within an experiment.
319///
320/// Training sessions represent individual training runs with specific
321/// hyperparameters and configurations. Each training session has a unique ID
322/// displayed in hexadecimal format with a "t-" prefix (e.g., "t-789012").
323///
324/// # Examples
325///
326/// ```rust
327/// use edgefirst_client::TrainingSessionID;
328/// use std::str::FromStr;
329///
330/// // Create from u64
331/// let training_id = TrainingSessionID::from(7901234);
332/// println!("{}", training_id); // Displays: t-7872f2
333///
334/// // Parse from string
335/// let training_id = TrainingSessionID::from_str("t-abc123").unwrap();
336/// assert_eq!(training_id.value(), 0xabc123);
337/// ```
338#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
339pub struct TrainingSessionID(u64);
340
341impl Display for TrainingSessionID {
342    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
343        write!(f, "t-{:x}", self.0)
344    }
345}
346
347impl From<u64> for TrainingSessionID {
348    fn from(id: u64) -> Self {
349        TrainingSessionID(id)
350    }
351}
352
353impl From<TrainingSessionID> for u64 {
354    fn from(val: TrainingSessionID) -> Self {
355        val.0
356    }
357}
358
359impl TrainingSessionID {
360    pub fn value(&self) -> u64 {
361        self.0
362    }
363}
364
365impl TryFrom<&str> for TrainingSessionID {
366    type Error = Error;
367
368    fn try_from(s: &str) -> Result<Self, Self::Error> {
369        TrainingSessionID::from_str(s)
370    }
371}
372
373impl TryFrom<String> for TrainingSessionID {
374    type Error = Error;
375
376    fn try_from(s: String) -> Result<Self, Self::Error> {
377        TrainingSessionID::from_str(&s)
378    }
379}
380
381impl FromStr for TrainingSessionID {
382    type Err = Error;
383
384    fn from_str(s: &str) -> Result<Self, Self::Err> {
385        let hex_part = s.strip_prefix("t-").ok_or_else(|| {
386            Error::InvalidParameters("Training Session ID must start with 't-' prefix".to_string())
387        })?;
388        let id = u64::from_str_radix(hex_part, 16)?;
389        Ok(TrainingSessionID(id))
390    }
391}
392
393/// Unique identifier for a validation session within an experiment.
394///
395/// Validation sessions represent model validation runs that evaluate trained
396/// models against test datasets. Each validation session has a unique ID
397/// displayed in hexadecimal format with a "v-" prefix (e.g., "v-345678").
398///
399/// # Examples
400///
401/// ```rust
402/// use edgefirst_client::ValidationSessionID;
403///
404/// // Create from u64
405/// let validation_id = ValidationSessionID::from(3456789);
406/// println!("{}", validation_id); // Displays: v-34c985
407///
408/// // Parse from string
409/// let validation_id: ValidationSessionID = "v-deadbeef".try_into().unwrap();
410/// assert_eq!(validation_id.value(), 0xdeadbeef);
411/// ```
412#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
413pub struct ValidationSessionID(u64);
414
415impl Display for ValidationSessionID {
416    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
417        write!(f, "v-{:x}", self.0)
418    }
419}
420
421impl From<u64> for ValidationSessionID {
422    fn from(id: u64) -> Self {
423        ValidationSessionID(id)
424    }
425}
426
427impl From<ValidationSessionID> for u64 {
428    fn from(val: ValidationSessionID) -> Self {
429        val.0
430    }
431}
432
433impl ValidationSessionID {
434    pub fn value(&self) -> u64 {
435        self.0
436    }
437}
438
439impl TryFrom<&str> for ValidationSessionID {
440    type Error = Error;
441
442    fn try_from(s: &str) -> Result<Self, Self::Error> {
443        let hex_part = s.strip_prefix("v-").ok_or_else(|| {
444            Error::InvalidParameters(
445                "Validation Session ID must start with 'v-' prefix".to_string(),
446            )
447        })?;
448        let id = u64::from_str_radix(hex_part, 16)?;
449        Ok(ValidationSessionID(id))
450    }
451}
452
453impl TryFrom<String> for ValidationSessionID {
454    type Error = Error;
455
456    fn try_from(s: String) -> Result<Self, Self::Error> {
457        ValidationSessionID::try_from(s.as_str())
458    }
459}
460
461#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
462pub struct SnapshotID(u64);
463
464impl Display for SnapshotID {
465    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
466        write!(f, "ss-{:x}", self.0)
467    }
468}
469
470impl From<u64> for SnapshotID {
471    fn from(id: u64) -> Self {
472        SnapshotID(id)
473    }
474}
475
476impl From<SnapshotID> for u64 {
477    fn from(val: SnapshotID) -> Self {
478        val.0
479    }
480}
481
482impl SnapshotID {
483    pub fn value(&self) -> u64 {
484        self.0
485    }
486}
487
488impl TryFrom<&str> for SnapshotID {
489    type Error = Error;
490
491    fn try_from(s: &str) -> Result<Self, Self::Error> {
492        let hex_part = s.strip_prefix("ss-").ok_or_else(|| {
493            Error::InvalidParameters("Snapshot ID must start with 'ss-' prefix".to_string())
494        })?;
495        let id = u64::from_str_radix(hex_part, 16)?;
496        Ok(SnapshotID(id))
497    }
498}
499
500#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
501pub struct TaskID(u64);
502
503impl Display for TaskID {
504    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
505        write!(f, "task-{:x}", self.0)
506    }
507}
508
509impl From<u64> for TaskID {
510    fn from(id: u64) -> Self {
511        TaskID(id)
512    }
513}
514
515impl From<TaskID> for u64 {
516    fn from(val: TaskID) -> Self {
517        val.0
518    }
519}
520
521impl TaskID {
522    pub fn value(&self) -> u64 {
523        self.0
524    }
525}
526
527impl TryFrom<&str> for TaskID {
528    type Error = Error;
529
530    fn try_from(s: &str) -> Result<Self, Self::Error> {
531        TaskID::from_str(s)
532    }
533}
534
535impl TryFrom<String> for TaskID {
536    type Error = Error;
537
538    fn try_from(s: String) -> Result<Self, Self::Error> {
539        TaskID::from_str(&s)
540    }
541}
542
543impl FromStr for TaskID {
544    type Err = Error;
545
546    fn from_str(s: &str) -> Result<Self, Self::Err> {
547        let hex_part = s.strip_prefix("task-").ok_or_else(|| {
548            Error::InvalidParameters("Task ID must start with 'task-' prefix".to_string())
549        })?;
550        let id = u64::from_str_radix(hex_part, 16)?;
551        Ok(TaskID(id))
552    }
553}
554
555/// Unique identifier for a dataset within a project.
556///
557/// Datasets contain collections of images, annotations, and other data used for
558/// machine learning experiments. Each dataset has a unique ID displayed in
559/// hexadecimal format with a "ds-" prefix (e.g., "ds-123abc").
560///
561/// # Examples
562///
563/// ```rust
564/// use edgefirst_client::DatasetID;
565/// use std::str::FromStr;
566///
567/// // Create from u64
568/// let dataset_id = DatasetID::from(1193046);
569/// println!("{}", dataset_id); // Displays: ds-123abc
570///
571/// // Parse from string
572/// let dataset_id = DatasetID::from_str("ds-456def").unwrap();
573/// assert_eq!(dataset_id.value(), 0x456def);
574/// ```
575#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
576pub struct DatasetID(u64);
577
578impl Display for DatasetID {
579    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
580        write!(f, "ds-{:x}", self.0)
581    }
582}
583
584impl From<u64> for DatasetID {
585    fn from(id: u64) -> Self {
586        DatasetID(id)
587    }
588}
589
590impl From<DatasetID> for u64 {
591    fn from(val: DatasetID) -> Self {
592        val.0
593    }
594}
595
596impl DatasetID {
597    pub fn value(&self) -> u64 {
598        self.0
599    }
600}
601
602impl TryFrom<&str> for DatasetID {
603    type Error = Error;
604
605    fn try_from(s: &str) -> Result<Self, Self::Error> {
606        DatasetID::from_str(s)
607    }
608}
609
610impl TryFrom<String> for DatasetID {
611    type Error = Error;
612
613    fn try_from(s: String) -> Result<Self, Self::Error> {
614        DatasetID::from_str(&s)
615    }
616}
617
618impl FromStr for DatasetID {
619    type Err = Error;
620
621    fn from_str(s: &str) -> Result<Self, Self::Err> {
622        let hex_part = s.strip_prefix("ds-").ok_or_else(|| {
623            Error::InvalidParameters("Dataset ID must start with 'ds-' prefix".to_string())
624        })?;
625        let id = u64::from_str_radix(hex_part, 16)?;
626        Ok(DatasetID(id))
627    }
628}
629
630#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
631pub struct AnnotationSetID(u64);
632
633impl Display for AnnotationSetID {
634    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
635        write!(f, "as-{:x}", self.0)
636    }
637}
638
639impl From<u64> for AnnotationSetID {
640    fn from(id: u64) -> Self {
641        AnnotationSetID(id)
642    }
643}
644
645impl From<AnnotationSetID> for u64 {
646    fn from(val: AnnotationSetID) -> Self {
647        val.0
648    }
649}
650
651impl AnnotationSetID {
652    pub fn value(&self) -> u64 {
653        self.0
654    }
655}
656
657impl TryFrom<&str> for AnnotationSetID {
658    type Error = Error;
659
660    fn try_from(s: &str) -> Result<Self, Self::Error> {
661        AnnotationSetID::from_str(s)
662    }
663}
664
665impl TryFrom<String> for AnnotationSetID {
666    type Error = Error;
667
668    fn try_from(s: String) -> Result<Self, Self::Error> {
669        AnnotationSetID::from_str(&s)
670    }
671}
672
673impl FromStr for AnnotationSetID {
674    type Err = Error;
675
676    fn from_str(s: &str) -> Result<Self, Self::Err> {
677        let hex_part = s.strip_prefix("as-").ok_or_else(|| {
678            Error::InvalidParameters("Annotation Set ID must start with 'as-' prefix".to_string())
679        })?;
680        let id = u64::from_str_radix(hex_part, 16)?;
681        Ok(AnnotationSetID(id))
682    }
683}
684
685#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
686pub struct SampleID(u64);
687
688impl Display for SampleID {
689    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
690        write!(f, "s-{:x}", self.0)
691    }
692}
693
694impl From<u64> for SampleID {
695    fn from(id: u64) -> Self {
696        SampleID(id)
697    }
698}
699
700impl From<SampleID> for u64 {
701    fn from(val: SampleID) -> Self {
702        val.0
703    }
704}
705
706impl SampleID {
707    pub fn value(&self) -> u64 {
708        self.0
709    }
710}
711
712impl TryFrom<&str> for SampleID {
713    type Error = Error;
714
715    fn try_from(s: &str) -> Result<Self, Self::Error> {
716        let hex_part = s.strip_prefix("s-").ok_or_else(|| {
717            Error::InvalidParameters("Sample ID must start with 's-' prefix".to_string())
718        })?;
719        let id = u64::from_str_radix(hex_part, 16)?;
720        Ok(SampleID(id))
721    }
722}
723
724#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
725pub struct AppId(u64);
726
727impl Display for AppId {
728    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
729        write!(f, "app-{:x}", self.0)
730    }
731}
732
733impl From<u64> for AppId {
734    fn from(id: u64) -> Self {
735        AppId(id)
736    }
737}
738
739impl From<AppId> for u64 {
740    fn from(val: AppId) -> Self {
741        val.0
742    }
743}
744
745impl AppId {
746    pub fn value(&self) -> u64 {
747        self.0
748    }
749}
750
751impl TryFrom<&str> for AppId {
752    type Error = Error;
753
754    fn try_from(s: &str) -> Result<Self, Self::Error> {
755        let hex_part = s.strip_prefix("app-").ok_or_else(|| {
756            Error::InvalidParameters("App ID must start with 'app-' prefix".to_string())
757        })?;
758        let id = u64::from_str_radix(hex_part, 16)?;
759        Ok(AppId(id))
760    }
761}
762
763#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
764pub struct ImageId(u64);
765
766impl Display for ImageId {
767    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
768        write!(f, "im-{:x}", self.0)
769    }
770}
771
772impl From<u64> for ImageId {
773    fn from(id: u64) -> Self {
774        ImageId(id)
775    }
776}
777
778impl From<ImageId> for u64 {
779    fn from(val: ImageId) -> Self {
780        val.0
781    }
782}
783
784impl ImageId {
785    pub fn value(&self) -> u64 {
786        self.0
787    }
788}
789
790impl TryFrom<&str> for ImageId {
791    type Error = Error;
792
793    fn try_from(s: &str) -> Result<Self, Self::Error> {
794        let hex_part = s.strip_prefix("im-").ok_or_else(|| {
795            Error::InvalidParameters("Image ID must start with 'im-' prefix".to_string())
796        })?;
797        let id = u64::from_str_radix(hex_part, 16)?;
798        Ok(ImageId(id))
799    }
800}
801
802#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)]
803pub struct SequenceId(u64);
804
805impl Display for SequenceId {
806    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
807        write!(f, "se-{:x}", self.0)
808    }
809}
810
811impl From<u64> for SequenceId {
812    fn from(id: u64) -> Self {
813        SequenceId(id)
814    }
815}
816
817impl From<SequenceId> for u64 {
818    fn from(val: SequenceId) -> Self {
819        val.0
820    }
821}
822
823impl SequenceId {
824    pub fn value(&self) -> u64 {
825        self.0
826    }
827}
828
829impl TryFrom<&str> for SequenceId {
830    type Error = Error;
831
832    fn try_from(s: &str) -> Result<Self, Self::Error> {
833        let hex_part = s.strip_prefix("se-").ok_or_else(|| {
834            Error::InvalidParameters("Sequence ID must start with 'se-' prefix".to_string())
835        })?;
836        let id = u64::from_str_radix(hex_part, 16)?;
837        Ok(SequenceId(id))
838    }
839}
840
841/// The project class represents a project in the EdgeFirst Studio.  A project
842/// contains datasets, experiments, and other resources related to a specific
843/// task or workflow.
844#[derive(Deserialize, Clone, Debug)]
845pub struct Project {
846    id: ProjectID,
847    name: String,
848    description: String,
849}
850
851impl Display for Project {
852    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
853        write!(f, "{} {}", self.id(), self.name())
854    }
855}
856
857impl Project {
858    pub fn id(&self) -> ProjectID {
859        self.id
860    }
861
862    pub fn name(&self) -> &str {
863        &self.name
864    }
865
866    pub fn description(&self) -> &str {
867        &self.description
868    }
869
870    pub async fn datasets(
871        &self,
872        client: &client::Client,
873        name: Option<&str>,
874    ) -> Result<Vec<Dataset>, Error> {
875        client.datasets(self.id, name).await
876    }
877
878    pub async fn experiments(
879        &self,
880        client: &client::Client,
881        name: Option<&str>,
882    ) -> Result<Vec<Experiment>, Error> {
883        client.experiments(self.id, name).await
884    }
885}
886
887#[derive(Deserialize, Debug)]
888pub struct SamplesCountResult {
889    pub total: u64,
890}
891
892#[derive(Serialize, Clone, Debug)]
893pub struct SamplesListParams {
894    pub dataset_id: DatasetID,
895    #[serde(skip_serializing_if = "Option::is_none")]
896    pub annotation_set_id: Option<AnnotationSetID>,
897    #[serde(skip_serializing_if = "Option::is_none")]
898    pub continue_token: Option<String>,
899    #[serde(skip_serializing_if = "Vec::is_empty")]
900    pub types: Vec<String>,
901    #[serde(skip_serializing_if = "Vec::is_empty")]
902    pub group_names: Vec<String>,
903}
904
905#[derive(Deserialize, Debug)]
906pub struct SamplesListResult {
907    pub samples: Vec<Sample>,
908    pub continue_token: Option<String>,
909}
910
911/// Parameters for populating (importing) samples into a dataset.
912///
913/// Used with the `samples.populate2` API to create new samples in a dataset,
914/// optionally with annotations and sensor data files.
915#[derive(Serialize, Clone, Debug)]
916pub struct SamplesPopulateParams {
917    pub dataset_id: DatasetID,
918    #[serde(skip_serializing_if = "Option::is_none")]
919    pub annotation_set_id: Option<AnnotationSetID>,
920    #[serde(skip_serializing_if = "Option::is_none")]
921    pub presigned_urls: Option<bool>,
922    pub samples: Vec<Sample>,
923}
924
925/// Result from the `samples.populate2` API call.
926///
927/// The API returns an array of populated sample results, one for each sample
928/// that was submitted. Each result contains the sample UUID and presigned URLs
929/// for uploading the associated files.
930#[derive(Deserialize, Debug, Clone)]
931pub struct SamplesPopulateResult {
932    /// UUID of the sample that was populated
933    pub uuid: String,
934    /// Presigned URLs for uploading files for this sample
935    pub urls: Vec<PresignedUrl>,
936}
937
938/// A presigned URL for uploading a file to S3.
939#[derive(Deserialize, Debug, Clone)]
940pub struct PresignedUrl {
941    /// Filename as specified in the sample
942    pub filename: String,
943    /// S3 key path
944    pub key: String,
945    /// Presigned URL for uploading (PUT request)
946    pub url: String,
947}
948
949// ============================================================================
950// Annotation API Types
951// ============================================================================
952
953/// Annotation data for the server-side `annotation.add_bulk` API.
954///
955/// This struct represents annotations in the format expected by the server,
956/// which differs from our client-side `Annotation` struct. Key differences:
957/// - Uses `image_id` (server) vs `sample_id` (client)
958/// - Uses `type` string ("box", "seg") vs `AnnotationType` enum
959/// - Coordinates are stored as separate `x`, `y`, `w`, `h` fields
960/// - Polygon is stored as a JSON string
961#[derive(Serialize, Clone, Debug)]
962pub struct ServerAnnotation {
963    /// Label ID (resolved from label name before sending)
964    #[serde(skip_serializing_if = "Option::is_none")]
965    pub label_id: Option<u64>,
966    /// Label index (alternative to label_id)
967    #[serde(skip_serializing_if = "Option::is_none")]
968    pub label_index: Option<u64>,
969    /// Label name (alternative to label_id)
970    #[serde(skip_serializing_if = "Option::is_none")]
971    pub label_name: Option<String>,
972    /// Annotation type: "box" for bounding box, "seg" for segmentation
973    #[serde(rename = "type")]
974    pub annotation_type: String,
975    /// Bounding box X coordinate (normalized 0-1, center)
976    pub x: f64,
977    /// Bounding box Y coordinate (normalized 0-1, center)
978    pub y: f64,
979    /// Bounding box width (normalized 0-1)
980    pub w: f64,
981    /// Bounding box height (normalized 0-1)
982    pub h: f64,
983    /// Confidence score (0-1)
984    pub score: f64,
985    /// Polygon data as JSON string (for segmentation)
986    #[serde(skip_serializing_if = "String::is_empty")]
987    pub polygon: String,
988    /// Image/sample ID in the database
989    pub image_id: u64,
990    /// Annotation set ID
991    pub annotation_set_id: u64,
992    /// Object tracking reference (optional)
993    #[serde(skip_serializing_if = "Option::is_none")]
994    pub object_reference: Option<String>,
995}
996
997/// Parameters for the `annotation.add_bulk` API.
998#[derive(Serialize, Debug)]
999pub struct AnnotationAddBulkParams {
1000    pub annotation_set_id: u64,
1001    pub annotations: Vec<ServerAnnotation>,
1002}
1003
1004/// Parameters for the `annotation.bulk.del` API.
1005#[derive(Serialize, Debug)]
1006pub struct AnnotationBulkDeleteParams {
1007    pub annotation_set_id: u64,
1008    pub annotation_types: Vec<String>,
1009    /// Image IDs to delete annotations from (required if delete_all is false)
1010    #[serde(skip_serializing_if = "Vec::is_empty")]
1011    pub image_ids: Vec<u64>,
1012    /// Delete all annotations of the specified types in the annotation set
1013    #[serde(skip_serializing_if = "Option::is_none")]
1014    pub delete_all: Option<bool>,
1015}
1016
1017#[derive(Deserialize)]
1018pub struct Snapshot {
1019    id: SnapshotID,
1020    description: String,
1021    status: String,
1022    path: String,
1023    #[serde(rename = "date")]
1024    created: DateTime<Utc>,
1025}
1026
1027impl Display for Snapshot {
1028    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1029        write!(f, "{} {}", self.id, self.description)
1030    }
1031}
1032
1033impl Snapshot {
1034    pub fn id(&self) -> SnapshotID {
1035        self.id
1036    }
1037
1038    pub fn description(&self) -> &str {
1039        &self.description
1040    }
1041
1042    pub fn status(&self) -> &str {
1043        &self.status
1044    }
1045
1046    pub fn path(&self) -> &str {
1047        &self.path
1048    }
1049
1050    pub fn created(&self) -> &DateTime<Utc> {
1051        &self.created
1052    }
1053}
1054
1055#[derive(Serialize, Debug)]
1056pub struct SnapshotRestore {
1057    pub project_id: ProjectID,
1058    pub snapshot_id: SnapshotID,
1059    pub fps: u64,
1060    #[serde(rename = "enabled_topics", skip_serializing_if = "Vec::is_empty")]
1061    pub topics: Vec<String>,
1062    #[serde(rename = "label_names", skip_serializing_if = "Vec::is_empty")]
1063    pub autolabel: Vec<String>,
1064    #[serde(rename = "depth_gen")]
1065    pub autodepth: bool,
1066    pub agtg_pipeline: bool,
1067    #[serde(skip_serializing_if = "Option::is_none")]
1068    pub dataset_name: Option<String>,
1069    #[serde(skip_serializing_if = "Option::is_none")]
1070    pub dataset_description: Option<String>,
1071}
1072
1073#[derive(Deserialize, Debug)]
1074pub struct SnapshotRestoreResult {
1075    pub id: SnapshotID,
1076    pub description: String,
1077    pub dataset_name: String,
1078    pub dataset_id: DatasetID,
1079    pub annotation_set_id: AnnotationSetID,
1080    #[serde(default)]
1081    pub task_id: Option<TaskID>,
1082    pub date: DateTime<Utc>,
1083}
1084
1085/// Parameters for creating a snapshot from an existing dataset on the server.
1086///
1087/// This is used with the `snapshots.create` RPC to trigger server-side snapshot
1088/// generation from dataset data (images + annotations).
1089#[derive(Serialize, Debug)]
1090pub struct SnapshotCreateFromDataset {
1091    /// Name/description for the snapshot
1092    pub description: String,
1093    /// Dataset ID to create snapshot from
1094    pub dataset_id: DatasetID,
1095    /// Annotation set ID to use for snapshot creation
1096    pub annotation_set_id: AnnotationSetID,
1097}
1098
1099/// Result of creating a snapshot from an existing dataset.
1100///
1101/// Contains the snapshot ID and task ID for monitoring progress.
1102#[derive(Deserialize, Debug)]
1103pub struct SnapshotFromDatasetResult {
1104    /// The created snapshot ID
1105    #[serde(alias = "snapshot_id")]
1106    pub id: SnapshotID,
1107    /// Task ID for monitoring snapshot creation progress
1108    #[serde(default)]
1109    pub task_id: Option<TaskID>,
1110}
1111
1112#[derive(Deserialize)]
1113pub struct Experiment {
1114    id: ExperimentID,
1115    project_id: ProjectID,
1116    name: String,
1117    description: String,
1118}
1119
1120impl Display for Experiment {
1121    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1122        write!(f, "{} {}", self.id, self.name)
1123    }
1124}
1125
1126impl Experiment {
1127    pub fn id(&self) -> ExperimentID {
1128        self.id
1129    }
1130
1131    pub fn project_id(&self) -> ProjectID {
1132        self.project_id
1133    }
1134
1135    pub fn name(&self) -> &str {
1136        &self.name
1137    }
1138
1139    pub fn description(&self) -> &str {
1140        &self.description
1141    }
1142
1143    pub async fn project(&self, client: &client::Client) -> Result<Project, Error> {
1144        client.project(self.project_id).await
1145    }
1146
1147    pub async fn training_sessions(
1148        &self,
1149        client: &client::Client,
1150        name: Option<&str>,
1151    ) -> Result<Vec<TrainingSession>, Error> {
1152        client.training_sessions(self.id, name).await
1153    }
1154}
1155
1156#[derive(Serialize, Debug)]
1157pub struct PublishMetrics {
1158    #[serde(rename = "trainer_session_id", skip_serializing_if = "Option::is_none")]
1159    pub trainer_session_id: Option<TrainingSessionID>,
1160    #[serde(
1161        rename = "validate_session_id",
1162        skip_serializing_if = "Option::is_none"
1163    )]
1164    pub validate_session_id: Option<ValidationSessionID>,
1165    pub metrics: HashMap<String, Parameter>,
1166}
1167
1168#[derive(Deserialize)]
1169struct TrainingSessionParams {
1170    model_params: HashMap<String, Parameter>,
1171    dataset_params: DatasetParams,
1172}
1173
1174#[derive(Deserialize)]
1175pub struct TrainingSession {
1176    id: TrainingSessionID,
1177    #[serde(rename = "trainer_id")]
1178    experiment_id: ExperimentID,
1179    model: String,
1180    name: String,
1181    description: String,
1182    params: TrainingSessionParams,
1183    #[serde(rename = "docker_task")]
1184    task: Task,
1185}
1186
1187impl Display for TrainingSession {
1188    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1189        write!(f, "{} {}", self.id, self.name())
1190    }
1191}
1192
1193impl TrainingSession {
1194    pub fn id(&self) -> TrainingSessionID {
1195        self.id
1196    }
1197
1198    pub fn name(&self) -> &str {
1199        &self.name
1200    }
1201
1202    pub fn description(&self) -> &str {
1203        &self.description
1204    }
1205
1206    pub fn model(&self) -> &str {
1207        &self.model
1208    }
1209
1210    pub fn experiment_id(&self) -> ExperimentID {
1211        self.experiment_id
1212    }
1213
1214    pub fn task(&self) -> Task {
1215        self.task.clone()
1216    }
1217
1218    pub fn model_params(&self) -> &HashMap<String, Parameter> {
1219        &self.params.model_params
1220    }
1221
1222    pub fn dataset_params(&self) -> &DatasetParams {
1223        &self.params.dataset_params
1224    }
1225
1226    pub fn train_group(&self) -> &str {
1227        &self.params.dataset_params.train_group
1228    }
1229
1230    pub fn val_group(&self) -> &str {
1231        &self.params.dataset_params.val_group
1232    }
1233
1234    pub async fn experiment(&self, client: &client::Client) -> Result<Experiment, Error> {
1235        client.experiment(self.experiment_id).await
1236    }
1237
1238    pub async fn dataset(&self, client: &client::Client) -> Result<Dataset, Error> {
1239        client.dataset(self.params.dataset_params.dataset_id).await
1240    }
1241
1242    pub async fn annotation_set(&self, client: &client::Client) -> Result<AnnotationSet, Error> {
1243        client
1244            .annotation_set(self.params.dataset_params.annotation_set_id)
1245            .await
1246    }
1247
1248    pub async fn artifacts(&self, client: &client::Client) -> Result<Vec<Artifact>, Error> {
1249        client.artifacts(self.id).await
1250    }
1251
1252    pub async fn metrics(
1253        &self,
1254        client: &client::Client,
1255    ) -> Result<HashMap<String, Parameter>, Error> {
1256        #[derive(Deserialize)]
1257        #[serde(untagged, deny_unknown_fields, expecting = "map, empty map or string")]
1258        enum Response {
1259            Empty {},
1260            Map(HashMap<String, Parameter>),
1261            String(String),
1262        }
1263
1264        let params = HashMap::from([("trainer_session_id", self.id().value())]);
1265        let resp: Response = client
1266            .rpc("trainer.session.metrics".to_owned(), Some(params))
1267            .await?;
1268
1269        Ok(match resp {
1270            Response::String(metrics) => serde_json::from_str(&metrics)?,
1271            Response::Map(metrics) => metrics,
1272            Response::Empty {} => HashMap::new(),
1273        })
1274    }
1275
1276    pub async fn set_metrics(
1277        &self,
1278        client: &client::Client,
1279        metrics: HashMap<String, Parameter>,
1280    ) -> Result<(), Error> {
1281        let metrics = PublishMetrics {
1282            trainer_session_id: Some(self.id()),
1283            validate_session_id: None,
1284            metrics,
1285        };
1286
1287        let _: String = client
1288            .rpc("trainer.session.metrics".to_owned(), Some(metrics))
1289            .await?;
1290
1291        Ok(())
1292    }
1293
1294    /// Downloads an artifact from the training session.
1295    pub async fn download_artifact(
1296        &self,
1297        client: &client::Client,
1298        filename: &str,
1299    ) -> Result<Vec<u8>, Error> {
1300        client
1301            .fetch(&format!(
1302                "download_model?training_session_id={}&file={}",
1303                self.id().value(),
1304                filename
1305            ))
1306            .await
1307    }
1308
1309    /// Uploads an artifact to the training session.  The filename will
1310    /// be used as the name of the file in the training session while path is
1311    /// the local path to the file to upload.
1312    pub async fn upload_artifact(
1313        &self,
1314        client: &client::Client,
1315        filename: &str,
1316        path: PathBuf,
1317    ) -> Result<(), Error> {
1318        self.upload(client, &[(format!("artifacts/{}", filename), path)])
1319            .await
1320    }
1321
1322    /// Downloads a checkpoint file from the training session.
1323    pub async fn download_checkpoint(
1324        &self,
1325        client: &client::Client,
1326        filename: &str,
1327    ) -> Result<Vec<u8>, Error> {
1328        client
1329            .fetch(&format!(
1330                "download_checkpoint?folder=checkpoints&training_session_id={}&file={}",
1331                self.id().value(),
1332                filename
1333            ))
1334            .await
1335    }
1336
1337    /// Uploads a checkpoint file to the training session.  The filename will
1338    /// be used as the name of the file in the training session while path is
1339    /// the local path to the file to upload.
1340    pub async fn upload_checkpoint(
1341        &self,
1342        client: &client::Client,
1343        filename: &str,
1344        path: PathBuf,
1345    ) -> Result<(), Error> {
1346        self.upload(client, &[(format!("checkpoints/{}", filename), path)])
1347            .await
1348    }
1349
1350    /// Downloads a file from the training session.  Should only be used for
1351    /// text files, binary files must be downloaded using download_artifact or
1352    /// download_checkpoint.
1353    pub async fn download(&self, client: &client::Client, filename: &str) -> Result<String, Error> {
1354        #[derive(Serialize)]
1355        struct DownloadRequest {
1356            session_id: TrainingSessionID,
1357            file_path: String,
1358        }
1359
1360        let params = DownloadRequest {
1361            session_id: self.id(),
1362            file_path: filename.to_string(),
1363        };
1364
1365        client
1366            .rpc("trainer.download.file".to_owned(), Some(params))
1367            .await
1368    }
1369
1370    pub async fn upload(
1371        &self,
1372        client: &client::Client,
1373        files: &[(String, PathBuf)],
1374    ) -> Result<(), Error> {
1375        let mut parts = Form::new().part(
1376            "params",
1377            Part::text(format!("{{ \"session_id\": {} }}", self.id().value())),
1378        );
1379
1380        for (name, path) in files {
1381            let file_part = Part::file(path).await?.file_name(name.to_owned());
1382            parts = parts.part("file", file_part);
1383        }
1384
1385        let result = client.post_multipart("trainer.upload.files", parts).await?;
1386        trace!("TrainingSession::upload: {:?}", result);
1387        Ok(())
1388    }
1389}
1390
1391#[derive(Deserialize, Clone, Debug)]
1392pub struct ValidationSession {
1393    id: ValidationSessionID,
1394    description: String,
1395    dataset_id: DatasetID,
1396    experiment_id: ExperimentID,
1397    training_session_id: TrainingSessionID,
1398    #[serde(rename = "gt_annotation_set_id")]
1399    annotation_set_id: AnnotationSetID,
1400    #[serde(deserialize_with = "validation_session_params")]
1401    params: HashMap<String, Parameter>,
1402    #[serde(rename = "docker_task")]
1403    task: Task,
1404}
1405
1406fn validation_session_params<'de, D>(
1407    deserializer: D,
1408) -> Result<HashMap<String, Parameter>, D::Error>
1409where
1410    D: Deserializer<'de>,
1411{
1412    #[derive(Deserialize)]
1413    struct ModelParams {
1414        validation: Option<HashMap<String, Parameter>>,
1415    }
1416
1417    #[derive(Deserialize)]
1418    struct ValidateParams {
1419        model: String,
1420    }
1421
1422    #[derive(Deserialize)]
1423    struct Params {
1424        model_params: ModelParams,
1425        validate_params: ValidateParams,
1426    }
1427
1428    let params = Params::deserialize(deserializer)?;
1429    let params = match params.model_params.validation {
1430        Some(mut map) => {
1431            map.insert(
1432                "model".to_string(),
1433                Parameter::String(params.validate_params.model),
1434            );
1435            map
1436        }
1437        None => HashMap::from([(
1438            "model".to_string(),
1439            Parameter::String(params.validate_params.model),
1440        )]),
1441    };
1442
1443    Ok(params)
1444}
1445
1446impl ValidationSession {
1447    pub fn id(&self) -> ValidationSessionID {
1448        self.id
1449    }
1450
1451    pub fn name(&self) -> &str {
1452        self.task.name()
1453    }
1454
1455    pub fn description(&self) -> &str {
1456        &self.description
1457    }
1458
1459    pub fn dataset_id(&self) -> DatasetID {
1460        self.dataset_id
1461    }
1462
1463    pub fn experiment_id(&self) -> ExperimentID {
1464        self.experiment_id
1465    }
1466
1467    pub fn training_session_id(&self) -> TrainingSessionID {
1468        self.training_session_id
1469    }
1470
1471    pub fn annotation_set_id(&self) -> AnnotationSetID {
1472        self.annotation_set_id
1473    }
1474
1475    pub fn params(&self) -> &HashMap<String, Parameter> {
1476        &self.params
1477    }
1478
1479    pub fn task(&self) -> &Task {
1480        &self.task
1481    }
1482
1483    pub async fn metrics(
1484        &self,
1485        client: &client::Client,
1486    ) -> Result<HashMap<String, Parameter>, Error> {
1487        #[derive(Deserialize)]
1488        #[serde(untagged, deny_unknown_fields, expecting = "map, empty map or string")]
1489        enum Response {
1490            Empty {},
1491            Map(HashMap<String, Parameter>),
1492            String(String),
1493        }
1494
1495        let params = HashMap::from([("validate_session_id", self.id().value())]);
1496        let resp: Response = client
1497            .rpc("validate.session.metrics".to_owned(), Some(params))
1498            .await?;
1499
1500        Ok(match resp {
1501            Response::String(metrics) => serde_json::from_str(&metrics)?,
1502            Response::Map(metrics) => metrics,
1503            Response::Empty {} => HashMap::new(),
1504        })
1505    }
1506
1507    pub async fn set_metrics(
1508        &self,
1509        client: &client::Client,
1510        metrics: HashMap<String, Parameter>,
1511    ) -> Result<(), Error> {
1512        let metrics = PublishMetrics {
1513            trainer_session_id: None,
1514            validate_session_id: Some(self.id()),
1515            metrics,
1516        };
1517
1518        let _: String = client
1519            .rpc("validate.session.metrics".to_owned(), Some(metrics))
1520            .await?;
1521
1522        Ok(())
1523    }
1524
1525    pub async fn upload(
1526        &self,
1527        client: &client::Client,
1528        files: &[(String, PathBuf)],
1529    ) -> Result<(), Error> {
1530        let mut parts = Form::new().part(
1531            "params",
1532            Part::text(format!("{{ \"session_id\": {} }}", self.id().value())),
1533        );
1534
1535        for (name, path) in files {
1536            let file_part = Part::file(path).await?.file_name(name.to_owned());
1537            parts = parts.part("file", file_part);
1538        }
1539
1540        let result = client
1541            .post_multipart("validate.upload.files", parts)
1542            .await?;
1543        trace!("ValidationSession::upload: {:?}", result);
1544        Ok(())
1545    }
1546}
1547
1548#[derive(Deserialize, Clone, Debug)]
1549pub struct DatasetParams {
1550    dataset_id: DatasetID,
1551    annotation_set_id: AnnotationSetID,
1552    #[serde(rename = "train_group_name")]
1553    train_group: String,
1554    #[serde(rename = "val_group_name")]
1555    val_group: String,
1556}
1557
1558impl DatasetParams {
1559    pub fn dataset_id(&self) -> DatasetID {
1560        self.dataset_id
1561    }
1562
1563    pub fn annotation_set_id(&self) -> AnnotationSetID {
1564        self.annotation_set_id
1565    }
1566
1567    pub fn train_group(&self) -> &str {
1568        &self.train_group
1569    }
1570
1571    pub fn val_group(&self) -> &str {
1572        &self.val_group
1573    }
1574}
1575
1576#[derive(Serialize, Debug, Clone)]
1577pub struct TasksListParams {
1578    #[serde(skip_serializing_if = "Option::is_none")]
1579    pub continue_token: Option<String>,
1580    #[serde(skip_serializing_if = "Option::is_none")]
1581    pub types: Option<Vec<String>>,
1582    #[serde(rename = "manage_types", skip_serializing_if = "Option::is_none")]
1583    pub manager: Option<Vec<String>>,
1584    #[serde(skip_serializing_if = "Option::is_none")]
1585    pub status: Option<Vec<String>>,
1586}
1587
1588#[derive(Deserialize, Debug, Clone)]
1589pub struct TasksListResult {
1590    pub tasks: Vec<Task>,
1591    pub continue_token: Option<String>,
1592}
1593
1594#[derive(Deserialize, Debug, Clone)]
1595pub struct Task {
1596    id: TaskID,
1597    name: String,
1598    #[serde(rename = "type")]
1599    workflow: String,
1600    status: String,
1601    #[serde(rename = "manage_type")]
1602    manager: Option<String>,
1603    #[serde(rename = "instance_type")]
1604    instance: String,
1605    #[serde(rename = "date")]
1606    created: DateTime<Utc>,
1607}
1608
1609impl Task {
1610    pub fn id(&self) -> TaskID {
1611        self.id
1612    }
1613
1614    pub fn name(&self) -> &str {
1615        &self.name
1616    }
1617
1618    pub fn workflow(&self) -> &str {
1619        &self.workflow
1620    }
1621
1622    pub fn status(&self) -> &str {
1623        &self.status
1624    }
1625
1626    pub fn manager(&self) -> Option<&str> {
1627        self.manager.as_deref()
1628    }
1629
1630    pub fn instance(&self) -> &str {
1631        &self.instance
1632    }
1633
1634    pub fn created(&self) -> &DateTime<Utc> {
1635        &self.created
1636    }
1637}
1638
1639impl Display for Task {
1640    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1641        write!(
1642            f,
1643            "{} [{:?} {}] {}",
1644            self.id,
1645            self.manager(),
1646            self.workflow(),
1647            self.name()
1648        )
1649    }
1650}
1651
1652#[derive(Deserialize, Debug)]
1653pub struct TaskInfo {
1654    id: TaskID,
1655    project_id: Option<ProjectID>,
1656    #[serde(rename = "task_description")]
1657    description: String,
1658    #[serde(rename = "type")]
1659    workflow: String,
1660    status: Option<String>,
1661    progress: TaskProgress,
1662    #[serde(rename = "created_date")]
1663    created: DateTime<Utc>,
1664    #[serde(rename = "end_date")]
1665    completed: DateTime<Utc>,
1666}
1667
1668impl Display for TaskInfo {
1669    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1670        write!(f, "{} {}: {}", self.id, self.workflow(), self.description())
1671    }
1672}
1673
1674impl TaskInfo {
1675    pub fn id(&self) -> TaskID {
1676        self.id
1677    }
1678
1679    pub fn project_id(&self) -> Option<ProjectID> {
1680        self.project_id
1681    }
1682
1683    pub fn description(&self) -> &str {
1684        &self.description
1685    }
1686
1687    pub fn workflow(&self) -> &str {
1688        &self.workflow
1689    }
1690
1691    pub fn status(&self) -> &Option<String> {
1692        &self.status
1693    }
1694
1695    pub async fn set_status(&mut self, client: &Client, status: &str) -> Result<(), Error> {
1696        let t = client.task_status(self.id(), status).await?;
1697        self.status = Some(t.status);
1698        Ok(())
1699    }
1700
1701    pub fn stages(&self) -> HashMap<String, Stage> {
1702        match &self.progress.stages {
1703            Some(stages) => stages.clone(),
1704            None => HashMap::new(),
1705        }
1706    }
1707
1708    pub async fn update_stage(
1709        &mut self,
1710        client: &Client,
1711        stage: &str,
1712        status: &str,
1713        message: &str,
1714        percentage: u8,
1715    ) -> Result<(), Error> {
1716        client
1717            .update_stage(self.id(), stage, status, message, percentage)
1718            .await?;
1719        let t = client.task_info(self.id()).await?;
1720        self.progress.stages = Some(t.progress.stages.unwrap_or_default());
1721        Ok(())
1722    }
1723
1724    pub async fn set_stages(
1725        &mut self,
1726        client: &Client,
1727        stages: &[(&str, &str)],
1728    ) -> Result<(), Error> {
1729        client.set_stages(self.id(), stages).await?;
1730        let t = client.task_info(self.id()).await?;
1731        self.progress.stages = Some(t.progress.stages.unwrap_or_default());
1732        Ok(())
1733    }
1734
1735    pub fn created(&self) -> &DateTime<Utc> {
1736        &self.created
1737    }
1738
1739    pub fn completed(&self) -> &DateTime<Utc> {
1740        &self.completed
1741    }
1742}
1743
1744#[derive(Deserialize, Debug)]
1745pub struct TaskProgress {
1746    stages: Option<HashMap<String, Stage>>,
1747}
1748
1749#[derive(Serialize, Debug, Clone)]
1750pub struct TaskStatus {
1751    #[serde(rename = "docker_task_id")]
1752    pub task_id: TaskID,
1753    pub status: String,
1754}
1755
1756#[derive(Serialize, Deserialize, Debug, Clone)]
1757pub struct Stage {
1758    #[serde(rename = "docker_task_id", skip_serializing_if = "Option::is_none")]
1759    task_id: Option<TaskID>,
1760    stage: String,
1761    #[serde(skip_serializing_if = "Option::is_none")]
1762    status: Option<String>,
1763    #[serde(skip_serializing_if = "Option::is_none")]
1764    description: Option<String>,
1765    #[serde(skip_serializing_if = "Option::is_none")]
1766    message: Option<String>,
1767    percentage: u8,
1768}
1769
1770impl Stage {
1771    pub fn new(
1772        task_id: Option<TaskID>,
1773        stage: String,
1774        status: Option<String>,
1775        message: Option<String>,
1776        percentage: u8,
1777    ) -> Self {
1778        Stage {
1779            task_id,
1780            stage,
1781            status,
1782            description: None,
1783            message,
1784            percentage,
1785        }
1786    }
1787
1788    pub fn task_id(&self) -> &Option<TaskID> {
1789        &self.task_id
1790    }
1791
1792    pub fn stage(&self) -> &str {
1793        &self.stage
1794    }
1795
1796    pub fn status(&self) -> &Option<String> {
1797        &self.status
1798    }
1799
1800    pub fn description(&self) -> &Option<String> {
1801        &self.description
1802    }
1803
1804    pub fn message(&self) -> &Option<String> {
1805        &self.message
1806    }
1807
1808    pub fn percentage(&self) -> u8 {
1809        self.percentage
1810    }
1811}
1812
1813#[derive(Serialize, Debug)]
1814pub struct TaskStages {
1815    #[serde(rename = "docker_task_id")]
1816    pub task_id: TaskID,
1817    #[serde(skip_serializing_if = "Vec::is_empty")]
1818    pub stages: Vec<HashMap<String, String>>,
1819}
1820
1821#[derive(Deserialize, Debug)]
1822pub struct Artifact {
1823    name: String,
1824    #[serde(rename = "modelType")]
1825    model_type: String,
1826}
1827
1828impl Artifact {
1829    pub fn name(&self) -> &str {
1830        &self.name
1831    }
1832
1833    pub fn model_type(&self) -> &str {
1834        &self.model_type
1835    }
1836}
1837
1838#[cfg(test)]
1839mod tests {
1840    use super::*;
1841
1842    // ========== OrganizationID Tests ==========
1843    #[test]
1844    fn test_organization_id_from_u64() {
1845        let id = OrganizationID::from(12345);
1846        assert_eq!(id.value(), 12345);
1847    }
1848
1849    #[test]
1850    fn test_organization_id_display() {
1851        let id = OrganizationID::from(0xabc123);
1852        assert_eq!(format!("{}", id), "org-abc123");
1853    }
1854
1855    #[test]
1856    fn test_organization_id_try_from_str_valid() {
1857        let id = OrganizationID::try_from("org-abc123").unwrap();
1858        assert_eq!(id.value(), 0xabc123);
1859    }
1860
1861    #[test]
1862    fn test_organization_id_try_from_str_invalid_prefix() {
1863        let result = OrganizationID::try_from("invalid-abc123");
1864        assert!(result.is_err());
1865        match result {
1866            Err(Error::InvalidParameters(msg)) => {
1867                assert!(msg.contains("must start with 'org-'"));
1868            }
1869            _ => panic!("Expected InvalidParameters error"),
1870        }
1871    }
1872
1873    #[test]
1874    fn test_organization_id_try_from_str_invalid_hex() {
1875        let result = OrganizationID::try_from("org-xyz");
1876        assert!(result.is_err());
1877    }
1878
1879    #[test]
1880    fn test_organization_id_try_from_str_empty() {
1881        let result = OrganizationID::try_from("org-");
1882        assert!(result.is_err());
1883    }
1884
1885    #[test]
1886    fn test_organization_id_into_u64() {
1887        let id = OrganizationID::from(54321);
1888        let value: u64 = id.into();
1889        assert_eq!(value, 54321);
1890    }
1891
1892    // ========== ProjectID Tests ==========
1893    #[test]
1894    fn test_project_id_from_u64() {
1895        let id = ProjectID::from(78910);
1896        assert_eq!(id.value(), 78910);
1897    }
1898
1899    #[test]
1900    fn test_project_id_display() {
1901        let id = ProjectID::from(0xdef456);
1902        assert_eq!(format!("{}", id), "p-def456");
1903    }
1904
1905    #[test]
1906    fn test_project_id_from_str_valid() {
1907        let id = ProjectID::from_str("p-def456").unwrap();
1908        assert_eq!(id.value(), 0xdef456);
1909    }
1910
1911    #[test]
1912    fn test_project_id_try_from_str_valid() {
1913        let id = ProjectID::try_from("p-123abc").unwrap();
1914        assert_eq!(id.value(), 0x123abc);
1915    }
1916
1917    #[test]
1918    fn test_project_id_try_from_string_valid() {
1919        let id = ProjectID::try_from("p-456def".to_string()).unwrap();
1920        assert_eq!(id.value(), 0x456def);
1921    }
1922
1923    #[test]
1924    fn test_project_id_from_str_invalid_prefix() {
1925        let result = ProjectID::from_str("proj-123");
1926        assert!(result.is_err());
1927        match result {
1928            Err(Error::InvalidParameters(msg)) => {
1929                assert!(msg.contains("must start with 'p-'"));
1930            }
1931            _ => panic!("Expected InvalidParameters error"),
1932        }
1933    }
1934
1935    #[test]
1936    fn test_project_id_from_str_invalid_hex() {
1937        let result = ProjectID::from_str("p-notahex");
1938        assert!(result.is_err());
1939    }
1940
1941    #[test]
1942    fn test_project_id_into_u64() {
1943        let id = ProjectID::from(99999);
1944        let value: u64 = id.into();
1945        assert_eq!(value, 99999);
1946    }
1947
1948    // ========== ExperimentID Tests ==========
1949    #[test]
1950    fn test_experiment_id_from_u64() {
1951        let id = ExperimentID::from(1193046);
1952        assert_eq!(id.value(), 1193046);
1953    }
1954
1955    #[test]
1956    fn test_experiment_id_display() {
1957        let id = ExperimentID::from(0x123abc);
1958        assert_eq!(format!("{}", id), "exp-123abc");
1959    }
1960
1961    #[test]
1962    fn test_experiment_id_from_str_valid() {
1963        let id = ExperimentID::from_str("exp-456def").unwrap();
1964        assert_eq!(id.value(), 0x456def);
1965    }
1966
1967    #[test]
1968    fn test_experiment_id_try_from_str_valid() {
1969        let id = ExperimentID::try_from("exp-789abc").unwrap();
1970        assert_eq!(id.value(), 0x789abc);
1971    }
1972
1973    #[test]
1974    fn test_experiment_id_try_from_string_valid() {
1975        let id = ExperimentID::try_from("exp-fedcba".to_string()).unwrap();
1976        assert_eq!(id.value(), 0xfedcba);
1977    }
1978
1979    #[test]
1980    fn test_experiment_id_from_str_invalid_prefix() {
1981        let result = ExperimentID::from_str("experiment-123");
1982        assert!(result.is_err());
1983        match result {
1984            Err(Error::InvalidParameters(msg)) => {
1985                assert!(msg.contains("must start with 'exp-'"));
1986            }
1987            _ => panic!("Expected InvalidParameters error"),
1988        }
1989    }
1990
1991    #[test]
1992    fn test_experiment_id_from_str_invalid_hex() {
1993        let result = ExperimentID::from_str("exp-zzz");
1994        assert!(result.is_err());
1995    }
1996
1997    #[test]
1998    fn test_experiment_id_into_u64() {
1999        let id = ExperimentID::from(777777);
2000        let value: u64 = id.into();
2001        assert_eq!(value, 777777);
2002    }
2003
2004    // ========== TrainingSessionID Tests ==========
2005    #[test]
2006    fn test_training_session_id_from_u64() {
2007        let id = TrainingSessionID::from(7901234);
2008        assert_eq!(id.value(), 7901234);
2009    }
2010
2011    #[test]
2012    fn test_training_session_id_display() {
2013        let id = TrainingSessionID::from(0xabc123);
2014        assert_eq!(format!("{}", id), "t-abc123");
2015    }
2016
2017    #[test]
2018    fn test_training_session_id_from_str_valid() {
2019        let id = TrainingSessionID::from_str("t-abc123").unwrap();
2020        assert_eq!(id.value(), 0xabc123);
2021    }
2022
2023    #[test]
2024    fn test_training_session_id_try_from_str_valid() {
2025        let id = TrainingSessionID::try_from("t-deadbeef").unwrap();
2026        assert_eq!(id.value(), 0xdeadbeef);
2027    }
2028
2029    #[test]
2030    fn test_training_session_id_try_from_string_valid() {
2031        let id = TrainingSessionID::try_from("t-cafebabe".to_string()).unwrap();
2032        assert_eq!(id.value(), 0xcafebabe);
2033    }
2034
2035    #[test]
2036    fn test_training_session_id_from_str_invalid_prefix() {
2037        let result = TrainingSessionID::from_str("training-123");
2038        assert!(result.is_err());
2039        match result {
2040            Err(Error::InvalidParameters(msg)) => {
2041                assert!(msg.contains("must start with 't-'"));
2042            }
2043            _ => panic!("Expected InvalidParameters error"),
2044        }
2045    }
2046
2047    #[test]
2048    fn test_training_session_id_from_str_invalid_hex() {
2049        let result = TrainingSessionID::from_str("t-qqq");
2050        assert!(result.is_err());
2051    }
2052
2053    #[test]
2054    fn test_training_session_id_into_u64() {
2055        let id = TrainingSessionID::from(123456);
2056        let value: u64 = id.into();
2057        assert_eq!(value, 123456);
2058    }
2059
2060    // ========== ValidationSessionID Tests ==========
2061    #[test]
2062    fn test_validation_session_id_from_u64() {
2063        let id = ValidationSessionID::from(3456789);
2064        assert_eq!(id.value(), 3456789);
2065    }
2066
2067    #[test]
2068    fn test_validation_session_id_display() {
2069        let id = ValidationSessionID::from(0x34c985);
2070        assert_eq!(format!("{}", id), "v-34c985");
2071    }
2072
2073    #[test]
2074    fn test_validation_session_id_try_from_str_valid() {
2075        let id = ValidationSessionID::try_from("v-deadbeef").unwrap();
2076        assert_eq!(id.value(), 0xdeadbeef);
2077    }
2078
2079    #[test]
2080    fn test_validation_session_id_try_from_string_valid() {
2081        let id = ValidationSessionID::try_from("v-12345678".to_string()).unwrap();
2082        assert_eq!(id.value(), 0x12345678);
2083    }
2084
2085    #[test]
2086    fn test_validation_session_id_try_from_str_invalid_prefix() {
2087        let result = ValidationSessionID::try_from("validation-123");
2088        assert!(result.is_err());
2089        match result {
2090            Err(Error::InvalidParameters(msg)) => {
2091                assert!(msg.contains("must start with 'v-'"));
2092            }
2093            _ => panic!("Expected InvalidParameters error"),
2094        }
2095    }
2096
2097    #[test]
2098    fn test_validation_session_id_try_from_str_invalid_hex() {
2099        let result = ValidationSessionID::try_from("v-xyz");
2100        assert!(result.is_err());
2101    }
2102
2103    #[test]
2104    fn test_validation_session_id_into_u64() {
2105        let id = ValidationSessionID::from(987654);
2106        let value: u64 = id.into();
2107        assert_eq!(value, 987654);
2108    }
2109
2110    // ========== SnapshotID Tests ==========
2111    #[test]
2112    fn test_snapshot_id_from_u64() {
2113        let id = SnapshotID::from(111222);
2114        assert_eq!(id.value(), 111222);
2115    }
2116
2117    #[test]
2118    fn test_snapshot_id_display() {
2119        let id = SnapshotID::from(0xaabbcc);
2120        assert_eq!(format!("{}", id), "ss-aabbcc");
2121    }
2122
2123    #[test]
2124    fn test_snapshot_id_try_from_str_valid() {
2125        let id = SnapshotID::try_from("ss-aabbcc").unwrap();
2126        assert_eq!(id.value(), 0xaabbcc);
2127    }
2128
2129    #[test]
2130    fn test_snapshot_id_try_from_str_invalid_prefix() {
2131        let result = SnapshotID::try_from("snapshot-123");
2132        assert!(result.is_err());
2133        match result {
2134            Err(Error::InvalidParameters(msg)) => {
2135                assert!(msg.contains("must start with 'ss-'"));
2136            }
2137            _ => panic!("Expected InvalidParameters error"),
2138        }
2139    }
2140
2141    #[test]
2142    fn test_snapshot_id_try_from_str_invalid_hex() {
2143        let result = SnapshotID::try_from("ss-ggg");
2144        assert!(result.is_err());
2145    }
2146
2147    #[test]
2148    fn test_snapshot_id_into_u64() {
2149        let id = SnapshotID::from(333444);
2150        let value: u64 = id.into();
2151        assert_eq!(value, 333444);
2152    }
2153
2154    // ========== TaskID Tests ==========
2155    #[test]
2156    fn test_task_id_from_u64() {
2157        let id = TaskID::from(555666);
2158        assert_eq!(id.value(), 555666);
2159    }
2160
2161    #[test]
2162    fn test_task_id_display() {
2163        let id = TaskID::from(0x123456);
2164        assert_eq!(format!("{}", id), "task-123456");
2165    }
2166
2167    #[test]
2168    fn test_task_id_from_str_valid() {
2169        let id = TaskID::from_str("task-123456").unwrap();
2170        assert_eq!(id.value(), 0x123456);
2171    }
2172
2173    #[test]
2174    fn test_task_id_try_from_str_valid() {
2175        let id = TaskID::try_from("task-abcdef").unwrap();
2176        assert_eq!(id.value(), 0xabcdef);
2177    }
2178
2179    #[test]
2180    fn test_task_id_try_from_string_valid() {
2181        let id = TaskID::try_from("task-fedcba".to_string()).unwrap();
2182        assert_eq!(id.value(), 0xfedcba);
2183    }
2184
2185    #[test]
2186    fn test_task_id_from_str_invalid_prefix() {
2187        let result = TaskID::from_str("t-123");
2188        assert!(result.is_err());
2189        match result {
2190            Err(Error::InvalidParameters(msg)) => {
2191                assert!(msg.contains("must start with 'task-'"));
2192            }
2193            _ => panic!("Expected InvalidParameters error"),
2194        }
2195    }
2196
2197    #[test]
2198    fn test_task_id_from_str_invalid_hex() {
2199        let result = TaskID::from_str("task-zzz");
2200        assert!(result.is_err());
2201    }
2202
2203    #[test]
2204    fn test_task_id_into_u64() {
2205        let id = TaskID::from(777888);
2206        let value: u64 = id.into();
2207        assert_eq!(value, 777888);
2208    }
2209
2210    // ========== DatasetID Tests ==========
2211    #[test]
2212    fn test_dataset_id_from_u64() {
2213        let id = DatasetID::from(1193046);
2214        assert_eq!(id.value(), 1193046);
2215    }
2216
2217    #[test]
2218    fn test_dataset_id_display() {
2219        let id = DatasetID::from(0x123abc);
2220        assert_eq!(format!("{}", id), "ds-123abc");
2221    }
2222
2223    #[test]
2224    fn test_dataset_id_from_str_valid() {
2225        let id = DatasetID::from_str("ds-456def").unwrap();
2226        assert_eq!(id.value(), 0x456def);
2227    }
2228
2229    #[test]
2230    fn test_dataset_id_try_from_str_valid() {
2231        let id = DatasetID::try_from("ds-789abc").unwrap();
2232        assert_eq!(id.value(), 0x789abc);
2233    }
2234
2235    #[test]
2236    fn test_dataset_id_try_from_string_valid() {
2237        let id = DatasetID::try_from("ds-fedcba".to_string()).unwrap();
2238        assert_eq!(id.value(), 0xfedcba);
2239    }
2240
2241    #[test]
2242    fn test_dataset_id_from_str_invalid_prefix() {
2243        let result = DatasetID::from_str("dataset-123");
2244        assert!(result.is_err());
2245        match result {
2246            Err(Error::InvalidParameters(msg)) => {
2247                assert!(msg.contains("must start with 'ds-'"));
2248            }
2249            _ => panic!("Expected InvalidParameters error"),
2250        }
2251    }
2252
2253    #[test]
2254    fn test_dataset_id_from_str_invalid_hex() {
2255        let result = DatasetID::from_str("ds-zzz");
2256        assert!(result.is_err());
2257    }
2258
2259    #[test]
2260    fn test_dataset_id_into_u64() {
2261        let id = DatasetID::from(111111);
2262        let value: u64 = id.into();
2263        assert_eq!(value, 111111);
2264    }
2265
2266    // ========== AnnotationSetID Tests ==========
2267    #[test]
2268    fn test_annotation_set_id_from_u64() {
2269        let id = AnnotationSetID::from(222333);
2270        assert_eq!(id.value(), 222333);
2271    }
2272
2273    #[test]
2274    fn test_annotation_set_id_display() {
2275        let id = AnnotationSetID::from(0xabcdef);
2276        assert_eq!(format!("{}", id), "as-abcdef");
2277    }
2278
2279    #[test]
2280    fn test_annotation_set_id_from_str_valid() {
2281        let id = AnnotationSetID::from_str("as-abcdef").unwrap();
2282        assert_eq!(id.value(), 0xabcdef);
2283    }
2284
2285    #[test]
2286    fn test_annotation_set_id_try_from_str_valid() {
2287        let id = AnnotationSetID::try_from("as-123456").unwrap();
2288        assert_eq!(id.value(), 0x123456);
2289    }
2290
2291    #[test]
2292    fn test_annotation_set_id_try_from_string_valid() {
2293        let id = AnnotationSetID::try_from("as-fedcba".to_string()).unwrap();
2294        assert_eq!(id.value(), 0xfedcba);
2295    }
2296
2297    #[test]
2298    fn test_annotation_set_id_from_str_invalid_prefix() {
2299        let result = AnnotationSetID::from_str("annotation-123");
2300        assert!(result.is_err());
2301        match result {
2302            Err(Error::InvalidParameters(msg)) => {
2303                assert!(msg.contains("must start with 'as-'"));
2304            }
2305            _ => panic!("Expected InvalidParameters error"),
2306        }
2307    }
2308
2309    #[test]
2310    fn test_annotation_set_id_from_str_invalid_hex() {
2311        let result = AnnotationSetID::from_str("as-zzz");
2312        assert!(result.is_err());
2313    }
2314
2315    #[test]
2316    fn test_annotation_set_id_into_u64() {
2317        let id = AnnotationSetID::from(444555);
2318        let value: u64 = id.into();
2319        assert_eq!(value, 444555);
2320    }
2321
2322    // ========== SampleID Tests ==========
2323    #[test]
2324    fn test_sample_id_from_u64() {
2325        let id = SampleID::from(666777);
2326        assert_eq!(id.value(), 666777);
2327    }
2328
2329    #[test]
2330    fn test_sample_id_display() {
2331        let id = SampleID::from(0x987654);
2332        assert_eq!(format!("{}", id), "s-987654");
2333    }
2334
2335    #[test]
2336    fn test_sample_id_try_from_str_valid() {
2337        let id = SampleID::try_from("s-987654").unwrap();
2338        assert_eq!(id.value(), 0x987654);
2339    }
2340
2341    #[test]
2342    fn test_sample_id_try_from_str_invalid_prefix() {
2343        let result = SampleID::try_from("sample-123");
2344        assert!(result.is_err());
2345        match result {
2346            Err(Error::InvalidParameters(msg)) => {
2347                assert!(msg.contains("must start with 's-'"));
2348            }
2349            _ => panic!("Expected InvalidParameters error"),
2350        }
2351    }
2352
2353    #[test]
2354    fn test_sample_id_try_from_str_invalid_hex() {
2355        let result = SampleID::try_from("s-zzz");
2356        assert!(result.is_err());
2357    }
2358
2359    #[test]
2360    fn test_sample_id_into_u64() {
2361        let id = SampleID::from(888999);
2362        let value: u64 = id.into();
2363        assert_eq!(value, 888999);
2364    }
2365
2366    // ========== AppId Tests ==========
2367    #[test]
2368    fn test_app_id_from_u64() {
2369        let id = AppId::from(123123);
2370        assert_eq!(id.value(), 123123);
2371    }
2372
2373    #[test]
2374    fn test_app_id_display() {
2375        let id = AppId::from(0x456789);
2376        assert_eq!(format!("{}", id), "app-456789");
2377    }
2378
2379    #[test]
2380    fn test_app_id_try_from_str_valid() {
2381        let id = AppId::try_from("app-456789").unwrap();
2382        assert_eq!(id.value(), 0x456789);
2383    }
2384
2385    #[test]
2386    fn test_app_id_try_from_str_invalid_prefix() {
2387        let result = AppId::try_from("application-123");
2388        assert!(result.is_err());
2389        match result {
2390            Err(Error::InvalidParameters(msg)) => {
2391                assert!(msg.contains("must start with 'app-'"));
2392            }
2393            _ => panic!("Expected InvalidParameters error"),
2394        }
2395    }
2396
2397    #[test]
2398    fn test_app_id_try_from_str_invalid_hex() {
2399        let result = AppId::try_from("app-zzz");
2400        assert!(result.is_err());
2401    }
2402
2403    #[test]
2404    fn test_app_id_into_u64() {
2405        let id = AppId::from(321321);
2406        let value: u64 = id.into();
2407        assert_eq!(value, 321321);
2408    }
2409
2410    // ========== ImageId Tests ==========
2411    #[test]
2412    fn test_image_id_from_u64() {
2413        let id = ImageId::from(789789);
2414        assert_eq!(id.value(), 789789);
2415    }
2416
2417    #[test]
2418    fn test_image_id_display() {
2419        let id = ImageId::from(0xabcd1234);
2420        assert_eq!(format!("{}", id), "im-abcd1234");
2421    }
2422
2423    #[test]
2424    fn test_image_id_try_from_str_valid() {
2425        let id = ImageId::try_from("im-abcd1234").unwrap();
2426        assert_eq!(id.value(), 0xabcd1234);
2427    }
2428
2429    #[test]
2430    fn test_image_id_try_from_str_invalid_prefix() {
2431        let result = ImageId::try_from("image-123");
2432        assert!(result.is_err());
2433        match result {
2434            Err(Error::InvalidParameters(msg)) => {
2435                assert!(msg.contains("must start with 'im-'"));
2436            }
2437            _ => panic!("Expected InvalidParameters error"),
2438        }
2439    }
2440
2441    #[test]
2442    fn test_image_id_try_from_str_invalid_hex() {
2443        let result = ImageId::try_from("im-zzz");
2444        assert!(result.is_err());
2445    }
2446
2447    #[test]
2448    fn test_image_id_into_u64() {
2449        let id = ImageId::from(987987);
2450        let value: u64 = id.into();
2451        assert_eq!(value, 987987);
2452    }
2453
2454    // ========== ID Type Hash and Equality Tests ==========
2455    #[test]
2456    fn test_id_types_equality() {
2457        let id1 = ProjectID::from(12345);
2458        let id2 = ProjectID::from(12345);
2459        let id3 = ProjectID::from(54321);
2460
2461        assert_eq!(id1, id2);
2462        assert_ne!(id1, id3);
2463    }
2464
2465    #[test]
2466    fn test_id_types_hash() {
2467        use std::collections::HashSet;
2468
2469        let mut set = HashSet::new();
2470        set.insert(DatasetID::from(100));
2471        set.insert(DatasetID::from(200));
2472        set.insert(DatasetID::from(100)); // duplicate
2473
2474        assert_eq!(set.len(), 2);
2475        assert!(set.contains(&DatasetID::from(100)));
2476        assert!(set.contains(&DatasetID::from(200)));
2477    }
2478
2479    #[test]
2480    fn test_id_types_copy_clone() {
2481        let id1 = ExperimentID::from(999);
2482        let id2 = id1; // Copy
2483        let id3 = id1; // Also Copy (no need for clone())
2484
2485        assert_eq!(id1, id2);
2486        assert_eq!(id1, id3);
2487    }
2488
2489    // ========== Edge Cases ==========
2490    #[test]
2491    fn test_id_zero_value() {
2492        let id = ProjectID::from(0);
2493        assert_eq!(format!("{}", id), "p-0");
2494        assert_eq!(id.value(), 0);
2495    }
2496
2497    #[test]
2498    fn test_id_max_value() {
2499        let id = ProjectID::from(u64::MAX);
2500        assert_eq!(format!("{}", id), "p-ffffffffffffffff");
2501        assert_eq!(id.value(), u64::MAX);
2502    }
2503
2504    #[test]
2505    fn test_id_round_trip_conversion() {
2506        let original = 0xdeadbeef_u64;
2507        let id = TrainingSessionID::from(original);
2508        let back: u64 = id.into();
2509        assert_eq!(original, back);
2510    }
2511
2512    #[test]
2513    fn test_id_case_insensitive_hex() {
2514        // Hexadecimal parsing should handle both upper and lowercase
2515        let id1 = DatasetID::from_str("ds-ABCDEF").unwrap();
2516        let id2 = DatasetID::from_str("ds-abcdef").unwrap();
2517        assert_eq!(id1.value(), id2.value());
2518    }
2519
2520    #[test]
2521    fn test_id_with_leading_zeros() {
2522        let id = ProjectID::from_str("p-00001234").unwrap();
2523        assert_eq!(id.value(), 0x1234);
2524    }
2525
2526    // ========== Parameter Tests ==========
2527    #[test]
2528    fn test_parameter_integer() {
2529        let param = Parameter::Integer(42);
2530        match param {
2531            Parameter::Integer(val) => assert_eq!(val, 42),
2532            _ => panic!("Expected Integer variant"),
2533        }
2534    }
2535
2536    #[test]
2537    fn test_parameter_real() {
2538        let param = Parameter::Real(2.5);
2539        match param {
2540            Parameter::Real(val) => assert_eq!(val, 2.5),
2541            _ => panic!("Expected Real variant"),
2542        }
2543    }
2544
2545    #[test]
2546    fn test_parameter_boolean() {
2547        let param = Parameter::Boolean(true);
2548        match param {
2549            Parameter::Boolean(val) => assert!(val),
2550            _ => panic!("Expected Boolean variant"),
2551        }
2552    }
2553
2554    #[test]
2555    fn test_parameter_string() {
2556        let param = Parameter::String("test".to_string());
2557        match param {
2558            Parameter::String(val) => assert_eq!(val, "test"),
2559            _ => panic!("Expected String variant"),
2560        }
2561    }
2562
2563    #[test]
2564    fn test_parameter_array() {
2565        let param = Parameter::Array(vec![
2566            Parameter::Integer(1),
2567            Parameter::Integer(2),
2568            Parameter::Integer(3),
2569        ]);
2570        match param {
2571            Parameter::Array(arr) => assert_eq!(arr.len(), 3),
2572            _ => panic!("Expected Array variant"),
2573        }
2574    }
2575
2576    #[test]
2577    fn test_parameter_object() {
2578        let mut map = HashMap::new();
2579        map.insert("key".to_string(), Parameter::Integer(100));
2580        let param = Parameter::Object(map);
2581        match param {
2582            Parameter::Object(obj) => {
2583                assert_eq!(obj.len(), 1);
2584                assert!(obj.contains_key("key"));
2585            }
2586            _ => panic!("Expected Object variant"),
2587        }
2588    }
2589
2590    #[test]
2591    fn test_parameter_clone() {
2592        let param1 = Parameter::Integer(42);
2593        let param2 = param1.clone();
2594        assert_eq!(param1, param2);
2595    }
2596
2597    #[test]
2598    fn test_parameter_nested() {
2599        let inner_array = Parameter::Array(vec![Parameter::Integer(1), Parameter::Integer(2)]);
2600        let outer_array = Parameter::Array(vec![inner_array.clone(), inner_array]);
2601
2602        match outer_array {
2603            Parameter::Array(arr) => {
2604                assert_eq!(arr.len(), 2);
2605            }
2606            _ => panic!("Expected Array variant"),
2607        }
2608    }
2609}