Skip to main content

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