Coverage Report

Created: 2026-01-25 15:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/noah/src/realizar/src/uri.rs
Line
Count
Source
1
//! Pacha URI scheme support for model loading
2
//!
3
//! Enables direct model loading from the Pacha registry using URIs like:
4
//! - `pacha://model-name:version`
5
//! - `pacha://model-name` (latest version)
6
//!
7
//! ## Example
8
//!
9
//! ```rust,ignore
10
//! use realizar::uri::{PachaUri, resolve_model_uri};
11
//!
12
//! let uri = PachaUri::parse("pacha://llama-7b:1.0.0")?;
13
//! let model_path = resolve_model_uri(&uri).await?;
14
//! ```
15
//!
16
//! ## Integration
17
//!
18
//! The Pacha URI scheme integrates with:
19
//! - Model registry for automatic metadata retrieval
20
//! - Lineage tracking for inference metrics
21
//! - Content addressing for integrity verification
22
23
#[cfg(feature = "registry")]
24
use std::path::PathBuf;
25
26
use crate::error::{RealizarError, Result};
27
28
/// Parsed Pacha URI
29
///
30
/// Represents a `pacha://model:version` URI for model loading.
31
#[derive(Debug, Clone, PartialEq, Eq)]
32
pub struct PachaUri {
33
    /// Model name/identifier
34
    pub model: String,
35
    /// Version (None means latest)
36
    pub version: Option<String>,
37
}
38
39
impl PachaUri {
40
    /// Parse a Pacha URI string
41
    ///
42
    /// Supported formats:
43
    /// - `pacha://model-name:version`
44
    /// - `pacha://model-name` (latest version)
45
    ///
46
    /// # Errors
47
    ///
48
    /// Returns error if URI is malformed or uses unsupported scheme
49
21
    pub fn parse(uri: &str) -> Result<Self> {
50
        // Check for pacha:// scheme
51
21
        let 
path20
= uri
52
21
            .strip_prefix("pacha://")
53
21
            .ok_or_else(|| RealizarError::InvalidUri(
format!1
(
"Expected pacha:// scheme: {uri}"1
)))
?1
;
54
55
20
        if path.is_empty() {
56
2
            return Err(RealizarError::InvalidUri(
57
2
                "Model name required after pacha://".to_string(),
58
2
            ));
59
18
        }
60
61
        // Split model:version
62
18
        let (
model16
,
version16
) = if let Some(
colon_pos13
) = path.rfind(':') {
63
13
            let model = &path[..colon_pos];
64
13
            let version = &path[colon_pos + 1..];
65
66
13
            if model.is_empty() {
67
1
                return Err(RealizarError::InvalidUri(
68
1
                    "Model name cannot be empty".to_string(),
69
1
                ));
70
12
            }
71
12
            if version.is_empty() {
72
1
                return Err(RealizarError::InvalidUri(
73
1
                    "Version cannot be empty after colon".to_string(),
74
1
                ));
75
11
            }
76
77
11
            (model.to_string(), Some(version.to_string()))
78
        } else {
79
5
            (path.to_string(), None)
80
        };
81
82
16
        Ok(Self { model, version })
83
21
    }
84
85
    /// Check if this is a Pacha URI
86
    #[must_use]
87
4
    pub fn is_pacha_uri(uri: &str) -> bool {
88
4
        uri.starts_with("pacha://")
89
4
    }
90
91
    /// Convert back to URI string
92
    #[must_use]
93
8
    pub fn to_uri_string(&self) -> String {
94
8
        match &self.version {
95
5
            Some(v) => format!("pacha://{}:{v}", self.model),
96
3
            None => format!("pacha://{}", self.model),
97
        }
98
8
    }
99
100
    /// Get the version or "latest" if none specified
101
    #[must_use]
102
2
    pub fn version_or_latest(&self) -> &str {
103
2
        self.version.as_deref().unwrap_or("latest")
104
2
    }
105
}
106
107
impl std::fmt::Display for PachaUri {
108
1
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109
1
        write!(f, "{}", self.to_uri_string())
110
1
    }
111
}
112
113
/// Model metadata retrieved from Pacha registry
114
#[derive(Debug, Clone)]
115
#[cfg(feature = "registry")]
116
pub struct ModelMetadata {
117
    /// Model name
118
    pub name: String,
119
    /// Model version
120
    pub version: String,
121
    /// Content hash (BLAKE3)
122
    pub content_hash: String,
123
    /// Signer public key (Ed25519) if signed
124
    pub signer_key: Option<String>,
125
    /// Local cached path
126
    pub local_path: PathBuf,
127
}
128
129
/// Resolver for Pacha URIs
130
///
131
/// Handles model resolution from the Pacha registry.
132
#[cfg(feature = "registry")]
133
pub struct PachaResolver {
134
    registry: pacha::Registry,
135
}
136
137
#[cfg(feature = "registry")]
138
impl PachaResolver {
139
    /// Create a new resolver with default registry
140
    ///
141
    /// # Errors
142
    ///
143
    /// Returns error if registry cannot be opened
144
    pub fn new() -> Result<Self> {
145
        let registry = pacha::Registry::open_default()
146
            .map_err(|e| RealizarError::RegistryError(format!("Failed to open registry: {e}")))?;
147
        Ok(Self { registry })
148
    }
149
150
    /// Create a resolver with a specific registry
151
    #[must_use]
152
    pub fn with_registry(registry: pacha::Registry) -> Self {
153
        Self { registry }
154
    }
155
156
    /// Resolve a Pacha URI to model metadata and artifact data
157
    ///
158
    /// This function:
159
    /// 1. Parses the Pacha URI
160
    /// 2. Retrieves model from registry
161
    /// 3. Returns the metadata and artifact data
162
    ///
163
    /// # Errors
164
    ///
165
    /// Returns error if:
166
    /// - Model not found in registry
167
    /// - Retrieval fails
168
    pub fn resolve(&self, uri: &PachaUri) -> Result<(ModelMetadata, Vec<u8>)> {
169
        use pacha::model::ModelVersion;
170
171
        // Parse version
172
        let version = match &uri.version {
173
            Some(v) => ModelVersion::parse(v)
174
                .map_err(|e| RealizarError::RegistryError(format!("Invalid version: {e}")))?,
175
            None => ModelVersion::new(0, 0, 0), // Will get latest
176
        };
177
178
        // Get model metadata from registry
179
        let model = self
180
            .registry
181
            .get_model(&uri.model, &version)
182
            .map_err(|e| RealizarError::ModelNotFound(format!("{}: {e}", uri.model)))?;
183
184
        // Get the artifact data
185
        let artifact = self
186
            .registry
187
            .get_model_artifact(&uri.model, &version)
188
            .map_err(|e| {
189
                RealizarError::RegistryError(format!("Failed to get model artifact: {e}"))
190
            })?;
191
192
        let metadata = ModelMetadata {
193
            name: uri.model.clone(),
194
            version: model.version.to_string(),
195
            content_hash: model.content_address.to_string(),
196
            signer_key: None, // Signing info retrieved separately via pacha::signing
197
            local_path: PathBuf::new(), // Artifact is returned directly, no file path
198
        };
199
200
        Ok((metadata, artifact))
201
    }
202
203
    /// Get model metadata without loading artifact
204
    ///
205
    /// # Errors
206
    ///
207
    /// Returns error if model not found
208
    pub fn get_metadata(&self, uri: &PachaUri) -> Result<ModelMetadata> {
209
        use pacha::model::ModelVersion;
210
211
        let version = match &uri.version {
212
            Some(v) => ModelVersion::parse(v)
213
                .map_err(|e| RealizarError::RegistryError(format!("Invalid version: {e}")))?,
214
            None => ModelVersion::new(0, 0, 0),
215
        };
216
217
        let model = self
218
            .registry
219
            .get_model(&uri.model, &version)
220
            .map_err(|e| RealizarError::ModelNotFound(format!("{}: {e}", uri.model)))?;
221
222
        Ok(ModelMetadata {
223
            name: uri.model.clone(),
224
            version: model.version.to_string(),
225
            content_hash: model.content_address.to_string(),
226
            signer_key: None, // Signing info retrieved separately via pacha::signing
227
            local_path: PathBuf::new(),
228
        })
229
    }
230
}
231
232
/// Lineage information for model tracing
233
///
234
/// Propagated to inference metrics for observability.
235
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
236
pub struct ModelLineage {
237
    /// Original Pacha URI
238
    pub uri: String,
239
    /// Model name
240
    pub model: String,
241
    /// Model version
242
    pub version: String,
243
    /// Content hash (BLAKE3) for integrity
244
    pub content_hash: String,
245
    /// Whether the model was verified (signature check)
246
    pub verified: bool,
247
    /// Timestamp when lineage was captured
248
    pub captured_at: u64,
249
}
250
251
impl ModelLineage {
252
    /// Create lineage from Pacha URI and metadata
253
    #[cfg(feature = "registry")]
254
    #[must_use]
255
    pub fn from_metadata(uri: &PachaUri, metadata: &ModelMetadata) -> Self {
256
        Self {
257
            uri: uri.to_uri_string(),
258
            model: metadata.name.clone(),
259
            version: metadata.version.clone(),
260
            content_hash: metadata.content_hash.clone(),
261
            verified: metadata.signer_key.is_some(),
262
            captured_at: std::time::SystemTime::now()
263
                .duration_since(std::time::UNIX_EPOCH)
264
                .map(|d| d.as_secs())
265
                .unwrap_or(0),
266
        }
267
    }
268
269
    /// Create lineage without registry metadata
270
    #[must_use]
271
5
    pub fn from_uri(uri: &PachaUri) -> Self {
272
        Self {
273
5
            uri: uri.to_uri_string(),
274
5
            model: uri.model.clone(),
275
5
            version: uri.version.clone().unwrap_or_else(|| 
"unknown"2
.
to_string2
()),
276
5
            content_hash: String::new(),
277
            verified: false,
278
5
            captured_at: std::time::SystemTime::now()
279
5
                .duration_since(std::time::UNIX_EPOCH)
280
5
                .map(|d| d.as_secs())
281
5
                .unwrap_or(0),
282
        }
283
5
    }
284
}
285
286
#[cfg(test)]
287
mod tests {
288
    use super::*;
289
290
    #[test]
291
1
    fn test_parse_full_uri() {
292
1
        let uri = PachaUri::parse("pacha://llama-7b:1.0.0").expect("test");
293
1
        assert_eq!(uri.model, "llama-7b");
294
1
        assert_eq!(uri.version, Some("1.0.0".to_string()));
295
1
    }
296
297
    #[test]
298
1
    fn test_parse_uri_without_version() {
299
1
        let uri = PachaUri::parse("pacha://mistral-7b").expect("test");
300
1
        assert_eq!(uri.model, "mistral-7b");
301
1
        assert_eq!(uri.version, None);
302
1
    }
303
304
    #[test]
305
1
    fn test_parse_uri_with_semver() {
306
1
        let uri = PachaUri::parse("pacha://gpt2:2.0.0-beta.1").expect("test");
307
1
        assert_eq!(uri.model, "gpt2");
308
1
        assert_eq!(uri.version, Some("2.0.0-beta.1".to_string()));
309
1
    }
310
311
    #[test]
312
1
    fn test_parse_uri_with_org() {
313
1
        let uri = PachaUri::parse("pacha://paiml/trueno-llm:1.0").expect("test");
314
1
        assert_eq!(uri.model, "paiml/trueno-llm");
315
1
        assert_eq!(uri.version, Some("1.0".to_string()));
316
1
    }
317
318
    #[test]
319
1
    fn test_invalid_scheme() {
320
1
        let result = PachaUri::parse("http://example.com/model");
321
1
        assert!(result.is_err());
322
1
    }
323
324
    #[test]
325
1
    fn test_empty_model() {
326
1
        let result = PachaUri::parse("pacha://");
327
1
        assert!(result.is_err());
328
1
    }
329
330
    #[test]
331
1
    fn test_empty_version() {
332
1
        let result = PachaUri::parse("pacha://model:");
333
1
        assert!(result.is_err());
334
1
    }
335
336
    #[test]
337
1
    fn test_is_pacha_uri() {
338
1
        assert!(PachaUri::is_pacha_uri("pacha://model:1.0"));
339
1
        assert!(PachaUri::is_pacha_uri("pacha://model"));
340
1
        assert!(!PachaUri::is_pacha_uri("http://example.com"));
341
1
        assert!(!PachaUri::is_pacha_uri("/path/to/model.gguf"));
342
1
    }
343
344
    #[test]
345
1
    fn test_to_uri_string() {
346
1
        let uri = PachaUri::parse("pacha://model:1.0").expect("test");
347
1
        assert_eq!(uri.to_uri_string(), "pacha://model:1.0");
348
349
1
        let uri_no_version = PachaUri::parse("pacha://model").expect("test");
350
1
        assert_eq!(uri_no_version.to_uri_string(), "pacha://model");
351
1
    }
352
353
    #[test]
354
1
    fn test_version_or_latest() {
355
1
        let uri = PachaUri::parse("pacha://model:2.0").expect("test");
356
1
        assert_eq!(uri.version_or_latest(), "2.0");
357
358
1
        let uri_no_version = PachaUri::parse("pacha://model").expect("test");
359
1
        assert_eq!(uri_no_version.version_or_latest(), "latest");
360
1
    }
361
362
    #[test]
363
1
    fn test_display() {
364
1
        let uri = PachaUri::parse("pacha://llama:1.0.0").expect("test");
365
1
        assert_eq!(format!("{uri}"), "pacha://llama:1.0.0");
366
1
    }
367
368
    #[test]
369
1
    fn test_lineage_from_uri() {
370
1
        let uri = PachaUri::parse("pacha://model:1.0").expect("test");
371
1
        let lineage = ModelLineage::from_uri(&uri);
372
373
1
        assert_eq!(lineage.uri, "pacha://model:1.0");
374
1
        assert_eq!(lineage.model, "model");
375
1
        assert_eq!(lineage.version, "1.0");
376
1
        assert!(!lineage.verified);
377
1
        assert!(lineage.captured_at > 0);
378
1
    }
379
380
    // =========================================================================
381
    // Coverage Tests
382
    // =========================================================================
383
384
    #[test]
385
1
    fn test_pacha_uri_clone_and_eq() {
386
1
        let uri1 = PachaUri::parse("pacha://model:1.0").expect("test");
387
1
        let uri2 = uri1.clone();
388
1
        assert_eq!(uri1, uri2);
389
1
    }
390
391
    #[test]
392
1
    fn test_lineage_from_uri_no_version() {
393
1
        let uri = PachaUri::parse("pacha://model-name").expect("test");
394
1
        let lineage = ModelLineage::from_uri(&uri);
395
1
        assert_eq!(lineage.version, "unknown");
396
1
    }
397
398
    #[test]
399
1
    fn test_parse_empty_model_error_msg() {
400
1
        let result = PachaUri::parse("pacha://");
401
1
        match result {
402
1
            Err(e) => assert!(e.to_string().contains("required")),
403
0
            Ok(_) => panic!("Expected error"),
404
        }
405
1
    }
406
407
    #[test]
408
1
    fn test_parse_empty_model_name_before_colon() {
409
1
        let result = PachaUri::parse("pacha://:1.0");
410
1
        match result {
411
1
            Err(e) => assert!(e.to_string().contains("empty")),
412
0
            Ok(_) => panic!("Expected error"),
413
        }
414
1
    }
415
416
    #[test]
417
1
    fn test_lineage_content_hash_empty() {
418
1
        let uri = PachaUri::parse("pacha://test").expect("test");
419
1
        let lineage = ModelLineage::from_uri(&uri);
420
1
        assert!(lineage.content_hash.is_empty());
421
1
    }
422
423
    #[test]
424
1
    fn test_model_lineage_serialize() {
425
1
        let uri = PachaUri::parse("pacha://model:1.0").expect("test");
426
1
        let lineage = ModelLineage::from_uri(&uri);
427
1
        let json = serde_json::to_string(&lineage).expect("test");
428
1
        assert!(json.contains("model"));
429
1
        assert!(json.contains("1.0"));
430
1
    }
431
432
    #[test]
433
1
    fn test_model_lineage_deserialize() {
434
1
        let json = r#"{"uri":"pacha://test:1.0","model":"test","version":"1.0","content_hash":"","verified":false,"captured_at":12345}"#;
435
1
        let lineage: ModelLineage = serde_json::from_str(json).expect("test");
436
1
        assert_eq!(lineage.model, "test");
437
1
        assert_eq!(lineage.version, "1.0");
438
1
    }
439
440
    #[test]
441
1
    fn test_pacha_uri_debug() {
442
1
        let uri = PachaUri::parse("pacha://model:1.0").expect("test");
443
1
        let debug = format!("{:?}", uri);
444
1
        assert!(debug.contains("PachaUri"));
445
1
        assert!(debug.contains("model"));
446
1
    }
447
448
    #[test]
449
1
    fn test_model_lineage_debug() {
450
1
        let uri = PachaUri::parse("pacha://model:1.0").expect("test");
451
1
        let lineage = ModelLineage::from_uri(&uri);
452
1
        let debug = format!("{:?}", lineage);
453
1
        assert!(debug.contains("ModelLineage"));
454
1
    }
455
}