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    progress: TaskProgress,
1263    #[serde(rename = "created_date")]
1264    created: DateTime<Utc>,
1265    #[serde(rename = "end_date")]
1266    completed: DateTime<Utc>,
1267}
1268
1269impl Display for TaskInfo {
1270    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1271        write!(f, "{} {}: {}", self.id, self.workflow(), self.description())
1272    }
1273}
1274
1275impl TaskInfo {
1276    pub fn id(&self) -> TaskID {
1277        self.id
1278    }
1279
1280    pub fn project_id(&self) -> Option<ProjectID> {
1281        self.project_id
1282    }
1283
1284    pub fn description(&self) -> &str {
1285        &self.description
1286    }
1287
1288    pub fn workflow(&self) -> &str {
1289        &self.workflow
1290    }
1291
1292    pub fn status(&self) -> &Option<String> {
1293        &self.status
1294    }
1295
1296    pub async fn set_status(&mut self, client: &Client, status: &str) -> Result<(), Error> {
1297        let t = client.task_status(self.id(), status).await?;
1298        self.status = Some(t.status);
1299        Ok(())
1300    }
1301
1302    pub fn stages(&self) -> HashMap<String, Stage> {
1303        match &self.progress.stages {
1304            Some(stages) => stages.clone(),
1305            None => HashMap::new(),
1306        }
1307    }
1308
1309    pub async fn update_stage(
1310        &mut self,
1311        client: &Client,
1312        stage: &str,
1313        status: &str,
1314        message: &str,
1315        percentage: u8,
1316    ) -> Result<(), Error> {
1317        client
1318            .update_stage(self.id(), stage, status, message, percentage)
1319            .await?;
1320        let t = client.task_info(self.id()).await?;
1321        self.progress.stages = Some(t.progress.stages.unwrap_or_default());
1322        Ok(())
1323    }
1324
1325    pub async fn set_stages(
1326        &mut self,
1327        client: &Client,
1328        stages: &[(&str, &str)],
1329    ) -> Result<(), Error> {
1330        client.set_stages(self.id(), stages).await?;
1331        let t = client.task_info(self.id()).await?;
1332        self.progress.stages = Some(t.progress.stages.unwrap_or_default());
1333        Ok(())
1334    }
1335
1336    pub fn created(&self) -> &DateTime<Utc> {
1337        &self.created
1338    }
1339
1340    pub fn completed(&self) -> &DateTime<Utc> {
1341        &self.completed
1342    }
1343}
1344
1345#[derive(Deserialize, Debug)]
1346pub struct TaskProgress {
1347    stages: Option<HashMap<String, Stage>>,
1348}
1349
1350#[derive(Serialize, Debug, Clone)]
1351pub struct TaskStatus {
1352    #[serde(rename = "docker_task_id")]
1353    pub task_id: TaskID,
1354    pub status: String,
1355}
1356
1357#[derive(Serialize, Deserialize, Debug, Clone)]
1358pub struct Stage {
1359    #[serde(rename = "docker_task_id", skip_serializing_if = "Option::is_none")]
1360    task_id: Option<TaskID>,
1361    stage: String,
1362    #[serde(skip_serializing_if = "Option::is_none")]
1363    status: Option<String>,
1364    #[serde(skip_serializing_if = "Option::is_none")]
1365    description: Option<String>,
1366    #[serde(skip_serializing_if = "Option::is_none")]
1367    message: Option<String>,
1368    percentage: u8,
1369}
1370
1371impl Stage {
1372    pub fn new(
1373        task_id: Option<TaskID>,
1374        stage: String,
1375        status: Option<String>,
1376        message: Option<String>,
1377        percentage: u8,
1378    ) -> Self {
1379        Stage {
1380            task_id,
1381            stage,
1382            status,
1383            description: None,
1384            message,
1385            percentage,
1386        }
1387    }
1388
1389    pub fn task_id(&self) -> &Option<TaskID> {
1390        &self.task_id
1391    }
1392
1393    pub fn stage(&self) -> &str {
1394        &self.stage
1395    }
1396
1397    pub fn status(&self) -> &Option<String> {
1398        &self.status
1399    }
1400
1401    pub fn description(&self) -> &Option<String> {
1402        &self.description
1403    }
1404
1405    pub fn message(&self) -> &Option<String> {
1406        &self.message
1407    }
1408
1409    pub fn percentage(&self) -> u8 {
1410        self.percentage
1411    }
1412}
1413
1414#[derive(Serialize, Debug)]
1415pub struct TaskStages {
1416    #[serde(rename = "docker_task_id")]
1417    pub task_id: TaskID,
1418    #[serde(skip_serializing_if = "Vec::is_empty")]
1419    pub stages: Vec<HashMap<String, String>>,
1420}
1421
1422#[derive(Deserialize, Debug)]
1423pub struct Artifact {
1424    name: String,
1425    #[serde(rename = "modelType")]
1426    model_type: String,
1427}
1428
1429impl Artifact {
1430    pub fn name(&self) -> &str {
1431        &self.name
1432    }
1433
1434    pub fn model_type(&self) -> &str {
1435        &self.model_type
1436    }
1437}
1438
1439#[cfg(test)]
1440mod tests {
1441    use super::*;
1442
1443    // ========== OrganizationID Tests ==========
1444    #[test]
1445    fn test_organization_id_from_u64() {
1446        let id = OrganizationID::from(12345);
1447        assert_eq!(id.value(), 12345);
1448    }
1449
1450    #[test]
1451    fn test_organization_id_display() {
1452        let id = OrganizationID::from(0xabc123);
1453        assert_eq!(format!("{}", id), "org-abc123");
1454    }
1455
1456    #[test]
1457    fn test_organization_id_try_from_str_valid() {
1458        let id = OrganizationID::try_from("org-abc123").unwrap();
1459        assert_eq!(id.value(), 0xabc123);
1460    }
1461
1462    #[test]
1463    fn test_organization_id_try_from_str_invalid_prefix() {
1464        let result = OrganizationID::try_from("invalid-abc123");
1465        assert!(result.is_err());
1466        match result {
1467            Err(Error::InvalidParameters(msg)) => {
1468                assert!(msg.contains("must start with 'org-'"));
1469            }
1470            _ => panic!("Expected InvalidParameters error"),
1471        }
1472    }
1473
1474    #[test]
1475    fn test_organization_id_try_from_str_invalid_hex() {
1476        let result = OrganizationID::try_from("org-xyz");
1477        assert!(result.is_err());
1478    }
1479
1480    #[test]
1481    fn test_organization_id_try_from_str_empty() {
1482        let result = OrganizationID::try_from("org-");
1483        assert!(result.is_err());
1484    }
1485
1486    #[test]
1487    fn test_organization_id_into_u64() {
1488        let id = OrganizationID::from(54321);
1489        let value: u64 = id.into();
1490        assert_eq!(value, 54321);
1491    }
1492
1493    // ========== ProjectID Tests ==========
1494    #[test]
1495    fn test_project_id_from_u64() {
1496        let id = ProjectID::from(78910);
1497        assert_eq!(id.value(), 78910);
1498    }
1499
1500    #[test]
1501    fn test_project_id_display() {
1502        let id = ProjectID::from(0xdef456);
1503        assert_eq!(format!("{}", id), "p-def456");
1504    }
1505
1506    #[test]
1507    fn test_project_id_from_str_valid() {
1508        let id = ProjectID::from_str("p-def456").unwrap();
1509        assert_eq!(id.value(), 0xdef456);
1510    }
1511
1512    #[test]
1513    fn test_project_id_try_from_str_valid() {
1514        let id = ProjectID::try_from("p-123abc").unwrap();
1515        assert_eq!(id.value(), 0x123abc);
1516    }
1517
1518    #[test]
1519    fn test_project_id_try_from_string_valid() {
1520        let id = ProjectID::try_from("p-456def".to_string()).unwrap();
1521        assert_eq!(id.value(), 0x456def);
1522    }
1523
1524    #[test]
1525    fn test_project_id_from_str_invalid_prefix() {
1526        let result = ProjectID::from_str("proj-123");
1527        assert!(result.is_err());
1528        match result {
1529            Err(Error::InvalidParameters(msg)) => {
1530                assert!(msg.contains("must start with 'p-'"));
1531            }
1532            _ => panic!("Expected InvalidParameters error"),
1533        }
1534    }
1535
1536    #[test]
1537    fn test_project_id_from_str_invalid_hex() {
1538        let result = ProjectID::from_str("p-notahex");
1539        assert!(result.is_err());
1540    }
1541
1542    #[test]
1543    fn test_project_id_into_u64() {
1544        let id = ProjectID::from(99999);
1545        let value: u64 = id.into();
1546        assert_eq!(value, 99999);
1547    }
1548
1549    // ========== ExperimentID Tests ==========
1550    #[test]
1551    fn test_experiment_id_from_u64() {
1552        let id = ExperimentID::from(1193046);
1553        assert_eq!(id.value(), 1193046);
1554    }
1555
1556    #[test]
1557    fn test_experiment_id_display() {
1558        let id = ExperimentID::from(0x123abc);
1559        assert_eq!(format!("{}", id), "exp-123abc");
1560    }
1561
1562    #[test]
1563    fn test_experiment_id_from_str_valid() {
1564        let id = ExperimentID::from_str("exp-456def").unwrap();
1565        assert_eq!(id.value(), 0x456def);
1566    }
1567
1568    #[test]
1569    fn test_experiment_id_try_from_str_valid() {
1570        let id = ExperimentID::try_from("exp-789abc").unwrap();
1571        assert_eq!(id.value(), 0x789abc);
1572    }
1573
1574    #[test]
1575    fn test_experiment_id_try_from_string_valid() {
1576        let id = ExperimentID::try_from("exp-fedcba".to_string()).unwrap();
1577        assert_eq!(id.value(), 0xfedcba);
1578    }
1579
1580    #[test]
1581    fn test_experiment_id_from_str_invalid_prefix() {
1582        let result = ExperimentID::from_str("experiment-123");
1583        assert!(result.is_err());
1584        match result {
1585            Err(Error::InvalidParameters(msg)) => {
1586                assert!(msg.contains("must start with 'exp-'"));
1587            }
1588            _ => panic!("Expected InvalidParameters error"),
1589        }
1590    }
1591
1592    #[test]
1593    fn test_experiment_id_from_str_invalid_hex() {
1594        let result = ExperimentID::from_str("exp-zzz");
1595        assert!(result.is_err());
1596    }
1597
1598    #[test]
1599    fn test_experiment_id_into_u64() {
1600        let id = ExperimentID::from(777777);
1601        let value: u64 = id.into();
1602        assert_eq!(value, 777777);
1603    }
1604
1605    // ========== TrainingSessionID Tests ==========
1606    #[test]
1607    fn test_training_session_id_from_u64() {
1608        let id = TrainingSessionID::from(7901234);
1609        assert_eq!(id.value(), 7901234);
1610    }
1611
1612    #[test]
1613    fn test_training_session_id_display() {
1614        let id = TrainingSessionID::from(0xabc123);
1615        assert_eq!(format!("{}", id), "t-abc123");
1616    }
1617
1618    #[test]
1619    fn test_training_session_id_from_str_valid() {
1620        let id = TrainingSessionID::from_str("t-abc123").unwrap();
1621        assert_eq!(id.value(), 0xabc123);
1622    }
1623
1624    #[test]
1625    fn test_training_session_id_try_from_str_valid() {
1626        let id = TrainingSessionID::try_from("t-deadbeef").unwrap();
1627        assert_eq!(id.value(), 0xdeadbeef);
1628    }
1629
1630    #[test]
1631    fn test_training_session_id_try_from_string_valid() {
1632        let id = TrainingSessionID::try_from("t-cafebabe".to_string()).unwrap();
1633        assert_eq!(id.value(), 0xcafebabe);
1634    }
1635
1636    #[test]
1637    fn test_training_session_id_from_str_invalid_prefix() {
1638        let result = TrainingSessionID::from_str("training-123");
1639        assert!(result.is_err());
1640        match result {
1641            Err(Error::InvalidParameters(msg)) => {
1642                assert!(msg.contains("must start with 't-'"));
1643            }
1644            _ => panic!("Expected InvalidParameters error"),
1645        }
1646    }
1647
1648    #[test]
1649    fn test_training_session_id_from_str_invalid_hex() {
1650        let result = TrainingSessionID::from_str("t-qqq");
1651        assert!(result.is_err());
1652    }
1653
1654    #[test]
1655    fn test_training_session_id_into_u64() {
1656        let id = TrainingSessionID::from(123456);
1657        let value: u64 = id.into();
1658        assert_eq!(value, 123456);
1659    }
1660
1661    // ========== ValidationSessionID Tests ==========
1662    #[test]
1663    fn test_validation_session_id_from_u64() {
1664        let id = ValidationSessionID::from(3456789);
1665        assert_eq!(id.value(), 3456789);
1666    }
1667
1668    #[test]
1669    fn test_validation_session_id_display() {
1670        let id = ValidationSessionID::from(0x34c985);
1671        assert_eq!(format!("{}", id), "v-34c985");
1672    }
1673
1674    #[test]
1675    fn test_validation_session_id_try_from_str_valid() {
1676        let id = ValidationSessionID::try_from("v-deadbeef").unwrap();
1677        assert_eq!(id.value(), 0xdeadbeef);
1678    }
1679
1680    #[test]
1681    fn test_validation_session_id_try_from_string_valid() {
1682        let id = ValidationSessionID::try_from("v-12345678".to_string()).unwrap();
1683        assert_eq!(id.value(), 0x12345678);
1684    }
1685
1686    #[test]
1687    fn test_validation_session_id_try_from_str_invalid_prefix() {
1688        let result = ValidationSessionID::try_from("validation-123");
1689        assert!(result.is_err());
1690        match result {
1691            Err(Error::InvalidParameters(msg)) => {
1692                assert!(msg.contains("must start with 'v-'"));
1693            }
1694            _ => panic!("Expected InvalidParameters error"),
1695        }
1696    }
1697
1698    #[test]
1699    fn test_validation_session_id_try_from_str_invalid_hex() {
1700        let result = ValidationSessionID::try_from("v-xyz");
1701        assert!(result.is_err());
1702    }
1703
1704    #[test]
1705    fn test_validation_session_id_into_u64() {
1706        let id = ValidationSessionID::from(987654);
1707        let value: u64 = id.into();
1708        assert_eq!(value, 987654);
1709    }
1710
1711    // ========== SnapshotID Tests ==========
1712    #[test]
1713    fn test_snapshot_id_from_u64() {
1714        let id = SnapshotID::from(111222);
1715        assert_eq!(id.value(), 111222);
1716    }
1717
1718    #[test]
1719    fn test_snapshot_id_display() {
1720        let id = SnapshotID::from(0xaabbcc);
1721        assert_eq!(format!("{}", id), "ss-aabbcc");
1722    }
1723
1724    #[test]
1725    fn test_snapshot_id_try_from_str_valid() {
1726        let id = SnapshotID::try_from("ss-aabbcc").unwrap();
1727        assert_eq!(id.value(), 0xaabbcc);
1728    }
1729
1730    #[test]
1731    fn test_snapshot_id_try_from_str_invalid_prefix() {
1732        let result = SnapshotID::try_from("snapshot-123");
1733        assert!(result.is_err());
1734        match result {
1735            Err(Error::InvalidParameters(msg)) => {
1736                assert!(msg.contains("must start with 'ss-'"));
1737            }
1738            _ => panic!("Expected InvalidParameters error"),
1739        }
1740    }
1741
1742    #[test]
1743    fn test_snapshot_id_try_from_str_invalid_hex() {
1744        let result = SnapshotID::try_from("ss-ggg");
1745        assert!(result.is_err());
1746    }
1747
1748    #[test]
1749    fn test_snapshot_id_into_u64() {
1750        let id = SnapshotID::from(333444);
1751        let value: u64 = id.into();
1752        assert_eq!(value, 333444);
1753    }
1754
1755    // ========== TaskID Tests ==========
1756    #[test]
1757    fn test_task_id_from_u64() {
1758        let id = TaskID::from(555666);
1759        assert_eq!(id.value(), 555666);
1760    }
1761
1762    #[test]
1763    fn test_task_id_display() {
1764        let id = TaskID::from(0x123456);
1765        assert_eq!(format!("{}", id), "task-123456");
1766    }
1767
1768    #[test]
1769    fn test_task_id_from_str_valid() {
1770        let id = TaskID::from_str("task-123456").unwrap();
1771        assert_eq!(id.value(), 0x123456);
1772    }
1773
1774    #[test]
1775    fn test_task_id_try_from_str_valid() {
1776        let id = TaskID::try_from("task-abcdef").unwrap();
1777        assert_eq!(id.value(), 0xabcdef);
1778    }
1779
1780    #[test]
1781    fn test_task_id_try_from_string_valid() {
1782        let id = TaskID::try_from("task-fedcba".to_string()).unwrap();
1783        assert_eq!(id.value(), 0xfedcba);
1784    }
1785
1786    #[test]
1787    fn test_task_id_from_str_invalid_prefix() {
1788        let result = TaskID::from_str("t-123");
1789        assert!(result.is_err());
1790        match result {
1791            Err(Error::InvalidParameters(msg)) => {
1792                assert!(msg.contains("must start with 'task-'"));
1793            }
1794            _ => panic!("Expected InvalidParameters error"),
1795        }
1796    }
1797
1798    #[test]
1799    fn test_task_id_from_str_invalid_hex() {
1800        let result = TaskID::from_str("task-zzz");
1801        assert!(result.is_err());
1802    }
1803
1804    #[test]
1805    fn test_task_id_into_u64() {
1806        let id = TaskID::from(777888);
1807        let value: u64 = id.into();
1808        assert_eq!(value, 777888);
1809    }
1810
1811    // ========== DatasetID Tests ==========
1812    #[test]
1813    fn test_dataset_id_from_u64() {
1814        let id = DatasetID::from(1193046);
1815        assert_eq!(id.value(), 1193046);
1816    }
1817
1818    #[test]
1819    fn test_dataset_id_display() {
1820        let id = DatasetID::from(0x123abc);
1821        assert_eq!(format!("{}", id), "ds-123abc");
1822    }
1823
1824    #[test]
1825    fn test_dataset_id_from_str_valid() {
1826        let id = DatasetID::from_str("ds-456def").unwrap();
1827        assert_eq!(id.value(), 0x456def);
1828    }
1829
1830    #[test]
1831    fn test_dataset_id_try_from_str_valid() {
1832        let id = DatasetID::try_from("ds-789abc").unwrap();
1833        assert_eq!(id.value(), 0x789abc);
1834    }
1835
1836    #[test]
1837    fn test_dataset_id_try_from_string_valid() {
1838        let id = DatasetID::try_from("ds-fedcba".to_string()).unwrap();
1839        assert_eq!(id.value(), 0xfedcba);
1840    }
1841
1842    #[test]
1843    fn test_dataset_id_from_str_invalid_prefix() {
1844        let result = DatasetID::from_str("dataset-123");
1845        assert!(result.is_err());
1846        match result {
1847            Err(Error::InvalidParameters(msg)) => {
1848                assert!(msg.contains("must start with 'ds-'"));
1849            }
1850            _ => panic!("Expected InvalidParameters error"),
1851        }
1852    }
1853
1854    #[test]
1855    fn test_dataset_id_from_str_invalid_hex() {
1856        let result = DatasetID::from_str("ds-zzz");
1857        assert!(result.is_err());
1858    }
1859
1860    #[test]
1861    fn test_dataset_id_into_u64() {
1862        let id = DatasetID::from(111111);
1863        let value: u64 = id.into();
1864        assert_eq!(value, 111111);
1865    }
1866
1867    // ========== AnnotationSetID Tests ==========
1868    #[test]
1869    fn test_annotation_set_id_from_u64() {
1870        let id = AnnotationSetID::from(222333);
1871        assert_eq!(id.value(), 222333);
1872    }
1873
1874    #[test]
1875    fn test_annotation_set_id_display() {
1876        let id = AnnotationSetID::from(0xabcdef);
1877        assert_eq!(format!("{}", id), "as-abcdef");
1878    }
1879
1880    #[test]
1881    fn test_annotation_set_id_from_str_valid() {
1882        let id = AnnotationSetID::from_str("as-abcdef").unwrap();
1883        assert_eq!(id.value(), 0xabcdef);
1884    }
1885
1886    #[test]
1887    fn test_annotation_set_id_try_from_str_valid() {
1888        let id = AnnotationSetID::try_from("as-123456").unwrap();
1889        assert_eq!(id.value(), 0x123456);
1890    }
1891
1892    #[test]
1893    fn test_annotation_set_id_try_from_string_valid() {
1894        let id = AnnotationSetID::try_from("as-fedcba".to_string()).unwrap();
1895        assert_eq!(id.value(), 0xfedcba);
1896    }
1897
1898    #[test]
1899    fn test_annotation_set_id_from_str_invalid_prefix() {
1900        let result = AnnotationSetID::from_str("annotation-123");
1901        assert!(result.is_err());
1902        match result {
1903            Err(Error::InvalidParameters(msg)) => {
1904                assert!(msg.contains("must start with 'as-'"));
1905            }
1906            _ => panic!("Expected InvalidParameters error"),
1907        }
1908    }
1909
1910    #[test]
1911    fn test_annotation_set_id_from_str_invalid_hex() {
1912        let result = AnnotationSetID::from_str("as-zzz");
1913        assert!(result.is_err());
1914    }
1915
1916    #[test]
1917    fn test_annotation_set_id_into_u64() {
1918        let id = AnnotationSetID::from(444555);
1919        let value: u64 = id.into();
1920        assert_eq!(value, 444555);
1921    }
1922
1923    // ========== SampleID Tests ==========
1924    #[test]
1925    fn test_sample_id_from_u64() {
1926        let id = SampleID::from(666777);
1927        assert_eq!(id.value(), 666777);
1928    }
1929
1930    #[test]
1931    fn test_sample_id_display() {
1932        let id = SampleID::from(0x987654);
1933        assert_eq!(format!("{}", id), "s-987654");
1934    }
1935
1936    #[test]
1937    fn test_sample_id_try_from_str_valid() {
1938        let id = SampleID::try_from("s-987654").unwrap();
1939        assert_eq!(id.value(), 0x987654);
1940    }
1941
1942    #[test]
1943    fn test_sample_id_try_from_str_invalid_prefix() {
1944        let result = SampleID::try_from("sample-123");
1945        assert!(result.is_err());
1946        match result {
1947            Err(Error::InvalidParameters(msg)) => {
1948                assert!(msg.contains("must start with 's-'"));
1949            }
1950            _ => panic!("Expected InvalidParameters error"),
1951        }
1952    }
1953
1954    #[test]
1955    fn test_sample_id_try_from_str_invalid_hex() {
1956        let result = SampleID::try_from("s-zzz");
1957        assert!(result.is_err());
1958    }
1959
1960    #[test]
1961    fn test_sample_id_into_u64() {
1962        let id = SampleID::from(888999);
1963        let value: u64 = id.into();
1964        assert_eq!(value, 888999);
1965    }
1966
1967    // ========== AppId Tests ==========
1968    #[test]
1969    fn test_app_id_from_u64() {
1970        let id = AppId::from(123123);
1971        assert_eq!(id.value(), 123123);
1972    }
1973
1974    #[test]
1975    fn test_app_id_display() {
1976        let id = AppId::from(0x456789);
1977        assert_eq!(format!("{}", id), "app-456789");
1978    }
1979
1980    #[test]
1981    fn test_app_id_try_from_str_valid() {
1982        let id = AppId::try_from("app-456789").unwrap();
1983        assert_eq!(id.value(), 0x456789);
1984    }
1985
1986    #[test]
1987    fn test_app_id_try_from_str_invalid_prefix() {
1988        let result = AppId::try_from("application-123");
1989        assert!(result.is_err());
1990        match result {
1991            Err(Error::InvalidParameters(msg)) => {
1992                assert!(msg.contains("must start with 'app-'"));
1993            }
1994            _ => panic!("Expected InvalidParameters error"),
1995        }
1996    }
1997
1998    #[test]
1999    fn test_app_id_try_from_str_invalid_hex() {
2000        let result = AppId::try_from("app-zzz");
2001        assert!(result.is_err());
2002    }
2003
2004    #[test]
2005    fn test_app_id_into_u64() {
2006        let id = AppId::from(321321);
2007        let value: u64 = id.into();
2008        assert_eq!(value, 321321);
2009    }
2010
2011    // ========== ImageId Tests ==========
2012    #[test]
2013    fn test_image_id_from_u64() {
2014        let id = ImageId::from(789789);
2015        assert_eq!(id.value(), 789789);
2016    }
2017
2018    #[test]
2019    fn test_image_id_display() {
2020        let id = ImageId::from(0xabcd1234);
2021        assert_eq!(format!("{}", id), "im-abcd1234");
2022    }
2023
2024    #[test]
2025    fn test_image_id_try_from_str_valid() {
2026        let id = ImageId::try_from("im-abcd1234").unwrap();
2027        assert_eq!(id.value(), 0xabcd1234);
2028    }
2029
2030    #[test]
2031    fn test_image_id_try_from_str_invalid_prefix() {
2032        let result = ImageId::try_from("image-123");
2033        assert!(result.is_err());
2034        match result {
2035            Err(Error::InvalidParameters(msg)) => {
2036                assert!(msg.contains("must start with 'im-'"));
2037            }
2038            _ => panic!("Expected InvalidParameters error"),
2039        }
2040    }
2041
2042    #[test]
2043    fn test_image_id_try_from_str_invalid_hex() {
2044        let result = ImageId::try_from("im-zzz");
2045        assert!(result.is_err());
2046    }
2047
2048    #[test]
2049    fn test_image_id_into_u64() {
2050        let id = ImageId::from(987987);
2051        let value: u64 = id.into();
2052        assert_eq!(value, 987987);
2053    }
2054
2055    // ========== ID Type Hash and Equality Tests ==========
2056    #[test]
2057    fn test_id_types_equality() {
2058        let id1 = ProjectID::from(12345);
2059        let id2 = ProjectID::from(12345);
2060        let id3 = ProjectID::from(54321);
2061
2062        assert_eq!(id1, id2);
2063        assert_ne!(id1, id3);
2064    }
2065
2066    #[test]
2067    fn test_id_types_hash() {
2068        use std::collections::HashSet;
2069
2070        let mut set = HashSet::new();
2071        set.insert(DatasetID::from(100));
2072        set.insert(DatasetID::from(200));
2073        set.insert(DatasetID::from(100)); // duplicate
2074
2075        assert_eq!(set.len(), 2);
2076        assert!(set.contains(&DatasetID::from(100)));
2077        assert!(set.contains(&DatasetID::from(200)));
2078    }
2079
2080    #[test]
2081    fn test_id_types_copy_clone() {
2082        let id1 = ExperimentID::from(999);
2083        let id2 = id1; // Copy
2084        let id3 = id1; // Also Copy (no need for clone())
2085
2086        assert_eq!(id1, id2);
2087        assert_eq!(id1, id3);
2088    }
2089
2090    // ========== Edge Cases ==========
2091    #[test]
2092    fn test_id_zero_value() {
2093        let id = ProjectID::from(0);
2094        assert_eq!(format!("{}", id), "p-0");
2095        assert_eq!(id.value(), 0);
2096    }
2097
2098    #[test]
2099    fn test_id_max_value() {
2100        let id = ProjectID::from(u64::MAX);
2101        assert_eq!(format!("{}", id), "p-ffffffffffffffff");
2102        assert_eq!(id.value(), u64::MAX);
2103    }
2104
2105    #[test]
2106    fn test_id_round_trip_conversion() {
2107        let original = 0xdeadbeef_u64;
2108        let id = TrainingSessionID::from(original);
2109        let back: u64 = id.into();
2110        assert_eq!(original, back);
2111    }
2112
2113    #[test]
2114    fn test_id_case_insensitive_hex() {
2115        // Hexadecimal parsing should handle both upper and lowercase
2116        let id1 = DatasetID::from_str("ds-ABCDEF").unwrap();
2117        let id2 = DatasetID::from_str("ds-abcdef").unwrap();
2118        assert_eq!(id1.value(), id2.value());
2119    }
2120
2121    #[test]
2122    fn test_id_with_leading_zeros() {
2123        let id = ProjectID::from_str("p-00001234").unwrap();
2124        assert_eq!(id.value(), 0x1234);
2125    }
2126
2127    // ========== Parameter Tests ==========
2128    #[test]
2129    fn test_parameter_integer() {
2130        let param = Parameter::Integer(42);
2131        match param {
2132            Parameter::Integer(val) => assert_eq!(val, 42),
2133            _ => panic!("Expected Integer variant"),
2134        }
2135    }
2136
2137    #[test]
2138    fn test_parameter_real() {
2139        let param = Parameter::Real(2.5);
2140        match param {
2141            Parameter::Real(val) => assert_eq!(val, 2.5),
2142            _ => panic!("Expected Real variant"),
2143        }
2144    }
2145
2146    #[test]
2147    fn test_parameter_boolean() {
2148        let param = Parameter::Boolean(true);
2149        match param {
2150            Parameter::Boolean(val) => assert!(val),
2151            _ => panic!("Expected Boolean variant"),
2152        }
2153    }
2154
2155    #[test]
2156    fn test_parameter_string() {
2157        let param = Parameter::String("test".to_string());
2158        match param {
2159            Parameter::String(val) => assert_eq!(val, "test"),
2160            _ => panic!("Expected String variant"),
2161        }
2162    }
2163
2164    #[test]
2165    fn test_parameter_array() {
2166        let param = Parameter::Array(vec![
2167            Parameter::Integer(1),
2168            Parameter::Integer(2),
2169            Parameter::Integer(3),
2170        ]);
2171        match param {
2172            Parameter::Array(arr) => assert_eq!(arr.len(), 3),
2173            _ => panic!("Expected Array variant"),
2174        }
2175    }
2176
2177    #[test]
2178    fn test_parameter_object() {
2179        let mut map = HashMap::new();
2180        map.insert("key".to_string(), Parameter::Integer(100));
2181        let param = Parameter::Object(map);
2182        match param {
2183            Parameter::Object(obj) => {
2184                assert_eq!(obj.len(), 1);
2185                assert!(obj.contains_key("key"));
2186            }
2187            _ => panic!("Expected Object variant"),
2188        }
2189    }
2190
2191    #[test]
2192    fn test_parameter_clone() {
2193        let param1 = Parameter::Integer(42);
2194        let param2 = param1.clone();
2195        assert_eq!(param1, param2);
2196    }
2197
2198    #[test]
2199    fn test_parameter_nested() {
2200        let inner_array = Parameter::Array(vec![Parameter::Integer(1), Parameter::Integer(2)]);
2201        let outer_array = Parameter::Array(vec![inner_array.clone(), inner_array]);
2202
2203        match outer_array {
2204            Parameter::Array(arr) => {
2205                assert_eq!(arr.len(), 2);
2206            }
2207            _ => panic!("Expected Array variant"),
2208        }
2209    }
2210
2211    // ========== Comprehensive TypeID Conversion Tests (macro-driven) ==========
2212
2213    macro_rules! test_typeid_conversions {
2214        ($test_name:ident, $type:ty, $prefix:literal, $wrong_prefix:literal) => {
2215            #[test]
2216            fn $test_name() {
2217                // 1. From<u64> round-trip
2218                let id = <$type>::from(0xabc123);
2219                assert_eq!(id.value(), 0xabc123);
2220
2221                // 2. Display format
2222                assert_eq!(format!("{}", id), concat!($prefix, "-abc123"));
2223
2224                // 3. FromStr valid
2225                let id: $type = concat!($prefix, "-abc123").parse().unwrap();
2226                assert_eq!(id.value(), 0xabc123);
2227
2228                // 4. FromStr wrong prefix
2229                assert!(concat!($wrong_prefix, "-abc").parse::<$type>().is_err());
2230
2231                // 5. FromStr missing prefix
2232                assert!("abc123".parse::<$type>().is_err());
2233
2234                // 6. FromStr invalid hex
2235                assert!(concat!($prefix, "-xyz").parse::<$type>().is_err());
2236
2237                // 7. TryFrom<&str>
2238                let id = <$type>::try_from(concat!($prefix, "-abc123")).unwrap();
2239                assert_eq!(id.value(), 0xabc123);
2240
2241                // 8. TryFrom<String>
2242                let id = <$type>::try_from(concat!($prefix, "-abc123").to_string()).unwrap();
2243                assert_eq!(id.value(), 0xabc123);
2244
2245                // 9. Serde round-trip
2246                let id = <$type>::from(0xabc123);
2247                let json = serde_json::to_string(&id).unwrap();
2248                let parsed: $type = serde_json::from_str(&json).unwrap();
2249                assert_eq!(id, parsed);
2250
2251                // 10. From<T> for u64
2252                let id = <$type>::from(0xabc123);
2253                let val: u64 = id.into();
2254                assert_eq!(val, 0xabc123);
2255            }
2256        };
2257    }
2258
2259    test_typeid_conversions!(test_organization_id_conversions, OrganizationID, "org", "p");
2260    test_typeid_conversions!(test_project_id_conversions, ProjectID, "p", "org");
2261    test_typeid_conversions!(test_experiment_id_conversions, ExperimentID, "exp", "p");
2262    test_typeid_conversions!(
2263        test_training_session_id_conversions,
2264        TrainingSessionID,
2265        "t",
2266        "v"
2267    );
2268    test_typeid_conversions!(
2269        test_validation_session_id_conversions,
2270        ValidationSessionID,
2271        "v",
2272        "t"
2273    );
2274    test_typeid_conversions!(test_snapshot_id_conversions, SnapshotID, "ss", "ds");
2275    test_typeid_conversions!(test_task_id_conversions, TaskID, "task", "t");
2276    test_typeid_conversions!(test_dataset_id_conversions, DatasetID, "ds", "ss");
2277    test_typeid_conversions!(
2278        test_annotation_set_id_conversions,
2279        AnnotationSetID,
2280        "as",
2281        "ds"
2282    );
2283    test_typeid_conversions!(test_sample_id_conversions, SampleID, "s", "p");
2284    test_typeid_conversions!(test_app_id_conversions, AppId, "app", "p");
2285    test_typeid_conversions!(test_image_id_conversions, ImageId, "im", "se");
2286    test_typeid_conversions!(test_sequence_id_conversions, SequenceId, "se", "im");
2287}