1use 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
15impl 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, }
29 }
30}
31
32pub 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
58pub 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
80pub 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
104pub 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
127pub 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
138impl 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
165impl 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
181impl 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
231impl 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 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
274impl 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
284impl 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
296impl 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
308impl 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
334impl 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
353impl 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 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
379impl 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
392pub 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
403pub fn parse_version(s: &str) -> Result<Version, ApiError> {
405 Version::parse(s).map_err(|e| ApiError::bad_request(format!("Invalid version: {}", e)))
406}