refactor: deduplicate volume operations between runtimes#40
Conversation
…d.rs Deduplicate volume JSON parsing logic that was duplicated across native_podman.rs and orbstack_runtime.rs. Three new pub(crate) helpers: - parse_volume_labels: extracts label map from Podman volume JSON - parse_volume_list_json: parses volume ls JSON with prefix filtering - parse_volume_inspect_json: parses volume inspect JSON into VolumeInfo Removes ~75 lines of duplication. Adds 20 unit tests covering happy paths, edge cases (null/missing labels, empty input, invalid JSON), and non-string label filtering. Co-Authored-By: Claude <noreply@anthropic.com>
Extract volume_info_from_json helper to deduplicate VolumeInfo construction, replace imperative loops with iterator chains in collect_disk_usage and parse_volume_list_json, flatten nested conditionals in volume_disk_usage.
src/orchestration/mod.rs
Outdated
| } | ||
|
|
||
| let volumes: Vec<serde_json::Value> = | ||
| serde_json::from_str(stdout).map_err(|e| MinoError::Internal(e.to_string()))?; |
There was a problem hiding this comment.
JSON Error Type Regression
The new helpers parse_volume_list_json and parse_volume_inspect_json convert serde_json::Error via .map_err(|e| MinoError::Internal(e.to_string())). This discards the structured error source chain and loses the MinoError::Json variant that is already defined with #[from].
The codebase has:
// In src/error.rs
Json(#[from] serde_json::Error)This means you can use the ? operator to automatically convert the error. This is a MEDIUM-priority fix since centralizing JSON parsing is the right time to fix this across both call sites at once.
Suggested fix:
// Before (line 137):
let volumes: Vec<serde_json::Value> =
serde_json::from_str(stdout).map_err(|e| MinoError::Internal(e.to_string()))?;
// After:
let volumes: Vec<serde_json::Value> = serde_json::from_str(stdout)?;Apply the same change at line 175 in parse_volume_inspect_json.
This improves error diagnostics and makes error categorization more precise for callers that pattern-match on MinoError::Json.
src/orchestration/mod.rs
Outdated
| name: &str, | ||
| ) -> MinoResult<Option<VolumeInfo>> { | ||
| let volumes: Vec<serde_json::Value> = | ||
| serde_json::from_str(stdout).map_err(|e| MinoError::Internal(e.to_string()))?; |
There was a problem hiding this comment.
JSON Error Type at Second Call Site
Same issue as the comment at line 137: this line should use the MinoError::Json variant via the ? operator instead of .map_err(|e| MinoError::Internal(e.to_string())).
Suggested fix:
// Before:
let volumes: Vec<serde_json::Value> =
serde_json::from_str(stdout).map_err(|e| MinoError::Internal(e.to_string()))?;
// After:
let volumes: Vec<serde_json::Value> = serde_json::from_str(stdout)?;| /// when the array is empty. The `name` parameter is used as the canonical volume | ||
| /// name (preserving existing behavior where callers pass the requested name rather | ||
| /// than trusting the JSON `Name` field). | ||
| pub(crate) fn parse_volume_inspect_json( |
There was a problem hiding this comment.
Asymmetric Empty-Input Handling Between Sibling Functions
The parse_volume_list_json function has an explicit empty/whitespace guard (lines 131-133), but the sibling function parse_volume_inspect_json (starting at line 170) does not. Both functions parse the same kind of Podman JSON output.
If parse_volume_inspect_json receives empty/whitespace stdout, it will produce a MinoError::Internal from serde_json::from_str, while parse_volume_list_json gracefully returns an empty result. This internal inconsistency creates a subtle trap for future callers.
Suggested fix:
Add the same empty-input guard to parse_volume_inspect_json. This makes the two functions symmetric in their handling of edge cases and prevents silent errors from malformed Podman output.
src/orchestration/mod.rs
Outdated
| let result = volumes | ||
| .iter() | ||
| .filter_map(|vol| { | ||
| let name = vol["Name"].as_str().unwrap_or_default(); |
There was a problem hiding this comment.
Silent Data Loss: unwrap_or_default() Masks Missing Volume Names
The line uses unwrap_or_default() to convert missing or non-string Name field to empty string. This empty name then fails the prefix check and is silently filtered out, hiding potentially malformed volume entries.
While Podman always includes Name, this pattern creates a silent data loss path. If Podman ever returns a volume with a missing or non-string Name, it would disappear with no warning.
Suggested fix:
Use filter_map with explicit type checking to make intent clear. This makes the contract explicit: only volumes with valid string names are processed. Missing names are filtered rather than coerced to empty strings.
Code Review Summary - PR #40Blocking Issues (MEDIUM priority - fix before merge):
Should-Fix Issues (MEDIUM priority - consider for this PR):
Test Quality: 20 new unit tests added covering edge cases. 323 unit + 13 integration tests pass. Overall Assessment: Excellent deduplication refactor. Blocking issues are straightforward to fix in Code review via Claude Code | 2026-03-11 |
…me parsing - Use serde_json `?` operator with MinoError::Json(#[from]) instead of map_err to MinoError::Internal, preserving structured error type and source chain (SF-1) - Add empty-input guard to parse_volume_inspect_json for consistency with parse_volume_list_json (SF-2) - Use `as_str()?` in filter_map to make skip-on-missing-Name explicit instead of unwrap_or_default (NTH-1) - Add created_at assertion to inspect single-volume test (NTH-2) - Add whitespace-only input tests for both parse functions (NTH-3) Co-Authored-By: Claude <noreply@anthropic.com>
Replace match/early-return pattern with .first().map() combinator for cleaner optional transformation.
Summary
Closes #24.
pub(crate)volume JSON parsing helpers intosrc/orchestration/mod.rs:parse_volume_labels,parse_volume_list_json,parse_volume_inspect_json(plus privatevolume_info_from_json)volume_listandvolume_inspectin bothnative_podman.rsandorbstack_runtime.rsto delegate to shared helpers viasuper::collect_disk_usageandvolume_disk_usagewith idiomatic iterator patternsNo behavioral changes. No trait or API changes. All callers continue using
dyn ContainerRuntimeunchanged.Test plan
cargo test— 310 unit + 13 integration tests passcargo clippy— clean (2 pre-existing warnings in unmodified code)mino cache liston Linux/macOS