llm_registry_api/grpc/
converters.rs

1// ! Type converters between protobuf and domain types
2//!
3//! This module provides conversion functions between the generated protobuf types
4//! and the core domain types used throughout the registry.
5
6use super::proto;
7use crate::error::ApiError;
8use llm_registry_core::{
9    Asset, AssetId, AssetMetadata, AssetReference, AssetStatus, AssetType, Checksum,
10    HashAlgorithm, Provenance, StorageBackend, StorageLocation,
11};
12use llm_registry_service::{DependencyNode, SortField, SortOrder};
13use semver::Version;
14
15// ============================================================================
16// Enum Conversions
17// ============================================================================
18
19impl From<AssetType> for proto::AssetType {
20    fn from(at: AssetType) -> Self {
21        match at {
22            AssetType::Model => proto::AssetType::Model,
23            AssetType::Pipeline => proto::AssetType::Pipeline,
24            AssetType::TestSuite => proto::AssetType::TestSuite,
25            AssetType::Policy => proto::AssetType::Policy,
26            AssetType::Dataset => proto::AssetType::Dataset,
27            AssetType::Custom(_) => proto::AssetType::Model, // Default for custom
28        }
29    }
30}
31
32/// Convert i32 to AssetType (helper function to avoid orphan rule violations)
33pub fn asset_type_from_i32(value: i32) -> Result<AssetType, ApiError> {
34    match proto::AssetType::try_from(value) {
35        Ok(proto::AssetType::Unspecified) => {
36            Err(ApiError::bad_request("Asset type must be specified"))
37        }
38        Ok(proto::AssetType::Model) => Ok(AssetType::Model),
39        Ok(proto::AssetType::Pipeline) => Ok(AssetType::Pipeline),
40        Ok(proto::AssetType::TestSuite) => Ok(AssetType::TestSuite),
41        Ok(proto::AssetType::Policy) => Ok(AssetType::Policy),
42        Ok(proto::AssetType::Dataset) => Ok(AssetType::Dataset),
43        Err(_) => Err(ApiError::bad_request("Invalid asset type")),
44    }
45}
46
47impl From<AssetStatus> for proto::AssetStatus {
48    fn from(status: AssetStatus) -> Self {
49        match status {
50            AssetStatus::Active => proto::AssetStatus::Active,
51            AssetStatus::Deprecated => proto::AssetStatus::Deprecated,
52            AssetStatus::Archived => proto::AssetStatus::Archived,
53            AssetStatus::NonCompliant => proto::AssetStatus::NonCompliant,
54        }
55    }
56}
57
58/// Convert i32 to AssetStatus (helper function to avoid orphan rule violations)
59pub fn asset_status_from_i32(value: i32) -> Result<AssetStatus, ApiError> {
60    match proto::AssetStatus::try_from(value) {
61        Ok(proto::AssetStatus::Unspecified) => Ok(AssetStatus::Active),
62        Ok(proto::AssetStatus::Active) => Ok(AssetStatus::Active),
63        Ok(proto::AssetStatus::Deprecated) => Ok(AssetStatus::Deprecated),
64        Ok(proto::AssetStatus::Archived) => Ok(AssetStatus::Archived),
65        Ok(proto::AssetStatus::NonCompliant) => Ok(AssetStatus::NonCompliant),
66        Err(_) => Err(ApiError::bad_request("Invalid asset status")),
67    }
68}
69
70impl From<HashAlgorithm> for proto::HashAlgorithm {
71    fn from(alg: HashAlgorithm) -> Self {
72        match alg {
73            HashAlgorithm::SHA256 => proto::HashAlgorithm::Sha256,
74            HashAlgorithm::SHA3_256 => proto::HashAlgorithm::Sha3256,
75            HashAlgorithm::BLAKE3 => proto::HashAlgorithm::Blake3,
76        }
77    }
78}
79
80/// Convert i32 to HashAlgorithm (helper function to avoid orphan rule violations)
81pub fn hash_algorithm_from_i32(value: i32) -> Result<HashAlgorithm, ApiError> {
82    match proto::HashAlgorithm::try_from(value) {
83        Ok(proto::HashAlgorithm::Unspecified) | Ok(proto::HashAlgorithm::Sha256) => {
84            Ok(HashAlgorithm::SHA256)
85        }
86        Ok(proto::HashAlgorithm::Sha3256) => Ok(HashAlgorithm::SHA3_256),
87        Ok(proto::HashAlgorithm::Blake3) => Ok(HashAlgorithm::BLAKE3),
88        Err(_) => Err(ApiError::bad_request("Invalid hash algorithm")),
89    }
90}
91
92impl From<SortField> for proto::SortField {
93    fn from(field: SortField) -> Self {
94        match field {
95            SortField::CreatedAt => proto::SortField::CreatedAt,
96            SortField::UpdatedAt => proto::SortField::UpdatedAt,
97            SortField::Name => proto::SortField::Name,
98            SortField::Version => proto::SortField::Version,
99            SortField::SizeBytes => proto::SortField::SizeBytes,
100        }
101    }
102}
103
104/// Convert i32 to SortField (helper function to avoid orphan rule violations)
105pub fn sort_field_from_i32(value: i32) -> Result<SortField, ApiError> {
106    match proto::SortField::try_from(value) {
107        Ok(proto::SortField::Unspecified) | Ok(proto::SortField::CreatedAt) => {
108            Ok(SortField::CreatedAt)
109        }
110        Ok(proto::SortField::UpdatedAt) => Ok(SortField::UpdatedAt),
111        Ok(proto::SortField::Name) => Ok(SortField::Name),
112        Ok(proto::SortField::Version) => Ok(SortField::Version),
113        Ok(proto::SortField::SizeBytes) => Ok(SortField::SizeBytes),
114        Err(_) => Err(ApiError::bad_request("Invalid sort field")),
115    }
116}
117
118impl From<SortOrder> for proto::SortOrder {
119    fn from(order: SortOrder) -> Self {
120        match order {
121            SortOrder::Ascending => proto::SortOrder::Ascending,
122            SortOrder::Descending => proto::SortOrder::Descending,
123        }
124    }
125}
126
127/// Convert i32 to SortOrder (helper function to avoid orphan rule violations)
128pub fn sort_order_from_i32(value: i32) -> Result<SortOrder, ApiError> {
129    match proto::SortOrder::try_from(value) {
130        Ok(proto::SortOrder::Unspecified) | Ok(proto::SortOrder::Descending) => {
131            Ok(SortOrder::Descending)
132        }
133        Ok(proto::SortOrder::Ascending) => Ok(SortOrder::Ascending),
134        Err(_) => Err(ApiError::bad_request("Invalid sort order")),
135    }
136}
137
138// ============================================================================
139// Complex Type Conversions
140// ============================================================================
141
142/// Convert domain Asset to proto Asset
143impl From<Asset> for proto::Asset {
144    fn from(asset: Asset) -> Self {
145        proto::Asset {
146            id: asset.id.to_string(),
147            asset_type: proto::AssetType::from(asset.asset_type) as i32,
148            metadata: Some(proto::AssetMetadata::from(asset.metadata)),
149            storage: Some(proto::StorageLocation::from(asset.storage)),
150            checksum: Some(proto::Checksum::from(asset.checksum)),
151            status: proto::AssetStatus::from(asset.status) as i32,
152            provenance: asset.provenance.map(proto::Provenance::from),
153            dependencies: asset
154                .dependencies
155                .into_iter()
156                .map(proto::AssetReference::from)
157                .collect(),
158            created_at: asset.created_at.to_rfc3339(),
159            updated_at: asset.updated_at.to_rfc3339(),
160            deprecated_at: asset.deprecated_at.map(|dt| dt.to_rfc3339()),
161        }
162    }
163}
164
165/// Convert domain AssetMetadata to proto
166impl From<AssetMetadata> for proto::AssetMetadata {
167    fn from(meta: AssetMetadata) -> Self {
168        proto::AssetMetadata {
169            name: meta.name,
170            version: meta.version.to_string(),
171            description: meta.description,
172            license: meta.license,
173            tags: meta.tags,
174            annotations: meta.annotations,
175            size_bytes: meta.size_bytes,
176            content_type: meta.content_type,
177        }
178    }
179}
180
181/// Convert domain StorageLocation to proto
182impl From<StorageLocation> for proto::StorageLocation {
183    fn from(storage: StorageLocation) -> Self {
184        let (backend_type, config) = match storage.backend {
185            StorageBackend::S3 { bucket, region, endpoint } => (
186                proto::StorageBackend::S3 as i32,
187                Some(proto::storage_config::Config::S3(proto::S3Config {
188                    bucket,
189                    region,
190                    endpoint,
191                })),
192            ),
193            StorageBackend::GCS { bucket, project_id } => (
194                proto::StorageBackend::Gcs as i32,
195                Some(proto::storage_config::Config::Gcs(proto::GcsConfig {
196                    bucket,
197                    project_id,
198                })),
199            ),
200            StorageBackend::AzureBlob { account_name, container } => (
201                proto::StorageBackend::AzureBlob as i32,
202                Some(proto::storage_config::Config::Azure(proto::AzureBlobConfig {
203                    account_name,
204                    container,
205                })),
206            ),
207            StorageBackend::MinIO { bucket, endpoint } => (
208                proto::StorageBackend::Minio as i32,
209                Some(proto::storage_config::Config::Minio(proto::MinIoConfig {
210                    bucket,
211                    endpoint,
212                })),
213            ),
214            StorageBackend::FileSystem { base_path } => (
215                proto::StorageBackend::Filesystem as i32,
216                Some(proto::storage_config::Config::Filesystem(
217                    proto::FileSystemConfig { base_path },
218                )),
219            ),
220        };
221
222        proto::StorageLocation {
223            backend: backend_type,
224            path: storage.path,
225            uri: storage.uri,
226            config: config.map(|c| proto::StorageConfig { config: Some(c) }),
227        }
228    }
229}
230
231/// Convert proto StorageLocation to domain
232impl TryFrom<proto::StorageLocation> for StorageLocation {
233    type Error = ApiError;
234
235    fn try_from(proto: proto::StorageLocation) -> Result<Self, Self::Error> {
236        let backend = if let Some(config) = proto.config.and_then(|c| c.config) {
237            match config {
238                proto::storage_config::Config::S3(s3) => StorageBackend::S3 {
239                    bucket: s3.bucket,
240                    region: s3.region,
241                    endpoint: s3.endpoint,
242                },
243                proto::storage_config::Config::Gcs(gcs) => StorageBackend::GCS {
244                    bucket: gcs.bucket,
245                    project_id: gcs.project_id,
246                },
247                proto::storage_config::Config::Azure(azure) => StorageBackend::AzureBlob {
248                    account_name: azure.account_name,
249                    container: azure.container,
250                },
251                proto::storage_config::Config::Minio(minio) => StorageBackend::MinIO {
252                    bucket: minio.bucket,
253                    endpoint: minio.endpoint,
254                },
255                proto::storage_config::Config::Filesystem(fs) => StorageBackend::FileSystem {
256                    base_path: fs.base_path,
257                },
258            }
259        } else {
260            // Default to FileSystem if no config provided
261            StorageBackend::FileSystem {
262                base_path: "/var/lib/llm-registry".to_string(),
263            }
264        };
265
266        Ok(StorageLocation {
267            backend,
268            path: proto.path,
269            uri: proto.uri,
270        })
271    }
272}
273
274/// Convert domain Checksum to proto
275impl From<Checksum> for proto::Checksum {
276    fn from(checksum: Checksum) -> Self {
277        proto::Checksum {
278            algorithm: proto::HashAlgorithm::from(checksum.algorithm) as i32,
279            value: checksum.value,
280        }
281    }
282}
283
284/// Convert proto Checksum to domain
285impl TryFrom<proto::Checksum> for Checksum {
286    type Error = ApiError;
287
288    fn try_from(proto: proto::Checksum) -> Result<Self, Self::Error> {
289        Ok(Checksum {
290            algorithm: hash_algorithm_from_i32(proto.algorithm)?,
291            value: proto.value,
292        })
293    }
294}
295
296/// Convert domain Provenance to proto
297impl From<Provenance> for proto::Provenance {
298    fn from(prov: Provenance) -> Self {
299        proto::Provenance {
300            source: prov.source_repo,
301            author: prov.author,
302            created: Some(prov.created_at.to_rfc3339()),
303            metadata: prov.build_metadata,
304        }
305    }
306}
307
308/// Convert proto Provenance to domain
309impl TryFrom<proto::Provenance> for Provenance {
310    type Error = ApiError;
311
312    fn try_from(proto: proto::Provenance) -> Result<Self, Self::Error> {
313        use chrono::DateTime;
314
315        let created_at = if let Some(created_str) = proto.created {
316            DateTime::parse_from_rfc3339(&created_str)
317                .map_err(|e| ApiError::bad_request(format!("Invalid timestamp: {}", e)))?
318                .with_timezone(&chrono::Utc)
319        } else {
320            chrono::Utc::now()
321        };
322
323        Ok(Provenance {
324            source_repo: proto.source,
325            commit_hash: None,
326            build_id: None,
327            author: proto.author,
328            created_at,
329            build_metadata: proto.metadata,
330        })
331    }
332}
333
334/// Convert domain AssetReference to proto
335impl From<AssetReference> for proto::AssetReference {
336    fn from(ref_: AssetReference) -> Self {
337        let reference = match ref_ {
338            AssetReference::ById { id } => proto::asset_reference::Reference::Id(id.to_string()),
339            AssetReference::ByNameVersion { name, version } => {
340                proto::asset_reference::Reference::NameVersion(proto::NameVersion {
341                    name,
342                    version: version.to_string(),
343                })
344            }
345        };
346
347        proto::AssetReference {
348            reference: Some(reference),
349        }
350    }
351}
352
353/// Convert proto AssetReference to domain
354impl TryFrom<proto::AssetReference> for AssetReference {
355    type Error = ApiError;
356
357    fn try_from(proto: proto::AssetReference) -> Result<Self, Self::Error> {
358        match proto.reference {
359            Some(proto::asset_reference::Reference::Id(id)) => {
360                let asset_id = id
361                    .parse::<AssetId>()
362                    .map_err(|e| ApiError::bad_request(format!("Invalid asset ID: {}", e)))?;
363                Ok(AssetReference::ById { id: asset_id })
364            }
365            Some(proto::asset_reference::Reference::NameVersion(nv)) => {
366                // Validate version format
367                Version::parse(&nv.version)
368                    .map_err(|e| ApiError::bad_request(format!("Invalid version: {}", e)))?;
369                Ok(AssetReference::ByNameVersion {
370                    name: nv.name,
371                    version: nv.version,
372                })
373            }
374            None => Err(ApiError::bad_request("Asset reference must be specified")),
375        }
376    }
377}
378
379/// Convert domain DependencyNode to proto
380impl From<DependencyNode> for proto::DependencyNode {
381    fn from(node: DependencyNode) -> Self {
382        proto::DependencyNode {
383            asset_id: node.asset_id.to_string(),
384            name: node.name,
385            version: node.version.to_string(),
386            depth: node.depth,
387            dependency_count: node.dependencies.len() as u32,
388        }
389    }
390}
391
392// ============================================================================
393// Helper Functions
394// ============================================================================
395
396/// Convert RFC3339 string to chrono DateTime
397pub fn parse_timestamp(s: &str) -> Result<chrono::DateTime<chrono::Utc>, ApiError> {
398    chrono::DateTime::parse_from_rfc3339(s)
399        .map(|dt| dt.with_timezone(&chrono::Utc))
400        .map_err(|e| ApiError::bad_request(format!("Invalid timestamp: {}", e)))
401}
402
403/// Convert Version string to semver::Version
404pub fn parse_version(s: &str) -> Result<Version, ApiError> {
405    Version::parse(s).map_err(|e| ApiError::bad_request(format!("Invalid version: {}", e)))
406}