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(rename = "docker_task_id")]
1013    pub task_id: 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(rename = "manage_types", skip_serializing_if = "Option::is_none")]
1486    pub manager: Option<Vec<String>>,
1487    #[serde(skip_serializing_if = "Option::is_none")]
1488    pub status: Option<Vec<String>>,
1489}
1490
1491#[derive(Deserialize, Debug, Clone)]
1492pub struct TasksListResult {
1493    pub tasks: Vec<Task>,
1494    pub continue_token: Option<String>,
1495}
1496
1497#[derive(Deserialize, Debug, Clone)]
1498pub struct Task {
1499    id: TaskID,
1500    name: String,
1501    #[serde(rename = "type")]
1502    workflow: String,
1503    status: String,
1504    #[serde(rename = "manage_type")]
1505    manager: Option<String>,
1506    #[serde(rename = "instance_type")]
1507    instance: String,
1508    #[serde(rename = "date")]
1509    created: DateTime<Utc>,
1510}
1511
1512impl Task {
1513    pub fn id(&self) -> TaskID {
1514        self.id
1515    }
1516
1517    pub fn name(&self) -> &str {
1518        &self.name
1519    }
1520
1521    pub fn workflow(&self) -> &str {
1522        &self.workflow
1523    }
1524
1525    pub fn status(&self) -> &str {
1526        &self.status
1527    }
1528
1529    pub fn manager(&self) -> Option<&str> {
1530        self.manager.as_deref()
1531    }
1532
1533    pub fn instance(&self) -> &str {
1534        &self.instance
1535    }
1536
1537    pub fn created(&self) -> &DateTime<Utc> {
1538        &self.created
1539    }
1540}
1541
1542impl Display for Task {
1543    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1544        write!(
1545            f,
1546            "{} [{:?} {}] {}",
1547            self.id,
1548            self.manager(),
1549            self.workflow(),
1550            self.name()
1551        )
1552    }
1553}
1554
1555#[derive(Deserialize, Debug)]
1556pub struct TaskInfo {
1557    id: TaskID,
1558    project_id: Option<ProjectID>,
1559    #[serde(rename = "task_description")]
1560    description: String,
1561    #[serde(rename = "type")]
1562    workflow: String,
1563    status: Option<String>,
1564    progress: TaskProgress,
1565    #[serde(rename = "created_date")]
1566    created: DateTime<Utc>,
1567    #[serde(rename = "end_date")]
1568    completed: DateTime<Utc>,
1569}
1570
1571impl Display for TaskInfo {
1572    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1573        write!(f, "{} {}: {}", self.id, self.workflow(), self.description())
1574    }
1575}
1576
1577impl TaskInfo {
1578    pub fn id(&self) -> TaskID {
1579        self.id
1580    }
1581
1582    pub fn project_id(&self) -> Option<ProjectID> {
1583        self.project_id
1584    }
1585
1586    pub fn description(&self) -> &str {
1587        &self.description
1588    }
1589
1590    pub fn workflow(&self) -> &str {
1591        &self.workflow
1592    }
1593
1594    pub fn status(&self) -> &Option<String> {
1595        &self.status
1596    }
1597
1598    pub async fn set_status(&mut self, client: &Client, status: &str) -> Result<(), Error> {
1599        let t = client.task_status(self.id(), status).await?;
1600        self.status = Some(t.status);
1601        Ok(())
1602    }
1603
1604    pub fn stages(&self) -> HashMap<String, Stage> {
1605        match &self.progress.stages {
1606            Some(stages) => stages.clone(),
1607            None => HashMap::new(),
1608        }
1609    }
1610
1611    pub async fn update_stage(
1612        &mut self,
1613        client: &Client,
1614        stage: &str,
1615        status: &str,
1616        message: &str,
1617        percentage: u8,
1618    ) -> Result<(), Error> {
1619        client
1620            .update_stage(self.id(), stage, status, message, percentage)
1621            .await?;
1622        let t = client.task_info(self.id()).await?;
1623        self.progress.stages = Some(t.progress.stages.unwrap_or_default());
1624        Ok(())
1625    }
1626
1627    pub async fn set_stages(
1628        &mut self,
1629        client: &Client,
1630        stages: &[(&str, &str)],
1631    ) -> Result<(), Error> {
1632        client.set_stages(self.id(), stages).await?;
1633        let t = client.task_info(self.id()).await?;
1634        self.progress.stages = Some(t.progress.stages.unwrap_or_default());
1635        Ok(())
1636    }
1637
1638    pub fn created(&self) -> &DateTime<Utc> {
1639        &self.created
1640    }
1641
1642    pub fn completed(&self) -> &DateTime<Utc> {
1643        &self.completed
1644    }
1645}
1646
1647#[derive(Deserialize, Debug)]
1648pub struct TaskProgress {
1649    stages: Option<HashMap<String, Stage>>,
1650}
1651
1652#[derive(Serialize, Debug, Clone)]
1653pub struct TaskStatus {
1654    #[serde(rename = "docker_task_id")]
1655    pub task_id: TaskID,
1656    pub status: String,
1657}
1658
1659#[derive(Serialize, Deserialize, Debug, Clone)]
1660pub struct Stage {
1661    #[serde(rename = "docker_task_id", skip_serializing_if = "Option::is_none")]
1662    task_id: Option<TaskID>,
1663    stage: String,
1664    #[serde(skip_serializing_if = "Option::is_none")]
1665    status: Option<String>,
1666    #[serde(skip_serializing_if = "Option::is_none")]
1667    description: Option<String>,
1668    #[serde(skip_serializing_if = "Option::is_none")]
1669    message: Option<String>,
1670    percentage: u8,
1671}
1672
1673impl Stage {
1674    pub fn new(
1675        task_id: Option<TaskID>,
1676        stage: String,
1677        status: Option<String>,
1678        message: Option<String>,
1679        percentage: u8,
1680    ) -> Self {
1681        Stage {
1682            task_id,
1683            stage,
1684            status,
1685            description: None,
1686            message,
1687            percentage,
1688        }
1689    }
1690
1691    pub fn task_id(&self) -> &Option<TaskID> {
1692        &self.task_id
1693    }
1694
1695    pub fn stage(&self) -> &str {
1696        &self.stage
1697    }
1698
1699    pub fn status(&self) -> &Option<String> {
1700        &self.status
1701    }
1702
1703    pub fn description(&self) -> &Option<String> {
1704        &self.description
1705    }
1706
1707    pub fn message(&self) -> &Option<String> {
1708        &self.message
1709    }
1710
1711    pub fn percentage(&self) -> u8 {
1712        self.percentage
1713    }
1714}
1715
1716#[derive(Serialize, Debug)]
1717pub struct TaskStages {
1718    #[serde(rename = "docker_task_id")]
1719    pub task_id: TaskID,
1720    #[serde(skip_serializing_if = "Vec::is_empty")]
1721    pub stages: Vec<HashMap<String, String>>,
1722}
1723
1724#[derive(Deserialize, Debug)]
1725pub struct Artifact {
1726    name: String,
1727    #[serde(rename = "modelType")]
1728    model_type: String,
1729}
1730
1731impl Artifact {
1732    pub fn name(&self) -> &str {
1733        &self.name
1734    }
1735
1736    pub fn model_type(&self) -> &str {
1737        &self.model_type
1738    }
1739}
1740
1741#[cfg(test)]
1742mod tests {
1743    use super::*;
1744
1745    // ========== OrganizationID Tests ==========
1746    #[test]
1747    fn test_organization_id_from_u64() {
1748        let id = OrganizationID::from(12345);
1749        assert_eq!(id.value(), 12345);
1750    }
1751
1752    #[test]
1753    fn test_organization_id_display() {
1754        let id = OrganizationID::from(0xabc123);
1755        assert_eq!(format!("{}", id), "org-abc123");
1756    }
1757
1758    #[test]
1759    fn test_organization_id_try_from_str_valid() {
1760        let id = OrganizationID::try_from("org-abc123").unwrap();
1761        assert_eq!(id.value(), 0xabc123);
1762    }
1763
1764    #[test]
1765    fn test_organization_id_try_from_str_invalid_prefix() {
1766        let result = OrganizationID::try_from("invalid-abc123");
1767        assert!(result.is_err());
1768        match result {
1769            Err(Error::InvalidParameters(msg)) => {
1770                assert!(msg.contains("must start with 'org-'"));
1771            }
1772            _ => panic!("Expected InvalidParameters error"),
1773        }
1774    }
1775
1776    #[test]
1777    fn test_organization_id_try_from_str_invalid_hex() {
1778        let result = OrganizationID::try_from("org-xyz");
1779        assert!(result.is_err());
1780    }
1781
1782    #[test]
1783    fn test_organization_id_try_from_str_empty() {
1784        let result = OrganizationID::try_from("org-");
1785        assert!(result.is_err());
1786    }
1787
1788    #[test]
1789    fn test_organization_id_into_u64() {
1790        let id = OrganizationID::from(54321);
1791        let value: u64 = id.into();
1792        assert_eq!(value, 54321);
1793    }
1794
1795    // ========== ProjectID Tests ==========
1796    #[test]
1797    fn test_project_id_from_u64() {
1798        let id = ProjectID::from(78910);
1799        assert_eq!(id.value(), 78910);
1800    }
1801
1802    #[test]
1803    fn test_project_id_display() {
1804        let id = ProjectID::from(0xdef456);
1805        assert_eq!(format!("{}", id), "p-def456");
1806    }
1807
1808    #[test]
1809    fn test_project_id_from_str_valid() {
1810        let id = ProjectID::from_str("p-def456").unwrap();
1811        assert_eq!(id.value(), 0xdef456);
1812    }
1813
1814    #[test]
1815    fn test_project_id_try_from_str_valid() {
1816        let id = ProjectID::try_from("p-123abc").unwrap();
1817        assert_eq!(id.value(), 0x123abc);
1818    }
1819
1820    #[test]
1821    fn test_project_id_try_from_string_valid() {
1822        let id = ProjectID::try_from("p-456def".to_string()).unwrap();
1823        assert_eq!(id.value(), 0x456def);
1824    }
1825
1826    #[test]
1827    fn test_project_id_from_str_invalid_prefix() {
1828        let result = ProjectID::from_str("proj-123");
1829        assert!(result.is_err());
1830        match result {
1831            Err(Error::InvalidParameters(msg)) => {
1832                assert!(msg.contains("must start with 'p-'"));
1833            }
1834            _ => panic!("Expected InvalidParameters error"),
1835        }
1836    }
1837
1838    #[test]
1839    fn test_project_id_from_str_invalid_hex() {
1840        let result = ProjectID::from_str("p-notahex");
1841        assert!(result.is_err());
1842    }
1843
1844    #[test]
1845    fn test_project_id_into_u64() {
1846        let id = ProjectID::from(99999);
1847        let value: u64 = id.into();
1848        assert_eq!(value, 99999);
1849    }
1850
1851    // ========== ExperimentID Tests ==========
1852    #[test]
1853    fn test_experiment_id_from_u64() {
1854        let id = ExperimentID::from(1193046);
1855        assert_eq!(id.value(), 1193046);
1856    }
1857
1858    #[test]
1859    fn test_experiment_id_display() {
1860        let id = ExperimentID::from(0x123abc);
1861        assert_eq!(format!("{}", id), "exp-123abc");
1862    }
1863
1864    #[test]
1865    fn test_experiment_id_from_str_valid() {
1866        let id = ExperimentID::from_str("exp-456def").unwrap();
1867        assert_eq!(id.value(), 0x456def);
1868    }
1869
1870    #[test]
1871    fn test_experiment_id_try_from_str_valid() {
1872        let id = ExperimentID::try_from("exp-789abc").unwrap();
1873        assert_eq!(id.value(), 0x789abc);
1874    }
1875
1876    #[test]
1877    fn test_experiment_id_try_from_string_valid() {
1878        let id = ExperimentID::try_from("exp-fedcba".to_string()).unwrap();
1879        assert_eq!(id.value(), 0xfedcba);
1880    }
1881
1882    #[test]
1883    fn test_experiment_id_from_str_invalid_prefix() {
1884        let result = ExperimentID::from_str("experiment-123");
1885        assert!(result.is_err());
1886        match result {
1887            Err(Error::InvalidParameters(msg)) => {
1888                assert!(msg.contains("must start with 'exp-'"));
1889            }
1890            _ => panic!("Expected InvalidParameters error"),
1891        }
1892    }
1893
1894    #[test]
1895    fn test_experiment_id_from_str_invalid_hex() {
1896        let result = ExperimentID::from_str("exp-zzz");
1897        assert!(result.is_err());
1898    }
1899
1900    #[test]
1901    fn test_experiment_id_into_u64() {
1902        let id = ExperimentID::from(777777);
1903        let value: u64 = id.into();
1904        assert_eq!(value, 777777);
1905    }
1906
1907    // ========== TrainingSessionID Tests ==========
1908    #[test]
1909    fn test_training_session_id_from_u64() {
1910        let id = TrainingSessionID::from(7901234);
1911        assert_eq!(id.value(), 7901234);
1912    }
1913
1914    #[test]
1915    fn test_training_session_id_display() {
1916        let id = TrainingSessionID::from(0xabc123);
1917        assert_eq!(format!("{}", id), "t-abc123");
1918    }
1919
1920    #[test]
1921    fn test_training_session_id_from_str_valid() {
1922        let id = TrainingSessionID::from_str("t-abc123").unwrap();
1923        assert_eq!(id.value(), 0xabc123);
1924    }
1925
1926    #[test]
1927    fn test_training_session_id_try_from_str_valid() {
1928        let id = TrainingSessionID::try_from("t-deadbeef").unwrap();
1929        assert_eq!(id.value(), 0xdeadbeef);
1930    }
1931
1932    #[test]
1933    fn test_training_session_id_try_from_string_valid() {
1934        let id = TrainingSessionID::try_from("t-cafebabe".to_string()).unwrap();
1935        assert_eq!(id.value(), 0xcafebabe);
1936    }
1937
1938    #[test]
1939    fn test_training_session_id_from_str_invalid_prefix() {
1940        let result = TrainingSessionID::from_str("training-123");
1941        assert!(result.is_err());
1942        match result {
1943            Err(Error::InvalidParameters(msg)) => {
1944                assert!(msg.contains("must start with 't-'"));
1945            }
1946            _ => panic!("Expected InvalidParameters error"),
1947        }
1948    }
1949
1950    #[test]
1951    fn test_training_session_id_from_str_invalid_hex() {
1952        let result = TrainingSessionID::from_str("t-qqq");
1953        assert!(result.is_err());
1954    }
1955
1956    #[test]
1957    fn test_training_session_id_into_u64() {
1958        let id = TrainingSessionID::from(123456);
1959        let value: u64 = id.into();
1960        assert_eq!(value, 123456);
1961    }
1962
1963    // ========== ValidationSessionID Tests ==========
1964    #[test]
1965    fn test_validation_session_id_from_u64() {
1966        let id = ValidationSessionID::from(3456789);
1967        assert_eq!(id.value(), 3456789);
1968    }
1969
1970    #[test]
1971    fn test_validation_session_id_display() {
1972        let id = ValidationSessionID::from(0x34c985);
1973        assert_eq!(format!("{}", id), "v-34c985");
1974    }
1975
1976    #[test]
1977    fn test_validation_session_id_try_from_str_valid() {
1978        let id = ValidationSessionID::try_from("v-deadbeef").unwrap();
1979        assert_eq!(id.value(), 0xdeadbeef);
1980    }
1981
1982    #[test]
1983    fn test_validation_session_id_try_from_string_valid() {
1984        let id = ValidationSessionID::try_from("v-12345678".to_string()).unwrap();
1985        assert_eq!(id.value(), 0x12345678);
1986    }
1987
1988    #[test]
1989    fn test_validation_session_id_try_from_str_invalid_prefix() {
1990        let result = ValidationSessionID::try_from("validation-123");
1991        assert!(result.is_err());
1992        match result {
1993            Err(Error::InvalidParameters(msg)) => {
1994                assert!(msg.contains("must start with 'v-'"));
1995            }
1996            _ => panic!("Expected InvalidParameters error"),
1997        }
1998    }
1999
2000    #[test]
2001    fn test_validation_session_id_try_from_str_invalid_hex() {
2002        let result = ValidationSessionID::try_from("v-xyz");
2003        assert!(result.is_err());
2004    }
2005
2006    #[test]
2007    fn test_validation_session_id_into_u64() {
2008        let id = ValidationSessionID::from(987654);
2009        let value: u64 = id.into();
2010        assert_eq!(value, 987654);
2011    }
2012
2013    // ========== SnapshotID Tests ==========
2014    #[test]
2015    fn test_snapshot_id_from_u64() {
2016        let id = SnapshotID::from(111222);
2017        assert_eq!(id.value(), 111222);
2018    }
2019
2020    #[test]
2021    fn test_snapshot_id_display() {
2022        let id = SnapshotID::from(0xaabbcc);
2023        assert_eq!(format!("{}", id), "ss-aabbcc");
2024    }
2025
2026    #[test]
2027    fn test_snapshot_id_try_from_str_valid() {
2028        let id = SnapshotID::try_from("ss-aabbcc").unwrap();
2029        assert_eq!(id.value(), 0xaabbcc);
2030    }
2031
2032    #[test]
2033    fn test_snapshot_id_try_from_str_invalid_prefix() {
2034        let result = SnapshotID::try_from("snapshot-123");
2035        assert!(result.is_err());
2036        match result {
2037            Err(Error::InvalidParameters(msg)) => {
2038                assert!(msg.contains("must start with 'ss-'"));
2039            }
2040            _ => panic!("Expected InvalidParameters error"),
2041        }
2042    }
2043
2044    #[test]
2045    fn test_snapshot_id_try_from_str_invalid_hex() {
2046        let result = SnapshotID::try_from("ss-ggg");
2047        assert!(result.is_err());
2048    }
2049
2050    #[test]
2051    fn test_snapshot_id_into_u64() {
2052        let id = SnapshotID::from(333444);
2053        let value: u64 = id.into();
2054        assert_eq!(value, 333444);
2055    }
2056
2057    // ========== TaskID Tests ==========
2058    #[test]
2059    fn test_task_id_from_u64() {
2060        let id = TaskID::from(555666);
2061        assert_eq!(id.value(), 555666);
2062    }
2063
2064    #[test]
2065    fn test_task_id_display() {
2066        let id = TaskID::from(0x123456);
2067        assert_eq!(format!("{}", id), "task-123456");
2068    }
2069
2070    #[test]
2071    fn test_task_id_from_str_valid() {
2072        let id = TaskID::from_str("task-123456").unwrap();
2073        assert_eq!(id.value(), 0x123456);
2074    }
2075
2076    #[test]
2077    fn test_task_id_try_from_str_valid() {
2078        let id = TaskID::try_from("task-abcdef").unwrap();
2079        assert_eq!(id.value(), 0xabcdef);
2080    }
2081
2082    #[test]
2083    fn test_task_id_try_from_string_valid() {
2084        let id = TaskID::try_from("task-fedcba".to_string()).unwrap();
2085        assert_eq!(id.value(), 0xfedcba);
2086    }
2087
2088    #[test]
2089    fn test_task_id_from_str_invalid_prefix() {
2090        let result = TaskID::from_str("t-123");
2091        assert!(result.is_err());
2092        match result {
2093            Err(Error::InvalidParameters(msg)) => {
2094                assert!(msg.contains("must start with 'task-'"));
2095            }
2096            _ => panic!("Expected InvalidParameters error"),
2097        }
2098    }
2099
2100    #[test]
2101    fn test_task_id_from_str_invalid_hex() {
2102        let result = TaskID::from_str("task-zzz");
2103        assert!(result.is_err());
2104    }
2105
2106    #[test]
2107    fn test_task_id_into_u64() {
2108        let id = TaskID::from(777888);
2109        let value: u64 = id.into();
2110        assert_eq!(value, 777888);
2111    }
2112
2113    // ========== DatasetID Tests ==========
2114    #[test]
2115    fn test_dataset_id_from_u64() {
2116        let id = DatasetID::from(1193046);
2117        assert_eq!(id.value(), 1193046);
2118    }
2119
2120    #[test]
2121    fn test_dataset_id_display() {
2122        let id = DatasetID::from(0x123abc);
2123        assert_eq!(format!("{}", id), "ds-123abc");
2124    }
2125
2126    #[test]
2127    fn test_dataset_id_from_str_valid() {
2128        let id = DatasetID::from_str("ds-456def").unwrap();
2129        assert_eq!(id.value(), 0x456def);
2130    }
2131
2132    #[test]
2133    fn test_dataset_id_try_from_str_valid() {
2134        let id = DatasetID::try_from("ds-789abc").unwrap();
2135        assert_eq!(id.value(), 0x789abc);
2136    }
2137
2138    #[test]
2139    fn test_dataset_id_try_from_string_valid() {
2140        let id = DatasetID::try_from("ds-fedcba".to_string()).unwrap();
2141        assert_eq!(id.value(), 0xfedcba);
2142    }
2143
2144    #[test]
2145    fn test_dataset_id_from_str_invalid_prefix() {
2146        let result = DatasetID::from_str("dataset-123");
2147        assert!(result.is_err());
2148        match result {
2149            Err(Error::InvalidParameters(msg)) => {
2150                assert!(msg.contains("must start with 'ds-'"));
2151            }
2152            _ => panic!("Expected InvalidParameters error"),
2153        }
2154    }
2155
2156    #[test]
2157    fn test_dataset_id_from_str_invalid_hex() {
2158        let result = DatasetID::from_str("ds-zzz");
2159        assert!(result.is_err());
2160    }
2161
2162    #[test]
2163    fn test_dataset_id_into_u64() {
2164        let id = DatasetID::from(111111);
2165        let value: u64 = id.into();
2166        assert_eq!(value, 111111);
2167    }
2168
2169    // ========== AnnotationSetID Tests ==========
2170    #[test]
2171    fn test_annotation_set_id_from_u64() {
2172        let id = AnnotationSetID::from(222333);
2173        assert_eq!(id.value(), 222333);
2174    }
2175
2176    #[test]
2177    fn test_annotation_set_id_display() {
2178        let id = AnnotationSetID::from(0xabcdef);
2179        assert_eq!(format!("{}", id), "as-abcdef");
2180    }
2181
2182    #[test]
2183    fn test_annotation_set_id_from_str_valid() {
2184        let id = AnnotationSetID::from_str("as-abcdef").unwrap();
2185        assert_eq!(id.value(), 0xabcdef);
2186    }
2187
2188    #[test]
2189    fn test_annotation_set_id_try_from_str_valid() {
2190        let id = AnnotationSetID::try_from("as-123456").unwrap();
2191        assert_eq!(id.value(), 0x123456);
2192    }
2193
2194    #[test]
2195    fn test_annotation_set_id_try_from_string_valid() {
2196        let id = AnnotationSetID::try_from("as-fedcba".to_string()).unwrap();
2197        assert_eq!(id.value(), 0xfedcba);
2198    }
2199
2200    #[test]
2201    fn test_annotation_set_id_from_str_invalid_prefix() {
2202        let result = AnnotationSetID::from_str("annotation-123");
2203        assert!(result.is_err());
2204        match result {
2205            Err(Error::InvalidParameters(msg)) => {
2206                assert!(msg.contains("must start with 'as-'"));
2207            }
2208            _ => panic!("Expected InvalidParameters error"),
2209        }
2210    }
2211
2212    #[test]
2213    fn test_annotation_set_id_from_str_invalid_hex() {
2214        let result = AnnotationSetID::from_str("as-zzz");
2215        assert!(result.is_err());
2216    }
2217
2218    #[test]
2219    fn test_annotation_set_id_into_u64() {
2220        let id = AnnotationSetID::from(444555);
2221        let value: u64 = id.into();
2222        assert_eq!(value, 444555);
2223    }
2224
2225    // ========== SampleID Tests ==========
2226    #[test]
2227    fn test_sample_id_from_u64() {
2228        let id = SampleID::from(666777);
2229        assert_eq!(id.value(), 666777);
2230    }
2231
2232    #[test]
2233    fn test_sample_id_display() {
2234        let id = SampleID::from(0x987654);
2235        assert_eq!(format!("{}", id), "s-987654");
2236    }
2237
2238    #[test]
2239    fn test_sample_id_try_from_str_valid() {
2240        let id = SampleID::try_from("s-987654").unwrap();
2241        assert_eq!(id.value(), 0x987654);
2242    }
2243
2244    #[test]
2245    fn test_sample_id_try_from_str_invalid_prefix() {
2246        let result = SampleID::try_from("sample-123");
2247        assert!(result.is_err());
2248        match result {
2249            Err(Error::InvalidParameters(msg)) => {
2250                assert!(msg.contains("must start with 's-'"));
2251            }
2252            _ => panic!("Expected InvalidParameters error"),
2253        }
2254    }
2255
2256    #[test]
2257    fn test_sample_id_try_from_str_invalid_hex() {
2258        let result = SampleID::try_from("s-zzz");
2259        assert!(result.is_err());
2260    }
2261
2262    #[test]
2263    fn test_sample_id_into_u64() {
2264        let id = SampleID::from(888999);
2265        let value: u64 = id.into();
2266        assert_eq!(value, 888999);
2267    }
2268
2269    // ========== AppId Tests ==========
2270    #[test]
2271    fn test_app_id_from_u64() {
2272        let id = AppId::from(123123);
2273        assert_eq!(id.value(), 123123);
2274    }
2275
2276    #[test]
2277    fn test_app_id_display() {
2278        let id = AppId::from(0x456789);
2279        assert_eq!(format!("{}", id), "app-456789");
2280    }
2281
2282    #[test]
2283    fn test_app_id_try_from_str_valid() {
2284        let id = AppId::try_from("app-456789").unwrap();
2285        assert_eq!(id.value(), 0x456789);
2286    }
2287
2288    #[test]
2289    fn test_app_id_try_from_str_invalid_prefix() {
2290        let result = AppId::try_from("application-123");
2291        assert!(result.is_err());
2292        match result {
2293            Err(Error::InvalidParameters(msg)) => {
2294                assert!(msg.contains("must start with 'app-'"));
2295            }
2296            _ => panic!("Expected InvalidParameters error"),
2297        }
2298    }
2299
2300    #[test]
2301    fn test_app_id_try_from_str_invalid_hex() {
2302        let result = AppId::try_from("app-zzz");
2303        assert!(result.is_err());
2304    }
2305
2306    #[test]
2307    fn test_app_id_into_u64() {
2308        let id = AppId::from(321321);
2309        let value: u64 = id.into();
2310        assert_eq!(value, 321321);
2311    }
2312
2313    // ========== ImageId Tests ==========
2314    #[test]
2315    fn test_image_id_from_u64() {
2316        let id = ImageId::from(789789);
2317        assert_eq!(id.value(), 789789);
2318    }
2319
2320    #[test]
2321    fn test_image_id_display() {
2322        let id = ImageId::from(0xabcd1234);
2323        assert_eq!(format!("{}", id), "im-abcd1234");
2324    }
2325
2326    #[test]
2327    fn test_image_id_try_from_str_valid() {
2328        let id = ImageId::try_from("im-abcd1234").unwrap();
2329        assert_eq!(id.value(), 0xabcd1234);
2330    }
2331
2332    #[test]
2333    fn test_image_id_try_from_str_invalid_prefix() {
2334        let result = ImageId::try_from("image-123");
2335        assert!(result.is_err());
2336        match result {
2337            Err(Error::InvalidParameters(msg)) => {
2338                assert!(msg.contains("must start with 'im-'"));
2339            }
2340            _ => panic!("Expected InvalidParameters error"),
2341        }
2342    }
2343
2344    #[test]
2345    fn test_image_id_try_from_str_invalid_hex() {
2346        let result = ImageId::try_from("im-zzz");
2347        assert!(result.is_err());
2348    }
2349
2350    #[test]
2351    fn test_image_id_into_u64() {
2352        let id = ImageId::from(987987);
2353        let value: u64 = id.into();
2354        assert_eq!(value, 987987);
2355    }
2356
2357    // ========== ID Type Hash and Equality Tests ==========
2358    #[test]
2359    fn test_id_types_equality() {
2360        let id1 = ProjectID::from(12345);
2361        let id2 = ProjectID::from(12345);
2362        let id3 = ProjectID::from(54321);
2363
2364        assert_eq!(id1, id2);
2365        assert_ne!(id1, id3);
2366    }
2367
2368    #[test]
2369    fn test_id_types_hash() {
2370        use std::collections::HashSet;
2371
2372        let mut set = HashSet::new();
2373        set.insert(DatasetID::from(100));
2374        set.insert(DatasetID::from(200));
2375        set.insert(DatasetID::from(100)); // duplicate
2376
2377        assert_eq!(set.len(), 2);
2378        assert!(set.contains(&DatasetID::from(100)));
2379        assert!(set.contains(&DatasetID::from(200)));
2380    }
2381
2382    #[test]
2383    fn test_id_types_copy_clone() {
2384        let id1 = ExperimentID::from(999);
2385        let id2 = id1; // Copy
2386        let id3 = id1; // Also Copy (no need for clone())
2387
2388        assert_eq!(id1, id2);
2389        assert_eq!(id1, id3);
2390    }
2391
2392    // ========== Edge Cases ==========
2393    #[test]
2394    fn test_id_zero_value() {
2395        let id = ProjectID::from(0);
2396        assert_eq!(format!("{}", id), "p-0");
2397        assert_eq!(id.value(), 0);
2398    }
2399
2400    #[test]
2401    fn test_id_max_value() {
2402        let id = ProjectID::from(u64::MAX);
2403        assert_eq!(format!("{}", id), "p-ffffffffffffffff");
2404        assert_eq!(id.value(), u64::MAX);
2405    }
2406
2407    #[test]
2408    fn test_id_round_trip_conversion() {
2409        let original = 0xdeadbeef_u64;
2410        let id = TrainingSessionID::from(original);
2411        let back: u64 = id.into();
2412        assert_eq!(original, back);
2413    }
2414
2415    #[test]
2416    fn test_id_case_insensitive_hex() {
2417        // Hexadecimal parsing should handle both upper and lowercase
2418        let id1 = DatasetID::from_str("ds-ABCDEF").unwrap();
2419        let id2 = DatasetID::from_str("ds-abcdef").unwrap();
2420        assert_eq!(id1.value(), id2.value());
2421    }
2422
2423    #[test]
2424    fn test_id_with_leading_zeros() {
2425        let id = ProjectID::from_str("p-00001234").unwrap();
2426        assert_eq!(id.value(), 0x1234);
2427    }
2428
2429    // ========== Parameter Tests ==========
2430    #[test]
2431    fn test_parameter_integer() {
2432        let param = Parameter::Integer(42);
2433        match param {
2434            Parameter::Integer(val) => assert_eq!(val, 42),
2435            _ => panic!("Expected Integer variant"),
2436        }
2437    }
2438
2439    #[test]
2440    fn test_parameter_real() {
2441        let param = Parameter::Real(2.5);
2442        match param {
2443            Parameter::Real(val) => assert_eq!(val, 2.5),
2444            _ => panic!("Expected Real variant"),
2445        }
2446    }
2447
2448    #[test]
2449    fn test_parameter_boolean() {
2450        let param = Parameter::Boolean(true);
2451        match param {
2452            Parameter::Boolean(val) => assert!(val),
2453            _ => panic!("Expected Boolean variant"),
2454        }
2455    }
2456
2457    #[test]
2458    fn test_parameter_string() {
2459        let param = Parameter::String("test".to_string());
2460        match param {
2461            Parameter::String(val) => assert_eq!(val, "test"),
2462            _ => panic!("Expected String variant"),
2463        }
2464    }
2465
2466    #[test]
2467    fn test_parameter_array() {
2468        let param = Parameter::Array(vec![
2469            Parameter::Integer(1),
2470            Parameter::Integer(2),
2471            Parameter::Integer(3),
2472        ]);
2473        match param {
2474            Parameter::Array(arr) => assert_eq!(arr.len(), 3),
2475            _ => panic!("Expected Array variant"),
2476        }
2477    }
2478
2479    #[test]
2480    fn test_parameter_object() {
2481        let mut map = HashMap::new();
2482        map.insert("key".to_string(), Parameter::Integer(100));
2483        let param = Parameter::Object(map);
2484        match param {
2485            Parameter::Object(obj) => {
2486                assert_eq!(obj.len(), 1);
2487                assert!(obj.contains_key("key"));
2488            }
2489            _ => panic!("Expected Object variant"),
2490        }
2491    }
2492
2493    #[test]
2494    fn test_parameter_clone() {
2495        let param1 = Parameter::Integer(42);
2496        let param2 = param1.clone();
2497        assert_eq!(param1, param2);
2498    }
2499
2500    #[test]
2501    fn test_parameter_nested() {
2502        let inner_array = Parameter::Array(vec![Parameter::Integer(1), Parameter::Integer(2)]);
2503        let outer_array = Parameter::Array(vec![inner_array.clone(), inner_array]);
2504
2505        match outer_array {
2506            Parameter::Array(arr) => {
2507                assert_eq!(arr.len(), 2);
2508            }
2509            _ => panic!("Expected Array variant"),
2510        }
2511    }
2512}