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)]
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#[derive(Deserialize)]
950pub struct Snapshot {
951    id: SnapshotID,
952    description: String,
953    status: String,
954    path: String,
955    #[serde(rename = "date")]
956    created: DateTime<Utc>,
957}
958
959impl Display for Snapshot {
960    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
961        write!(f, "{} {}", self.id, self.description)
962    }
963}
964
965impl Snapshot {
966    pub fn id(&self) -> SnapshotID {
967        self.id
968    }
969
970    pub fn description(&self) -> &str {
971        &self.description
972    }
973
974    pub fn status(&self) -> &str {
975        &self.status
976    }
977
978    pub fn path(&self) -> &str {
979        &self.path
980    }
981
982    pub fn created(&self) -> &DateTime<Utc> {
983        &self.created
984    }
985}
986
987#[derive(Serialize, Debug)]
988pub struct SnapshotRestore {
989    pub project_id: ProjectID,
990    pub snapshot_id: SnapshotID,
991    pub fps: u64,
992    #[serde(rename = "enabled_topics", skip_serializing_if = "Vec::is_empty")]
993    pub topics: Vec<String>,
994    #[serde(rename = "label_names", skip_serializing_if = "Vec::is_empty")]
995    pub autolabel: Vec<String>,
996    #[serde(rename = "depth_gen")]
997    pub autodepth: bool,
998    pub agtg_pipeline: bool,
999    #[serde(skip_serializing_if = "Option::is_none")]
1000    pub dataset_name: Option<String>,
1001    #[serde(skip_serializing_if = "Option::is_none")]
1002    pub dataset_description: Option<String>,
1003}
1004
1005#[derive(Deserialize, Debug)]
1006pub struct SnapshotRestoreResult {
1007    pub id: SnapshotID,
1008    pub description: String,
1009    pub dataset_name: String,
1010    pub dataset_id: DatasetID,
1011    pub annotation_set_id: AnnotationSetID,
1012    #[serde(default)]
1013    pub task_id: Option<TaskID>,
1014    pub date: DateTime<Utc>,
1015}
1016
1017#[derive(Deserialize)]
1018pub struct Experiment {
1019    id: ExperimentID,
1020    project_id: ProjectID,
1021    name: String,
1022    description: String,
1023}
1024
1025impl Display for Experiment {
1026    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1027        write!(f, "{} {}", self.id, self.name)
1028    }
1029}
1030
1031impl Experiment {
1032    pub fn id(&self) -> ExperimentID {
1033        self.id
1034    }
1035
1036    pub fn project_id(&self) -> ProjectID {
1037        self.project_id
1038    }
1039
1040    pub fn name(&self) -> &str {
1041        &self.name
1042    }
1043
1044    pub fn description(&self) -> &str {
1045        &self.description
1046    }
1047
1048    pub async fn project(&self, client: &client::Client) -> Result<Project, Error> {
1049        client.project(self.project_id).await
1050    }
1051
1052    pub async fn training_sessions(
1053        &self,
1054        client: &client::Client,
1055        name: Option<&str>,
1056    ) -> Result<Vec<TrainingSession>, Error> {
1057        client.training_sessions(self.id, name).await
1058    }
1059}
1060
1061#[derive(Serialize, Debug)]
1062pub struct PublishMetrics {
1063    #[serde(rename = "trainer_session_id", skip_serializing_if = "Option::is_none")]
1064    pub trainer_session_id: Option<TrainingSessionID>,
1065    #[serde(
1066        rename = "validate_session_id",
1067        skip_serializing_if = "Option::is_none"
1068    )]
1069    pub validate_session_id: Option<ValidationSessionID>,
1070    pub metrics: HashMap<String, Parameter>,
1071}
1072
1073#[derive(Deserialize)]
1074struct TrainingSessionParams {
1075    model_params: HashMap<String, Parameter>,
1076    dataset_params: DatasetParams,
1077}
1078
1079#[derive(Deserialize)]
1080pub struct TrainingSession {
1081    id: TrainingSessionID,
1082    #[serde(rename = "trainer_id")]
1083    experiment_id: ExperimentID,
1084    model: String,
1085    name: String,
1086    description: String,
1087    params: TrainingSessionParams,
1088    #[serde(rename = "docker_task")]
1089    task: Task,
1090}
1091
1092impl Display for TrainingSession {
1093    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1094        write!(f, "{} {}", self.id, self.name())
1095    }
1096}
1097
1098impl TrainingSession {
1099    pub fn id(&self) -> TrainingSessionID {
1100        self.id
1101    }
1102
1103    pub fn name(&self) -> &str {
1104        &self.name
1105    }
1106
1107    pub fn description(&self) -> &str {
1108        &self.description
1109    }
1110
1111    pub fn model(&self) -> &str {
1112        &self.model
1113    }
1114
1115    pub fn experiment_id(&self) -> ExperimentID {
1116        self.experiment_id
1117    }
1118
1119    pub fn task(&self) -> Task {
1120        self.task.clone()
1121    }
1122
1123    pub fn model_params(&self) -> &HashMap<String, Parameter> {
1124        &self.params.model_params
1125    }
1126
1127    pub fn dataset_params(&self) -> &DatasetParams {
1128        &self.params.dataset_params
1129    }
1130
1131    pub fn train_group(&self) -> &str {
1132        &self.params.dataset_params.train_group
1133    }
1134
1135    pub fn val_group(&self) -> &str {
1136        &self.params.dataset_params.val_group
1137    }
1138
1139    pub async fn experiment(&self, client: &client::Client) -> Result<Experiment, Error> {
1140        client.experiment(self.experiment_id).await
1141    }
1142
1143    pub async fn dataset(&self, client: &client::Client) -> Result<Dataset, Error> {
1144        client.dataset(self.params.dataset_params.dataset_id).await
1145    }
1146
1147    pub async fn annotation_set(&self, client: &client::Client) -> Result<AnnotationSet, Error> {
1148        client
1149            .annotation_set(self.params.dataset_params.annotation_set_id)
1150            .await
1151    }
1152
1153    pub async fn artifacts(&self, client: &client::Client) -> Result<Vec<Artifact>, Error> {
1154        client.artifacts(self.id).await
1155    }
1156
1157    pub async fn metrics(
1158        &self,
1159        client: &client::Client,
1160    ) -> Result<HashMap<String, Parameter>, Error> {
1161        #[derive(Deserialize)]
1162        #[serde(untagged, deny_unknown_fields, expecting = "map, empty map or string")]
1163        enum Response {
1164            Empty {},
1165            Map(HashMap<String, Parameter>),
1166            String(String),
1167        }
1168
1169        let params = HashMap::from([("trainer_session_id", self.id().value())]);
1170        let resp: Response = client
1171            .rpc("trainer.session.metrics".to_owned(), Some(params))
1172            .await?;
1173
1174        Ok(match resp {
1175            Response::String(metrics) => serde_json::from_str(&metrics)?,
1176            Response::Map(metrics) => metrics,
1177            Response::Empty {} => HashMap::new(),
1178        })
1179    }
1180
1181    pub async fn set_metrics(
1182        &self,
1183        client: &client::Client,
1184        metrics: HashMap<String, Parameter>,
1185    ) -> Result<(), Error> {
1186        let metrics = PublishMetrics {
1187            trainer_session_id: Some(self.id()),
1188            validate_session_id: None,
1189            metrics,
1190        };
1191
1192        let _: String = client
1193            .rpc("trainer.session.metrics".to_owned(), Some(metrics))
1194            .await?;
1195
1196        Ok(())
1197    }
1198
1199    /// Downloads an artifact from the training session.
1200    pub async fn download_artifact(
1201        &self,
1202        client: &client::Client,
1203        filename: &str,
1204    ) -> Result<Vec<u8>, Error> {
1205        client
1206            .fetch(&format!(
1207                "download_model?training_session_id={}&file={}",
1208                self.id().value(),
1209                filename
1210            ))
1211            .await
1212    }
1213
1214    /// Uploads an artifact to the training session.  The filename will
1215    /// be used as the name of the file in the training session while path is
1216    /// the local path to the file to upload.
1217    pub async fn upload_artifact(
1218        &self,
1219        client: &client::Client,
1220        filename: &str,
1221        path: PathBuf,
1222    ) -> Result<(), Error> {
1223        self.upload(client, &[(format!("artifacts/{}", filename), path)])
1224            .await
1225    }
1226
1227    /// Downloads a checkpoint file from the training session.
1228    pub async fn download_checkpoint(
1229        &self,
1230        client: &client::Client,
1231        filename: &str,
1232    ) -> Result<Vec<u8>, Error> {
1233        client
1234            .fetch(&format!(
1235                "download_checkpoint?folder=checkpoints&training_session_id={}&file={}",
1236                self.id().value(),
1237                filename
1238            ))
1239            .await
1240    }
1241
1242    /// Uploads a checkpoint file to the training session.  The filename will
1243    /// be used as the name of the file in the training session while path is
1244    /// the local path to the file to upload.
1245    pub async fn upload_checkpoint(
1246        &self,
1247        client: &client::Client,
1248        filename: &str,
1249        path: PathBuf,
1250    ) -> Result<(), Error> {
1251        self.upload(client, &[(format!("checkpoints/{}", filename), path)])
1252            .await
1253    }
1254
1255    /// Downloads a file from the training session.  Should only be used for
1256    /// text files, binary files must be downloaded using download_artifact or
1257    /// download_checkpoint.
1258    pub async fn download(&self, client: &client::Client, filename: &str) -> Result<String, Error> {
1259        #[derive(Serialize)]
1260        struct DownloadRequest {
1261            session_id: TrainingSessionID,
1262            file_path: String,
1263        }
1264
1265        let params = DownloadRequest {
1266            session_id: self.id(),
1267            file_path: filename.to_string(),
1268        };
1269
1270        client
1271            .rpc("trainer.download.file".to_owned(), Some(params))
1272            .await
1273    }
1274
1275    pub async fn upload(
1276        &self,
1277        client: &client::Client,
1278        files: &[(String, PathBuf)],
1279    ) -> Result<(), Error> {
1280        let mut parts = Form::new().part(
1281            "params",
1282            Part::text(format!("{{ \"session_id\": {} }}", self.id().value())),
1283        );
1284
1285        for (name, path) in files {
1286            let file_part = Part::file(path).await?.file_name(name.to_owned());
1287            parts = parts.part("file", file_part);
1288        }
1289
1290        let result = client.post_multipart("trainer.upload.files", parts).await?;
1291        trace!("TrainingSession::upload: {:?}", result);
1292        Ok(())
1293    }
1294}
1295
1296#[derive(Deserialize, Clone, Debug)]
1297pub struct ValidationSession {
1298    id: ValidationSessionID,
1299    description: String,
1300    dataset_id: DatasetID,
1301    experiment_id: ExperimentID,
1302    training_session_id: TrainingSessionID,
1303    #[serde(rename = "gt_annotation_set_id")]
1304    annotation_set_id: AnnotationSetID,
1305    #[serde(deserialize_with = "validation_session_params")]
1306    params: HashMap<String, Parameter>,
1307    #[serde(rename = "docker_task")]
1308    task: Task,
1309}
1310
1311fn validation_session_params<'de, D>(
1312    deserializer: D,
1313) -> Result<HashMap<String, Parameter>, D::Error>
1314where
1315    D: Deserializer<'de>,
1316{
1317    #[derive(Deserialize)]
1318    struct ModelParams {
1319        validation: Option<HashMap<String, Parameter>>,
1320    }
1321
1322    #[derive(Deserialize)]
1323    struct ValidateParams {
1324        model: String,
1325    }
1326
1327    #[derive(Deserialize)]
1328    struct Params {
1329        model_params: ModelParams,
1330        validate_params: ValidateParams,
1331    }
1332
1333    let params = Params::deserialize(deserializer)?;
1334    let params = match params.model_params.validation {
1335        Some(mut map) => {
1336            map.insert(
1337                "model".to_string(),
1338                Parameter::String(params.validate_params.model),
1339            );
1340            map
1341        }
1342        None => HashMap::from([(
1343            "model".to_string(),
1344            Parameter::String(params.validate_params.model),
1345        )]),
1346    };
1347
1348    Ok(params)
1349}
1350
1351impl ValidationSession {
1352    pub fn id(&self) -> ValidationSessionID {
1353        self.id
1354    }
1355
1356    pub fn name(&self) -> &str {
1357        self.task.name()
1358    }
1359
1360    pub fn description(&self) -> &str {
1361        &self.description
1362    }
1363
1364    pub fn dataset_id(&self) -> DatasetID {
1365        self.dataset_id
1366    }
1367
1368    pub fn experiment_id(&self) -> ExperimentID {
1369        self.experiment_id
1370    }
1371
1372    pub fn training_session_id(&self) -> TrainingSessionID {
1373        self.training_session_id
1374    }
1375
1376    pub fn annotation_set_id(&self) -> AnnotationSetID {
1377        self.annotation_set_id
1378    }
1379
1380    pub fn params(&self) -> &HashMap<String, Parameter> {
1381        &self.params
1382    }
1383
1384    pub fn task(&self) -> &Task {
1385        &self.task
1386    }
1387
1388    pub async fn metrics(
1389        &self,
1390        client: &client::Client,
1391    ) -> Result<HashMap<String, Parameter>, Error> {
1392        #[derive(Deserialize)]
1393        #[serde(untagged, deny_unknown_fields, expecting = "map, empty map or string")]
1394        enum Response {
1395            Empty {},
1396            Map(HashMap<String, Parameter>),
1397            String(String),
1398        }
1399
1400        let params = HashMap::from([("validate_session_id", self.id().value())]);
1401        let resp: Response = client
1402            .rpc("validate.session.metrics".to_owned(), Some(params))
1403            .await?;
1404
1405        Ok(match resp {
1406            Response::String(metrics) => serde_json::from_str(&metrics)?,
1407            Response::Map(metrics) => metrics,
1408            Response::Empty {} => HashMap::new(),
1409        })
1410    }
1411
1412    pub async fn set_metrics(
1413        &self,
1414        client: &client::Client,
1415        metrics: HashMap<String, Parameter>,
1416    ) -> Result<(), Error> {
1417        let metrics = PublishMetrics {
1418            trainer_session_id: None,
1419            validate_session_id: Some(self.id()),
1420            metrics,
1421        };
1422
1423        let _: String = client
1424            .rpc("validate.session.metrics".to_owned(), Some(metrics))
1425            .await?;
1426
1427        Ok(())
1428    }
1429
1430    pub async fn upload(
1431        &self,
1432        client: &client::Client,
1433        files: &[(String, PathBuf)],
1434    ) -> Result<(), Error> {
1435        let mut parts = Form::new().part(
1436            "params",
1437            Part::text(format!("{{ \"session_id\": {} }}", self.id().value())),
1438        );
1439
1440        for (name, path) in files {
1441            let file_part = Part::file(path).await?.file_name(name.to_owned());
1442            parts = parts.part("file", file_part);
1443        }
1444
1445        let result = client
1446            .post_multipart("validate.upload.files", parts)
1447            .await?;
1448        trace!("ValidationSession::upload: {:?}", result);
1449        Ok(())
1450    }
1451}
1452
1453#[derive(Deserialize, Clone, Debug)]
1454pub struct DatasetParams {
1455    dataset_id: DatasetID,
1456    annotation_set_id: AnnotationSetID,
1457    #[serde(rename = "train_group_name")]
1458    train_group: String,
1459    #[serde(rename = "val_group_name")]
1460    val_group: String,
1461}
1462
1463impl DatasetParams {
1464    pub fn dataset_id(&self) -> DatasetID {
1465        self.dataset_id
1466    }
1467
1468    pub fn annotation_set_id(&self) -> AnnotationSetID {
1469        self.annotation_set_id
1470    }
1471
1472    pub fn train_group(&self) -> &str {
1473        &self.train_group
1474    }
1475
1476    pub fn val_group(&self) -> &str {
1477        &self.val_group
1478    }
1479}
1480
1481#[derive(Serialize, Debug, Clone)]
1482pub struct TasksListParams {
1483    #[serde(skip_serializing_if = "Option::is_none")]
1484    pub continue_token: Option<String>,
1485    #[serde(skip_serializing_if = "Option::is_none")]
1486    pub types: Option<Vec<String>>,
1487    #[serde(rename = "manage_types", skip_serializing_if = "Option::is_none")]
1488    pub manager: Option<Vec<String>>,
1489    #[serde(skip_serializing_if = "Option::is_none")]
1490    pub status: Option<Vec<String>>,
1491}
1492
1493#[derive(Deserialize, Debug, Clone)]
1494pub struct TasksListResult {
1495    pub tasks: Vec<Task>,
1496    pub continue_token: Option<String>,
1497}
1498
1499#[derive(Deserialize, Debug, Clone)]
1500pub struct Task {
1501    id: TaskID,
1502    name: String,
1503    #[serde(rename = "type")]
1504    workflow: String,
1505    status: String,
1506    #[serde(rename = "manage_type")]
1507    manager: Option<String>,
1508    #[serde(rename = "instance_type")]
1509    instance: String,
1510    #[serde(rename = "date")]
1511    created: DateTime<Utc>,
1512}
1513
1514impl Task {
1515    pub fn id(&self) -> TaskID {
1516        self.id
1517    }
1518
1519    pub fn name(&self) -> &str {
1520        &self.name
1521    }
1522
1523    pub fn workflow(&self) -> &str {
1524        &self.workflow
1525    }
1526
1527    pub fn status(&self) -> &str {
1528        &self.status
1529    }
1530
1531    pub fn manager(&self) -> Option<&str> {
1532        self.manager.as_deref()
1533    }
1534
1535    pub fn instance(&self) -> &str {
1536        &self.instance
1537    }
1538
1539    pub fn created(&self) -> &DateTime<Utc> {
1540        &self.created
1541    }
1542}
1543
1544impl Display for Task {
1545    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1546        write!(
1547            f,
1548            "{} [{:?} {}] {}",
1549            self.id,
1550            self.manager(),
1551            self.workflow(),
1552            self.name()
1553        )
1554    }
1555}
1556
1557#[derive(Deserialize, Debug)]
1558pub struct TaskInfo {
1559    id: TaskID,
1560    project_id: Option<ProjectID>,
1561    #[serde(rename = "task_description")]
1562    description: String,
1563    #[serde(rename = "type")]
1564    workflow: String,
1565    status: Option<String>,
1566    progress: TaskProgress,
1567    #[serde(rename = "created_date")]
1568    created: DateTime<Utc>,
1569    #[serde(rename = "end_date")]
1570    completed: DateTime<Utc>,
1571}
1572
1573impl Display for TaskInfo {
1574    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1575        write!(f, "{} {}: {}", self.id, self.workflow(), self.description())
1576    }
1577}
1578
1579impl TaskInfo {
1580    pub fn id(&self) -> TaskID {
1581        self.id
1582    }
1583
1584    pub fn project_id(&self) -> Option<ProjectID> {
1585        self.project_id
1586    }
1587
1588    pub fn description(&self) -> &str {
1589        &self.description
1590    }
1591
1592    pub fn workflow(&self) -> &str {
1593        &self.workflow
1594    }
1595
1596    pub fn status(&self) -> &Option<String> {
1597        &self.status
1598    }
1599
1600    pub async fn set_status(&mut self, client: &Client, status: &str) -> Result<(), Error> {
1601        let t = client.task_status(self.id(), status).await?;
1602        self.status = Some(t.status);
1603        Ok(())
1604    }
1605
1606    pub fn stages(&self) -> HashMap<String, Stage> {
1607        match &self.progress.stages {
1608            Some(stages) => stages.clone(),
1609            None => HashMap::new(),
1610        }
1611    }
1612
1613    pub async fn update_stage(
1614        &mut self,
1615        client: &Client,
1616        stage: &str,
1617        status: &str,
1618        message: &str,
1619        percentage: u8,
1620    ) -> Result<(), Error> {
1621        client
1622            .update_stage(self.id(), stage, status, message, percentage)
1623            .await?;
1624        let t = client.task_info(self.id()).await?;
1625        self.progress.stages = Some(t.progress.stages.unwrap_or_default());
1626        Ok(())
1627    }
1628
1629    pub async fn set_stages(
1630        &mut self,
1631        client: &Client,
1632        stages: &[(&str, &str)],
1633    ) -> Result<(), Error> {
1634        client.set_stages(self.id(), stages).await?;
1635        let t = client.task_info(self.id()).await?;
1636        self.progress.stages = Some(t.progress.stages.unwrap_or_default());
1637        Ok(())
1638    }
1639
1640    pub fn created(&self) -> &DateTime<Utc> {
1641        &self.created
1642    }
1643
1644    pub fn completed(&self) -> &DateTime<Utc> {
1645        &self.completed
1646    }
1647}
1648
1649#[derive(Deserialize, Debug)]
1650pub struct TaskProgress {
1651    stages: Option<HashMap<String, Stage>>,
1652}
1653
1654#[derive(Serialize, Debug, Clone)]
1655pub struct TaskStatus {
1656    #[serde(rename = "docker_task_id")]
1657    pub task_id: TaskID,
1658    pub status: String,
1659}
1660
1661#[derive(Serialize, Deserialize, Debug, Clone)]
1662pub struct Stage {
1663    #[serde(rename = "docker_task_id", skip_serializing_if = "Option::is_none")]
1664    task_id: Option<TaskID>,
1665    stage: String,
1666    #[serde(skip_serializing_if = "Option::is_none")]
1667    status: Option<String>,
1668    #[serde(skip_serializing_if = "Option::is_none")]
1669    description: Option<String>,
1670    #[serde(skip_serializing_if = "Option::is_none")]
1671    message: Option<String>,
1672    percentage: u8,
1673}
1674
1675impl Stage {
1676    pub fn new(
1677        task_id: Option<TaskID>,
1678        stage: String,
1679        status: Option<String>,
1680        message: Option<String>,
1681        percentage: u8,
1682    ) -> Self {
1683        Stage {
1684            task_id,
1685            stage,
1686            status,
1687            description: None,
1688            message,
1689            percentage,
1690        }
1691    }
1692
1693    pub fn task_id(&self) -> &Option<TaskID> {
1694        &self.task_id
1695    }
1696
1697    pub fn stage(&self) -> &str {
1698        &self.stage
1699    }
1700
1701    pub fn status(&self) -> &Option<String> {
1702        &self.status
1703    }
1704
1705    pub fn description(&self) -> &Option<String> {
1706        &self.description
1707    }
1708
1709    pub fn message(&self) -> &Option<String> {
1710        &self.message
1711    }
1712
1713    pub fn percentage(&self) -> u8 {
1714        self.percentage
1715    }
1716}
1717
1718#[derive(Serialize, Debug)]
1719pub struct TaskStages {
1720    #[serde(rename = "docker_task_id")]
1721    pub task_id: TaskID,
1722    #[serde(skip_serializing_if = "Vec::is_empty")]
1723    pub stages: Vec<HashMap<String, String>>,
1724}
1725
1726#[derive(Deserialize, Debug)]
1727pub struct Artifact {
1728    name: String,
1729    #[serde(rename = "modelType")]
1730    model_type: String,
1731}
1732
1733impl Artifact {
1734    pub fn name(&self) -> &str {
1735        &self.name
1736    }
1737
1738    pub fn model_type(&self) -> &str {
1739        &self.model_type
1740    }
1741}
1742
1743#[cfg(test)]
1744mod tests {
1745    use super::*;
1746
1747    // ========== OrganizationID Tests ==========
1748    #[test]
1749    fn test_organization_id_from_u64() {
1750        let id = OrganizationID::from(12345);
1751        assert_eq!(id.value(), 12345);
1752    }
1753
1754    #[test]
1755    fn test_organization_id_display() {
1756        let id = OrganizationID::from(0xabc123);
1757        assert_eq!(format!("{}", id), "org-abc123");
1758    }
1759
1760    #[test]
1761    fn test_organization_id_try_from_str_valid() {
1762        let id = OrganizationID::try_from("org-abc123").unwrap();
1763        assert_eq!(id.value(), 0xabc123);
1764    }
1765
1766    #[test]
1767    fn test_organization_id_try_from_str_invalid_prefix() {
1768        let result = OrganizationID::try_from("invalid-abc123");
1769        assert!(result.is_err());
1770        match result {
1771            Err(Error::InvalidParameters(msg)) => {
1772                assert!(msg.contains("must start with 'org-'"));
1773            }
1774            _ => panic!("Expected InvalidParameters error"),
1775        }
1776    }
1777
1778    #[test]
1779    fn test_organization_id_try_from_str_invalid_hex() {
1780        let result = OrganizationID::try_from("org-xyz");
1781        assert!(result.is_err());
1782    }
1783
1784    #[test]
1785    fn test_organization_id_try_from_str_empty() {
1786        let result = OrganizationID::try_from("org-");
1787        assert!(result.is_err());
1788    }
1789
1790    #[test]
1791    fn test_organization_id_into_u64() {
1792        let id = OrganizationID::from(54321);
1793        let value: u64 = id.into();
1794        assert_eq!(value, 54321);
1795    }
1796
1797    // ========== ProjectID Tests ==========
1798    #[test]
1799    fn test_project_id_from_u64() {
1800        let id = ProjectID::from(78910);
1801        assert_eq!(id.value(), 78910);
1802    }
1803
1804    #[test]
1805    fn test_project_id_display() {
1806        let id = ProjectID::from(0xdef456);
1807        assert_eq!(format!("{}", id), "p-def456");
1808    }
1809
1810    #[test]
1811    fn test_project_id_from_str_valid() {
1812        let id = ProjectID::from_str("p-def456").unwrap();
1813        assert_eq!(id.value(), 0xdef456);
1814    }
1815
1816    #[test]
1817    fn test_project_id_try_from_str_valid() {
1818        let id = ProjectID::try_from("p-123abc").unwrap();
1819        assert_eq!(id.value(), 0x123abc);
1820    }
1821
1822    #[test]
1823    fn test_project_id_try_from_string_valid() {
1824        let id = ProjectID::try_from("p-456def".to_string()).unwrap();
1825        assert_eq!(id.value(), 0x456def);
1826    }
1827
1828    #[test]
1829    fn test_project_id_from_str_invalid_prefix() {
1830        let result = ProjectID::from_str("proj-123");
1831        assert!(result.is_err());
1832        match result {
1833            Err(Error::InvalidParameters(msg)) => {
1834                assert!(msg.contains("must start with 'p-'"));
1835            }
1836            _ => panic!("Expected InvalidParameters error"),
1837        }
1838    }
1839
1840    #[test]
1841    fn test_project_id_from_str_invalid_hex() {
1842        let result = ProjectID::from_str("p-notahex");
1843        assert!(result.is_err());
1844    }
1845
1846    #[test]
1847    fn test_project_id_into_u64() {
1848        let id = ProjectID::from(99999);
1849        let value: u64 = id.into();
1850        assert_eq!(value, 99999);
1851    }
1852
1853    // ========== ExperimentID Tests ==========
1854    #[test]
1855    fn test_experiment_id_from_u64() {
1856        let id = ExperimentID::from(1193046);
1857        assert_eq!(id.value(), 1193046);
1858    }
1859
1860    #[test]
1861    fn test_experiment_id_display() {
1862        let id = ExperimentID::from(0x123abc);
1863        assert_eq!(format!("{}", id), "exp-123abc");
1864    }
1865
1866    #[test]
1867    fn test_experiment_id_from_str_valid() {
1868        let id = ExperimentID::from_str("exp-456def").unwrap();
1869        assert_eq!(id.value(), 0x456def);
1870    }
1871
1872    #[test]
1873    fn test_experiment_id_try_from_str_valid() {
1874        let id = ExperimentID::try_from("exp-789abc").unwrap();
1875        assert_eq!(id.value(), 0x789abc);
1876    }
1877
1878    #[test]
1879    fn test_experiment_id_try_from_string_valid() {
1880        let id = ExperimentID::try_from("exp-fedcba".to_string()).unwrap();
1881        assert_eq!(id.value(), 0xfedcba);
1882    }
1883
1884    #[test]
1885    fn test_experiment_id_from_str_invalid_prefix() {
1886        let result = ExperimentID::from_str("experiment-123");
1887        assert!(result.is_err());
1888        match result {
1889            Err(Error::InvalidParameters(msg)) => {
1890                assert!(msg.contains("must start with 'exp-'"));
1891            }
1892            _ => panic!("Expected InvalidParameters error"),
1893        }
1894    }
1895
1896    #[test]
1897    fn test_experiment_id_from_str_invalid_hex() {
1898        let result = ExperimentID::from_str("exp-zzz");
1899        assert!(result.is_err());
1900    }
1901
1902    #[test]
1903    fn test_experiment_id_into_u64() {
1904        let id = ExperimentID::from(777777);
1905        let value: u64 = id.into();
1906        assert_eq!(value, 777777);
1907    }
1908
1909    // ========== TrainingSessionID Tests ==========
1910    #[test]
1911    fn test_training_session_id_from_u64() {
1912        let id = TrainingSessionID::from(7901234);
1913        assert_eq!(id.value(), 7901234);
1914    }
1915
1916    #[test]
1917    fn test_training_session_id_display() {
1918        let id = TrainingSessionID::from(0xabc123);
1919        assert_eq!(format!("{}", id), "t-abc123");
1920    }
1921
1922    #[test]
1923    fn test_training_session_id_from_str_valid() {
1924        let id = TrainingSessionID::from_str("t-abc123").unwrap();
1925        assert_eq!(id.value(), 0xabc123);
1926    }
1927
1928    #[test]
1929    fn test_training_session_id_try_from_str_valid() {
1930        let id = TrainingSessionID::try_from("t-deadbeef").unwrap();
1931        assert_eq!(id.value(), 0xdeadbeef);
1932    }
1933
1934    #[test]
1935    fn test_training_session_id_try_from_string_valid() {
1936        let id = TrainingSessionID::try_from("t-cafebabe".to_string()).unwrap();
1937        assert_eq!(id.value(), 0xcafebabe);
1938    }
1939
1940    #[test]
1941    fn test_training_session_id_from_str_invalid_prefix() {
1942        let result = TrainingSessionID::from_str("training-123");
1943        assert!(result.is_err());
1944        match result {
1945            Err(Error::InvalidParameters(msg)) => {
1946                assert!(msg.contains("must start with 't-'"));
1947            }
1948            _ => panic!("Expected InvalidParameters error"),
1949        }
1950    }
1951
1952    #[test]
1953    fn test_training_session_id_from_str_invalid_hex() {
1954        let result = TrainingSessionID::from_str("t-qqq");
1955        assert!(result.is_err());
1956    }
1957
1958    #[test]
1959    fn test_training_session_id_into_u64() {
1960        let id = TrainingSessionID::from(123456);
1961        let value: u64 = id.into();
1962        assert_eq!(value, 123456);
1963    }
1964
1965    // ========== ValidationSessionID Tests ==========
1966    #[test]
1967    fn test_validation_session_id_from_u64() {
1968        let id = ValidationSessionID::from(3456789);
1969        assert_eq!(id.value(), 3456789);
1970    }
1971
1972    #[test]
1973    fn test_validation_session_id_display() {
1974        let id = ValidationSessionID::from(0x34c985);
1975        assert_eq!(format!("{}", id), "v-34c985");
1976    }
1977
1978    #[test]
1979    fn test_validation_session_id_try_from_str_valid() {
1980        let id = ValidationSessionID::try_from("v-deadbeef").unwrap();
1981        assert_eq!(id.value(), 0xdeadbeef);
1982    }
1983
1984    #[test]
1985    fn test_validation_session_id_try_from_string_valid() {
1986        let id = ValidationSessionID::try_from("v-12345678".to_string()).unwrap();
1987        assert_eq!(id.value(), 0x12345678);
1988    }
1989
1990    #[test]
1991    fn test_validation_session_id_try_from_str_invalid_prefix() {
1992        let result = ValidationSessionID::try_from("validation-123");
1993        assert!(result.is_err());
1994        match result {
1995            Err(Error::InvalidParameters(msg)) => {
1996                assert!(msg.contains("must start with 'v-'"));
1997            }
1998            _ => panic!("Expected InvalidParameters error"),
1999        }
2000    }
2001
2002    #[test]
2003    fn test_validation_session_id_try_from_str_invalid_hex() {
2004        let result = ValidationSessionID::try_from("v-xyz");
2005        assert!(result.is_err());
2006    }
2007
2008    #[test]
2009    fn test_validation_session_id_into_u64() {
2010        let id = ValidationSessionID::from(987654);
2011        let value: u64 = id.into();
2012        assert_eq!(value, 987654);
2013    }
2014
2015    // ========== SnapshotID Tests ==========
2016    #[test]
2017    fn test_snapshot_id_from_u64() {
2018        let id = SnapshotID::from(111222);
2019        assert_eq!(id.value(), 111222);
2020    }
2021
2022    #[test]
2023    fn test_snapshot_id_display() {
2024        let id = SnapshotID::from(0xaabbcc);
2025        assert_eq!(format!("{}", id), "ss-aabbcc");
2026    }
2027
2028    #[test]
2029    fn test_snapshot_id_try_from_str_valid() {
2030        let id = SnapshotID::try_from("ss-aabbcc").unwrap();
2031        assert_eq!(id.value(), 0xaabbcc);
2032    }
2033
2034    #[test]
2035    fn test_snapshot_id_try_from_str_invalid_prefix() {
2036        let result = SnapshotID::try_from("snapshot-123");
2037        assert!(result.is_err());
2038        match result {
2039            Err(Error::InvalidParameters(msg)) => {
2040                assert!(msg.contains("must start with 'ss-'"));
2041            }
2042            _ => panic!("Expected InvalidParameters error"),
2043        }
2044    }
2045
2046    #[test]
2047    fn test_snapshot_id_try_from_str_invalid_hex() {
2048        let result = SnapshotID::try_from("ss-ggg");
2049        assert!(result.is_err());
2050    }
2051
2052    #[test]
2053    fn test_snapshot_id_into_u64() {
2054        let id = SnapshotID::from(333444);
2055        let value: u64 = id.into();
2056        assert_eq!(value, 333444);
2057    }
2058
2059    // ========== TaskID Tests ==========
2060    #[test]
2061    fn test_task_id_from_u64() {
2062        let id = TaskID::from(555666);
2063        assert_eq!(id.value(), 555666);
2064    }
2065
2066    #[test]
2067    fn test_task_id_display() {
2068        let id = TaskID::from(0x123456);
2069        assert_eq!(format!("{}", id), "task-123456");
2070    }
2071
2072    #[test]
2073    fn test_task_id_from_str_valid() {
2074        let id = TaskID::from_str("task-123456").unwrap();
2075        assert_eq!(id.value(), 0x123456);
2076    }
2077
2078    #[test]
2079    fn test_task_id_try_from_str_valid() {
2080        let id = TaskID::try_from("task-abcdef").unwrap();
2081        assert_eq!(id.value(), 0xabcdef);
2082    }
2083
2084    #[test]
2085    fn test_task_id_try_from_string_valid() {
2086        let id = TaskID::try_from("task-fedcba".to_string()).unwrap();
2087        assert_eq!(id.value(), 0xfedcba);
2088    }
2089
2090    #[test]
2091    fn test_task_id_from_str_invalid_prefix() {
2092        let result = TaskID::from_str("t-123");
2093        assert!(result.is_err());
2094        match result {
2095            Err(Error::InvalidParameters(msg)) => {
2096                assert!(msg.contains("must start with 'task-'"));
2097            }
2098            _ => panic!("Expected InvalidParameters error"),
2099        }
2100    }
2101
2102    #[test]
2103    fn test_task_id_from_str_invalid_hex() {
2104        let result = TaskID::from_str("task-zzz");
2105        assert!(result.is_err());
2106    }
2107
2108    #[test]
2109    fn test_task_id_into_u64() {
2110        let id = TaskID::from(777888);
2111        let value: u64 = id.into();
2112        assert_eq!(value, 777888);
2113    }
2114
2115    // ========== DatasetID Tests ==========
2116    #[test]
2117    fn test_dataset_id_from_u64() {
2118        let id = DatasetID::from(1193046);
2119        assert_eq!(id.value(), 1193046);
2120    }
2121
2122    #[test]
2123    fn test_dataset_id_display() {
2124        let id = DatasetID::from(0x123abc);
2125        assert_eq!(format!("{}", id), "ds-123abc");
2126    }
2127
2128    #[test]
2129    fn test_dataset_id_from_str_valid() {
2130        let id = DatasetID::from_str("ds-456def").unwrap();
2131        assert_eq!(id.value(), 0x456def);
2132    }
2133
2134    #[test]
2135    fn test_dataset_id_try_from_str_valid() {
2136        let id = DatasetID::try_from("ds-789abc").unwrap();
2137        assert_eq!(id.value(), 0x789abc);
2138    }
2139
2140    #[test]
2141    fn test_dataset_id_try_from_string_valid() {
2142        let id = DatasetID::try_from("ds-fedcba".to_string()).unwrap();
2143        assert_eq!(id.value(), 0xfedcba);
2144    }
2145
2146    #[test]
2147    fn test_dataset_id_from_str_invalid_prefix() {
2148        let result = DatasetID::from_str("dataset-123");
2149        assert!(result.is_err());
2150        match result {
2151            Err(Error::InvalidParameters(msg)) => {
2152                assert!(msg.contains("must start with 'ds-'"));
2153            }
2154            _ => panic!("Expected InvalidParameters error"),
2155        }
2156    }
2157
2158    #[test]
2159    fn test_dataset_id_from_str_invalid_hex() {
2160        let result = DatasetID::from_str("ds-zzz");
2161        assert!(result.is_err());
2162    }
2163
2164    #[test]
2165    fn test_dataset_id_into_u64() {
2166        let id = DatasetID::from(111111);
2167        let value: u64 = id.into();
2168        assert_eq!(value, 111111);
2169    }
2170
2171    // ========== AnnotationSetID Tests ==========
2172    #[test]
2173    fn test_annotation_set_id_from_u64() {
2174        let id = AnnotationSetID::from(222333);
2175        assert_eq!(id.value(), 222333);
2176    }
2177
2178    #[test]
2179    fn test_annotation_set_id_display() {
2180        let id = AnnotationSetID::from(0xabcdef);
2181        assert_eq!(format!("{}", id), "as-abcdef");
2182    }
2183
2184    #[test]
2185    fn test_annotation_set_id_from_str_valid() {
2186        let id = AnnotationSetID::from_str("as-abcdef").unwrap();
2187        assert_eq!(id.value(), 0xabcdef);
2188    }
2189
2190    #[test]
2191    fn test_annotation_set_id_try_from_str_valid() {
2192        let id = AnnotationSetID::try_from("as-123456").unwrap();
2193        assert_eq!(id.value(), 0x123456);
2194    }
2195
2196    #[test]
2197    fn test_annotation_set_id_try_from_string_valid() {
2198        let id = AnnotationSetID::try_from("as-fedcba".to_string()).unwrap();
2199        assert_eq!(id.value(), 0xfedcba);
2200    }
2201
2202    #[test]
2203    fn test_annotation_set_id_from_str_invalid_prefix() {
2204        let result = AnnotationSetID::from_str("annotation-123");
2205        assert!(result.is_err());
2206        match result {
2207            Err(Error::InvalidParameters(msg)) => {
2208                assert!(msg.contains("must start with 'as-'"));
2209            }
2210            _ => panic!("Expected InvalidParameters error"),
2211        }
2212    }
2213
2214    #[test]
2215    fn test_annotation_set_id_from_str_invalid_hex() {
2216        let result = AnnotationSetID::from_str("as-zzz");
2217        assert!(result.is_err());
2218    }
2219
2220    #[test]
2221    fn test_annotation_set_id_into_u64() {
2222        let id = AnnotationSetID::from(444555);
2223        let value: u64 = id.into();
2224        assert_eq!(value, 444555);
2225    }
2226
2227    // ========== SampleID Tests ==========
2228    #[test]
2229    fn test_sample_id_from_u64() {
2230        let id = SampleID::from(666777);
2231        assert_eq!(id.value(), 666777);
2232    }
2233
2234    #[test]
2235    fn test_sample_id_display() {
2236        let id = SampleID::from(0x987654);
2237        assert_eq!(format!("{}", id), "s-987654");
2238    }
2239
2240    #[test]
2241    fn test_sample_id_try_from_str_valid() {
2242        let id = SampleID::try_from("s-987654").unwrap();
2243        assert_eq!(id.value(), 0x987654);
2244    }
2245
2246    #[test]
2247    fn test_sample_id_try_from_str_invalid_prefix() {
2248        let result = SampleID::try_from("sample-123");
2249        assert!(result.is_err());
2250        match result {
2251            Err(Error::InvalidParameters(msg)) => {
2252                assert!(msg.contains("must start with 's-'"));
2253            }
2254            _ => panic!("Expected InvalidParameters error"),
2255        }
2256    }
2257
2258    #[test]
2259    fn test_sample_id_try_from_str_invalid_hex() {
2260        let result = SampleID::try_from("s-zzz");
2261        assert!(result.is_err());
2262    }
2263
2264    #[test]
2265    fn test_sample_id_into_u64() {
2266        let id = SampleID::from(888999);
2267        let value: u64 = id.into();
2268        assert_eq!(value, 888999);
2269    }
2270
2271    // ========== AppId Tests ==========
2272    #[test]
2273    fn test_app_id_from_u64() {
2274        let id = AppId::from(123123);
2275        assert_eq!(id.value(), 123123);
2276    }
2277
2278    #[test]
2279    fn test_app_id_display() {
2280        let id = AppId::from(0x456789);
2281        assert_eq!(format!("{}", id), "app-456789");
2282    }
2283
2284    #[test]
2285    fn test_app_id_try_from_str_valid() {
2286        let id = AppId::try_from("app-456789").unwrap();
2287        assert_eq!(id.value(), 0x456789);
2288    }
2289
2290    #[test]
2291    fn test_app_id_try_from_str_invalid_prefix() {
2292        let result = AppId::try_from("application-123");
2293        assert!(result.is_err());
2294        match result {
2295            Err(Error::InvalidParameters(msg)) => {
2296                assert!(msg.contains("must start with 'app-'"));
2297            }
2298            _ => panic!("Expected InvalidParameters error"),
2299        }
2300    }
2301
2302    #[test]
2303    fn test_app_id_try_from_str_invalid_hex() {
2304        let result = AppId::try_from("app-zzz");
2305        assert!(result.is_err());
2306    }
2307
2308    #[test]
2309    fn test_app_id_into_u64() {
2310        let id = AppId::from(321321);
2311        let value: u64 = id.into();
2312        assert_eq!(value, 321321);
2313    }
2314
2315    // ========== ImageId Tests ==========
2316    #[test]
2317    fn test_image_id_from_u64() {
2318        let id = ImageId::from(789789);
2319        assert_eq!(id.value(), 789789);
2320    }
2321
2322    #[test]
2323    fn test_image_id_display() {
2324        let id = ImageId::from(0xabcd1234);
2325        assert_eq!(format!("{}", id), "im-abcd1234");
2326    }
2327
2328    #[test]
2329    fn test_image_id_try_from_str_valid() {
2330        let id = ImageId::try_from("im-abcd1234").unwrap();
2331        assert_eq!(id.value(), 0xabcd1234);
2332    }
2333
2334    #[test]
2335    fn test_image_id_try_from_str_invalid_prefix() {
2336        let result = ImageId::try_from("image-123");
2337        assert!(result.is_err());
2338        match result {
2339            Err(Error::InvalidParameters(msg)) => {
2340                assert!(msg.contains("must start with 'im-'"));
2341            }
2342            _ => panic!("Expected InvalidParameters error"),
2343        }
2344    }
2345
2346    #[test]
2347    fn test_image_id_try_from_str_invalid_hex() {
2348        let result = ImageId::try_from("im-zzz");
2349        assert!(result.is_err());
2350    }
2351
2352    #[test]
2353    fn test_image_id_into_u64() {
2354        let id = ImageId::from(987987);
2355        let value: u64 = id.into();
2356        assert_eq!(value, 987987);
2357    }
2358
2359    // ========== ID Type Hash and Equality Tests ==========
2360    #[test]
2361    fn test_id_types_equality() {
2362        let id1 = ProjectID::from(12345);
2363        let id2 = ProjectID::from(12345);
2364        let id3 = ProjectID::from(54321);
2365
2366        assert_eq!(id1, id2);
2367        assert_ne!(id1, id3);
2368    }
2369
2370    #[test]
2371    fn test_id_types_hash() {
2372        use std::collections::HashSet;
2373
2374        let mut set = HashSet::new();
2375        set.insert(DatasetID::from(100));
2376        set.insert(DatasetID::from(200));
2377        set.insert(DatasetID::from(100)); // duplicate
2378
2379        assert_eq!(set.len(), 2);
2380        assert!(set.contains(&DatasetID::from(100)));
2381        assert!(set.contains(&DatasetID::from(200)));
2382    }
2383
2384    #[test]
2385    fn test_id_types_copy_clone() {
2386        let id1 = ExperimentID::from(999);
2387        let id2 = id1; // Copy
2388        let id3 = id1; // Also Copy (no need for clone())
2389
2390        assert_eq!(id1, id2);
2391        assert_eq!(id1, id3);
2392    }
2393
2394    // ========== Edge Cases ==========
2395    #[test]
2396    fn test_id_zero_value() {
2397        let id = ProjectID::from(0);
2398        assert_eq!(format!("{}", id), "p-0");
2399        assert_eq!(id.value(), 0);
2400    }
2401
2402    #[test]
2403    fn test_id_max_value() {
2404        let id = ProjectID::from(u64::MAX);
2405        assert_eq!(format!("{}", id), "p-ffffffffffffffff");
2406        assert_eq!(id.value(), u64::MAX);
2407    }
2408
2409    #[test]
2410    fn test_id_round_trip_conversion() {
2411        let original = 0xdeadbeef_u64;
2412        let id = TrainingSessionID::from(original);
2413        let back: u64 = id.into();
2414        assert_eq!(original, back);
2415    }
2416
2417    #[test]
2418    fn test_id_case_insensitive_hex() {
2419        // Hexadecimal parsing should handle both upper and lowercase
2420        let id1 = DatasetID::from_str("ds-ABCDEF").unwrap();
2421        let id2 = DatasetID::from_str("ds-abcdef").unwrap();
2422        assert_eq!(id1.value(), id2.value());
2423    }
2424
2425    #[test]
2426    fn test_id_with_leading_zeros() {
2427        let id = ProjectID::from_str("p-00001234").unwrap();
2428        assert_eq!(id.value(), 0x1234);
2429    }
2430
2431    // ========== Parameter Tests ==========
2432    #[test]
2433    fn test_parameter_integer() {
2434        let param = Parameter::Integer(42);
2435        match param {
2436            Parameter::Integer(val) => assert_eq!(val, 42),
2437            _ => panic!("Expected Integer variant"),
2438        }
2439    }
2440
2441    #[test]
2442    fn test_parameter_real() {
2443        let param = Parameter::Real(2.5);
2444        match param {
2445            Parameter::Real(val) => assert_eq!(val, 2.5),
2446            _ => panic!("Expected Real variant"),
2447        }
2448    }
2449
2450    #[test]
2451    fn test_parameter_boolean() {
2452        let param = Parameter::Boolean(true);
2453        match param {
2454            Parameter::Boolean(val) => assert!(val),
2455            _ => panic!("Expected Boolean variant"),
2456        }
2457    }
2458
2459    #[test]
2460    fn test_parameter_string() {
2461        let param = Parameter::String("test".to_string());
2462        match param {
2463            Parameter::String(val) => assert_eq!(val, "test"),
2464            _ => panic!("Expected String variant"),
2465        }
2466    }
2467
2468    #[test]
2469    fn test_parameter_array() {
2470        let param = Parameter::Array(vec![
2471            Parameter::Integer(1),
2472            Parameter::Integer(2),
2473            Parameter::Integer(3),
2474        ]);
2475        match param {
2476            Parameter::Array(arr) => assert_eq!(arr.len(), 3),
2477            _ => panic!("Expected Array variant"),
2478        }
2479    }
2480
2481    #[test]
2482    fn test_parameter_object() {
2483        let mut map = HashMap::new();
2484        map.insert("key".to_string(), Parameter::Integer(100));
2485        let param = Parameter::Object(map);
2486        match param {
2487            Parameter::Object(obj) => {
2488                assert_eq!(obj.len(), 1);
2489                assert!(obj.contains_key("key"));
2490            }
2491            _ => panic!("Expected Object variant"),
2492        }
2493    }
2494
2495    #[test]
2496    fn test_parameter_clone() {
2497        let param1 = Parameter::Integer(42);
2498        let param2 = param1.clone();
2499        assert_eq!(param1, param2);
2500    }
2501
2502    #[test]
2503    fn test_parameter_nested() {
2504        let inner_array = Parameter::Array(vec![Parameter::Integer(1), Parameter::Integer(2)]);
2505        let outer_array = Parameter::Array(vec![inner_array.clone(), inner_array]);
2506
2507        match outer_array {
2508            Parameter::Array(arr) => {
2509                assert_eq!(arr.len(), 2);
2510            }
2511            _ => panic!("Expected Array variant"),
2512        }
2513    }
2514}