Skip to content

Commit 5df134d

Browse files
committed
feat: add human and machine readable error output in json
1 parent 9d723b7 commit 5df134d

File tree

15 files changed

+644
-58
lines changed

15 files changed

+644
-58
lines changed

Cargo.lock

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ readme.workspace = true
3232

3333
[features]
3434
diagnostics = ["tabwriter", "human_bytes"]
35+
json = ["serde", "serde_json", "petgraph/serde-1"]
3536

3637
[dependencies]
3738
ahash = "0.8.12"
@@ -41,6 +42,7 @@ tracing = "0.1.41"
4142
elsa = "1.11.2"
4243
bitvec = "1.0.1"
4344
serde = { version = "1.0", features = ["derive"], optional = true }
45+
serde_json = { version = "1.0.142", optional = true }
4446
futures = { version = "0.3", default-features = false, features = ["alloc", "async-await"] }
4547
event-listener = "5.4"
4648
indexmap = "2"

cpp/CMakeLists.txt

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -125,17 +125,21 @@ install(
125125

126126
if(BUILD_SHARED_LIBS)
127127
if(WIN32)
128-
install(FILES $<TARGET_FILE:${resolvo_cpp_impl}> DESTINATION ${CMAKE_INSTALL_BINDIR})
128+
install(FILES $<TARGET_FILE:${resolvo_cpp_impl}>
129+
DESTINATION ${CMAKE_INSTALL_BINDIR})
129130
else()
130-
install(FILES $<TARGET_FILE:${resolvo_cpp_impl}> DESTINATION ${CMAKE_INSTALL_LIBDIR})
131+
install(FILES $<TARGET_FILE:${resolvo_cpp_impl}>
132+
DESTINATION ${CMAKE_INSTALL_LIBDIR})
131133
endif()
132134
if(WIN32)
133-
install(FILES $<TARGET_LINKER_FILE:${resolvo_cpp_impl}>
134-
DESTINATION ${CMAKE_INSTALL_LIBDIR}
135-
RENAME resolvo_cpp.lib)
135+
install(
136+
FILES $<TARGET_LINKER_FILE:${resolvo_cpp_impl}>
137+
DESTINATION ${CMAKE_INSTALL_LIBDIR}
138+
RENAME resolvo_cpp.lib)
136139
endif()
137140
else()
138-
install(FILES $<TARGET_FILE:${resolvo_cpp_impl}> DESTINATION ${CMAKE_INSTALL_LIBDIR})
141+
install(FILES $<TARGET_FILE:${resolvo_cpp_impl}>
142+
DESTINATION ${CMAKE_INSTALL_LIBDIR})
139143
endif()
140144

141145
include(CMakePackageConfigHelpers)

cpp/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -674,7 +674,7 @@ pub extern "C" fn resolvo_solve(
674674
*error = problem.display_user_friendly(&solver).to_string().into();
675675
false
676676
}
677-
Err(resolvo::UnsolvableOrCancelled::Cancelled(cancelled)) => {
677+
Err(resolvo::UnsolvableOrCancelled::Cancelled { reason: _ }) => {
678678
*error = String::from("cancelled");
679679
false
680680
}

examples/json_errors.rs

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
//! Demonstrates machine-readable error format support in resolvo
2+
//!
3+
//! This example shows how to:
4+
//! 1. Handle dependency resolution errors programmatically
5+
//! 2. Serialize errors to JSON for integration with external tools
6+
//! 3. Use different error reporting formats
7+
//!
8+
//! Run with: cargo run --example json_errors --features json
9+
//! This will create error_examples.json with the exported error data
10+
11+
use std::{collections::HashMap, fs};
12+
13+
#[cfg(feature = "json")]
14+
use resolvo::{
15+
Candidates, Dependencies, DependencyProvider, HintDependenciesAvailable, Interner,
16+
KnownDependencies, NameId, Problem, Requirement, SolvableId, Solver, StringId,
17+
UnsolvableOrCancelled, VersionSetId,
18+
};
19+
20+
#[cfg(feature = "json")]
21+
#[derive(Debug)]
22+
struct ExampleProvider;
23+
24+
#[cfg(feature = "json")]
25+
impl ExampleProvider {
26+
fn new() -> Self {
27+
Self
28+
}
29+
}
30+
31+
#[cfg(feature = "json")]
32+
impl Interner for ExampleProvider {
33+
fn display_solvable(&self, _solvable: SolvableId) -> impl std::fmt::Display + '_ {
34+
35+
}
36+
37+
fn display_name(&self, _name: NameId) -> impl std::fmt::Display + '_ {
38+
"example-package"
39+
}
40+
41+
fn display_version_set(&self, _version_set: VersionSetId) -> impl std::fmt::Display + '_ {
42+
">=1.0.0"
43+
}
44+
45+
fn display_string(&self, _string_id: StringId) -> impl std::fmt::Display + '_ {
46+
"example string"
47+
}
48+
49+
fn version_set_name(&self, _version_set: VersionSetId) -> NameId {
50+
NameId(0)
51+
}
52+
53+
fn solvable_name(&self, _solvable: SolvableId) -> NameId {
54+
NameId(0)
55+
}
56+
57+
fn version_sets_in_union(
58+
&self,
59+
_version_set_union: resolvo::VersionSetUnionId,
60+
) -> impl Iterator<Item = VersionSetId> {
61+
std::iter::empty()
62+
}
63+
64+
fn resolve_condition(&self, _condition: resolvo::ConditionId) -> resolvo::Condition {
65+
resolvo::Condition::Requirement(VersionSetId(0))
66+
}
67+
}
68+
69+
#[cfg(feature = "json")]
70+
impl DependencyProvider for ExampleProvider {
71+
async fn filter_candidates(
72+
&self,
73+
_candidates: &[SolvableId],
74+
_version_set: VersionSetId,
75+
_inverse: bool,
76+
) -> Vec<SolvableId> {
77+
vec![]
78+
}
79+
80+
async fn get_candidates(&self, _name: NameId) -> Option<Candidates> {
81+
Some(Candidates {
82+
candidates: vec![],
83+
favored: None,
84+
locked: None,
85+
hint_dependencies_available: HintDependenciesAvailable::None,
86+
excluded: vec![],
87+
})
88+
}
89+
90+
async fn sort_candidates(
91+
&self,
92+
_solver: &resolvo::SolverCache<Self>,
93+
_solvables: &mut [SolvableId],
94+
) {
95+
}
96+
97+
async fn get_dependencies(&self, _solvable: SolvableId) -> Dependencies {
98+
Dependencies::Known(KnownDependencies::default())
99+
}
100+
}
101+
102+
#[cfg(feature = "json")]
103+
fn create_missing_dependency_error() -> (UnsolvableOrCancelled, Solver<ExampleProvider>) {
104+
let provider = ExampleProvider::new();
105+
let mut solver = Solver::new(provider);
106+
107+
let problem = Problem::new().requirements(vec![Requirement::Single(VersionSetId(1)).into()]);
108+
109+
match solver.solve(problem) {
110+
Ok(_) => panic!("Expected error for missing dependency"),
111+
Err(error) => (error, solver),
112+
}
113+
}
114+
115+
fn demonstrate_json_error_serialization() -> serde_json::Value {
116+
#[cfg(feature = "json")]
117+
let mut examples: HashMap<&str, serde_json::Value> = HashMap::new();
118+
#[cfg(not(feature = "json"))]
119+
let examples: HashMap<&str, serde_json::Value> = HashMap::new();
120+
121+
#[cfg(feature = "json")]
122+
let (error, solver) = create_missing_dependency_error();
123+
124+
#[cfg(feature = "json")]
125+
{
126+
if let Ok(json) = error.to_json() {
127+
examples.insert(
128+
"unsolvable_compact",
129+
serde_json::from_str::<serde_json::Value>(&json).unwrap(),
130+
);
131+
132+
if let Ok(deserialized) = UnsolvableOrCancelled::from_json(&json) {
133+
if !verify_lossless_conversion(&error, &deserialized) {
134+
eprintln!("Warning: Lossless conversion failed for unsolvable error");
135+
}
136+
}
137+
}
138+
139+
if let UnsolvableOrCancelled::Unsolvable(conflict) = &error {
140+
if let Ok(graph_json) = conflict.to_graph_json(&solver) {
141+
examples.insert(
142+
"unsolvable_graph",
143+
serde_json::from_str::<serde_json::Value>(&graph_json).unwrap(),
144+
);
145+
}
146+
}
147+
148+
let cancelled_scenarios = vec![
149+
(
150+
"User requested cancellation during package resolution".to_string(),
151+
"cancelled_error",
152+
),
153+
(
154+
"Operation timed out after 30 seconds".to_string(),
155+
"timeout_error",
156+
),
157+
("Network connection lost".to_string(), "network_error"),
158+
(
159+
"Insufficient memory to continue".to_string(),
160+
"memory_error",
161+
),
162+
];
163+
164+
for (reason, key) in cancelled_scenarios {
165+
let cancelled_error = UnsolvableOrCancelled::Cancelled { reason };
166+
167+
if let Ok(cancelled_json) = cancelled_error.to_json_pretty() {
168+
if let Ok(deserialized_cancelled) =
169+
UnsolvableOrCancelled::from_json(&cancelled_json)
170+
{
171+
if verify_lossless_conversion(&cancelled_error, &deserialized_cancelled) {
172+
examples.insert(
173+
key,
174+
serde_json::from_str::<serde_json::Value>(&cancelled_json).unwrap(),
175+
);
176+
} else {
177+
eprintln!("Warning: Lossless conversion failed for {}", key);
178+
}
179+
}
180+
}
181+
}
182+
}
183+
184+
#[cfg(not(feature = "json"))]
185+
{
186+
eprintln!("JSON feature not enabled. Run with: --features json");
187+
}
188+
189+
serde_json::to_value(examples).unwrap()
190+
}
191+
192+
/// Verifies that two UnsolvableOrCancelled errors are functionally identical
193+
#[cfg(feature = "json")]
194+
fn verify_lossless_conversion(
195+
original: &UnsolvableOrCancelled,
196+
deserialized: &UnsolvableOrCancelled,
197+
) -> bool {
198+
match (original, deserialized) {
199+
(
200+
UnsolvableOrCancelled::Unsolvable(orig_conflict),
201+
UnsolvableOrCancelled::Unsolvable(deser_conflict),
202+
) => orig_conflict.clause_count() == deser_conflict.clause_count(),
203+
(
204+
UnsolvableOrCancelled::Cancelled {
205+
reason: orig_reason,
206+
},
207+
UnsolvableOrCancelled::Cancelled {
208+
reason: deser_reason,
209+
},
210+
) => orig_reason == deser_reason,
211+
_ => false,
212+
}
213+
}
214+
215+
fn main() {
216+
let json_examples = demonstrate_json_error_serialization();
217+
218+
let json_output = serde_json::json!({
219+
"title": "Resolvo Machine-Readable Error Examples",
220+
"description": "Examples of JSON-serialized error formats from resolvo dependency solver",
221+
"examples": json_examples
222+
});
223+
224+
let json_string = serde_json::to_string_pretty(&json_output).unwrap();
225+
fs::write("examples/jsonoutput/error_examples.json", &json_string)
226+
.expect("Failed to write JSON file");
227+
228+
println!("Examples exported to examples/jsonoutput/error_examples.json");
229+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
{
2+
"description": "Examples of JSON-serialized error formats from resolvo dependency solver",
3+
"examples": {
4+
"cancelled_error": {
5+
"Cancelled": {
6+
"reason": "User requested cancellation during package resolution"
7+
}
8+
},
9+
"memory_error": {
10+
"Cancelled": {
11+
"reason": "Insufficient memory to continue"
12+
}
13+
},
14+
"network_error": {
15+
"Cancelled": {
16+
"reason": "Network connection lost"
17+
}
18+
},
19+
"timeout_error": {
20+
"Cancelled": {
21+
"reason": "Operation timed out after 30 seconds"
22+
}
23+
},
24+
"unsolvable_compact": {
25+
"Unsolvable": {
26+
"clauses": [
27+
2
28+
]
29+
}
30+
},
31+
"unsolvable_graph": {
32+
"graph": {
33+
"edge_property": "directed",
34+
"edges": [
35+
[
36+
0,
37+
1,
38+
{
39+
"Requires": {
40+
"single": 1
41+
},
42+
"display": "requires: example-package >=1.0.0"
43+
}
44+
]
45+
],
46+
"node_holes": [],
47+
"nodes": [
48+
{
49+
"Solvable": 0,
50+
"display": "<root>"
51+
},
52+
{
53+
"UnresolvedDependency": null,
54+
"display": "unresolved dependency"
55+
}
56+
]
57+
},
58+
"root_node": 0,
59+
"unresolved_node": 1
60+
}
61+
},
62+
"title": "Resolvo Machine-Readable Error Examples"
63+
}

0 commit comments

Comments
 (0)